Introduction
Modern web application security relies on authorization checks at every API boundary. When an application trusts client-side identity without verifying it on the backend, it opens the door to serious logic flaws.
This post walks through a multi-stage exploit chain from the Skill Assessment of HTB's Web Attacks module, part of the CEWS certification path. By combining an Insecure Direct Object Reference (IDOR) flaw with HTTP Verb Tampering, full Account Takeover (ATO) was achieved. From there, In-Band XXE injection with a PHP Stream Filter was used to extract server-side source code.
The Attack Chain at a Glance
- Initial Access: Logging in with the provided credentials and identifying the starting user context.
- API Enumeration: Finding an admin account and reset token through IDOR in the site's URL structure, which then exposed API endpoints.
- Account Takeover: Abusing loose cookie-to-request mapping to reset the administrator's password.
- Data Exfiltration: Using XXE injection to read server files via Base64 encoding.
Technical Deep-Dive
Phase 0: Initial Access
The Skill Assessment provided a starting point: authenticate htb-student with the password Academy_student!.
After logging in, the application landed on profile.php, showing a standard user dashboard for Paolo Perrone at Schaefer Inc.
Intercepting the profile page request in Burp Suite revealed that the application was already leaking the user's ID both in the URL and in a uid cookie. The API endpoint /api.php/user/74 confirmed this, returning the current user's details directly:
This was the first signal that user IDs were being used as direct object references with no additional validation, setting the stage for IDOR enumeration.
Phase 1: API Enumeration & Insecure Direct Object Reference (IDOR)
The application exposed user IDs directly in its URL structure, allowing any authenticated user to reference other users' resources by simply changing the ID in the URL. This led to the discovery of two sensitive API endpoints:
/api.php/user/${uid}/api.php/token/${uid}
The backend never checked whether the logged-in user had permission to access other users' profiles or tokens.
A simple Bash script iterated through user IDs to find interesting accounts:
#!/bin/bash
BASE_URL="http://154.57.164.71:30176/api.php/user"
for uid in {1..100}; do
response=$(curl -s -w "\n" "${BASE_URL}/${uid}")
if [ -n "$response" ]; then
echo "[*] User ID: ${uid} | ${response}"
fi
doneThe script found an admin account at ID 52:
{"uid":"52","username":"a.corrales","full_name":"Amor Corrales","company":"Administrator"}
A direct GET request to /api.php/token/52 then returned the admin's active reset token with no errors:
{"token":"e51a85fa-17ac-11ec-8e51-e78234eb7b0c"}
Phase 2: HTTP Verb Tampering & Account Takeover (ATO)
The goal was to use the stolen token against the password reset page at /reset.php.
Sending a standard application/json POST request returned a Missing parameters error right away.

Looking closer at the backend logic revealed the problem: the PHP code was checking permissions by comparing the uid cookie against the uid value in the request, essentially doing $_COOKIE['uid'] === $_REQUEST['uid'].
By sending the parameters as URL query strings instead of a POST body, and setting the cookie to uid=52, the authorization check passed completely:
POST /reset.php?uid=52&token=e51a85fa-17ac-11ec-8e51-e78234eb7b0c&password=a HTTP/1.1
Host: 154.57.164.71:30176
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: */*
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=16vrs65ncrfrgv51r7kiogl864; uid=52
Content-Length: 0
Connection: closeThe server returned 200 OK with:
Password changed successfully
Logging in as a.corrales / a gave full access to the admin panel.


Phase 3: In-Band XXE Injection
With admin access, the focus shifted to /event.php, which contains a form for creating new events. Before injecting anything malicious, a legitimate request was sent first to understand how the form data is transmitted to the backend.

Intercepting the request in Burp Suite revealed that the form data is sent as raw XML to /addEvent.php, and the event name field is reflected back in the response as "Event 'a' has been created." This confirmed that whatever ends up in the <name> tag gets reflected on the page, making it the injection point

The next step was to verify that the parser actually processes XML entities before attempting anything more aggressive. A simple internal entity was defined and referenced in the <name> tag:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE name [
<!ENTITY company "Inlane Freight">
]>
<root>
<name>&company;</name>
<details>aaa</details>
<date>2026-05-20</date>
</root>
The response returned "Event 'Inlane Freight' has been created", confirming the parser resolves entities and reflects them. With that confirmed, the next step was testing whether external entities work as well, by pointing one at a known local file: