API testing module - Web Security Academy - PortSwigger
General Information
APIs (Application Programming Interfaces) enable software systems and applications to communicate and share data. API testing is important as vulnerabilities in APIs may undermine core aspects of a website’s confidentiality, integrity, and availability. All dynamic websites are composed of APIs, so classic web vulnerabilities like SQL injection could be classed as API testing. In this topic, we’ll teach you how to test APIs that aren’t fully used by the website front-end, with a focus on RESTful and JSON APIs. We’ll also teach you how to test for server-side parameter pollution vulnerabilities that may impact internal APIs.
API recon
Recon, which stands for reconnaissance, is a step to find out as much information about the API in website as possible.
From above information, hackers can discover multiple ways to exploit the website.
At first, comes to the definition of API endpoints: This is the location where API receives requests about a specific resource on its server.
For example, look at the following GET
request:
1
GET /api/books HTTP/1.1
The API endpoint in this case is /api/books
. Maybe this endpoint results in a request for getting information of books from a library (for example).
Another endpoint: /api/books/detective
: Request for interaction with detective books from a library.
After successfully identify the API endpoints, hackers need to determine how to interact with these endpoints. Maybe hackers can explore via valid HTTP request to the API.
For example:
Find out the input data that the API processed.
The types of input that the API accepts, including supported HTTP methods and media formats.
Rate limits and authentication mechanisms.
API documetation
Documentation can be in both human-readable and machine-readable forms.
Human-readable documentation is designed for developers to understand how to use the API. It may include detailed explanations, examples, and usage scenarios. This document is often available for everyone to access, so hackers also can use it to know about how the API works.
Machine-readable documentation is designed to be processed by software for automating tasks like API integration and validation. It’s written in structured formats like JSON or XML.
Note
Use Burp Suite Scanner to crawl the APIs.
After have identified an API endpoint for a particular resource, remember to investigate through the path of that API endpoint. For example:
/api/swagger/v1/users/123
→ also explore these paths:/api/swagger/v1/users
→/api/swagger/v1
→/api/swagger
→/api
.
Lab 1: Exploiting an API endpoint using documentation
Our mission is find the exposed API documentation and delete carlos
.
I will use Burp Suite to control the request and find out the exposed API refered to user data.
After login, there’s a form to update email. Let’s try submitting a temporary email.
There’s an API endpoint: /api/user/wiener
.
It seems that I can change the endpoint into /api/user/wiener
and change the HTTP method into DELETE
(try this method at first, if it is not supported in this API, I will find another way).
The returned status is User deleted
, it seems that I have successfully deleted the user named carlos
.
Let’s reload the website to check.
Identifying API endpoints
Some notes on how to identify API endpoints effectively:
Users can use Burp Scanner to crawl information about API, then manually investigate how to attack via that API endpoints.
Also remember to look at JavaScript files or something as the source code of the website. These files can contain information about API used in the website.
Interacting with API endpoints
After identifying API endpoints, use Burp Repeater or Burp Intruder to explore how API responds to request, in order to change HTTP method, add some fields, …
Identify supported HTTP methods
An API endpoint may support different HTTP methods. It’s therefore important to test all potential methods when investigating API endpoints. This may enable users to identify additional endpoint functionality, opening up more attack surface.
Users can use built-in HTTP verbs in Burp Suite to explore which HTTP methods are supported with each API endpoint.
Identify supported content types
API endpoints often expect data in a specific format. They may therefore behave differently depending on the content type of the data provided in a request.
Changing the content type may enable us to:
Triggering hidden errors to access important information.
Bypass flawed security.
Take advantage of differences in processing logic. For example, an API is secured with JSON format, but is insecured with XML.
To change the content type, modify the Content-Type
header, then reformat the request body accordingly.
Lab 2: Finding and exploiting an unused API endpoint
My mission in this Lab is to buy a product named Lightweight "l33t" Leather Jacket
without any payment.
I have found an API endpoint: /api/products/1/price
.
I guess that 1
is the product’s id number.
Let’s send this request to Repeater to view how this API responds.
There’s a field called price
, its value is the price of this product.
I will try sending a POST
request (to change information), including a field
price with the value of 0.
Based on the respond, POST
method is not allowed on this API endpoint, I can only use GET
and PATCH
.
Besides that, I have to add a header named Content-Type
and set the value to application/json
like in the respond. This is to satisfy the condition about supported types.
Now the price of this product is turn into 0. Let’s add to cart and place order to finish this Lab.
Finding hidden parameters
Sometimes, there are some parameters which supported by API but are not documented. In this case, there’re some tools in Burp to find hidden parameters like that.
Burp Intruder enables users to automatically discover hidden parameters, using a wordlist of common parameter names to replace existing parameters (but only available with Burp Suite Pro, can find the same version on Github) or add new parameters.
Besides, make sure that users also include names that are relevant to the application based on initial recon.
Mass assignment vulnerabilities
Mass assignment (also known as auto-binding) can inadvertently create hidden parameters. It occurs when software frameworks automatically bind request parameters to fields on an internal object. Mass assignment may therefore result in the application supporting parameters that were never intended to be processed by the developer.
Identifying hidden parameters
Users can identify hidden parameters base on the object returned from the server.
Let’s look at this example:
1
2
3
4
5
PATCH /api/users/
{
"username": "wiener",
"email": "wiener@example.com",
}
A concurrent GET /api/users/123
request returns the following JSON:
1
2
3
4
5
6
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"isAdmin": "false"
}
This may indicate that the hidden id
and isAdmin
parameters are bound to the internal user object, alongside the updated username and email parameters.
Testing mass assignment vulnerabilities
To test if user can modify the hidden id
and isAdmin
parameters, add it to the PATCH
request. For example:
1
2
3
4
5
{
"username": "wiener",
"email": "wiener@example.com",
"isAdmin": false,
}
In addition, send the request with an invalid value of the isAdmin
parameter, like this:
1
2
3
4
5
{
"username": "wiener",
"email": "wiener@example.com",
"isAdmin": foo,
}
If the server respond differently when receive the invalid value, it means that the invalid value affects the logic of the program, but the valid one doesn’t → users can change the value of the parameter.
Try sending this request:
1
2
3
4
5
{
"username": "wiener",
"email": "wiener@example.com",
"isAdmin": true,
}
If the isAdmin
value is bound to the user object without any strictly checking, the user will be incorrectly escalated to the admin privileges.
LAB 3: Exploiting a mass assignment vulnerability
After testing all functions of the website, there’s 2 API endpoints which seems the key of this problem.
The API endpoint named /api/checkout
, supports both GET
and POST
HTTP method → maybe this is the API for checking information about the order.
Try sending the GET
request, I have received a notable object returned: chosen_discount
. It seems that if I want to solve this LAB, I have to modify the value of the field called percentage
to 100, which means I have a 100 percent discount for the product.
Insert the object called chosen_discount
to the POST
request, then send it, and the LAB is solved.
Server-side parameter pollution
Systems often contain internal APIs that aren’t directly accessible via internet. Server-side parameter pollution occurs when users try to inject input in the request to internal API.
This can lead to:
Override existing parameters.
Modify the application behavior.
Access unauthorized data.
User can inject input in query parameters, for example, from fields, headers, URL path, …
Testing for server-side parameter pollution in the query string
User can inject syntax characters like #
, &
, =
in the input and observe how the application responds to the request.
Consider a vulnerable application the enables users to search for other users based on their username.
This is the request:
1
GET /userSearch?name=peter&back=/home
Then, the server make this request to internal API:
1
GET /users/search?name=peter&publicProfile=true
Truncating query strings
We can use a URL-encoded #
character to attempt to truncate the server-side request.
For example, we could modify the URL like:
1
GET /userSearch?name=peter%23foo&back=/home
The front-end will try to access the following URL:
1
GET /users/search?name=peter#foo&publicProfile=true
Note: It’s essential that user URL-encode the character input in URL. Otherwise, application will interpret it as a fragment identifier and will not pass it to the internal API.
For example, if user modify the URL like:
1
[URL]#section2
In this case, application will direct user to the element with the id named section2
.
Back to the main topic, review the response for clues about whether the query has been truncated. For example, if the response returns the user peter
, the server-side query may have been truncated. If an Invalid name
error message is returned, the application may have treated foo as part of the username. This suggests that the server-side request may not have been truncated.
If you’re able to truncate the server-side request, this removes the requirement for the publicProfile
field to be set to true.
Then, user may be able to exploit this to return private user profiles.
Injecting invalid parameters
Use an URL-encoded &
character to attempt to add a second parameter to the server-side request.
For example, modify the request like this:
1
GET /userSearch?name=peter%26foo=xyz&back=/home
The front-end will try to access this following URL:
1
GET /userSearch?name=peter&foo=xyz&publicProfile=true
Review the response for clues about how the additional parameter is parsed. For example, if the response is unchanged this may indicate that the parameter was successfully injected but ignored by the application.
Injecting valid parameters
Based on the knowledge on Finding hidden parameters
section, we can know which parameters are able to be injected to the query string.
For example, modify the request like this:
1
GET /userSearch?name=peter%26email=foo&back=/home
This results in the following server-side request to the internal API:
1
GET /users/search?name=peter&email=foo&publicProfile=true
Review the response for clues about how the additional parameter is parsed.
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, modify the request like this:
1
GET /userSearch?name=peter%26name=carlos&back=/home
This results in the following server-side request to the internal API:
1
GET /users/search?name=peter&name=carlos&publicProfile=true
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 an Invalid username error 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 overriding existing parameters, we may be able to conduct an exploit like: add a parameter like name=administrator
to the request → enable to log in as administrator.
LAB 4: Exploiting server-side parameter pollution in a query string
This LAB requires me to log in as the administrator
and delete the user named carlos
.
It’s surprised that when I log in as the default username and password: {wiener, peter}, the application returns an error message.
Let’s try using the function Forgot password
.
Submitting with the username administrator
.
There’s a POST request appearing in Burp. Let’s add it to Repeater.
This is a normal response. Let’s apply some knowledge that I have studied in the previous section.
Try truncating the URL with the URL-encoded #
character. In the response, there’s an error message: “Field not specified”.
Let’s add another parameter called “field” to the URL.
Invalid value for “field” parameter. Let’s use Intruder to brute-force the value for “field”.
(I don’t have Burp Pro, so I can’t use the default payload provided by Burp. Instead, I have to find another one on Github)
After brute-force, I have found 2 payloads: email
and username
, with the status code 200 returned.
This is a normal response. But I have to find more hints.
In the browser’s network tab, I have found a file called forgotPassword.js
. It seems that this file is about the operation behind the function Forgot password
.
I have noticed that the main function in this script is forgotPwdReady
. This function checks if the URL contains a parameter called reset_token
and its value.
But now, how can I find out the value for reset_token
? Try inserting reset_token
on field
. Notice that in the previous step, when I type email
as the value of field
, it returns the email of the username. So, as logical, application will returns the reset_token
value when I insert it as the value of field
.
That’s right. The reset_token
is 13520f6h8xy4qljek2wwks4djlna4fsf
.
Now, insert this parameter and value to the URL with the syntax provided in the script: /forgot-password?reset_token=${resetToken}
Then log in with the password and delete the user carlos
. The LAB is solved.