https://portswigger.net/web-security/all-labs#api-testing
今年年初4月打的靶场了,发到csdn上
lab-1-发现 API 端点并利用
挂上 burp suite 代理,不断点击站点内功能点,查看 history,发现在更新 email 功能处存在调用 api 接口。
发送到 repeater 进行测试:
不断缩短接口路径/api/user/wiener->/api/user->/api,同时切换请求方法为 GET,尝试是否出其他数据。
通过测试,当使用 GET 方法访问/api接口时,站点将返回接口文档(下图)
内容包括:当前 api 接口支持的通信方法 ,API 端点(Endpoint),参数,响应值等。
当然,发现这个 API 接口文档还有其他手法:直接对站点进行接口字典扫描。
使用 burpsuite-intruder 模块:
- 对路径设置 payload 位置
- 导入常见接口字典
- 注意一定要关闭 url 编码,不然接口路径中的
/会被编码为%2f,根本无法测试
开启了 url 编码就是下面这样
下图是未进行 url 编码的爆破测试,我们可以发现接口为/api时出现了 302 跳转,那么我们直接在浏览器上拼接接口路径,尝试请求。
https://0a6500a90488b291822a15fa00ac00c9.web-security-academy.net/api发现可以访问接口文档,非常不错!
根据实验要求,我们需要调用这个接口来删除用户:carlos
我们先来使用 get 方法请求一下这个接口
https://0a6500a90488b291822a15fa00ac00c9.web-security-academy.net/api/user/carlos接口回显:无权限。
说明后端对于这个 API 接口也是存在一定的鉴权的,我们当前是未登录状态,没有任何权限,但是如果我们登录这个系统的一个账户获取一定的权限,能否调用 API 接口?
来试一试,使用实验给出的账密wiener:peter,登录系统
成功登录,再次尝试调用接口
https://0ab9007c04ac6dc6831f46c3000600fa.web-security-academy.net/api/user/carlos直接返回了用户名和绑定邮箱。
那我们来尝试删除carlos用户,这一步需要来抓个包,我们直接把刚刚请求的数据包发送到 repeater。
修改一下请求方法GET->DELETE,发包。
成功删除用户carlos。
再次在浏览器上请求用户信息,接口回显用户不存在(已经被删除了)
实验完成
总结
本次实验总体分为两个步骤
- 发现接口
- 调用接口
本实验存在接口文档,可以直接扫出来,然后根据接口文档调用接口进行测试。
如果没有接口文档,我们可以挂着 burp 四处点击功能点,然后回来看 history 中的数据包,可能会存在一些被调用的接口。
这一步的目的主要是为了找到 baseurl,请求到后端。
找到 baseurl 之后,再去字典爆破接口名,或者去翻找 js 中存在的接口信息,尝试调用。
本实验测试的接口也是存在一定的鉴权,但是当我们登录到系统获取了调用接口的权限之后就能够调用接口删除其他用户,这是一种思路,漏洞挖掘的时候也可以这样干。
lab-2-寻找并利用隐藏的 API 端点
实验描述
利用 api 端点实现物品购买,实际上是使用 API 端点实现支付绕过。
先登录账号
接下来是寻找 API 端点,常规手法是开启 burp 代理,点功能,查看请求中具有 API 端点特征的 url
发现在添加物品到购物车时,会调用一个 api:
/api/products/1/price
发送到 repeater 中进行测试
尝试使用 OPTIONS 方法查看 API 端点支持的 HTTPS 通信方法。
根据返回可以看到允许 GET,PATCH 两种。
如果存在 PATCH,并且 GET 请求中返回的 json 格式数据存在price,可以尝试直接修改 price 实现支付绕过。
先修改通信方法为 PATCH
返回的错误信息表示请求体只支持 json 数据,那么就在数据包内添加一个字段:
Content-Type:application/json并且需要将 API 端点之前 GET 方法正常返回的 json 格式数据稍微修改一下,拼接到当前数据包下。
正常 GET 方法请求到的数据:
拼接过来之后需要把 message 参数删掉,因为这个参数与我们的目的无关,没有修改的必要。
(需要把那个,逗号删除,否则会因为 json 格式错误造成通信错误)
返回信息表示,需要 price 参数为有效的非负整数。
"price":"$1337.00"当前的参数值是字符串,那么就修改为一个非负整数。
"price":1发送后,发现成功执行,将后端物品价格修改为 0.01,当然我们账户上分币没有,还需要修改为 0。
刷新靶场页面,可以看到价格已经被修改为 0。
直接加到购物车购买即可,实验完成。
总结
- 本实验先由 brup 挂上代理,点击 web 站点功能点,发现 API 端点
- 使用 OPTIONS 方法对端点请求,发现端点支持的 HTTPS 通信方法
- API 端点通信方法中允许 PATCH,并且存在参数
price,尝试直接使用 PATCH 方法修改 price 参数值 - 根据返回错误信息,一步一步修改数据包内容,最终实现参数修改,造成支付绕过 0 元购。
lab-3-批量赋值漏洞
依然挂 burp 代理,点功能,查找 api 端点调用处。
发现:
在点击购物车,增减物品数量时,会 GET 方法调用/api/checkoutAPI 端点。
在place order 下订单时,会 POST 方法调用/api/checkoutAPI 端点。
GET
POST
通过对比这两个数据包,可以看到 GET 方法获取到的数据量大于 POST 方法获取的数据量
目前已知API 端点/api/checkout的通信数据有:
{ "chosen_discount": { "percentage": 0 }, "chosen_products": [ { "product_id": "1", "name": "Lightweight \"l33t\" Leather Jacket", "quantity": 1, "item_price": 133700 } ] }其中包含两个主要的属性:
chosen_discount:选择的折扣
chosen_products:选择的产品
但是 POST 方法只上传了chosen_products属性,那么可以尝试添加chosen_discount属性,测试 API 接口是否存在赋值漏洞。
(即 POST 提交的chosen_discount属性是否能修改后端的chosen_discount值。后端鉴权不完善就可以实现。)
先完全赋值过来,发送请求包,响应包依然显示:资金不足。
没有报错,至少说明后端允许在 POST 请求时,json 数据中携带chosen_discount属性。因为这个属性表示的是产品折扣值。我们需要购买这个产品,尝试将折扣值改为 100,后端一般是最终价=原价-原价*折扣价,再将用户账户金额-最终价。
可以看到,将折扣值修改为 100 时可以实现 0 元购,绕过支付。
实验完成。
总结
- 对于 web 站点,功能要点完,找到每一个调用 API 端点的功能点。最好去把 API-sword 配置一下。
- 本实验本质上是批量赋值漏洞,但是利用起来就是一个增加数据造成 0 元购的支付绕过漏洞。
lab-4-服务器端参数污染
依旧挂上 burp 代理四处点功能,但是没有发现明显的 API 端点
发现在登录页面存在忘记密码操作。
那么要登录 administrator 账户,可以在这里想想办法。
抓取这个数据包
发送到 repeater 进行测试
根据 web 响应,发现忘记密码功能存在邮箱验证,并且 POST 请求中包含username参数,那么我们对请求包进行测试。
测试参数截断
在 username 参数后面拼接%23即#截断字符,响应:Field not specified字段未定义。
修改 payload 为%2,响应Invalid username无效用户名。
那么说明%23可以对后端参数造成截断。
但是这里貌似没有深入利用手法,继续测试参数注入。
注入无效参数
%26x=y,回显:参数不支持。
那么表示后端接收了这个参数,并且解析了参数,发现参数不支持。
如果后端可以解析参数,那么可以尝试注入有效参数,试试能不能出货。
测试有效参数与无效参数的响应页面区别
无效参数x
有效参数username
可以清晰看到,当提交无效参数时,响应为:
{"error": "Parameter is not supported."}提交有效参数时,响应为:
{"type":"ClientError","code":400,"error":"Invalid username."}可以根据这个进行爆破,测试出其他隐藏的有效参数。
爆破有效字段
定位 payload,添加 burp 内置的Form field names - long大字典。
发现存在有效参数field。
继续对field参数值进行爆破。
我们调用了 burp 内置的字典,发现爆破无效,那么我们使用 ai 来现场写一个对于忘记密码功能点的参数字典。
再对字典进行处理,删除多余空行,文字描述。
导入到 burp 爆破模块尝试。
发现存在reset_token参数,并且返回了结果。
那么我们就将这个参数注入到忘记密码的请求数据包内
看到返回了 result,这个参数名为 reset_token,意思是密码重置的凭证,我们猜测在完成了邮箱验证之后会自动调用这个参数,实现密码重新设置。
那么我们直接将这个参数包括它的值添加到原数据包中。
我们先去 http-history 中抓取一个 GET 类型请求
发送到 repeater
对其添加request query parameters(request 查询参数)。
这里需要将%26修改为&参数间隔符,并且在路径后添加?查询开始符。
csrf=NJgfJ6qnXKVod4Alen5YhG75GJJajvEJ&username=administrator&reset_token=ufopy4z496pyxvow1j1qxe5vrie16x7l发送请求,响应 200,在 response 中渲染一下
出现密码重置页面,执行成功。
那我们就直接在 web 上把请求的参数写到链接上,发送请求。
可以看到,成功跳转密码重置页面。
然后随便输一个密码,重置 administrator 的密码,再使用该密码登录 administrator 的账号,删除carlos用户即可完成实验。
补充分析
关于 field 字段
这个字段是本实验的关键,他使我们能够获取其他字段的更详细信息。
本实验可以获取的字段信息有:email,username,reset_token。
并且当我们不使用 field 字段请求数据时,响应的数据与使用 field 字段,值为 email 时响应的数据完全相同。
我们有理由怀疑后端代码中的 field 字段默认值为 email。
并且当我们使用%23对参数进行截断时,响应包中的"Field not specified."字段未定义,就是指%23截断符将 username 字段之后的默认字段 field=email 截断失效。后端未接收到 field 字段值(field 值未定义),无法返回有效内容。
reset_token 隐藏参数发现
我们在上方已经说明了可以使用 ai 构造场景参数字典对参数进行爆破的方法,这个方法具有普适性。
但是本实验还存在一种方法:JS 文件审计。
我们将 http-history 中filter by file extension按文件拓展名过滤的HIde隐藏选项关闭。这一步的目的是让我们可以看到 js 文件的通信。
最好的方法是将 js 写到上方的show only中,js 文件还是有作用的。
我们点忘记密码功能点之后,查看 http-history,发现请求了一个 js 文件:forgotPassword.js
也可以 f12 查看站点源码,但是在我实验时源码里没有 forgetPassword.js 这个文件,很奇怪。
我们将文件路径拼接起来访问这个 JS 文件
发现其中存在一个forgotPwdReady 的函数,并且存在字段reset_token。
这样就找到了隐藏字段 reset_token。
总结
- 在 http-history 中要开启 js 文件的通信,点击对应功能点时,注意调用的 js 文件,其中可能存在隐藏信息:API 接口,参数,加密方法……
- 服务器端参数污染测试方法
%23#截断,根据返回包分析是否有效。%26&参数连接符添加无效参数,测试后端是否解析添加的参数。%26&参数连接符添加有效参数,并寻找该功能点隐藏的其他有效参数。
- 寻找隐藏参数可以使用 ai 生成特定场景下的参数字典,或者在 js 文件中寻找突破
lab-5-REST 路径中测试服务器端参数污染
依旧挂上 burp 代理四处点功能,数据包 url 中没有发现明显的 API 端点调用。
依然存在forgot-password功能点
对该功能点进行测试,发送到 repeater。
尝试参数污染
设置%23 截断参数
响应返回
"Invalid route. Please refer to the API definition" 无效的路由(路径),请参考API定义。表示%23 截断了 username 字段后方的其他参数,导致 API 路径拼接错误。
这种可能是将 username 拼接到了后端 API 路径上。
继续测试注入参数。
注入无效参数%26x=y
响应:
"The provided username \"administrator&x=y\" does not exist" 提供的username参数XXX不存在观察响应的参数administrator&x=y\,我们输入的%26x=y被后端拼接到原参数值上。并且自动在输入的参数后方添加了\路径分隔符。前方也存在一个\路径分隔符。
更印证了我们的判断,后端将接收 username 拼接到后端代码中写好的 API 路径中。
有效参数污染
%26username=carlos
%26email=1@gmail.com
两个有效参数都被拼接到路径中,那么后端应该是将这里写死了,无法注入参数。
尝试其他突破口。
如果这里是直接拼接路径,那么可以思考,使用路径穿越是否可以访问其他文件或端口。
测试正斜杠拼接../,后端会直接过滤正斜杠。
尝试反斜杠拼接..\,后端会双写反斜杠。
添加%23截断符,对后方参数进行截断。
../../../../../../../hostname%23返回: 意外的 API 服务响应,没有找到,你请求的这个 URL 没有找到。
表示成功进行了目录穿越,但是当前根目录下不存在 hostname 这个路径。
对 hostname 设置 payload 发送到 intruder ,使用 burp 内置的目录穿越字典,爆破服务器端常见文件,但是没出货。
使用自己构造的 API 接口文档字典爆破。
发现服务器端存在一个名为openapi.json的文件。
转发到 repeater 详细分析
"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 ..."我们使用 ai 对 API 文档进行美化
{ "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": {} } ] } } } }在响应中存在一个暴露的 API 端点
"/api/internal/v1/users/{username}/field/{field}\"可以看到,我们的 username 参数正是拼接到该端点上。
尝试修改目录穿越 payload 调用 API 端点。
administrator/field/x%23响应:由于安全原因,该版本 API 仅支持 email 字段。
对于这种有限制的 API 端点,存在路径遍历绕过手法,称为路径规范化绕过。
通常需要一步一步测试。
获取到了后端接收参数拼接的 API 路径:
/api/internal/v1/users/{username}/field/{field}\从传递的参数 username 入手,对前方的路径使用路径遍历序列进行相对路径覆盖。
username=../users/administrator/field/passwordResetToken%23username=../../v1/users/administrator/field/passwordResetToken%23当使用第二种 payload 进行路径规范化绕过时,服务器返回数据
{ "type": "passwordResetToken", "result": "ih99sntv8f6fk8903y136e5uwliigabx" }成功绕过 API 端点字段限制。
这是对本质原理的猜测,不一定正确:
但是一般情况下,一个系统内可能存在其他版本的 API 端点,并且暴露的 API 文档中路径"/api/internal/v1/users/{username}/field/{field}"存在版本关键字v1,还有internal关键字表示内部版本。
很大概率存在其他版本 API 端点。
对于这个路径,/api/是它的 baseurl 基础路径,/internal/v1/是 API 的内部版本标识,后面的/users/{username}/field/{field}是资源路径。
那么我们来尝试切换 API 版本。
username=../../v1/users/administrator/field/passwordResetToken%23获取到 passwordResetToken 数据之后,直接构造一个 GET 请求,即可绕过忘记密码邮箱验证,直接进入重置密码阶段。
这一步的原理,在 lab4 已经分析过了。
总结
本实验考察
- 服务器端参数污染:参数截断,参数注入。主要使用参数截断。
- rest-ful 接口文档获取;通过服务器端参数污染-参数注入,并观察响应包回显,判断出后端使用 rest-ful 类型 API 接口。
- 根据 username 参数进行路径遍历攻击,配合 burp-intruder 爆破出服务器端 api 文档,获取到该接口的路径
- 通过 JS 文件审计,或者 snow-eyes 找到 passwordResetToken 参数
- 路径规范规范化绕过 API 端点对于字段的请求限制