Featured image of post 防火墙翻越

防火墙翻越

非常应景的实验,弥补了一直以来的知识缺口

实验环境

我使用的是 Ubuntu 24.04.4,基本环境配置见ARP攻击原理学习

网络结构如下:

节点 IP 地址 作用
A 区普通主机 1 10.8.0.5 用来测试从 A 区访问 B 区服务
A 区普通主机 2 10.8.0.6 用来进行辅助连通性测试
A 区网关 10.8.0.99 A 区出口,也是 SSH 隧道和 VPN 隧道的一端
B 区服务器 1 192.168.20.5 B 区目标服务器,提供 Telnet 等被保护服务
B 区服务器 2 192.168.20.6 B 区目标服务器,用来进行辅助连通性测试
B 区网关 192.168.20.99 B 区出口,也是 SSH 隧道、SOCKS 代理和 VPN 隧道的一端
中间路由器 10.8.0.11 / 192.168.20.11 负责转发 A 区和 B 区之间的流量,并执行防火墙规则

启动实验前先清理容器环境,防止被之前的网络配置干扰

1
2
sudo docker stop $(sudo docker ps -q) 2>/dev/null
sudo docker system prune -f

进入实验目录并启动

1
2
cd seed-labs/category-network/Firewall_Evasion/Labsetup
sudo docker compose up -d

验证容器状态

1
sudo docker ps

基础网络测试

实验开始前,首先确认从A1到B1,和从A1到B2容器的网络连通性

1
2
sudo docker exec A1-10.8.0.5 ping -c 3 192.168.20.5
sudo docker exec A1-10.8.0.5 ping -c 3 192.168.20.6

查看中间路由器的路由表,这里可以看到它有两个网卡,eth0连接 A 区网段10.8.0.0/24,自己的地址是 10.8.0.11eth1连接 B 区网段192.168.20.0/24,自己的地址是 192.168.20.11。说明这个路由器就是两个区域通信的中转

1
sudo docker exec router-firewall ip route show

进入防火墙容器,查看默认的 iptables 规则

1
2
3
4
5
sudo docker exec -it router-firewall /bin/bash
# 查看完整的 filter 表
iptables -L -v -n
# 只看 FORWARD 链
iptables -L FORWARD -v -n

这部分规则模拟的就是一个简易的公司内网防火墙环境,类比到真实情况,A 就是防火墙外侧网络,可以作为外部访问者或者代理服务器,B 就是受保护的内网。这里重点是看FORWARD链,也就是路由器转发包的规则,默认策略是 ACCEPT

从 A 区方向进来的 TCP 流量中,已经建立的连接和SSH流量,也就是目标端口是22的流量可以继续通过,其他普通 TCP 会被丢弃,这样 A 区主机如果直接访问 B 区的 Telnet、HTTP 等普通 TCP 服务,就会被防火墙阻断

从 B 区方向进来的所有流量中,目标是 93.184.216.0/24的流量会被丢弃,这个 IP 在之后的实验中对应的是www.example.com,这就体现了 B 区主机访问某个网站被防火墙阻断的场景

静态端口转发

通过建立 SSH 静态隧道,将对本地端口的访问转发到远程内网的特定服务上,这里把 A 区网关的 8000 端口映射到 192.168.20.5:23,从而让区域 A 的主机穿透防火墙,访问区域 B 被保护的 Telnet 服务

新开一个终端窗口,进入 A 区网关容器

1
sudo docker exec -it A-10.8.0.99 /bin/bash

在 A 区网关上建立 SSH 隧道,目的是将10.8.0.99:8000映射到192.168.20.5:23。执行命令后终端会持续运行来维持 SSH 隧道

1
ssh -4NT -L 0.0.0.0:8000:192.168.20.5:23 seed@192.168.20.99
  • -L:启用本地端口转发

  • 0.0.0.0:8000:让 A 区网关上的 SSH 客户端监听所有网卡上的 8000 端口

  • 192.168.20.5:23:如果监听到8000端口的连接,就通过 SSH 隧道转发给 B 区网关,B 区网关再连接 B1 的 Telnet 服务

  • seed@192.168.20.99:SSH 连接到 B 区网关

  • -N:不执行远程命令,只做端口转发

  • -T:不分配伪终端

新开一个终端进入 A1 容器,通过 A 区网关的 8000 端口连接 B1 的 Telnet 服务

1
2
sudo docker exec -it A1-10.8.0.5 /bin/bash
telnet 10.8.0.99 8000

连接成功后会弹出 Ubuntu 的登录提示,输入用户和密码后,可以输入下面的命令看到当前处于 192.168.20.5主机上,说明连接已经成功建立

1
ip -br a

为了观察防火墙视角下的数据包,可以在路由器容器中抓包

1
tcpdump -n -i any

可以看到防火墙主要能看到 10.8.0.99192.168.20.99:22 的 SSH 流量,真正的 Telnet 数据被包在 SSH 加密连接内部,防火墙看不到里面访问的是 192.168.20.5:23,这就是静态端口转发可以绕过防火墙限制的原因

动态 SOCKS 代理

静态端口转发每次只能绑定一个固定目标,而动态端口转发会在本地启动一个 SOCKS 代理,仅指定代理出入口,但访问目标不写死在 SSH 命令里。因此能让 B 区主机通过 A 区网关作为出口,来访问原本被防火墙拦截的网站

新开一个终端窗口,进入 B 区网关容器

1
sudo docker exec -it B-192.168.20.99 /bin/bash

在 B 区网关上建立动态代理隧道,目的是通过 SSH 隧道把请求发到 A 区网关,再让 A 区网关去访问目标。执行命令后终端会持续运行来维持 SSH 隧道

1
ssh -4NT -D 0.0.0.0:9000 seed@10.8.0.99
  • -D:启用动态端口转发,也就是创建 SOCKS 代理。

  • 0.0.0.0:9000:让 B 区网关上的 SSH 客户端监听所有网卡上的 9000 端口

  • seed@10.8.0.99:SSH 连接到 A 区网关

进入 B1 容器,修改本地映射,将域名与被防火墙封锁的网段绑定

1
2
sudo docker exec -it B1-192.168.20.5 /bin/bash
echo "93.184.216.34 www.example.com" >> /etc/hosts

直接访问目标网站,可以发现没有反应,说明无法访问

1
curl www.example.com

用 SOCKS5 代理访问网站,这次可以成功拿到网页 HTML 内容。这是因为 B1 会把请求交给 B 区网关的 SOCKS 代理,也就是192.168.20.99:9000,代理再通过 SSH 隧道把请求发给 A 区网关来访问外部网站。其中socks5h 中的 h 就表示域名解析也交给代理端处理

1
curl --proxy socks5h://192.168.20.99:9000 www.example.com

浏览器也是类似的流程,首先在 Firefox 浏览器中如图配置代理

访问www.example.com,可以发现页面成功加载了

关闭动态代理隧道,再次访问就发现代理服务器拒绝连接,这和真实网络中的代理是类似的

Layer 3 VPN 隧道

这部分更接近真正 VPN 的工作方式,它会创建一个虚拟网卡 tun0,然后把数据包放进 SSH 隧道中传输

开始这个部分前,先重启实验容器,清理之前的隧道和路由配置来防止干扰

1
2
sudo docker compose down
sudo docker compose up -d

A 区访问 B 区

这里先在防火墙上添加规则,禁止 A 区网段直接访问 B 区网段

1
2
sudo docker exec -it router-firewall /bin/bash
iptables -A FORWARD -s 10.8.0.0/24 -d 192.168.20.0/24 -j DROP

此时从 A 区网关 ping B1 会失败

1
sudo docker exec -it A-10.8.0.99 ping 192.168.20.5

为了实现访问A 网关不再把包直接发向 B 区,而是先把包送进 tun0,再用 SSH 封装起来发给 B 网关。防火墙看到的外层还是 SSH,不是普通的 A 到 B 访问

进入 A 区网关容器,使用 SSH 的 -w 参数建立三层隧道。这个终端需要保持挂起来维持 VPN 隧道

1
2
3
4
5
sudo docker exec -it A-10.8.0.99 /bin/bash
ssh -w 0:0 root@192.168.20.99 \
    -o "PermitLocalCommand=yes" \
    -o "LocalCommand= ip addr add 192.168.53.88/24 dev tun0 && ip link set tun0 up" \
    -o "RemoteCommand=ip addr add 192.168.53.99/24 dev tun0 && ip link set tun0 up"

-w 0:0 会在 SSH 两端创建 tun0 虚拟网卡。A 侧的 tun0 地址是 192.168.53.88/24,B 侧的 tun0 地址是 192.168.53.99/24。这相当于在 A 区网关和 B 区网关之间创建了一条虚拟三层链路。

新开一个终端进入 A 区网关,配置路由。为了避免 SSH 隧道自己的连接被错误引进 tun0,可以先给 B 区网关加一条单独的主机路由。再把访问 B 区网段的流量引入 tun0

1
2
3
sudo docker exec -it A-10.8.0.99 /bin/bash
ip route add 192.168.20.99/32 via 10.8.0.11
ip route replace 192.168.20.0/24 dev tun0

接着给 B1 和 B2 配置回程路由,让它们知道返回 192.168.53.0/24 这个隧道网段时应该交给 B 区网关

1
2
sudo docker exec -it B1-192.168.20.5 ip route add 192.168.53.0/24 via 192.168.20.99
sudo docker exec -it B2-192.168.20.6 ip route add 192.168.53.0/24 via 192.168.20.99

回到 A 区网关再次ping B1。此时虽然防火墙已经阻断了 10.8.0.0/24192.168.20.0/24 的直接访问,但是 A 区网关访问 B 区网段时,数据包先进入 tun0,再被封装进 SSH 连接中。防火墙看到的外层流量仍然是 A 网关到 B 网关的 SSH 连接,看不到内部的三层访问。

1
ping 192.168.20.5

B 区访问外部

这个场景用于模拟 B 区主机访问某个外部网段被防火墙阻断,然后通过带 NAT 的 VPN 隧道从 A 区出口访问外部网络。

测试前需要断开上一个场景的隧道,并重启容器清理路由。

1
2
sudo docker-compose down
sudo docker-compose up -d

在防火墙上添加出站阻断规则。

1
2
3
sudo docker exec -it router-firewall /bin/bash
iptables -A FORWARD -s 192.168.20.0/24 -d 93.184.216.0/24 -j DROP
exit

进入 B 区网关,建立带 NAT 的 VPN 隧道。

1
2
3
4
5
6
7
8
sudo docker exec -it B-192.168.20.99 /bin/bash
ssh -w 0:0 root@10.8.0.99 \
    -o "PermitLocalCommand=yes" \
    -o "LocalCommand= ip addr add 192.168.53.88/24 dev tun0 && ip link set tun0 up \
       && ip route add 93.184.216.0/24 dev tun0 \
       && iptables -t nat -A POSTROUTING -j MASQUERADE -o tun0" \
    -o "RemoteCommand=ip addr add 192.168.53.99/24 dev tun0 && ip link set tun0 up \
       && iptables -t nat -A POSTROUTING -j MASQUERADE -o eth0"

这里多了两处 NAT 配置。

B 区网关上的 NAT 用来处理进入隧道前的源地址转换。

1
iptables -t nat -A POSTROUTING -j MASQUERADE -o tun0

A 区网关上的 NAT 用来处理从 A 区出口访问外部网络时的源地址转换。

1
iptables -t nat -A POSTROUTING -j MASQUERADE -o eth0

接着给 B1 和 B2 添加到目标网段的路由,让它们访问 93.184.216.0/24 时先交给 B 区网关。

1
2
sudo docker exec -it B1-192.168.20.5 ip route add 93.184.216.0/24 via 192.168.20.99
sudo docker exec -it B2-192.168.20.6 ip route add 93.184.216.0/24 via 192.168.20.99

在 B1 中访问 www.example.com

1
sudo docker exec -it B1-192.168.20.5 curl www.example.com

这里的流量路径可以理解为:

1
2
3
4
5
6
B1
    -> B 区网关 192.168.20.99
    -> tun0
    -> SSH 隧道
    -> A 区网关 10.8.0.99
    -> 外部网站

直接访问时,B1 到 93.184.216.0/24 的流量会命中防火墙的 DROP 规则。建立 VPN 隧道后,B1 的流量先进入 B 区网关,再通过 tun0 被封装到 SSH 连接中,最后从 A 区网关访问外部目标。

NAT 的作用是让返回包能正确回来。如果没有 NAT,外部目标或者中间节点可能不知道怎么返回到 B1 的私有地址。使用 MASQUERADE 后,出口侧会把源地址改成合适的地址,并记录连接关系,回包回来时再反向转换。

实验理解

这次实验最重要的点是理解防火墙的视角。防火墙不是天然知道用户真正想访问什么,它只能根据自己能看到的数据包字段做判断。

直接访问时,防火墙可以看到:

1
2
10.8.0.x -> 192.168.20.x
192.168.20.x -> 93.184.216.x

所以规则可以按照源网段和目的网段进行拦截。

建立 SSH 隧道后,防火墙看到的外层流量变成:

1
2
10.8.0.99 -> 192.168.20.99:22
192.168.20.99 -> 10.8.0.99:22

内部真正访问的 Telnet、HTTP 或者某个 IP 网段,都被封装在 SSH 连接里面。除非防火墙能解密或识别隧道行为,否则它只能把这部分流量当成普通 SSH 流量处理。

静态端口转发适合访问一个固定服务。

动态 SOCKS 代理适合让应用程序通过代理访问多个目标。

Layer 3 VPN 隧道更接近真正的 VPN,可以通过虚拟网卡和路由把一整段 IP 流量引入隧道。

这也解释了 VPN 的基本工作方式。VPN 的本质不是单纯“换 IP”,而是创建一条新的虚拟网络路径。系统把部分流量送进虚拟网卡,VPN 程序再把这些流量封装进另一条连接,发送到远端节点解封装,最后由远端节点继续访问目标网络。

这个实验对我来说比较应景。之前经常听到代理、隧道、VPN、NAT、路由这些词,但它们之间的关系一直比较散。这次通过 Docker 网络把它们连起来之后,能明显看到一条完整路径:防火墙负责拦截,路由决定流量走向,SSH 负责隧道封装,tun0 负责承载三层数据包,NAT 负责处理回包路径。

学完以后再看常见的 VPN 或代理软件,就不会只停留在“开了以后能访问”这个层面,而是能理解它大概在系统网络栈里动了哪些东西。

Like 0
本站已不稳定运行 小时 分钟
共发表文章 30 篇 ,总计 130.73 k 字
本站总访问量:
使用 Hugo 构建
主题 StackJimmy 设计