justCTF 2025

positive_player
前置知识--JS原型链污染
原型链概念
JavaScript 的每个对象拥有一个原型对象,以该原型为模板、继承方法和属性;原型对象也可能拥有原型,并从中继承方法和属性,以此类推。这种关系常被称为原型链 (prototype chain)。
任何对象的祖先原型都是 object.prototype。
相关属性
一、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()的基本逻辑和代码如下:
javascript
// 符合模式一:obj[a][b] = value
function merge(target, source) {
for (let key in source) {
if (typeof target[key] === 'object' && typeof source[key] === 'object') {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
若 source 包含可枚举 属性 __proto__, 则可以新增/修改 tagret[__proto__] 属性
以下代码存在原型链污染漏洞:
javascript
let obj = {};
merge(obj, JSON.parse('{"__proto__": {"isAdmin": true}}'));
// 此时 obj.__proto__.isAdmin = true
// 而 obj.__proto__ 就是 Object.prototype(因为 obj 是 {})
// 所以 Object.prototype.isAdmin = true
let user = {};
console.log(user.isAdmin); // true
即使 user 是新对象,也"自动"获得了 isAdmin: true。
按路径定义属性
有些JavaScript库的函数支持根据指定的路径修改或定义对象的属性值。如以下的函数:
javascript
// 将对象object的指定路径path的属性值修改为value
theFunction(object, path, value)
如果攻击者可以控制路径path的值,那么将路径设置为_proto_.value,运行theFunction函数后就有可能将value属性注入到object的原型中
如joint.js中的代码:
javascript
export const setByPath = function(obj, path, value, delimiter) {
const keys = Array.isArray(path) ? path : path.split(delimiter || '/');
const last = keys.length - 1;
let diver = obj;
let i = 0;
for (; i < last; i++) {
const key = keys[i];
const value = diver[key];
// diver creates an empty object if there is no nested object under such a key.
// This means that one can populate an empty nested object with setByPath().
diver = value || (diver[key] = {}); //自动创建中间层级:如果路径中的某层不存在,自动创建为{}
}
diver[keys[last]] = value;
return obj;
};
setByPath函数在对象 obj 中,将 path 路径对应的属性设置为 value
输入以下的路径,那就会造成原型污染:
javascript
const jointjs = require("jointjs");
const obj = {};
console.log("Before : " + obj.polluted);
jointjs.util.setByPath({ }, '__proto__/polluted', "yes", '/');
console.log("After : " + obj.polluted);
漏洞危害
权限提升
假设后端检查权限,通过污染让所有对象都有 isAdmin: true → 直接获得管理员权限
javascript
if (user.isAdmin) grantAdminAccess();
绕过属性检查
如果污染了 Object.prototype.hasOwnProperty,就可以绕过检查
javascript
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 以为安全
}
}
远程代码执行
通常发生在代码程序执行了对象上的一个特殊属性。如
javascript
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 ,发现如下代码:
javascript
// 15. Define the `/flag` endpoint (protected)
app.get('/flag', isAuthenticated, (req, res, next)=>{
if(users[req.session.userId].isAdmin == true){
return res.end(FLAG);
}
return res.end("Not admin :(");
});
代码逻辑如下:
1)用户访问 /flag 页面时触发该处理器
2)首先验证用户是否成功登录
3)若成功登录,判断是否具备管理员权限;若具备则返回FLAG,反之返回 Not admin
结合上述分析,需要一个具备管理员权限的用户,登录成功后访问 /flag 路径即可获得FLAG。
二、定位危险函数 deepMerge
分析源码发现,定义了递归合并函数 deepMerge ,代码如下:
javascript
// 6. A function to recursively merge objects
const deepMerge = (target, source) => {
for (const key in source) {
if (source[key] instanceof Object && key in target) {
Object.assign(source[key], deepMerge(target[key], source[key]));
}
}
Object.assign(target || {}, source);
return target;
};
函数作用是将 source 对象的所有可枚举属性地复制到 target(有可能为{})上。
存在递归合并函数时,考虑原型链污染漏洞。
三、分析原型链污染的可能性
查找函数调用链,发现 app.get('theme',....) 调用 deepMerge ,部分代码如下:
javascript
// 15. Define the `/theme` endpoint (protected)
app.get('/theme', isAuthenticated, (req, res) => {
......
// 解析请求参数,过滤关键字 __proto__,prototype,constructor等
const parsedUpdates = parseQueryParams(queryString);
if (Object.keys(parsedUpdates).length > 0) {
// 调用deepMerge
user.themeConfig = deepMerge(user.themeConfig, parsedUpdates);
}
......
});
但是在调用 parseQueryParams 函数之前,先调用parseQueryParams函数解析请求参数,过滤了 ['__proto__', 'prototype', 'constructor'] 等关键字。代码如下:
javascript
// 7. A function to parse a query string with dot-notation keys.
const parseQueryParams = (queryString) => {
.....
for (const [key, value] of params.entries()) {
const path = key.split('.');
.....
// 过滤关键字
if(['__proto__', 'prototype', 'constructor'].includes(part)){
part = '__unsafe$' + part;
}
......
return result;
};
故直接污染 object.prototype.isAdmin的思路行不通
四、原型链污染扩展
再次查看获取 flag 的相关代码
javascript
// 15. Define the `/flag` endpoint (protected)
app.get('/flag', isAuthenticated, (req, res, next)=>{
// 关键判断
if(users[req.session.userId].isAdmin == true){
return res.end(FLAG);
}
return res.end("Not admin :(");
});
关键代码是 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 等都可以)。
理想情况是先注册、再登录。但是在注册时提示用户已经存在:

查看注册相关的代码:
javascript
app.post('/register', (req, res) => {
const { username, password } = req.body;
// 判断用户名是否存在
if (users[username]) {
req.session.errorMessage = 'User already exists!';
return res.redirect('/register');
}
......
};
如前所述,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
python
// 原型污染 url
http://192.168.43.148:3000/theme?toString.isAdmin=1
访问过程中parseQueryParams 函数会生成对象:
javascript
{ 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 绕过密码检查。