最近发现公司测试在内网部署了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注入
未完待续-。-