4.5.2 原理描述

前文中我们给出了一种基于ARP欺骗和DNS劫持的攻击场景。要深入理解这个场景,就需要从Pod视角来看一下场景内的流量是如何流动的。我们以场景中涉及的example.com为例进行讲解。

假设某Pod A需要访问example.com,那么它首先必须知道该域名对应的IP,因此,它需要发出一个DNS查询请求。向哪里发送呢?默认情况下,Pod的DNS策略为ClusterFirst[1],也就是说,Pod A会向集群DNS服务kube-dns发起请求。DNS请求实际上是一个UDP报文,在我们的例子中,kube-dns服务的IP为10.96.0.10,而Pod A的IP为10.244.0.195,两者不在同一子网。因此,该UDP报文会被Pod A发送给默认网关,也就是cni0。接着,结合背景知识部分可知,节点iptables对该报文进行DNAT处理,将目的地改为10.244.0.134,也就是CoreDNS Pod的IP地址。

那么,怎么把报文发送过去呢?cni0通过查询自己维护的MAC地址表,找到10.244.0.134对应的MAC地址,然后将报文发到网桥的对应端口上。CoreDNS Pod收到报文后,向上级DNS服务器查询example.com的IP,收到结果后向Pod A发出DNS响应。至此,Pod A知道了example.com对应的IP。

接下来,Pod A就可以向example.com对应的IP发出基于TCP的HTTP请求了,这是一个正常的IP路由流程,不再赘述。

如何实施中间人攻击呢?假如攻击者想要欺骗Pod A,就应该想办法让Pod A以为攻击者所在的Pod才是DNS服务器。然而,Pod A并未直接向10.244.0.134发出ARP请求,上面过程提到的ARP解析是由cni0负责的。因此,攻击者只需要让cni0以为CoreDNS Pod的IP地址10.244.0.134对应的MAC地址为攻击者所在Pod网卡的MAC地址即可。那么攻击者可以持续向cni0发送ARP响应帧,告诉cni0,自己才是10.244.0.134。

根据ARP,攻击者可以按照图4-15给出的方式构造响应帧,其中,上方是ARP帧格式,下方是攻击者构造的具体响应帧内容。

图4-15 ARP响应帧结构

假设攻击者Pod对cni0网桥的ARP欺骗成功,理论上,稍后它将收到由Pod A发送的DNS查询请求。后面的攻击就比较顺利了——向Pod A返回DNS响应,声称example.com对应的IP地址是自己的IP;很快,它就会收到Pod A对example.com的HTTP请求,此时,攻击者即可任意定制HTTP响应内容。

至此,原理上似乎没有问题。但攻击者在Pod内,怎么获得cni0网桥和CoreDNS Pod的网络信息呢?

首先是cni0网桥。网桥的IP和MAC地址获取方式比较多样。例如,由于cni0既是网桥又是默认网关,我们可以直接查询Pod的路由表,获得网桥IP地址:


root@test:/# route -n
Kernel IP routing table
Destination     Gateway         Genmask        Flags Metric Ref    Use Iface
0.0.0.0         10.244.0.1      0.0.0.0        UG    0      0       0  eth0
10.244.0.0      0.0.0.0         255.255.255.0  U     0      0       0  eth0
10.244.0.0      10.244.0.1      255.255.0.0    UG    0      0       0  eth0

然后直接查询ARP缓存,获得网桥MAC地址。如果没有,可以先向网桥发送一个ARP请求:


root@test:/# arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
10.244.0.1               ether   0a:58:0a:f4:00:01   C                     eth0

或者,也可以直接向集群外部发送一个ICMP消息,设置ttl为1,然后从返回的ICMP消息中同时获得网桥的IP和MAC地址。相关的Python代码如下:


from scapy.layers.inet import IP, Ether, ICMP
from scapy.sendrecv import srp1
def get_bridge_mac_ip(verbose):
    res = srp1(Ether() / IP(dst="8.8.8.8", ttl=1) / ICMP(), verbose=verbose)
    return res[Ether].src, res[IP].src

我们再来看如何获得CoreDNS Pod的IP和MAC地址。结合背景知识部分DNS内容可知,Pod内部仅仅能拿到一个kube-dns服务的IP,通常是10.96.0.10。但是,如果攻击者Pod向该服务发送一个DNS查询请求,实际上是服务背后的CoreDNS Pod来回复DNS响应的(经过DNAT处理,目的地改为CoreDNS Pod)。而DNS响应又是一个UDP报文,因此我们可以从中提取到CoreDNS Pod的MAC地址。但是,DNS响应又会被进行SNAT处理,其中的IP地址被重新替换为kube-dns服务IP10.96.0.10。所以,以上步骤只能让攻击者拿到CoreDNS Pod的MAC地址。

如何获取它的IP地址呢?攻击者可以向整个子网的每个IP发出ARP请求,收集它们的MAC地址,然后与前面获得的CoreDNS Pod的MAC地址进行比对,如果一致,则说明对应IP即为CoreDNS Pod的IP。

相关的Python代码如下:


from scapy.layers.inet import IP, UDP, Ether
from scapy.sendrecv import srp1, srp
from scapy.layers.dns import DNS, DNSQR

def get_coredns_pod_mac_ip(kube_dns_svc_ip, self_ip, verbose):
    mac = srp1(Ether() / IP(dst=kube_dns_svc_ip) /
               UDP(dport=53) / DNS(rd=1, qd=DNSQR()), verbose=verbose).src
    answers, _ = srp(Ether(dst="ff:ff:ff:ff:ff:ff") /
                     ARP(pdst="{}/24".format(self_ip)), timeout=4, verbose=
                         verbose)
    for answer in answers:
        if answer[1].src == mac:
            return mac, answer[1][ARP].psrc
    return None, None

原理部分到此结束,下面我们进入实战环节。

[1] 可以通过“kubectl get pod [POD-NAME] -o yaml”查看。