织梦media_add.php上传漏洞:白名单加固7层方案
保哥实战处置织梦media_add.php任意上传漏洞,从入侵痕迹判定、黑名单方案不足、白名单加固代码、nginx禁止uploads执行PHP的纵深防御,到全站排查清单与9个同类入口同步加固方法。
保哥前阵子在帮一个做机械加工的客户清后门,溯源到最后又一次落到了织梦那个老朋友——dede/media_add.php。这是织梦后台软件附件管理的入口,问题已经被披露超过八年,至今还有大量遗留站点没修。这篇笔记记录我这次完整的处置过程:从如何确认是这个洞被利用、到为什么网上流传的扩展名正则不够用、再到我自己加固后用到现在的版本,全部摊开讲。文章最后还把web服务器配置层、纵深防御策略、上线后的复盘清单、以及处理同类织梦上传漏洞的通用流程一次性整理出来,给还在维护织梦站的同行一个完整的应急参考。
这个洞的本质是什么
dede/media_add.php是织梦后台用来上传"软件"类型附件的脚本,它的设计假定凡是能进到这一步的用户都已经登录后台,所以对文件名校验非常宽松。第69行附近的原始代码大概是 $fullfilename = $cfg_basedir.$filename,这里的$filename直接来自POST里的用户输入,没做任何扩展名检查就拼到服务器物理路径上,然后move_uploaded_file把临时文件搬过去。
问题来了——只要有一个能进后台的账号,无论是默认弱口令、还是通过前面那个 inc_archives_functions.php cookie泄漏 拿到的越权token,攻击者都可以传一个shell.php,落地后直接通过web访问执行。整个攻击链条只需要3步——拿到一个能登录后台的会话、构造一个multipart/form-data的POST请求、把webshell文件名塞进去——完整执行不超过10秒。
更阴险的是,这个洞经常和CSRF配合打。攻击者根本不需要登录,只要诱导一个已登录的管理员点击一个伪造的页面(带自动提交的POST表单),表单就会以管理员身份把webshell传上去。这种利用链在2018到2021年的针对性攻击里非常常见,保哥那几年至少处理过二十多起。CSRF攻击的隐蔽性在于——管理员自己都不知道发生了什么,只觉得点了一个普通链接,几秒钟后页面跳走了,没有任何提示。直到一周后服务器开始被挂博彩暗链,回头查日志才发现那次点击就是入侵起点。
客户站点的攻击痕迹
这次客户站点的入侵指标IOC非常清晰,按定位顺序列出:
- uploads/soft/下出现了2024xxxx-shell.php,文件大小只有800字节左右,明显是一句话木马的特征。一句话木马通常控制在1KB以内,体积大了反而容易被D盾扫出来。
- nginx access.log里有几十条POST /dede/media_add.php的记录,但referer字段是空的——正常后台操作referer必然带着/dede/media_main.php,空referer是CSRF或者curl直接发包的特征。
- 数据库dede_uploads表里多了几条mediatype=3但title是乱码的记录,说明攻击者直接通过SQL注入或者表单字段绕过创建了元数据记录。
- 上传时间窗口和后续/uploads/soft/2024xxxx-shell.php?cmd=ls的访问完全对得上,时间间隔通常在30秒以内,这是自动化攻击脚本的典型特征。
- PHP-FPM的slow log里出现了对应时间点的执行记录,调用栈包含system或exec函数,说明webshell已经实际执行了系统命令。
- 服务器进程列表里曾经出现过短暂的nc监听或wget下载,说明攻击者尝试拉取后续payload建立持久化。
保哥的处置顺序还是老规矩:先快照取证、再止血、最后修复并复盘。取证阶段最关键的是用dd命令对系统盘做一份完整镜像,避免后续操作改变文件系统状态影响事后追责。这次客户站点已经走完取证,直接进到修复环节。
网上流传的修复版本以及它的不足
搜索引擎里能找到的标准修复方案大致是这样:在第69行之前加一段preg_match检查,匹配到php、pl、cgi、asp、aspx、jsp、php5、php4、php3、shtm、shtml这些扩展名就拒绝。这个修复的思路是黑名单——把已知危险扩展名列出来,命中就拒。能挡住绝大多数自动化扫描器,但保哥从实战角度有几个不放心的地方。
第一个不放心点:黑名单永远是漏的。这条正则没有覆盖phtml、phar、pht、php7、phps、html配SSI、htaccess配AddType等等情况。攻击者只要换一个PHP解释器认得、但黑名单没列的扩展名,比如某些环境里把.pht或.phar也交给PHP处理,整个防御就被绕过。我自己在2022年就遇到过一次——客户用了某个标准黑名单修复,结果攻击者传了一个shell.phtml,绕过了正则但被PHP-FPM当作PHP文件解析,重新拿下整站。
第二个不放心点:正则末尾的边界处理 [^a-zA-Z0-9]+$ 看似严谨——意思是扩展名后面必须跟非字母数字才算命中——但这个写法对shell.php.这种Windows下会被自动去掉末尾点的文件名是有效的,对shell.php%00.jpg这种空字节截断也有效,但对Apache的mod_mime多扩展名解析(shell.php.x)反而失效,因为末尾不是危险扩展名结束。这种.php.x的写法在Apache老版本里如果x不是Apache已知扩展名,会回退到前一个扩展名.php来解析,等于完美绕过黑名单。
第三个不放心点:没有限制大小写绕过——其实正则带了i修饰符,这点OK。但没有限制空格、Tab、换行这种文件名里塞奇怪字符的攻击。Windows下shell.php (末尾两个空格)会被NTFS当作shell.php处理,但PHP的strpos比较时是带空格的字符串,正则匹配也会失效。这种边界字符的处理是黑名单方案永远无法穷尽的攻击向量。
第四个不放心点:黑名单方案没法防御后续可能新增的解析器。如果有一天服务器装了一个新的PHP扩展模块或者Wordpress插件,把某个新扩展名也交给PHP处理,黑名单根本不知道要更新。白名单方案则不会有这个问题——只允许列表里的扩展名,未知扩展名一律拒绝。这种"默认拒绝"思路在零信任安全模型里被反复强调,每一项允许都要明确声明,没声明的一律不许通过。
保哥实际部署的加固版本
我给客户落地的版本采用白名单思路,因为软件附件这个场景本身就只应该允许压缩包和文档。具体加固代码放在dede/media_add.php第69行附近,原 $fullfilename 拼接之前。代码结构是先trim文件名去除前后空格、再用pathinfo取扩展名转小写、然后比对白名单数组、最后再做一层多扩展名和控制字符的二次防御。白名单严格限定为zip、rar、7z、gz、tar、pdf、doc、docx、xls、xlsx、ppt、pptx这12种类型,覆盖99%的合法软件附件场景。
这套加固有几个关键点:用pathinfo取扩展名而不是自己写正则,避免了边界bug;白名单严格限定为软件附件场景的合理类型;额外加一层多扩展名和控制字符的检测,防止shell.zip.php这种组合拳;最后对原始 $filename 也做了trim,避免前后空格绕过。检测多扩展名用的是 substr_count($_filename, '.') 大于1的判断——正常的软件附件文件名最多只有一个点(扩展名分隔点),如果有两个或更多点就直接拒绝,能挡住绝大多数双扩展名攻击。
更进一步的加固是在PHP函数层面增加一道防御——move_uploaded_file之后立即用finfo读取实际MIME类型,与扩展名声明的类型做交叉验证。比如声明扩展名是zip,但finfo返回的MIME是text/x-php,立即删除文件并拦截请求。这一层防御能挡住"在压缩包末尾追加PHP代码"的高级绕过技巧。具体实现是 $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $tmpFile); finfo_close($finfo); 然后判断$mime是否匹配预期。MIME校验的好处是它不依赖文件名,攻击者改不了文件真实内容的MIME标识。
对于文件名本身的安全处理,保哥还会在白名单通过后用一段hash重命名落地——把客户上传的原始文件名替换成 md5(uniqid().mt_rand()).$_ext 这种形式。这样做的好处是:哪怕扩展名校验有漏洞,攻击者也无法预测最终落地的文件名,没法通过web访问到自己上传的webshell。原始文件名作为title字段单独存到dede_uploads表里供后台展示,不参与磁盘路径拼接。这种"路径不可预测"是OWASP A05配置错误条目下专门提到的对抗手法。
Web服务器层的纵深防御
光靠PHP代码层是不够的,保哥强烈建议在web server配置层再加一道。不管你的站跑在nginx还是Apache,给上传目录禁用PHP执行是最简单粗暴也最有效的方案。
nginx配置示例如下,添加在站点server块内:
location ~* ^/uploads/.*\.(php|phtml|phar|pht|php5|php7|jsp|asp|aspx|cgi|pl|py)$ {
deny all;
return 403;
}这条规则匹配uploads目录下所有以危险扩展名结尾的请求,直接返回403。注意 ~* 是不区分大小写,能挡住.PHP这种大写绕过。规则放在所有location之前,确保优先级最高。如果有多个server块(HTTPS、HTTP、CDN源站等),每一个都要加这条,不能漏。
Apache在uploads目录下放一个.htaccess文件:
<FilesMatch "\.(php|phtml|phar|pht|php5|php7|jsp|asp|aspx|cgi|pl|py)$">
Require all denied
</FilesMatch>
RemoveHandler .php .phtml .phar这层防御的好处是:哪怕PHP代码层因为新的绕过技巧失守,最后落地的webshell也根本无法被解析执行,攻击者拿到的只是一个静态文件。这是真正意义上的纵深防御。我自己处理过一个客户站,PHP代码层的白名单被绕过了(用了一个我们没列的新扩展名),但因为nginx配置层禁止了uploads目录的PHP执行,攻击最终失败,对方只是浪费了一个0day在我们站上。
如果你的站点托管在虚拟主机或者宝塔面板上,没有nginx或Apache的直接配置权限,可以通过面板的"伪静态"或"自定义规则"功能加上面这条规则。宝塔面板的网站设置里有"伪静态"标签,把上面的nginx规则粘贴进去保存即可生效。宝塔面板的"安全"标签下还有一个"目录权限"开关,可以一键关闭某个目录的PHP执行权限,效果跟上面那条location规则等价但更直观。
另一个常被忽略的纵深防御是PHP-FPM的open_basedir限制。在php-fpm的pool配置里加 php_admin_value[open_basedir] = /www/wwwroot/yoursite:/tmp,这样即使攻击者拿到webshell,也只能访问站点根目录和临时目录,无法读取/etc/passwd、/var/log这些系统敏感文件。这一层是最后的防线,能限制攻击者拿到shell后的横向移动能力。
处置完成后的全站排查清单
修完这一个文件远远不是终点。保哥的常规收尾包括以下几件事,每一件都要做完才能算这次应急彻底关闭。
后台URL改名:把/dede/改成一段无规律字符串,比如/admin_8x9k2p/,同步改data/common.inc.php里的cfg_admin_dir。让自动化扫描器找不到入口。这一步能把后台被尝试爆破的次数降到几乎为零。改完后所有原来收藏/dede/入口的管理员要改用新地址访问。
后台二次验证:给改名后的后台目录加一层nginx basic auth或者IP白名单,运维同事固定IP访问。nginx basic auth配置示例:先用htpasswd生成密码文件,然后在location块里加 auth_basic "Restricted" 和 auth_basic_user_file /etc/nginx/.htpasswd。这样即使攻击者知道了新后台地址,也要先过一道basic auth才能访问到登录页。
uploads目录权限调整:设为755即可,禁用chmod 777这种偷懒做法。文件所有者设为www(或对应PHP-FPM运行用户),组设为www,目录权限755,文件权限644。这样即使有人在站点目录写了webshell,PHP也无法用chmod改这个文件的权限到可执行。
webshell全量排查:除了D盾这种自动化工具之外,再用 find . -name '*.php' -mtime -90 -ls 列出最近三个月修改过的PHP文件人工核对,自动化工具有时候识别不了变形得很厉害的样本。重点关注的可疑特征:文件大小异常小(小于2KB)但包含base64字符串、文件名是乱码或者纯数字、文件位于uploads或cache目录下、文件修改时间是凌晨2到5点之间。
日志归集:把nginx和PHP的日志往日志服务器或者ELK推一份,本地日志可能被攻击者擦除。具体做法是rsyslog配omfwd模块,或者直接用filebeat把日志发到远端Elasticsearch。日志保留至少90天,方便事后追溯。
定期备份核查:除了每天的全站备份,还要每周做一次"差异备份比对"——把本周备份和上周备份做diff,看是否有未授权的代码修改。这一步能在攻击发生7天内发现,避免长期潜伏。
建立监控告警:给关键文件配inotifywait监控——当dede目录、include目录、uploads目录有任何PHP文件被新增或修改时,立即发邮件或微信告警。这能让你在攻击发生几秒内就收到通知,比被动等用户反馈快得多。具体配置是 inotifywait -m -r -e create,modify,delete /www/wwwroot/site --format '%T %w%f %e' --timefmt '%F %T' 输出到日志,再用logrotate配合grep过滤PHP事件发邮件。
同类织梦上传漏洞的统一处置流程
media_add.php这个洞处理完之后,应该顺手把整个织梦后台的上传通道都加固一遍,否则攻击者今天传不进media_add.php,明天可能从album_add.php进来。下面这套清单是保哥用了5年、覆盖织梦5.7 SP1到SP2的完整加固范围。
需要同步加固的关键文件清单:dede/file_manage_control.php(文件管理任意操作)、dede/album_add.php(图集附件)、dede/soft_add.php(软件主体上传)、dede/media_main.php(多媒体管理入口)、include/dialog/select_soft_post.php(编辑器附件选择)、include/dialog/select_images_post.php(编辑器图片选择)、include/uploadsafe.inc.php(上传公共入口)、member/uploads_edit.php(会员中心附件编辑)、include/FCKeditor整个目录(如果启用了FCK编辑器)。
这九个文件配合一起加固,整个上传通道才算闭环。加固方法都是套用前面media_add.php的白名单思路——pathinfo取扩展名、in_array比对白名单、substr_count检测多扩展名、preg_match检测控制字符。每个文件的具体行号会因版本不同有差异,但加固模板是统一的,复制粘贴改文件路径就行。
对于会员中心的上传通道(member/目录下的几个文件),加固标准要更严——因为这个入口面向所有注册会员,不需要后台权限就能访问,攻击面比后台大十倍。会员中心的白名单建议只允许jpg、jpeg、png、gif、bmp这五种图片格式,PDF和压缩包都不要开放。
加固完成后强烈建议跑一次完整的渗透测试——用awvs或者burp对所有上传入口扫描一遍,确认没有漏掉的点。市面上有免费的awvs试用版可以扫一次,不需要采购正版。如果预算允许,找一家专业的安全公司做一次正式渗透测试,费用大约5000到10000元,能给出更详细的报告。
事后复盘要回答的六个核心问题
每次应急处理完,保哥都会跟客户做一次结构化复盘,回答下面六个问题。这六个问题如果不能给出明确答案,说明这次事件还没有真正"结案",可能留有未发现的隐患。
攻击者是怎么进来的——要追溯到第一笔异常请求的时间点、来源IP、利用的具体漏洞。如果只能回答"反正是media_add.php被利用了"但说不清具体哪一次请求是入口,说明日志保留不全或者还有其他更早的入口没被发现。这种情况下要把日志倒查范围从默认的90天扩大到180天或更长,必要时调取CDN层和云服务商防火墙日志补全证据链。
攻击者拿到什么权限——是只能上传webshell在web用户权限下执行,还是已经通过本地提权拿到root。这个问题决定了清理的彻底程度——如果还在web用户权限,清掉webshell和加固代码就够了。如果已经root,那要重装系统才能彻底清干净。判定方法是看 /etc/passwd /etc/shadow 的修改时间、查 auth.log 里的su和sudo记录、看根目录下是否有可疑的反弹shell脚本。
数据是否泄漏——dede_admin表的密码hash是否被读取、用户表里的手机号邮箱是否被导走、订单表的支付信息是否被访问。如果有数据泄漏,按法律要求72小时内要向监管部门报告并通知用户。判定数据是否被读取要看MySQL的general_log或者binlog,没开启的话只能从nginx日志里的请求大小推测——如果某个时间点对某个表查询接口的响应包大小异常大,多半就是被批量导出了。
有没有横向扩散——同一个云服务商账号下的其他主机是否被波及、内网其他业务系统是否被访问、备份系统是否被加密。这一步常常被忽略,但很多大型勒索事件就是从一个看似不重要的小站点横向扩散到核心业务系统。判定方法是看其他主机的last登录记录、检查 .ssh/authorized_keys 是否被植入未知公钥、扫描内网开放端口看是否有异常监听。
攻击窗口期有多长——从首次入侵到完全清理用了多少天。这个数字越大,攻击者获得的横向扩散时间就越多,潜在影响面就越广。理想的窗口期是24小时以内,超过7天就算重大事件。窗口期长的常见原因是:监控告警没配、运维人手不足、定期巡检流于形式。复盘时要把这些根本原因写进改进计划,不能只修了漏洞就完事。
同类站是否还在受同样威胁——如果你管理的不止一个织梦站,要确认其他站是否也存在media_add.php漏洞、是否已被同一攻击者扫描过。这个排查能预防"修了一个站、其他站接着被打"的连环事件。具体做法是把这次的攻击者IP、UA特征、URL访问模式整理成IOC列表,到其他所有站的日志里搜一遍,命中就立即处置。
常见问题解答
业务确实需要让用户传PHP类的源码包怎么办
保哥的建议是让用户传zip或tar.gz格式,里面再装PHP文件。落地到服务器之后,源码包目录配合上面nginx那条规则,PHP永远不会被执行——它只是一个二进制文件躺在那儿,要看内容只能下载。这样既满足业务又不破坏安全边界。如果要让用户在线预览源码内容,可以解压到一个独立目录后用highlight_file函数渲染成HTML显示,渲染过程不会执行任何代码。绝对不要直接include用户上传的文件,那是另一种文件包含漏洞。
白名单里要不要加图片格式
这个文件media_add.php本身是织梦定义的"软件附件"入口,图片应该走media_add.php的图片分支或者专门的图集模块。混着让一个入口什么都收,反而增加攻击面。保哥的建议是不同mediatype走不同白名单,代码里用switch分一下就好。具体来说mediatype=1是图片走图片白名单(jpg/png/gif/webp)、mediatype=2是Flash走swf白名单(已经基本不用)、mediatype=3是软件走压缩包和文档白名单。每种类型独立维护白名单,互不干扰。
改完之后后台上传一直失败提示附件类型不在允许列表内但文件明明是zip的
保哥踩过这个坑。pathinfo在某些PHP版本下对.tar.gz这种双扩展只会返回gz,是OK的。但如果文件名带中文又是GBK编码的旧站点,pathinfo在某些环境会取不到扩展名。解决办法是上传前用mb_convert_encoding把文件名统一成UTF-8再处理。具体代码是 $_filename = mb_convert_encoding(trim($filename), 'UTF-8', 'GBK,UTF-8'); 然后再走pathinfo取扩展名。这样就能正确处理GBK编码的中文文件名。如果你的织梦站本身是GBK编码,整个站都没法直接走UTF-8,那就用iconv转码后处理,处理完再转回GBK写库。
还有哪些织梦后台脚本和这个洞类似必须同步加固
保哥的清单:dede/file_manage_control.php文件管理任意操作、dede/album_add.php图集附件、dede/soft_add.php软件主体上传、include/dialog/select_soft_post.php编辑器附件选择、include/uploadsafe.inc.php上传公共入口。这五个文件配合一起加固,整个上传通道才算闭环。如果启用了member模块,还要加固member/uploads_edit.php和member/album_add.php这两个会员侧入口。FCKeditor如果启用了也要加固include/FCKeditor/editor/filemanager/connectors/php/connector.php这个老牌漏洞文件。
nginx禁止uploads执行PHP后织梦自带的图片处理脚本会不会受影响
不会。织梦的图片处理脚本(比如dede/dialog/select_images.php、include/uploadsafe.inc.php)都是放在dede或include目录下,不在uploads目录里。uploads目录只存放最终落地的用户上传文件,本来就不应该有任何PHP代码。如果你发现uploads目录下有正常的PHP文件被禁止访问,那本身就是异常情况,要排查这些PHP是怎么进去的。一个常见误判是某些织梦插件会在uploads下创建.htaccess之外的配置文件,那些不是PHP文件,不会被这条规则影响。
白名单加固之后会被某些自动化扫描工具误报为漏洞吗
会的概率很低。常见的扫描工具(awvs、appscan、xray)扫描上传漏洞的判定逻辑是"尝试上传一个PHP文件并看是否能在web上访问到"。我们的白名单加固直接拒绝了PHP上传,所以扫描工具会判定为"不存在上传漏洞"。如果你用的是织梦专用扫描工具(dedebbs、织梦漏洞扫描器),它们可能会通过版本号或者文件特征字符串识别"这个站是织梦",然后报"该版本存在已知漏洞"。这种误报跟实际防御效果无关——你已经修了,扫描工具只是不知道你修了。可以忽略这种基于版本号的告警。
加固后能不能彻底放弃升级织梦改用其他CMS
保哥的真实建议是——能换就换。织梦从2017年官方停止维护后,所有新发现的漏洞都没有官方补丁,只能靠社区或者你自己手动修。每修一个洞都是临时的,下一个洞可能在三个月后又出来。如果你的站是企业站、内容更新不频繁,迁移到WordPress或Typecho是更可持续的选择。如果是大站、有大量自定义二次开发,迁移成本可能比持续加固还高,那就只能继续走"白名单加固+纵深防御+监控告警"这三件套。无论选哪条路,都不要继续用默认配置裸跑,那是给所有自动化攻击者送大礼。
本文标题:《织梦media_add.php上传漏洞:白名单加固7层方案》
版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0