【问题背景】
在Web应用开发中,经常会需要获取客户端IP地址。一个典型的例子就是投票系统,为了防止刷票,需要限制每个IP地址只能投票一次。
【如何获取客户端IP】
在Java中,获取客户端IP最直接的方式就是使用request.getRemoteAddr()
。这种方式能获取到连接服务器的客户端IP,在中间没有代理的情况下,的确是最简单有效的方式。但是目前互联网Web应用很少会将应用服务器直接对外提供服务,一般都会有一层Nginx做反向代理和负载均衡,有的甚至可能有多层代理。在有反向代理的情况下,直接使用request.getRemoteAddr()
获取到的IP地址是Nginx所在服务器的IP地址,而不是客户端的IP。
HTTP协议是基于TCP协议的,由于request.getRemoteAddr()
获取到的是TCP层直接连接的客户端的IP,对于Web应用服务器来说直接连接它的客户端实际上是Nginx,也就是TCP层是拿不到真实客户端的IP。
为了解决上面的问题,很多HTTP代理会在HTTP协议头中添加X-Forwarded-For
头,用来追踪请求的来源。X-Forwarded-For的格式如下:
X-Forwarded-For: client1, proxy1, proxy2
X-Forwarded-For包含多个IP地址,每个值通过逗号+空格分开,最左边(client1)是最原始客户端的IP地址,中间如果有多层代理,每一层代理会将连接它的客户端IP追加在X-Forwarded-For右边。
下面就是一种常用的获取客户端真实IP的方法,首先从HTTP头中获取X-Forwarded-For
,如果X-Forwarded-For
头存在就按逗号分隔取最左边第一个IP地址,不存在直接通过request.getRemoteAddr()
获取IP地址:
public String getClientIp(HttpServletRequest request) {
String xff = request.getHeader("X-Forwarded-For");
if (xff == null) {
return request.getRemoteAddr();
} else {
return xff.contains(",") ? xff.split(",")[0] : xff;
}
}
另外,要让Nginx支持X-Forwarded-For头,需要配置:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
$proxy_add_x_forwarded_for
会将和Nginx直接连接的客户端IP追加在请求原有X-Forwarded-For
值的右边。
X-Forwarded-For 详细介绍链接
X-Forwarded-For(XFF)是用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段。
如果没有XFF或者另外一种相似的技术,所有通过代理服务器的连接只会显示代理服务器的IP地址(而非连接发起的原始IP地址), 这样的代理服务器实际上充当了匿名服务提供者的角色, 如果连接的原始IP地址不可得,恶意访问的检测与预防的难度将大大增加。XFF的有效性依赖于代理服务器提供的连接原始IP地址的真实性,因此, XFF的有效使用应该保证代理服务器是可信的, 比如可以通过建立可信服务器白名单的方式。
一般的客户端(例如浏览器)发送HTTP请求是没有X-Forwarded-For
头的,当请求到达第一个代理服务器时,代理服务器会加上X-Forwarded-For
请求头,并将值设为客户端的IP地址(也就是最左边第一个值),后面如果还有多个代理,会依次将IP追加到X-Forwarded-For头最右边,最终请求到达Web应用服务器,应用通过获取X-Forwarded-For
头取左边第一个IP即为客户端真实IP。
但是如果客户端在发起请求时,请求头上带上一个伪造的X-Forwarded-For
,由于后续每层代理只会追加而不会覆盖,那么最终到达应用服务器时,获取的左边第一个IP地址将会是客户端伪造的IP。也就是上面的Java代码中getClientIp()
方法获取的IP地址很有可能是伪造的IP地址,如果一个投票系统用这种方式做的IP限制,那么很容易会被刷票。
为了防范伪造X-Forwarded-For
引起的IP伪造,可以在直接对外的Nginx反向代理服务器上配置:
proxy_set_header X-Forwarded-For $remote_addr;
这里使用$remote_addr
替代上面的$proxy_add_x_forwarded_for
。$proxy_add_x_forwarded_for
会在原有X-Forwarded-For
上追加IP,这就相当于给了伪造X-Forwarded-For
的机会。而$remote_addr
是获取的是直接TCP连接的客户端IP(类似于Java中的request.getRemoteAddr()
),这个是无法伪造的,即使客户端伪造也会被覆盖掉,而不是追加。需要注意的是,如果有多层代理,那么只要在直接对外访问的Nginx上配置X-Forwarded-For
为$remote_addr
,内部层的Nginx还是要配置为$proxy_add_x_forwarded_for
,不然内部层的Nginx又会覆盖掉客户端的真实IP。
【区别】X-Forwarded-For 和 X-Real-IP ?
比如来自IP:4.4.4.4的一个请求,header包含这样一行
X-Forwarded-For: 1.1.1.1, 2.2.2.2, 3.3.3.3
代表 请求由1.1.1.1发出,经过三层代理,第一层是2.2.2.2,第二层是3.3.3.3,而本次请求的来源IP 4.4.4.4是第三层代理。
而X-Real-IP
,一般只记录真实发出请求的客户端IP。面的例子,如果配置了X-Read-IP,将会是:
X-Real-IP: 1.1.1.1
所以 ,如果只有一层代理,这两个头的值就是一样的。
详细介绍链接
User Agent中文名为用户代理,是Http协议中的一部分,属于头域的组成部分,User Agent也简称UA。它是一个特殊字符串头,是一种向访问网站提供你所使用的浏览器类型及版本、操作系统及版本、浏览器内核、等信息的标识。通过这个标 识,用户所访问的网站可以显示不同的排版从而为用户提供更好的体验或者进行信息统计;例如用手机访问谷歌和电脑访问是不一样的,这些是谷歌根据访问者的 UA来判断的。UA可以进行伪装。
浏览器的UA字串的标准格式:浏览器标识 (操作系统标识; 加密等级标识; 浏览器语言) 渲染引擎标识版本信息,但各个浏览器有所不同。
实践中客户端无法直接跟服务端发起请求的时候,我们就需要代理服务。代理可以实现客户端与服务端之间的通信,我们的Nginx也可以实现相应的代理服务。代理分为正向代理和反向代理。
正向代理和反向代理的区别我在知乎上找到两张图可以帮助我们很好的理解:
正向代理:客户端 <一> 代理 一>服务端
正向代理简单地打个租房的比方:
A(客户端)想租C(服务端)的房子,但是A(客户端)并不认识C(服务端)租不到。
B(代理)认识C(服务端)能租这个房子所以你找了B(代理)帮忙租到了这个房子。
这个过程中C(服务端)不认识A(客户端)只认识B(代理)
C(服务端)并不知道A(客户端)租了房子,只知道房子租给了B(代理)。
反向代理:客户端 一>代理 <一> 服务端
反向代理也用一个租房的例子:
A(客户端)想租一个房子,B(代理)就把这个房子租给了他。
这时候实际上C(服务端)才是房东。
B(代理)是中介把这个房子租给了A(客户端)。
这个过程中A(客户端)并不知道这个房子到底谁才是房东
他都有可能认为这个房子就是B(代理)的
由上的例子和图我们可以知道,正向代理和反向代理的区别在于代理的对象不一样,正向代理的代理对象是客户端,反向代理的代理对象是服务端。换句话说,代理服务器站在客户端那边就是正向代理,代理服务器站在原始服务器那边就是反向代理。
【环境介绍】墨者学院在线靶场链接
尝试直接投票,提示“请使用微信打开”:
那就使用微信访问并成功投票,二次投票时发现受到限制,无法刷票:
为了实现恶意无限刷票,我们可以伪造微信请求头和IP地址即可。
用BurpSuite抓取PC端网页的投票请求,如下:
再使用BurpSuite拦截手机微信的投票请求,如下:
下面开始对PC端的请求包进行伪造:
User-Agent
改为手机微信请求包的User-Agent
值:X-Forwarded-For:10.1.0.7
因为一次IP访问成功就表示成功投票一次,所以将投票请求包发送到Inturder爆破
模块,对X-Forwarded-For:10.1.0.7
设置变量,伪造多个IP进行投票:
将IP设置从1-254,从而实现自动投票254票:
开始攻击(自动投票):
返回PC端查看投票情况:
同样的套路,继续设置Forwarded-For:10.1.0.7
变量,再暴力投票一波:
完成任务,排名第一,并且拿到了Key:
墨者学院靶场环境
开启环境:
进入评论页面:
找到zhangyu并点赞,首次点赞成功:
再次点赞时提示点赞失败:
应该对IP进行限制了,没事,下面开始进行IP伪造。先BurpSuite抓取点赞的请求包:
直接发送至Intruder模块,在请求包中增加X-Forwarded-For:1.1.1.1
,并添加变量:
设置变量为0-254,并开始暴力点赞:
返回网页查看结果: