我先援引一下其它博主的内容

lylme_spage六零导航页,未授权webshell上传。php 的文件上传的漏洞,使用$_FILE['type']来判断文件类型是否在白名单内,该值是由浏览器提供的,不是可靠的文件类型验证方法。

概况与复现

lylme_spage,六零导航页,“六零导航页 (LyLme Spage) 致力于简洁高效无广告的上网导航和搜索入口,支持后台添加链接、自定义搜索引擎,沉淀最具价值链接,全站无商业推广,简约而不简单。”,项目地址:https://github.com/LyLme/lylme_spage
影响版本v1.9.5

实际搭建六零导航页后,关闭收录功能,发现漏洞并没有关闭。

主要问题在于 /include/file.php

漏洞概述

六零导航页是一款开源的导航页面,设计简洁、无广告,具备可自定义的搜索引擎和隐私链接功能。然而,由于其 /include/file.php 接口存在任意文件上传漏洞,未经授权的攻击者可以通过这个接口上传任意文件。利用此漏洞,攻击者可能将恶意代码上传至服务器,并取得执行权限,进而控制系统。

漏洞利用的 Proof of Concept (PoC)

可以通过以下的 HTTP POST 请求来利用此漏洞:

POST /include/file.php HTTP/1.1
Host: x.x.x.x
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
Content-Type: multipart/form-data; boundary=---------------------------575673989461736
Content-Length: 234

-----------------------------575673989461736
Content-Disposition: form-data; name="file"; filename="test.php"
Content-Type: image/png

<?php echo "HelloWorldtest"; unlink(__FILE__); ?>
-----------------------------575673989461736--

解释

  • 这个请求向 /include/file.php 发送了带有 .php 扩展名的文件 test.php,文件内容包含简单的 PHP 代码:<?php echo "HelloWorldtest"; unlink(__FILE__); ?>
  • 此代码在上传后执行时会显示 HelloWorldtest,然后删除自身 (unlink(__FILE__))。
  • 若成功上传并执行,则证明此漏洞存在。

漏洞检测脚本

可以使用 Python 检测脚本来验证系统是否存在该漏洞: GitHub,通过下列指令批量检测:
url.txt 包含需要测试的目标 URL 清单。

python 60NavigationPage_CVE-2024-34982_ArbitraryFileUploads.py -f url.txt

python 60NavigationPage_CVE-2024-34982_ArbitraryFileUploads.py -u http://xxxx.xxx.top

修复方法

网上都说官方已针对此漏洞发布补丁,然而并没有找到。
所以让我们手动来修复吧~

将上传目录设置为不可执行

首先,一个很直接的方法就是直接禁止文件上传目录的php执行,这样就算能上传,也不能执行恶意脚本~

在Nginx下对站点加上如下配置:

location /files/upload/ {
    disable_symlinks on;
    autoindex off;
    location ~ \.php$ {
        return 403;
    }
}

如果用的是Apache服务器,则在上传目录中添加 .htaccess 文件(针对 Apache 服务器):

php_flag engine off
RemoveHandler .php .phtml .php3

修复漏洞文件

file.php 的主要问题包括:

  1. 文件类型验证不足:仅依赖文件扩展名和用户提供的 MIME 类型,容易被绕过。
  2. 缺乏对上传文件内容的验证:未确认文件实际内容是否为有效图片。

那我们据此开始修复漏洞

1. 验证上传的 URL

download_img 函数中,添加对传入 URL 的合法性验证,防止非法 URL 导致的安全问题。

修改前:

function download_img($url)
{
    // 原有代码
}

修改后:

function download_img($url)
{
    // 验证 URL 是否有效
    if (!filter_var($url, FILTER_VALIDATE_URL)) {
        exit(json_encode(['code' => '-2', 'msg' => '无效的URL']));
    }

    // 继续原有代码
}

2. 严格验证文件扩展名和 MIME 类型

validate_file_type 函数中,同时验证文件的扩展名和实际的 MIME 类型,防止通过修改扩展名或 MIME 类型绕过验证。

修改前:

function validate_file_type($type)
{
    switch ($type) {
        case 'jpeg':
        case 'jpg':
            $type = 'image/jpeg';
            break;
        case 'png':
            $type = 'image/png';
            break;
        case 'gif':
            $type = 'image/gif';
            break;
        case 'ico':
            $type = 'image/x-icon';
            break;
    }

    $allowed_types = array("image/jpeg", "image/png", "image/gif", "image/x-icon");
    return in_array($type, $allowed_types);
}

修改后:

function validate_file_type($extension, $mime_type = null)
{
    $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'ico'];
    $allowed_mime_types = [
        'jpg'  => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'png'  => 'image/png',
        'gif'  => 'image/gif',
        'ico'  => 'image/x-icon',
    ];

    // 验证扩展名
    if (!in_array(strtolower($extension), $allowed_extensions)) {
        return false;
    }

    // 如果提供了 MIME 类型,进一步验证
    if ($mime_type) {
        return isset($allowed_mime_types[strtolower($extension)]) && $allowed_mime_types[strtolower($extension)] === $mime_type;
    }

    return true;
}

3. 使用 finfo 获取真实的 MIME 类型

upload_img 函数中,使用 finfo_file 获取上传文件的真实 MIME 类型,而不是依赖用户提供的 $_FILES["type"]

修改前:

$type = $upfile["type"];

修改后:

// 使用 finfo 获取真实的 MIME 类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $upfile["tmp_name"]);
finfo_close($finfo);

并在调用 validate_file_type 时传入 $mime_type

if (!validate_file_type($img_ext, $mime_type)) {
    exit(json_encode(['code' => '-4', 'msg' => '上传的图片类型不支持']));
}

4. 验证文件内容是否为有效图片

upload_imgdownload_img 函数中,使用 getimagesize 确认文件实际内容是有效的图片。

upload_img 函数中添加:

// 验证文件内容是否为有效图片
$image_info = getimagesize($upfile["tmp_name"]);
if ($image_info === false) {
    exit(json_encode(['code' => '-4', 'msg' => '上传的文件不是有效图片']));
}

download_img 函数中添加:

// 验证下载的文件是否为有效图片
$tmp_file = tempnam(sys_get_temp_dir(), 'img_');
file_put_contents($tmp_file, $data);
$image_info = getimagesize($tmp_file);
if ($image_info === false) {
    unlink($tmp_file);
    exit(json_encode(['code' => '-4', 'msg' => '抓取的文件不是有效图片']));
}
unlink($tmp_file);

5. 限制文件大小

确保上传和下载的文件大小不超过设定的最大值(如5MB)。

download_img 函数中:

if ($size > $maxsize) {
    exit(json_encode([
        'code' => '-1',
        'msg' => '抓取的图片超过' . ($maxsize / pow(1024, 2)) . 'M,当前为:' . round($size / pow(1024, 2), 2) . 'M'
    ]));
}

upload_img 函数中:

if ($upfile["size"] > $maxsize) {
    exit(json_encode(['code' => '-1', 'msg' => '图片不能超过' . ($maxsize / pow(1024, 2)) . 'M']));
}

完整修改

<?php
header('Content-Type: application/json');
require_once("common.php");
define('SAVE_PATH', 'files/'); // 保存路径

// 1. 下载图片函数
function download_img($url)
{
    if (!filter_var($url, FILTER_VALIDATE_URL)) {
        exit(json_encode(['code' => '-2', 'msg' => '无效的URL']));
    }

    $IMG_NAME = uniqid("img_"); // 文件名
    $maxsize = pow(1024, 2) * 5; // 文件大小5M
    $size = remote_filesize($url); // 文件大小

    if ($size > $maxsize) {
        exit(json_encode([
            'code' => '-1',
            'msg' => '抓取的图片超过' . ($maxsize / pow(1024, 2)) . 'M,当前为:' . round($size / pow(1024, 2), 2) . 'M'
        ]));
    }

    $img_ext = strtolower(pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION));
    if (!validate_file_type($img_ext)) {
        exit(json_encode(['code' => '-4', 'msg' => '抓取的图片类型不支持']));
    }

    $img_name = $IMG_NAME . '.' . $img_ext;
    $dir = ROOT . SAVE_PATH . 'download/';
    $save_to = $dir . $img_name;

    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }

    // 使用 curl 下载图片
    // ... (保持原有 curl 代码不变)

    // 验证下载的文件是否为有效图片
    $tmp_file = tempnam(sys_get_temp_dir(), 'img_');
    file_put_contents($tmp_file, $data);
    $image_info = getimagesize($tmp_file);
    if ($image_info === false) {
        unlink($tmp_file);
        exit(json_encode(['code' => '-4', 'msg' => '抓取的文件不是有效图片']));
    }
    unlink($tmp_file);

    // 保存文件
    if (file_put_contents($save_to, $data) === false) {
        exit(json_encode(['code' => '-1', 'msg' => '保存图片失败']));
    }

    $fileurl = '/' . SAVE_PATH . 'download/' . $img_name;
    echo json_encode([
        'code' => '200',
        'msg' => '抓取图片成功',
        'url' => $fileurl,
        'size' => round($fileSize / 1024, 2) . 'KB'
    ]);
    return $save_to;
}

// 2. 上传图片函数
function upload_img($upfile)
{
    if ($upfile['error'] !== UPLOAD_ERR_OK) {
        exit(json_encode(['code' => '-1', 'msg' => '文件上传错误']));
    }

    $IMG_NAME = uniqid("img_"); // 文件名
    $maxsize = pow(1024, 2) * 5; // 文件大小5M

    $dir = ROOT . SAVE_PATH . 'upload/';
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }

    // 获取真实的 MIME 类型
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime_type = finfo_file($finfo, $upfile["tmp_name"]);
    finfo_close($finfo);

    // 获取文件扩展名
    $img_ext = strtolower(pathinfo($upfile["name"], PATHINFO_EXTENSION));

    // 验证文件类型
    if (!validate_file_type($img_ext, $mime_type)) {
        exit(json_encode(['code' => '-4', 'msg' => '上传的图片类型不支持']));
    }

    // 验证文件大小
    if ($upfile["size"] > $maxsize) {
        exit(json_encode(['code' => '-1', 'msg' => '图片不能超过' . ($maxsize / pow(1024, 2)) . 'M']));
    }

    // 验证文件内容是否为有效图片
    $image_info = getimagesize($upfile["tmp_name"]);
    if ($image_info === false) {
        exit(json_encode(['code' => '-4', 'msg' => '上传的文件不是有效图片']));
    }

    $img_name = $IMG_NAME . '.' . $img_ext;
    $save_to = $dir . $img_name;
    $url = '/' . SAVE_PATH . 'upload/' . $img_name;

    if (!move_uploaded_file($upfile["tmp_name"], $save_to)) {
        exit(json_encode(['code' => '-1', 'msg' => '上传失败']));
    }

    echo json_encode(['code' => '200', 'msg' => '上传成功', 'url' => $url]);
    return $save_to;
}

// 3. 文件验证函数
function validate_file_type($extension, $mime_type = null)
{
    $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'ico'];
    $allowed_mime_types = [
        'jpg'  => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'png'  => 'image/png',
        'gif'  => 'image/gif',
        'ico'  => 'image/x-icon',
    ];

    if (!in_array($extension, $allowed_extensions)) {
        return false;
    }

    if ($mime_type) {
        return isset($allowed_mime_types[$extension]) && $allowed_mime_types[$extension] === $mime_type;
    }

    return true;
}

// 4. 主逻辑处理
if (empty($_POST["url"]) && !empty($_FILES["file"])) {
    $filename = upload_img($_FILES["file"]);
    if (isset($islogin) && $islogin == 1 && isset($_GET["crop"]) && $_GET["crop"] == "no") {
        // 不压缩图片
        exit();
    }
} elseif (!empty($_POST["url"])) {
    $filename = download_img($_POST["url"]);
} else {
    exit(json_encode(['code' => '0', 'msg' => 'error']));
}

imagecropper($filename, 480, 480);
?>

效果

只进行上传目录执行限制时,上传文件后访问其文件:

修复文件漏洞后,尝试用60NavigationPage_CVE-2024-34982_ArbitraryFileUploads.py对漏洞文件发起攻击。