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.
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:
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:
Which produces the following crash:
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()
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"
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.
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
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.
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:
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:
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:
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.
- 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
). - That we insert the address in the correct endianness. My machine is little endian so the address
0x01020304
will be inserted as0x04030201
.
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.
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.
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
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:
- Found a Stack Overflow vulnerability
- Created a malicious firewall configuration to trigger the vulnerability
- Engineered our exploit to achieve WWW (Write What Where) and jump to an arbitrary address
- Bypassed ASLR (an exploit mitigation) by not using any library, heap or stack address
- Bypassed DEP (an exploit mitigation) by using the
system
function - 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.
- Attacking The Network’s Security Core - Hunting For Vulnerabilities In A Network Security Tool
- Stack Overflow CVE-2019-17424 Vulnerability Write-Up and RCE Exploit Walk Through (this)
- Heap Overflow CVE-2019-17425 Vulnerability Write-Up and Information Leak Exploit Explained (coming soon)
- Fuzzing A Network Security Product - Finding CVE-2019-17422 & CVE-2019-17423 With The HonggFuzz Fuzzer (coming soon)