Client-side prototype pollution
PortSwigger Web Academy 中关于 Client-side prototype pollution 的部分
Client-side prototype pollution
概述
prototype污染是一个JavaScript漏洞,该漏洞使得攻击者可以向全局prototype添加proerties,这些properties可能会被用户定义的对象继承。
通常prototype污染都是和其他漏洞一起利用打组合拳。
JavaScript prototypes
和其他基于类的语言不同,JavaScript使用Prototypes继承模型。
对象
JavaScript的对象实际上就是被称为properties的键值对
|
|
访问一个对象的proerties可以通过如下两种方式
|
|
properties同样也可以是函数,被称为方法
|
|
prototypes
任何一个JavaScript的对象被链接到另一种类型的对象,被称之为prototype。默认情况下,JavaScript自动为新对象分配其内置的全局prototype之一。例如string会被自动被分配内置的String.prototype。
|
|
对象自动继承被分配的prototype的所有properties,除非该对象已经有了相关的定义。这允许开发者创建新的类重用已有的类的properties或方法。
内置全局prototypes提供了一些有用的properties和方法去处理基本的数据类型。例如String.prototype
有toLowerCase()
方法,这可以让字符串自然就存在一个随时可用的方法将他们转换成lowercase,这就节省了开发者的精力。
对象继承
当引用一个对象的property的时候,JavaScript引擎会先在对象本身去寻找,没找到再去对应的全局prototype寻找
prototype链
每一个对象的prototype也是一个对象,这个对象同样也会有对应的prototype。JavaScript中几乎所有的东西都是一个对象,这个链的终点就是Object.prototype
,它的prototype是null。
__proto__
每个对象都有一个特殊的property去访问他的prototype。虽然这没有一个正式标准化的名字,但是大多数浏览器以__proto__
作为行业标准。这个property提供了读写两种操作,不仅可以读取prototype和他的properties,并且还可以在必要的时候修改它。
同样有两种方法访问__proto__
|
|
也可以多来几个访问prototype的prototype
|
|
修改prototype
只需要正常的修改即可,比如给String.prototype
添加一个方法
|
|
字符串都继承了这个prototype,所以都能调用这个方法。
漏洞产生
JavaScript函数递归地将包含可控properties的对象合并到现有对象中的时候,就有可能出现prototype污染漏洞。攻击者可以通过__proto__
或者其他任意嵌套的properties去注入。
由于__proto__
的含义,合并操作可以将properties分配给对象的prototype而不是它本身。
漏洞利用
- 污染源:可以去污染的全局prototype
- 一个支持任意代码执行的方法或者DOM元素
- gadget:
- 它被不安全地使用
- 继承了攻击者污染的prototype,被修改的properties不能在gadget上已有定义。一些网站会让对象的prototype为null以确保它没有继承任何东西。
污染源
污染源允许攻击者输入添加properties到全局prototype,常见污染源如下:
- URL
- JSON输入
- Web信息
基于URL的prototype污染
|
|
当将查询字符串分解成键值对时,__proto__
可能被解释为任意字符串,合并到对象时不会合并到对象本身,而是分配给prototype,语句类似这样:
|
|
基于JSON 输入的prototype污染
用户可控的对象通常使用JSON.parse()
方法派生自JSON字符串。JSON.parse()
方法将JSON对象的任何key视作字符串,包括__proto__
这样的。
假设攻击者通过Web信息注入恶意的JSON:
|
|
再通过JSON.parse()
方法将它转换为JavaScript对象,生成的对象就会具有__proto__
这样的property。
|
|
如果这样的对象与现有对象合并,并且没有进行适当的过滤,就可以导致prototype污染。
例子
很多JavaScript库允许开发者给对象使用不用的配置选项。库代码检查开发人员是否显示地向对象添加属性,如果添加则会相应地调整配置。如果特定选项的property不存在就会使用预定义的默认选项。
|
|
假设库代码使用transport_url
向页面添加一个脚本引用
|
|
如果网站开发者为由为config
对象设置transport_url
property的话,这就是一个gadget。攻击者可以利用自己的transport_url
污染全局Object.prototype
,这将被config
对象继承。脚本的src
也被设置为攻击者指定的域名。
如果这个prototype可以被查询参数污染,受害者只需点击下方链接即可从攻击者指定的域中导入一个JS文件
|
|
攻击者可以直接诶嵌入XSS的payload,例如
|
|
上述URL后面的//
时为了绕过/example.js
后缀
发现漏洞
寻找污染点
手工挖掘
手工挖掘就是试错,尝试采用不同的方法向Object.prototype
添加任意的property。
1 .尝试去在一些地方注入任意的property,比如:
- vulnerable-website.com/?__proto__[foo]=bar
2 .在浏览器console检查Object.prototype
以确认这个property是否成功污染了它
3 .如果property没有被添加到全局prototype中,尝试不同的方法,比如用.
而不是[]
:
- vulnerable-website.com/?__proto__.foo=bar
使用 DOM Invader
用于代替手工挖掘
寻找gadget
手工挖掘
- 观察源代码并确认被使用的任何properties
- 拦截包含要测试的JavaScript的响应数据包
- 在脚本开头添加一个
debuger
,然后转发剩余的数据包 - 打开脚本被载入的页面,添加的
debuger
会暂停脚本的执行 - 此时在浏览器console输入以下命令:
|
|
这个property被记录到全局Object.prototype
。每次访问这个property的时候,浏览器都会将堆栈跟踪记录到console
- 继续执行脚本并且监视console。只要堆栈真的被记录了就可以确定这个property被访问了
- 展开堆栈跟踪并且使用它提供的链接跳转到正在读取property的代码所在的行
- 使用浏览器调试,逐步执行以查看这个property是否被传递给sink,比如
innerHTML
或eval()
使用DOM Invader
手工寻找gadget在目前网站通常依赖于大量第三方库的情况下是一个艰巨的任务。
通过constructor实现prototype污染
上面阐述的经典的prototype污染,一个常见的防御方法就是在合并用户可控对象之前去掉任何带有__proto__
的property。事实上有其他方法在不依赖__proto__
字符串的情况下引用Object.prototype
除非prototype为null,否则每个JS对象都有一个名为constructor
的property,其中包含对创建它的构造函数的引用。下面两条语句就是调用Object
构造函数:
|
|
也可以直接调用construcotr
:
|
|
函数实际上也是对象。每个构造函数都有一个叫做prototype
的porperty,它指向了被分配给由这个构造函数创建出的任何对象的prototype,所以也可以通过这个来访问对象的prototype
|
|
由此可见,myObject.constructor.prototype
等价于myObject.__proto__
,攻击的时候就有一个可供替代的选项
通过浏览器API实现prototype污染
fetch()
fetch
API能够简单地发送HTTP请求,fetch()
函数总共接收两个参数:
- URL
- 一个可以指定请求数据包一些参数的对象
下面这个例子展示了如何通过这个函数发送一个POST请求:
|
|
上述代码定义了method
和boby
两个properties,但还有一些prpperities没有定义。攻击者可以使用自己的headers
property污染Object.prototype
,然后被传递到fetch()
函数的对象继承,随即发送请求。
|
|
攻击者可以通过x-username
headers污染Object.prototype
:
|
|
Object.defineProperty()
开发者可以使用Object.defineProperty()
来使得对象有不可被修改的property:
|
|
上述代码看上去可以是一个合理的写法,实际上是有缺陷的。就像上边的fetch()
函数一样,Object.defineProperty()
也接收一个对象。开发者可以使用这个对象为正在定义的属性赋初值,不过如果只是为了防犯这个攻击,开发者可能不会设置一个初始值。
攻击者可以通过恶意value
这个property污染Object.prototype
绕过这个防御。如果它被传递给Object.defineProtoperty()
的对象继承了,用户可控的值可能最终就被分配给gadget property。