justCTF 2025
positive_player
前置知识–JS原型链污染
相关属性
一、proto_ 属性
每个对象实例都有的一个内部属性,指向该对象的“原型对象”,即该对象从哪里继承属性和方法
可通过 __proto__ 访问其原型对象
二、prototype 属性
函数(包括构造函数)的一个属性
本质上是一个模板对象,新实例会继承它上面的属性和方法
三、constructor 属性
默认存在于函数的 prototype 对象上。
指向创建该 prototype 对象的构造函数本身。如 A.prototype.constructor 默认指向 A。
重要关系
任意对象可通过属性 __proto__ 访问其原型对象,即 obj.__proto__ == Object.prototype
实例对象可以通过原型链访问到 constructor 属性,即:obj.constructor.prototype === Object.prototype。比如:
原型链污染
JavaScript类的所有属性都允许被公开的访问和修改,包括属性 __proto__ ,constructor和prototype。
原型污染指的是攻击者能够修改应用程序或库使用的对象原型(通常是 Object.prototype)的属性,且被所有经过该原型链的对象所继承,从而导致不可预期的行为,如拒绝服务攻击(通过触发JavaScript异常)或者远程代码执行等。
原型链污染目的是在Object.prototype上造成污染,主要有两种场景:
不安全的对象递归合并
按路径定义属性
不安全的对象递归合并
递归合并函数merge()的基本逻辑和代码如下:
1 | // 符合模式一:obj[a][b] = value |
若 source 包含可枚举属性 __proto__, 则可以新增/修改 tagret[__proto__] 属性
以下代码存在原型链污染漏洞:
1 | let obj = {}; |
即使 user 是新对象,也“自动”获得了 isAdmin: true。
按路径定义属性
有些JavaScript库的函数支持根据指定的路径修改或定义对象的属性值。如以下的函数:
1 | // 将对象object的指定路径path的属性值修改为value |
如果攻击者可以控制路径path的值,那么将路径设置为_proto_.value,运行theFunction函数后就有可能将value属性注入到object的原型中
如joint.js中的代码:
1 | export const setByPath = function(obj, path, value, delimiter) { |
setByPath函数在对象 obj 中,将 path 路径对应的属性设置为 value
输入以下的路径,那就会造成原型污染:
1 | const jointjs = require("jointjs"); |
漏洞危害
权限提升
假设后端检查权限,通过污染让所有对象都有 isAdmin: true → 直接获得管理员权限
1 | if (user.isAdmin) grantAdminAccess(); |
绕过属性检查
如果污染了 Object.prototype.hasOwnProperty,就可以绕过检查
1 | for (let key in obj) { |
远程代码执行
通常发生在代码程序执行了对象上的一个特殊属性。如
1 | eval(someobject.someattr) |
在这种情况下,如果攻击者污染了 Object.prototype.someattr ,那么就可能导致远程代码执行。
DOS
通常发生在 Object 对象持有的一些方法被隐式调用(如toString 和 valueOf)。
攻击者可以污染 Object.prototype.someattr 并改变为一个程序非预期的值,如Int 或 Object,可能导致程序无法正常工作,从而造成DoS。
原型污染防范
过滤关键字:
__proto__、prototype和constructor属性避免使用不规范的递归。即使使用也要严格检查
key,不能是__proto__和constructor。考虑使用不带原型的对象,从而打断原型链。如
Object.create(null)。使用
Map替换Object。
题目描述
由 Gemini 生成的 Express 应用,包括用户注册、登录功能和自定义主题等功能。
注册用户 user1/123 ,登录之后可看到的页面如右图
解题思路
一、定位关键字 flag
分析源码,搜索关键字 flag ,发现如下代码:
1 | // 15. Define the `/flag` endpoint (protected) |
代码逻辑如下:
1)用户访问 /flag 页面时触发该处理器
2)首先验证用户是否成功登录
3)若成功登录,判断是否具备管理员权限;若具备则返回FLAG,反之返回 Not admin
结合上述分析,需要一个具备管理员权限的用户,登录成功后访问 /flag 路径即可获得FLAG。
二、定位危险函数 deepMerge
分析源码发现,定义了递归合并函数 deepMerge ,代码如下:
1 | // 6. A function to recursively merge objects |
函数作用是将 source 对象的所有可枚举属性地复制到 target(有可能为{})上。
存在递归合并函数时,考虑原型链污染漏洞。
三、分析原型链污染的可能性
查找函数调用链,发现 app.get('theme',....) 调用 deepMerge ,部分代码如下:
1 | // 15. Define the `/theme` endpoint (protected) |
但是在调用 parseQueryParams 函数之前,先调用parseQueryParams函数解析请求参数,过滤了 ['__proto__', 'prototype', 'constructor'] 等关键字。代码如下:
1 | // 7. A function to parse a query string with dot-notation keys. |
故直接污染 object.prototype.isAdmin的思路行不通
四、原型链污染扩展
再次查看获取 flag 的相关代码
1 | // 15. Define the `/flag` endpoint (protected) |
关键代码是 users[req.session.userId].isAdmin == true ,其中:
req.session.userId 在用户登录认证通过后赋值为 username
users 初始化为 {} ,在用户注册成功后存入数据 { username: { password, userThemeConfig, isAdmin } }
可知users.__proto__ 就是 Object.prototype ,如下:
故users 除了自身的属性:user1 之外;还有继承自原型(即 Object)的属性,如 constructor,hasOwnProperty,isPrototypeOf 和 toString 等
users[req.session.userId] 本质上是获取users的一个属性,所以 req.session.userId 不一定是合法的用户名(如 user1),也可以是 users 的其它属性(如 constructor,hasOwnProperty,isPrototypeOf 和 toString 等),只要保证users[某属性] 返回非空的结果即可
然后确保 users[某属性] 的.isAdmin 值为 1,就可以使得 if 条件判断为真,进获取 FLAG 。
综上所述,攻击思路如下:
1)通过原型链污染原型Object的属性(如 constructor,hasOwnProperty和 toString 等),在该受污染属性上添加 isAdmin 、并将值设置为1 。
2)尝试以该属性名称为 username 注册/登录系统,则 users[受污染属性].isAdmin 值为1 ,然后访问 /flag 即可获取FLAG。
第1步前面已讨论过,存在递归合并函数 deepMerge ,可以实现原型链污染。下面讨论第2步如何实现以特殊用户(属性名称)登录系统
五、登录认证绕过
尝试以原型Object的属性名称注册/登录用户,以 toString 为例(使用其它继承自Object的属性,如 constructor,hasOwnProperty和 toLocalString 等都可以)。
理想情况是先注册、再登录。但是在注册时提示用户已经存在:
查看注册相关的代码:
1 | app.post('/register', (req, res) => { |
如前所述,users 的原型为 Object 。当 username 为 toString时,因为users自身不包含 toString 属性,故users[username] 返回的是users.__proto__.toString 即Object.toString 方法。如下:
users[username]非空,返回用户已存在,所以没有办法再次注册。
尝试 toString/123 直接登录,调试发现执行到304行验证用户名和密码时,关键变量的值如下:
user 值为 Object.toString() 方法,非空;
user.password == toString().password == undefined
故要想通过304行的检查,将变量 password 的值设置为 undefined 即可。这样就可以绕过认证,以用户名 toString 成功登录系统。
六、攻击步骤
结合以上分析,可执行的攻击步骤如下:
- 污染原型属性
Object.toString
原型链污染发生的函数为 deepMerge,其调用链为:
app.get('/theme',isAuthenticated,...) –> parseQueryParams –> deepMerge
访问 /theme 时需要用户已经登录,故先注册、登录普通用户 user1 ,然后通过传递 toString.isAdmin=1 的查询参数,污染 Object.prototype.toString,在toString上添加 isAdmin、并将值置为 1
1 | // 原型污染 url |
访问过程中parseQueryParams 函数会生成对象:
1 | { toString: { isAdmin: "1" } } |
随后调用 deepMerge 函数合并对象时,会将 { isAdmin: "1" } 合并到 Object.prototype.toString 上,导致所有对象的 toString.isAdmin 被污染。下图是污染前后 的toString ,可以看到污染后的 toString 多了 isAdmin 属性,且值为1:
- 登录为
toString用户
污染成功后,使用 toString用户名登录,password处随便填写,使用bp抓包后删除password字段后发送
页面跳转后说明登录成功,然后访问 /flag 成功

题目总结
考察点
- JS原型链污染漏洞利用及扩展
- 登录认证函数逻辑漏洞的识别与利用
关键技巧
- 原型污染扩展:
利用原生属性(如 toString)绕过对 __proto__ 的过滤。
通过 deepMerge 将污染扩散到原型链。
- 认证函数逻辑漏洞:
利用 users 对象继承 Object.prototype 的特性,使 toString 成为“已存在用户”。
通过 undefined === undefined 绕过密码检查。