-
-
[原创]CVE-2020-8558-跨主机访问127.0.0.1
-
发表于: 2022-8-15 11:29 8289
-
背景
假设机器A和机器B在同一个局域网,机器A使用nc -l 127.0.0.1 8888
,在机器B上可以访问机器A上"仅绑定在127.0.0.1的服务"吗?
1 2 3 4 | [root@instance - h9w7mlyv ~] # nc -l 127.0.0.1 8888 & [ 1 ] 44283 [root@instance - h9w7mlyv ~] # netstat -antp|grep 8888 tcp 0 0 127.0 . 0.1 : 8888 0.0 . 0.0 : * LISTEN 44283 / nc |
nc用法可能不同,有的使用 nc -l 127.0.0.1 -p 8888 监听8888端口
kubernetes的kube-proxy组件之前披露过CVE-2020-8558漏洞,这个漏洞就可以让"容器内的恶意用户、同一局域网其他机器"访问到node节点上"仅绑定在127.0.0.1的服务"。这样有可能访问到监听在本地的"kubernetes无需认证的apiserver",进而控制集群。
本文会带你做两种网络环境(vpc和docker网桥模式)下的漏洞原理分析,并复现漏洞。
漏洞分析
怎么复现?
terraform可以基于声明式api编排云上的基础设施(虚拟机、网络等)
你也可以按照文章后面的步骤来复现漏洞。
为什么可以访问其他节点的"仅绑定在127.0.0.1的服务"?
假设实验环境是,一个局域网内有两个节点A和B、交换机,ip地址分别是ip_a和ip_b,mac地址分别是mac_a和mac_b。
来看看A机器访问B机器时的一个攻击场景。
如果在tcp握手时,A机器构造一个"恶意的syn包",数据包信息是:
源ip | 源mac | 目的ip | 目的mac | 目的端口 | 源端口 |
---|---|---|---|---|---|
ip_a | mac_a | 127.0.0.1 | mac_b | 8888 | 44444(某个随机端口) |
此时如果交换机只是根据mac地址做数据转发,它就将syn包发送给B。
syn包的数据流向是:A -> 交换机 -> B
B机器网卡在接收到syn包后:
- 链路层:发现目的mac是自己,于是扔给网络层处理
- 网络层:发现ip是本机网卡ip,看来要给传输层处理,而不是转发
- 传输层:发现当前"网络命名空间"确实有服务监听
127.0.0.1:8888
, 和 "目的ip:目的端口" 可以匹配上,于是准备回复syn-ack包
从"内核协议栈"角度看,发送包会经过"传输层、网络层、链路层、设备驱动",接受包刚好相反,会经过"设备驱动、链路层、网络层、传输层"
syn-ack数据包信息是:
源ip | 源mac | 目的ip | 目的mac | 目的端口 | 源端口 |
---|---|---|---|---|---|
127.0.0.1 | mac_b | ip_a | mac_a | 44444(某个随机端口) | 8888 |
syn-ack包的数据流向是:B -> 交换机 -> A
A机器网卡在收到syn-ack包后,也会走一遍"内核协议栈"的流程,然后发送ack包,完成tcp握手。
这样A就能访问到B机器上"仅绑定在127.0.0.1的服务"。所以,在局域网内,恶意节点"似乎"很容易就能访问到其他节点的"仅绑定在127.0.0.1的服务"。
但实际上,A访问到B机器上"仅绑定在127.0.0.1的服务"会因为两大类原因失败:
- 交换机有做检查,比如它不允许数据包的目的ip地址是127.0.0.1,这样第一个syn包就不会转发给B,tcp握手会失败。公有云厂商的交换机(比如ovs)应该就有类似检查,所以我在某个公有云厂商vpc网络环境下测试,无法成功复现漏洞。
- 数据包到了主机,但是因为ip是127.0.0.1,很特殊,所以"内核协议栈"为了安全把包丢掉了。
所以不能在云vpc环境下实验,于是我选择了复现"容器访问宿主机上的仅绑定在127.0.0.1的服务"。
先来看一下,"内核协议栈"为了防止恶意访问"仅绑定在127.0.0.1的服务"都做了哪些限制。
"内核协议栈"做了哪些限制?
先说结论,下面三个内核参数都会影响
- route_localnet
- rp_filter
- accept_local
以docker网桥模式为例,想要在docker容器中访问到宿主机的"仅绑定在127.0.0.1的服务",就需要:
- 宿主机上 route_localnet=1
- docker容器中 rp_filter=0、accept_local=1、route_localnet=1
宿主机网络命名空间中
1 2 3 4 | [root@instance - h9w7mlyv ~] # sysctl -a|grep route_localnet net.ipv4.conf. all .route_localnet = 1 net.ipv4.conf.default.route_localnet = 1 ... |
容器网络命名空间中
1 2 3 4 5 6 7 8 9 | [root@instance - h9w7mlyv ~] # sysctl -a|grep accept_local net.ipv4.conf. all .accept_local = 1 net.ipv4.conf.default.accept_local = 1 net.ipv4.conf.eth0.accept_local = 1 [root@instance - h9w7mlyv ~] # sysctl -a|grep '\.rp_filter' net.ipv4.conf. all .rp_filter = 0 net.ipv4.conf.default.rp_filter = 0 net.ipv4.conf.eth0.rp_filter = 0 ... |
容器中和宿主机中因为是不同的网络命名空间,所以关于网络的内核参数是隔离的,并一定相同。
route_localnet配置
是什么?
内核文档提到route_localnet参数,如果route_localnet等于0,当收到源ip或者目的ip是"loopback地址"(127.0.0.0/8)时,就会认为是非法数据包,将数据包丢弃。
宿主机上curl 127.0.0.1时,源ip和目的都是127.0.0.1,此时网络能正常通信,说明数据包并没有被丢弃。说明这种情景下,没有调用到 ip_route_input_noref 函数查找路由表。
CVE-2020-8558漏洞中,kube-proxy设置route_localnet=1,导致关闭了上面所说的检查。
内核协议栈中哪里用route_localnet配置来检查?
https://elixir.bootlin.com/linux/v4.18/source/net/ipv4/route.c#L1912
ip_route_input_slow 函数中用到 route_localnet配置,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | / * * NOTE. We drop all the packets that has local source * addresses, because every properly looped back packet * must have correct destination already attached by output routine. * * Such approach solves two big problems: * 1. Not simplex devices are handled properly. * 2. IP spoofing attempts are filtered with 100 % of guarantee. * called with rcu_read_lock() * / static int ip_route_input_slow(struct sk_buff * skb, __be32 daddr, __be32 saddr, u8 tos, struct net_device * dev, struct fib_result * res) { ... / * Following code try to avoid calling IN_DEV_NET_ROUTE_LOCALNET(), * and call it once if daddr or / and saddr are loopback addresses * / if (ipv4_is_loopback(daddr)) { / / 目的地址是否 "loopback地址" if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net)) / / localnet配置是否开启。net是网络命名空间,in_dev是接收数据包设备配置信息 goto martian_destination; / / 认为是非法数据包 } else if (ipv4_is_loopback(saddr)) { / / 源地址是否 "loopback地址" if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net)) goto martian_source; / / 认为是非法数据包 } ... err = fib_lookup(net, &fl4, res, 0 ); / / 查找 "路由表" ,res存放查找结果 ... if (res - > type = = RTN_BROADCAST) ... if (res - > type = = RTN_LOCAL) { / / 数据包应该本机处理 err = fib_validate_source(skb, saddr, daddr, tos, 0 , dev, in_dev, &itag); / / "反向查找" , 验证源地址是否有问题 if (err < 0 ) goto martian_source; goto local_input; / / 本机处理 } if (!IN_DEV_FORWARD(in_dev)) { / / 没有开启ip_forward配置时,认为不支持 转发数据包 err = - EHOSTUNREACH; goto no_route; } ... err = ip_mkroute_input(skb, res, in_dev, daddr, saddr, tos, flkeys); / / 认为此包需要 "转发" } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | static int ip_rcv_finish(struct net * net, struct sock * sk, struct sk_buff * skb) { ... / * * Initialise the virtual path cache for the packet. It describes * how the packet travels inside Linux networking. * / if (!skb_valid_dst(skb)) { / / 是否有路由缓存. 宿主机curl 127.0 . 0.1 时,就有缓存,不用查找路由表。 err = ip_route_input_noref(skb, iph - >daddr, iph - >saddr, iph - >tos, dev); / / 查找路由表 if (unlikely(err)) goto drop_error; } ... return dst_input(skb); / / 将数据包交给tcp层(ip_local_deliver) 或 转发数据包(ip_forward) |
在收到数据包时,从ip层来看,数据包会经过 ip_rcv(ip层入口函数) -> ip_rcv_finish -> ip_route_input_slow。
在ip_route_input_slow函数中可以看到,如果源ip或者目的ip是"loopback地址",并且接收数据包的设备没有配置route_localnet选项时,就会认为是非法数据包。
rp_filter和accept_local
是什么?
内核网络参数详解 提到,rp_filter=1时,会严格验证源ip。
怎么检查源ip呢?就是收到数据包后,将源ip和目的ip对调,然后再查找路由表,找到会用哪个设备回包。如果"回包的设备"和"收到数据包的设备"不一致,就有可能校验失败。这个也就是后面说的"反向检查"。
内核协议栈中哪里用rp_filter和accept_local配置来检查?
上面提到 收到数据包时,从ip层来看,会执行 ip_route_input_slow 函数查找路由表。
ip_route_input_slow 函数会执行 fib_validate_source 函数执行 "验证源ip",会使用到rp_filter和accept_local配置
https://elixir.bootlin.com/linux/v4.18/source/net/ipv4/fib_frontend.c#L412
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | / * Ignore rp_filter for packets protected by IPsec. * / int fib_validate_source(struct sk_buff * skb, __be32 src, __be32 dst, u8 tos, int oif, struct net_device * dev, struct in_device * idev, u32 * itag) { int r = secpath_exists(skb) ? 0 : IN_DEV_RPFILTER(idev); / / r = rp_filter配置 struct net * net = dev_net(dev); if (!r && !fib_num_tclassid_users(net) && (dev - >ifindex ! = oif || !IN_DEV_TX_REDIRECTS(idev))) { / / dev - >ifindex ! = oif 表示 不是lo虚拟网卡接收到包 if (IN_DEV_ACCEPT_LOCAL(idev)) / / accept_local配置是否打开。idev是接受数据包的网卡配置 goto ok; / * with custom local routes in place, checking local addresses * only will be too optimistic, with custom rules, checking * local addresses only can be too strict, e.g. due to vrf * / if (net - >ipv4.fib_has_custom_local_routes || fib4_has_custom_rules(net)) / / 检查 "网络命名空间" 中是否有自定义的 "策略路由" goto full_check; if (inet_lookup_ifaddr_rcu(net, src)) / / 检查 "网络命名空间" 中是否有设备的ip和源ip(src值)相同 return - EINVAL; ok: * itag = 0 ; return 0 ; } full_check: return __fib_validate_source(skb, src, dst, tos, oif, dev, r, idev, itag); / / __fib_validate_source中会执行 "反向检查源ip" } |
当在容器中curl 127.0.0.1 --interface eth0
时,有一些结论:
- 宿主机收到请求包时,无论 accept_local和rp_filter是啥值,都通过fib_validate_source检查
- 容器中收到请求包时,必须要设置 accept_local=1、rp_filter=0,才能不被"反向检查源ip"
如果容器中 accept_local=1、rp_filter=0 有一个条件不成立,就会发生丢包。这个时候如果你在容器网络命名空间用tcpdump -i eth0 'port 8888' -n -e
观察,就会发现诡异的现象:容器接收到了syn-ack包,但是没有回第三个ack握手包。如下图
小技巧:nsenter -n -t 容器进程pid 可以进入到容器网络空间,接着就可以tcpdump抓"容器网络中的包"
docker网桥模式下复现漏洞
docker网桥模式下漏洞原理是什么?
借用网络上的一张图来说明docker网桥模式
在容器内curl 127.0.0.1:8888 --interface eth0
时,发送第一个syn包时,在网络层查找路由表
1 2 3 | [root@instance - h9w7mlyv ~] # ip route show default via 172.17 . 0.1 dev eth0 172.17 . 0.0 / 16 dev eth0 proto kernel scope link src 172.17 . 0.3 |
因此会走默认网关(172.17.0.1),在链路层就会找网关的mac地址
1 2 | [root@instance - h9w7mlyv ~] # arp -a|grep 172.17.0.1 _gateway ( 172.17 . 0.1 ) at 02 : 42 :af: 2e :cd:ae [ether] on eth0 |
实际上02:42:af:2e:cd:ae
就是docker0网桥的mac地址,所以网关就是docker0网桥
1 2 3 4 5 6 | [root@instance - h9w7mlyv ~] # ifconfig docker0 docker0: flags = 4163 <UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 172.17 . 0.1 netmask 255.255 . 0.0 broadcast 172.17 . 255.255 ... ether 02 : 42 :af: 2e :cd:ae txqueuelen 0 (Ethernet) ... |
因此第一个syn包信息是
源ip | 目的ip | 源mac | 目的mac | 源端口 | 目的端口 |
---|---|---|---|---|---|
容器eth0 ip | 127.0.0.1 | 容器eth0 mac | docker0 mac | 4444(随机端口) | 8888 |
syn包数据包数据流向是 容器内eth0 -> veth -> docker0。
veth设备作为docker0网桥的"从设备",接收到syn包后直接转发,不会调用到"内核协议栈"的网络层。
docker0网桥设备收到syn包后,在"内核协议栈"的链路层,看到目的mac是自己,就把包扔给网络层处理。在网络层查路由表,看到目的ip是本机ip,就将包扔给传输层处理。在传输层看到访问"127.0.0.1:8888",就会查看是不是有服务监听在"127.0.0.1:8888"。
怎么复现?
从上面分析可以看出来,需要将宿主机docker0网桥设备route_localnet设置成1。
宿主机docker0网桥设备需要设置rp_filter和accept_local选项吗?答案是不需要,因为docker0网桥设备在收到数据包在网络层做"反向检查源地址"时,会知道"响应数据包"也从docker0网桥发送。"发送和接收数据包的设备"是匹配的,所以能通过"反向检查源地址"的校验。
容器中eth0网卡需要设置rp_filter=0、accept_local=1、localnet=1。为什么容器中eth0网卡需要设置rp_filter和accept_local选项呢?因为eth0网桥设备如果做"反向检查源地址",就会知道响应包应该从lo网卡发送。"接收到数据包的设备是eth0网卡",而"发送数据包的设备应该是lo网卡",两个设备不匹配,"反向检查"就会失败。rp_filter=0、accept_local=1可以避免做"反向检查源地址"。
即使ifconfig lo down,
ip route show table local
仍能看到local表中有回环地址的路由。
下面你可以跟着我来用docker复现漏洞。
首先在宿主机上打开route_localnet配置
1 | [root@instance - h9w7mlyv ~] # sysctl -w net.ipv4.conf.all.route_localnet=1 |
然后创建容器,并进入到容器网络命名空间,设置rp_filter=0、accept_local=1
1 2 3 4 5 6 7 8 9 10 11 | [root@instance - h9w7mlyv ~] # docker run -d busybox tail -f /dev/null // 创建容器 62ba93fbbe7a939b7fff9a9598b546399ab26ea97858e73759addadabc3ad1f3 [root@instance - h9w7mlyv ~] # docker top 62ba93fbbe7a939b7fff9a9598b546399ab26ea97858e73759addadabc3ad1f3 UID PID PPID C STIME TTY TIME CMD root 43244 43224 0 12 : 33 ? 00 : 00 : 00 tail - f / dev / null [root@instance - h9w7mlyv ~] # nsenter -n -t 43244 // 进入到容器网络命名空间 [root@instance - h9w7mlyv ~] # [root@instance - h9w7mlyv ~] # sysctl -w net.ipv4.conf.all.accept_local=1 // 设置容器中的accept_local配置 [root@instance - h9w7mlyv ~] # sysctl -w net.ipv4.conf.all.rp_filter=0 // 设置容器中的rp_filter配置 [root@instance - h9w7mlyv ~] # sysctl -w net.ipv4.conf.default.rp_filter=0 [root@instance - h9w7mlyv ~] # sysctl -w net.ipv4.conf.eth0.rp_filter=0 |
如果你是
docker exec -ti busybox sh
进入到容器中,然后执行sysctl -w
配置内核参数,就会发现报错,因为/proc/sys目录默认是作为只读挂载到容器中的,而内核网络参数就在/proc/sys/net目录下。
然后就可以在容器中使用curl 127.0.0.1:端口号 --interface eth0
来访问宿主机上的服务。
kubernetes对漏洞的修复
在 这个pr 中kubelet添加了一条iptables规则
1 2 | root@ip - 172 - 31 - 14 - 33 :~ # iptables-save |grep localnet - A KUBE - FIREWALL ! - s 127.0 . 0.0 / 8 - d 127.0 . 0.0 / 8 - m comment - - comment "block incoming localnet connections" - m conntrack ! - - ctstate RELATED,ESTABLISHED,DNAT - j DROP |
这条规则使得,在tcp握手时,第一个syn包如果目的ip是"环回地址",同时源ip不是"环回地址"时,包会被丢弃。
所以如果你复现时是在kubernetes环境下,就需要删掉这条iptables规则。
或许你会有疑问,源ip不也是可以伪造的嘛。确实是这样,所以在 https://github.com/kubernetes/kubernetes/pull/91569 中有人评论到,上面的规则,不能防止访问本地udp服务。
总结
公有云vpc网络环境下,可能因为交换机有做限制而导致无法访问其他虚拟机的"仅绑定在127.0.0.1的服务"。
docker容器网桥网络环境下,存在漏洞的kube-proxy已经设置了宿主机网络的route_localnet选项,但是因为在容器中/proc/sys
默认只读,所以无法修改容器网络命名空间下的内核网络参数,也很难做漏洞利用。
kubernetes的修复方案并不能防止访问本地udp服务。
如果kubernetes使用了cni插件(比如calico ipip网络模型),你觉得在node节点能访问到master节点的"仅绑定在127.0.0.1的服务"吗?
参考
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
- [原创]CVE-2020-8558-跨主机访问127.0.0.1 8290
- "容器逃逸失败"案例分析 8427
- [原创]扫描器程序卡住的bug复盘 819
- [原创]Python栈溢出CVE-2021-3177分析 14221