通过Server Push和SXG实现跨源web攻击的学习

前言

本文来自清华大学的一项研究

https://i.blackhat.com/BH-USA-25/Presentations/USA-25-Chen-Cross-Origin-Web-Attacks-via-HTTP2-Server-Push-and-Signed-HTTP-Exchange-Thursday.pdf

为了避免做重复机械的翻译工作,我会同时从一个水平一般的学习者的角度讲解我学到的东西,如有不当之处,务必指正

SOP是什么

SOP全称same-origin policy,翻译成中文就是同源策略

SOP是web安全的基石,它被设计用于防护跨源攻击

那么什么是同源什么是跨源呢,要弄清这个问题,我们需要先知道源的定义:由协议、主机名、端口号三者确定的三元组,(e.g. {“https”,”a.com”,”443”})只有这三部分完全相同时才叫做同源,很显然他是基于URI的(URI-based)

image-20251216223320979

图中的a.com和b.com显然为不同源的两个站点,SOP就像一个隔板,将这两个网站隔开,如果用户通过浏览器从a.com向b.com的服务器发送请求,同源策略会拦截b服务器返回的数据,比如常见的xss,在跨站攻击的时候可以通过iframe从一个网站引入另一个网站的资源,如果不同源,就会被SOP拦截

SAN是什么

SAN全称是Subject Alternative Name,翻译成中文是使用者备用名称

在TLS/SSL证书中,SAN的作用是解决单证书适配多域名的需求,在早期传统的证书中,存在一个CN字段,用于表示主域名,仅支持单域名,而SAN支持绑定多个域名,并且,除了域名,SAN还支持绑定IP,邮箱,URI

image-20251216225329616

这就是SAN字段可能包含的内容,与之相似的还有一个叫做通配符证书的东西,通配符证书也就是类似于*.example.com

它可以作用所有后缀为.example.com的子域名,显然灵活性远远不如SAN

HTTP/2 和 HTTP/3认为所有在证书的SAN里面的主域名是”同源“的,这里的同源和浏览器SOP认为的同源不完全一样

主要是视这些主机具有同一 TLS 权限

SAN-based origin

显然和前面的URI-based origin相比,基于SAN的同源要求更松,也就意味着它的灵活性更好,96%的证书在SAN列表中有多个域名,甚至3.2%的证书还包含来自不同组织的域名

image-20251217141829547

这种更为激进的“源”会给网络带来什么新的威胁?

在这之前,我们需要了解一些前置知识

HTTP/2 Server Push

image-20251217181945138

传统的没有Push的方式需要浏览器向服务器请求多次来获取所需的所有资源,这回浪费时间,影响用户体验。

而Push就是当用户请求了html之后,服务器就会直接把需要的资源比如css发到浏览器。

那么他是怎么实现的呢

通过Nginx的实现

配置文件conf/conf.d/default.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server {
listen 443 ssl http2;
server_name localhost;

ssl on;
ssl_certificate /etc/nginx/certs/example.crt;
ssl_certificate_key /etc/nginx/certs/example.key;

ssl_session_timeout 5m;

ssl_ciphers HIGH:!aNULL:!MD5;
ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
http2_push /style.css;
http2_push /example.png;
}
}

关注最后两行的内容,它表明如果用户请求根目录,就会自动push style.css和example.png这两个文件

通过Apache的实现

配置文件httpd.conf或者.htaccess

1
2
3
4
<FilesMatch "\index.html$">
Header add Link "</styles.css>; rel=preload; as=style"
Header add Link "</example.png>; rel=preload; as=image"
</FilesMatch>

通过后端实现

前面两种是通过代理的配置文件实现,略显繁琐

可以让后端应用返回一个HTTP回应的头信息Link命令,服务器看见这个命令,就会自动Push指定的文件

1
Link: </styles.css>; rel=preload; as=style, </example.png>; rel=preload; as=image

此时,再修改Nginx的配置

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 443 ssl http2;

# ...

root /var/www/html;

location = / {
proxy_pass http://upstream;
http2_push_preload on;
}
}

解决缓存问题

因为浏览器是有缓存的,他会把css等文件缓存到本地,这时再重复推送显然很浪费带宽。Nginx官方提出的一种解决办法是仅当用户第一次访问的时候才Push,根据Cookie来判断用户是否是第一次访问,配置方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 443 ssl http2 default_server;

ssl_certificate ssl/certificate.pem;
ssl_certificate_key ssl/key.pem;

root /var/www/html;
http2_push_preload on;

location = /demo.html {
add_header Set-Cookie "session=1";
add_header Link $resources;
}
}


map $http_cookie $resources {
"~*session=1" "";
default "</style.css>; as=style; rel=preload";
}

SXG

SXG全称是Signed HTTP Exchange

借助SXG,发布商可以对单个HTTP交换(即请求/响应对)进行签名,这可让发布商安全地确保内容可移植(即可供其他方重新分发),同时保留内容的完整性和出处。

image-20251217184652639

比如这里,a网站发布了一个内容,b网站作为一个服务商,可以让用户查看这个内容,在用户查看这个内容的时候,浏览器回加载这个Signed Exchange,它可以安全的在地址栏里面显示发布商的网址,也就是a.com。常见的场景是CDN。

如果想要不通过SXG来实现这个功能,则需要不同的网站共享TLS的密钥,这会带来一些安全问题。

image-20251217185116043

共同点

  • 他们都遵守SAN-based origin
  • 他们都可以通过服务器的响应在共享的证书中表明(欺骗)他们的“源”(通过:authority伪标头和request-url签名标头)

影响

攻击者可以Push/Provide资源到SAN列表中的其他站点,也就是CrossPush And CrossSXG 攻击

image-20251218093831312
  1. 获取一个与受害者网站共享的证书
  2. 引诱用户访问攻击者的网站
  3. 在攻击者服务器向用户Push内容时,因为与victim.com同在一个SAN列表中,所以他们拥有相同的TLS权限,攻击者可以通过伪造:authority来指向受害者的网站,攻击者也可以通过篡改 SXG 中的request-urlvalidity-url头部,虚假标注恶意内容的来源,指向victim.com
  4. 浏览器会接收到攻击者服务器Push的恶意脚本(比如XSS),将其视为来自同源的victim.com网站,并在访问victim.com时执行它

这使得路径外的攻击者借助共享证书发起可以实际落地的web攻击

1
2
3
tips:
路径内攻击者:处于受害者和目标服务器的网络传输路径中(比如中间路由器,代理节点),可以直接监听,篡改数据
路径外攻击者:不在这个传输路径中,无法直接监听和篡改数据,但可以借助其他漏洞发起攻击

攻击方式

借助HTTP body

通用的XSS

有一篇博客很好的讲解了这个过程: https://tttang.com/archive/1703/

image-20251218100946792

即使有CSP,也无法阻止这样的攻击

操纵Cookie

image-20251218101150976

原理

​ 在HTTP/2协议中,TLS权限的主体是按照SAN列表来定的,证书里的所有域名都是同一权限,而浏览器的同源策略是按 照URI的域名来判定的,这就导致了差异,如果攻击者服务器与victim.com同在一个SAN列表中,攻击者服务器在设置 domain=victim.com时,HTTP/2会认为这是合法的,从而导致浏览器认为这个设置也是合法的,于是允许了这次Set- Cookie

绕过HSTS

**HSTS:**HTTP Strict Transport Security,即HTTP严格传输安全,也就是强制浏览器仅通过HTTPS与服务器进行通信

image-20251218102349850

利用共享的证书,攻击者可以通过设置max-age=0,并且让浏览器误以为这是victim.com自己发的,从而让HSTS失效

恶意文件下载

image-20251218102658939

攻击者通过设置Conten-Disposition,attachment告诉浏览器这是一个附件,需要下载,同时利用共享的证书,让浏览器信任攻击者的服务器,下载了恶意的木马文件

如何让这些攻击方式落地?

要是实现这些攻击,考虑以下三个问题

  1. 如何获得与受害者服务器共享的证书
  2. 如何让攻击持久化
  3. 如何绕过潜在的对策,比如证书撤销

获得与受害者服务器共享的证书

利用一些缺陷

/.well-known目录中不安全的文件上传

1
2
/.well-known是HTTP协议中预定义的标准目录,用于存放网站对外提供的公共配置文件,其中./well-known/acme-challenge/是与证书签发强相关的路径,在签发证书时,如果使用“HTTP-01 挑战”验证域名所有权,CA会访问该路径下的特定文件,以此确认申请者对域名的控制权,验证通过后才会签发TLS证书
如果此目录未做严格的上传权限控制,攻击者可以上传CA要求的验证文件来伪造域名所有权的证明,非法申请包含目标域名的共享TLS证书

域名所有权下未受保护的 _acme-challengeDNS 记录

1
简单来说,就是在签发TLS证书的时候,CA机构需要确认申请者对域名的持有权,会要求进行验证,其中一种方式是“DNS-01”挑战,需要域名持有者在自己的 DNS 服务器中添加一条名为_acme-challenge.目标域名的 TXT 记录,记录值由 CA 提供。CA 会检查这条 DNS 记录是否存在且正确,以此确认 “申请者确实拥有该域名”,随后签发证书。如果_acme-challenge DNS 记录处于未受保护状态,攻击者可以篡改记录,那么攻击者就可以伪造受害者的身份来申请证书,获得与受害者共享的证书

邮件服务商在保护域名的管理邮箱地址时存在疏忽

1
域名注册时需要填写管理邮箱,如果攻击者掌控了这个邮箱,或者能拿到验证邮件,那么就可以掌控这个域名,申请证书

目前没有强制的措施能够保证域名的所有者和证书的所有者身份一致,那么攻击者是完全可以获取到共享的证书的

具体的获取方法

方法一:倒卖域名

​ 攻击者可以申请一张包含多个域名的证书,然后把其中的某个域名卖给受害者

方法二:域名接管

​ 有一些域名可能曾经被注册过,但是过期后没有继续续费,但是他曾经申请的TLS证书还是有效的,如果该域名与一些 重要的域名同在一个SAN列表中,攻击者就可以购买这个域名并对这些重要域名发起攻击

延长攻击持续时间

先介绍一下验证复用机制,在TLS生态中,CA为了简化验证流程,当用户首次通过DNS-01/HTTP-01/邮箱验证之后,CA会缓存这次验证结果,在一段有效期内,如果用户再次申请该域名的证书,无需重复进行验证,之间复用之前的缓存结果即可签发证书。攻击者先购买一个域名,在申请完证书之后就有了缓存,如果这个时候域名过期了,被受害者买去了,攻击者任然可以继续申请证书。

下图为不利用验证复用机制的情况下攻击可以持续的时间,在域名被买走之前攻击者申请了一个证书,攻击可以持续直到证书过期。

image-20251219202958245

而在利用验证复用的情况下,攻击持续时间会大大加长

image-20251219203227646

绕过防御策略

当受害者意识到攻击者获得了共享的证书时,肯定会想办法撤销证书。而这个证书包含了攻击者域名和受害者域名,想要撤销需要满足以下两点要求之一

  • 对证书上的所有域名执行并通过DOV认证(DOV:Domain-Validated Only,也就是对域名所有权进行严格的鉴定)
  • 拥有私钥

而受害者这两点要求都没办法满足,对于第一点,因为攻击者的域名也在证书上,显然受害者没法对其完成DOV认证;对于第二点,证书是攻击者签发的,私钥在攻击者那里,也没办法满足。

所以受害者无法撤销这样的共享证书

大规模测试

论文中,研究员们利用这个方法分别对客户端和服务端进行了真实的实验,验证了攻击的可行性,这部分省略,感兴趣的师傅可以查看原文

https://i.blackhat.com/BH-USA-25/Presentations/USA-25-Chen-Cross-Origin-Web-Attacks-via-HTTP2-Server-Push-and-Signed-HTTP-Exchange-Thursday.pdf

缓解攻击

从浏览器供应商

在浏览器中强制执行一致的authority(IP)以缓解CrossPUSH

强制单域名证书来缓解CrossSXG

从证书认证

在 SAN 列表中的域名所有者提出请求时,协助其将自己的域名从共享证书中移除

从用户

在注册域名时检查证书状态

CTF例题

题目分析

SECCON CTF 14(2025) Quals–broken challenge

这题考察的是xss,只有一个bot,可以让bot访问可控的网站,访问方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export const visit = async (url) => {
console.log(`start: ${url}`);

const browser = await puppeteer.launch({
headless: true,
executablePath: "/usr/bin/chromium",
args: [
"--no-sandbox",
"--disable-dev-shm-usage",
"--js-flags=--noexpose_wasm,--jitless",
"--disable-features=HttpsFirstBalancedModeAutoEnable",
],
});

const context = await browser.createBrowserContext();

try {
await context.setCookie({
name: "FLAG",
value: flag.value,
domain: "hack.the.planet.seccon",
path: "/",
});

const page = await context.newPage();
await page.goto(url, { timeout: 3_000 });
await sleep(5_000);
await page.close();
} catch (e) {
console.error(e);
}

await context.close();
await browser.close();

console.log(`end: ${url}`);
};

并且一个特定的自签名证书颁发机构 (cert.crt) 被安装在机器人的环境中,作为“受信任的根证书”。访问/'hint可以获取私钥

1
2
3
4
5
app.get("/hint", (req, res) => {
res.render("hint", {
hint: fs.readFileSync("./cert.key"),
});
});

因为该域名根本不存在,所以没办法进行DNS解析

思路

这题就可以利用前面提到的SXG,我们有了这个站点的证书,就可以利用起来伪造一个由hack.the.planet.seccon网站签发的SXG文件,内容是一个外带cookie的payload,然后让bot访问这个网页,浏览器会认为这个文件的发布者是hack.the.planet.seccon,于是就我们的外带的payload就可以读到cookie并发到我们的vps上,拿到flag

感想

认认真真翻译一篇论文,遇到不懂的就去查,学到了非常多的东西,尽管零零散散的看,前前后后花了四五天,但是还是挺有意义的。


通过Server Push和SXG实现跨源web攻击的学习
http://example.com/2025/12/16/通过Server Push和SXG实现跨源web攻击的学习/
作者
onehang
发布于
2025年12月16日
许可协议