redpwnctf-web-blueprint-javascript 原型鏈污染學習總結


 前幾天看了redpwn的一道web題,node.js的web,涉及知識點是javascript 原型鏈污染,以前沒咋接觸過node,因此記錄一下學習過程

1.本機node.js環境安裝

題目都給了源碼,所以本地就可以直接安裝package.json依賴並本地模擬調試

首先subline安裝node環境:

http://nodejs.org/ 

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

https://xz.aliyun.com/t/2802

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM