Stack Overflow CVE-2019-17424 Sanitizer Crash

Stack Overflow CVE-2019-17424 Vulnerability Write-Up and RCE Exploit Walk Through

This is Part 2 in a 4 part series about my process hunting for vulnerabilities in a network auditing tool (used to protect networks by detecting and fixing security holes), and fully exploiting one of the vulnerabilities I found. I recommend reading the series in ascending numeric order. Link to part 1 here. Links to parts 3, and 4 at the end of this post.

This post describes how I found CVE-2019-17424 and successfully exploited the vulnerability in the precompiled, packaged product.

Vulnerability

Reader’s Exercise

I found CVE-2019-17424 by manually reviewing the source code of nipper-ng. Provided below is an excerpt from the source code containing only the vulnerable function. You are welcome to take it as an exercise to find the vulnerability in the code below:

/* Original nipper-ng processPrivilage implementation */

/* struct used later in the code */
struct ciscoCommand
{
    int parts;
    char part[40][128];
};

void processPrivilage(
  char *line /* data pointed to by @line is user controlled */, 
  struct nipperConfig *nipper)
{
    // Variables...
    struct privilageLevels *privilagePointer = 0;
    struct ciscoCommand command;
    char tempString[sizeof(line)];

    // Debug
    if (nipper->debugMode == true)
    {
        printf("Privilage Line: %s\n", line);
    }

    // Is this the first privilage
    if (nipper->ios->privilage == 0)
    {
        // Create storage for enable...
        privilagePointer = malloc(sizeof(struct privilageLevels));
        memset(privilagePointer, 0, sizeof(struct privilageLevels));

        // Sort out pointers...
        privilagePointer->next = nipper->ios->privilage;
        nipper->ios->privilage = privilagePointer;
    }
    else
    {
        // Get last privilage
        privilagePointer = nipper->ios->privilage;
        while (privilagePointer->next != 0)
            privilagePointer = privilagePointer->next;

        // Create structure
        privilagePointer->next = malloc(sizeof(struct privilageLevels));
        memset(privilagePointer->next, 0, sizeof(struct privilageLevels));
        privilagePointer = privilagePointer->next;

        // Init
        privilagePointer->next = 0;
    }

    // Init
    command = splitLine(line); 
    /* the splitLine function fills @command (of type struct ciscoCommand) 
       with the first 120 chars of the first 40 words from @line, and correctly
       sets @command.parts to the number of words it stored in array @command.part
    */
    strcpy(tempString, "");

    // Privilage Level
    privilagePointer->level = atoi(command.part[3]);

    // Command Access
    int loop;
    for (loop = 4; loop < command.parts; loop++)
    {
        sprintf(tempString, "%s %s", tempString, command.part[loop]);
    }
    strncpy(privilagePointer->command, tempString, sizeof(privilagePointer->command));
}

Notice: The vulnerability in the code above is identified in the paragraph below. If you want to try to find the vulnerability yourself, only keep reading after you have found it.

Vulnerability

As defined in an earlier post, a vulnerability is a bug that causes unintended consequences. In the code above, the vulnerability in how the size allocated for tempString is calculated (line 17). The size of tempString is allocated according to sizeof(line). However, since line is of type char *, sizeof returns the size of the pointer itself: 4 bytes on 32 bit systems, and 8 bytes on 64 bit systems. Later in the code a for loop (lines 65-68) is used to copy attacker controlled data from line to tempString and can result in a stack overflow if the attacker supplies a more than 4 bytes to copy. I reported this vulnerability to the vendor, and received CVE-2019-17424 to identify this vulnerability.

Exploiting CVE-2019-17424

Environment Setup

Investing in a comfortable and convenient working environment pays back huge returns as it makes all following work more fun and efficient. For this project we will work on Linux (nipper-ng compiles by default for Linux) and we’ll need a debugger and hex editor.

My favorite Linux debugger is gdb with the pwndbg add-on which provides a much more appealing and convenient interface, as well as added useful commands.

pwndbg breakpoint on main
pwndbg breakpoint on main
gdb breakpoint on main
gdb breakpoint on main

Exploit Plan

Since CVE-2019-17424 allows us to overflow data on the stack, the most simple exploit would be to overwrite the return address allowing us to jump to an arbitrary address, thus executing arbitrary code.

Triggering The Stack Overflow

To exploit the vulnerability we first need to trigger the bug. We know the vulnerability is in the processPrivilage() function, and searching the codebase we learn processPrivilage() is called from processIOSInput() when the following conditions are met:

flow to reach vulnerable processPrivilage() function

Reading the code above, we learn that processPrivilage() is called when there is a line in the parsed firewall configuration that starts with the words "privilage exec level". “

Let’s generate a malicious firewall configuration file to trigger the stack overflow, which will hopefully crash the program. Our initial trigger file consist of only one line:

Initial exploit file
Initial exploit file

Which produces the following crash:

First exploit crash in pwndbg
First exploit crash in pwndbg

This crash confirms the existence of the bug. However, when we look at the crash’s stacktrace (bottom blue window in picture above), it’s not what we expect. We want to see “a”s (hex 61) in the calls stack; but instead we see garbage numbers such as “303d6d72” and “35333b31”. Let’s debug to understand what’s going on.

Intermission

The rest of this post describes in high detail each exploit development stage until achieving stable remote code execution. If you have any questions feel free to DM me or tweet at me on twitter. Also, follow me for news about my latest vulnerabilities Twitter user @va_start.

Debugging Initial Failed Exploit Attempt

Let’s open IDA Pro and examine the stack variables of processPrivilage() to determine exactly which variables we’re overwriting when we overflow tempString on the stack. Having the source code available made naming stack variables so much easier when reversing processPrivilage() in IDA.

To fully follow the rest of this blog post I recommend opening nipper-ng’s source and its binary in IDA. Below are the stack variables in the call frame of processPrivilage()

Stack variables of of function processPrivilage()
Stack variables of of function`processPrivilage()`

There are other, compiler generated, stack variables purposely hidden that we will currently ignore as they don’t interfere with our exploit process. I might discuss them in a later blog post as they truly deserve a whole post of their own.

As predicted, tempString is 4 bytes long. When we overflow it with our data it first overwrites command, then privilagePointer, then index, then s (saved registers), and lastly, r (the function’s return value).

This is interesting since if we look at line 67 in the code snippet above, command is the structure with our string which we’re copying from; so how can we overflow past it if our data is from there? command’s size is 0x1404 bytes (calculated from the fact that privilagePointer, the adjacent variable, is at offset -0x10 and command is at offset -0x1414 ; abs(-0x1414 - -0x10)=0x1404). Even if we fill all 0x1404 bytes of command with our data, when they’re copied to tempString we will only reach offset -0x14, which is inside command and doesn’t overwrite any interesting variable. So how it is possible that we’re, against all logic, crashing the program?

Everything needed to answer this question is available in the code snippet of processPrivilage() and the picture of the stack variables. You are welcome to solve this question as an exercise before reading on where I share the answer.

Debugging The Crash

Even though the crash may currently seem unexplainable, there is no incomprehensible magic in exploit development - with enough debugging anything can be explained.

Let’s focus on tempString as we know that’s the variable we overflow. Let’s print tempStrings’s contents after each time sprintf writes to it (line 67/address 0x806732c according to IDA). To achieve this I’ll use Dynamic Printf which is a powerful way to print dynamic data (registers or memory) when debugging a program. As with any function, the EBP register points to the start of the stack frame, so to access tempString at offset -0x1418 from the stackframe, I’ll use the following gdb dprintf command: “dprintf \*0x806732c, "tempString: (len %x) %s\n", strlen((char*)$ebp-0x1418), (char*)$ebp-0x1418"

Using dprintf to print the contents of 'tempString' after sprintf
Using dprintf to print the contents of `tempString` after sprintf

Immediately we realize that sprintf is being called multiple times! This is surprising because if we look at the conditions of the for loop (line 65) we should only loop once: We have 5 words in our command (“privilage”, “exec”, “level”, “1”, “aaa…”) so command.parts - 4 (the loop starts at 4) = 1.

Let’s dive deeper into why the for loop happens multiple times. Let’s print the current index (variable loop in source) and command.parts before the loop.

IDA Pro display of the 'for' loop from line 65
IDA Pro display of the '`for`' loop from line 65

Again, to do this we will use dprintf. The proper dprintf command to print both variables is “dprintf *0x08067336, "index: %x command.parts: %x\n", *(int *)((char *)$ebp-0xc), $eax

Conclusion

command.parts is overflowed with our input data ("aaaa" = 61616161 hex)
command.parts is overflowed with our input data \("aaaa" = 61616161 hex\)

After running the dprintf command mentioned above, it becomes obvious why our loop ran multiple times! We overflow the beginning of the command struct, who’s first element, .parts, is used to decide when to exit the loop! We can easily see this in the dprintf output above: after the first loop iteration, command.parts contains data from our input.

Additionally, this explains why we currently crash, and also why we have junk in our initial backtrace instead of the 61616161’s we were expecting. With index (which is called loop in the source code) ever increasing, we will quickly start to access junk when fetching data from command.part[loop] (line 67).

Successful Workaround

The primary issue is that we lose control once loop or index are really big. This is because we start to access garbage when those variables are used in line 67 in command.part[loop] (as explained above). This leads to unexploitable and therefore useless crashes. To solve this, we can simply add more parts to our command so that command.part will be a bigger array. Once command.part will be a bigger array, command.part[loop] will access valid data for bigger values of loop.

To add elements to the command.part array, we add more parts to our exploit line. The newly modified exploit file is displayed below.

The new exploit file (notice all text is on the same line)
The new exploit file (notice all text is on the same line)

Victorious Stack Overflow Crash

When we run nipper under a debugger and with the above exploit file, we are greeted with the charming crash below:

nipper-ng crash with attacker controlled stack and registers annotated in bright orange
nipper-ng crash with attacker controlled stack and registers annotated in bright orange

This crash is great! We can see that we completely control the stack backtrace in the lower left corner - meaning we will be able to control which address the function will jump to when it finishes. In addition, we also control the contents of certain registers (EDX, EDI) and the data of where other registers point (EAX, ECX, ESI, EBP).

Stack Overflow Exploit Development

Write-What-Where 🖊

Looking at the crash above, and specifically at the assembly, we can see the program is crashing in strncpy since it’s trying to write data (that we control) from register XMM1 to the address in EDX (which we control). This gives us a Write-What-Where primitive. By changing EDX, which we control, we can write anything anywhere!

Even though WWW (Write-What-Where) is is very useful in exploit development, we have an easier method of directly achieving RCE (remote code execution): we control the return address of the function allowing us to jump anywhere.

Remote Code Execution

To achieve RCE by reaching the return address of processPrivilage(), we must not crash in the WWW from strncpy. To do that we need to supply a valid address in EDX which is currently aaaa (61616161 in hex). The only issue is we currently don’t know which “a”s in our exploit are the “a”s that eventually reach EDX.

pwnlib.util.cyclic

To understand which “a”s eventually overwrite the stack and registers, we will replace all sets of 4 “a”’s in our exploit file with a unique pattern that we can later trace back when the program crashes. A convenient tool to do this is pwnlibs’s util.cylic.cylic() function.

Our exploit file has 14 lines of 70 “a”s. We will use the following python commands to generate an appropriate unique stream and broken up it into lines of 70 charterers:

Using pwnlib to generate a unique stream for our exploit
Using pwnlib to generate a unique stream for our exploit

Finalizing The Exploit

After updating our exploit file with the output from the command above, we run nipper-ng with a debugger and get the following crash:

nipper-ng crash

Reviewing the crash we notice that EDX is being overwritten with “saah”. Since “saah” only appears once in our exploit file, we can easily find and replace it with a valid, writable address to keep nipper-ng from crashing. Let’s do that.

When inserting an address instead of a string currently in our exploit file, we need to ensure 2 things.

  1. That the address doesn’t include any characters that will change the parsing of our file. These characters include tab (0x09), space (0x20), and newline (0x10, 0x13).
  2. That we insert the address in the correct endianness. My machine is little endian so the address 0x01020304 will be inserted as 0x04030201.

Ensuring I followed the 2 rules above, I replaced “saah” with the address 0x080fa001 which is a writable address in the executable. Now strncpy copies our data to 0x080fa001 instead of crashing. It’s important to note that since 0x080fa001 is an address in the executable and the executable isn’t compiled as PIE (Position Independent Executable), that address will never change - allowing us to bypass ASLR (an exploit mitigation). Let’s run nipper with our new exploit file and see where the next crash is.

Nipper-ng crashes because the EIP ("xaah") is an invalid address
Nipper-ng crashes because the `EIP` \("xaah"\) is an invalid address

The next crash happens when we the program tries to jump to 0x68616178 (“xaah”). In a similar fashion to the way we fixed the previous crash, let’s replace “xaah” with an address we want to jump to. Because nipper-ng makes use of the library function system (which executes shell commands), it’s already in the code and we can use it too. I replaced “xaah” with the address of a call to system and let nipper run again.

GDB showing that nipper-ng has started a new process
GDB showing that nipper-ng has started a new process

Congratulations to us! Nipper-ng has started a child process - showing that system was indeed called! Now we just need to pass a shell command as a parameter to system to execute something useful.The added advantage of using system to run code is that it bypasses DEP (an exploit mitigation) - in that we don’t need to execute any shellcode.

system reads its parameter from the stack which fortunately for us we already control. I set the next dword (4 bytes) after “xaah”, which will be the parameter passed to system, to 0x080fa015 - an address we control the contents of thanks to the strncpy from eariler. Because strncpy copies from our exploit file over to the address which is later passed to system, we will further change our exploit file by inserting the command we want to execute: gnome-calculator.

Victory

CVE-2019-17424 Nipper-ng popping calc gif
Nipper-ng popping calc!

When we run nipper-ng with our malicious firewall configuration exploit file, the gnome calculator pops up! Awesome!

Automating Exploit Generation

To enable you to expirement, and since I know not everyone wants their exploit to open a calculator, I built a python exploit generator to enable easy exploit generation with custom shell commands avaible here.

Bypassing Blacklisted Space Character

There is an issue with using a space (” “) in the payload command passed to system as a space will influence how our file is parsed, misaligning the rest of the exploit. To bypass this restriction, we can use the shell’s “Internal Field Separator variable”, IFS, instead.

So touch /tmp/pwnd becomes touch$\{IFS\}/tmp/pwnd

Summary

To summarize our adventure this post:

  1. Found a Stack Overflow vulnerability
  2. Created a malicious firewall configuration to trigger the vulnerability
  3. Engineered our exploit to achieve WWW (Write What Where) and jump to an arbitrary address
  4. Bypassed ASLR (an exploit mitigation) by not using any library, heap or stack address
  5. Bypassed DEP (an exploit mitigation) by using the system function
  6. Achieved RCE!

Patch

As mentioned in the previous post, the vendor no longer supports this product so they won’t be releasing a patch. To protect users that still user nipper-ng (it comes installed by default on Kali for example), I’ll provide a patching method for those interested. The patch I recommend is to simply disable the vulnerable processPrivelage function by erasing everything from the function except for the return. No code, no vulnerabilities.

Feedback

Please let me know what you though of this post via twitter or a comment below.

Next Chapters

This is Part 2 in a 4 part series about my process hunting for vulnerabilities in a network auditing tool and fully exploiting one of the vulnerabilities I found. I recommend reading the series in ascending numeric order.

  1. Attacking The Network’s Security Core - Hunting For Vulnerabilities In A Network Security Tool
  2. Stack Overflow CVE-2019-17424 Vulnerability Write-Up and RCE Exploit Walk Through (this)
  3. Heap Overflow CVE-2019-17425 Vulnerability Write-Up and Information Leak Exploit Explained (coming soon)
  4. Fuzzing A Network Security Product - Finding CVE-2019-17422 & CVE-2019-17423 With The HonggFuzz Fuzzer (coming soon)