使用文件扩展名正则表达式验证防止恶意/可执行文件上传。

11
今天我的一位同事与我打赌,他知道一种方法可以提供一个特殊格式的字符串,可以通过以下正则表达式检查,并仍然提供带有扩展名为.php.jsp.asp的文件名:
if (preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $var) && preg_match('/\.(asp|jsp|php)$/i', $var) == false) 
{
    echo "No way you have extension .php or .jsp or .asp after this check.";
}

尽管我已经尝试了很多方法并在网络上搜索,但我仍然无法找到一个可以使这种事情成为可能的漏洞。我是否忽略了什么?如果“null字节”漏洞得到解决,还可能有什么问题呢?

注意:我绝不意味着这段代码是检查文件扩展名的完美方法,preg_match()函数可能存在缺陷,或者文件内容可能是不同的格式,我只是就正则表达式语法本身提出问题。

编辑-实际代码:

if (isset($_FILES["image"]) && $_FILES["image"]["name"] && preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $_FILES["image"]["name"]) && preg_match('/\.(asp|jsp|php)$/i', $_FILES["image"]["name"]) == false) {
    $time = time();
    $imgname = $time . "_" . $_FILES["image"]["name"];
    $dest = "../uploads/images/";

    if (file_exists($dest) == false) {
        mkdir($dest);
    }

    copy($_FILES['image']['tmp_name'], $dest . $imgname);
    
}else{
    echo "Invalid image file";
}
    

PHP版本:5.3.29

编辑:尾声

结果发现这个“漏洞”只出现在Windows上。无论如何,它确实做到了我的同事告诉我的那样——通过正则表达式检查并保存带有可执行文件扩展名的文件。以下是在WampServer 2.2PHP 5.3.13上进行测试的:

将以下字符串传递给上面的正则表达式检查test.php:.jpg(注意所需扩展名末尾的“:”冒号符号)将对其进行验证,并且函数copy()似乎省略了冒号符号之后的所有内容,包括符号本身。再次强调,这只适用于Windows。在Linux上,文件将完全按照传递给函数的相同名称编写。


@SeanBright:确实,但这不是问题,因为他使用的是非严格比较== - Casimir et Hippolyte
1
@SeanBright 确实。关于这个问题本身有什么要说的吗? - astralmaster
@Paladin76 不,我问过他了。那会违反赌约的条款。 :) - astralmaster
他是否可以通过使用双字节字符来混淆它? - Steve
1
@revo 它是 D (PCRE_DOLLAR_ENDONLY)。请参考 http://php.net/manual/en/reference.pcre.pattern.modifiers.php。 - Mariano
显示剩余52条评论
4个回答

11

没有单一的步骤或全面直接的方式来利用你的代码,但以下是一些想法。

在这个例子中,你将它传递给了 copy(),但你已经提到你一直在使用这种方法来验证文件扩展名,所以我假设你可能已经在不同的 PHP 版本上使用这个过程和其他函数进行了其他操作。

把这个看作一个测试过程(利用 include、require):

$name = "test.php#.txt";
if (preg_match('/\.(xml|csv|txt)$/i', $name) && preg_match('/\.(asp|jsp|php)$/i', $name) == false) {
    echo "in!!!!";
    include $name;
} else {
    echo "Invalid data file";
}

即使上传了'test.php'文件并将其包含在临时文件夹中,此代码仍将打印“in!!!!”并执行它 - 当然,在这种情况下,攻击者已经控制了您的系统,但我们也考虑一下这种情况。这不是上传过程中常见的情况,但可以通过结合多种方法来利用。

接下来,如果您执行:

//$_FILES['image']['name'] === "test.php#.jpg";
$name = $_FILES['image']['name'];
if (preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $name) && preg_match('/\.(asp|jsp|php)$/i', $name) == false) {
    echo "in!!!!";
    copy($_FILES['image']['tmp_name'], "../uploads/".$name);
} else {
    echo "Invalid image file";
}

再次完全没问题。文件被复制到“uploads”文件夹中 - 您无法直接访问它(因为Web服务器将削减#右侧的内容),但您注入了该文件,攻击者可能会找到一种方法或另一个弱点来稍后调用它。

这样的执行场景示例在共享和托管站点中很常见,其中文件由PHP脚本提供,该脚本(在某些不安全的情况下)可能通过使用错误类型的函数(如requireincludefile_get_contents)加载文件,从而执行文件。

NULL字节 空字节攻击是php <5.3的一个重大弱点,但在5.4+版本中的一些函数(包括所有与文件相关的函数以及许多扩展中的其他函数)中由于回归而重新引入了该漏洞。 它已经被修补了多次,但仍然存在,许多旧版本仍在使用。 如果您处理旧的PHP版本,那么您绝对会暴露:

//$_FILES['image']['name'] === "test.php\0.jpg";
$name = $_FILES['image']['name'];
if (preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $name) && preg_match('/\.(asp|jsp|php)$/i', $name) == false) {
    echo "in!!!!";
    copy($_FILES['image']['tmp_name'], "../uploads/".$name);
} else {
    echo "Invalid image file";
}

会打印出 "in!!!!",并复制名为 "test.php" 的文件。

PHP解决这个问题的方法是在将字符串传递给更深层次的C程序创建实际字符数组之前检查字符串的长度,因此如果字符串被空字节截断(在C中表示字符串结束),则长度将不匹配。阅读更多

奇怪的是,即使在已修补的和现代的PHP版本中,这个问题仍然存在:

$input = "foo.php\0.gif";
include ($input); // Will load foo.php :)

我的结论: 您验证文件扩展名的方法可以得到显着改进-您的代码允许名为test.php#.jpg的PHP文件通过,而实际上它不应该通过。成功的攻击通常是通过结合多个漏洞,即使是小漏洞来执行的-您应该将任何意外的结果和行为视为一个漏洞。

注意:文件名和图片存在许多问题,因为它们经常在以后的页面中包含,如果没有正确过滤和安全地包含,您就会暴露于更多的XSS问题,但这超出了本话题的范围。


如果你限制它只能是图像,那么大多数情况下不会出现这种错误,但这是一个常见的初学者错误,可能会通过require或include返回任何类型的文件给用户,而不是使用readfile - 或者在基于模板的系统中,一旦加载了文件,你可以找到一个页面,根据url参数从基础上加载其模板,然后调用你注入的文件...发挥你的想象力 :) - Shlomi Hassid
最佳的 null byte 回归参考:bugs.php,按 Ctrl+F 并输入 null bytes - 所有与路径相关的错误修复都与此问题相关。 - Shlomi Hassid
攻击者为什么要使用 test.php\0.jpg,而不是直接使用 test.php.jpg 呢?抱歉,我是新手。 - choz
@ShlomiHassid 哇.. 这真是危险的东西,伙计.. 谢谢你提醒我.. +1 - choz
1
@choz 如果你想要冒险,可以跟踪错误报告和修订版本,甚至可以做出贡献。 :) - Shlomi Hassid
显示剩余5条评论

2

试试这段代码。

$allowedExtension = array('jpeg','png','bmp'); // make list of all allowed extension

if(isset($_FILES["image"]["name"])){
     $filenameArray = explode('.',$_FILES["image"]["name"]);
     $extension = end($filenameArray);
     if(in_array($extension,$allowedExtension)){
        echo "allowed extension";
     }else{
          echo "not allowed extension";
     }
}

0

preg_match()函数在匹配成功时返回1,在匹配失败时返回0,如果发生错误则返回FALSE。

$var = "test.php";
if (preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $var) === 1 
    && preg_match('/\.(asp|jsp|php)$/i', $var) !== 1) 
{
    echo "No way you have extension .php or .jsp or .asp after this check.";
} else{
    echo "Invalid file";
}

因此,当您使用代码进行检查时,请使用=== 1

理想情况下,您应该使用。

function isImageFile($file) {
    $info = pathinfo($file);
    return in_array(strtolower($info['extension']), 
                    array("jpg", "jpeg", "gif", "png", "bmp"));
}

0

我记得在某些版本的PHP < 5.3.X中,PHP允许字符串包含0x00,这个字符被认为是字符串的结尾。
因此,例如,如果您的字符串包含:myfile.exe\0.jpg,那么preg_match()将匹配jpg,但其他PHP函数将停止在myfile.exe,,如include()copy()函数。


自 PHP 5.2.X 起已经修补。 - astralmaster
不,只有在PHP 5.3.X及以上版本才支持...至少我已经在Windows上测试了我的示例,在PHP 5.2.10上仍然存在这个问题 :) - Halayem Anis
@HalayemAnis 许多旧版本都会被修补回来,尤其是在实施安全补丁的良好维护系统上 - 你可以从某个修订版本之前构建并自行测试。 - Shlomi Hassid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接