这是我第三次在自己博客里找到致命漏洞了。第一次是一个第三方存储,解决方案是删了。第二次是"EMLOG相册",也就是这篇文章:https://www.leavesongs.com/PENETRATION/emlog-important-plugin-getshell.html。第三次就是这次,我写了一个利用脚本,直接把自己博客的整站备份文件下下来了,包括管理员密码。
导致漏洞的是这个插件:http://www.emlog.net/plugin/14 。其id为14,也是emlog存在较早的插件了,作者是emlog大版主KLLER。
说说漏洞成因。
这个插件是自动备份用的。它在前端放一个ajax控件,在每次用户访问时请求一次插件,插件检查一下上次备份的文件时间,如果相隔时间超过一定值,那么就再次进行备份。通过这个方法来达到"自动备份"的效果。
看代码,kl_auto_backup_and_mail_do.php,这就是ajax请求的文件,是不限制权限的。
<?php
$is_reproduct = false;
echo KL_AUTO_BACKUP_AND_MAIL_THE_TIME."\n";
if(KL_AUTO_BACKUP_AND_MAIL_LAST_BACKUP_FILE != '' && file_exists(KL_AUTO_BACKUP_AND_MAIL_LAST_BACKUP_FILE))
{
$delay_time = time() - filemtime(KL_AUTO_BACKUP_AND_MAIL_LAST_BACKUP_FILE);
if($delay_time > intval(KL_AUTO_BACKUP_AND_MAIL_THE_TIME)) $is_reproduct = true;
echo $delay_time;
}else{
$is_reproduct = true;
}
$is_reproduct表示是否备份,KL_AUTO_BACKUP_AND_MAIL_THE_TIME是临界时间,KL_AUTO_BACKUP_AND_MAIL_LAST_BACKUP_FILE是上次备份的文件名。
如果KL_AUTO_BACKUP_AND_MAIL_LAST_BACKUP_FILE存在,进入if语句。判断现在时间和上次备份时间的差是否大于临界时间,如果大于则将$is_reproduct设为true。最后会输出$delay_time。
$delay_time是个很重要的值,它代表着"当前时间"和上次备份的文件的"创建时间"之差。而"当前时间"我们是知道的,通过这里输出的$delay_time,我们就可以计算出上次备份文件的创建时间。
这个时间很重要,后面会用到。
往后看代码:
若$is_reproduct为true则进入if语句,并删除上一次的备份文件(严格来说是上上一次的备份文件,此处不影响后面的漏洞利用过程)。之后,它将此时的时间翻来覆去计算为一个文件名,并将所有数据库data写入了这个文件。
归根结底,文件名是和时间戳一一对应的。那么反过来,只要知道这个文件的创建时间,那么就可以反推出文件名。
而通过之前的分析,我们可以得出上一次创建的备份文件的创建时间,那么其实就可以推出他的文件名了。
那么,这样就造成了一个"备份文件名可被准确计算"的漏洞,造成整站数据库备份泄露。
但漏洞利用还是有几处不稳定的地方:
- 备份文件创建时,计算当前时间和最后文件创建好linux系统里的文件mtime不一定相等,因为中间还执行了sql语句耽误了一些时间,所以文件名的时间不一定能准确预测,但差距不会太大,一两秒而已。
- 利用时http传输消耗一些时间,导致我们获得的now time和服务器上获取的time()不一定相等,有一定误差。
- 我们的时区和网站服务器的时区不一定相等,而网站所在服务器是怎样设置时区的我们不知道,所以需要一个个尝试。
这三个不稳定的地方,导致我们的POC需要暴力跑个十几、上百次才能找到最终的备份文件,但跑的成本很低,对攻击者来说压力不大,可以接受。
KLLER自身不知道为何没有跑出文件,但我手工翻了下emlog论坛,找到了三个受害者:
工具跑了差不多50次,跑出了最终的备份文件:
这个洞还属于0day漏洞,影响虽说有限,但威力巨大,一下可以拿到整站的数据库,值得关注。
解决方法:
- 暂时删除该插件
- 服务器/WAF对于所有后缀为.sql的请求都拦截
- 将$defname修改为随机字符串,如:
<?php
/*
* getRandStr是emlog自带的随机字符串生成函数,利用随机字符串即可避免此问题
*/
$defname = 'emlog_'. gmdate('Ymd', time() + $timezone * 3600) . '_' . substr(md5(getRandStr()),0,18);