提要

本文适用情况:

用户第一层访问的是CDN,代理链路可信不会修改你的自定义请求头。

本文使用的CDN系统是FlexCDN,若你的CDN使用的是其他系统可能不通用,请自行寻找请求变量

按照本文教程配置成功后可以实现“一头走天下”:只要代理链路不对自定义头修改,任何代理链路和源站都可以直接读取这个头获得访客ip,而不需要繁琐的配置XFF

不想看长文的直接跳转到正片


最近我看到有些同学们配XFF出现了很多问题,依照我早期摸索出的经验,写下这篇文章。

基础知识

什么是HTTP请求体

HTTP请求体由三部分组成:请求行、请求头、报文体。

这是一个正常的的HTTP请求

HTTP请求示例

其中,第一行的①、②、③统称为请求行;报文头也称为请求头

可以看到,一个正常的HTTP请求带有这些参数,接下来我将通过访问哔哩哔哩网站并抓包来详细讲解。

HTTP请求示例2

在这个请求体示例中,由于是用的GET方法进行请求,GET请求方法是指:向服务器发出请求,意图从服务器下载东西。因此不含有报文体。

可以看到,在我访问哔哩哔哩的请求头中,我使用HTTP/1.1协议通过GET方法向服务器发出请求;在User-Agent栏,也就是我们熟称的浏览器UA中可以看到,我正在使用版本号为537.36的电脑版Edge浏览器进行访问,这个浏览器的内核是Chrome136.0.0.0,一些手机浏览器访问电脑版网页也是通过伪造浏览器UA来实现。

什么是X-Forward-For

X-Forwarded-For 是相对通用的 HTTP 请求头。

当CDN部署在最外层,HTTP 流量是经过CDN进行代理传回的,由于网络连接被CDN截胡,源站服务器无法得知真正的客户端 IP。

这时代理设备(CDN)会给当前的流量加上一个 X-Forwarded-For 头,里面的内容就是连接这个代理的客户端 IP。

下面这个例子中 HTTP 代理通过 X-Forwarded-For 头告诉服务器,真正的客户端地址是 1.2.3.4

1
2
3
4
GET / HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36
X-Forwarded-For: 1.2.3.4

X-Forwarded-For 实际上是一个链式结构。如果流量经过了多层代理设备,X-Forwarded-For 会记录途径的所有 IP。

下面这个例子中 HTTP 代理通过 X-Forwarded-For 头告诉服务器,流量经过了三层代理,真正的客户端地址是 1.2.3.4

第一层代理的是 11.12.13.14,第二层代理的地址是 21.22.23.24,第三次代理的地址可以直接通过网络连接获取。

1
2
3
4
GET / HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36
X-Forwarded-For: 1.2.3.4, 11.12.13.14, 21.22.23.24
  • 从网络连接中获取: 当雷池作为最外层代理设备,无其他前置代理时选用

  • X-Forwarded-For 中获取上一级代理的地址:在流量到达雷池之前还有一层代理设备(如 Nginx,CDN 等)时可选用

  • X-Forwarded-For 中获取上上一级代理的地址:在流量到达雷池之前还有两层代理设备(如 Nginx,CDN 等)时可选用

  • X-Forwarded-For 中获取上上上一级代理的地址:在流量到达雷池之前还有三层代理设备(如 Nginx,CDN 等)时可选用

  • 从其他 HTTP Header 中获取

    :有几种情况

    • 流量经过了一些特殊的反向代理设备,这类代理不会发送 X-Forwarded-For 头,但是可以通过配置,把 IP 通过其他头发过来
    • 流量到达雷池有多种途径,可能有一层代理,也可能有两层代理,可以通过配置前置代理设备来统一 HTTP

问题

X-Forward-For的链式结构会带来一些问题,比如这位同学

3

这位同学的CDN请求雷池的请求头中,XFF携带的ip数量不等,原因是用户的请求头中就有残留XFF报头,有的CDN只会在XFF后面加上直接获取的ip,而不会清空重写,这一般是用户存在代理导致的。

一般来说多的前几个是代理ip,cdn添加的是直接从网络连接获取ip,不是这位同学所说的最左边才是真实ip(当然也可能是代理转发前的ip),因此可以通过从后往前读xff获取cdn从网络连接中直接获取的ip

但是还有什么办法,更通俗易懂呢?

X-Real-IP

有的同学说,X-Real-IP不会被伪造,经我测试,在一些存在漏洞的CDN中依然可以被伪造,例子如下

第一步,我提前在CDN之前就添加了X-Real-IP请求头,写入阿里云DNS 的ip223.5.5.5。在雷池中配置使用X-Real-IP读取访客ip。

这个请求体中包含恶意元素,将使得雷池WAF将拦截我的请求,便于我观察转发后的流量。

第一步

发包后,雷池拦截

5

可以看到,X-Real-IP并没有被清空重写,而是直接就这样发出去了,我CDN是有开传递X-Real-IP的。

这下不得了了,此时攻击者直接变成了223.5.5.5(阿里云DNS的IP),并且IP 威胁情报-长亭百川云平台甚至可以查到?!

6

阿里云对不起。。。

正片

配置CDN

在CDN控制台中,添加请求报头,报头名称你可以自定义,这里我使用X-Real-Forwarded-For作为我自定义的请求报头。

值填${rawRemoteAddr},意为CDN通过网络连接直接获取的ip地址。

7

配置雷池

没有装雷池的可以直接往下看,由于自定义报头不存在链式结构,因此每一层都可以直接通过这个头获得ip。

在雷池——防护应用——高级配置中的源IP获取方式中,设置从HTTP Header中获取,值填你自定义的请求报头。

9

配置源站

在你的Nginx配置文件中加入以下配置

1
2
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Real-Forwarded-For; #填你自定义的请求头

记得要在前面加两个缩进

10

然后保存

测试

对我的博客进行模拟 SQL 注入攻击并抓包,如何测试防护效果请翻阅雷池官方文档:测试防护效果 | 雷池 SafeLine

11

可以看到被拦截了,返回了403,我们回雷池看一下日志

12

可以看到被拦截,请求报文中带有我们自定义的X-Real-Forwarded-For,记录了我的真实ip。

接下来我进行请求重放,在用户端就伪造这个请求头,模拟这个请求头被黑客知道了的情况

一般别人不可能知道我们自定义的请求头,因为这个请求头只在CDN和雷池(或源站)之间通信,不对外发送和开放。

但是为了确保严谨性,我还是进行请求重放测试

13

我自己手动添加了一个自定义的请求头,模拟被黑客知道然后伪造请求头攻击的情况。

可以看到被雷池BYPASS,回到雷池日志看看情况

15

可以看到自定义请求头仍然被替换成了访客的真实ip,也就是说通过伪造请求头骗过源站的方式不凑效了。

实现了一头走天下:只要代理链路不对这个头修改,任何代理链路都可以直接读取这个头获得访客ip,而不需要繁琐的配置XFF。

答疑

为什么我的宝塔面板同时出现雷池和访客的ip?(如下图)

16

答:因为你在雷池启用了上游服务器健康检查。

解决方法:在雷池面板防护应用的应用详情中,点基本信息——基础配置右边的笔,关掉上游服务器健康检查。