CVE-2019-14899:探测并劫持VPN隧道TCP连接

 

0x00 前言

最近我们发现了Linux、FreeBSD、OpenBSD、MacOS、iOS以及Android中的一个漏洞,恶意访问点(access point,ap)或者邻近用户可以利用该漏洞判断相连用户是否在使用VPN链路、推测其他用户正在访问的站点、确定正在使用的TCP序列号(Sequence Number)及确认号(Acknowledgment Number),最终成功将数据注入TCP流中。利用这些信息,攻击者可以劫持VPN隧道内的活动连接。

该漏洞适用于OpenVPN、WireGuard以及IKEv2/IPSec型VPN,我们还没有对tor进行完整测试,但由于tor工作在SOCKS层,涉及用户空间中的身份认证及加密,因此我们相信tor并没有受该漏洞影响。然而需要注意的是,该漏洞效果并不会因为用户具体使用的VPN技术而受到影响,即使来自用户的响应数据经过加密,我们还是可以根据数据包的大小及发送的数据包数量(比如challenge ACK)来判断正在通过加密VPN隧道发送的数据包类型。

我们已经在今年早些时候向Android报告了与该问题相关的一个漏洞,拿到的编号为CVE-2019-9461。在这个漏洞的CVE描述中,Android设备会响应通过无线接口发送给用户虚拟IP地址的未经请求的数据包,但这并没有解释清这种攻击的根本问题,因此Android的最新补丁并没有修改关于反向路径(reverse path)的相关设置。

在Ubuntu 19.10发行之前,这种攻击方法并不适用于我们测试过的Linux发行版,后面我们注意到受影响的系统中rp_filter被设置为“松散”(loose)模式。2018年11月28日,systemd仓库中sysctl.d/50-default.conf的默认设置从“严格”(strict)模式更改为“松散”模式。因此在此日期之后,如果发行版使用的是未修改配置的systemd,那么就会受该漏洞影响。我们测试的大多数Linux发行版使用的是其他init系统,对应的值为0,这也是Linux内核的默认值。

 

0x01 漏洞复现

在漏洞报告中,我们描述了在Linux系统上复现该漏洞的过程,也介绍了攻击过程在不同架构上的区别。

执行该攻击需要3个步骤:

1、探测VPN客户端的虚拟IP地址;

2、使用虚拟IP地址来推测活动的连接;

3、根据对未经请求数据包的加密响应来确定活动连接的序列号及确认号,以便劫持TCP会话。

为了复现攻击过程,我们需要使用4个组件:

1、受害者设备(连接到AP,地址为192.168.12.x10.8.0.8);

2、AP(攻击者可控,地址为192.168.12.1);

3、VPN服务器(攻击者不可控,地址为10.8.0.1);

4、Web服务器(攻击者不可控,实际环境中的某个公网IP地址)。

受害者设备连接到某个AP,在测试环境中,该AP为运行create_ap的一台笔记本。随后,受害者设备与VPN服务商建立连接。

AP可以在整个虚拟IP空间中(OpenVPN的默认地址范围为10.8.0.0/24)向受害者发送SYN-ACK报文,从而判断受害者的虚拟IP地址。当SYN-ACK发送到受害者设备的正确虚拟IP时,设备会响应RST包;当SYN-ACK发送到错误的虚拟IP地址时,攻击者不会收到任何数据。

为了快速演示这种差别,我们可以在运行create_ap的AP设备上使用nping命令,其中源IP为AP网关,目的IP为分配给VPN客户端tun接口的虚拟IP地址。ap0create_ap在攻击者设备上创建的接口,目的MAC地址为受害者无线网卡的MAC地址。

例如,如下命令可以让通过正确地址让受害者生成RST响应:

nping --tcp --flags SA --source-ip 192.168.12.1 --dest-ip 10.8.0.8 --rate 3 -c 3 -e ap0 --dest-mac 08:00:27:9c:53:12

而如下命令中的地址不正确,因此受害者不会返回响应包:

nping --tcp --flags SA --source-ip 192.168.12.1 --dest-ip 10.8.0.9 --rate 3 -c 3 -e ap0 --dest-mac 08:00:27:9c:53:12

与之类似,如果想测试受害者是否与特定网站(如64.106.46.56)存在活动连接,我们可以从64.106.46.56的80端口(或443端口)向受害者IP所使用的整个端口空间发送SYNSYN-ACK报文。如果四元组正确,那么受害者每秒最多返回2个ACK响应;如果四元组不正确,受害者会针对收到的每个报文返回RST响应。

为了快速测试这种场景,我们可以在受害者设备上创建一个netcat连接,命令如下:

Netcat 64.106.46.56 80 -p 40404

生成challenge ACK的正确四元组如下所示:

nping --tcp --flags SA --source-ip 64.106.46.56 -g 80 --dest-ip 10.8.0.8 -p 40404 --rate 10 -c 10 -e ap0 --dest-mac 08:00:27:9c:53:12

如果使用如下不正确的四元组,则会对收到的每个报文生成一个RST:

nping --tcp --flags SA --source-ip 64.106.46.56 -g 80 --dest-ip 10.8.0.8 -p 40405 --rate 10 -c 10 -e ap0 --dest-mac 08:00:27:9c:53:12

最后,一旦攻击者检测到目标用户与外部服务器建立活跃TCP连接,就可以尝试推测下一个序列号以及确认号,以便将伪造报文注入当前连接。为了找到正确的序列号及ACK编号,我们将使用前面的方法,让客户端在加密连接中返回响应。攻击者不断在连接中注入伪造的重置报文,直到嗅探到ACK。攻击者可以分析与伪造报文对应的加密响应的大小及时间,正确推断出从客户端发往VPN服务端的数据包是否为challenge ACK。如果收到的重置报文中包含当前连接TCP窗口内的序列号,那么受害者的设备就会触发TCP challenge ACK。例如,如果客户端使用OpenVPN来与VPN服务端交换加密报文,那么当触发challenge ACK时,客户端将始终返回大小为79的一个SSL报文。

攻击者必须在整个序列号空间中伪造重置报文,直到其中某个报文触发加密的challenge ACK。伪造报文大小是非常关键的一个因素,会影响正确序列号的推断,但应该适当挑选,避免跳过客户端的接收窗口。在实际使用中,当攻击者认为已经嗅探到经过加密的challenge ACK时,可以伪造具有相同序列号的X个报文来验证这一点。如果触发了大小为79的X个加密响应,那么攻击者就能确定已正确触发challenge ACK(每秒最多2个大小为79的报文)。

当攻击者成功推测出客户端当前连接的窗口内序列号时,可以快速推测出注入攻击所需的正确序列号及窗口内ACK。首先,攻击者可以猜测窗口内ACK号,使用该编号来伪造空的push-ACK。一旦伪造的报文触发另一个challenge ACK,那么攻击者就找到了正确的窗口内ACK号。最后,攻击者使用该ACK号及序列号来继续伪造空的TCP数据报文,每次发送后都递减序列号。一旦攻击者伪造的序列号等于正确序列号的值减1,受害者就会响应另一个challenge ACK。现在攻击者可以使用推测出的ACK及下一个序列号,将任意payload注入当前加密连接中。

下面我们可以继续使用相同的四元组来测试,观察实验结果。

使用前面的四元组,我们来发送序列号在50,000范围内的RST,直到触发challenge ACK:

nping --tcp --flags R --source-ip 64.106.46.56 -g 80 --dest-ip 10.8.0.8 -p 40404 --rate 10 -c 10 -e ap0 --dest-mac 08:00:27:9c:53:12 --seq [SEQ RANGE]

如果数据包落在窗口内,受害者就会响应challenge ACK(每秒最多响应2个)。与Android不同,这些报文仍然经过加密,来自于虚拟接口,但我们仍然可以根据报文的大小来推测报文的内容。加密的challenge ACK报文大小会大于加密的RST报文。我们可以在受害者主机上运行tcmpdump,查看正确的序列号及确认号来加快测试过程。

找到窗口内序列号后,我们可以使用该序列号伪造空的PSH-ACK,将确认号可用空间切分成8份来推测确认号,从而找到窗口内确认号。在大多数情况下,其中7份会触发challenge ACK,这样我们就能快速找到落在确认号窗口中的编号值。我们对不能响应challenge ACK的编号空间比较感兴趣。我们可以使用如下命令,通过正确的序列号以及位于正确范围内的确认号来观测实验结果:

nping --tcp --flags PA --source-ip 64.106.46.56 -g 80 --dest-ip 10.8.0.8 -p 40404 --rate 10 -c 10 -e ap0 --dest-mac 08:00:27:9c:53:12 -seq 12345678 --ack [ACK RANGE]

最后,我们可以使用窗口内序列号及确认号来伪造空的PSH-ACK,当触发另一个challenge ACK时就将序列号减1。此时序列号比下一个预期的序列号要小1,然后我们就可以将任意数据注入活动的TCP连接中。

测试命令如下:

nping --tcp --flags PA --source-ip 64.106.46.56 -g 80 --dest-ip 10.8.0.8 -p 40404 --rate 10 -c 10 -e ap0 --dest-mac 08:00:27:9c:53:12 -seq [EXACT] --ack [IN-WINDOW] --data-string “hello,world.”

 

0x02 受影响系统

经过测试,我们发现有些操作系统受该攻击影响,如下所示:

Ubuntu 19.10 (systemd)
Fedora (systemd)
Debian 10.2 (systemd)
Arch 2019.05 (systemd)
Manjaro 18.1.1 (systemd)

Devuan (sysV init)
MX Linux 19 (Mepis+antiX)
Void Linux (runit)

Slackware 14.2 (rc.d) 
Deepin (rc.d)
FreeBSD (rc.d) 
OpenBSD (rc.d)

该列表并不完整,我们将继续测试其他发行版。根据上面列表,我们知道漏洞可以影响某些init系统,而不单单局限于systemd

 

0x03 系统差异

在其他操作系统上,攻击效果略有不同。

Android:在攻击第一阶段,如果未经请求的SYN-ACK使用的端口正确,那么Android会响应未经加密的RST,否则响应ICMP报文。在攻击第二阶段,Android会针对正确四元组响应RST。

MacOS/iOS:攻击第一阶段无法正常进行,但我们可以使用Apple主机上的开放端口来推测虚拟IP地址。这里我们使用的是5223,该端口用于iCloud、iMessage、FaceTime、Game Center、Photo Stream以及推送通知等。

我们知道手机将通过5223端口与某台推送服务器通信,在macOS上,我们观测到受害者用来连接VPN服务器的端口与该端口比较接近(在我们测试环境中,这两个端口相差10以内)。

nping --tcp --flags SA --source-ip 17.57.144.[84-87] -g 5223 --dest-ip 10.8.0.8 -p [X] --rate 3 -c 3 -e ap0 --dest-mac 08:00:27:9c:53:12

对于iOS设备,我们无法通过这种方式选择客户端的源端口,但可以选择~48000-50000范围内的端口(在iOS 13.1测试系统中,端口范围为48162-49555)。

FreeBSD:前2个阶段与Linux系统相同,然而在最后一个阶段我们不需要使用ACK号,因此可以直接略过该阶段。

OpenBSD:在攻击第一阶段,如果伪造的SYN报文匹配正确的虚拟IP,那么OpenBSD就会响应未加密的RST报文,否则响应未加密的NTP报文或者完全不响应。在第二阶段,响应报文经过加密,但我们可以通过报文的大小来判断哪些报文未challenge ACK(与Linux类似)。我们可以发送序列号正确的RST来重置连接。

 

0x04 缓解措施

1、启用反向路径过滤

潜在问题:移动设备上的异步路由会变得不可靠。此外,我们不确定这种方案是否行之有效,因为使用其他网络栈的其他操作系统貌似会受该漏洞影响。此外,即使反向路径过滤处于“strict”模式,这种攻击方式的前两个阶段也可以完成,因此AP可以推测活动连接情况。我们相信攻击者可以完成整个攻击,但还没有完成进行后续研究。

2、启用Bogon过滤

潜在问题:本地网络及VPN使用的本地网络地址可能受影响,此外也会影响包括伊朗在内的某些国家,这些国家会在公共空间中使用保留地址。

3、加密报文大小及时间

由于攻击者可以通过报文的大小及数量来绕过VPN服务提供的加密机制,因此我们可能可以在加密报文中添加某种填充数据,使报文具备相同大小。此外,由于我们可以使用challenge ACK的限制来判断加密报文是否为challenge ACK,因此可以考虑让主机在处理时间耗尽后响应大小相同的报文,阻止攻击者推测出正确信息。

(完)