• 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注入

    未完待续-。-

     
  • 相关阅读:
    SlidingMenu和ActionBarSherlock结合滑动式菜单都
    Actionbarsherlock 简明教程
    Ajax表单提交插件jquery form
    form 转json最佳示例
    构造AJAX参数, 表单元素JSON相互转换
    jquery序列化form表单使用ajax提交后处理返回的json数据
    firefox插件poster的使用,发起自定义http请求
    android学习8(ListView高级使用)
    Linux server关闭自己主动
    阅读安卓在线(Android)系统源代码
  • 原文地址:https://www.cnblogs.com/xsseng/p/11846000.html
Copyright © 2020-2023  润新知