OverTheWire - Behemoth
behemoth
behemoth.labs.overthewire.org 2221
level 0 → level 1
1 | SSH Information |
1 | behemoth0@behemoth:~$ ls -la /behemoth/ |
Behemoth 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.
1 | behemoth0@behemoth:~$ /behemoth/behemoth0 |
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.
1 | behemoth0@behemoth:~$ ltrace /behemoth/behemoth0 |
The password is revealed by ltrace: eatmyshorts. Enter
it to get a shell as behemoth1.
1 | behemoth0@behemoth:~$ /behemoth/behemoth0 |
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:
1 | behemoth1@behemoth:/tmp/behemoth1$ python3 -c " |
1 | from pwn import * |
Payload
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:
1 | behemoth1@behemoth:/tmp/behemoth1$ export EGG=$(python3 -c " |
Exploit with the env address:
1 | behemoth1@behemoth:/tmp/behemoth1$ (python3 -c " |
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:
1 | behemoth2@behemoth:/tmp$ echo '#!/bin/bash' > touch |
Alternative symlink approach: create a symlink named after the PID pointing to the password file before cat runs (within the 2000 second window).
JQ6tZGqt0ilevel 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
1 | behemoth3@behemoth:/behemoth$ /behemoth/behemoth3 |
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
1 | from pwn import * |
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
1 | behemoth3@behemoth:/tmp/behemoth3$ export EGG=$(python3 -c " |
The returned address shifts with argv length. To keep it stable, pad
argv[0] when running the binary to match the getenv call
length:
1 | behemoth3@behemoth:/tmp/behemoth3$ ARGV0=$(python3 -c "print('/behemoth/behemoth3' + ' ' * 40)") |
Format string calculation
puts@GOT = 0x0804b218. The EGG address has two 16-bit halves: - low =
egg_addr & 0xffff - high =
(egg_addr >> 16) & 0xffff
Payload structure: 1
2[puts@GOT addr (4B)] [puts@GOT+2 (4B)] [fmt specifiers] [newline]
↑ arg 1 for %1$hn ↑ arg 2 for %2$hn
The first 8 bytes are printed as literal output, then
%<val1>c pads to reach low, then
%<val2>c pads further to reach high:
1 | from pwn import * |
Run
1 | behemoth3@behemoth:/tmp/behemoth3$ python3 -c " |
If 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).
hpjUdlG723level 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:
1 | behemoth4@behemoth:/tmp/behemoth4$ for i in $(seq 1 65535); do |
This can take a few seconds to minutes—each run has a PID, and you need one that falls in your symlink range.
mVfC4rBKZ4level 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:
1 | # Session 1 - listener |
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:
1 | from pwn import * |
Extract the shellcode bytes:
1 | behemoth6@behemoth:/tmp/behemoth6$ python3 -c " |
Or if you prefer NASM, extract with pwntools instead of
grep -Po:
Write to shellcode.txt and run:
1 | behemoth6@behemoth:/tmp/behemoth6$ python3 -c " |
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
1 | behemoth7@behemoth:~$ /behemoth/behemoth7 $(perl -e 'print "\x90"') |
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
1 | behemoth7@behemoth:/behemoth$ gdb -q ./behemoth7 |
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:
1 | [A × 528] [RET → ESP] [NOP sled × 200] [shellcode] |
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.
1 | from pwn import * |
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:
1 | behemoth7@behemoth:/tmp$ python3 -c " |
Stack is at 0xffffd8f0 — stable per session (ASLR
disabled server-side), varies between logins.
Exploit
1 | behemoth7@behemoth:/tmp$ /behemoth/behemoth7 $(python3 -c ' |
Replace 0xffffd8f0 with the ESP value from your gdb
session. If the shell doesn't spawn, the address missed the NOP sled —
adjust ±16.