Exploiting server-side parameter pollution in a query string | 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 query string! 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 login as an user by entering the username and password field and click the "Log in" button. We can also go to the "Forgot password?" link to reset an account's password:

We can try to enter a random username and click the "Submit" button:


Burp Suite HTTP history:

When we clicked the "Submit" button, it'll send a POST request to /forgot-password with parameter csrf and username.
Now, what if we enter a valid username, like administrator?


Hmm… Looks like it'll send an email to the account's email address.
In the /forgot-password page, it also loaded a JavaScript file:
<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?reset_token=${resetToken}`;
}
else
{
const forgotPasswordBtn = document.getElementById("forgot-password-btn");
forgotPasswordBtn.addEventListener("click", displayMsg);
}
});
When the DOM (Document Object Model) has fully loaded, it'll retrieve our GET parameter reset-token's value. If that parameter's value exist, it'll redirect us to /forgot-password?reset_token=<resetToken>.
If we try to send a GET request with an invalid reset token at /forgot-password?reset_token=<resetToken>, it'll response "Invalid token":

Hmm… I wonder how the reset token checking works…
It could be retrieve the token and validate it with a SQL query. Or, using an internal API to validate the token. Let's try to test the latter.
Exploitation
Some systems contain internal APIs that aren't directly accessible from the internet. Server-side parameter pollution occurs when a website embeds user input in a server-side request to an internal API without adequate encoding. This means that an attacker may be able to manipulate or inject parameters, which may enable them to, for example:
- Override existing parameters.
- Modify the application behavior.
- Access unauthorized data.
We can test any user input for any kind of parameter pollution. For example, query parameters, form fields, headers, and URL path parameters may all be vulnerable.
In our case, we want to test the reset_token parameter is whether vulnerable to server-side parameter pollution or not.
To test for server-side parameter pollution in the query string, place query syntax characters like #, &, and = in our input and observe how the application responds.
We can try to make an education guess about how our reset_token parameter is being parsed.
When we try to reset an account's password, our browser sends the following request:
GET /forgot-password?reset_token=0123456789abcdef
To validate the reset_token, the server queries an internal API with the following request:
GET /api/resetPassword?reset_token=0123456789abcdef&username=wiener
- Truncating query strings
We can use a URL-encoded # character to attempt to truncate the server-side request. To help us interpret the response, we could also add a string after the # character.
For example, we could modify the query string to the following:
GET /forgot-password?reset_token=0123456789abcdef%23foo
The front-end will try to access the following URL:
GET /api/resetPassword?reset_token=0123456789abcdef#foo&username=wiener
Note
It's essential that we URL-encode the
#character. Otherwise the front-end application will interpret it as a fragment identifier and it won't be passed to the internal API.
- Injecting invalid parameters
We can use an URL-encoded & character to attempt to add a second parameter to the server-side request.
For example, we could modify the query string to the following:
GET /forgot-password?reset_token=0123456789abcdef%26foo=xyz
This results in the following server-side request to the internal API:
GET /api/resetPassword?reset_token=0123456789abcdef&foo=xyz&username=wiener
- Injecting valid parameters
If we're able to modify the query string, we can then attempt to add a second valid parameter to the server-side request.
For example, if we've identified the email parameter, we could add it to the query string as follows:
GET /forgot-password?reset_token=0123456789abcdef%26email=foo
This results in the following server-side request to the internal API:
GET /api/resetPassword?reset_token=0123456789abcdef&email=foo&username=wiener
- Overriding existing parameters
To confirm whether the application is vulnerable to server-side parameter pollution, we could try to override the original parameter. Do this by injecting a second parameter with the same name.
For example, we could modify the query string to the following:
GET /forgot-password?reset_token=0123456789abcdef%26username=peter
This results in the following server-side request to the internal API:
GET /api/resetPassword?reset_token=0123456789abcdef&username=carlos&username=wiener
The internal API interprets two username parameters. The impact of this depends on how the application processes the second parameter. This varies across different web technologies. For example:
- PHP parses the last parameter only. This would result in a user search for
carlos. - ASP.NET combines both parameters. This would result in a user search for
peter,carlos, which might result in anInvalid usernameerror message. - Node.js / express parses the first parameter only. This would result in a user search for
peter, giving an unchanged result.
If we're able to override the original parameter, you may be able to conduct an exploit. For example, we could add name=administrator to the request. This may enable you to log in as the administrator user.
After some trials and errors, I found that the reset_token doesn't have any changes when I did the above testing.
Instead, the POST request to /forgot-password has some changes.
Normal response:

After truncating query strings via #:

Hmm… Field not specified.??
That being said, when we send this POST request:
POST /forgot-password HTTP/2
csrf=ggIz6CxBvVugiFQkf34MG3MC6Zcvj8AG&username=foobar%23
The server-side request to the internal API might be this:
GET /api/user?username=foobar#&otherparameter=blah
That being said, the username parameter is parsed to the internal API.
Also, by injecting invalid parameters, we can get error Parameter is not supported.:
POST /forgot-password HTTP/2
csrf=ggIz6CxBvVugiFQkf34MG3MC6Zcvj8AG&username=foobar%26foo=xyz

Which means the internal API interpreted &foo=xyz as a separate parameter.
After fumbling around, I found that the field is a parameter name! This parameter's value may refer to other parameter name.
Let's try username:
POST /forgot-password HTTP/2
csrf=ggIz6CxBvVugiFQkf34MG3MC6Zcvj8AG&username=administrator%26field=username

Ah ha! I started to understand this! The field in the internal API means return a specific object's attribute (field)'s value!
In our case, we injected the field to be username and caused the internal API returned the user's object attribute username's value!
Hmm… Did you recall the reset_token parameter name?
What if the internal API return user administrator's reset token??
POST /forgot-password HTTP/2
csrf=ggIz6CxBvVugiFQkf34MG3MC6Zcvj8AG&username=administrator%26field=reset_token

Let's go!!! We got user administrator's reset token (qb7tt2w2b1ooq4xrlxlpopbwwby0dgb4)!!
Finally, we can send a GET request to /forgot-password with parameter reset_token=qb7tt2w2b1ooq4xrlxlpopbwwby0dgb4:
GET /forgot-password?reset_token=qb7tt2w2b1ooq4xrlxlpopbwwby0dgb4 HTTP/2

Nice! We can now reset administrator's password!

Then login as administrator:


We're in! Let's go to the "Admin panel" and delete user carlos:


Conclusion
What we've learned:
- Exploiting server-side parameter pollution in a query string