尽可能详细的文件上传讲解

 

大部分看到这篇文章的人应该都明白什么是文件上传,文件上传是攻击非常常见的方式,网上也有非常多的优秀文章,不过还是想自己整理+归纳+总结+讲解一篇关于文件上传的文章

这里借助一个靶场总结了一下常见的文件上传漏洞类型,在平时的业务、实战或CTF中可能会遇到这个漏洞,在做靶场时应该抱着学习此类型及其相关技术为目的,不要单纯拿结果

因为lmn有做过几遍upload-labs master靶场的题目,而且靶场囊括的类型也比较全面,这里也就结合了靶场的题目

 

0x01 upload-labs master的安装

“upload-labs master”这个靶场在接触文件上传的人应该都听过,lmn也做了几遍整理过笔记,毕竟网上教程这么多这里就简单总结一下

下面借助了很多靶场中的题目,可以自行安装

下载地址:https://github.com/c0ny1/upload-labs

运行环境:

操作系统:windows、Linux

php版本:大部分都是推荐 5.2.17版本

别忘了新建一个upload

配置好就可以开始了!

 

0x02 前端JavaScript检测

这道题Pass-01就是利用前端校验,也属于客户端校验,经常碰到前端使用JS代码检测被上传文件的上传类型和文件大小,只有前端教研安全性很低,可以通过禁用JS来绕过

上传入口看到一个 onsubmit 参数,onsubmit是在表单提交之前调用,在我们点击提交之后,就会调用这个事件句柄函数,也就是 checkFile()

确定为js绕过代码

推荐一个很好用的禁止js的插件,叫NoScript,可以方便打开或禁止js

还有一种方法就是直接删掉checkFile()函数

 

0x03 检测文件类型

文件上传中文件类型的检测也比较常见比较好绕过,例如这么一段代码

$_FILES[‘upload_file’][‘type’] == ‘image/jpeg’

意思是判断文件类型是否等于image/jpeg,是的话提交成功,不是则通知用户提交失败

这里可以拓展一下php的$_FILES系统函数用法

  1. $_FILES[‘myFile’][‘name’] 表示文件的名称
  2. $_FILES[‘myFile’][‘type’] 表示文件的 MIME 类型
  3. $_FILES[‘myFile’][‘size’] 已上传文件的大小(单位:字节)
  4. $_FILES[‘myFile’][‘tmp_name’] 储存的临时文件名,一般是系统默认
  5. $_FILES[‘myFile’][‘error’] 该文件上传相关的错误代码,PHP4.2版本后增加的

例如Pass-02(MIME 类型验证)这道题,尝试通过burp拦截包对Content-Type进行修改

MIME全名叫多用途互联网邮件扩展(Multipurpose Internet MailExtensions),现在被应用到多种协议里,MIME的常见形式是一个主类型加一个子类型,用斜线分隔

  1. 百科给出的比较全面的类型:
  2. 超文本标记语言文本 .html text/html
  3. xml文档 .xml text/xml
  4. XHTML文档 .xhtml application/xhtml+xml
  5. 普通文本 .txt text/plain
  6. RTF文本 .rtf application/rtf
  7. PDF文档 .pdf application/pdf
  8. Microsoft Word文件 .word application/msword
  9. PNG图像 .png image/png
  10. GIF图形 .gif image/gif
  11. JPEG图形 .jpeg,.jpg image/jpeg

Content-Type(内容类型):用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件

查看题目给出的源码进行分析,我添加上了一些注释,会的可以忽略,不会的可以参考

$is_upload = false;
$msg = null;
// isset()函数用于检测变量是否已设置并且非NULL
if (isset($_POST[‘submit’])) {
// file_exists() 函数检查文件或目录是否存在
// $UPLOAD_ADDR 为上传到哪个地址
if (file_exists($UPLOAD_ADDR)) {
// 必须满足 upload_file 的类型为’image/jpeg’或’image/png’或’image/gif’
if (($_FILES[‘upload_file’][‘type’] == ‘image/jpeg’) || ($_FILES[‘upload_file’][‘type’] == ‘image/png’) || ($_FILES[‘upload_file’][‘type’] == ‘image/gif’)) {
// move_uploaded_file() 函数将上传的文件移动到新位置
if (move_uploaded_file($_FILES[‘upload_file’][‘tmp_name’], $UPLOAD_ADDR . ‘/’ . $_FILES[‘upload_file’][‘name’])) {
$img_path = $UPLOAD_ADDR . $_FILES[‘upload_file’][‘name’];
$is_upload = true;
}
} else {
$msg = ‘文件类型不正确,请重新上传!’;
}
} else {
$msg = $UPLOAD_ADDR.’文件夹不存在,请手工创建!’;
}
}

0x04 根据文件头检测文件类型

根据文件内容类型

与上一个类似,检查文件的类型,但这个是通过检查文件的内容,根据文件的内容判断文件类型,这里就不能直接burp抓包修改Content-Type了

upload-labs master中有几道关于此类型的题,我们先看 Pass-14(图片马字节)这道

首先看一下题目给出的判断文件类型的源码:

function getReailFileType($filename){
// 为移植性考虑,强烈建议在用 fopen() 打开文件时总是使用 ‘b’ 标记。
$file = fopen($filename, “rb”);
// 只读2字节,fread单位为字节
$bin = fread($file, 2);
fclose($file);
// unpack() 函数从二进制字符串对数据进行解包
// 前面的参数表示在解包数据时所使用的格式
$strInfo = @unpack(“C2chars”, $bin);
// intval() 函数用于获取变量的整数值
$typeCode = intval($strInfo[‘chars1’].$strInfo[‘chars2’]);
$fileType = ”;
switch($typeCode){
case 255216:
$fileType = ‘jpg’;
break;
case 13780:
$fileType = ‘png’;
break;
case 7173:
$fileType = ‘gif’;
break;
default:
$fileType = ‘unknown’;
}
return $fileType;
}

其中最关键的读取代码为

$bin = fread($file, 2); //只读2字节
fclose($file);

根据源码知道只对文件的头2个字节做检测

这里就需要制作木马图片,然后上传即可

copy 1.jpg /b + 1l.php /a 1.jpg

/be为二进制,表示以二进制格式合并1.jpg和1l.php

也可以用winhex、010editor等工具在图片添加上图片的文件头

而Pass-15(图片马getimagesize)这题添加了getimagesize()函数,getimagesize()对目标文件的16进制去进行一个读取,可以伪造假图片,上传同14

Pass-16(图片马php_exif)

$image_type = exif_imagetype($filename);

exif_imagetype()函数是PHP中的内置函数,用于确定图像的类型(读取一个图像的第一个字节并检查其签名。如果发现了恰当的签名则返回一个对应的常量,否则返回 FALSE。)

上传同14

常见的头部对应关系比如(可以多总结一下)

.JPEG;.JPE;.JPG,”JPGGraphic File”

.gif,”GIF 89A”

.zip,”Zip Compressed”

.doc;.xls;.xlt;.ppt;.apr,”MS Compound Document v1 or Lotus Approach APRfile”

 

0x04 检测文件名进行过滤

黑名单绕过

黑名单校验就很不安全,很多网站会采用黑名单过滤的方法,但是又很容易就被绕过,例如用一些常见的扩展名就可以轻而易举绕过。

例如Pass-03(黑名单验证)这道题,首先分析源码

$is_upload = false;
$msg = null;
// isset() 用于检测变量是否已设置并且非NULL
// 判断是否存在通过POST方式提交过来的变量
if (isset($_POST[‘submit’])) {
// file_exists() 检查文件或目录是否存在
if (file_exists($UPLOAD_ADDR)) {
$deny_ext = array(‘.asp’,’.aspx’,’.php’,’.jsp’);
// trim() 函数移除字符串两侧的空白字符或其他预定义字符
// 空格、制表符 tab、换行符等等
$file_name = trim($_FILES[‘upload_file’][‘name’]);
// 删除文件名末尾的点
$file_name = deldot($file_name);
// 搜索 “.” 在字符串中的位置并返回从该位置到字符串结尾的所有字符
$file_ext = strrchr($file_name, ‘.’);
// 转换为小写
$file_ext = strtolower($file_ext);
// 去除字符串::$DATA
$file_ext = str_ireplace(‘::$DATA’, ”, $file_ext);
// 收尾去空
$file_ext = trim($file_ext);

该代码是对上传后的文件后缀进行检测,我们可以上传不在黑名单的文件后缀,.php3可以被解析成.php,前提是Apache的httpd.conf中配置有如下代码:

AddType application/x-httpd-php .php .php3 .phtml

常见的可执行文件的后缀:

  1. PHP: php2、php3、php5、phtml、pht
  2. ASP: aspx、ascx、ashx、cer、asa
  3. JSP: jspx

换后缀名上传成功

当然,针对这道题有一种非常巧妙的解题方法,也适用于一些其他道题,仔细分析中间那段代码

上传XXX.php. .

先将文件名最后的点删掉,再通过 strrchr() 函数返回最右边“.”的后面饿字符

strrchr() 函数查找字符串在另一个字符串中最后一次出现的位置,并返回从该位置到字符串结尾的所有字符

这样上传的文件名就为 XXX.php. 因为在存储时会默认删掉这个点,也就可以上传成功

这里有个Tips:Apache的解析顺序是从右到左开始解析文件后缀的,如果最右侧扩展名不可识别,就继续往左判断。直到遇到可以解析的文件后缀为止

利用上面的方法可以解一下这道题:Pass-05

$deny_ext = array(“.php”,”.php5″,”.php4″,…”.htaccess”);
$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); //首尾去空

XXX.php. .

相同原理,听说为新加题目,查看几年前笔记确实没有此题

大小写绕过

根据Pass-06(大小写过滤)这个题,我们清楚看到是函数并没有对大小写进行检查,即使是列了黑名单,也会因为这个问题而绕过

直接分析源码

// 缩写为减少文章篇幅,实际看 Pass-04
$deny_ext = array(“.php”,”.php5″,”.php4″,…”.htaccess”);
$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); //首尾去空

通过分析 Pass-04 我们一定记着但是当时有一句

$file_ext = strtolower($file_ext); //转换为小写

既然不再转换大小写,那就直接大小写绕过

白名单绕过

白名单绕过虽然相比于黑名单更加安全,但是还是会存在其他问题,在upload-labs master中,给出一个%00截断与0x00阶段的题目

Pass-12(%00截断)

$ext_arr = array(‘jpg’,’png’,’gif’);
// substr — 返回字符串的子串
// strrpos – 查找在字符串中最后一次出现的位置
$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’];
$img_path = $_GET[‘save_path’].”/”.rand(10, 99).date(“YmdHis”).”.”.$file_ext;

当网站上传XXX.php%00.jpg时,通过白名单绕过,保存文件时,遇到%00字符就会截断后面的.jpg,文件最终保存为XXX.php

Pass-13(0x00截断)

查看源代码发现 此题在上题将GET换为POST,利用Pass-11的方法 ,但在url解码中%00不会被解析,但是我们可以使用0x00进行截断

空字符绕过

参考:Pass-07

分析源码

$deny_ext = array(“.php”,”.php5″,”.php4″,…”.htaccess”);
$file_name = $_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

先删掉最后的“.”,再返回“.”后面的字符,之后是大小写处理,直接上传“.php ”(php后面有个空格)即可绕过

deldot()绕过

参考:Pass-08(deldot())

分析源码

$deny_ext = array(“.php”,”.php5″,”.php4″,…”.htaccess”);
$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); //首尾去空

先去空,然后返回“.”后面的字符,之后转换大小写,这里也有一个逻辑错误,直接返回最后一个点之后的消息,那直接后缀名改为“.php.”即可绕过

流特性绕过

参考:Pass-09

还是分析源码

$deny_ext = array(“.php”,”.php5″,”.php4″,…”.htaccess”);
$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);
//去除字符串::$DATA

这个关于windows下文件的流特性

::$DATA 会把 之后的数据当成文件流处理,不会检测后缀名,保持“::$DATA”之前的文件名

双写绕过

参考:Pass-11

$file_name = trim($_FILES[‘upload_file’][‘name’]);
$file_name = str_ireplace($deny_ext,””, $file_name);
$temp_file = $_FILES[‘upload_file’][‘tmp_name’];
$img_path = UPLOAD_PATH.’/’.$file_name;

str_ireplace() 函数替换字符串中的一些字符(不区分大小写)

既然替换一次,可采用双写绕过

XXX.pphphp

注意⚠️:XXX.phphpp这样就是不行的,因为他会去掉从前面来说的第一个php,去掉后会成为XXX.hpp

除这些之外upload labs还有一道Pass-10(过滤)

直接分析源码

$deny_ext = array(“.php”,”.php5″,”.php4″,…”.htaccess”);
$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); //首尾去空

采用抓包添加后缀 .php. .

 

0x05 检测文件内容进行过滤

例如 Pass-04这道题,这道题除了可以采用 Pass-03 中的一种读源码的方法,更主要的是学习使用 .htaccess文件

分析源码,是在基础上添加了更多的后缀过滤

$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); //收尾去空

htaccess文件能够更改服务器的设置,全称为Hypertext Access,称为超文本入口,提供了针对目录改变配置的方法,可在一个特定的文档目录中放置一个包含一个或多个指令的文件。

如果想详看可以参考或自行搜索:https://www.cnblogs.com/adforce/archive/2012/11/23/2784664.html

对于这道题,我们可以上传一个.htaccess 文件重写服务器的文件解析

开启htaccess的方法:

打开apache的httpd.conf设置文件

查找到的AllowOverride None,将其改为AllowOverride All

创建一个.htaccess文件

内容写成:

AddType application/x-httpd-php .jpg,可将jpg文件解析为php文件

SetHandler application/x-httpd-php,可将其他所有文件解析为php文件

之后上传文件就可以了

二次渲染绕过

二次渲染指的是提取了文件中的图片数据,然后再对图片重新渲染,这样加在其中的恶意代码就不见了

例如Pass-17(二次渲染绕过)这个题

采用的二次渲染技术,就是根据用户上传的图片,新生成一个图片,将原始图片删除,将新图片添加到数据库中。

关于这道题,lmn写多少都不如这篇文章详细,所以干脆不写了,可以参考参考

https://xz.aliyun.com/t/2657#toc-13

处这两个以外也可以学习一下文件包含漏洞,例如,如果网站对jsp,php等文件内容进行校验,可以通过上传(无限制)的文件例如.txt文件,并通过例如php语句进行文件包含,从而达到目的

常见的文件包含例如:

  1. include()
  2. require()
  3. include_once()
  4. require_once()

include(),只生成警告(E_WARNING),并且脚本会继续

require(),会生成致命错误(E_COMPILE_ERROR)并停止脚本

include_once()与require_once(),如果文件已包含,则不会包含,其他特性如上

upload其他优秀题目

Pass-18 19(条件竞争)

竞争条件:多个线程或者进程在读写一个共享数据时结果依赖于它们执行的相对时间的情形

先将文件上传到服务器,然后判断文件后缀是否在白名单里,如果在则重命名,否则删除

这里的原理就是上传info.php,边上传边访问,保证在上传之前访问到,利用burp的intruder模块不断上传

这里需要借助一段代码

<?php fputs(fopen(‘info.php’,’w’),'<?php phpinfo();?>’);?>

因为在没有文件时,w写参数会自动创建文件,并将后面的字放到前面的文件中,所以用burp的intruder连续访问,因为是多个进程或者线程在读写数据,其最终的的结果依赖于多个进程的指令执行顺序,也就是可能在被删除前访问到文件

Pass-20(/.绕过)

查看源码

save_name为页面上我们提交的文件名,因此抓包修改为upload-19.php/.

upload-19是因为上传处默认文件名就是这个

 

06 文件上传的危害

文件上传漏洞很容易带来严重的安全问题,可以利用文件上传漏洞上传Webshell

webshell,web指的是在web服务器上,shell是用脚本语言编写的脚本程序,通常黑会自己编写webshell,并上传到目标web服务器的页面的目录下,然后通过目标系统进行入侵

根据不同的分类可以分为很多类,比如根据功能可以分为大马和小马,小马通常指一句话木马,例如将<%eval request(“pass”)%>这句话写入一个后缀为.asp的文件中,然后传到服务器上面。eval方法将request(“pass”)转换成代码执行,request函数的作用是应用外部文件。

根据脚本的类型可以分为jsp、asp、aspx、php等

现在对于不同的web服务器系统对应的有不同的web服务端程序,windows端主流的有iis,linux端主流的有Nginx。这些服务对服务器会带来一些隐患,这些服务器上都存在一些漏洞,很容易被黑客利用

利用上面提到的文件上传绕过方法,可以将恶意文件传入服务器中

文件上传绕waf

通过绕过waf达到防止恶意文件被拦截的效果,这里给出几种大家屡试不爽的方法

  1. 有时候可以通过之前给出的替换不常见的但可达到相同解析效果的文件名
  2. 制造一些“垃圾数据”,可以降低waf的检测
  3. 例如早期的安全狗就可以通过多加几个filename绕过
  4. (看到有人说有些waf会检测是否为POST,如果为POST则会校验数据包内容,这里更改POST为GET)
  5. 删除Conten-Type字段
  6. 删除Content-Disposition字段里的空格
  7. 修改Content-Disposition字段值的大小写
  8. (文件名处回车)

参考文章:

https://blog.csdn.net/skynet_x/article/details/109285482?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~aggregatepage~first_rank_ecpm_v1~rank_v31_ecpm-10-109285482.pc_agg_new_rank&utm_term=upload-labs-master&spm=1000.2123.3001.4430

https://blog.csdn.net/qq_42357070/article/details/82881393?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164543284216780255288719%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=164543284216780255288719&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-82881393.pc_search_insert_ulrmf&utm_term=webshell&spm=1018.2226.3001.4187

https://blog.csdn.net/qq_42181428/article/details/87090539

(完)