Skip to content
Back to all posts

HTB: Jail

· 24 min insane Linux Jail

A stack buffer overflow with socket-reuse shellcode, NFS SUID escalation via raw syscall assembly, an rvim Python escape, and PwnKit combine for a four-stage privilege escalation on CentOS 7.

Overview

Jail is an Insane-rated Linux machine running CentOS 7 with kernel 3.10.0-514. It exposes six ports, including a custom binary (jail) on port 7411 that implements a simple authentication protocol with a textbook stack buffer overflow. The exploit development is where the difficulty lies: the attacker IP contains 0x0a (decimal 10, newline), which is the delimiter character for the protocol. Standard reverse shell shellcode embeds the IP address and gets truncated. The solution is socket-reuse shellcode that redirects stdin/stdout/ stderr to the existing socket file descriptor.

Privilege escalation spans three users across four techniques. NFS with no_all_squash and no nosuid flag allows writing a SUID binary as frank (uid 1000) from the attacker’s machine. Static binary compilation with raw syscalls avoids glibc version mismatches between the attacker (glibc 2.34+) and target (glibc 2.17). From frank, a restricted vim (rvim) escape via Python’s :py command (available in CentOS 7’s vim build) provides access as adm. Finally, PwnKit (CVE-2021-4034) exploits a memory corruption in pkexec when invoked with argc=0 to achieve root.

Reconnaissance

nmap -sC -sV -T4 10.129.13.13
PortServiceProduct / VersionNotes
22SSHOpenSSH 6.6.1CentOS 7
80HTTPApache 2.4.6 (CentOS)ASCII art jail cell
111rpcbind
2049NFSTwo exported shares
7411jailCustom binaryAuthentication service
20048mountd

NFS exports:

/opt           (rw,sync,root_squash,no_all_squash)
/var/nfsshare  (rw,sync,root_squash,no_all_squash)

The no_all_squash option preserves UIDs from the mounting client. Combined with no nosuid flag on the export, this allows SUID binary creation that executes as the mapped user on the target.

Attack Surface Analysis

Port 80 serves ASCII art. Directory enumeration finds /jailuser/dev/ containing three files: jail (32-bit ELF), jail.c (source code), and compile.sh (build script). The compile script reveals -z execstack, making the stack executable.

The jail binary implements a simple protocol: DEBUG enables debug mode, USER admin sets the username, PASS <password> authenticates (hardcoded password: 1974jailbreak!). The vulnerability sits in the auth() function:

int auth(char *username, char *password) {
    char userpass[16];
    strcpy(userpass, password);  // No bounds check on 16-byte buffer
}

The DEBUG command prints the exact address of userpass, confirming ASLR is disabled: Debug: userpass buffer @ 0xffffd610.

Vulnerability Analysis

Buffer overflow in jail binary

AttributeValue
TypeStack buffer overflow
CWECWE-120 (Buffer Copy without Checking Size)
Buffer size16 bytes
Offset to EIP28 bytes
ASLRDisabled (exact address leaked via DEBUG)
NXDisabled (-z execstack)
CanaryAbsent

Bad byte problem

The attacker IP 10.10.14.84 contains 0x0a (decimal 10, ASCII newline). The jail binary uses strtok with \n as delimiter, so any shellcode containing 0x0a bytes is truncated. This rules out standard reverse shell shellcode that embeds the IP address directly.

CVE-2021-4034: PwnKit

AttributeValue
CVECVE-2021-4034
Componentpolkit pkexec 0.112
TypeMemory corruption via argc=0
PrerequisiteAny local user account

When pkexec is invoked with an empty argv array, it reads argv[1] from envp[0] (out-of-bounds). The resolution through g_find_program_in_path() writes back to the same memory location, overwriting envp[0] and injecting a GCONV_PATH environment variable. Error message handling loads a gconv module from the attacker-controlled path, executing a malicious shared library constructor as root.

Exploitation

Step 1: Socket-reuse shellcode for nobody shell

Instead of a reverse shell, the exploit uses 33 bytes of shellcode that reuses the existing socket connection (file descriptor 4):

  1. dup2(4, 0): redirect stdin to the socket
  2. dup2(4, 1): redirect stdout to the socket
  3. dup2(4, 2): redirect stderr to the socket
  4. execve("/bin/sh", NULL, NULL): spawn shell

The exploit payload:

buf_addr = 0xffffd610
ret_addr = buf_addr + 28 + 4 + 16  # 0xffffd640
payload = b"PASS " + b"A"*28 + struct.pack("<I", ret_addr) \
        + b"\x90"*64 + shellcode + b"\n"

The protocol sequence: connect, send DEBUG, send USER admin, send the overflow payload as PASS. The return address points into the NOP sled. Shell as nobody (uid 99).

Step 2: NFS SUID escalation to frank

/var/nfsshare has mode 0731 with GID 1000. On the target, UID 1000 is frank. Because no_all_squash preserves UIDs and no nosuid flag is set, I create a SUID binary on the NFS share from the attacker machine.

The binary must avoid glibc version mismatches. I use raw x86_64 syscalls with no libc dependency:

.global _start
_start:
    # setresgid(1000, 1000, 1000)
    mov $119, %rax
    mov $1000, %rdi
    mov $1000, %rsi
    mov $1000, %rdx
    syscall
    # setresuid(1000, 1000, 1000)
    mov $117, %rax
    # ... same args ...
    syscall
    # execve("/bin/bash", ["/bin/bash", "/var/nfsshare/cmd.sh", NULL], NULL)

Compiled with gcc -nostdlib -static, placed on the NFS share with chmod 4755. The SUID binary executes a script that writes an ED25519 SSH key to frank’s .ssh/authorized_keys. RSA keys fail because OpenSSH 6.6.1 does not support RSA-SHA2.

ssh -i jail_frank [email protected]

User flag obtained.

Step 3: rvim Python escape to adm

Frank has a sudo rule:

(adm) NOPASSWD: /usr/bin/rvim /var/www/html/jailuser/dev/jail.c

Restricted vim (rvim) blocks :!, :shell, and -S. But CentOS 7’s vim build includes +python/dyn (dynamic Python 2 support). The :py command works in rvim on this version:

:py import os; os.execvp('/var/nfsshare/adm_enum.sh', ['sh'])

Automating this requires handling two interactive prompts (swap file and TERM warning) via PTY-based keystroke injection.

Step 4: PwnKit for root

The adm user itself is a dead end (uid 3, gid 4, matches only other::--- on critical ACLs). But frank can use PwnKit directly.

The exploit creates a directory structure matching the write-back path:

./GCONV_PATH=./           directory searched by PATH
./GCONV_PATH=./evildir    fake executable found by g_find_program_in_path
./evildir/                 directory pointed to by GCONV_PATH
./evildir/gconv-modules    gconv configuration loading pwnkit module
./evildir/pwnkit.so        malicious shared library

The shared library constructor calls setuid(0) and copies bash to /tmp/rootbash with SUID. All commands must use absolute paths because PATH is corrupted to GCONV_PATH=. during exploitation.

/tmp/rootbash -p -c 'cat /root/root.txt'
# [flag redacted]

Root flag obtained.

Post-Exploitation

CentOS 7 with kernel 3.10.0-514, SELinux enforcing (targeted policy) with unconfined_t context. Eleven dead-end approaches were investigated:

  • DirtyCow (kernel patched at 327, this is 514)
  • NFS root_squash bypass (UID 0 mapped to nfsnobody)
  • suexec capabilities (cap_setuid+ep but requires apache group)
  • CGI/PHP webshell deployment (write-protected directories)
  • /opt/logreader modification via NFS or as adm (ACL blocks both)
  • Cron job hijack (no root crontab)
  • jail.c backdoor (writable by adm but no compilation trigger)

The most instructive dead end is the /opt/logreader/ ACL analysis. The directory has POSIX ACLs granting frank r-x via named user entry, root full access via owner, and other::---. The adm user (uid 3, gid 4) matches only the other entry and has zero access.

Defensive Analysis

PhaseMITRE ATT&CKDetection
Initial AccessT1190 Exploit Public-Facing AppBuffer overflow on custom jail binary (port 7411)
ExecutionT1059.004 Unix Shell/bin/sh spawned via socket-reuse shellcode
Privilege EscalationT1548 Abuse Elevation ControlNFS SUID binary creation via no_all_squash
Privilege EscalationT1059.006 Pythonrvim :py escape to execute commands as adm
Privilege EscalationT1068 Exploitation for Priv EscPwnKit CVE-2021-4034 pkexec argc=0
PersistenceT1098.004 SSH Authorized KeysED25519 key written to frank’s authorized_keys
Credential AccessT1552.001 Credentials in FilesHardcoded credentials in jail binary source

The custom binary on port 7411 is the primary attack surface. Process monitoring should flag /bin/sh spawned as a child of the jail process. NFS share access logging would detect the SUID binary creation. For PwnKit, monitoring execve calls to pkexec with argc=0 (via auditd or eBPF) is the definitive detection.

Remediation

PriorityActionEffortImpact
P0Upgrade polkit to version patched for CVE-2021-4034LowCritical
P0Add nosuid option to NFS exportsLowCritical
P0Fix buffer overflow in jail binary (use strncpy with bounds check)LowCritical
P1Remove source code and binaries from web-accessible directoryLowHigh
P1Remove hardcoded credentials from jail binaryLowHigh
P1Upgrade OpenSSH to current versionMediumHigh
P2Enable ASLR and compile with stack canaries and NXLowMedium
P2Restrict rvim Python support (compile vim without +python)MediumMedium
P3Migrate from CentOS 7 to a supported distributionHighHigh

The NFS misconfiguration is the most impactful single fix. Adding nosuid to both exports eliminates the SUID binary escalation path entirely, regardless of the no_all_squash setting. The PwnKit fix is equally critical; polkit should be upgraded on every CentOS 7 system that has not already applied the patch.

Key Takeaways

  1. Bad byte analysis is essential for exploit development. The 0x0a byte in the attacker IP forced a fundamentally different shellcode strategy. Socket-reuse shellcode avoids embedding the IP entirely. Always check the protocol delimiter against payload bytes before writing shellcode.

  2. NFS misconfigurations compound. no_all_squash alone is not exploitable without the missing nosuid flag. Both conditions must hold for SUID binary escalation. Cross-compilation with raw syscalls (-nostdlib -static) eliminates glibc version mismatches between attacker and target.

  3. rvim restrictions are version-dependent. CentOS 7’s vim build includes dynamic Python support that remains functional in restricted mode. Modern vim versions block :py in rvim. Always test the specific binary on the target rather than assuming restriction effectiveness from documentation.

  4. PwnKit requires absolute paths in the payload. PATH is corrupted to GCONV_PATH=. during the exploit. Every command in the shared library constructor must use absolute paths (/bin/cp, /bin/chmod), or they fail silently.