Nodejs代码安全审计之YAPI

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

未完待续-。-

 
原文地址:https://www.cnblogs.com/xsseng/p/11846000.html