用户在使用本文信息时,应自行承担风险。本文不对用户因使用本文信息而导致的任何直接或间接损失承担责任。

知识补给库
学习内容:利用数组绕过

PHP函数
explode()函数
explode 函数用于将字符串分割成数组
函数原型:array explode(string $delimiter, string $string, int $limit = PHP_INT_MAX)

• 参数
• delimiter:分隔符,用于指定字符串分割的依据
• $string:需要被分割的字符串
• $limit:可选参数,用于限制返回数组的最大元素数量。如果省略,默认为 PHP_INT_MAX,即不限制分割次数。
• 返回值:返回一个数组,数组中的每个元素是根据 $delimiter 分割后的字符串片段
$string = "muma.php";
$result = explode('.', $string);
print_r($result);

strtolower()函数
strtolower 函数是一个用于将字符串转换为小写的内置函数
函数原型:string strtolower(string $string)

• 参数$string:需要被转换为小写的字符串
• 返回值:所有大写字母都被转换为小写的字符串
$string = "Hello, WORLD!";
$lowercaseString = strtolower($string);
echo $lowercaseString;

end()函数
end() 函数是一个用于处理数组的内置函数,其主要功能是将数组的内部指针移动到最后一个元素,并返回该元素的值。

• 简单讲:获取数组中最后一个元素的值
函数原型:mixed end(array &$array)
• 参数$array: 是需要操作的数组,必须是一个变量
• 返回值:如果数组不为空,返回数组的最后一个元素的值;如果数组为空,则返回 FALSE
$file = array("muma","php");
echo end($file);

reset()函数
reset() 函数是一个用于处理数组的内置函数,其主要功能是将数组的内部指针重置到第一个元素,并返回该元素的值。

• 简单讲:获取数组中的第一个元素的值
函数原型:mixed reset(array &$array)
• 参数$array :是需要操作的数组,必须是一个变量
• 返回值:如果数组不为空,返回数组的第一个元素的值;如果数组为空,则返回 false
$file = array("muma","php");
echo reset($file);

count()函数
count() 函数用于计算数组中的元素数量或对象中的属性数量
函数原型:int count(mixed $array_or_countable, int $mode = COUNT_NORMAL)

• 参数
• $array_or_countable:需要计数的数组或实现了 Countable 接口的对象
• $mode:可选参数,用于指定计数模式。默认值为 COUNT_NORMAL
• COUNT_NORMAL(默认值):仅计算第一层的元素数量
• COUNT_RECURSIVE:递归地计算多维数组中的所有元素数量
• 返回值:返回一个整数,表示数组或对象中的元素数量。如果参数为空数组或空对象,返回值为 0
$fruits = array("apple", "banana", "cherry");
$fruitCount = count($fruits);
echo $fruitCount;

数组
从下面这段代码可以看出数组$file中下标1~7的元素的值为空,但是下标8的内容为a

• count()函数计数不会将值为空的下标进行计数
$file[0] = 1;
$file[8] = 'a';

echo count($file);

代码审计
打开第21关的源代码,由于代码较长,逐段进行分析


直接看最里层的if(也就是第三个if判断),判断上传的文件是否为空,不为空则继续向下执行。
if里面是文件类型的检查,判断是否为jpeg、png和gif这三种类型的文件。如果是则允许上传,否则不允许上传

• 可以通过抓包修改Content-Type的值进行绕过
继续向下分析代码,如下图所示


假如上传的文件类型符合要求,代码会执行到else代码块中


• $file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];用于获取网页中用户上传的文件保存名称
• 这里是三目运算符,如网页中设置的名称为空,则使用默认的保存名称;否则使用用户设置的名称
• if (!is_array($file))判断上传的文件保存名称是否为数组
• 如果不是数组,按照.将上传的文件保存名称分割成一个数组
• 这里是问题所在:如果上传的是数组,则不会执行这个if语块
• $file = explode('.', strtolower($file));是将文件名按照.分隔为数组
• 假如index.php经过explode()函数处理后变为数组:["index","php"]
继续向下分析源码


• $ext = end($file);是获取数组中的最有一个元素的值
• 假如数组内容为:["muma","php"],则最后一个元素为php
• $allow_suffix = array('jpg','png','gif');是一个白名单,用户可以上传的后缀名称
• if (!in_array($ext, $allow_suffix))判断用户上传文件的后缀是否在白名单中,在白名单中则执行else语块
• $file_name = reset($file) . '.' . $file[count($file) - 1];取数组的第一个元素与下标为count($file) - 1的元素进行拼接
• 如果数组内容为["muma","php"],则拼接为muma.php
• $temp_file = $_FILES['upload_file']['tmp_name'];是上传文件临时存储路径
• $img_path = UPLOAD_PATH . '/' .$file_name;是服务器存储文件的最终路径
• if (move_uploaded_file($temp_file, $img_path))判断将上传的文件从临时目录移动到最终的存储路径是否移动成功
靶场实战
打开靶场第21关,上传一句话木马文件,保存名称可以任意。


在BP中打开监听功能,点击上传按钮上传文件截获数据包。


先来绕过Content-Type检查,也就是绕过下面代码的检查


将Content-Type的值进行修改,改为白名单中的任意一个即可。

图片
接下来绕过保存名称按照.进行分割的问题。也就是下面这段代码的绕过。


这里判断上传的保存名称是否为数组,如果不是数组,则执行if语块,如果是数组就可以绕过if语块。


将保存名称改为数组,将save_name改为save_ame[0]即可。

• 由于在网页中保存名称写的是jpg因此要在这里改为php

这样就绕过了数组的验证。接下来绕过数组末尾元素检查与保存文件名拼接问题。


上面代码是获取数组的最后一个元素与白名单进行比对。因此数组的最后一个元素必须是png、jpg与gif其中的一种。


上图中红框中的内容用PHP表示,则为:$save_name[0] = "upload-20.php"
上图中绿框中的内容用PHP表示,则为:$save_name[5] = "jpg"
因为红框与绿框中的内容是同一个数组

• 因此可以表示为:$save_name = ["upload-20.php", , , , , "jpg"]
• 下标1~4中的元素内容为NULL
因为数组的最后一个元素为jpg,因此可以绕过后缀白名单的检查。
接下来就是处理文件名与文件后缀的拼接问题


这里解释为什么在数据包中设置为save_name[5]的原因。

• 首先说明下标不需要必须是5,只要大于1即可。
下面解释数组下标为什么要大于1

• 数组$save_name的内容为["upload-20.php", , , , , "jpg"]
• 使用count()函数对数组计算,返回值为2,在代码中还有一个-1操作,因此最终的结果为1
• $file_name的结果应该是数组的首元素与数组中下标为1的元素进行拼接
• 数组首元素为"upload-20.php";数组中下标为1的元素为NULL
• 两者拼接到一起为upload-20.php,即实现了木马文件的上传
假如在数据包中写入的是save_name[1]

• 数组内容变为:["upload-20.php","jpg"]
• 则按照上述步骤,最终的文件名变为:upload-20.php.jpg,而不是php文件
点击Forward按钮发送数据包,回到浏览器可以看到木马文件上传成功


将URL复制到蚁剑中进行远程连接


使用蚁剑连接成功

小结
这个关卡整体难度不大,但是需要结合源代码和抓包一起分析,可能稍微有些困难。慢慢动手多实践几遍。