Blog: How-Tos

From PNG tEXt to Persistent XSS

Jon Murray 06 Apr 2018

I was on job for a client and was playing around with various endpoints they have for uploading files. They’re really strict on several things and will only accept files with a .PNG extension. In one place, however, you were able to upload files with a .html extension…score. Well, not really. You’re allowed to upload stuff in blobs, like the below:

{“Name”:”image.png.html”,”Data”:”data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==”}

(simple 1px red dot)

Changing the data:type to anything interesting (data:html, url, etc. etc.) fails validation. Changing the PNG to anything also fails validation (assuming it is not a legit PNG). However, the Base64 is decoded and executes on the resultant html page, like the below:

‰PNG  IHDRo&åIDAT×cøÿÿ?Ã à „Ð1ñ‚XÍ
õ5ËÑŽ

­IEND®B`‚

So that got me thinking… i should be able to just tack on “/><script>alert(“xss”);</script>” on the end in base64, and it’ll work, right? Well no. That failed validation as well.

Top answers on Stack Overflow suggested doing magic byte header checks to ensure the PDF is valid i.e. checking if the first few bytes are “0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A” or a bit more/bit less.

So I constructed a “fake” PNG with the magic bytes, then “/><script>alert(“XSS”);</script>” as a byte array, encoded that, and fired it off. That failed validation.. which is a good thing. Just checking for magic bytes is something I’d flag on a code review. They’re using .NET so I figured they’d be doing something similar to the following, which is the right way:

   try
            {
                using (var ms = new MemoryStream(mPNGBytes))
                {
                    var aa = Image.FromStream(ms);
                    if (aa.Height >= 0)
                    {
                        Console.WriteLine("Valid image");
                    }
                }
            }
            catch (Exception)
            {
               // invalid PNG
            }

I spent a little while knocking up a program hoping to get lucky and basically iterating through a legit PNG, and inserting my XSS string as a byte array at position 1, position 2, etc. and then adding in the inserting or appending the remaining bytes to see if anything passed validation. Nothing did, and it was a waste of time.

Off to the RFC. There is the possibility to add in “text blocks” into PNG files (https://www.w3.org/TR/2003/REC-PNG-20031110/#11tEXt) . These are legit things which are effectively for metadata (copyright, etc.). There’s a few SDKs around but they’re all dated or hard to use. Using ImagMagick did the trick. Using the example command:

convert Head_red.png -set 'Copyright' '/><script>alert("1");</script>' -set 'Title' '/><script>alert("2");</script>' -set comment '/><script>alert("3");</script>' OUT.png

Appends the following footers to the PNG:

This passes validation now. Sure enough, uploading it with the html endpoint results in:

{"Name":"image.png.html","Data":"data:image/png;base64,<base64 string>}

and the response:

{"Id":845,"Name":"image.png.html","StorageFolder":"fjgsc452kgebjli2","ContentType":"","Size":31115,"Height":739,"Width":590,"Deleted":false,"Created":"0001-01-01T00:00:00","DomainId":0,"Domain":null,"ApplicationUserId":null,"ApplicationUser":null,"PrivacySetting":"MyCompany","Tags":[],"Url":"https://████████████.blob.core.windows.net/images/fjgsc452kgebjli2/image.png.html","ThumbnailUrl":"https://████████████.blob.core.windows.net/images/fjgsc452kgebjli2/image.png_thumbnail.html"}

So finally, we get XSS at https://████████████.blob.core.windows.net/images/fjgsc452kgebjli2/image.png.html:

There are text blocks for many image file types such as jpeg and gif (and plenty of non-image file types too) so make sure input filtering correctly validates appended or embedded content too.