CVE-2020-8558-跨主机访问127.0.0.1

发布于:2023-01-22 ⋅ 阅读:(322) ⋅ 点赞:(0)

声明

出品|leveryd(ID:leveryd)

以下内容,来自先知社区的Ahoge作者原创,由于传播,利用此文所提供的信息而造成的任何直接或间接的后果和损失,均由使用者本人负责,长白山攻防实验室以及文章作者不承担任何责任。

背景

假设机器A和机器B在同一个局域网,机器A使用nc -l 127.0.0.1 8888,在机器B上可以访问机器A上"仅绑定在127.0.0.1的服务"吗?

[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的漏洞靶场。

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

宿主机网络命名空间中

[root@instance-h9w7mlyv ~]# sysctl -a|grep route_localnet
net.ipv4.conf.all.route_localnet = 1
net.ipv4.conf.default.route_localnet = 1
...

容器网络命名空间中

[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配置

是什么?

内核文档[3]提到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配置,如下:

/*
 * 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);  // 认为此包需要"转发"
}

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

/* 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握手包。如下图

![图片](https://mmbiz.qpic.cn/mmbiz_png/FwyeCXsWiaYwT9ePZcgMkrbKk0ic8t7muJM7PZayXtlLEUcuOiaWOtZiaNCrzcic4DcBjXEUC9mcDgMQNs6FvqNUbJw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1)

> 小技巧:nsenter -n -t 容器进程pid 可以进入到容器网络空间,接着就可以tcpdump抓"容器网络中的包"

docker网桥模式下复现漏洞
===============

docker网桥模式下漏洞原理是什么?
-------------------

借用网络上的一张图来说明docker网桥模式![图片](https://mmbiz.qpic.cn/mmbiz_png/FwyeCXsWiaYwT9ePZcgMkrbKk0ic8t7muJrficQooOl61MFEic8ZdV1OYyOgbleN7rPeIBeph9KTUP8LQU6JU7clCA/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1)

在容器内`curl 127.0.0.1:8888 --interface eth0`时,发送第一个syn包时,在网络层查找路由表

[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地址

[root@instance-h9w7mlyv ~]# arp -a|grep 172.17.0.1
_gateway (172.17.0.1) at 02:42:af:2e💿ae [ether] on eth0


实际上`02:42:af:2e:cd:ae`就是docker0网桥的mac地址,所以网关就是docker0网桥

[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💿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配置

[root@instance-h9w7mlyv ~]# sysctl -w net.ipv4.conf.all.route_localnet=1


然后创建容器,并进入到容器网络命名空间,设置rp\_filter=0、accept\_local=1

[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`来访问宿主机上的服务。

![图片](https://mmbiz.qpic.cn/mmbiz_png/FwyeCXsWiaYwT9ePZcgMkrbKk0ic8t7muJEXGeMibHdGjpFs05n0DCr15QnzcjlGDmbHWUaWKsqibV0LtTKubSYMDQ/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1)

  

kubernetes对漏洞的修复
================

在 这个pr\[5\] 中kubelet添加了一条iptables规则

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的服务"吗?

### 参考资料

*   terraform: https://www.terraform.io/  
    
      
    
*   漏洞靶场:
    
    https://github.com/HuoCorp/TerraformGoat/blob/main/kubernetes/kube-proxy/CVE-2020-8558/README\_CN.md
    
      
    
*   内核文档: 
    
    https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt
    
      
    
*   内核网络参数详解:
    
     https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt
    

  

*   这个pr: https://github.com/kubernetes/kubernetes/pull/91569/commits/8bed088224fb38b41255b37e59a1701caefa171b
    
      
    
*   内核网络参数详解: 
    
    https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt
    

  



本文含有隐藏内容,请 开通VIP 后查看