最近发现公司测试在内网部署了YAPI,同事在对yapi进行测试过程中很快就发现了一个xss漏洞,于是自己也就动手审计起来,这是nodejs的代码,之前看过一篇相关的审计漏洞详情,发现nodejs对漏洞的审计主要还是着重于几个要点
- 文件操作类漏洞,诸如任意文件上传、文件读写漏洞等
- 命令、代码执行漏洞
- SQL注入漏洞
文件操作
首先,对于文件操作类漏洞,nodejs我就搜索require('fs')来追踪关键代码,整个yapi项目对于文件写入仅仅有两处地方,都位于控制器下的test.js文件
/**
* 测试 单文件上传
* @interface /test/single/upload
* @method POST
* @returns {Object}
* @example
*/
async testSingleUpload(ctx) {
try {
// let params = ctx.request.body;
let req = ctx.req;
let chunks = [],
size = 0;
req.on('data', function(chunk) {
chunks.push(chunk);
size += chunk.length;
});
req.on('finish', function() {
console.log(34343);
});
req.on('end', function() {
let data = new Buffer(size);
for (let i = 0, pos = 0, l = chunks.length; i < l; i++) {
let chunk = chunks[i];
chunk.copy(data, pos);
pos += chunk.length;
}
fs.writeFileSync(path.join(yapi.WEBROOT_RUNTIME, 'test.text'), data, function(err) {
return (ctx.body = yapi.commons.resReturn(null, 402, '写入失败'));
});
});
ctx.body = yapi.commons.resReturn({ res: '上传成功' });
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
/**
* 测试 文件上传
* @interface /test/files/upload
* @method POST
* @returns {Object}
* @example
*/
async testFilesUpload(ctx) {
try {
let file = ctx.request.body.files.file;
let newPath = path.join(yapi.WEBROOT_RUNTIME, 'test.text');
fs.renameSync(file.path, newPath);
ctx.body = yapi.commons.resReturn({ res: '上传成功' });
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
对于以上两个接口来说,一个是将临时文件直接写入到 yapi.WEBROOT_RUNTIME 目录下命名为 test.text,一个则是将临时文件移到该地方命名为test.text,两处代码近乎相似,对于我们来说没有办法控制文件名,通过控制文件名进行跨目录。但是这让我们有权限在yapi.WEBROOT_RUNTIME 目录下写入一个内容可控的文件以及temp目录下写入临时文件,也可能成为后面漏洞需要的步骤,所以记录了下来。
命令执行
对于命令执行,nodejs提供的require('child_process').exec可以用于访问系统命令,但是这在yapi中不被使用,作为测试工具,我们会发现yapi用上了vm来执行jscode,这个地方可以用来研究下,可能就会出现命令执行漏洞
首先utis中提供了一种方法来执行js代码,这个似乎用于自动化测试断言的
/**
* 沙盒执行 js 代码
* @sandbox Object context
* @script String script
* @return sandbox
*
* @example let a = sandbox({a: 1}, 'a=2')
* a = {a: 2}
*/
exports.sandbox = (sandbox, script) => {
const vm = require('vm');
sandbox = sandbox || {};
script = new vm.Script(script);
const context = new vm.createContext(sandbox);
script.runInContext(context, {
timeout: 3000
});
return sandbox;
在runCaseScript调用了它,但是为查阅资料发现sanbox启动的沙箱执行js不能引入危险的对象诸如fs来对系统进行任何操作,如果要通过这种方法进行命令执行,无非就是发现了js的命令执行漏洞。但是对于vm来说还存在一个问题就是带入的变量可能存在安全问题。
sandbox是外部环境要带入到沙盒中为沙盒执行js提供的变量,这个变量可以是一个require对象,也可以是其他上下文的变量,所以如果存在带入危险或者其他变量,则存在信息泄漏的可能,我们继续看看runCaseScript
exports.runCaseScript = async function runCaseScript(params, colId, interfaceId) {
const colInst = yapi.getInst(interfaceColModel);
let colData = await colInst.get(colId);
const logs = [];
const context = {
assert: require('assert'),
status: params.response.status,
body: params.response.body,
header: params.response.header,
records: params.records,
params: params.params,
log: msg => {
logs.push('log: ' + convertString(msg));
}
};
let result = {};
try {
if(colData.checkHttpCodeIs200){
let status = +params.response.status;
if(status !== 200){
throw ('Http status code 不是 200,请检查(该规则来源于于 [测试集->通用规则配置] )')
}
}
if(colData.checkResponseField.enable){
if(params.response.body[colData.checkResponseField.name] != colData.checkResponseField.value){
throw (`返回json ${colData.checkResponseField.name} 值不是${colData.checkResponseField.value},请检查(该规则来源于于 [测试集->通用规则配置] )`)
}
}
if(colData.checkResponseSchema){
const interfaceInst = yapi.getInst(interfaceModel);
let interfaceData = await interfaceInst.get(interfaceId);
if(interfaceData.res_body_is_json_schema && interfaceData.res_body){
let schema = JSON.parse(interfaceData.res_body);
let result = schemaValidator(schema, context.body)
if(!result.valid){
throw (`返回Json 不符合 response 定义的数据结构,原因: ${result.message}
数据结构如下:
${JSON.stringify(schema,null,2)}`)
}
}
}
if(colData.checkScript.enable){
let globalScript = colData.checkScript.content;
// script 是断言
if (globalScript) {
logs.push('执行脚本:' + globalScript)
result = yapi.commons.sandbox(context, globalScript);
}
}
let script = params.script;
// script 是断言
if (script) {
logs.push('执行脚本:' + script)
result = yapi.commons.sandbox(context, script);
}
result.logs = logs;
return yapi.commons.resReturn(result);
} catch (err) {
logs.push(convertString(err));
result.logs = logs;
logs.push(err.name + ': ' + err.message)
return yapi.commons.resReturn(result, 400, err.name + ': ' + err.message);
}
};
context作为变量将被带入到沙盒中,一看params基本无解,这个变量是http请求参数的,代码可以追踪到interfacCol.js
async runCaseScript(ctx) {
let params = ctx.request.body;
ctx.body = await yapi.commons.runCaseScript(params, params.col_id, params.interface_id, this.getUid());
}
我们可以看到params就是request.body,所以并没有什么安全问题,带入以后也不会有什么信息泄漏,这个可以参考下koa2的文档
ctx.header ctx.headers ctx.method ctx.method= ctx.url ctx.url= ctx.originalUrl ctx.origin ctx.href ctx.path ctx.path= ctx.query ctx.query= ctx.querystring ctx.querystring= ctx.host ctx.hostname ctx.fresh ctx.stale ctx.socket ctx.protocol ctx.secure ctx.ip ctx.ips ctx.subdomains ctx.is() ctx.accepts() ctx.acceptsEncodings() ctx.acceptsCharsets() ctx.acceptsLanguages() ctx.get()
这些东西几乎都是我们自己传给服务器的,几乎不存在可以得到我们在常规情况下不能得到的信息,除了多重代理下xff头可能会泄漏的情况,几乎没有漏洞利用的空间。那么剩下的只有assert: require('assert')了,对于php来说assert可是可以执行命令的,但是似乎node.js不允许你这么做,所以这里暂且保留,也是一个风险点
mongodb注入
未完待续-。-