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 隧道的一端
B 区服务器 1 192.168.20.5 B 区目标服务器,提供 Telnet 服务
B 区服务器 2 192.168.20.6 B 区目标服务器,辅助测试
B 区网关 192.168.20.99 B 区出口,也是 SSH 隧道的一端
中间路由器 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

输入用户和密码登录后,输入下面的命令

1
ip -br a

可以看到已经成功登录到了 192.168.20.5的终端

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

1
tcpdump -n -i any

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

动态 SOCKS 代理

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

SOCKS 代理在应用层,处理的是应用发起的具体访问请求

新开一个终端窗口,进入 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 区网关来访问外部网站

这里使用的是 socks5hh 表示域名解析交给代理端完成。因此 B1 在/etc/hosts 中对 www.example.com 的映射主要用来直接访问触发防火墙阻断,而通过 SOCKS 代理访问时,域名可能被代理出口重新解析

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

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

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

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

3 层 VPN 隧道

这部分更接近真正 VPN 的工作方式,它会创建一个虚拟网卡 tun0,然后把数据包放进 SSH 隧道中传输。3层指的是网络层,记录了源IP、目标IP

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

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 区,而是建立 SSH 连接。但由于SSH只是一个通信协议,不参与流量的规划,因此还要在 A 区网关和 B 区网关创建虚拟网卡 tun0,作用是分离需要代理的流量。这样只需要把访问 B 区网段的流量引到 tun0,这些数据包就会被 SSH 封装后发到 B 区网关

进入 A 区网关容器,用 SSH 的 -w 参数建立三层 VPN 隧道。执行命令后终端会持续运行来维持 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 的三层隧道功能,在 A 区和 B 区网关分别创建 tun0 虚拟网卡
  • root@192.168.20.99:SSH 连接到 B 区网关,也就是 VPN 隧道出口
  • PermitLocalCommand=yes:允许 SSH 连接建立后在本地自动执行命令
  • LocalCommand=...:在 A 区网关上配置 tun0,分配 192.168.53.88/24并启动网卡
  • RemoteCommand=...:在 B 区网关上配置 tun0,分配 192.168.53.99/24并启动网卡

上一步只是搭建了传输的基础,但还没有对流量进行规划。这一步需要配置路由,给访问流量和回程流量添加规则

进入 A 区网关配置路由。为了避免 SSH 隧道自己的连接进入 tun0,先给 B 区网关加一条单独的规则来让它走原来的路径,再把访问 B 区网段的流量引入 tun0

1
2
3
4
5
sudo docker exec -it A-10.8.0.99 /bin/bash
# 访问192.168.20.99的流量走原来的中间路由器
ip route add 192.168.20.99/32 via 10.8.0.11
# 访问所有192.168.20.0网段的流量走虚拟网卡
ip route replace 192.168.20.0/24 dev tun0

还需要给 B1 配置回程路由,将目标是 192.168.53.0/24 的回包交给 B 区网关

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

回到 A 区网关再次 ping B1,此时数据包先进入 tun0,再被封装进 SSH 连接中,防火墙看到的外层还是 SSH,不会进行拦截,因此成功发送了数据包

1
ping 192.168.20.5

B区访问外部

注意:实验中的www.example.com通过CDN解析,域名解析出的IP经常会变化,变化一次就会导致之前的一大串配置全部失效,需要修改IP后重新执行。这个方案稳定性极低,但也是无奈之举,更好的实验流程是将访问目标设置为自己有公网IP的服务器

开始这个部分前,重启容器来清理路由

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

首先在A网关尝试访问目标网站,输出中可以看到www.example.com被映射到了104.20.23.154。这个IP经常变化,因此虽然在VPN转发流程中A区网关并不参与DNS解析,还是需要执行下面的命令来找到有效的IP

1
sudo docker exec A-10.8.0.99 curl -4 --connect-timeout 8 -v www.example.com

重新在路由器容器建立B区网段对于目标网段的防火墙阻断

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

将 B1 的本地映射改成 104.20.23.154

1
2
3
4
5
sudo docker exec -it B1-192.168.20.5 /bin/bash
sed '/www.example.com/d' /etc/hosts > /tmp/hosts
cat /tmp/hosts > /etc/hosts
echo "104.20.23.154 www.example.com" >> /etc/hosts
getent ahostsv4 www.example.com

直接访问目标网站被拒绝

进入 B 区网关容器,用 SSH 的 -w 参数建立带 NAT 的三层 VPN 隧道。执行命令后终端会持续运行来维持 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 104.20.23.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"
  • -w 0:0:启用 SSH 的三层隧道功能,在 B 区网关和 A 区网关之间创建 tun0 虚拟网卡
  • root@10.8.0.99:SSH 连接到 A 区网关,也就是 VPN 隧道出口
  • PermitLocalCommand=yes:允许 SSH 连接建立后在本地自动执行命令
  • LocalCommand=...:在 B 区网关上配置 tun0,分配 192.168.53.88/24,启动网卡,并把目标网段的流量导入隧道
  • RemoteCommand=...:在 A 区网关上配置tun0,分配 192.168.53.99/24并启动网卡

这里两侧都配置 NAT是为了解决返回路径问题。B 区网关上的NAT会把 B1 发出的流量伪装成 B 区网关的隧道地址 192.168.53.88,这样 A 区网关会将数据包从 tun0 送回 B 区网关,网关再根据NAT记录发给B1。A 区网关上的NAT会把从隧道转发出来的流量伪装成 A 区网关的出口地址 10.8.0.99,这样目标IP的返回包会先回到 A 区网关,再由 NAT 记录转发回隧道

然后给 B1 添加到目标网段的规则,让它访问 104.20.23.0/24 时先交给 B 区网关

1
sudo docker exec -it B1-192.168.20.5 ip route add 104.20.23.0/24 via 192.168.20.99

在 B1 容器中访问 www.example.com,直接 curl www.example.com 可能会变成 IPv6 访问,因此使用下面的命令验证

1
curl -4 www.example.com

B1 先把访问目标 IP 的数据包交给 B 区网关,B 区网关通过 NAT 把源地址改成隧道入口地址后送进tun0,SSH 隧道把数据包带到 A 区网关,A 区网关再通过 NAT 把源地址改成自己的出口地址然后从 eth0 发出。回包按照 NAT 记录逐层返回最终回到 B1,就这样实现了防火墙的越过

实验理解

在这个实验中,不管用什么方法绕过防火墙,最外层都是一个SSH隧道。整个过程中,防火墙负责拦截不允许的流量,路由决定数据包应该走普通网络还是进入隧道,SSH 作为隧道承载流量,tun0 是让流量进入隧道的虚拟网卡,NAT 负责处理返回路径。因此 VPN 本质是在原有网络外创建了一条新的虚拟网络路径

方式 本质 结果
直接 SSH 远程登录 B 网关 在 B 网关上执行命令
SSH 端口转发 转发特定端口到固定地址 只能访问指定服务
SSH SOCKS 应用层主动走代理 代理代替连接目标
SSH + tun0 新增虚拟网络 系统按照路由分流

附:VPN 访问排查

B区访问外部部分,原本是将 www.example.com 绑定到 93.184.216.34,再添加防火墙规则来阻断 B 区主机访问这个IP,最后建立VPN隧道绕过防火墙,但发现VPN建立完成后依旧无法访问

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

域名解析

在B1容器检查域名解析,发现显示的是 IPv6 地址,说明这个域名的解析可能自动走了IPv6

1
sudo docker exec B1-192.168.20.5 getent hosts www.example.com

查询 IPv4 解析结果,可以看到确实绑定了 93.184.216.34

1
getent ahostsv4 www.example.com

强制用 IPv4 访问目标,结果依旧失败

1
curl -4 --connect-timeout 5 www.example.com

网关配置

检查 B 区网关的 VPN 隧道、路由、 NAT 配置

1
2
3
sudo docker exec B-192.168.20.99 ip -br a
sudo docker exec B-192.168.20.99 ip route get 93.184.216.34
sudo docker exec B-192.168.20.99 iptables -t nat -L POSTROUTING -v -n

B 区网关上可以看到 tun0,地址是 192.168.53.88/24。访问 93.184.216.34 的路由是 dev tun0,说明网关会把目标流量送进 VPN 隧道。NAT 表中可以看到 MASQUERADE -o tun0,计数也增加了,说明有数据包经过 NAT 规则

检查 A 区网关的隧道、出站路由、NAT 、IP 转发状态

1
2
3
4
5
sudo docker exec A-10.8.0.99 ip -br a
sudo docker exec A-10.8.0.99 ip route get 93.184.216.34
sudo docker exec A-10.8.0.99 iptables -t nat -L POSTROUTING -v -n
sudo docker exec A-10.8.0.99 sysctl net.ipv4.ip_forward
sudo docker exec B-192.168.20.99 sysctl net.ipv4.ip_forward

A 区网关上可以看到 tun0,地址是 192.168.53.99/24。访问 93.184.216.34 的路由是 via 10.8.0.1 dev eth0 src 10.8.0.99,说明网关会把目标流量从 eth0 发出,并通过 10.8.0.1 访问外部网络。NAT 表中可以看到 MASQUERADE -o eth0,计数也增加了,说明有数据包经过 NAT 规则。两边的 ip_forward 也都是 1,说明配置部分没有问题

数据链路

之后抓包来查看数据包的路径,在 A 区网关上抓 tun0 的流量

1
sudo docker exec -it A-10.8.0.99 tcpdump -n -i tun0

然后在 B1 中重新访问目标网站

1
sudo docker exec B1-192.168.20.5 curl -4 --connect-timeout 5 www.example.com

抓到如下的流量,这里的 192.168.53.88 > 93.184.216.34.80 说明 B 区网关已经把 B1 的访问流量送进 VPN 隧道并到达 A 区网关的 tun0

检查 A 区网关是否把收到的流量继续从 eth0 发出去

1
sudo docker exec -it A-10.8.0.99 tcpdump -n -i eth0 host 93.184.216.34

抓到如下流量,可以看到源地址被 NAT 成 10.8.0.99,数据包也成功发出,说明传输路径是通的

目标IP

既然数据传输没有问题,那么问题很可能出在 A 区网关访问目标IP的时候。尝试直接访问 93.184.216.34,发现连接不上

1
sudo docker exec A-10.8.0.99 curl -4 --connect-timeout 8 -v http://93.184.216.34

直接访问 www.example.com 却成功了

1
sudo docker exec A-10.8.0.99 curl -4 --connect-timeout 8 -v www.example.com

输出中可以看到实际连接的是 104.20.23.154,而下面的这段信息说明请求经过 Cloudflare

1
2
3
Server: cloudflare
cf-cache-status: HIT
CF-RAY: a0c2ec3ca90216f0-FRA

可以得出 www.example.com 由 CDN 网络提供访问,域名解析可能返回不同的公网 IP。而之前只配置了 93.184.216.0/24的 VPN 隧道,当域名解析到新的IP地址时,原本的规则就无法匹配IP了,导致表现出来的结果是无法访问

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