作者:Threezh1
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送!
投稿郵箱:paper@seebug.org
前言
這次在打X-AUCN2020比賽過程中遇到了一道Nodejs原型鏈污染的題,賽后看到0ops的師傅竟然可以污染任意值,所以想對這個過程進行再次的分析梳理。
題目中的污染空值過程與污染任意值的payload均參考于比賽中的Writeup: https://github.com/NeSE-Team/XNUCA2020Qualifier/tree/main/Web/oooooooldjs
測試用例
測試例子:
const express = require('express')
const app = express()
const port = 9000
app.use(express.json())
app.use(express.urlencoded({
extended: true
}))
const {
body,
validationResult
} = require('express-validator')
middlewares = [
body('*').trim() // 對所以鍵值進行trim處理
]
app.use(middlewares)
app.post("/user", (req, res) => {
const foo = "hellowrold"
return res.status(200).send(foo)
})
app.listen(port, () => {
console.log(`server listening on ${port}`)
})
依賴包版本:
npm init
npm install lodash@4.17.16
npm install express-validator@6.6.0
npm install express
express-validator參考:https://express-validator.github.io/docs/
在分析這個原型鏈污染漏洞之前,我們先對express-validator的過濾器(sanitizer)的實現流程進行一個分析。
過濾器(sanitizer)實現流程
在src/middlewares/validation-chain-builders.js文件中找到body的實現

傳遞到了check_1.check方法中,跟入check.js文件

location傳遞進來后傳遞到setLocations方法里創建了一個builder對象,并傳入到chain_1.SanitizersImpl方法中。對于return,在題目的Wirteup中有以下的描述:
先看return的地方,check函數里的middleware就是express-validator最終對接express的中間件。utils_1.bindAll函數做的事情就是把對象原型鏈上的函數綁定成了對象的一個屬性,因為Object.assign只做淺拷貝,utils.bindAll之后Object.assign就可以把sanitizers和validators上面的方法都拷貝到middleware上面了,這樣就能通過這個middleware調用所有的驗證和過濾函數。
針對bindAll,我個人的理解是:bindAll函數就是把需要調用的方法都綁定到middleware上進而實現鏈式調用。
- 什么是鏈式調用:https://juejin.im/post/6844904030221631495
- bindAll方法: https://my.oschina.net/cangy/blog/301038
傳入bindAll的參數值是通過Chain_1.SanitizersImpl返回的,可以通過chain.js確定到這個函數的定義位置為src/chain/sanitizers-impl.js。

在這個類中存在很多的過濾器(sanitizer),過濾器實現的方法都調用了this.addStandardSanitization()將過濾器傳入到sanitization_1.Sanitization()方法中,得到的結果最終傳遞給this.builder.addItem()。
先來看sanitization_1.Sanitization()方法,位置在:src/context-items/sanitization.js:

這個Sanitization類中的run方法最終通過調用sanitizer方法設置了context的值。(context后面的處理過程在漏洞分析部分)
再來看this.builder.addItem()做了什么,位置在src/context-builder.js

就是把傳入進來的值壓入this.stack棧中。
回到Sanitization類中的run方法,這個run方法是在哪調用的呢?再看到check.js,這里創建了一個runner對象,并在middleware里調用了run方法:

同樣可以從chain/index.js中找到實現runner.run方法的具體位置為:

這里可以看到是從context.stack里面循環遍歷了contextItem,并調用了其run方法。在這條循環語句處下斷點查看一下context的內容:

在stack里面就是包含了我們所調用的過濾器,而這個context.stack也就是this.builder.addItem()所設置的值。
這就是完整的express-validator的過濾器(sanitizer)的實現流程,wp中對這個過程有一個總結:
express-validator的做法是把各種validator和sanitizers的方法綁定到check函數返回的middleware上,這些validator和sanitizer的方法通過往context.stack屬性里面push context-items,最終在ContextRunnerImpl.run()方法里遍歷context.stack上面的context-items,逐一調用run方法實現validation或者是sanitization
我這里畫了一個流程圖來梳理這一過程:
(這個流程圖畫的比較復雜,如果你嘗試跟過一遍的話再來看這個流程圖就會比較容易理解一些
lodash < 4.17.17 原型鏈污染
https://snyk.io/vuln/SNYK-JS-LODASH-608086
lod = require('lodash')
lod.setWith({}, "__proto__[test]", "123")
lod.set({}, "__proto__[test2]", "456")
console.log(Object.prototype)
express-validator中lodash原型鏈污染漏洞攻擊面
在題目環境中npm install的時候就會有提示,express-validator庫中的所依賴的lodash庫存在原型鏈污染漏洞。

這是因為express-validator的依賴包中,lodash的安裝版本最低為4.17.15的,所以在一定條件下會存在原型鏈污染漏洞。(這里的測試環境我們安裝的是4.17.16版本,lodash在4.17.17以下存在原型鏈污染漏洞)

繼續分析:
跟著上面過濾器(sanitizer)實現流程的最后幾步,runner.run方法在context.stack里面循環遍歷了contextItem,并調用了其run方法。
我們先來看看這個值的傳入過程是怎么樣的。
請求中值的傳入過程
測試數據包:
{"__proto__[test]": "123 "}
在調用run方法時傳入了一個instance.value的變量,這個變量的值是我們傳入json數據當中的值,run方法在調用過濾器處理后給其賦予了一個新的值。
我們下斷點來查看一下:

經過過濾器處理后(也就是經過了一個trim()處理):

可以看到,newvalue是instance.value經過run方法處理后得到的值,一直往上推可以得知instance的實現方法是this.selectFields,位置是在select-fields.js文件中:

select-fields.js:

這個文件的處理過程中我們需要了解到的就是在segments.reduce函數中對輸入的值進行了一些判斷和替換。重要的點就是當傳入的鍵中存在. ,則會在字符兩邊加上[" "],并且最終返回的是一個字符串形式的結果。(對于這些語句更為詳細的原因可以參考writeup中對這一段的描述)
接著之前的過程,在經過了過濾器的處理之后,會通過lodash.set對指定的path設置新值,也就是如圖中的_.set(req[location], path, newValue)過程。
現在可以嘗試一下能不能通過lodash.set原型鏈污染來污染指定的值:

嘗試污染proto[test],結果發現是污染并沒有成功:

原因是因為,當lodash.set中第一個參數存在一個與第二個參數同名的鍵時,污染就會失敗,測試如下:

所以,我們就要嘗試去繞過這個點。 我們來看一下這個語句:
path !== '' ? _.set(req[location], path, newValue) : _.set(req, location, newValue);
這里的第一個參數是從請求中直接取出來的,path是經過先前處理后的出來的值。所以能不能通過這個處理來進行繞過呢?當然是可以的。 當我們傳入:
{"\"].__proto__[\"test":"123 "}
這里的鍵為"].__proto__["test,由于字符里面存在.,所以在segments.reduce函數處理時會對其左右加雙引號和中括號,最終變成:[""].__proto__["test"]。這時在調用set函數時,值的情況就為:

這時就不存在同名的鍵了,于是查看污染的后的值發現:

我們設置的值并沒有傳遞進去,而是污染為了一個空值。為什么傳遞進來的newValue為空值呢?
從select-fields.js中可以看到,是因為取值時,使用的是lodash.get方法從req['body']中取被處理后的鍵值,處理后的鍵是不存在的,所以取出來的值就為undefined。

當undefined傳遞到Sanitization.run方法中后,經過了一個.toString()的處理變成了''空字符串。

lodash.get方法中讀取鍵值的妙用
那我們還有沒有辦法污染呢?結果肯定是有的,我們跟入這個lodash.get方法,這個方法的具體實現位置位于:lodash/get.js

繼續跟蹤到lodash/_baseGet.js

從中我們可以看到這個函數取值的一些邏輯,首先,path經過了castPath處理將字符串形式的路徑轉為了列表,如下面的內容所示。轉換完后通過一個while循環將值循環取出,并在object這個字典里去取出對應的值。
// 初始值
['a'].__proto__.['b']
// 轉換完后的值
["a","__proto__","b",]
那這個地方能不能利用呢?當然也是可以的,我們來看下最終的payload:
{"a": {"__proto__": {"test": "testvalue"}}, "a\"].__proto__[\"test": 222}
這個時候我們在這個函數處下斷點就可以看到,a\"].__proto__[\"test經過castPath處理變成了["a", "proto", "test"],在Object循環取值最終取到的是"a": {"__proto__": {"test": 'testvalue'}中的test鍵的值,這樣就達到了控制value的目的。
還未遍歷前:
最后一次遍歷:

最終污染成功:

總結思考
整個流程下來,能夠污染任意值的關鍵點在于:
- lodash.set存在原型鏈污染漏洞
- express-validator對鍵名的處理
- lodash.get取值邏輯
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1426/
暫無評論