December 28th 2020, 1:02:55 am
说在前面
周末是XCTF的HarmonyOS和HMS专场比赛,起床后就开始看,因为这一道题花了比较长的时间才做出来,所以觉得还是有记录一下的必要。
题目考点:
- Nodejs简单审计
- 原型链污染漏洞
- hbs模板注入导致信息泄露
解题过程
题目简览
题目打开后需要输入用户名登录:

登录之后是一个计算ip地址的一些信息的工具:

点击去开发会跳转到/admin去,显示forbidden

简单审计
在打开题目后链接就会跳转到http://124.71.204.195:31799/?f=login.html,简单尝试一下就可以发现这里是一个任意文件读取:

可以把所有题目源码与相关依赖文件都下载下来,本地可以直接调试。需要注意的是这里没法下载.env文件,这个文件是用于dotenv模块加载环境变量配置的。
简单审计一下可以了解到这些文件的基本用途:
- app.js 配置并启动express(其中包括设置session、静态文件、模板解析引擎、路由等等)
- login.js 用于设置
req.session.name为传入的username - calc.js 用于计算ip相关信息的代码(用于混淆用的,实际解题毫无用处)
- admin.js 会经过一个ip等信息的判断,可以设置计算ip页下发的内容,后面详细分析
- util.js 一些处理函数
主要来看admin.js:
1 | module.exports = (app, env) => { |
把GET请求的过程理一下:
- 通过
JSON.parse()解析了一个json数据,一共有三个键:name、time、ip,而这个name是可控的,并且拼接到json中可以添加键值 - 通过一次循环,将
user的值传递给userinfo,但是当键名为isAdmin时,会把这个值置0 - 判断
req.session.ip是否为127.0.0.1,如果是的话就把userinfo.isAdmin设置为1 - 把
userinfo的值传给session - 判断
userinfo.isAdmin是否为1,如果不为1,则返回forbidden
这里我们的主要目的就是怎么让isAdmin为1,大概有这么几个思路:
- 在上方第二步那里通过在键名里加一些特殊字符的
trick让isAdmin无法被置为0 (fuzz了一下00-ff,失败) - 伪造ip地址
- 队友说可以通过伪造
req.ip,设置app.set('trust proxy', true);就可以通过X-Forwarded-For伪造。参考 (trust proxy默认为false失败) - 我想的是通过控制
JSON.parse()的参数值,让后面的ip不解析,而解析一个我们所控制的ip值来绕过ip检测 (没找到可以成功解析的方式 失败)
- 队友说可以通过伪造
- SESSION伪造 (之前说过无法读取
.env,session的secret在这个文件里,不知道secret也不能任意上传文件的情况下无法伪造 失败) - 是否有原型链污染可以利用?
原型链污染
前三种思路都失败了,后面发现是在JSON.parse这里存在一个原型链污染问题。

通过构造username为:Threezh1","__proto__":{"isAdmin":1},"test":"就可以污染到isAdmin。通过req.session.isAdmin = userinfo.isAdmin这句命令传递后,我们再访问/admin页面就不会是forbidden了。

这里提交的是一个code参数,POST发包给/admin页面。
hbs模板注入导致信息泄露
接着来看一下POST请求是怎么处理的:
- 判断
req.session.isAdmin和code参数 - 对
code参数值进行了过滤,list列表里的内容都会过滤转义 - 最后拼接了一个html页面源码写到本地文件再回显到页面上
这里是一个比较明显的模板注入,搜了一下关于hbs的相关模板注入,找到了一篇关于Handlebars库导致RCE的文章:漏洞挖掘:Handlebars库 模板注入导致RCE 0day
但很快啊,就发现RCE行不通,题目环境中的是最新版,文章中的payload已经不管用了。只能想其他的办法。
注意到传入模板变量时候的一个问题:这里传入的env是把包含所有环境变量的变量传入进去了。又因为页面中的一部分是我们能传入code参数控制的。所以我们是可以通过一些模板定义好的语句读出所有的env所包含的环境变量的。
1 | res.render("users/"+filename, { |
首先是寻找到了这么一篇帖子:2020 Defenit CTF Writeup 里面的payload形式是这样的:
1 | {{#each this}}{{@key}} => {{this.toString}}<br>{{/each}} |
由于这里的@key由于@被过滤了,以及这样简单的遍历没法得到环境变量中的内容,最后构造一了一个可以拿到所有环境变量的语句参考:
1 | {{#each this}}{{#with this}}{{#each this}}<h3>{{this.toString}}</h1><br>{{/each}}{{/with}}{{/each}} |
submit之后就可以在页面上看到flag了:

当时我写的时候比较匆忙,这个payload可以变得更短,payload@p1g3:
1 | {{#each this}}{{#each this}}{{this.toString}}{{/each}}{{/each}} |
总结
感觉题目不算特别难,但就是有很多地方意想不到。对各种细节还需要再注意一些…