前几天上网冲浪时发现了曾经比较火的一个漏洞,是phpcms的头像上传漏洞,感觉挺有意思的,我们来尝试复现一下这个场景。
phpcms对头像上传是这么处理:上传上去的zip文件,它先解压好,然后删除非图片文件。
关键代码:
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);
}
}
}
}
这段代码会读取我们文件夹中是否会有恶意代码,如果有则删除。
假如我们删除一个压缩包web.zip
这个压缩包里的文件夹是这个格式:
这个web.php就是我们的恶意代码
当我们选择上传后,文件夹中只留下了图片
这就是phpcms最早的防范机制
但是这段代码存在一个漏洞,它并没有递归的检查文件夹,如果我构造这样一个压缩包:
将恶意代码放在一个文件夹中,由于没有递归检查,我们的恶意代码保存了下来
phpcms迅速修复了这个漏洞
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);
}
}
}
}
}
他们添加了递归删除功能
原来的方法行不通了,还有什么方法呢
我们来看这段代码:
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) {
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文件!');
这段代码的逻辑是先解压上传的压缩包,然后再删除压缩包。
那么我们可以在解压完到还没删除的这一极短的窗口期,访问解压完的文件,并在它的上级目录生成恶意代码
首先我们写这样一个代码:
<?php file_put_contents('../../../web.php','<?php phpinfo();?>');
这个代码的作用是将生成的恶意代码向上跳三级目录,跳到upload这个目录(upload目录是系统自带的目录,一般程序不会检查到这个目录)
我们用Yakit抓包
之后重复发包,我这里设置1000次
然后进这个网址,手动刷新页面
成功
之后phpcms修复了这个漏洞
他们添加了一行代码
$temp_dir = $dir . md5(time().rand(1000, 9999)) . '/';
这行代码使目录变成了随机数,那么上一个方法就行不通了
那怎么办呢
我们看这段代码
当解压失败就退出解压过程
这是一个很平常的思路,解压失败了当然要退出,但我们可以构造一个解压了一半就退出的压缩包。这个压缩包可以解压出部分文件,但会在解压未完成时报错。因而程序停在了这里,这就导致了后面的删除操作都没有了作用。
那么怎么构造一个解压到一半就报错的压缩包呢?
首先我们创建一个文件夹,在文件夹中放入以下文件:
第一个是恶意代码文件,第二个是文本文件(注意顺序不能错,恶意代码文件要在最前面)
然后将文件压缩,用010editor打开
可以看到一共有五部分
点开第四部分
因为windows下不允许文件名中包含冒号 ,因此可以将2.txt后面加一个冒号
此时解压就会出错,但1.php被保留了下来