upload-labs通关笔记-第21关 文件上传之数组绕过
目录
一、数组参数
二、PHP函数
1、empty函数
2、三元表达式
3、is_array函数
4、explode函数
5、end函数
6、reset函数
7、count函数
三、代码审计
1、MIME检查绕过
2、文件名不是数组需要设为数组
3、end函数获取文件名后缀
4、文件名处理
四、渗透实战
1、制作脚本test21.php
2、PHP版本切换
3、浏览图片
3、bp开启拦截
4、点击上传
5、bp拦截
6、修改报文
(1)修改MIME
(2)save_name文件名修改
7、发包并获取脚本地址
8、访问脚本
本文通过《upload-labs靶场通关笔记系列》来进行upload-labs靶场的渗透实战,本文讲解upload-labs靶场第21关数组绕过渗透实战。
一、数组参数
在 PHP 里,数组是一种强大且常用的数据类型数组常被用作参数传递。下面从多个方面为你详细介绍 PHP 数组参数的相关内容。cooper.php内容如下所示,那么执行效果。
<?php
$save_name = $_POST['save_name'];
var_dump($save_name);
?>
这段代码的主要功能是从 POST 请求中获取名为 save_name
的表单数据,并使用 var_dump
函数输出该数据的类型和值。
二、PHP函数
1、empty函数
在 PHP 里,empty
函数用于检查一个变量是否为空。此函数可以判断多种数据类型的变量是否为空,在实际开发中,常用于验证表单数据、检查变量是否被赋值等场景。
empty(mixed $var): bool
(1)参数说明
$var
:该参数是要检查的变量,它可以是任意数据类型,像字符串、整数、数组、对象等
(2)返回值
若变量为空,empty
函数返回 true
;若变量不为空,返回 false
。
2、三元表达式
三元运算符的基本结构为 条件? 表达式1 : 表达式2。其工作逻辑是:先对 条件 进行判断,如果 条件 为真,则返回 表达式1 的值;如果 条件 为假,则返回 表达式2 的值。
$file = empty($_POST['save_name'])? $_FILES['upload_file']['name'] : $_POST['save_name'];
在这行代码里,条件是 empty($_POST['save_name'])。empty 函数用于检查一个变量是否为空。
这段代码的含义是,如果 $_POST['save_name'] 为空,也就是用户在表单中没有填写 save_name 字段或者填写的值为空,那么 $file 会被赋值为上传文件的原始文件名;如果 $_POST['save_name'] 不为空,那么 $file 会被赋值为 $_POST['save_name'] 的值。
3、is_array函数
is_array是 PHP 中的一个内置函数,其主要功能是判断一个变量是否为数组类型。在编写 PHP 代码时,经常需要根据变量的类型来执行不同的操作,is_array函数可以帮助开发者快速、准确地判断变量是否为数组,从而避免因类型错误导致的程序异常。
is_array ( mixed $value ) : bool
- 参数
$value
:该参数为必需项,代表要进行判断的变量,它可以是任意类型的数据。 - 返回值:如果 $value 是数组类型,函数将返回布尔值 true;反之,如果 $value 不是数组类型,函数则返回布尔值 false。
4、explode函数
explode函数是 PHP 里一个常用的字符串处理函数,它的作用是把一个字符串依据指定的分隔符拆分成多个子字符串,最终以数组形式返回这些子字符串。
explode ( string $separator , string $string , int $limit = PHP_INT_MAX ) : array
(1)参数说明
$separator
:这是必需参数,代表用于分割字符串的分隔符。它可以是单个字符,也可以是字符串。$string
:同样是必需参数,指的是要进行分割操作的原始字符串。$limit
:此为可选参数,用来指定分割后数组元素的最大数量。具体情况如下:- 若
$limit
为正数,返回的数组最多包含$limit
个元素,最后一个元素会包含字符串剩余部分。 - 若
$limit
为负数,会返回除了最后abs($limit)
个元素之外的所有元素。 - 若
$limit
为 0,会被当作 1 处理。
- 若
(2)返回值
函数会返回一个由分割后的子字符串组成的数组。
(3)示例代码
$file = explode('.', strtolower($file));
假设文件名是21. php.jpg ,那么file被通过点分割分为3部分从而形成数组。
file[0]=21
file[1]=php
file[2]=jpg
5、end函数
end函数是一个用于操作数组的函数,它的主要作用是将数组内部指针移动到最后一个元素,并返回该元素的值。当你只需要获取数组的最后一个元素时,end
函数是一个简单直接的方法
end(array &$array): mixed
(1)参数说明
$array
:这是一个必需的参数,代表要操作的数组。这里使用了引用传递(&
),意味着函数会直接修改传入数组的内部指针位置。
(2)返回值
函数会返回数组的最后一个元素的值。如果数组为空,end
函数将返回 false
。
(3)示例
对于file数组为如下内容,end(file)将返回file的最后一个元素file[2],即jpg
file[0]=21
file[1]=php
file[2]=jpg
6、reset函数
reset函数是用于操作数组内部指针的函数,它可以把数组的内部指针重置到第一个元素,并且返回该元素的值。
reset(array &$array): mixed
(1)参数说明
$array
:这是必需参数,指的是要操作的数组。使用引用传递(&
),意味着函数会直接修改传入数组的内部指针位置
(2)返回值
函数会返回数组的第一个元素的值。如果数组为空,reset
函数将返回 false
。
(3)使用场景
- 重新开始数组遍历:当你在遍历数组的过程中移动了指针,之后又想从数组的第一个元素开始遍历时,就可以使用
reset
函数。 - 获取数组首元素:如果需要直接获取数组的第一个元素,
reset
函数能很方便地实现
7、count函数
在 PHP 中,count 函数是一个常用的用于统计数组或对象中元素数量的函数。下面为你详细介绍 count 函数的相关信息。
count(mixed $value, int $mode = COUNT_NORMAL): int
(1)参数说明
$value
:该参数为必需项,它可以是数组或者实现了Countable
接口的对象。当传入其他类型的值时,若为NULL
则返回 0;若为其他非数组和非Countable
对象类型,返回 1。$mode
:此为可选参数,有两个取值:COUNT_NORMAL
或 0:这是默认值,只统计数组或对象的顶层元素数量。COUNT_RECURSIVE
或 1:用于递归统计多维数组中所有元素的数量,即会深入到数组的每一个子数组中进行统计。
(2)返回值
函数返回数组或对象中元素的数量,返回值类型为整数。
三、代码审计
打开靶场第21关,本关卡通过后缀过滤白名单的检测方法,具体源码如下所示。
这段代码实现了一个简单的文件上传功能,并且对上传的文件类型进行了限制,详细版注释如下所示。
<?php
// 初始化上传成功标志变量,初始值为 false,表示尚未成功上传文件
$is_upload = false;
// 初始化消息变量,用于存储上传过程中的提示信息,初始值为 null
$msg = null;// 检查 $_FILES 数组中名为 'upload_file' 的文件是否存在且不为空
if (!empty($_FILES['upload_file'])) {// 定义一个数组,存储允许上传的文件 MIME 类型$allow_type = array('image/jpeg', 'image/png', 'image/gif');// 检查上传文件的 MIME 类型是否在允许的类型数组中if (!in_array($_FILES['upload_file']['type'], $allow_type)) {// 如果不在允许的类型数组中,设置提示消息为禁止上传该类型文件$msg = "禁止上传该类型文件!";} else {// 如果 MIME 类型允许,检查文件名// 如果 $_POST['save_name'] 为空,则使用上传文件的原始文件名,否则使用用户指定的保存文件名$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)) {// 如果移动成功,设置提示消息为文件上传成功,并将上传成功标志设置为 true$msg = "文件上传成功!";$is_upload = true;} else {// 如果移动失败,设置提示消息为文件上传失败$msg = "文件上传失败!";}}}
} else {// 如果 $_FILES['upload_file'] 为空,设置提示消息为请选择要上传的文件$msg = "请选择要上传的文件!";
}
?>
1、MIME检查绕过
首先代码会进行MIME检查,需要修改MIME为'image/jpeg','image/png','image/gif'之一才能渗透成功,具体代码如下所示。
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){$msg = "禁止上传该类型文件!";
}
2、文件名不是数组需要设为数组
接下来获取文件名(在1.2部分讲解这部分内容,默认为$_POST['save_name']),通过判断文件名是否为数组,如果不是需要使用.分割文件名将其切割为数组(在1.4节部分讲解这部内容并进行示例)。
$file = empty($_POST['save_name'])? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {$file = explode('.', strtolower($file));
}
3、end函数获取文件名后缀
这部分通过end函数获取文件后缀名(在1.5章节讲解这部分内容),换句话说这个题如果想渗透成功,确保fille的最后一个元素一定要是jpg或者png或者gif。
$ext = end($file); // 从数组中获取最后一个元素作为扩展名
$allow_suffix = array('jpg','png','gif'); // 类型的白名单数组
if (!in_array($ext, $allow_suffix)) {$msg = "禁止上传该后缀文件!";
}
4、文件名处理
$file_name = reset($file) . '.' . $file[count($file) - 1];
reset
作用是将数组的内部指针移动到数组的第一个元素,并返回该元素的值。在explode函数处理后$file是
一个数组,reset($file)
会返回数组$file
的第一个元素,通常情况下就是文件名的主体部分(不包含扩展名)。count($file)
用于返回数组$file
中元素的数量。count($file) - 1
计算得到数组$file
最后一个元素的索引。因为在 PHP 中,数组的索引是从 0 开始的,所以最后一个元素的索引是元素数量减 1。$file[count($file) - 1]
则通过这个索引获取数组$file
的最后一个元素,通常这个元素就是文件的扩展名。- .在 PHP 中是字符串连接运算符,用于将两个字符串连接成一个新的字符串。
reset($file) . '.' . $file[count($file) - 1]
把文件名数组的第一部分、点号和文件扩展名连接起来,形成一个完整的文件名,并将其赋值给变量$file_name
。 如上代码
如上代码用大白话来讲就是拼接文件名:数组第一个元素 + . + 数组最后一个元素
比如文件名为cooper.php,则 $file_name = cooper + . + php
不过这段别扭的代码暴露了一个安全问题,因为数组的元素是可以构造的,假设我们传递的数组只有2个元素,分别是第一个save_name[0]和第三个save_name[2],使得save_name[1]为空,这时最后一项是jpg使得绕过白名单,而count=2(因为没有第2个,只有1和3),举例我们使用如下方法传入参数。
save_name[0] = cooper.php
save_name[2] = jpg
使用end方法检测后缀是否白名单,于是可以绕过白名单
end(file1)=jpg
按照拼接方法那我们构造的文件最后的方式为:
$file_name = cooper.php . $file[1] ==> cooper.php.空
这样我们的文件名拼接出来就是cooper.php.结合windows系统的尾点会被忽略,最后被存储的就是cooper.php,从而绕过服务器的检测。
四、渗透实战
1、制作脚本test21.php
<?
phpphpinfo();
?>
2、PHP版本切换
将php版本切换到5.3,具体如下所示。
3、浏览图片
进入靶场21关,选择test21.php,注意下面保存名称默认为upload-20.jpg,具体如下所示。
3、bp开启拦截
4、点击上传
5、bp拦截
bp捕获到上传报文,下图红框的部分涉及到两个文件名,其中根据源码分析我们指导save_name即为需要修改的文件名POST参数save_name的值,需要将"upload-20.jpg"后缀改为数组形式,其中save_name[0]为"upload-21.php",save_name[2]为"jpg",原始报文如下所示。
6、修改报文
(1)修改MIME
Content-Type :修改为image/jpeg 或image/png 或image/gif
(2)save_name文件名修改
将save_name原始的"upload-29.jpg"后缀改为数组形式,其中save_name[0]为"upload-21.php",save_name[2]为"jpg",具体修改方法为复制22-25行,粘贴到26行后面
在第一个save_name后加[0],将upload-20.jpg修改为upload-21.php
在第二个save_name后加[2],将upload-20.jpg修改为jpg
修改后效果如下所示。
7、发包并获取脚本地址
将bp的inception设置为off,此时修改后的报文发送成功。
回到靶场的Pass21关卡,图片已经上传成功,在图片处右键复制图片地址。
右键图片获取图片地址,如下所示获取到图片URL。
http://127.0.0.1/upload-labs/upload/upload-21.php.
8、访问脚本
如下所示访问上传脚本获取到服务器的php信息,证明文件上传成功。