This article was last updated on <span id="expire-date"></span> days ago, the information described in the article may be outdated.
最近在做 Hack the Box 的 CTF 挑战,发现一道很有意思的 Web 渗透相关的题目:Neo Vault,自己也是花了好几天才断断续续做出来,在此记录一下解题过程。
0x0: Authentication
题目给的是一个典型的 NextJS 构建的系统,有常用的注册、登陆接口等等。开始时,我们是没有任何系统的访问权限的。想要拿到 Flag,需要先注册一个新用户,再去后台观察有没有可以利用的漏洞。
首先看一下鉴权模式:登陆时候向 /api/v2/auth/login
发送一个 POST 请求,携带 JSON 参数的用户邮箱地址以及密码。如果用户提供的用户名即密码是正确的,则返回一个 200 的消息,在响应里面使用 Set-Cookie
设置 Token:
观察返回的 Token,这个 eyJhbG
对于后端程序员来说应该是再熟悉不过了,发现整体 Token 是 .
拼接的三段内容,尝试 JWT 解码,得到下面的内容:
1 | { |
而在 /api/v2/transactions
中,可以看到系统中存在另一个用户 neo_system
,并在 Response 中已经返回了该用户的 ID。所以暂时的思路就是尝试用该系统用户身份登陆,看看是否能拿到 Flag。
0x1: JWT Penetration
JSON Web Token 的特点就是用户相关的鉴权内容是明文 Base64 编码在 Token 中的,为了防止攻击者私自修改相关的鉴权内容,最后会使用 Signature 来对得到的鉴权内容进行签名。一般使用的签名方法有 RS256/HS256,分别对应着非对称签名以及对称签名。
但是,并不是所有程序都会完整的实现 JWT 的签名验证操作:
- 例如程序可能仅仅使用了
decode
而不是verify
函数,导致签名事实上完全没有被验证; - 除此之外,还有些程序没有设置合理的 Signature Algorithm 方法列表,导致
alg
参数为none
时,也可以在没有签名的情况下使用 JWT; - 还有些程序会检测
alg
参数字符串是否为none
,但是没有针对noNe
这种变体进行检测,导致出现签名验证失效的情况; - 最后,对于 HS256 这种对称签名来说,使用一个强密钥是必要的,否则容易被爆破。
于是,整体的攻击思路就从上面几种方法入手:
- 首先我尝试了直接去除最后的 SIgnature,并将
_id
字段替换为neo_system
的 ID,访问任意一个被保护的接口,得到 HTTP 401 Unauthorized; - 之后我尝试了
none/noNe
这种指定空签名验证的方式,仍然没有成功; - 最后,使用 Hashcat(参数
-m 16500
)跑了 rockyou 以及 常见的 HS256 密钥字典,仍然未果;
到此为止,JWT 的渗透测试暂时失败了。
0x2: SQL Injection and Cataloge scan
尝试对登陆、交易历史获取、更改邮箱等接口进行 SQL 注入。这里使用 sqlmap 进行测试,结果测试的所有接口都没有结果:
之后,使用 dirsearch 对目标系统金策目录扫描,也没有得到结果。
到此为止,可以确认系统使用了成熟的 JWT 鉴权中间件,以及在 ORM 之前使用了参数类型验证,并且没有什么公开的多余接口可供使用。
0x3: noSQL Injection and user listing
到这,我突然反应过来,_id: 6889a2c93d6e9096745a869a
这种形式不是很典型的 MongoDB 的 ObjectID 生成算法得到的 ID 吗? 于是,我开始尝试对之前 SQL 注入的接口进行 noSQL 的注入测试。
结果在 GET /api/v2/auth/inquire?username=
发现了一个注入点。该接口接收一个 username
参数,并返回三种结果:
- 用户不存在(404, User not found):传入的的用户名在系统中不存在对应的用户;
- 用户为当前登录用户的本身(400, Cannot inquire own account):传入的用户名与当前登录的用户名相同;
- 用户存在(200):列出用户的名称以及对应的
_id
;
这个接口的弱点在于,其没有对 username
进行类型检查,这导致可以对该接口使用类似重言注入的操作。具体来说,如果使用 username[$gt]=abcdef
作为查询参数,会出现下列的情况:
由于
username[$gt]=abcdef
在 JSON 解码的时候会被当作一个Object
对待,则对应的request.args
得到:{"username": {"$gt": "abcdef"}}
;而这里没有对
username
这个参数进行类型检查,导致Object
被传入 ORM,变成查询:1
db.users.find({'username': {$gt: 'abcdef'}}).limit(1)
这里的
$gt
是 MongoDB 的查询操作符,表征列出大于传入值的所有记录,相应的还有其他操作符$lte, $in, $nin, $regex
等等。
尽管这个接口没有将所有匹配的记录行返回,但是其返回的状态就可以用来盲注。为了方便起见,我又注册了一个 neo_system_
用户,在字符串上,这个用户名肯定比 neo_system
大,于是我使用 $gt
进行查询,发现系统中还存在另外一个用户 user_with_flag
:
这很显然就是作者给我们拿 Flag 的用户了。但是,接下来该怎么办呢?
0x4: Endpoint Degree
在观察系统中提供的接口时,我发现了一个很有意思的接口:/api/v2/transactions/deposit
,这个接口看起来是用来做储蓄用的,但是这个接口只会返回一个 301,并且携带一条消息:
1 | {"message":"Under maintenance","error":"v2 version is under maintenance"} |
更有趣的是,这个接口尽管返回 301,但是没有提供 Location
;所以严格来说,这是一个不合法的响应。那么,既然 v2 版本的 API 正在维护,说明一定存在一个 v1 版本的 API,于是对 JavaScript 代码进行检查,果然发现了 v1 版本 API 的地址:
1 | endpointsV1: { |
但是我尝试了 deposit 的 v1 版本 API,无论输入什么都只会返回一个 500 错误;而测试的其他几个端口全部都返回 301 到 v2 版本的 API,这到底是什么意思呢?
0xf: Hidden Flag
因为一直没有思路,于是我就关掉了 Instance。但是在这个时候,我注意到了作者给这个题目的 Introduction:
Neovault is a trusted banking app for fund transfers and downloading transaction history. You’re invited to explore the app, find potential vulnerabilities, and uncover the hidden flag within.
这个系统最主要的两个功能是:转账 以及 下载历史交易记录。
之前的盲注点是在转账功能中发现的,那么进一步的进展应该就是要在历史交易记录 API 上找。这个时候我突然想起来,之前没有对 v1
版本的交易记录下载功能进行测试。于是,尝试 POST 这个接口:
1 | POST /api/v1/transactions/download-transactions |
才发现,这个接口可以直接提供对应的 _id
用来查询用户的历史交易记录,而不需要鉴权。
于是,将 user_with_flag
的 ID 传入,果然返回了 200;打开下载的 PDF,就拿到了隐藏的 Flag:
哈哈,果然不是很简单呢!
Comments