最近發現公司測試在內網部署了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注入
未完待續-。-