Images are usually stored as binary files, while a Javascript file is basically just text. Both have to follow their own rules: Images have a concrete file format, encoding the data in a certain way. Javascript files have to follow a specific syntax in order to be executable. I wondered: Can I create an image file that has valid Javascript syntax, so it is also executable?
Before you read on, I highly recommend checking out this code sandbox with the result of my experimentation:
https://codesandbox.io/s/executable-gif-8yq0j?file=/index.html
If you want to check out the image and inspect it yourself, you can download it here:
https://executable-gif.glitch.me/image.gif
Choosing the Right Image Type
Unfortunately, images contain a lot of binary data which will throw an error if interpreted as Javascript. So my first idea was: What if we just put all the image data in a large comment, like this:
/*ALL OF THE BINARY IMAGE DATA*/
That would be a valid Javascript file. However, image files need to start with a specific sequence of bytes; a file header that is specific to the image format. PNG files for example always have to start with the byte sequence 89 50 4E 47 0D 0A 1A 0A. If an image would start with /*
, it would not be a valid image file anymore.
This file header lead to the next idea: What if we could use this byte sequence as variable name and have a huge string assignment like this:
PNG=`ALL OF THE BINARY IMAGE DATA`;
We are using template strings instead of the normal "
or '
strings because the binary data could contain linebreaks and template strings are better at dealing with those.
Unfortunately, most byte sequences for image file headers contain unprintable characters that are not allowed in variable names. But there is one image format we can use: GIF. The GIF header block is 47 49 46 38 39 61, which conveniently spells GIF89a in ASCII, a perfectly legal variable name!
Choosing the Right Image Dimensions
Now that we found an image format that starts with a valid variable name, we need to add the equals and backtick characters. The next four bytes of the file are therefore: 3D 09 60 04
In the gif format, the four bytes following the header specify the dimensions of the image. We have to fit 3D (the equals sign) and 60 (the backtick that opens the string) in there. GIF uses little endian so the second and fourth character have a huge influence on image dimensions. We want to keep them as small as possible to not end up with an image that is tens of thousands pixels wide. Therefore we store the big bytes 3D and 60 in the least significant bytes.
The second byte of the image width needs to be a valid whitespace character, because this would be the space between the equals sign and the beginning of the string GIF89a= `...
. Keep in mind that the hexcode of the characters should be as small as possible, or the image would be huge.
The smallest whitespace character is 09, the horizontal tab. This gives us an image width of 3D 09, which in little endian translates to 2365; a bit wider than I would have liked, but still reasonable.
For the second height byte, we can just choose something that produces a nice aspect ratio. I chose 04, which produces a height of 60 04, or 1120.
Getting our own script in there
Right now, our executable gif does not really do anything. It just assigns a large string to the global variable GIF89a
. We want something interesting to happen! Most of the data inside the GIF is for encoding the image, so if we try to add Javascript in there, we would probably end up with a very corrupted image. But for some reason, the GIF format contains something called a Comment Extension. This is a place to store some metadata that will not be interpreted by the GIF decoder - a perfect place for our Javascript logic.
This comment extension comes right after the GIF color table. Since we can put any content in there, we can easily close the GIF89a string, add all our Javascript and then start a multiline comment block, so the rest of the image does not interfere with the Javascript parser.
All in all, our file could then look like this:
GIF89a= ` BINARY COLOR TABLE DATA ... COMMENT BLOCK:
`;alert("Javascript!");/*
REST OF THE IMAGE */
There is a small restriction: While the comment block itself can be of any size, it is composed of multiple sub-blocks, each of which has a maximum size of 255. Between the subblocks is a single byte that indicates the length of the next subblock. So in order to fit a larger script in there, it has to be divided into smaller chunks, like this:
alert('Javascript');/*0x4A*/console.log('another subblock');/*0x1F*/...
The hexcodes in the comments are the bytes that indicate the size of the next subblock. They are irrelevant for the Javascript, but required for the GIF file format. In order to prevent them from interfering with the rest of the code, they have to be in comments. I wrote a small script that processes the script chunks and adds them to the image file: https://gist.github.com/SebastianStamm/c2433819cb9e2e5af84df0904aa43cb8
Cleaning up the Binary
Now that we have the basic structure down, we need to make sure that the binary image data does not ruin our syntax. As explained in the previous section, the file has three sections: The first one is an assignment to the variable GIF89a, the second one is the Javascript code and the third one is a multiline comment.
Let's have a look at the first part, the variable assignment:
GIF89a= ` BINARY DATA `;
If the binary data would contain the character `
or the character combination ${
we are in trouble because this would either end the template string or produce an invalid expression. The fix here is quite easy: Just change the binary data! E.g. instead of using the `
character (hexcode 60), we could use the character a
(hexcode 61). Since this part of the file contains the color table, it would result in some of the colors being slightly off, e.g. using the color #286148
instead of #286048
. It is unlikely that someone will notice the difference.
Fighting the corruption
At the end of the Javascript code, we opened a multiline comment in order to make sure the binary image data does not interfere with the Javascript parsing:
alert("Script done");/*BINARY IMAGE DATA ...
If the image data would contain the character sequence */
, the comment would end prematurely, which would result in an invalid Javascript file. Here again we can manually change one of the two characters so they no longer end the comment. However, since we are now in the encoded image section, this will result in a corrupted image, like this:
In extreme cases the image could not be displayed at all. By carefully choosing which bit to flip I was able to minimize the corruption. Fortunately there were only a handful of instances of the harmful */
combination to deal with. There is still a bit of corruption visible in the final image, e.g. at the bottom of the "Valid Javascript File" string, but overall I am pretty happy with the result.
Ending the File
The last operation we have to perform is at the end of the file. The file has to end with the bytes 00 3B. So we have to end our comment earlier. Since this is the end of the file and any potential corruption would not be very visible, I just ended the multi block comment and added a single line comment so that the end of the file would not cause any problems when parsing:
/* BINARY DATA*/// 00 3B
Convincing the Browser to Execute an Image
Now, after all this, we finally have a file that is both an image as well as a valid Javascript file. But there is one last challenge we have to overcome: If we upload the image to a server and try to use it in a script tag, we would most likely see an error like this:
Refused to execute script from 'http://localhost:8080/image.gif' because its MIME type ('image/gif') is not executable.
So the browser rightfully says "That's an image! I am not going to execute that!". Which in most cases is a good mindset to have. But we want to execute it anyway. Our solution here is to just not tell the browser that it's an image. For that I wrote a small server that serves the image without any header information.
Without the MIME type information from the header, the browser does not know that it is an image and just does what fits the context best: Display it as image in an <img>
tag, or execute it as Javascript in a <script>
tag.
But ... why?
That is something I have yet to figure out. It was a nice mental challenge to make this stuff work, but if you can think of any scenarios where this might actually be useful, please let me know!
Top comments (19)
π€―
That is pretty awesome. Thanks. I'm thinking about possible security implications of this. Just like when PDFs got hacked. But the concept is different here. It would be worse if the file would be in an img tag and the browser would display it and run the JavaScript code.
Exactly
This post is a zero day attack πππ
But we want to execute it anyway. Our solution here is to just not tell the browser that it's an image. For that I wrote a small server that serves the image without any header information
Its a nice project tho
How is it a zero day attack? The
<img>
tag will never execute its content as javascript, regardless of the response headers.Wow, reallyβ¦ coolβ¦ π€ β¦ scary?
One use could be to embed trackers in image files (giphy does that already maybe?). The other one would be straight hacking.
None of that sounds "great user experience", but it's good to know it's a thing π¬
How would you embed a tracker? The code won't execute unless it's in a script tag, at which point you might as well just load javascript.
If you're talking about embedding information in an image, there's already ways to do that (search for steganography).
Well I don't know precisely, but I was thinking something around a giphy copy-paste style: either with iframe, or little bit of JS script that seems to load a valid image. Like "copy this
<script> β¦ </script>
at the bottom of your page and insert your .gif URL", which seems to be perfectly valid .gif if you open in browser.Or a service like cloudinary could do that:Β inject script in your images and then execute it inside their SDK (while the url still gives a perfectly valid image).
That seems over complicated thoughβ¦giphy just juste plain iframe without hiding anything and nobody cares π
This is awesome! btw nice snake game π
From a previous conversation: dev.to/phlash909/comment/caol
Sorry if I missed something from the article, but did you successfully execute some JS from an tag in the end? It looks like that wouldn't be achievable?
My goal was not to execute code from an img tag, my goal was to create a file that is both an image and a script. Using the same file, the browser shows it as image in an img tag (without executing any Javascript), and executes the Javascript in a script tag (without showing any image).
Ah i see. Very interesting and informative article!
Wow that's cool
This is actually very interesting! Although the fact of making the browser do things that it wouldn't do otherwise makes me think it would be used by someone somewhere in a malicious way.
Thanks this made my day
Wow, that's incredible.