PHP 代码审计:(一)文件上传

PHP 代码审计:(一)文件上传

0x00 概述

在网站的运营过程中,不可避免地要对网站的某些页面或内容进行更新,这时便需要使用到网站上的文件上传功能。如果不对被上传文件进行限制,或限制被绕过,该功能便有可能会被利用于上传可执行文件、脚本到服务器上,今儿进一步导致服务器沦陷。

由此可见,了解上传漏洞的前提是了解文件上传这个功能,以及其中的原理。如果只知道有文件上传这个东西,以及可能会出现漏洞,那么跟不知道一个样。

具体来说,一些用户上传的文件还是PHP脚本,这些PHP脚本上传到服务器上能够被用户通过服务器直接访问,其中包含的一些命令就会被执行。文件上传的功能就是如此强大,如果你的网站在文件上传方面控制得不够好,就会沦陷。

导致文件上传漏洞的原因较多,主要包含:

  • 服务器配置不当

  • 开源编辑器上传漏洞

  • 本地文件上传限制被绕过

  • 过滤不严或被绕过

  • 文件解析漏洞导致文件执行

  • 文件路径截断

  • ...

其中,开源编辑器漏洞和文件上传漏洞原理一样,只不过多了一个编辑器。上传的时候还是会把我们的脚本上传上去。

过滤不严这个非常常见,后面的例子中我们会看到。比如大小写问题,网站只验证是否是小写,我们就可以把后缀名改成大写。

然后是文件解析漏洞。比如 Windows 系统会涉及到这种情况:文件名为1.php;.jpg,IIS 6.0 可能会认为它是jpg文件,但是执行的时候会以php文件来执行。我们就可以利用这个解析漏洞来上传。再比如 Linux 中有一些未知的后缀,比如a.php.xxx。由于 Linux 不认识这个后缀名,它就可能放行了,攻击者再执行这个文件,网站就有可能被控制。

最后是路径截断,就是在上传的文件中使用一些特殊的符号,使文件在上传时被截断。比如a.php%00.jpg,这样在网站中验证的时候,会认为后缀是jpg,但是保存到硬盘的时候会被截断为a.php,这样就是直接的php文件了。

常用来截断路径的字符是:

  • \0

  • ?

  • %00

  • 也可以使用超长的文件路径造成截断。

这些都是可能导致截断的字符。需要注意的是,在实战中由于网站的编/解码规则不同,需要灵活应用。比如\0不行了就换成%00,也可以尝试各种编码,比如 base64 或者 unicode,多试几次。

0x01 代码

文件上传首先需要一个表单,如下,我们把它叫做a.html

这里有几个要素:

  • action属性是提交的目标。

  • method属性是提交所用的HTTP方法,常用的就是 POST 和 GET,文件上传一般用 POST。

  • enctype属性必须要写成这样,因为文件上传和普通的提交具有不同的编码方式。如果不写的话,可能会被当做urlencoded,就是k1=v1&k2=v2的键值对形式,导致解析不出东西。

  • 最后是文件输入框,它的name属性非常重要,它是PHP脚本中寻找文件的关键字。

接下来是PHP脚本中的东西,PHP中通过$_FILES对象来读取文件,通过下列几个属性:

  • $_FILES[file]['name'] - 被上传文件的名称。

  • $_FILES[file]['type'] - 被上传文件的类型。

  • $_FILES[file]['size'] - 被上传文件的大小(字节)。

  • $_FILES[file]['tmp_name'] - 被上传文件在服务器保存的路径,通常位于临时目录中。

  • $_FILES[file]['error'] - 错误代码,0为无错误,其它都是有错误。

t.php中的代码写成这样:

可以看到那个aaa就是文件输入框中的name属性。

我们把这两个文件放到服务器的目录中,或者直接在目录下启动PHP自带的服务器。之后打开a.html随便传上去一个文件,会得到这样的结果,这里我直接上传了a.html

array(5) { 
    ["name"]=> string(6) "a.html" 
    ["type"]=> string(9) "text/html" 
    ["tmp_name"]=> string(44) "C:\Users\asus\AppData\Local\Temp\php43A1.tmp" 
    ["error"]=> int(0) 
    ["size"]=> int(133) 
}

需要说的是,在处理文件上传的时候,不应信任文件类型type,因为类型在浏览器生成之后,是可以改的。甚至可以手动构造出于类型与实际内容不匹配的数据包。

同时也不应该信任文件名称name。而是应该分离文件名与扩展名,对扩展名进行白名单过滤。文件名按需舍弃重新生成,或者过滤后再使用。

过度信任这些东西会产生一些隐患,下面我们就会看到。

0x02 实战

实战部分我会使用DVWA中的例子来演示。DVWA是用PHP+Mysql编写的一套用于常规WEB漏洞教学和检测的WEB脆弱性测试程序。包含了SQL注入、XSS、盲注等常见的一些安全漏洞。项目主页在这里,源码在Github上。

下载下来部署完成之后,我们打开vulnerabilities/upload/source/,这里是上传漏洞部分的源码,可以看到有三个难度:低中高。

先看低级难度low.php

if( isset( $_POST[ 'Upload' ] ) ) {
    // Where are we going to be writing to?
    $target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
    $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
    // Can we move the file to the upload folder?
    if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
        // No
        $html .= '
Your image was not uploaded.
'; } else { // Yes! $html .= "
{$target_path} succesfully uploaded!
"; } }

可以看到什么过滤都没有,可以直接上传任意文件。

然后是中级medium.php

if( isset( $_POST[ 'Upload' ] ) ) {
    // Where are we going to be writing to?
    $target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
    $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
    // File information
    $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
    $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
    $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
    // Is it an image?
    if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&
        ( $uploaded_size < 100000 ) ) {
        // Can we move the file to the upload folder?
        if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
            // No
            $html .= '
Your image was not uploaded.
'; } else { // Yes! $html .= "
{$target_path} succesfully uploaded!
"; } } else { // Invalid file $html .= '
Your image was not uploaded. We can only accept JPEG or PNG images.
'; } }

注意第 10 行和第 11 行,它验证了类型必须是jpgpng,并且尺寸必须小于一定数值。后者可以不用管它。刚才我说过类型是不可信的,我们可以抓包,将其中的类型改掉,之后提交。

因为这里要演示如何突破限制上传,并不演示如何利用上传的脚本,所以我直接新建了个PHP文件,随便写了点东西进去。打开 burpsuite 抓包,我们会在请求正文中看到这样的东西:

------WebKitFormBoundaryh4zhLV52OKhf6aJg
Content-Disposition: form-data; name="uploaded"; filename="a.php"
Content-Type: application/octet-stream

右键"send to repeater",然后把那个application/octet-stream改成image/jpeg。点击"go"发送。

最后是高级high.php

if( isset( $_POST[ 'Upload' ] ) ) {
    // Where are we going to be writing to?
    $target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
    $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
    // File information
    $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
    $uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
    $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
    $uploaded_tmp  = $_FILES[ 'uploaded' ][ 'tmp_name' ];
    // Is it an image?
    if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
        ( $uploaded_size < 100000 ) &&
        getimagesize( $uploaded_tmp ) ) {
        // Can we move the file to the upload folder?
        if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) {
            // No
            $html .= '
Your image was not uploaded.
'; } else { // Yes! $html .= "
{$target_path} succesfully uploaded!
"; } } else { // Invalid file $html .= '
Your image was not uploaded. We can only accept JPEG or PNG images.
'; } }

观察相同的位置,这回改成了使用后缀名判断。我们这个时候可以在后缀之前插入\0,即a.php\0.jpg,要注意它不是一个斜杠加上一个零,而是空字符。这样判断的时候,后缀是.jpg,写到磁盘的时候就会截断为a.php。既可以上传也可以执行。

我们同样抓包并送到repeter。先把a.php改成a.php.jpg,然后切换到 hex 编辑模式来插入空字符。

鼠标拖动的范围就是a.php.jpg,在第二个2e格子上右键,点击"insert byte",会自动插入一个\0。之后点击"go"就大功告成了。

注意,如果这里插入%00,上传倒是能成功,但是访问的时候会直接当成图片,不会执行里面的内容。

0x03 解决方案

同目录下还有个impossible.php,里面包含了正确的做法。大家可以看一下。里面的代码使用了$target_file = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;来生成独立的文件名,这样就不会被原文件名中各种截断字符干扰了。

0x04 注

部分内容来源于白帽学院 - 代码审计。

你可能感兴趣的:(网络安全,php)