凌晨两点,漏洞攻击者把一段PHP代码塞进某个上传接口,漏洞文件名是漏洞avatar_20231107.jpg.php。服务器没拦,漏洞因为后缀白名单只检查了最后一个点。漏洞三分钟后,漏洞他通过浏览器访问这个文件,漏洞看到熟悉的漏洞eval执行界面——这台服务器从此不再属于管理员。
漏洞webshell的漏洞本质就这么简单:一个能让你在目标机器上执行命令的网页文件。它不神秘,漏洞也不高大上,漏洞就是漏洞代码审计没做干净的副产品。我见过最离谱的漏洞一次,有人把webshell直接放在网站的漏洞robots.txt里注释掉,扫描器路过八百遍都当它是漏洞普通文本。
写webshell的姿势千奇百怪,但核心逃不出三种。第一种是一句话木马,代码短得可怜,<?php @eval($_POST['x']);?>,二十个字符,够用。第二种是加密混淆型,把代码base64编码再拼接字符串,最后用create_function或者assert执行,专门骗静态扫描。第三种最阴——内存马,不落盘,直接注入到Java的Filter或者Spring的Controller里,重启才消失,日志查不到文件痕迹。
有个客户问过我:你们的安全设备天天报webshell,为什么还是被拿走了数据?我看了下报告,设备报的是cmd.php,而攻击者真正用的是藏在/wp-includes/images/里的2019.jpg,图片头正常,中间夹着一段GIF89a注释,注释里藏着PHP代码。设备只认后缀和文件头,不认内容流。这就像保安只查你口袋里有没有刀,但人家把刀片缝在领带夹里。
检测webshell为什么难?因为正常业务代码本身就是一堆字符串拼接和动态执行,你没法简单粗暴地禁止所有eval。PHP的preg_replace加/e修饰符能执行代码,Java的ScriptEngine.eval()也能跑JS,这些函数在合法业务里照样被用。安全团队只能靠特征库匹配已知样本,遇到改几个变量名的新变种就抓瞎。
更麻烦的是,有些开发框架自己就长得像webshell。ThinkPHP的where方法可以传数组,数组里能带表达式;Laravel的query()允许原始SQL拼接。你要是误判了,把正常业务代码当恶意文件删掉,业务直接崩。误报比漏报还让人头疼,因为你需要花三倍时间证明自己是无辜的。
真正管用的防御手段其实很土。文件上传目录直接禁止执行脚本,这是最有效的——nginx里配location ~* \.(php)$ { deny all; },管你什么马都跑不起来。其次,把写入权限和解析权限分离,上传目录只给写不给执行,这是架构层面的解法。最后才是实时监控,盯着文件改动和进程行为。
我见过最讽刺的案例:某公司花大价钱买了WAF和RASP,但运维为了图省事,把网站根目录整个设为777权限。攻击者上传了webshell后,先chmod 777,再改掉所有文件的属主,最后把日志清了。事后调查时,连系统审计日志都被覆盖了,因为webshell能执行find / -exec rm -rf。设备再贵也架不住人犯懒。
写webshell的人也在进化。现在流行的是用php.ini的auto_prepend_file配合user.ini做持久化,每次请求自动加载恶意代码,文件本身却是个干净的图片。还有利用.htaccess把jpg当php解析的,老招数但依然有效。攻击者不追求代码多精巧,只追求能活过你的扫描周期。
说到底,漏洞webshell是代码和信任之间的裂缝。你信任了用户输入,信任了文件后缀,信任了扫描结果,唯独没信任自己的架构设计。安全不是装几个软件的事,是每一层都要默认不可信。今天堵住了一个上传点,明天可能从日志接口进来。只要代码还在拼接字符串,webshell的土壤就永远存在。
凌晨四点,攻击者已经拿到数据库备份,开始打包下载。他留下的那个webshell还在监听,像一个嵌在网页里的幽灵,等着下一次指令。而你还在睡梦里,以为防火墙开着就天下太平。









