picoCTF2019: Client-side-again

問題

問題文

Can you break into this super secure portal? https://2019shell1.picoctf.com/problem/32255/ (link) or http://2019shell1.picoctf.com:32255

Hints: What is obfuscation?

解答例

指針

解説

問題文で与えられた URL のリンクを踏み, サイトを見ると入力フォームが見つかる. サイトのソースコードを見ると以下のように, 認証をクライアントサイドのスクリプトで処理していることがわかる.

<script type="text/javascript">
  var _0x5a46=['25df2}','_again_b','this','Password\x20Verified','Incorrect\x20password','getElementById','value','substring','picoCTF{','not_this'];(function(_0x4bd822,_0x2bd6f7){var _0xb4bdb3=function(_0x1d68f6){while(--_0x1d68f6){_0x4bd822['push'](_0x4bd822['shift']());}};_0xb4bdb3(++_0x2bd6f7);}(_0x5a46,0x1b3));var _0x4b5b=function(_0x2d8f05,_0x4b81bb){_0x2d8f05=_0x2d8f05-0x0;var _0x4d74cb=_0x5a46[_0x2d8f05];return _0x4d74cb;};function verify(){checkpass=document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];split=0x4;if(checkpass[_0x4b5b('0x2')](0x0,split*0x2)==_0x4b5b('0x3')){if(checkpass[_0x4b5b('0x2')](0x7,0x9)=='{n'){if(checkpass[_0x4b5b('0x2')](split*0x2,split*0x2*0x2)==_0x4b5b('0x4')){if(checkpass[_0x4b5b('0x2')](0x3,0x6)=='oCT'){if(checkpass[_0x4b5b('0x2')](split*0x3*0x2,split*0x4*0x2)==_0x4b5b('0x5')){if(checkpass['substring'](0x6,0xb)=='F{not'){if(checkpass[_0x4b5b('0x2')](split*0x2*0x2,split*0x3*0x2)==_0x4b5b('0x6')){if(checkpass[_0x4b5b('0x2')](0xc,0x10)==_0x4b5b('0x7')){alert(_0x4b5b('0x8'));}}}}}}}}else{alert(_0x4b5b('0x9'));}}
</script>

これを読み解けば良いのだが, とても読みづらい.

とりあえず読みにくい JavaScript コードを少しでも読みやすくするため, JavaScript の整形ツールである js-beautify を使ってみることにする.

以下に示すコードが js-beautify によって整形した結果である.

(function(_0x4bd822, _0x2bd6f7) {
    var _0xb4bdb3 = function(_0x1d68f6) {
        while (--_0x1d68f6) {
            _0x4bd822['push'](_0x4bd822['shift']());
        }
    };
    _0xb4bdb3(++_0x2bd6f7);
}(_0x5a46, 0x1b3));
var _0x4b5b = function(_0x2d8f05, _0x4b81bb) {
    _0x2d8f05 = _0x2d8f05 - 0x0;
    var _0x4d74cb = _0x5a46[_0x2d8f05];
    return _0x4d74cb;
};

function verify() {
    checkpass = document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];
    split = 0x4;
    if (checkpass[_0x4b5b('0x2')](0x0, split * 0x2) == _0x4b5b('0x3')) {
        if (checkpass[_0x4b5b('0x2')](0x7, 0x9) == '{n') {
            if (checkpass[_0x4b5b('0x2')](split * 0x2, split * 0x2 * 0x2) == _0x4b5b('0x4')) {
                if (checkpass[_0x4b5b('0x2')](0x3, 0x6) == 'oCT') {
                    if (checkpass[_0x4b5b('0x2')](split * 0x3 * 0x2, split * 0x4 * 0x2) == _0x4b5b('0x5')) {
                        if (checkpass['substring'](0x6, 0xb) == 'F{not') {
                            if (checkpass[_0x4b5b('0x2')](split * 0x2 * 0x2, split * 0x3 * 0x2) == _0x4b5b('0x6')) {
                                if (checkpass[_0x4b5b('0x2')](0xc, 0x10) == _0x4b5b('0x7')) {
                                    alert(_0x4b5b('0x8'));
                                }
                            }
                        }
                    }
                }
            }
        }
    } else {
        alert(_0x4b5b('0x9'));
    }
}

だいぶ読みやすくなった.

次に _0x4b5b() 関数が返す値を Chrome DevTools の Console 機能を使って確かめていく. 問題のサイトを Chrome 系のブラウザで開き, Chrome DevTools を起動する.

以下のように Chrome DevTools の Console 機能を使って, _0x4b5b() 関数の戻り値を確認する.

>_0x4b5b('0x0')
<"getElementById"
>_0x4b5b('0x1')
<"value"
<_0x4b5b('0x2')
>"substring"
<_0x4b5b('0x3')
>"picoCTF{"
<_0x4b5b('0x4')
>"not_this"
<_0x4b5b('0x5')
>"25df2}"
<_0x4b5b('0x6')
>"_again_b"
<_0x4b5b('0x7')
>"this"
<_0x4b5b('0x8')
>"Password Verified"
<_0x4b5b('0x9')
>"Incorrect password"

これらの値を verify() 関数の可読性が高まるよう, コード内に適切に置換する. ここでは Python の replace メソッドを使って文字列の置換をしてみる.

  • replace.py
#!/usr/bin/env python3

obfuscated = """
function verify() {
    checkpass = document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];
    split = 0x4;
    if (checkpass[_0x4b5b('0x2')](0x0, split * 0x2) == _0x4b5b('0x3')) {
        if (checkpass[_0x4b5b('0x2')](0x7, 0x9) == '{n') {
            if (checkpass[_0x4b5b('0x2')](split * 0x2, split * 0x2 * 0x2) == _0x4b5b('0x4')) {
                if (checkpass[_0x4b5b('0x2')](0x3, 0x6) == 'oCT') {
                    if (checkpass[_0x4b5b('0x2')](split * 0x3 * 0x2, split * 0x4 * 0x2) == _0x4b5b('0x5')) {
                        if (checkpass['substring'](0x6, 0xb) == 'F{not') {
                            if (checkpass[_0x4b5b('0x2')](split * 0x2 * 0x2, split * 0x3 * 0x2) == _0x4b5b('0x6')) {
                                if (checkpass[_0x4b5b('0x2')](0xc, 0x10) == _0x4b5b('0x7')) {
                                    alert(_0x4b5b('0x8'));
                                }
                            }
                        }
                    }
                }
            }
        }
    } else {
        alert(_0x4b5b('0x9'));
    }
}
"""

plain = obfuscated.replace("_0x4b5b('0x0')", "\"getElementById\"")
plain = plain.replace("_0x4b5b('0x1')", "\"value\"")
plain = plain.replace("_0x4b5b('0x2')", "\"substring\"")
plain = plain.replace("_0x4b5b('0x3')", "\"picoCTF{\"")
plain = plain.replace("_0x4b5b('0x4')", "\"not_this\"")
plain = plain.replace("_0x4b5b('0x5')", "\"25df2}\"")
plain = plain.replace("_0x4b5b('0x6')", "\"_again_b\"")
plain = plain.replace("_0x4b5b('0x7')", "\"this\"")
plain = plain.replace("_0x4b5b('0x8')", "\"Password Verified\"")
plain = plain.replace("_0x4b5b('0x9')", "\"Incorrect password\"")

print(plain)
  • 実行結果
function verify() {
    checkpass = document["getElementById"]('pass')["value"];
    split = 0x4;
    if (checkpass["substring"](0x0, split * 0x2) == "picoCTF{") {
        if (checkpass["substring"](0x7, 0x9) == '{n') {
            if (checkpass["substring"](split * 0x2, split * 0x2 * 0x2) == "not_this") {
                if (checkpass["substring"](0x3, 0x6) == 'oCT') {
                    if (checkpass["substring"](split * 0x3 * 0x2, split * 0x4 * 0x2) == "25df2}") {
                        if (checkpass['substring'](0x6, 0xb) == 'F{not') {
                            if (checkpass["substring"](split * 0x2 * 0x2, split * 0x3 * 0x2) == "_again_b") {
                                if (checkpass["substring"](0xc, 0x10) == "this") {
                                    alert("Password Verified");
                                }
                            }
                        }
                    }
                }
            }
        }
    } else {
        alert("Incorrect password");
    }
}

コードを読むと, 入力された文字列の部分文字列がそれぞれ, 以下を満たしていれば認証を通ることがわかる.

checkpass = document["getElementById"]('pass')["value"];
checkpass["substring"]( 0, 8 ) == "picoCTF{" 
checkpass["substring"]( 7, 9 ) == '{n'
checkpass["substring"]( 8, 16) == "not_this"
checkpass["substring"]( 3, 6 ) == 'oCT'
checkpass["substring"](24, 32) == "25df2}"
checkpass['substring']( 6, 11) == 'F{not'
checkpass["substring"](16, 24) == "_again_b"
checkpass["substring"](12, 16) == "this"

したがって, "picoCTF{" + "not_this" + "_again_b" + "25df2}" の結果が flag となる.

$ python
>>> "picoCTF{" + "not_this" + "_again_b" + "25df2}"
'picoCTF{not_this_again_b25df2}'

flag: picoCTF{not_this_again_b25df2}

感想

久しぶりにブログを更新した. 今後は更新頻度を上げて, CTF, 競プロともに演習量を増やしていきたい.

参考文献