使用regex验证文件扩展名防止恶意/可执行文件上传


Malicious/Executable file upload prevention using regex validation on file extension

一位同事今天和我打赌,他知道一种方法可以提供一个特殊格式的字符串,该字符串可以通过以下正则表达式检查,并且仍然提供扩展名为.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.";
}

尽管我竭尽全力,在网上搜索,但我还是找不到一个能让这一切成为可能的缺陷。我能俯瞰什么吗?假定";空字节";如果处理了漏洞,这里还有什么问题?

注意:我绝不意味着这段代码是一种检查文件扩展名的完整方法,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上。尽管如此,它还是按照我的同事告诉我的那样做了——通过了regex检查,并用可执行扩展名保存了文件。以下内容在WampServer 2.2PHP 5.3.13:上进行了测试

将以下字符串传递给test.php:.jpg上方的正则表达式检查(请注意,所需扩展末尾的":"冒号符号)将验证它,函数copy()似乎省略了冒号符号之后的所有内容,包括符号本身。同样,这只适用于windows。在linux上,文件将使用与传递给函数的名称完全相同的名称编写。

没有一个单一的步骤或完全直接的方法来利用您的代码,但这里有一些想法。

在本例中,您将它传递给copy(),但您已经提到,您已经使用此方法验证文件ext一段时间了,所以我认为您在其他情况下可能也在不同的PHP版本上使用了此过程和其他函数。

将此视为一个测试程序(利用包括,要求):

$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";
}

这将通过打印"in!!!"并执行"test.php"来结束,即使它被上传,它也会从tmp文件夹中包含它——当然,在这种情况下,您已经被攻击者拥有,但让我们也考虑一下这些选项。这不是上传过程的常见场景,但它是一个可以通过组合几种方法来利用的概念:

让我们继续-如果你执行:

//$_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脚本提供服务,PHP脚本(在某些不安全的情况下)可能会通过将文件包含错误类型的函数(如requireincludefile_get_contents)来加载文件,这些函数都是易受攻击的并且可以执行文件。

空字节空字节攻击是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解决这一问题的方法是,在将字符串传递给创建实际char数组的更深入的C过程之前和之后检查字符串长度,如果字符串被空字节截断(这表示C中字符串的末尾),则长度将不匹配。阅读更多

奇怪的是,即使在修补过的现代PHP版本中,它仍然存在:

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

我的结论:验证文件扩展名的方法可以得到显著改进——您的代码允许一个名为test.php#.jpg的PHP文件通过,而它不应该通过。成功的攻击大多是通过组合几个漏洞——甚至是小漏洞——你应该把任何意外的结果和行为视为一个整体。

注意:还有更多关于文件名和图片的问题,因为它们在以后的页面中会被多次包含,如果它们没有被正确过滤和安全包含,你会暴露在更多的XSS内容中,但这已经脱离了主题。

请尝试此代码。

$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";
     }
}

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"));
}

我记得在PHP<5.3.X,PHP允许字符串包含0x00,此字符被视为字符串的末尾
例如,如果您的字符串包含:myfile.exe''0.jpg,则preg_match()将与jpgHP函数将停止在myfile.exe中,include()copy()函数