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

<form action="t.php" method="post" enctype="multipart/form-data">
    <input name="aaa" type="file" />
    <input type="submit" />
</form>

这里有几个要素:

  • 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中的代码写成这样:

<?php

if(!isset($_FILES['aaa'])) {
    echo 'file not found';
    exit();
}

var_dump($_FILES['aaa'])

可以看到那个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 .= '<pre>Your image was not uploaded.</pre>';
    }
    else {
        // Yes!
        $html .= "<pre>{$target_path} succesfully uploaded!</pre>";
    }
}

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

然后是中级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 .= '<pre>Your image was not uploaded.</pre>';
        }
        else {
            // Yes!
            $html .= "<pre>{$target_path} succesfully uploaded!</pre>";
        }
    }
    else {
        // Invalid file
        $html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
    }
}

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

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

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

<?php

phpinfo();
------WebKitFormBoundaryh4zhLV52OKhf6aJg--

右键"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 .= '<pre>Your image was not uploaded.</pre>';
        }
        else {
            // Yes!
            $html .= "<pre>{$target_path} succesfully uploaded!</pre>";
        }
    }
    else {
        // Invalid file
        $html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
    }
}

观察相同的位置,这回改成了使用后缀名判断。我们这个时候可以在后缀之前插入\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 注

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值