BKP CTF 2015 - Kendall

Kendall was a 300 point “red” challenge - an exploitable. This was a pretty involved challenge but it was simple once you realized what you had to do. Launching the binary would start a forking server for some DHCP Management Console.

Playing around with the console, it’s clear that authenticating is going to be integral to solving the challenge. The authenticate function opens a password.txt file and compares it with your input. You would probably be able to use the strcmp as a timing oracle to brute force the password, but that’s kind of lame.

While reversing, we noticed the same strange function being used to read user input being used everywhere. Strange, mostly because it only accepted a size parameter. It didn’t accept a destination buffer nor did it allocate space for one - it just used the same statically sized 128 byte long buffer in the .bss segment.


If you look at the screenshot of the .bss segment, you’ll notice that the admin_flag is suspiciously right next to the global string buffer. If anything that wrote to the global string buffer were vulnerable to a buffer overflow, we would be able to change the admin_flag to any value we want. Unfortunately, the only function to write to this buffer is the read_into_global_thing function, and it caps out the length to 128 bytes.

Normally to save time, I avoid reversing any functions that are dedicated to things like handling user input. More often than not, they end up being boring boilerplate code to handle things like watching for newlines and resizing allocated memory. However, since this function was a little strange, I decided to reverse this one anyway. Upon further inspection, the bug became clear:

The code does something like this:

if (bytes_to_read > 128) {
    bytes_to_read = 128;

int bytes_read = 0;
while (1) {
    global_string_buf[bytes_read] = fgetc(stdin);
    if (global_string_buf[bytes_read] == '\n') {
        global_string_buf[bytes_read] = '\0';

    if (bytes_to_read > bytes_read) {


The problem with this is that the bounds check happens in the wrong place - it should be after the bytes_read variable is already updated, or before the write. Either way, we now have our first off-by-one vulnerability that lets us modify the the admin_flag variable in the .bss segment.

admin_flag is initialized to \x01\x00\x00\x00, or just 1. For us to be authenticated, it needs to be set to 0. We can do this by sending 129 bytes: 128*‘A’, and a newline. The newline will turn the \x01\x00\x00\x00 into \x0a\x00\x00\x00 (10), but the read_into_global_thing function replaces newlines with a null byte - satisfying the authentication requirement. Now, we just need to find a call to this function that takes 128 as the size parameter - or we won’t even be able to touch the next variable! Luckily for us, out of the four calls to the function, there is indeed one that is called with a size of 128 bytes:

In the “filters” functionality, the application reads the full 128 bytes, enough for us to trigger the off-by-one overwrite. Once we send 128 bytes and a newline at the end, the data ends up looking like this:

Now that we’re authenticated, let’s take a look at our options. Looking at the configuration menu, there are a few things we can modify: start/end ip, netmask, and nameserver - stuff you would expect from a DHCP server.

Taking a look at the DHCP Menu, however, there’s some functionality for renewing leases, which is implemented in a super shady way:

It takes our configuration options and passes them straight to system()! Anything that can be influenced by user input ending up in a system() call should always be investigated. Unfortunately “proper” validation is done, and the IP addresses can’t contain anything that isn’t 0-9 or a .. There are one byte overflows in every configuration option, but since the snprintf does proper bounds checking, we can’t combine them for a more useful buffer overflow.

While stuck looking for more vulnerabilities, something popped up into my head: we don’t know the contents of the ./renew_leases script! In a last ditch attempt to solve the challenge, I changed the nameserver to the server of my IP address, and left a UDP listener on port 53: nc -ul 53. Lo and behold!

$ nc -ul 53

Looks like an actual DNS lookup! To save time, I ended up running a DNS server, minidns, that resolves all names to the current IP. Now we went from attacking some DHCP controller to performing a man in the middle attack on some client in a fictitious network. To figure out more of what was going on, I ran an apache server that would respond to any requests, regardless of which host it was intended for. The first request I saw:

GET /yandsearch?text=scallops%20boston HTTP/1.1
Accept-Encoding: identity
Host: yandex.ru
X-Manufacturer: Lenovo
Connection: close
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20120101 Firefox/29.0

X-Manufacturer isn’t a standard header, or even a realistic one - it just seems like a reference to the Superfish controversy that had been making the headlines recently. To confirm this, another DNS lookup for my.bank had come through. The final step was very clear: get an SSL cert for my.bank signed with the leaked Superfish CA cert. Playing with OpenSSL isn’t fun, but at least there’s not much guessing left. Just in case, I tried to make most of the MITM by redirecting the client to other sites such as bostonkey.party to see if it had any credentials for them that I could scrape. Nothing came out of that.

After downloading the Superfish CA cert (via Reddit), I used OpenSSL to create a cert that’s valid for my.bank:

openssl genrsa -out ia.key 4096
openssl req -new -key ia.key -out ia.csr
openssl x509 -req -days 730 -in ia.csr -CA superfish.crt -CAkey superfish.key -set_serial 01 -out ia.crt

A few minutes later after running the renew lease command, this HTTP request came in: - - [28/Feb/2015:18:49:20 -0500] "GET /login/username=FLG-SIK9KSRBHIYUKNGEBXlKW3B7HS2I HTTP/1.1" 200 1939

And boom, we have the flag.