phpcms头像上传漏洞引发的故事
目录
关键代码
第一次防御
第一次绕过
第二次防御
第二次绕过
第三次防御
第三次绕过
如何构造一个出错的压缩包
第四次防御
第四次绕过
本篇文章是参考某位大佬与开发人员对于文件包含漏洞的较量记录下的故事,因为要学习文件包含漏洞,就将大佬的文件作为参考来通过学习+练习的方式复现一下这次较量的全过程
故事还要从phpcms曾经火极一时的头像上传漏洞说起,因为这个漏洞,互联网上大量站点被黑,影响极为恶劣。
简单来说phpcms对头像上传是这么处理:上传上去的zip文件,它先解压好,然后删除非图片文件。
关键代码
前端代码:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>文件上传章节练习题</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<style type="text/css">
.login-box{
margin-top: 100px;
height: 500px;
border: 1px solid #000;
}
body{
background: white;
}
.btn1{
width: 200px;
}
.d1{
display: block;
height: 400px;
}
</style>
</head>
<body>
<form method="post" action="upload.php" enctype="multipart/form-data">
<input type="file" name="file" value=""/>
<input type="submit" name="submit" value="upload"/>
</form>
</body>
</html>
后端代码:
<?php
header("Content-Type:text/html; charset=utf-8");
require_once('pclzip.lib.php');
$file = $_FILES['file'];
if ($file['size'] == 0) {
exit("请勿上传空文件");
}
$name = $file['name'];
$dir = 'uploads/';
$ext = strtolower(substr(strrchr($name, '.'), 1));
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') {
// $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 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文件!');
}
第一次防御
访问前端页面发现我们是可以上传一个文件的,因此这样就可以创建一个phpinfo.php文件和一个1.jpg文件,然后放在php文件夹中,然后将该文件夹压缩上传:
然后尝试进行上传该压缩包:
上传完成后可以看到上传成功了
然后我们可以在那个对应的目录中进行查看上传后的文件内容:
可以看到上传后的文件只剩下了一个1.jpg文件,和它一起在压缩包中的phpinfo.php文件被删除了
第一次绕过
这里那就无法上传php文件了,那么我们就真的就没有办法了吗?大佬告诉了我们解决方案:
通过代码审计,可以看到,它删除的时候没有递归删除,也没有删除文件夹。
这样,只要我们的webshell放在压缩包的文件夹中,就可避免被删除了。
因此这样就可以创建一个phpinfo文件夹里面再放入一个phpinfo.php文件一个单独的1.jpg文件,然后放在php2文件夹中,然后将该文件夹压缩上传:
上传完成后可以再去uploads文件夹中查看就会发现已经成功的上传了php文件了
可以看到,现在就成功的上传了phpinfo文件并且里面的phpinfo.php文件保留了下来,并且1.jpg也上传了,这样就成功的绕过了限制
第二次防御
后面网页管理员对网页进行了一些安全加固,将后端的检查文件代码修改为下列的形式:
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);
}
}
}
}
}
第一次绕过的方法的根本原因是因为没有考虑文件在文件夹中的情况,只删除了压缩包根目录下的非法文件,而没有删除其文件夹中的非法文件。
所以补丁就采用了递归删除的方式,将压缩包中所有非法文件删除。
但是采用了递归的方式就真的可以防御好了吗?并不是的
第二次绕过
因为通过分析代码后发现,删除的方式是先上传后删除,那么就可以尝试利用上传成功和删除的时间差来尝试访问该文件
因此这里使用Burpsuite来进行不断的上传,然后手动的访问一下上传后的文件看看会不会成功呢
首先使用Burpsuite对上传页面进行抓包,然后发送到intruder页面,添加payload:
然后设置发送1000个包,然后开始
下面就是尝试快速的不断的访问一下上传后的php文件:
可以看到通过上传后与删除的时间差是可以访问到php文件的,因此这种防御方法也是可以绕过的
第三次防御
上面的绕过方法出现后,后端人员也是快速的修改了,增加了一下代码:
$temp = FCPATH.'cache/attach/'.md5(uniqid().rand(0, 9999)).'/';
就是将压缩包放在一个随机命名的文件夹中再解压缩,这样你猜不到访问地址也就没法去暴力getshell了。
第三次绕过
通过对后端代码的审计后,发现当解压发生失败时,就退出解压缩过程。
这也是一个很平常的思路,失败了肯定要报错并退出,因为后面的代码没法运行了。
但是,程序员不会想到,有些压缩包能在解压到一半的时候出错。
什么意思,也就说我可以构造一个“出错”的压缩包,它可以解压出部分文件,但绝对会在解压未完成时出错。这是造成了一个状况:我上传的压缩包被解压了一半,webshell被解压出来了,但因为解压失败这里exit($this->pclzip->zip(true));
退出了程序执行,后面一切的删除操作都没有了作用。
如何构造一个出错的压缩包
因为这里我们首先构造一个解压会出错的压缩包,这里龙哥给我们交了几种好用的方法:
我这里就以两个解压的程序作为例子:
-
Windows下的7zip
-
PHP自带的ZipArchive库
7zip的容忍度很低,只要压缩包中某一个文件的CRC校验码出错,就会报错退出。
如何修改压缩包里文件的CRC校验码呢?可以使用010editor。
我们先准备两个文件,一个PHP文件1.php,一个文本文件2.txt,其中1.php是webshell。
然后将这两个文件压缩成shell.zip。
然后我们用010editor打开shell.zip,可以看到右下角有这个文件的格式信息,它被分成5部分
我们打开第4部分,其中有个deCrc,我们随便把值改成其他的值,然后保存。
此时用7zip解压就会出错,解压出的1.php是完好的,2.txt是一个空文件。
我们再用PHP自带的ZipArchive库,测试这个zip,发现解压并没有出错,这也说明ZipArchive的容忍度比较高。
那么我们又如何让ZipArchive出错呢?最简单的方法,我们可以在文件名上下功夫。
比如,Windows下不允许文件名中包含冒号(:)
我们就可以在010editor中将2.txt的deFileName属性的值改成“2.tx:”。
此时解压就会出错,但1.php被保留了下来。
注:在Linux下也有类似的方法,我们可以将文件名改成5个斜杠(/)。
因此这里我们第三次绕过的思路有出来了,使用一个会报错的zip文件,上传后会将php文件解压问成功,进行报错了,因此php文件会被保留
第四次防御
这里的漏洞也是被后端人员发现后修复了,因此将代码修改为下面的形式:
if(!$zip->extractTo($temp_dir)) {
check_dir($temp_dir);
exit('fail to zip');
}
可以看到,它会对解压失败的文件也进行一次检查,将里面非图片的文件删除
第四次绕过
但是这样真的就安全了吗?答案是不安全的
压缩包中通常是不含有诸如“../”、“..”这种文件名的,但通常不含有不代表不能含有。
如果把压缩包中某文件名改成../../../../../index.php,是不是就能直接把你首页变成我的webshell呀?
这就是因为抄袭者并没有真正领悟zip这个类的使用方法,导致了这个安全问题。
先把自己的shell改名字成aaaaaaaaaaaaaaaaaaaa.php
之所以起这个名字,就是预留一些空间,方便我之后将文件名改成../../../aaaaaaaaaaa.php而不用怕字符串长度不对。
把文件直接打包成zip,用010editor打开:
将画框的俩文件名的前9个字符改成../../../
然后再上传就可以成功上传了
总结一下:
1、没有对上传的文件进行递归删除,导致文件夹中的目录可以上传
2、先上传,后删除,可以利用时间竞争来访问上传后的文件
3、对上传后的文件进行一个重命名,随机名称,可以利用解压报错绕过
4、对解压后的文件进行检查,可以利用../../路径穿越进行绕过
到此这个黑客大佬与后端开发人员的对抗就结束了,但是还有很多web页面存储漏洞仍然存在着漏洞等着我们去发现