-
-
[原创]CVE-2020-8554 kubernetes中间人劫持漏洞分析
-
发表于: 2020-12-29 18:33 2337
-
一、漏洞简介
2020年12月8日,Kubernetes安全通告中披露了一个中间人劫持漏洞CVE-2020-8554。如果攻击者在Kubernetes集群中具有创建和更新Service和Pod对象权限,那么通过设置LoadBalancer或ExternalIP,攻击者能够劫持集群内其他Pod或者节点访问该ExternalIP的流量,并将其转发到攻击者创建的恶意Pod中,造成中间人攻击。
该漏洞影响Kubernetes所有版本,是设计上的缺陷,目前没有针对该漏洞的补丁更新。
二、Kubernetes基本架构
Kubernetes是一套容器集群管理系统,可以实现容器集群的自动化部署、自动扩缩容、维护等功能。由Google公司在2014年启动,最初源于Google内部的Borg。Kubernetes 拥有自动缩放、健康检查、服务发现、负载均衡、滚动更新、存储编排等等特性,为管理各种基础架构中相关分布式组件和服务提供了非常高效的解决方案。整体架构如下图所示:
Kubernetes架构中涉及到的一些概念:
1) Master:负责整个集群的管理控制,用于监控、编排、调度集群中的各个工作节点。开发维护人员通过访问API Server,实现对kubernetes集群中各种资源的增加、删除、修改、查询等操作。Master节点通常会占据一个独立的服务器,基于高可用原因,也可能占据多台。
2) Node:是Kubernetes集群中的各个工作节点,可以是物理机也可以是虚拟机,由Master管理,提供运行容器所需的各种环境,对容器进行实际的控制。一个Kubernetes集群中可以有多个Node节点,可以不断地将新Node节点加入到Kubernetes集群中。
3) Pod:Kubernetes管理的基本对象是pod,而不是容器。一个pod由一个或者多个关系密切的容器组成,它们共享环境、存储、网络空间,有着相同的生命周期,一起作为一个整体编排到Node节点上。
4) Service:Kubernetes中pod是有生命周期的,可以被创建,也可以被销毁。每个Pod都有一个动态分配的IP地址,在一个Pod被销毁后,生命就永远结束,基于相同配置创建出来的新的Pod,它的IP地址是不一样的。为了解决IP地址不固定,为了能让前端应用找到对应的后端Pod,Kubernetes定义了一种名为Service的抽象。Pod可以赋予一个标签,通过标签选择器对Pod进行逻辑分组,并定义了分组访问策略。前端应用不需要关心后端Pod是否变化,只需要访问Service即可。一个service可以包含一个或多个pod。
三、Kubernetes service
3.1 Kubernetes中不同种类的IP地址
1) Node的IP地址:节点物理网卡的IP地址,是一个真实存在的物理网络。所有属于这个网络的服务器都能通过这个网络直接通信,即便有些服务器没有加入Kubernetes集群中。这也说明,在Kubernetes集群之外的节点访问Kubernetes集群之内的某个节点或者TCP/IP服务时,都必须通过Node IP通信。
2) Pod的IP地址:Docker 运行时根据docker0网桥的IP地址段进行分配,通常是一个虚拟的二层网络。Kubernetes中不同Pod中的容器就是通过这个虚拟的二层网络进行通信,真实的TCP/IP流量则是通过Node IP所在的物理网卡传递出去。
3) Service的IP地址:也即Cluster IP,是一种虚拟的IP,由Kubernetes管理,从Cluster IP地址池中分配,仅仅作用于Kubernetes Service这个对象。它无法被Ping通,因为没有一个“实体网络对象”来响应,只能结合Service Port组成一个具体的通信端口,在集群内部访问。Cluster IP属于Kubernetes集群内部的地址,不具备TCP/IP通信的基础,无法在集群外部直接使用这个地址。
以下图为例:
192.168.100.0/24网段中,有5台服务器:
Master 192.168.100.100
Node① 192.168.100.101
Node② 192.168.100.102
Node③ 192.168.100.103
ServerN 192.168.100.104
其中192.168.100.100、192.168.100.101、192.168.100.102、192.168.100.103组成了一个Kubernetes集群,但是ServerN 192.168.100.104没有加入到集群中。即便如此,ServerN也可以和Kubernetes集群中的192.168.100.100、192.168.100.101、192.168.100.102、192.168.100.103相互通信,因为它们属于同一个网段。
在集群外部的ServerN 192.168.100.104,无法访问集群内部的service 10.96.0.1、也无法访问pod② 10.244.1.7、pod③ 10.244.2.4
3.2 iptables
service的IP地址是一种虚拟的IP,系统中没有网络设备绑定这个IP地址,Kubernetes利用了iptables来做针对service的路由和负载均衡。
通常所说的iptables,实际上是由netfilter(内核模式)和iptables(用户模式)两部分组成。netfilter是linux内核2.4中引入的一个子系统,在linux网络协议栈中增加了一组回调函数挂载点,通过这些挂接点挂接的钩子函数,可以在Linux网络栈处理数据包的过程中,依据指定的处理规则集,对数据包进行过滤、修改、丢弃等操作;iptables是在用户模式下运行的进程,负责插入、修改和删除数据包过滤表中的规则。二者互相配合来实现整个Linux网络协议栈中灵活的数据包处理机制。
如上图所示,netfilter在网络协议栈内加了5个挂载点,对应iptables的5条内置链,根据实际情况的不同,数据报文经过的链可能不同:
1. PREROUTING:做源地址转换。
2. INPUT:处理输入本地进程的数据包。
3. FORWARD:处理转发到其他机器、network namespace 的数据包。
4. OUTPUT:处理本地进程的输出数据包。
5. POSTROUTING:做目标地址转换。
在每条链上,都放置了一连串的规则,每个经过这条链的数据报文,都要将这"链上的所有规则匹配一遍,如果有符合条件的规则,则执行规则对应的动作:
链上的规则有些很相似,例如一些规则用于过滤IP或者端口,一些规则用于修改报文,这时,可以把具有相同功能的规则放在一起,组成一个集合,叫做“表”。不同功能的规则,放置在不同的表中进行管理。在iptables中,按优先级从高到低,有如下5种表:
1. raw表:用于去除iptables对数据包的连接追踪机制。
2. mangle表:修改数据包的IP 头信息。
3. nat表:修改数据包的源地址或者目的地址;。
4. filter表:控制到达某条链上的数据包是继续放行、直接丢弃、拒绝。
5. security表:用于在数据包上应用SELinux,不常用。
并不是每一条链都能挂上所有的表,以PREROUTING为例,它只能挂raw、mangle、nat三个表:
其他链能挂的表可以参加本节内容第一张图。
3.3 Kubernetes中service的对外发布方式
从上文描述中我们可以知道:Service的Cluster IP属于Kubernetes集群内部的地址,无法在集群外部访问。但在实际业务系统中,肯定有服务需要提供给Kubernetes集群外部的应用或者用户使用,为了解决这个问题,有如下几种对外发布的方式:
3.3.1 NodePort
在service的定义中,指定一个端口号,然后Kubernetes会在集群里的每个Node上都开启一个对应的监听端口,集群外部应用或者用户只要访问任意一个Node的IP地址:端口号,就可以访问这个service。 这个端口号是在主机的网络空间中分配出来的(而不是Kubernetes创建的各种虚拟网络空间中),这样才能被集群外部主机访问。
如上图所示,在Kubernetes集群内部的Node上,可以直接通过IP地址10.96.0.1访问service。该service启用NodePort发布之后,在Node①节点上开启了12345端口,在Kubernetes集群外部的ServerN 192.168.100.104现在可以通过192.168.100.101:12345来访问该service了。
3.3.2 LoadBalancer
NodePort可以让服务被集群外部的主机访问,更进一步,如果想让service能在互联网上被访问,可以采取LoadBalancer的发布方式。
LoadBalancer独立于Kubernetes集群之外,连通了外部网络和内部网络,可以采用硬件或软件的方式实现,例如Nginx。它的公网IP由Kubernetes集群所在的云服务商提供,Kubernetes集群内部service在创建时,需要在status.loadBalancer中指定这个IP地址。
外部用户访问LoadBalancer的公网IP地址,然后再由LoadBalancer根据指定规则,转发到内部网络的Kubernetes集群中的各个节点上,再通过Node的IP地址:端口号,访问到具体的service。通过这种方式,内部网络之外的用户就可以访问到集群内部的service了。
3.3.3 ExternalIP
如果有一个IP地址,通过它能够路由到Kubernetes集群中一个或者多个Node上,那么service就可以借助这个IP地址发布出去。
在配置service时,通过externalIPs属性指定具体的IP地址,集群外部用户只要访问这个IP地址:端口,流量再路由进集群一个或者多个Node上,即可访问到集群内部的service了。当然,管理员需要在这个IP地址上做相应的路由配置。
NodePort、LoadBalancer是两种不同类型的service,通过spec.type属性指定。External IP只是一种属性,可以在任意类型的service中通过spec.externalIPs指定。可以说NodePort是基础,外部流量无论是通过LoadBalancer还是ExternalIP方式,进入到集群之后,都得继续依靠NodePort来转发给具体的service。
四、漏洞分析
4.1 漏洞原因
上文所述LoadBalancer、ExternalIP,两者的一个共同点是:需要指定一个IP地址,流量从这个IP地址流入到Kubernetes集群中。在Kubernetes的设计者看来,这个IP地址是在Kubernetes集群外面的,它不在Kubernetes集群的管理范围内,而是应该依靠集群的管理员、租户来保证IP地址的合法、有效性,所以没有对这个IP地址做任何判断。由此引发了本文所述的CVE-2020-8554 kubernetes中间人劫持漏洞。
4.2 搭建分析环境
在了解Kubernetes基本架构、iptables规则后,现在可以来分析CVE-2020-8554 kubernetes中间人劫持漏洞底层原因了。
搭建了4台主机,均采用CentOS 7,Kubernetes 1.20.0:
k8smaster 192.168.100.100
k8snode1 192.168.100.101
k8snode2 192.168.100.102
k8snode3 192.168.100.103
采用python:3.7镜像创建一个pod,标签设为test: testpodlabel,namespace默认是default:
创建一个类型为NodePort的service:
查看pod的创建结果:
从结果中可以看到,Kubernetes在k8snode1节点上创建了一个pod,这个pod的IP地址是10.244.1.17
继续查看service的创建结果:
这里有两个service,一个是kubernetes,这个是系统默认创建的;另外一个是我们创建的testservice,它的Cluster IP是10.102.24.38,访问端口是80,映射在主机上的端口是30001。
在k8snode3上访问一下以上pod、service、k8snode1三个IP地址:
可以看到这三种IP地址访问得到的内容都是一样的,这也说明,kubernetes集群中创建的pod、service,在Kubernetes集群的每个节点上都能顺利访问到。
此时,如果访问一下k8snode1节点的80端口:
默认情况下没有web服务在k8snode1节点上监听80端口,所以此时curl访问结果是拒绝连接。
4.3 Kubernetes中service的iptables规则
Kubernetes扩充了iptables默认的5条链,自定义了KUBE-SERVICES,KUBE-NODEPORTS,KUBE-POSTROUTING,KUBE-MARK-MASQ和KUBE-MARK-DROP等五个链。访问service的虚拟IP,主要是通过 KUBE-SERVICES链来实现。在k8snode1节点上运行iptables -L -v -n -t nat,可以查看系统中nat表的所有规则,规则有点多,一步步分析。
从中可以看到,KUBE-SERVICES链附加在系统默认的PREROUTING和OUTPUT链上,所有到达系统的流量、进程即将发出的流量,都将经过KUBE-SERVICES链:
/*和*/之间,是Kubernetes在添加规则时加上的注释,从这些注释里面,我们可以找到创建的default/testservice有两行规则:
KUBE-MARK-MASQ所在的这条规则暂时不用管,来看下面的KUBE-SVC-ZVPUSHI6OBHNIBWB所在的这条规则,它表示:
如果满足条件:数据包协议是TCP,访问的是80端口,目标IP地址是10.102.24.38,来源IP地址随便是什么,那么就跳到KUBE-SVC-ZVPUSHI6OBHNIBWB链上去。
继续分析KUBE-SVC-ZVPUSHI6OBHNIBWB这条链:
来源、目标IP地址都是0.0.0.0,prot表示的协议也是all,这意味着只要数据包进入到这条链中,立马无条件跳到KUBE-SEP-URHUM5XIMG7YZIEJ链中去,继续跟踪分析:
看第二条规则,这里对TCP协议做了DNAT(目标地址转换),原来的访问service时的目标地址是10.102.24.38,现在转换到了10.244.1.17去了。而10.244.1.17这个IP地址,就是我们创建的pod的IP地址。
从以上分析中可以看到,当访问service的cluster IP地址时,经过iptables一系列链和规则的匹配、处理,最终做了DNAT,跳转到service对应的pod的IP地址去了。
以上规则是在k8snode1上的,不过从上一节在k8snode3上访问pod、service、k8snode1三个IP地址的结果来看,这也意味着,在Kubernetes集群的master、node所有节点上,都存在着同样的iptables规则,以便在Kubernetes集群的每个节点上都能顺利访问到service。事实上也是如此,可以在每个master、node所有节点上都运行一遍iptables -L -v -n -t nat,一步步查找default/testservice的最终跳转。
4.4 ExternalIP
以上创建的pod、service都是在default namespace下,现在创建一个新的名为cve-2020-8554的namespace:
在cve-2020-8554 namespace下,创建一个劫持用的pod:
再创建一个service,指定externalIPs为k8snode1的IP地址192.168.100.101:
查看一下创建结果:
cve-2020-8554-pod的IP地址为10.244.2.10,cve-2020-8554-service的cluster IP地址为10.109.130.73。继续在k8snode3上访问一下这几个IP地址:
结果是符合预期的。现在再访问一下192.168.100.101:
这个结果就不一样了。在上文的“4.2. 搭建分析环境”中,因为没有web服务在k8snode1节点上监听80端口,所以curl访问结果是拒绝连接,但现在却可以顺利访问。这意味着192.168.100.101:80被劫持了!
在添加了externalIPs后,每个节点的KUBE-SERVICES中新添加了如下的iptables规则:
KUBE-MARK-MASQ继续不管,分析下面的两条规则的匹配条件:
规则1:目标IP地址是192.168.100.101,访问端口是80,数据包不是从网桥上来的,来源IP地址不是本地网络设备绑定的IP地址
规则2:目标IP地址是192.168.100.101,访问端口是80,目标IP地址是本地网络设备绑定的IP地址
在k8snode3上访问192.168.100.101:80,数据包是从本地网卡上发出的,来源IP地址当然是本地的,所以不匹配规则1;目标地址是192.168.100.101,它不是本地的IP地址,所以不匹配规则2。于是数据包经过网卡发出给了192.168.100.101
数据包到达k8snode1(192.168.100.101)时,端口是80,是从k8snode3(192.168.100.103)来的,不是本地的,于是匹配中了规则1,于是跳转到了KUBE-SVC-SC7RWRXUJPVQFBIC链中:
按照上文“4.3.Kubernetes中service的iptables规则”中的分析,最终会做DNAT,访问到cve-2020-8554-pod的IP 地址10.244.2.10。也就是说,在Kubernetes集群中访问192.168.100.101:80,都将被劫持到10.244.2.10:80。
如果指定externalIPs为Kubernetes集群外部的IP地址,例如百度的IP地址14.215.177.38,在KUBE-SERVICES链的上述两条规则中,只是改变了destination,从192.168.100.101变成14.215.177.38,其他的匹配条件不变。在k8snode3上访问14.215.177.38:80,数据包是从本地网卡上发出的,来源IP地址当然是本地的,所以不匹配规则1;目标地址是192.168.100.101,它不是本地的IP地址,所以不匹配规则2。于是数据包经过网卡发出给了14.215.177.38。所以这时并不会劫持14.215.177.38的访问,仍然访问到了真正的内容。
4.5 LoadBalancer
创建一个LoadBalancer类型的service,loadBalancerIP指定为想要劫持的IP地址:
查看创建结果:
可以看到EXTERNAL-IP显示的是pending,说明这个service还没有完全创建成功,必须要手动修改service的状态:
之后查看创建结果:
现在EXTERNAL-IP显示的是需要劫持的IP地址了,继续在k8snode1(192.168.100.101)上分析iptables规则。KUBE-SERVICES中新添加了如下的iptables规则:
这条规则非常简单,没有其他的匹配条件,只要访问14.215.177.38并且端口是80,将跳转到KUBE-FW-SFLNKK2MY3Y4V7NO链。按照上文的分析,一步步追踪这条链:
进一步无条件地跳转到KUBE-SVC-SFLNKK2MY3Y4V7NO这条链:
再无条件地跳转到KUBE-SEP-VZTFGMFKC7VACONL这条链:
这里最终做了DNAT,转换到访问cve-2020-8554-pod的IP地址10.244.2.10上去了。
这些规则会在Kubernetes集群每个节点上生成并执行,所以这里实现了对整个Kubernetes集群访问14.215.177.38的劫持:
4.6 IPVS模式
随着Kubernetes集群规模的增长,资源的可扩展性变得越来越重要,特别是对于那些运行大型工作负载的企业,其服务的可扩展性尤其重要。iptables是为防火墙而设计的,底层路由表的实现是链表,对路由规则的增加、删除、修改、查询等操作都涉及遍历一次链表。在集群规模扩大时,随着服务的增多,pod的增多,将使得内核非常频繁地处理iptables规则的刷新,将带来严重的性能问题。为解决iptables的性能问题,Kubernetes引入了IPVS模式,在1.11版本中达到稳定。
IPVS仍然会依赖iptables规则,在开启IPVS模式后,以“4.4. ExternalIP”中所述配置,重新创建一个service,指定externalIPs为14.215.177.38,在k8snode1(192.168.100.101)上查看iptables规则,KUBE-SERVICES链仍然存在,如下所示:
和之前描述的已经不一样了,KUBE-SERVICES链中不再有单独的service的跳转规则,和externalIP相关的,是如下两条规则:
规则1:目标IP地址和端口匹配KUBE-EXTERNAL-IP集合,数据包不是从网桥上来的,来源IP地址不是本地网络设备绑定的IP地址
规则2:目标IP地址和端口匹配KUBE-EXTERNAL-IP集合,目标IP地址是本地网络设备绑定的IP地址
可以看到,之前的规则是匹配指定的一条IP地址,现在IPVS模式下是匹配一个集合,需要处理的IP地址放到这个集合中。这样可以大大地减少iptables规则,优化性能。
通过ipset命令,可以查看KUBE-EXTERNAL-IP集合内容:
当在k8snode1(192.168.100.101)中访问14.215.177.38:80时,是匹配中KUBE-EXTERNAL-IP集合的,数据包是从本地网卡上发出的,来源IP地址当然是本地的,所以不匹配规则1。
对于规则2的匹配,这里就涉及到IPVS一个有意思的点了。启用IPVS后,系统中创建了创建了dummy0、kube-ipvs0两块虚拟网卡:
再看一下Kubernetes集群中创建的所有service:
对比可以看到,所有service的cluster IP地址都绑定到了kube-ipvs0这块网卡上,我们做劫持用的cve-2020-8554-service的External IP 14.215.177.38也在这块网卡上。
因此在匹配规则2时,虽然访问的真实的目标IP地址14.215.177.38在遥远的百度服务器上,但因为kube-ipvs0绑定了这个IP地址,所以它现在变成了一个本地的IP地址了,造成的结果是,访问14.215.177.38:80时匹配中了规则2,接下来由以下IPVS规则:
访问14.215.177.38:80转换到访问cve-2020-8554-pod的IP地址10.244.2.10上去了。
这里实现了对整个Kubernetes集群访问14.215.177.38的劫持:
对比可以看到,在IPVS模式下,劫持情形更为严重。
4.7 漏洞小结
1. iptables模式下,采用ExternalIP无法劫持集群外部IP,但LoadBalancer可以;
2. ipvs模式下,两者都可以劫持集群外部IP;
3. 从上文分析中可以看到,在Linux内核中执行各种规则的匹配、链的跳转,是没有Kubernetes中namespace概念的,所以这种劫持是跨namespace的,一个恶意用户的劫持,能影响集群中其他所有的用户。
4. iptables规则遍布Kubernetes集群所有的master、node节点,所以劫持效果也遍布所有的master、node节点。
5. 事实上,不仅可以劫持TCP流量,甚至可以劫持UDP流量,因此可以利用该漏洞实现对集群内部DNS的劫持,由此产生更严重的利用效果。
五、防御建议
1. 正如前文所述, LoadBalancer、ExternalIP这两者情况下指定的IP地址,Kubernetes本身无法确保它的合法性,而是应该依靠集群的管理员来划分合法范围,因此Kubernetes官方引入了一个新项目:https://github.com/kubernetes-sigs/externalip-webhook,用于提供给管理员,限制合法的ExternalIP范围。在合法范围外的IP地址,禁止service的创建。
2. LoadBalancer需要patch service的status,设置LoadBalancer的外部IP地址。集群管理员可以禁止租户的patch权限,即可解决LoadBalancer这个问题。
3. 建议持续关注官方对该漏洞的处理讨论:https://github.com/kubernetes/kubernetes/issues/97110
六、参考链接
[Security Advisory] CVE-2020-8554: Man in the middle using LoadBalancer or ExternalIPs
https://groups.google.com/g/kubernetes-security-announce/c/iZWsF9nbKE8
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)