Hackthebox: MakeSense
Overview
MakeSense is a medium-difficulty machine from Hack The Box that is all about reading the application instead of fighting it. We start with a WordPress agency site running a custom webagency theme whose home-grown contact form stores our message and renders it back to an administrator unsanitised, giving us a stored XSS with an admin reader — which we use to mint ourselves a brand new administrator account. The theme editor is locked down with DISALLOW_FILE_EDIT, but plugin upload is not, so we ship a one-file plugin webshell and land code execution as www-data. From there, wp-config.php hands us the database password, which is reused for SSH as walter and gives us the user flag. Privilege escalation is the genuinely fun part: an internal OCR web app listening on 127.0.0.1:8001 runs as root and lets us save whatever text Tesseract reads out of an image to any filename we want. We render a PHP webshell as a PNG, let the OCR engine read it back, save the recognized text as shell.php, and the root-owned PHP server executes our own handwriting.

Reconnaissance
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
The usual SSH on 22, plus an Apache-backed HTTPS site on 443. The TLS certificate leaks the vhost makesense.htb, so into /etc/hosts it goes:
1 | echo "10.129.46.133 makesense.htb" | sudo tee -a /etc/hosts |
Web Application - https://makesense.htb/
Fingerprinting the site, we find a WordPress install — and more interestingly, the active theme is not something off the shelf:
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
A custom webagency theme is exactly where we want to look, since custom theme code is where the non-standard bugs live. The homepage is a typical agency landing page with a contact form (name / email / phone / message) and a “Call” feature, and the theme ships two JavaScript files:
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
A whisper wrapper on an agency site is the “unusual feature” of the box — there’s a whole voice-to-text pipeline for phone calls in there, including an encrypted save_voice_results flow. We’ll come back to the fancy stuff only if the boring path fails. First, the contact form.
Reading main.js
Looking at main.js, the contact form submits over the WordPress AJAX API:
1 | $('#contactForm').on('submit', function (e) { |
And the endpoint plus the nonce are localised straight into the page source, so no authentication is needed to submit:
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
So a submission is just an unauthenticated POST to admin-ajax.php with action=submit_contact_form.
Foothold — Stored XSS in the Contact Form
The question we always ask about a contact form is: does an admin read these messages in wp-admin, and is the message rendered raw? On this box, it is. The message field is stored and shown back to the administrator verbatim, which makes this a stored XSS with an admin viewer — meaning we can run JavaScript as the administrator.
The highest-value thing to do with that primitive is to mint ourselves a new administrator account. Our payload fetches /wp-admin/user-new.php (same origin, so the admin’s cookies ride along), scrapes the _wpnonce_create-user token out of the HTML, and POSTs a new user with the administrator role:
1 | <script> |
We drop that into the message field and fire it straight at the AJAX endpoint:
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
Now we just wait for the admin bot to open the submission. Rather than guessing, we poll the login endpoint until our new account actually works:
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
The admin viewed our message, our script ran with their session, and pwn is now a full administrator. We’re in wp-admin.
RCE as www-data — Plugin Upload
The obvious next move as a WordPress admin is the Theme File Editor, but this install has it locked down:
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
That’s DISALLOW_FILE_EDIT doing its job. Fine — administrators can still upload plugins, which is just as good for our purposes. We grab the upload nonce first:
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
Then we build a one-file plugin that is nothing but a webshell, zip it up, and push it through the uploader:
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
The plugin unpacks to wp-content/plugins/pwn/pwn.php, and we don’t even need to activate it — the file is directly reachable. We ring it:
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
Code execution as www-data.
User Flag — walter
WordPress keeps its database credentials in plaintext in wp-config.php, and this box doesn’t make us work for a password reuse:
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
Checking /etc/passwd shows local walter and admin accounts with /bin/bash, and the DB password is reused for walter‘s SSH:
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
And that’s the user flag.
Privilege Escalation — The OCR App That Types Out Our Shell
Looking at what’s listening locally as walter, something is bound to loopback only:
1 | walter@makesense:~$ ss -tlnp | grep 8001 |
That’s the “MakeSense” recognizer — a canvas web app where you sketch some text, it OCRs it with Tesseract, and then lets you save the recognized text to a filename of your choice. It sits behind HTTP Basic auth, and walter‘s password works there too. Critically, it runs as root:
1 | walter@makesense:~$ curl -s -u 'walter:JbhHDAEgXvri3!' \ |
So it’s PHP’s built-in development server, rooted at /root/ocr4/, running as root — and the “save” feature writes into /root/ocr4/saved/. There is no extension check and no content filter on the filename we supply. If we save the recognized text as shell.php, the root-owned PHP server will happily execute it the next time we request it. The recognizer is our arbitrary file-write primitive; all we have to do is feed it the right text.
Rendering a Webshell Tesseract Can Read
The catch is that the input is an image, so Tesseract has to read our PHP back exactly — one mangled quote or bracket and the saved file is broken PHP. The target has no PIL, but it does ship the DejaVu mono font, so we pull that font over and render the payload on our own box with an identical glyphset:
1 | # mkshell.py |
Before trusting the target’s OCR with it, we sanity-check the render locally against the same engine:
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
Character for character. Now we ship it over and drive the app entirely across the loopback interface. One detail worth calling out: the ocr_id returned after recognition is bound to the session cookie, so the recognize call and the save call must share a cookie jar — otherwise the save silently fails to find your OCR result.
1 | ┌──(kali㉿kali)-[~/Hackthebox/MakeSense] |
1 | walter@makesense:~$ AUTH="walter:JbhHDAEgXvri3!"; J=/tmp/j |
The root-owned server just wrote our handwriting to /root/ocr4/saved/shell.php.
Root Flag
All that’s left is to request the file we just drew, and watch PHP run it as root:
1 | walter@makesense:~$ curl -s -u "$AUTH" "http://localhost:8001/saved/shell.php?c=id" |
And that’s root. If you’d rather drive the whole thing from your own box instead of from the shell, just forward the port first — ssh -L 8001:127.0.0.1:8001 [email protected] — and run the exact same curls against localhost:8001.
Kill Chain
1 | Recon → WordPress + custom 'webagency' theme (contact form, whisper voice feature) |
Takeaways
- A contact form is an injection sink with an admin reader. Storing user input and rendering it into the wp-admin panel raw turns “leave us a message” into “run code in an administrator’s browser.” Escape on output, always.
DISALLOW_FILE_EDITis only half a control. It stopped the theme editor but not plugin upload — and a WordPress administrator is effectively RCE unless you also lock down plugin and theme installation withDISALLOW_FILE_MODS.- DB password = SSH password.
wp-config.phpis the first thing worth reading the moment you’rewww-data, and reusing that password for a shell account is the entire user step on this box. - The OCR app is the textbook “trusted output written to an executable location” bug. It runs as root, applies no extension or content check on the save filename, and serves its own docroot with
php -S— so an attacker-controlled image becomes an attacker-controlled root-executed file. The exploit isn’t in Tesseract at all; it’s in trusting whatever Tesseract reads and letting the caller name the output.php.
That was it for MakeSense, hope you learned something new!
-0xkujen
- Title: Hackthebox: MakeSense
- Author: Foued SAIDI
- Created at : 2026-11-02 18:42:19
- Updated at : 2026-11-02 20:54:36
- Link: https://kujen5.github.io/2026/11/02/Hackthebox-MakeSense/
- License: This work is licensed under CC BY-NC-SA 4.0.