目录
Upload-labs/Pass-02 Content-Type绕过
Upload-labs/Pass-04 .htaccess绕过(思维导图:黑名单->htaccess绕过)
文件上传常见验证
文件上传常见验证:后缀名,类型、文件头等
后缀名(直接验证):黑名单、白名单
文件类型(间接验证):MIME信息
文件头(间接验证):内容头信息
方法:查看源码、抓包修改包信息
后缀名(直接验证):黑名单、白名单
黑名单:明确不允许上传的文件格式
asp php jsp aspx cgi war
假如说黑名单里的格式不全,比如php5和phtml格式有一定几率(取决于对方搭建平台的情况)支持执行出php代码的效果,这就是黑名单验证的缺陷(可能不全)
白名单:明确可以上传的文件格式
jpg png zip rar gif
白名单相对于黑名单安全一些,只能上传允许上传的文件,要想别的法子绕过
文件类型(间接验证):MIME信息
用 uploadlabs/pass-01示例,上传一个gif文件,抓个数据包解释一下:上传文件时每个文件会自带一个MIME信息,每个网站都有,写在数据包里 Content-Type 处,值为 image/gif
假设上传jsp文件,抓包拦截看到的 Content-Type 是 Application/octet-stream
修改这个数据包的 Content-Type 为 image/jpg 让对方误以为上传的是jpg格式从而通过验证
文件头(间接验证):内容头信息
用notepad打开png图片,发现png头部都长下面这个样子:
同理gif、jpg、等等等的文件头部信息。
头部信息可以通过抓包修改、代码修改等等手段绕过、伪造,所以单独用头部信息验证也是不严谨不安全的。
表单上传代码简要分析,理解文件上传流程
以 uploadlabs/pass-02 为例,看一下 index.php
<?php include '../config.php'; include '../head.php'; include '../menu.php'; $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.'文件夹不存在,请手工创建!'; } } ?>
<div id="upload_panel"> <ol> <li> <h3>任务</h3> <p>上传一个<code>webshell</code>到服务器。</p> </li> <li> <h3>上传区</h3> <form enctype="multipart/form-data" method="post" onsubmit="return checkFile()"> <p>请选择要上传的图片:<p> <input class="input_file" type="file" name="upload_file"/> <input class="button" type="submit" name="submit" value="上传"/> </form> <div id="msg"> <?php if($msg != null){ echo "提示:".$msg; } ?> </div> <div id="img"> <?php if($is_upload){ echo '<img src="'.$img_path.'" width="250px" />'; } ?> </div> </li> <?php if($_GET['action'] == "show_code"){ include 'show_code.php'; } ?> </ol> </div>
其中:
- enctype:上传文件类型
- method:提交方式
- onsubmit:鼠标触发时返回checkFile()函数执行结果,通过checkFile()函数进行过滤
php $_FILES:PHP中$_FILES函数细说-php教程-PHP中文网
html+php混编模式下,建议的文件上传/表单上传代码:其他语言也大差不差,明白各个地方传什么值就行
<?php echo $_FILES['upload_file']['name']; // 输出上传文件的文件名 echo $_FILES['upload_file']['type']; // 输出上传文件的文件类型 // 如果要做文件类型过滤的话就可以这样写 if $_FILES['upload_file']['type'] != 'image/png'{ echo 类似上传失败的错误信息; } echo $_FILES['upload_file']['size']; // 输出上传文件的文件大小 ?> <form enctype="multipart/form-data" method="post" action=""> <p> 请选择要上传的图片: <p> <input class="input_file" type="file" name="upload_file"/> <input class="button" type="submit" name="submit" value="上传"/> </form>
现在明白了表单上传流程逻辑,明白了混编是怎么个写法,可以靶场实战了:
Upload-labs/Pass-02 Content-Type绕过
所谓Content-Type(互联网媒体类型)也就是MIME类型,比如以下类型以及其它等等
HTML: text/html JPEG: image/jpeg GIF: image/gif JS文档: application/javascript
点击显示源码,先进行代码分析:
<li id="show_code"> <h3>代码</h3> <pre> <code class="line-numbers language-php">$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.'文件夹不存在,请手工创建!'; } } </code> </pre> </li>
首先作者先判断我们用POST方式提交(标签名为submit)的变量是否存在,如果存在,则接下来判断upload这个文件夹是否存在,如果存在则继续判断我们上传文件的文件类型,如果其文件类型为image/jpeg或者为image/png或者为image/gif,那么让temp_file这个变量获取存储在服务器中文件的临时名称,然后让img_path这个变量为upload+/+被上传文件的名称。再进行判断如果move_uploaded_file()函数返回ture(也就是成功将文件移动到img_path路径下)那么is_upload变量为true,即上传成功。
只需抓包,然后更改其content-type类型为:
image/jpeg
即可绕过,符合源码中对于Content-Type的限制,成功绕过了验证,完成了文件上传。
Upload-labs/Pass-03 黑名单绕过
所谓黑名单就是限制了哪些不可以,除了不可以的都可以。所以我们只要构造黑名单之外的后缀名即可绕过。
$is_upload = false; $msg = null; if (isset($_POST['submit'])) { if (file_exists(UPLOAD_PATH)) { // deny extension $deny_ext = array('.asp','.aspx','.php','.jsp'); // trim 去空格什么的 $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 . '文件夹不存在,请手工创建!'; } }
可以不上传php格式,上传后缀名为
.php1、.php2、phtml
等等都可以完成绕过,然后成功上传一句话木马,用菜刀或者蚁剑连接即可。(能否成功执行取决于对方网站平台,比如Apache httpd.conf配置文件)
Upload-labs/Pass-04 .htaccess绕过(思维导图:黑名单->htaccess绕过)
这关源码与上一关不同的就是它的黑名单数组几乎包括了我们能想到的后缀名,所以我们要用特殊的方法来进行绕过。
.htaccess文件,全称是超文本入口,提供了针对目录改变配置的方法,即在一个特定的文档目录中放置一个包含一个或多个指令的文件,以作用于此目录及其所有子目录。作为用户,所能使用的命令受到限制。管理员可以通过Apache的AllowOverride指令来设置。
$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 . '文件夹不存在,请手工创建!'; } }
可以通过此文件,将jpg文件全部解析为php文件。只需新建.htaccess格式的文件,用notepad打开,写入下面的代码:
<FilesMatch "cimer"> Sethandler application/x-httpd-php </FilesMatch>
上传.htaccess文件设置的关键字的文件名,即上传一个黑名单没有过滤的随意后缀名文件,但文件名中一定要包含cimer,如"cimer000123.jpg",内容为一句话木马。此时"cimer000123.jpg"会被Apache当作php解析。
Upload-labs/Pass-06 大小写
黑名单数组不仅包含三四关所有的文件类型,而且不让.htaccess上传。
但源码少了对于文件名都要转换为小写的验证 $file_ext = strtolower($file_ext);
所以可以通过大小写来进行绕过:
$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 = 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 = '此文件类型不允许上传!'; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } }
Upload-labs/Pass-07 无首尾去空格
查看源码发现少了对文件名处理的最后一行:$file_ext = trim($file_ext); //首尾去空
<li id="show_code"> <h3>代码</h3> <pre> <code class="line-numbers language-php">$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 . '文件夹不存在,请手工创建!'; } } </code> </pre> </li>
Windows下xx.jpg[空格] 或者xx.jpg.这两类文件都是不允许存在的,若这样命名,windows会默认去除空格或点。
如果在上传文件的后缀名里面加上空格,匹配到的含空格的字符串就在黑名单之外,就可以成功进行上传。
Upload-labs/Pass-09 ::$DATA绕过
查看源码发现少了对文件名处理的删除文件名末尾的点:$file_name = deldot($file_name);
Windows下xx.jpg[空格] 或者xx.jpg.这两类文件都是不允许存在的,若这样命名,windows会默认去除空格或点。
$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 . '文件夹不存在,请手工创建!'; } }
在文件名后面加上::$DATA 绕过文件上传检测,这个::$DATA 到底是什么?
php在windows系统下,如果文件后缀名加上了::$DATA,会把::$DATA 之后的数据当成文件流处理,不会检测后缀名,且保持::$DATA 之前的文件名。
$file_ext = str_ireplace('::$DATA', '', $file_ext);
上传cimer111.php,burpsuite一抓,改filename后缀为cimer111.php::$DATA。
成功上传php文件。
Upload-labs/Pass-10 双写绕过
waf 常用手段。服务端逻辑对不合法的后缀名进行了替换为空。
扩充下思路:例如后端限制了php,如果发现我们上传的文件名后缀为php,就替换为空
a.php -> .php -> a 一次过滤
a.pphphp ->php -> a.php 循环过滤,更安全$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","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess","ini"); $file_name = trim($_FILES['upload_file']['name']); $file_name = str_ireplace($deny_ext,"", $file_name); $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 = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } }
代码仅做了一次过滤/替换为空,所以上传一个.php文件,抓包修改后缀为a.pphphp,即可绕过过滤。
Upload-labs/Pass-11 白名单 %00截断
只允许:jpg, png, gif
$is_upload = false; $msg = null; if(isset($_POST['submit'])){ // 白名单 $ext_arr = array('jpg','png','gif'); // 避免多个点绕过 $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1); if(in_array($file_ext,$ext_arr)){ $temp_file = $_FILES['upload_file']['tmp_name']; $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext; if(move_uploaded_file($temp_file,$img_path)){ $is_upload = true; } else { $msg = '上传出错!'; } } else{ $msg = "只允许上传.jpg|.png|.gif类型文件!"; } }
- $_FILES['upload_file']['name']:这部分代码从$_FILES 超全局数组中获取了上传文件的原始文件名。$_FILES 超全局数组用于处理文件上传操作,其中 'upload_file' 是上传文件表单字段的名称。
- strrpos($_FILES['upload_file']['name'], "."):这部分代码使用了PHP的strrpos函数,它用来查找字符串中最后一次出现指定字符的位置。在这里,它查找了文件名中最后一个.(点号)的位置。这个位置将被用于确定文件扩展名的起始点。
- +1:这是简单的加法操作,目的是将上述找到的点号的位置加1,以获取文件扩展名的起始位置。这是因为点号本身并不属于扩展名,需要从点号的下一个字符开始提取扩展名。
- substr($_FILES['upload_file']['name'], strrpos($_FILES['upload_file']['name'], ".")+1):最后,使用substr函数,从原始文件名中提取文件扩展名。substr函数接受两个参数,第一个参数是源字符串(这里是文件名),第二个参数是起始位置(从点号后一位开始),然后它返回从起始位置开始的子字符串,即文件的扩展名。
一定要抓包观察参数细节!!抓包发现数据报头为:
POST /uploadlabs/Pass-11/index.php?savepath="../upload/ HTTP1.1
%00截断:%00后面的东西没有了
想要上传一个php文件,首先要抓包更改它的后缀名来满足白名单验证,并且因为源码+抓包都可判断文件的保存路径是拼接的,所以我们直接可以在可控的部分(数据报头)直接输入文件名,并且用%00截断来截断后面那些多余的内容:
Upload-labs/Pass-12 白名单
<li id="show_code"> <h3>代码</h3> <pre> <code class="line-numbers language-php">$is_upload = false; $msg = null; if(isset($_POST['submit'])){ $ext_arr = array('jpg','png','gif'); $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1); if(in_array($file_ext,$ext_arr)){ $temp_file = $_FILES['upload_file']['tmp_name']; $img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext; if(move_uploaded_file($temp_file,$img_path)){ $is_upload = true; } else { $msg = "上传失败"; } } else { $msg = "只允许上传.jpg|.png|.gif类型文件!"; } } </code> </pre> </li>
都一样,只不过 $img_path 由POST方式接收,上一关是GET方式直接在数据报头修改拼接上传地址了。
因为GET会自动解码(比如在网页地址上敲个空格就会自动变成 %20,你写个 %20 网页也知道这是个空格),在URL中 %00 表示ascll码中的 0,而ascii中0作为特殊字符保留,表示字符串结束。当URL中出现 %00,就认为读取已结束。
但POST不会对%00进行解码。所以有两种方法:
- 将其直接URL解码
- Burp抓包拦截更改其十六进制进制码
上传图片的原地址为:
删除掉 %00 之后的多余的内容,就可以成功进行访问所上传的php文件。