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

未完待續-。-

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM