背景
今天想从【今日热榜】这个 App 中获取榜单的 Get 请求,从而可以“借”它们的数据做个榜单类的小程序。用 Charles 抓手机的包之后,发现可以抓取到一个 Get 请求,回包的内容也正确被抓取了,可是当我复制这段 Get 请求的 url 到浏览器的时候,发现没有对应的回包了
奇怪,为什么会这样呢,决定开始研究下
分析
url 是类似以下结构
1 | http://api.tophub.today/nodes/128?nonce=xxx&sign=yyy×tamp=123 |
主要有 3 个字段:nonce
、sign
和 timestamp
基于这几个关键词去搜索,发现原来我这种行为属于【重放攻击】
什么是重放攻击
假如我们要通过 Get 请求传递用户的用户名和密码,明文传输的例子如下
1 | http://api.tophub.today/nodes/128?id=hello&password=1234 |
这是裸奔,我们没这么蠢,于是对 password 进行 MD5 加密之后再传输,如下
1 | http://api.tophub.today/nodes/128?id=hello&sign=md5(password) |
其中 sign 的常用加密算法为 MD5,MD5 算法是一种不可逆算法,也就是说你加密之后就不能解密了
服务器的验证方法就是根据 id 从数据库查出用户的密码,再对其进行 MD5 加密,得到的结果与 sign 的值进行对比,如果相同则请求合法,反之则 sign 被篡改过
黑客通过抓包获取到该链接,他无法破解加密信息得到用户密码;但是他只要模拟此次的正常请求,就可以伪装成客户端同服务器通信。这样至少可以做两件事,第一件是跟我一样,可以随时随地模拟客户端取服务器的数据;第二件事是可以疯狂发送该请求致使服务器繁忙
如何防止重放攻击
时间戳
首先我们假设一次 HTTP 请求从发出到到达服务器的时间是不会超过 60s 的,而黑客获取链接到篡改链接再发送到服务的时间会超过 60s(不要问为什么是 60s,继续往下看)
当你发送一个请求时必须携带一个当前的时间戳 timestamp。假设值为 10
为了防止黑客修改时间戳,需要将 timestamp 也进行 MD5,放到 sign 中,如下
1 | ?id=hello&sign=md5(password+timestamp)×tamp=10 |
当请求到达服务器之后,服务器会获取当前时间,假设为 t2 = 80,很明显 t2 - timestamp > 60s,那么服务器就认为请求不合法。因为这个请求从客户端到服务器的时间竟然超过 60s。如果在 60s 内,再进行一次 md5 校验,检查 timestamp 有没有被篡改(一旦 timestamp 被篡改过,算出来的 md5 值一定与 sign 不同)
问题来了,如果黑客在 60s 内发起请求,这种方法就失效了
随机数
我们加入一个随机数 nonce,每次成功请求,服务器会保存当前成功请求的随机数 nonce 到缓存或数据库中,当请求再次进到服务器,判断携带的随机数 nonce 是否在缓存或者数据库中已经存在,如果存在,则认为请求非法
url 如下
1 | ?id=hello&sign=md5(password+nonce)&nonce=31415 |
同理,为了防止 nonce 被篡改,需要将 nonce 进行 MD5 加密到 sign
随机数的出现保证了请求的唯一性,但是存储 nonce 的集合会越来越大,为了防止 nonce 集合无限大,需要定期清理该集合,但是一旦该集合被清理,我们就无法验证被清理了的 nonce 参数了。
比如,假设该集合平均1天清理一次的话,我们抓取到的该 url,虽然当时无法进行重放攻击,但是我们还是可以每隔一天进行一次重放攻击的。而且存储24小时内,所有请求的 nonce 参数,也是一笔不小的开销
当然随机数还得确保唯一性
时间戳+随机数
时间戳屏蔽了 60s 后的请求,而 60s 内重复的请求可以用随机数来过滤
url 如下
1 | ?id=hello&sign=md5(password+timestamp+nonce)×tamp=10&nonce=31415 |
配合时间戳,服务器只要每次有新的请求进来的时候,确保此次请求没有被篡改的前提下,如果新请求带来的 timestamp 比最后一次更新 nonce 集合的时间晚 60秒,那么就可以清除 nonce 缓存了
1 | // 判断 time 参数是否有效 |