上次我po某cms的邮箱找回密码功能的验证模块不太严谨,这次又找到一个信息泄漏漏洞,导致的后果是攻击者可以重置任意邮箱账号的密码。攻击者可以通过cms的邮箱找回密码功能从自己的邮箱中获取关键参数key,拿到key之后在本地利用算法本身的问题可以算出配置文件中的私钥pwdhashmd5值。之后再利用找回密码逻辑上的问题与pwdhashmd5值可以自行构造密码重置链接通过服务端的检查重置任意邮箱账户的密码。


漏洞详情

生成重置密码链接中的key参数加密过程:

/upload/Application/Home/Controller/MembersController.class.php

图片1.png

$str 字符串:e=$email&k=$key&t=$time

这里的 $time 是第一个不可控点,但是通过在发送请求前和成功收到响应后记录记录时间可以将 $time 控制在一个很小的范围,即使考虑本地和服务器获取的时间区别前后各加上5秒的延迟补偿,时间范围最差也在20秒以内。

C(PWDHASH) 是网站程序加密过程的私钥,

在安装时随机生成 /upload/Application/Common/Common/function.php

图片2.png

$encrypt_key 是加密过程中的第2个不可控点,简记为en,共32000种可能。for循环加密过程很简单,将txt字符串和en字符串逐位异或的结果与en字符串逐位拼接,当下标走到32时,en的指针归0(因为en字符串只有32位,而通过简单判断txt字符串长度肯定超过32位),最后生成的tmp,记作tmp1字符串如下:

/upload/Application/Common/Common/function.php

图片3.png

passport_key的加密过程和encrypt类似,逐位异或,不拼接,for循环结束后生成的tmp,记作tmp2字符串如下:

en[0]^pwd[0]

txt[0]^en[0]^pwd[1]

en[15]^pwd[30]

txt[15]^en[15]^pwd[31]

passport_key tmp2 返回给encrypt 后再经过base64编码就是重置链接中的key参数。

现在回顾一下加密过程用到的参数

$email :输入邮箱,可控

$time :代码执行时间,通过监测发包和收包的时间,可控在一个范围内,比如10

md5(rand(0, 32000)) 32000种遍历结果

pwdhash :未知

即 en_func($email, $time, md5(rand(0, 32000)), md5(pwdhash)) = key

由于异或操作

$a ^ $ = $c  =>  $a^$c = $b

所以 md5(pwdhash) = de_func($email, $time, md5(rand(0, 32000)), key)

此外,对于time(以10秒为例) * 3200032万种输入,怎么过滤出正确的结果。

通过用邮箱中的key参数与其他参数异或,可以得到32万种md5(pwdhash),但是md5输出的字符串只可能包含数字和小写字母af,用这一点就能将正确结果过滤出来(实际测试过程中,不正确的输入除了会导致结果包含其他字母,因为异或实际上产生了大量非打印字符)

至此算出了pwdhashmd5

再看key的校验过程

/upload/Application/Home/Controller/MembersController.class.php

图片4.png

获取重置密码链接中的key参数后用decrypt方法对其进行解密,结果放在$data

然后验证了4个条件:

  1. data[e] 是否符合邮箱格式(甚至不判断是不是刚才的邮箱!)

  2. data[t] 代表的时间有没有过期

  3. data[k] 是否等于substr(md5($data[e].$data['t']),8,16)

  4. 前缀_members表中是否有与该邮箱关联的账号

再看解密函数怎么做的

图片5.png

这里就不再po密钥处理函数的过程了,简单说一下,首先base64解码key参数,然后交给passport_key函数按位异或,之前tmp2的内容

en[0]^pwd[0]

txt[0]^en[0]^pwd[1]

en[15]^pwd[30]

txt[15]^en[15]^pwd[31]

  passport_key的处理过程以字符串第1位为例

(en[0]^pwd[0]).   ^.   pwd[0] = en[0]

(txt[0]^en[0]^pwd[1])    ^.   pwd[1] = en[0] ^ txt[0]

……

……

回到decrypt函数这儿for循环的解密过程就是

en[0] ^ (en[0] ^ txt[0]) = txt[0]

en[1] ^ (en[1] ^ txt[1]) = txt[1]

……

……

 

最后得到的就是txt字符串

e=$email&k=$key&t=$time

而未知的那个032000md5值在解密的异或过程中就被抵消了。


POC

测试环境需要在cms后台开启另外smtp,另外,为了省去激活测试账号的步骤,我直接将email_audit列置1,对漏洞没有影响。

首先用一个可用邮箱注册账号,发包申请邮箱找回密码,记录收发包时间范围

payload_send_request.py

B1.jpg

记录的时间

图片7.png

用邮箱中的key参数和刚才记录的时间开始破解

图片8.png

32000*11种可能1秒就出货了

图片9.png

图片10.png

看一下算出来的pwdhash是否正确

image.png

再注册一个测试账号test22

图片12.png

请求重置密码

图片13.png


然后用刚才的pwdhash构造重置密码链接

B2.jpg

点击console中生成的链接

1.jpg

我们可以进一步验证数据库是否改变,随便输入一个新密码888888

AA.jpg

看看数据库,显然密码发生了变化

图片17.png


利用方式

从之前的分析过程可以看出这个漏洞的利用方式比较暴力,正常流程

1、通过自己的测试账号利用邮箱找回密码的过程本地算出pwdhash

2、利用邮箱找回密码功能提交目标邮箱账户

3、本地利用步骤1中的pwdhash构造重置密码链接重置目标邮箱账户的密码

这样做的结果是无法恢复目标账户的密码,并且目标的邮箱里留下了一封重置密码链接。

能否做到完美的利用,即既不往目标邮箱发送邮件还能在重置后还原。如果在生成重置链接的时候没有以session、数据库或者文件等可存储等形式存储账户标志(emailusernameuid等)或者即使存储在校验时不校验账户信息即可满足第一点;第二点比较困难,需要密码找回过程中用到原密码,并且我们能通过无论是输出或是其他方法去暴力验证。

user_getpass方法:

图片18.png

生成密码重置链接的过程中没有以任何可存储介质存储账户标志,但是在最后可以看到账户的uid被放入到了session

图片19.png

校验的user_setpass方法:

图片20.png

完全没有对uid的校验,还在最后把session中的原uid直接干掉,令人窒息的操作。。。

所以显然,可以在不向目标账户邮箱发送重置链接的情况下重置密码,大概过程就是

1、获取pwdhash

2、重置密码页面输入自己的邮箱

3、利用pwdhash和目标邮箱构造重置密码链接重置目标账号的密码

过程截图就不贴了。

至于第二点密码恢复,从这个过程可以看出显然做不到。如果碰到类似的漏洞,有没有可能恢复密码呢?我想到了Skype一类的app在重置密码时如果新密码是该账号用过的密码,那么它会提示你不要设置曾经用过的密码,顺着这个思路有可能给它暴力破解出来。但是这种方法也有个问题,这种方法不能在本地测试,校验一般是在服务端,一下子发这么多数据包大概率会被防火墙干掉。

作者:XiaoC,安全脉搏社区