Dreaming
NOTE: My IP address will be different from yours!
Lab Info
| Detail | Info |
|---|---|
| Room | Dreaming |
| Platform | TryHackMe |
| Difficulty | Easy — Intermediate |
| Type | CTF / Web Exploitation + Privilege Escalation |
| Knowledge Required | Web, Linux, MySQL, Python |
Tools Used
Nmap — Port scanning and service detection
Gobuster — Web directory enumeration
Exploit-DB — CVE lookup (CVE-2020-29607)
Python exploit script (EDB-49909) — Pluck CMS authenticated RCE
p0wny webshell — Remote code execution via browser
MySQL client — Database enumeration and command injection
Recon
As always, I start with Nmap before touching anything else.
nmap -T4 -Pn -A 10.49.160.69
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu
80/tcp open http Apache/2.4.41 (Ubuntu)
Two ports open — SSH and HTTP. The web server returned the default Apache Ubuntu page, which means nothing is served at the root. Time to fuzz for directories.
gobuster dir -u http://10.49.160.69 -w /usr/share/seclists/Discovery/Web-Content/common.txt
Gobuster found one interesting result: /app (Status: 301). Navigating to it showed a directory listing with a single folder: pluck-4.7.13/.
Identifying the CMS
Clicking into /app/pluck-4.7.13/ loaded a site called "dreaming" running Pluck CMS. Checking the page source confirmed it: a meta generator tag showing pluck 4.7.13, and in the footer, a link pointing directly to the admin login page at /app/pluck-4.7.13/login.php.
Before trying anything fancy, I checked Exploit-DB for known vulnerabilities in this version.
Initial Foothold — CVE-2020-29607
Exploit-DB had exactly what I needed: EDB-49909, a File Upload Remote Code Execution vulnerability in Pluck CMS 4.7.13 (CVE-2020-29607). It's an authenticated exploit, meaning I need valid credentials first.
I tried the most obvious password possible: password. It worked.
With admin access confirmed, I ran the Python exploit script:
python3 exploit.py 10.49.160.69 80 password /app/pluck-4.7.13
The script authenticated, uploaded a PHP webshell (shell.phar) through the "manage files" functionality, and printed its location:
Uploaded Webshell to: http://10.49.160.69:80/app/pluck-4.7.13/files/shell.phar
Navigating to that URL gave me a p0wny webshell running as www-data.
Flag 1 — Lucien
From the webshell, I listed /home and found four users: death, lucien, morpheus, ubuntu.
Trying to read any of their flag files directly as www-data returned Permission denied. So I searched for files owned by lucien:
find / -type f -user lucien 2>/dev/null
One result stood out immediately: /opt/test.py
Reading it revealed a Python script that automates logging into the Pluck CMS — and it had lucien's password hardcoded in plaintext.
I SSHed in as lucien using that password and got the first flag:
cat lucien_flag.txt
THM{REDACTED}
Flag 2 — Death (SQL Injection → Command Injection)
This is where it gets interesting.
As lucien, I navigated to /home/death. The flag file and getDreams.py were both permission denied. But sudo -l showed something useful:
(death) NOPASSWD: /usr/bin/python3 /home/death/getDreams.py
Lucien can run getDreams.py as death, but can't read it. Fortunately, there was a readable copy sitting in /opt/getDreams.py. Reading it showed a script that connects to a local MySQL database (library), pulls rows from the dreams table, and passes the dreamer and dream values into a subprocess call — with zero sanitization.
I then checked lucien's .bash_history and found the MySQL credentials sitting there in plaintext.
mysql -u lucien -p[REDACTED]
use library;
select * from dreams;
Found 4 rows (Alice, Bob, Carol, Dave with their dreams). The script echoed each row in the format dreamer + dream. Since this gets passed unsanitized to subprocess, I could inject shell commands through the dreamer field.
First I tested it:
INSERT INTO dreams (dreamer, dream) values ('whoami | bash', '-l');
sudo -u death /usr/bin/python3 /home/death/getDreams.py
Output printed death alongside the other rows. Command injection confirmed.
Then I used the same trick to read the actual getDreams.py in death's home directory (the one we couldn't read before):
INSERT INTO dreams (dreamer, dream) values ('cat /home/death/getDreams.py | bash', '-l');
Ran the script again. This time it dumped the file contents as output — including death's password in plaintext.
su death
cat /home/death/death_flag.txt
THM{REDACTED}
Flag 3 — Morpheus (Python Library Hijacking)
As death, I moved to /home/morpheus. Three files: kingdom, restore.py, and morpheus_flag.txt (Permission denied).
Reading restore.py:
from shutil import copy2 as backup
src_file = "/home/morpheus/kingdom"
dst_file = "/kingdom_backup/kingdom"
backup(src_file, dst_file)
print("The kingdom backup has been done!")
It imports copy2 from the shutil Python library and runs a backup. This script is clearly being executed on a schedule by a cron job running as morpheus.
I checked the actual shutil.py library file:
find / -type f -name shutil.py 2>/dev/null
ls -la /usr/lib/python3.8/shutil.py
-rw-rw-r-- 1 root death 51474 Mar 18 2025 /usr/lib/python3.8/shutil.py
The file is owned by root but the death group has write permission. Since I'm currently death, I can edit the library itself — this is Python library hijacking.
I injected a line into the copy2 function inside shutil.py:
os.system('chmod 777 /home/morpheus/morpheus_flag.txt')
(Had to set TERM=xterm-256color first because nano was throwing a terminal error.)
Then I waited. Within a minute, the cron job triggered restore.py, which imported shutil, which executed my injected line as morpheus, which changed the flag file's permissions.
cat morpheus_flag.txt
THM{REDACTED}
Flag Summary
| Flag | Method |
|---|---|
| Lucien | Hardcoded credentials in /opt/test.py |
| Death | SQL injection → command injection via getDreams.py |
| Morpheus | Python library hijacking via writable shutil.py + cron job |
Key Takeaways
1. Default passwords are never a joke The CMS admin password was literally password. Always change default credentials before deploying anything.
2. Never hardcode credentials Both lucien's password (in /opt/test.py) and death's password (inside getDreams.py) were stored in plaintext inside readable script files. Credentials belong in environment variables or a secrets manager, not source code.
3. Sanitize database input before using it in shell commands The getDreams.py script fetched unsanitized data from a database and passed it to subprocess. This created a SQL → OS command injection chain. If the input had been sanitized, the entire path to death's account would have been blocked.
4. Python library permissions matter A writable Python library file is a privilege escalation waiting to happen — especially when a higher-privileged cron job imports it. Standard library files should never be group-writable.
5. bash_history leaks more than you think MySQL credentials and navigation patterns were sitting in .bash_history. In a real engagement, this file alone can save hours of enumeration.
Day 2 of 30 ✅ — See you tomorrow.