#随便扯两句程度的导入
今天去打了省预赛,web第一题就是原型链污染(提示直接写在源码上了),刚好mini上做过同个考点的题,本来觉得必拿下啊,结果在提交post请求时一直爆炸(Missing id,详见后方源码)。
唔姆,其实就算确实能post成功也做不来的,因为我之前做过python的原型链污染,当时没去研究过js的(说起来js的这个考点更经典一些啊,没看真是疏忽了🥲)
坐牢三个钟头只做出那么一道签到题,这下我终于意识到会的东西不写写立马就不会了,content-type这些东西也太依赖复制粘贴,总之就是实力都是互联网给的,不是自己的,唉。既然如此,就写点笔记巩固以下好了
#什么是原型链?
请详见这里,写的很详细也很通俗易懂~
#什么是原型链污染
由于访问对象时,对象可以访问其原型的属性方法,那么当然也可以修改他们,并且,由于原型链继承机制,如果实例访问某属性发现未定义,则会尝试访问其原型是否有此属性,所以改变原型就能够影响其所有子类。这就是较为粗略的原型链污染的解释。
当然,也不是所有情况下都能够污染成功的。必须要在对象获取属性属于深拷贝的情况下才能够通过对象修改原型,否则只是将对象的原型指向另一个对象罢了,这样就不能达到修改的目的了。
一个例子就是Object.assign(target, …sources)这个方法。他执行的是浅拷贝,这意味着如果源对象(sources)的属性值是对象的引用,它只会复制引用值,而不是对象本身(所以是不会对sources产生任何影响的)。
因此,如果源对象的属性值是对象,那么目标对象拷贝得到的是这个对象的引用。
所以能够进行原型链污染的话基本上会存在一个用户自定义(或者第三方库定义,用户复用)的不安全的递归合并函数(也就是能够通过递归不断向上访问原型,并且通过深拷贝获得他们的属性值的函数)。
直接拿这次省赛预赛的题目举例子吧,下面是源码。
const express = require('express');
const _ = require('lodash');
const fs = require('fs');
const app = express();
app.use(express.json());
// 存储笔记的对象
const notes = {};
// 创建新笔记
app.post('/api/notes', (req, res) => {
const noteId = req.body.id;
const noteData = req.body;
if (!noteId) {
return res.status(400).json({ error: 'Missing id' });
}
// 使用lodash.merge,该版本存在原型链污染漏洞
notes[noteId] = {};
_.merge(notes[noteId], noteData);
console.log('Note prototype:', Object.getPrototypeOf(notes[noteId]));
console.log('Note properties:', notes[noteId]);
res.json(notes[noteId]);
});
// 获取笔记
app.get('/api/notes/:id', (req, res) => {
const noteId = req.params.id;
if (!notes[noteId]) {
return res.status(404).json({ error: 'Note not found' });
}
res.json(notes[noteId]);
});
// 获取flag (仅管理员可访问)
app.get('/api/flag', (req, res) => {
const noteId = req.headers['note-id'];
if (!noteId || !notes[noteId]) {
return res.status(403).json({ error: 'Authentication required' });
}
if (!notes[noteId].isAdmin) {
return res.status(403).json({ error: 'Admin access required' });
}
try {
const flag = fs.readFileSync('/flag', 'utf8');
res.json({ flag: flag.trim() });
} catch (err) {
res.status(500).json({ error: 'Error reading flag' });
}
});
app.listen(8000, () => {
console.log('Server running on port 8000');
});
可以看到其中有一条非常明显的注释,相当于是把答案直接告诉我们了(当然我不知道js的原型链污染我做不来一点🥲)。
这里merge函数的意思也很明确,就是将noteData中的数据分配给notes[noteId]这个对象。并且根据下面获取flag的代码我们可以发现notes[noteId]对象有一个isAdmin的属性,只要它true,就能通过携带note-id这个请求头标识来获取flag了。
根据原型链的知识,只要这样构造:
{
"id":1,
"__proto__":
{
"isAdmin":true
}
}
这样就可以直接访问到notes[noteId]的原型并且将其isAdmin属性设置为true,之后获取flag就畅通无阻了。
所以真正的了解漏洞的原理是非常重要的,这样就能更灵活的做题了😋