资讯专栏INFORMATION COLUMN

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

vslam / 672人阅读

摘要:代码审计一文件上传概述在网站的运营过程中,不可避免地要对网站的某些页面或内容进行更新,这时便需要使用到网站上的文件上传功能。

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文件了。

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

?

%00

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

这些都是可能导致截断的字符。需要注意的是,在实战中由于网站的编/解码规则不同,需要灵活应用。比如不行了就换成%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:UsersasusAppDataLocalTempphp43A1.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.
"; } }

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

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

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

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

0x03 解决方案

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

0x04 注

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/30407.html

相关文章

  • 浅谈开源web程序后台的安全性

    摘要:大学网站使用了名为的开源程序,青年使用一个已经公开的漏洞进入后台青年使用后台上传限制不严的缺陷上传了一个控制主机赠送我国国旗。这些程序都是国内开源程序中的佼佼者,也比较注重安全性。此时后台程序的安全性成为一个短板。 一、前言 不知怎的最近甚是思念校园生活,思念食堂的炒饭。那时会去各种安全bbs上刷刷帖子,喜欢看别人写的一些关于安全技巧或经验的总结;那时BBS上很多文章标题都是:成功渗...

    ad6623 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<