前幾天看了redpwn的一道web題,node.js的web,涉及知識點是javascript 原型鏈污染,以前沒咋接觸過node,因此記錄一下學習過程
1.本機node.js環境安裝
題目都給了源碼,所以本地就可以直接安裝package.json依賴並本地模擬調試

首先subline安裝node環境:
https://www.cnblogs.com/bluesky4485/p/3928364.html
https://github.com/tanepiper/SublimeText-Nodejs
2.題目源碼
const crypto = require('crypto')
const http = require('http')
const mustache = require('mustache')
const getRawBody = require('raw-body')
const _ = require('lodash')
const flag = require('./flag')
const indexTemplate = `
<!doctype html>
<style>
body {
background: #172159;
}
* {
color: #fff;
}
</style>
<h1>your public blueprints!</h1>
<i>(in compliance with military-grade security, we only show the public ones. you must have the unique URL to access private blueprints.)</i>
<br>
{{#blueprints}}
{{#public}}
<div><br><a href="/blueprints/{{id}}">blueprint</a>: {{content}}<br></div>
{{/public}}
{{/blueprints}}
<br><a href="/make">make your own blueprint!</a>
`
const blueprintTemplate = `
<!doctype html>
<style>
body {
background: #172159;
color: #fff;
}
</style>
<h1>blueprint!</h1>
{{content}}
`
const notFoundPage = `
<!doctype html>
<style>
body {
background: #172159;
color: #fff;
}
</style>
<h1>404</h1>
`
const makePage = `
<!doctype html>
<style>
body {
background: #172159;
color: #fff;
}
</style>
<div>content:</div>
<textarea id="content"></textarea>
<br>
<span>public:</span>
<input type="checkbox" id="public">
<br><br>
<button id="submit">create blueprint!</button>
<script>
submit.addEventListener('click', () => {
fetch('/make', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
content: content.value,
public: public.checked,
})
}).then(res => res.text()).then(id => location='/blueprints/' + id)
})
</script>
`
// very janky, but it works
const parseUserId = (cookies) => {
if (cookies === undefined) {
return null
}
const userIdCookie = cookies.split('; ').find(cookie => cookie.startsWith('user_id='))
if (userIdCookie === undefined) {
return null
}
return decodeURIComponent(userIdCookie.replace('user_id=', ''))
}
const makeId = () => crypto.randomBytes(16).toString('hex')
// list of users and blueprints
const users = new Map()
http.createServer((req, res) => {
let userId = parseUserId(req.headers.cookie)
let user = users.get(userId)
if (userId === null || user === undefined) {
// create user if one doesnt exist
userId = makeId()
user = {
blueprints: {
[makeId()]: {
content: flag,
},
},
}
users.set(userId, user)
}
// send back the user id
res.writeHead(200, {
'set-cookie': 'user_id=' + encodeURIComponent(userId) + '; Path=/',
})
if (req.url === '/' && req.method === 'GET') {
// list all public blueprints
res.end(mustache.render(indexTemplate, {
blueprints: Object.entries(user.blueprints).map(([k, v]) => ({
id: k,
content: v.content,
public: v.public,
})),
}))
} else if (req.url.startsWith('/blueprints/') && req.method === 'GET') {
// show an individual blueprint, including private ones
const blueprintId = req.url.replace('/blueprints/', '')
if (user.blueprints[blueprintId] === undefined) {
res.end(notFoundPage)
return
}
res.end(mustache.render(blueprintTemplate, {
content: user.blueprints[blueprintId].content,
}))
} else if (req.url === '/make' && req.method === 'GET') {
// show the static blueprint creation page
res.end(makePage)
} else if (req.url === '/make' && req.method === 'POST') {
// API used by the creation page
getRawBody(req, {
limit: '1mb',
}, (err, body) => {
if (err) {
throw err
}
let parsedBody
try {
// default values are easier to do than proper input validation
parsedBody = _.defaultsDeep({
publiс: false, // default private
cоntent: '', // default no content
}, JSON.parse(body))
} catch (e) {
res.end('bad json')
return
}
// make the blueprint
const blueprintId = makeId()
user.blueprints[blueprintId] = {
content: parsedBody.content,
public: parsedBody.public,
}
res.end(blueprintId)
})
} else {
res.end(notFoundPage)
}
}).listen(80, () => {
console.log('listening on port 80')
})
題目的邏輯也比較清楚,通過一個隨機hash作為索引來存儲每個人的content和public,這里flag存在的對應hash里不存在public,函數在后面遍歷所有存儲的content時就不會輸出帶有flag的content,因此目的也很明確,就是讓flag對應的public也為ture,從而遍歷輸出所有content,函數里面還提供了一個根據單獨的hash id來返回content,剛開始我以為randbytes可能可以碰撞,google了一下似乎這個是不太安全,但是重復的幾率幾乎可以不用考慮,所以這里就要涉及到原型鏈污染,因為是第一次接觸原型鏈勿擾,我也去看了幾篇文章,先補補基礎
3.基礎部分

這張圖來自先知上一個師傅的文章。
推薦p牛的文章https://www.freebuf.com/articles/web/200406.html,
在js里面萬物皆對象,而js提供了一些方法能夠使類的對象來訪問類中的屬性,而原型prototype是類的一個屬性,通過類名+prototype就能夠訪問類中所有的屬性和方法,比如定義了類Foo,那么Foo.protype就是該類的原型,那么該原型有兩個屬性:構造器constructor和__proto__屬性,constructor可以當成該類的構造函數,實際上就是它自身,而Foo類的原型的__proto__就是object類,即又向上找到了新的原型

我們可以通過Foo.prototype來訪問Foo類的原型,但Foo實例化出來的對象,是不能通過prototype訪問原型的。這時候,就該proto登場了。一個Foo類實例化出來的foo對象,可以通過foo.proto屬性來訪問Foo類的原型,也就是說:

即有以下兩點:
1.prototype是一個類的屬性,所有類對象在實例化的時候將會擁有prototype中的屬性和方法
2.一個對象的proto屬性,指向這個對象所在的類的prototype屬性
而關於javascript繼承要知道以下幾點:
1.每個構造函數(constructor)都有一個原型對象(prototype)
2.對象的proto屬性,指向類的原型對象prototype
3.JavaScript使用prototype鏈實現繼承機制
4.Object.prototype的proto就是null
而關於原型鏈污染要知道以下幾點:
我們已經知道類的對象的proto屬性指向類的prototype,那么我們修改了proto屬性的值,那么將對類中原有屬性產生影響

這修改了foo.__proto__的bar屬性實際上就是給foo的類添加了一個bar屬性,那么修改了以后當第一次查找bar值時會首先在foo對象自身找這個值,如果沒找到的話再在foo.__proto__中找,而從下圖可以看出來__proto__的確存在bar屬性

那么新建的對象也是{},因此他們實際上的__proto__指向是相同的,和python的基類有點像,因此新建的對象將在__proto__中找到bar值。

應用場景:
我們只要能夠控制數據或對象的鍵名即可,p牛提到了兩種
1.對象merge
2.對象clone(其實內核就是將待操作的對象merge到一個空對象中)
我在國外的pdf中發現了更多容易受到攻擊的庫,其中就包括4月份爆的ldhash的漏洞,也就是題目中要考察的

3.簡單例子分析

假如有以上merge()函數,那么其中我們要是能夠控制target的key,使其為__proto__,那么就有可能導致原型鏈污染

這里執行merge以后理論上應該新建一個新的對象,其也存在b為2,但是如下


這是因為,我們用JavaScript創建o2的過程(let o2 = {a: 1, “proto“: {b: 2}})中,proto已經代表o2的原型了,此時遍歷o2的所有鍵名,拿到的是[a, b],proto並不是一個key,自然也不會修改Object的原型。
所以這里必須配合上json.parse,也就是JSON解析的情況下,proto會被認為是一個真正的“鍵名”,而不代表“原型”,所以在遍歷o2的時候會存在這個鍵。


此時就成功對原型鏈進行了污染
4.回到題目
題目中使用的package.json中ldhash的版本過低



題目中使用defaultDeep來進行拷貝賦值操作,這里使用json.parse來接續body中的json數據,並且json數據是可控的,沒有過濾的

注意到這里題目flag對應的鍵名為content,並且包含content的是{},所以這里應該通過defaultDeep來使原型指向到{}的原型,從而來對其屬性進行注入。
這里如果只是注入單純的content屬性的話,相當於執行{}."content"={}."content",這里這樣不能操控我們前面所謂的鍵名,於是我們可以多傳遞一個"constructor"的構造器屬性,來構成
{}."constructor"."protype"

這里要注意令a為{},那么a的構造器即為object類即 f Object,那么再調用prototype即可訪問{}中所有的值,即對象中所有值,這里a.__proto__和a.constructor.prototype是等價的,我們可以把let a={},可以看作類object的
對象,這樣和前面說的Foo.prototype=foo.__proto__就是同樣的道理了。
但是這里和題目又有點稍微的不一致,因為這里a是實例化的有名字的對象,而題目中是{},可以在F12 console中看到它是沒有__proto__屬性的,因此不能夠通過{}.__proto__來訪問到object原型,但是{}是有唯一一個構造器屬性的來返回object類,所以這里payload可以構造為
"constructor":{"prototype":{"public":true}}
這樣就相當於給{content:flag}多添加了一條public屬性,那么當訪問flag對應的hash id去取puclic的值時,就會通過繼承鏈到object中找,就能找到我們注入的public:true

這樣就能拿到flag
5.總結
這篇文章只是介紹了利用原型污染來注入屬性,那么利用該種攻擊方式實際上還可以進行dos、命令執行攻擊,但是原理都是相同的,我們可以通過控制鍵名來“向上爬”,根據繼承鏈去找對象的原型,從而污染原型。
參考:
https://www.freebuf.com/articles/web/200406.html
