justCTF 2025JSpositive_player知识

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__constructorprototype

原型污染指的是攻击者能够修改应用程序或库使用的对象原型(通常是 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 对象持有的一些方法被隐式调用(如toStringvalueOf)。

攻击者可以污染 Object.prototype.someattr 并改变为一个程序非预期的值,如IntObject,可能导致程序无法正常工作,从而造成DoS。

原型污染防范
  1. 过滤关键字: __proto__prototypeconstructor 属性

  2. 避免使用不规范的递归。即使使用也要严格检查key,不能是__proto__constructor

  3. 考虑使用不带原型的对象,从而打断原型链。如Object.create(null)

  4. 使用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)的属性,如 constructorhasOwnPropertyisPrototypeOftoString

users[req.session.userId] 本质上是获取users的一个属性,所以 req.session.userId 不一定是合法的用户名(如 user1),也可以是 users 的其它属性(如 constructorhasOwnPropertyisPrototypeOftoString 等),只要保证users[某属性] 返回非空的结果即可

然后确保 users[某属性].isAdmin 值为 1,就可以使得 if 条件判断为真,进获取 FLAG

综上所述,攻击思路如下:

​ 1)通过原型链污染原型Object的属性(如 constructorhasOwnPropertytoString 等),在该受污染属性上添加 isAdmin 、并将值设置为1 。

​ 2)尝试以该属性名称为 username 注册/登录系统,则 users[受污染属性].isAdmin 值为1 ,然后访问 /flag 即可获取FLAG。

第1步前面已讨论过,存在递归合并函数 deepMerge ,可以实现原型链污染。下面讨论第2步如何实现以特殊用户(属性名称)登录系统

五、登录认证绕过

尝试以原型Object的属性名称注册/登录用户,以 toString 为例(使用其它继承自Object的属性,如 constructorhasOwnPropertytoLocalString 等都可以)。

理想情况是先注册、再登录。但是在注册时提示用户已经存在:

查看注册相关的代码:

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 。当 usernametoString时,因为users自身不包含 toString 属性,故users[username] 返回的是users.__proto__.toStringObject.toString 方法。如下:

users[username]非空,返回用户已存在,所以没有办法再次注册。

尝试 toString/123 直接登录,调试发现执行到304行验证用户名和密码时,关键变量的值如下:

user 值为 Object.toString() 方法,非空;

user.password == toString().password == undefined

故要想通过304行的检查,将变量 password 的值设置为 undefined 即可。这样就可以绕过认证,以用户名 toString 成功登录系统。

六、攻击步骤

结合以上分析,可执行的攻击步骤如下:

  1. 污染原型属性 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:

  1. 登录为 toString 用户

污染成功后,使用 toString用户名登录,password处随便填写,使用bp抓包后删除password字段后发送

页面跳转后说明登录成功,然后访问 /flag 成功

题目总结
考察点
  1. JS原型链污染漏洞利用及扩展
  2. 登录认证函数逻辑漏洞的识别与利用
关键技巧
  1. 原型污染扩展

​ 利用原生属性(如 toString)绕过对 __proto__ 的过滤。

​ 通过 deepMerge 将污染扩散到原型链。

  1. 认证函数逻辑漏洞

​ 利用 users 对象继承 Object.prototype 的特性,使 toString 成为"已存在用户"。

​ 通过 undefined === undefined 绕过密码检查。

参考链接:https://gist.github.com/terjanq/fa6f19d46bcb85bb61c146747dec0758#positive-players--write-up-by-terjanq

相关推荐
oliveira-time2 小时前
原型模式中的深浅拷贝
java·开发语言·原型模式
2501_941111462 小时前
C++中的原型模式
开发语言·c++·算法
亿坊电商2 小时前
PHP框架的资源管理机制如何优雅适配后台任务?
开发语言·php
VBA63372 小时前
YZ系列工具之YZ09: VBA_Excel之读心术
开发语言
pro_or_check2 小时前
自然语言编程:从一段Perl程序说起
开发语言
ᐇ9592 小时前
Java集合框架实战:HashMap与HashSet的妙用
java·开发语言
孤狼warrior3 小时前
公司信息建设库数据 使用调用堆栈的JS逆向爬虫
javascript·爬虫
csbysj20203 小时前
Scala 异常处理
开发语言
MediaTea3 小时前
Python 第三方库:cv2(OpenCV 图像处理与计算机视觉库)
开发语言·图像处理·python·opencv·计算机视觉