Jail — HackTheBox Machine Writeup
Nmap identifies port 7411 (custom jail daemon) and HTTP port 80. An open directory listing at /jailuser/dev/ leaks the full jail.c source code, compiled 32-bit binary, and compile.sh revealing an executable stack (-z execstack).
The jail daemon features a DEBUG command leaking the userpass[16] stack address and an unsafe strcpy(). Exploiting the 28-byte offset to EIP with a custom 27-byte Linux x86 dup2(1,0)+execve shellcode yields a shell as nobody.
/etc/exports exposes /var/nfsshare with no_all_squash. Mounting locally as UID 1000 (kali) and writing a statically linked C binary with setresuid(1000, 1000, 1000) and SUID permissions executes as frank on target.
frank has sudo permissions to run /usr/bin/rvim on jail.c as adm. While :! and :sh are blocked in restricted mode, rvim embeds Python (+python/dyn); executing :py import os; os.execl('/bin/sh','sh') spawns a shell as adm.
Adm's hidden keys directory holds an Atbash-encoded clue referencing Frank Morris's 1962 Alcatraz escape. Cracking keys.rar with Morris1962! extracts a 512-bit RSA public key; applying Wiener's small-d attack recovers the private key for root SSH login.
Attached Exploit Toolkit
All custom exploit scripts, helper utilities, and diagnostics developed for this machine are attached below. Click any script to launch the Interactive Code Explorer to inspect syntax-highlighted code, search with Ctrl+F, copy, or download the files directly.
Jail Exploit Toolkit (/autosolve/Jail_scripts)
5 FilesStandalone buffer overflow exploits, rvim PTY escape bridges, and diagnostic helpers used in this writeup.
32-bit x86 stack buffer overflow exploit with DEBUG pointer leak and custom 27-byte socket-reuse shellcode (dup2 + execve).
PTY-driven sudo rvim escape handling swap-file dialogues and injecting the :py embedded Python escape payload.
Direct pwntools-based PTY wrapper to execute commands as adm through the sudo /usr/bin/rvim rule.
Diagnostic script verifying stack address stability and fork() memory persistence across concurrent TCP sessions.
Full autonomous solver pipeline executing the complete 5-stage attack chain from reconnaissance to root flag recovery.
Reconnaissance — The Six Gates
::Port scan
The box answers on six TCP ports:
| Port | Service | Notes |
|---|---|---|
| 22 | SSH | OpenSSH 6.6.1 (CentOS-era) |
| 80 | HTTP | Apache 2.4.6 (CentOS) |
| 111 | rpcbind | NFS support |
| 2049 | NFS | exported shares |
| 7411 | daqstream | custom service — "OK Ready. Send USER command." |
| 20048 | mountd | NFS mount daemon |
MTU blackhole: the web server headers arrived but the 2 KB body never did — a classic path-MTU problem through the VPN. Clamping the MSS on the route fixed it instantly:
::The website
Port 80 serves a single page of ASCII art — a jail cell. Nothing dynamic.
But a directory listing at /jailuser/dev/ leaks the source code of the
service running on port 7411, the compiled binary, and the build script
compile.sh is the first big hint:
Two words that change everything: -m32 (32-bit binary) and
-z execstack (the stack is executable — DEP is off). On a 32-bit box
with an executable stack, a classic stack overflow becomes trivially
exploitable.
::
Both shares are exported to everyone. We'll come back to these — the
/var/nfsshare one is the key to the next user.
The Key Under the Mat
The jail.c source is short. Two functions matter:
Three things stand out:
- ▹Hardcoded credentials:
admin/1974jailbreak!. - ▹
DEBUGmode leaks the stack address ofuserpass— a gift for exploit development. - ▹
strcpy(userpass, password)with a 16-byte destination and no length check — the classic overflow. The password comes straight from thePASStoken we send to the service.
The service runs on port 7411 and speaks a tiny FTP-like protocol
Protocol order matters: DEBUG must be sent after USER and before the PASS that triggers auth(). The daemon processes DEBUG between the USER and PASS commands. Once enabled, every subsequent call to auth() will print the stack leak.
The leaked address 0xffffd610 is identical across connections — the
fork()-per-connection model gives every child the same stack layout, so a
leak in one connection is valid in the next. No ASLR drama.
Breaking Out — The Buffer Overflow
Live simulation of jail.c auth() buffer overflow & EIP hijack
Clean memory layout inside auth() before password input
When auth() is called, the CPU allocates a 16-byte userpass buffer and pushes the saved frame pointer (EBP) and saved return address (EIP) onto the stack. Saved EIP points back to handle() at 0x080489b0.
[ No Input Sent Yet ]::Understanding the Vulnerability (The "Why")
In C, when a function creates local variables—like char userpass[16] inside auth()—it allocates that memory on the call stack.
When auth() is called, the CPU also stores critical bookkeeping data on the stack so it knows where to resume execution when the function returns:
The vulnerability is inside auth():
strcpy() copies characters from our password into userpass without checking length. Because userpass is only 16 bytes:
- ▹Bytes
0-15filluserpass[16]. - ▹Bytes
16-19overwritechar *response. - ▹Bytes
20-23overwrite the savedEBP. - ▹Bytes
24-27(at offset 28 bytes) overwrite the Saved Return Address (EIP).
When auth() finishes and executes the ret (return) instruction, the CPU pops whatever value is in Saved EIP into the instruction pointer and jumps there. By overwriting this address, we hijack execution flow.
::Verifying the EIP Offset with GDB
Instead of guessing the offset, we verify it dynamically using a cyclic pattern (De Bruijn sequence) in GDB:
The offset is confirmed: exactly 28 bytes of padding reaches the return address.
::Crafting the Shellcode (Executable Stack)
Because compile.sh used -z execstack, the operating system leaves the stack memory marked as executable (RWX). We don't need complex ROP gadgets—we can place raw machine instructions (shellcode) directly on the stack and tell EIP to jump to them.
Our shellcode must satisfy three conditions:
- ▹Zero Null Bytes (
\x00): Functions likestrcpy()andsscanf()terminate upon encountering\x00. A null byte would truncate our payload before reaching the stack. - ▹Socket Redirection: The server's
handle()function already duplicatedSTDOUT(fd 1) andSTDERR(fd 2) to the network socket, but notSTDIN(fd 0). We calldup2(1, 0)so whatever we send over the network goes straight to the shell's input. - ▹Spawn
/bin/sh: Callexecve("/bin//sh", NULL, NULL)using the Linux x86 syscall interface (int 0x80).
Assembly breakdown:
The resulting 27-byte null-free machine code:
::Exploit Payload Layout & Execution
Our payload is constructed in three parts:
- ▹Why
base + 32?baseis the leaked address ofuserpass(+0x00). The return address sits at+0x1C(28 bytes) and takes 4 bytes. The shellcode immediately follows at+0x20(32 bytes). Setting EIP tobase + 32jumps straight into our shellcode.
The exploit runs in two phases:
- ▹Connection 1: Send
DEBUG,USER admin, andPASS testto leak the stack address ofuserpass(e.g.,0xffffd610). - ▹Connection 2: Send
USER adminandPASS <payload>to trigger the overflow and spawn the shell.
Running the exploit:
32-bit x86 stack buffer overflow exploit with DEBUG pointer leak and custom 27-byte socket-reuse shellcode (dup2 + execve).
Why it works even if the password is wrong: auth() returns right after strcpy() completes. The overwritten return address is popped into EIP the moment the function returns, regardless of whether strcmp(userpass, "1974jailbreak!") matched.
The First Transfer — nobody → frank
We're nobody, CentOS 7.3, SELinux enforcing. Enumeration time:
The interesting bit is the NFS option no_all_squash. With
root_squash, the root user gets mapped to nobody — but every other uid
passes through unchanged. Local uid 1000 on our Kali box is kali; on Jail,
uid 1000 is frank. Anything we write to the share as uid 1000 is owned
by frank on the remote side. And the SUID bit survives the trip.
So the plan: compile a tiny helper that calls setresuid(1000,1000,1000)
and spawns a shell, drop it on the share as the kali user, set it SUID,
and run it from the nobody shell.
Compiled statically — the remote CentOS 7 glibc (2.17) is far older than
Kali's, and a dynamic binary would refuse to run with GLIBC_x not found.
Why setresuid matters: a SUID binary owned by frank gives us euid
1000, but the real uid is still 99 — and bash drops privileges when
ruid ≠ euid. Setting all three ids to 1000 keeps the shell fully frank.
I installed my SSH key as frank for a stable foothold, then claimed the first flag:
The Guard's Terminal — frank → adm
As frank, sudo -l reveals a second sudo rule:
We can open that one file in rvim — restricted vim — as adm. The
restriction is real: :! shell commands and :sh are blocked. But rvim
still embeds interpreters. This vim was built with +python/dyn, and vim
script can hand the whole process to Python:
One line, and the vim process becomes a plain shell running as adm.
PTY-driven sudo rvim escape handling swap-file dialogues and injecting the :py embedded Python escape payload.
/var/adm is adm's home (the box uses a nonstandard home dir) and it
contains a hidden .keys directory — the road to root.
The Warden's Vault — adm → root
::The note and the cipher
The second file is Atbash — the alphabet mirrored onto itself (A↔Z, B↔Y, ...). A tiny decoder reveals the message:
"Hahaha! Nobody will guess my new password! Only a few lucky souls have Escaped from Alcatraz alive like I did!!!"
The user is frank — Frank Morris escaped Alcatraz in 1962. So the
password is Morris + 4 digits + a symbol.
::The encrypted archive
keys.rar holds rootauthorizedsshkey.pub (root's authorized SSH key) and
it's password-protected. We pull it to our box and attack it.
The password shape is exactly Morris?d?d?d?d?s — a hashcat mask over
10 digit × ~30 symbol combinations (~330k candidates). But the intended
answer is guessable directly: Morris1962! (escape year + !).
For the general case the same candidate space is a one-liner:
hashcat --stdout -a 3 "Morris?d?d?d?d?s" → feed the wordlist to
hashcat -m 23800 with a rar2john hash. On this box the structured
guess saved the GPU.
::Recovering the private key
The extracted public key is a 512-bit RSA key. RsaCtfTool runs its whole
battery of attacks and lands on the Wiener attack — a small private
exponent d, which can be recovered from n and e alone via continued
fractions:
::Root
The box runs OpenSSH 6.6.1, which only offers SHA-1 RSA signatures — modern clients refuse them by default, so the key needs the legacy algorithms re-enabled:
References

Red Team Consultant · Penetration Tester · Bug Bounty Hunter
Offensive security professional with 250+ vulnerabilities reported across 50+ organizations including Atlassian, Vimeo, and AT&T. Sharing research, tools, and field notes.