**前言:**前面已经了解了文件上传的基础,接下来详细的进行学习,首先学习后端黑白名单绕过

涉及知识

文件上传常见验证

文件上传的常见验证方式有:后缀名(直接)、类型(间接)、文件头(间接)等

后缀名验证

黑名单,白名单

后缀名黑名单

明确不允许上传的格式后缀,其他文件名后缀允许

如:asp php jsp aspx cgi war 等

绕过:如果php phtml..没有定义到后名单里,可以用这后缀名绕过限制,依旧可以达到上传效果

后缀名白名单

明确可以上传的格式后缀,其他文件名后缀不允许

如:jpg png zip rar gif 等

使用白名单验证要更安全

文件类型验证:MIME 信息

MIME

MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式

判断机制

Content-Type称之为MIME信息,一些网站是通过这个值来判断文件是何种格式。

当上传一个文件到网站时,该文件会自带一个信息,此信息和使用何种脚本没有关系。

利用

上传脚本文件时,如果网站使用MIME信息栏判断文件类型,就可以修改Content-Type信息来伪造,让对方误认为我们上传的是一个合法文件。

因此单独使用MIME信息判断文件类型的方法是不严谨的,有空子可钻。

抓包介绍MIME信息

在文件上传时抓包,通过数据包介绍MIME信息:

image-20220325185109198

查看数据包详细

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /PenTest/uploadlabs/Pass-01/index.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------352318732238310643913205352540
Content-Length: 202979
Origin: http://localhost
Connection: close
Referer: http://localhost/PenTest/uploadlabs/Pass-01/index.php
Upgrade-Insecure-Requests: 1

-----------------------------352318732238310643913205352540
//以下就是MIME信息
Content-Disposition: form-data; name="upload_file"; filename="WEB漏洞-文件上传导图.png"
Content-Type: image/png

‰PNG


上述数据包中的

1
2
Content-Disposition: form-data; name="upload_file"; filename="WEB漏洞-文件上传导图.png"
Content-Type: image/png //文件类型

就是文件的MIME信息

文件头验证:内容头信息

文件头

文件头是直接位于文件中的一段数据,是文件的一部分

判定机制

通过文件头部信息,判断文件的格式

利用

上传脚本文件时,如果网站使用文件头判断文件类型,就可以修改数据包中文件头的值,伪造文件头,让对方服务器判定我们上传的文件是合法文件。

因此单独使用文件头验证是不严谨的。

不同文件的文件头查看举例

PNG文件头

image-20220325185134711

GIF文件头

image-20220325185152789

PHP文件上传相关代码

创建一个文件上传HTML表单

允许用户从表单上传文件是非常有用的。

请看下面这个供上传文件的 HTML 表单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
</head>
<body>

<form action="upload_file.php" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>

</body>
</html>

将以上代码保存到 form.html 文件中。

image-20220325185214585

有关上面的 HTML 表单的一些注意项列举如下:

  • <form> 标签的 enctype 属性规定了在提交表单时要使用哪种内容类型。在表单需要二进制数据时,比如文件内容,请使用 “multipart/form-data“。
  • <input> 标签的 type=”file” 属性规定了应该把输入作为文件来处理。举例来说,当在浏览器中预览时,会看到输入框旁边有一个浏览按钮。

**注释:**允许用户上传文件是一个巨大的安全风险。请仅仅允许可信的用户执行文件上传操作。

创建PHP脚本接收上传表单数据

upload_file.php文件含有供上传文件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if ($_FILES["file"]["error"] > 0)
{
echo "错误:" . $_FILES["file"]["error"] . "<br>";
}
else
{
echo "上传文件名: " . $_FILES["file"]["name"] . "<br>";
echo "文件类型: " . $_FILES["file"]["type"] . "<br>";
echo "文件大小: " . ($_FILES["file"]["size"] / 1024) . " kB<br>";
echo "文件临时存储的位置: " . $_FILES["file"]["tmp_name"];
}
?>

通过使用 PHP 的全局数组 $_FILES,你可以从客户计算机向远程服务器上传文件。

当客户端提交后,我们获得了一个$_FILES 数组,$_FILES数组内容如下:

第一个下标是表单的 input标签的name属性的值,比如上述HTML代码中<input type="file" name="file" id="file">,其name属性值就是“file”;

第二个下标可以是 “name”、”type”、”size”、”tmp_name” 或 “error”。如下所示:

  • $_FILES["file"]["name"] - 上传文件的名称
  • $_FILES["file"]["type"] - 上传文件的类型
  • $_FILES["file"]["size"] - 上传文件的大小,以字节计
  • $_FILES["file"]["tmp_name"] - 存储在服务器的文件的临时副本的名称
  • $_FILES["file"]["error"] - 由文件上传导致的错误代码

这是一种非常简单文件上传方式。基于安全方面的考虑,应当增加有关允许哪些用户上传文件的限制。

PHP脚本增加上传文件限制

在这个脚本中,我们增加了对文件上传的限制。用户只能上传 .gif、.jpeg、.jpg、.png 文件,文件大小必须小于 200 kB:

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
28
29
30
31
<?php
// 允许上传的图片后缀
$allowedExts = array("gif", "jpeg", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp); // 获取文件后缀名
if ((($_FILES["file"]["type"] == "image/gif") //允许文件类型
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/jpg")
|| ($_FILES["file"]["type"] == "image/pjpeg")
|| ($_FILES["file"]["type"] == "image/x-png")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 204800) // 文件小于 200 kb
&& in_array($extension, $allowedExts))
{
if ($_FILES["file"]["error"] > 0)
{
echo "错误:: " . $_FILES["file"]["error"] . "<br>";
}
else
{
echo "上传文件名: " . $_FILES["file"]["name"] . "<br>";
echo "文件类型: " . $_FILES["file"]["type"] . "<br>";
echo "文件大小: " . ($_FILES["file"]["size"] / 1024) . " kB<br>";
echo "文件临时存储的位置: " . $_FILES["file"]["tmp_name"];
}
}
else
{
echo "非法的文件格式";
}
?>

PHP脚本保存被上传的文件

上面的实例在服务器的 PHP 临时文件夹中创建了一个被上传文件的临时副本。

这个临时的副本文件会在脚本结束时消失。要保存被上传的文件,我们需要把它拷贝到另外的位置:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
// 允许上传的图片后缀
$allowedExts = array("gif", "jpeg", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
echo $_FILES["file"]["size"];
$extension = end($temp); // 获取文件后缀名
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/jpg")
|| ($_FILES["file"]["type"] == "image/pjpeg")
|| ($_FILES["file"]["type"] == "image/x-png")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 204800) // 小于 200 kb
&& in_array($extension, $allowedExts))
{
if ($_FILES["file"]["error"] > 0)
{
echo "错误:: " . $_FILES["file"]["error"] . "<br>";
}
else
{
echo "上传文件名: " . $_FILES["file"]["name"] . "<br>";
echo "文件类型: " . $_FILES["file"]["type"] . "<br>";
echo "文件大小: " . ($_FILES["file"]["size"] / 1024) . " kB<br>";
echo "文件临时存储的位置: " . $_FILES["file"]["tmp_name"] . "<br>";

// 判断当前目录下的 upload 目录是否存在该文件
// 如果没有 upload 目录,你需要创建它,upload 目录权限为 777
if (file_exists("upload/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " 文件已经存在。 ";
}
else
{
// 如果 upload 目录不存在该文件则将文件上传到 upload 目录下
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $_FILES["file"]["name"]);
echo "文件存储在: " . "upload/" . $_FILES["file"]["name"];
}
}
}
else
{
echo "非法的文件格式";
}
?>

上面的脚本检测了文件是否已存在,如果不存在,则把文件拷贝到名为 “upload” 的目录下。

文件上传演示操作如下所示:

演示案例——upload-labs 关卡分析

使用upload-labs中的查看源码功能来分析各种文件上传时文件类型的判定机制

服务器端代码使用PHP搭建,上传后门尽量使用PHP后门,其他类型脚本未必能够解析。

注意:PHP是弱类型语言

  • PHP不必声明该变量的数据类型。

  • PHP 会根据变量的值,自动把变量转换为正确的数据类型。

  • 在强类型的编程语言中,我们必须在使用变量前先声明(定义)变量的类型和名称。

黑名单绕过

Pass-2(MIME类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) { //判断是否触发提交事件,即是否点击了提交按钮
if (file_exists(UPLOAD_PATH)) { //判断这个路径是否存在
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) { //判断文件类型是jpeg、png、gif三者之一
$temp_file = $_FILES['upload_file']['tmp_name']; //获取文件名
$img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name'] //定义img_path,其中.表示字符串连接
if (move_uploaded_file($temp_file, $img_path)) { //移动文件函数:将上传的文件移动到新位置,成功时返回true
$is_upload = true; //文件上传成功,则将$is_upload置为true
} else {
$msg = '上传出错!'; //文件上传失败,则将$msg置为'上传出错!'
}
} else {
$msg = '文件类型不正确,请重新上传!'; //文件类型错误提示
}
} else {
$msg = UPLOAD_PATH.'文件夹不存在,请手工创建!'; //文件上传目的路径不存在提示
}
}

可见此关卡判断文件类型的依据是MIME信息,可以通过修改Content-Type值来绕过,实现脚本上传

直接上传.php文件会报错

image-20220325185245565

抓包后修改Content-Type值为合法类型image/gif

image-20220325185313747

修改后再发送可见上传成功

image-20220325185335049

image-20220325185352332

Pass-3(特殊解析后缀——.php5等后缀解析成php)

apache服务器能够使用php解析.phtml .php3后缀

黑名单限制php上传,但apache对php3.php5.phtml等也可以直接解析为php,故可将php后缀改为php3等再上传,如果Apache不能解析,在Apache的配置文件httpd.conf文件中添语句php3即可

开关:更改Apache的 httpd.conf 配置文件,添加.php3 和 phtml,取消前面注释,保存并退出即可实现php解析.phtml .php3,大约在第403行,即下图所示行中添加需要解析的类型格式

image-20220325185410551

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
28
29
30
$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']);//trim()移除字符串两侧的空白字符或其他预定义字符。 功能除去字符串开头和末尾的空格或其他字符
//deldot()函数,是作者自定义的函数,作用是删除文件名末尾的点,它在靶场文件夹下面的common.php文件中
//deldot()函数从后向前检测,当检测到末尾的第一个点时会继续它的检测,但是遇到空格会停下来
$file_name = deldot($file_name);//deldot():删除文件名末尾的点,为了防止多后缀
$file_ext = strrchr($file_name, '.');//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 . '文件夹不存在,请手工创建!';
}
}

deldot()函数,是作者自定义的函数,作用是删除文件名末尾的点,它在靶场文件夹下面的common.php文件中,代码如下所示

函数功能:若文件名为a.jpg.......返回结果为a.jpg,即删除文件名末尾的所有.(如果有的话)

详细见http://localhost/deldot.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function deldot($s){
for($i = strlen($s)-1;$i>0;$i--){ //倒序遍历字符串$s,$i是字符串下标,从后到前
$c = substr($s,$i,1); //$c取字符串$i位置后的一个字符
if($i == strlen($s)-1 and $c != '.'){ //判断$i位置后的一个字符是否为.
return $s; //$i位置后的一个字符不是.,则返回字符串
}

if($c != '.'){
return substr($s,0,$i+1);
}
}
}
?>

本关卡使用黑名单限制php上传,但apache对php3.php5.phtml等也可以直接解析为php,故可将php后缀改为php3等,如果Apache不能解析,在Apache的配置文件httpd.conf文件中添语句php3即可。

这个开关一般是开启的,如果关闭,则不能采用这种方法绕过。

Pass-4(.htaccess解析)

仅存在于Apache中

.htaccess文件(或者”分布式配置文件”),全称是Hypertext Access(超文本入口)。提供了针对目录改变配置的方法,即在一个特定的文档目录中放置一个包含一个或多个指令的文件, 以作用于此目录及其所有子目录。作为用户,所能使用的命令受到限制

概述来说,htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过htaccess文件,可以帮我们实现:网页301重定向、自定义404错误页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能。

下面通过htaccess来实现上传文件后缀自定义

创建.htaccess文件

1
2
3
<FilesMatch "cimer">
SetHandler application/x-httpd-php
</FilesMatch>

上述代码含义是:如果文件名中匹配到出现cimer字符串,就将其当做application/x-httpd-php(即Content-Type值)文件进行解析

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
28
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".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",".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文件,上传成功:

image-20220325185437368

则接下来文件名中含有cimer的文件都会被当做php文件来解析,上传cimer.jpg文件

image-20220325185453463

访问上传文件的目录http://localhost/PenTest/uploadlabs/upload/cimer.jpg

可见cimer.jpg文件被当做PHP文件来解析了

其他文件名中不含cimer的图片文件不会被当做PHP文件来解析,因为不满足.htaccess中的要求。

image-20220325185511357

添加在图片文件末尾的PHP代码也成功执行了

Pass-6(大小写绕过)

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
$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 . '文件夹不存在,请手工创建!';
}
}

上述代码中,尽管对于黑名单中的文件名后缀做了扩充,但是却在提取文件后缀名后没有进行强制小写转换,因此可以通过文件名后缀大小写的变化绕过黑名单

上传一个.PHP的文件,可以成功上传

image-20220325185613521

Pass-7(空格绕过)

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
$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 = $_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

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 . '文件夹不存在,请手工创建!';
}
}

相比前一关,本关卡对提取出的文件名后缀会进行强制小写转换,但是对上传文件的后缀名没有进行首尾去空格的操作,因此可以上传一个.php +的文件(+表示前面是一个空格,实际不写)。

经过处理后的文件名后缀仍然可能存在空格,而黑名单中的文件名都是没有空格的,因此可以借此绕过黑名单。

注意:Windows中如果文件名最后存在空格,系统会默认删除,因此我们上传时先使用burp抓包,在文件名后添加空格,这样才能成功绕过。上传到Windows服务器保存后,又会自动删除文件名最后的空格,Linux服务器也会删除文件名最后的空格。

上传一个.php的文件,burp抓包,文件名添加空格

image-20220325185627214

上传成功

image-20220325185643070

Pass-8(点绕过)

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
$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 . '文件夹不存在,请手工创建!';
}
}

本关卡对文件名后缀处理时,缺少删除文件末尾的点的操作,因此在上传一个文件名末尾是·的文件就能绕过黑名单。

注意:Windows中如果文件名最后存在.,系统不能创建,因此我们上传时先使用burp抓包,在文件名后添加.,这样才能成功绕过。上传到Windows服务器保存后,又会自动删除文件名最后的.,Linux服务器会保留.。

上传一个.php的文件,burp抓包,文件名添加.

image-20220325185702605

上传成功

image-20220325185717308

Pass-9(::$DATA绕过)

必须是Windows下的PHP有如下特性:

在window的时候如果文件名+”::$DATA”会把::$DATA之后的数据当成文件流处理,不会检测后缀名,且保持::$DATA之前的文件名,他的目的就是不检查后缀名

例如:上传时的文件名为”phpinfo.php::$DATA”

​ Windows会自动去掉末尾的::$DATA变成”phpinfo.php”

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
$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 = 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 . '文件夹不存在,请手工创建!';
}
}

查看源码分析,发现少了去除::$DATA的操作,因此在上传一个文件名末尾是::$DATA的文件就能绕过黑名单。

注意:Windows中文件名最后不允许特殊符号存在,系统不能创建,因此我们上传时先使用burp抓包,在文件名后添加::$DATA,这样才能成功绕过。上传到Windows服务器保存后,又会自动删除文件名最后的::$DATA。

上传一个.php的文件,burp抓包,文件名添加::$DATA

image-20220325185755252

上传成功

image-20220325185823205

Pass-10(点空格绕过)

一次过滤和循环过滤

代码将字符串里的php替换为空

一次过滤

a.php –> a.

a.pphphp –> a.php 将中间的php过滤掉,还有个php

循环过滤 递归过滤

a.pphphp –> a. 只要有php出现就会一直过滤掉

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
28
$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 . '文件夹不存在,请手工创建!';
}
}

上述代码先去掉文件名末尾的.,然后首尾去空格,最后将处理后的文件名和黑名单进行对比。

而且只使用了一次过滤,而非循环过滤。

因此在上传文件时

  • 在文件名后加上.绕过黑名单
  • 加上空格绕过首尾去空格
  • 再加上.绕过删除文件末尾的点

上传文件之后处理过程就如下所示:

  • 上传文件名:a.php. .

  • 删除文件名末尾的点:a.php. +

  • 首尾去空:a.php.,.php.能够绕过黑名单过滤

  • 上传成功:Windows服务器保存后,又会自动删除文件名最后的.得到a.php

上传一个.php的文件,burp抓包,文件名添加. .

image-20220325185839748

上传成功

image-20220325185849832

Pass-11(双后缀名绕过)

str_replace() 和str_ireplace()函数

  • str_replace() 函数以其他字符替换字符串中的一些字符(区分大小写)。

  • str_ireplace()函数执行不区分大小写的搜索。

把字符串 “Hello world!” 中的字符 “world” 替换为 “Shanghai”:

1
2
3
<?php
echo str_replace("world","Shanghai","Hello world!");
?>

运行结果Hello Shanghai!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$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","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess","ini");

$file_name = trim($_FILES['upload_file']['name']);//文件名首尾去空
//str_ireplace() 函数替换字符串中的一些字符(不区分大小写),此处即把文件名中若出现在黑名单中的子串,就替换为空
$file_name = str_ireplace($deny_ext,"", $file_name);
$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 = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

本关卡上传脚本文件时,会将后缀名替换为空,之后再保存在服务器

尽管能够成功上传,但是到服务器之后没有后缀名,脚本无法被解析

image-20220325185903861

但是代码中只使用了一次替换为空而不是循环替换,即只将首次出现且在黑名单中的文件名后缀形式替换为空,因此可以使用双写后缀名进行绕过

a.pphphp—>a.php中间的php会被替换成空

上传一个.pphphp文件,上传成功

image-20220325185917830

白名单绕过

Pass-12(%00截断——GET)

%00截断原理

URL的%00截断本质上是ASCII码的中断值0截断

截断的核心,就是chr(0)这个字符,这个字符不为空(Null),也不是空字符(“”),更不是空格。

当程序在输出含有chr(0)变量时,chr(0)后面的数据会被停止,换句话说,就是误把它当成结束符,后面的数据直接忽略,这就导致漏洞产生

下面是用 URL 编码形式表示的 ASCII 字符

URL-encode ASCII Value
%00 0

即在url中%00表示ascll码中的0 ,而ascii中0作为特殊字符保留,表示字符串结束,所以当url中出现%00时就会认为读取已结束

比如:

0x开头表示16进制,0在十六进制中是00, 0x00就是%00解码成的16进制

image-20220325185933283

这里在php的后面添加了一个空格和字母a,其实a写不写都可以,这里加a是为了显示空格的位置。

空格是为了占位,方便修改00。

然后打开hex,(空格的16进制为0x20)修改16进制内容,把20改成00:

image-20220325185956482

修改后

image-20220325190009414

%00和0x00的区别
  • %00是URL中的,即%00是URL编码

  • 0x00是hex格式下的文件命名

%00截断的条件
  1. %00需要PHP版本小于5.3.4

  2. 打开php的配置文件php-ini,将magic_quotes_gpc设置为Off

image-20220325190027893

绕过操作

phpstudy调整PHP版本为5.2.17,关闭magic_quotes_gpc开关。

分析代码,发现是白名单限制,将文件名最后一个点后面的格式与白名单进行对比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$is_upload = false;
$msg = null;
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'];
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}

代码的关键一句在于 $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;,这句代码确定了上传文件1.jpg在服务器上被更改名称为$_GET['save_path']/随机值.jpg

通过抓包可以看到数据包提交的GET参数save_path的值为../upload/,也就是说最终保存到服务器的文件名称就由1.jpg变成了../upload//随机值.jpg

image-20220325190044040

因此设法使得$img_path的值出现00截断,就能成功上传所需的脚本文件。

直接在raw格式数据包中修改文件名添加%00截断

我们设想,在数据包中将图片文件命名为1.php%00.jpg,则.jpg后缀符合白名单规定,预测其可以上传,当 $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;拼接新的文件名后,move_uploaded_file($temp_file,$img_path)执行移动文件操作,应该能在upload目录下得到预期的1.php的脚本文件。也即

  1. .jpg后缀符合白名单可以上传
  2. 拼接最终文件名时%00截断生效
  3. 上传目录中得到1.php的脚本文件

按照上面的思路进行测试,上传一个图片马(PHP脚本写在文件最末),发现上传到服务器的文件仍然是名为随机值的图片文件。

抓包修改文件名添加%00截断

image-20220325190058804

上传结果仍然是名为随机值的图片文件,而不是预期的PHP文件。

image-20220325190111539

出错原因

==猜测原因==:raw格式数据包并非URL编码(GET提交参数那里里是URL编码,添加%00后可以被自动解码,解码之后得到00截断,参考下面在save_path的值中添加%00截断的方法),在raw格式数据包中添加%00(URL编码),这个%00并不会被解码为中断值0。

那么上传的文件名就经历如下过程:

  • 上传图片马,文件名为1.jpg
  • raw格式下文件名添加%00截断 1.php%00.jpg
  • 无URL解码$_FILES[‘upload_file’][‘name’]得到的文件名并不会截断即为1.php%00.jpg
  • strrpos($_FILES[‘upload_file’][‘name’],”.”)得到的文件名后缀为jpg
  • 拼接得到最终文件名为$img_path=../upload//随机值.jpg

因此不能直接在raw格式的数据包下的上传文件名中添加%00,而要在hex格式的数据包下的上传文件名中添加00,即在十六进制的数据包中添加00截断

具体做法是,先在raw格式的数据包的上传文件名中添加空格,打开hex格式查看,在上传文件名的那一行寻找20,因为20是空格的十六进制码,把这个20改为00,则上传以后会出现截断。

在hex格式数据包中修改文件名添加00截断

在十六进制的数据包中添加00截断,则预期.jpg能够符合白名单上传,并且在拼接最终文件名时00截断生效

按照上面的思路进行测试,上传一个图片马(PHP脚本写在文件最末),发现无法成功上传文件

抓包在hex格式下修改文件名添加00截断

image-20220325190129056

返回错误,文件名不符合白名单要求,上传失败。

错误原因

在多数情况下,截断绕过都是用在文件名后添加hex形式的%00(如上所示操作)来测试,例如filename='1.php%00.jpg',但是由于在PHP中,$_FILES['upload_file']['name']在得到文件名时,%00之后的内容已经被截断了,所以$_FILES['upload_file']['name']中得到的文件名后缀是php,而不是jpg,因而此时不能通过if(in_array($file_ext,$ext_arr))的检查。

又因为GET方法提交的参数save_path的值也会参与最终文件名的拼接,所以应该在save_path的值中添加%00截断。

在save_path的值中添加%00截断

我们又修改数据包中save_path的值为../upload/webshell.php%00(GET提交参数这里是URL编码,添加%00后可以被自动解码,解码之后得到00截断)。即在保存路径中添加截断上传

抓包修改save_path的值添加%00截断

image-20220325190152869

成功上传名为pass12.php的脚本文件

image-20220325190214363

Pass-13(%00截断——POST)

分析代码,仍然是白名单限制,但是save_path不是由GET方法提交,而是POST方法提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$is_upload = false;
$msg = null;
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'];
$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传失败";
}
} else {
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}

绕过操作

需要注意的是

  • GET提交参数那里里是URL编码,添加%00后可以被自动解码,解码之后得到00截断

  • POST提交参数如果添加%00这种URL编码,并不会被自动解码,也就不会得到00截断

基于以上的注意事项,在修改POST方法提交的save_path参数并添加%00截断时,由于%00是中断值0的URL编码,因此

  1. 修改POST方法提交的save_path参数并添加%00

image-20220325190249301

  1. 右键解码%00,在burp中显示为空格(或可直接在hex格式下的数据包中进行改包添加00截断)

image-20220325190305356

修改数据包之后提交

image-20220325190320162

可见如Pass-12一样,成功利用截断上传了一个名为pass13.php的脚本文件

image-20220325190332273

涉及资源

无

站内搜索

没有找到内容!