Remote Image Upload Leads to RCE (Inject Malicious Code to PHP-GD Image)

Muhammad R. Maulana
Mar 21 · 5 min read

بسم الله الرحمن الرحيم

On this web application, there are two ways to add an image to media library, first one is using local file upload and the second one is remote file upload from a Stock Photo website.

The first thing that came to my mind was that there might have SSRF vulnerability through the remote file upload feature. But I think it worth to test the local file upload first.

After several test on the local file upload, I can concluded that the app only allows image files (.jpg, .png, .gif and .svg), the .svg one could leads to XSS.

Then I moved on to remote file upload from a Stock Photo website feature, tried add an image and intercept the request. I found out there are 3 POST form-data that will be send to server: name, url and photoId.

I tried to change the Url to my server, but got Internal Server Error response and no any incoming connection on my server. Then I tried to remove the PhotoId data and repeat the request, again got Internal Server Error response but I got incoming connection on my server. I just point the url to my host without valid image file, so this may cause the Internal Server Error.

Later, I tried to provide a url with valid jpg image file and it successfully uploaded to media library. Then with the same image file, I modify the “name” form-data to image.html and image.php and surprisingly both successfully uploaded.

Long story short, I realized that I can’t inject php script into image exif metada because the app recreate the fetched image file with PHP-GD lib, after image re-creation, the exif metada will be removed.

I found several articles told that we still can inject PHP script into image and will not removed after image processing.

https://github.com/fakhrizulkifli/Defeating-PHP-GD-imagecreatefromjpeg

According to three articles above, we still can inject php script to the image but only with limited characters.

Also there is payload injector created by dlegs.

First attempt, I use dlegs tool to inject payload to .jpg image. You need to recreate a .jpg image with php-gd first then inject the payload with gd-jpeg.py

$ php gd.php image.jpg image-gd.jpg

Then inject the payload to image-gd.jpg

$ python gd-jpeg.py image-gd.jpg ‘<?php phpinfo()?>’ image-gd-poc.jpg

To make sure the injected payload working, try re-create the image-gd-poc.jpg with php-gd.

$ php gd.php image-gd-poc.jpg image-gd-poc-1.jpg

Then compare binary image-gd-poc.jpg and image-gd-poc-1.jpg

I use vbindiff to compare both image.

$ vbindiff image-gd-poc.jpg image-gd-poc-1.jpg

The injector writed by dlegs will inject your provided payload into a .jpg image file. This is only working if your target app recreate the image with default quality (default -1). However on my targeted app, it convert the image with quality 90, so the payload injected on image will removed.

The first attempt with payload on .jpg image was failed, so I try payload by @ABOUL3LA with .gif image file. On his article he provided POC.gif image that already injected with <?phpinfo()?>.

Finally, this is what I got after upload POC.gif and renaming the form-data “name” to test.php

If you want to use POC.gif payload by @ABOUL3LA the target app must haveshort_open_tag=Onset in php.ini, otherwise the php script won’t executed and you have to modify the payload with <?php ?> tag to make it work.

Reported this to the bug bounty program and get triaged in just few minutes, and they made quick patch in few hours.


I explore bit more into the php-gd lib, just want to know how many bytes we can inject to image. After several testing on different .jpg and .gif image, I can conclude that .jpg image can be injected with payload up to 13 bytes, but .gif image could be injected with more bytes.

GIF image that could be injected is GIF image that has null byte blocks. I found out that GIF images with Netscape Looping Application Extension have this null byte blocks.

Netscape Looping Application Extension is the most popular Application Extension Block that tells browser or other GIF viewer to loop the entire animated GIF file. http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension

This is example GIF image that has Netscape Looping Application Extension https://media2.giphy.com/media/6NjZoOdEbXs1W/source.gif

If we recreate the GIF image with php-gd, the null byte blocks not removed. Look at binary comparison between original gif and gd gif below.

So let’s try inject php shellcode to this null byte blocks and recreate the GIF image with PHP-GD, this is the binary comparison:

The injected payload remain there. Then you can upload the gif image to your vulnerable target app.

You can get the GIF Injector and other scripts here: https://gist.github.com/asdqwe3124/e63eba35dc8e6976af97f1a9348b277b


Muhammad R. Maulana

Written by

More From Medium

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade