前幾天看了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