nodejs 原型链污染
0x01 前言
最近真是忙滴焦头烂额,一堆事儿…… 因为也一直在看产品类的东西,所以学习也进入了一个瓶颈,今天打算先学点新东西。
大部分内容参考自 lx56 师傅的博客 https://blog.lxscloud.top/2022/11/13/nodejs%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/
0x02 前置基础知识
JavaScript
JavaScript(简称“JS”) 是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。虽然它是作为开发Web页面的脚本语言而出名,但是它也被用到了很多非浏览器环境中,JavaScript 基于原型编程、多范式的动态脚本语言,并且支持面向对象、命令式、声明式、函数式编程范式,是一门前端语言。
NodeJS
Node.js发布于2009年5月,由Ryan Dahl开发,是一个基于Chrome V8引擎的JavaScript运行环境,使用了一个事件驱动、非阻塞式I/O模型, 让JavaScript 运行在服务端的开发平台,它让JavaScript成为与 PHP、Python、Perl、Ruby 等服务端语言平起平坐的脚本语言。
简单来说是一门后端语言,可以解释 JavaScript
1. JavaScript 数据类型
let 和 var 关键字的区别
使用 var
或 let
关键字可以定义变量
let 和 var 的区别如下:
var
是全局作用域,let
只在当前代码块内有效- 当在代码块外访问
let
声明的变量时会报错 var
有变量提升,let
没有变量提升let
必须先声明再使用,否则报Uncaught ReferenceError xxx is not defined
;var
可以在声明前访问,只是会报 undefinedlet
变量不能重复声明,var
变量可以重复声明
普通变量
1 | var x=5; |
1 | let x=5; |
数组变量
1 | var a = new Array(); |
字典
1 | var a = {}; |
2. JavaScript 函数
在 Javascript 中,函数使用 function
关键字来进行声明
函数声明
声明一个函数 example
1 | function myFunction() { |
里面可传参可返回值
1 | function myFunction(a) { |
匿名函数
直接调用匿名函数
1 | (function(a)) { |
还可以把变量设成函数,调用 fn()
即调用了匿名函数的功能
1 | var fn = function() { |
闭包
假设在函数内部新建了一个变量,函数执行完毕之后,函数内部这个独立作用域或(封闭的盒子)就会删除,此时这个新建变量也会被删除。(有点像 PHP GC 回收机制)
如何令这个封闭的盒子是不会删除?可以使用“闭包”的方法(闭包涉及函数作用域、内存回收机制、作用域继承)
闭包后,内部函数可以访问外部函数作用域的变量,而外部的函数不能直接获取到内部函数的作用域变量
例如不使用额外的全局变量,实现一个计数器
因为 add
变量指定了函数自我调用的返回值(可以理解为计数器值保存在了 add
中), 每次调用值都加一而不是每次都是 1
1 | var add = (function () { |
3. JavaScript 类
在以前,如果要定义一个类,需要以定义“构造函数”的方式来定义,例如
1 | function newClass() { |
如果想添加一些方法呢?可以在内部使用构造方法
1 | function newClass() { |
为了简化编写 JavaScript 代码,ECMAScript 6
后增加了class语法
class 关键字
可以使用 class 关键字来创建一个类
形式如下(如果不定义构造方法,JavaScript 会自动添加一个空的构造方法)
1 | class ClassName { |
例子
1 | class myClass { |
使用 new 创建对象
1 | let testClass = new myClass("testtest"); |
测试
查看 testClass
对象的 test
属性的值,为 testtest
1 | console.log(testClass.test); |
往对象添加属性
直接使用 .
属性名即可,例如向 testClass
添加 aaa
属性
1 | testClass.aaa = 333; |
类的方法
形式如下
1 | class ClassName { |
4. NodeJS 的简单使用
- 安装 NodeJS express 服务器
1 | npm install express --save-dev |
- 编写一段源代码
1 | /* |
写完代码后运行 server
1 | node ezWebServer.js |
0x03 原型链污染
1. 什么是原型(JavaScript 原型链继承)
这里的原型指的是 prototype
,比如说上面前言部分讲的 JavaScript 类那里
我们使用 new
新建了一个 newClass
对象给 newObj
变量
1 | function newClass() { |
实际上这个 newObj
变量使用了原型(prototype)来实现对象的绑定【而不是绑定在“类”中,与 JavaScript 的特性有关,它的“类”与其它语言(例如 JAVA、C++)类不同,它的“类”基于原型】
prototype
是 newClass
类的一个属性,而所有用 newClass
类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法(这里和 Java 反射的概念挺像的),如下
简单来说就是:
prototype
是newClass
类的一个属性newClass
类实例化的对象newObj
不能访问prototype
,但可以通过.__proto__
来访问newClass
类的prototype
newClass
实例化的对象newObj
的.__proto__
指向newClass
类的prototype
这其实就导致了“未授权”的出现,用这一段代码来表示更为直观
关系可以用这一张图来表示
2. 原型链污染原理
现在已经知道实例化的对象的 .__proto__
指向类的 prototype
那么修改了实例化的对象的 .__proto__
的内容, 类的 prototype
的内容是否也会发生改变?
答案是肯定的,这就是原型链污染的利用方法。
这其实就有点像在上 C 语言或者其他编程语言课的时候,老师很喜欢说 copy 与 merge,被 new 出来的实例会影响到本身那一个对象,途径是 .__proto__
方法。
3. 哪些情况下原型链会被污染
在实际应用中,哪些情况下可能存在原型链能被攻击者修改的情况呢?
我们思考一下,哪些情况下我们可以设置 __proto__
的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:
- 对象 merge
- 对象 clone(其实内核就是将待操作的对象 merge 到一个空对象中)
以对象 merge 为例,我们想象一个简单的 merge 函数:
1 | function merge(target, source) { |
在合并的过程中,存在赋值的操作 target[key] = source[key]
,那么,这个 key 如果是 __proto__
,是不是就可以原型链污染呢?
我们用如下代码实验一下:
1 | let o1 = {} |
结果是,合并虽然成功了,但原型链没有被污染:
这是因为,我们用 JavaScript 创建 o2 的过程(let o2 = {a: 1, "__proto__": {b: 2}}
)中, __proto__
已经代表 o2 的原型了,此时遍历 o2 的所有键名,你拿到的是 [a, b]
,__proto__
并不是一个key,自然也不会修改 Object 的原型。
那么,如何让 __proto__
被认为是一个键名呢?
我们将代码改成如下:
1 | let o1 = {} |
可见,新建的 o3 对象,也存在 b 属性,说明 Object 已经被污染:
这是因为,JSON 解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历 o2 的时候会存在这个键。
merge 操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
4. 原型链污染例题
例题一、CatCTF 2022 wife(越权)
一道简单的 js 原型链污染,造成的漏洞是越权。题目逻辑很简单,要邀请码才能注册为admin,普通用户只能拿到 wife,没有 flag。
看一下注册的逻辑
1 | app.post('/register', (req, res) => { |
稍微搜一下 Object.assign
可以发现这个方法是可以触发原型链污染的,然后污染 __proto__.isAdmin
为 true 就可以了。当然这个题目当时是黑盒,所以需要 fuzz 一下,不放 hint 相当难做。
贴一个 payload
1 | {"__proto__":{"isAdmin":true} |
- 如此便可以造成越权,拿到 flag
例题2、通过原型链污染的 http://prompt.ml/13 一道 xss 攻击
以下摘自 https://xz.aliyun.com/t/7182
源代码
1 | function escape(input) { |
我们分析下题目:
1 | function extend(obj) { |
这个函数 extends
可以接收多个参数,然后赋值给了 source
变量,接着就对 obj
对象的键值进行了赋值操作,这个函数是可以导致原型污染链攻击的,但是具体怎么攻击我们还不知道,继续分析下去。
1 | var data = JSON.parse(input); //这里获取输入并且进行json解析 |
简单来说,需要我们传参传入 source 触发 xss,所以这里我们不妨采用原型链污染的方式去污染 config.__proto__['source']
试一试
可以看到的确可以这样子玩的,不过这里还有个"
的过滤,
1 | {"source":"%","__proto__": {"source": "123'"}} |
这样我们就能逃逸出第一个正则了,但是绕过 "
,我们可以考虑下 replace
一些性质
1 | '<img src="{{source}}">'.replace('{{source}}', source); |
我们看下文档:
字符串 stringObject 的 replace() 方法执行的是查找并替换的操作。它将在 stringObject 中查找与 regexp 相匹配的子字符串,然后用 replacement 来替换这些子串。如果 regexp 具有全局标志 g,那么 replace() 方法将替换所有匹配的子串。否则,它只替换第一个匹配子串。
replacement 可以是字符串,也可以是函数。如果它是字符串,那么每个匹配都将由字符串替换。但是 replacement 中的
$
字符具有特定的含义。如下表所示,它说明从模式匹配得到的字符串将用于替换。
我们可以利用第二个参数做点事情:
1 | '123'.replace("2",'$`'); |
利用RegExp对象的 "
来闭合自己
最终payload:
1 | {"source":"%","__proto__": {"source": "$` onerror=prompt(1)><!--"}} |
0x04 原型链污染的 rce 与调试分析
- 本文标题:JavaScript 原型链污染
- 创建时间:2022-12-29 11:19:48
- 本文链接:2022/12/29/JavaScript-原型链污染/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!