Hack The Box CTF Challange - NeoVault

tech

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:

Login-Set-Cookie

观察返回的 Token,这个 eyJhbG 对于后端程序员来说应该是再熟悉不过了,发现整体 Token 是 . 拼接的三段内容,尝试 JWT 解码,得到下面的内容:

1
2
3
4
5
6
7
8
9
{
"alg": "HS256",
"typ": "JWT"
}
{
"id": "6889a2c93d6e9096745a869a",
"iat": 1753850577,
"exp": 1753854177
}

而在 /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 这种对称签名来说,使用一个强密钥是必要的,否则容易被爆破。

于是,整体的攻击思路就从上面几种方法入手:

  1. 首先我尝试了直接去除最后的 SIgnature,并将 _id 字段替换为 neo_system 的 ID,访问任意一个被保护的接口,得到 HTTP 401 Unauthorized;
  2. 之后我尝试了 none/noNe 这种指定空签名验证的方式,仍然没有成功;
  3. 最后,使用 Hashcat(参数 -m 16500)跑了 rockyou 以及 常见的 HS256 密钥字典,仍然未果;

到此为止,JWT 的渗透测试暂时失败了。

0x2: SQL Injection and Cataloge scan

尝试对登陆、交易历史获取、更改邮箱等接口进行 SQL 注入。这里使用 sqlmap 进行测试,结果测试的所有接口都没有结果:

sqlmap-injection

之后,使用 dirsearch 对目标系统金策目录扫描,也没有得到结果。

到此为止,可以确认系统使用了成熟的 JWT 鉴权中间件,以及在 ORM 之前使用了参数类型验证,并且没有什么公开的多余接口可供使用。

0x3: noSQL Injection and user listing

到这,我突然反应过来,_id: 6889a2c93d6e9096745a869a 这种形式不是很典型的 MongoDB 的 ObjectID 生成算法得到的 ID 吗? 于是,我开始尝试对之前 SQL 注入的接口进行 noSQL 的注入测试。

结果在 GET /api/v2/auth/inquire?username= 发现了一个注入点。该接口接收一个 username 参数,并返回三种结果:

  1. 用户不存在(404, User not found):传入的的用户名在系统中不存在对应的用户;
  2. 用户为当前登录用户的本身(400, Cannot inquire own account):传入的用户名与当前登录的用户名相同;
  3. 用户存在(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

nosql-injection-users

这很显然就是作者给我们拿 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
2
3
4
5
6
7
8
9
10
11
12
endpointsV1: {
me: "/api/v1/auth/me",
login: "/api/v1/auth/login",
register: "/api/v1/auth/register",
logout: "/api/v1/auth/logout",
changeEmail: "/api/v1/auth/change-email",
transactions: "/api/v1/transactions",
deposit: "/api/v1/transactions/deposit",
balanceHistory: "/api/v1/transactions/balance-history",
categoryPercentages: "/api/v1/transactions/categories-spending",
downloadTransactions: "/api/v1/transactions/download-transactions"
}

但是我尝试了 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:

hidden-flag

哈哈,果然不是很简单呢!

Author: 桂小方

Permalink: https://init.blog/ctf-lordrukie-neovault/

文章许可协议:

如果你觉得文章对你有帮助,可以 支持我

Comments