CVE-2020-8558-跨主机访问127.0.0.1
2022-7-20 15:35:0 Author: paper.seebug.org(查看原文) 阅读量:23 收藏

作者:leveryd
原文链接:https://mp.weixin.qq.com/s/hvb_Kr6DqAPPfnN-lbx1aA

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

[[email protected] ~]# nc -l 127.0.0.1 8888 &
[1] 44283
[[email protected] ~]# 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

宿主机网络命名空间中

[[email protected] ~]# sysctl -a|grep route_localnet
net.ipv4.conf.all.route_localnet = 1
net.ipv4.conf.default.route_localnet = 1
...

容器网络命名空间中

[[email protected] ~]# 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
[[email protected] ~]# 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等于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=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握手包。如下图 img

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

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

借用网络上的一张图来说明docker网桥模式 img

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

[[email protected] ~]# 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地址

[[email protected] ~]# 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网桥

[[email protected] ~]# 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配置

[[email protected] ~]# sysctl -w net.ipv4.conf.all.route_localnet=1

然后创建容器,并进入到容器网络命名空间,设置rp_filter=0、accept_local=1

[[email protected] ~]# docker run -d busybox tail -f /dev/null     // 创建容器
62ba93fbbe7a939b7fff9a9598b546399ab26ea97858e73759addadabc3ad1f3
[[email protected] ~]# docker top 62ba93fbbe7a939b7fff9a9598b546399ab26ea97858e73759addadabc3ad1f3
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                43244               43224               0                   12:33               ?                   00:00:00            tail -f /dev/null
[[email protected] ~]# nsenter -n -t 43244     // 进入到容器网络命名空间
[[email protected] ~]#
[[email protected] ~]# sysctl -w net.ipv4.conf.all.accept_local=1  // 设置容器中的accept_local配置
[[email protected] ~]# sysctl -w net.ipv4.conf.all.rp_filter=0     // 设置容器中的rp_filter配置
[[email protected] ~]# sysctl -w net.ipv4.conf.default.rp_filter=0
[[email protected] ~]# 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来访问宿主机上的服务。

image

这个pr 中kubelet添加了一条iptables规则

[email protected]:~# 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的服务"吗?

内核网络参数详解


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1938/


文章来源: https://paper.seebug.org/1938/
如有侵权请联系:admin#unsafe.sh