介绍
之前学习过,jndi是个接口,通过这个接口,可以调用rmi或ldap。所以这次主要学习rmi与ldap的利用,通过分析jndi注入工具来学习,工具就选github有1.5k star的JNDI-Injection-Exploit
源码分析
源码结构如图
ServerStart为入口点,在这里设置了rmi、ldap、jetty的监听端口,待执行命令,并为它们仨分别创建线程并启动。
jettyServer
jettyServer使用jetty启动了一个web服务,用来下载ExecTemplateJDK7.class和ExecTemplateJDK8.class(就是一个简单的文件服务器)。但是它在返回ExecTemplateJDK7/8之前会插入命令。
如图
怎么将命令写入的?如图
这里用了ClassReader、ClassWriter、ClassVisitor三个类,它们都属于ASM 库,可以用来操作字节码
介绍如图
ClassVisitor 是一个抽象类,如图,以下方法交给子类实现
所以这段代码就是
创建一个reader读class
创建一个writer,传给了TransformClass,生成ClassVisitor对象
cr.accept(cv),从上面图片中的介绍可以知道,ClassReader接收一个ClassVisitor实现类,按顺序执行ClassVisitor中的方法
看一下TransformClass方法,如图,是一个ClassVisitor实现类,实现了visistMethod方法(上面图片红框有介绍)
visistMethod方法将命令传给了MethodVisitor。如图,看来就是在构造Rumtime.exec
所以,最后返回的字节码,只要被扫描类方法,就会命令执行。把生成的字节码反编译一下,如图。一番操作猛如虎,原来就是加了个静态代码块(动态生成代码太不容易了)
LDAPServer
ldapServer使用InMemoryDirectoryServer启动了一个ldap服务端。
如图,先创建个配置文件,然后设置配置文件,再用配置文件创建InMemoryDirectoryServer对象,再启动监听。
上面这些基本上是固定格式了,需要自定义的就是OperationInterceptor类。OperationInterceptor类是拦截器,可以拦截ldap通信,继承于InMemoryOperationInterceptor。
如图,ldap查询结果会传给processSearchResult处理,在sendResult方法中,获取到的javaFactory,就是template目录下的class文件,最后将这些信息写入到了result中。
注意这里的javaCodebase,Codebase是用来指定类的装载路径,如果设置为远程地址,则会从远程加载类。
这一步就是收到请求后,根据请求的名字(就是processSearchResult中的base)从Mapper里找出对应class文件,将路径写入Entry中。
客户端收到的返回数据如图,是一个Reference对象。
由于jdk1.8u191之后,不能通过这种方式jndi注入了,所以下面把jdk切换为1.7,调试下命令执行的原因
首先是InitialContext的lookup,创建了Url的Context对象(所有的lookup都要通过Context对象调用,Ctx是Context缩写),继续lookup。
到了ldapUrlContext的lookup,如图,调用了父类lookup。
如图,调用ldapCtx的lookup
如图,继续调用了p_lookup、c_lookup
最终在ldapCtx的c_lookup方法,获取到了查询结果,最终结果是一个Reference对象
后面又进入了DirectoryManager的getObjectInstance方法。refInfo就是上一步查询的Reference对象
然后调用了父类NamingManager的getObjectFactoryFromReference方法。
如图
而在这里,先尝试在本地加载ExecTemplateJDK7,获取不到,又从网络位置获取,第二个红框执行完后,弹出计算器。
从第三个红框可以看出,在加载类成功后,将对象转为ObjectFactiry类型,调用了NewInstance方法,将对象实例化。
再回头看一下getObjectInstance方法,看第二个红框,调用了恶意类的getObjectInstance方法。
所以,恶意类,不仅可以使用静态代码块命令执行,也可以继承ObjectFactory类,重写getObjectInstance方法(后面有例子)。
问题出来了,之前学习加载类时,不是说loadClass时不会对类初始化吗,那为什么loadClass后就会执行命令?
看一下的helper(VersionHelper12类型)的loadClass。
如图,可以看见VersionHelper12类的loadClass是通过Class.forName实现的,所以会初始化类。
总结一下,首先ldap查询结果是一个Reference对象,然后就会视同DirectoryManager的getObjectInstance方法,然后调用NamingManager的getObjectFactoryFromReference方法,导致命令执行。
RMIServer
之前在介绍RMI时已经写过RMIServer了,但是这个工具里,不是通过RMI相关类创建的RMIServer,,是直接通过socket,手工解析的协议。所以这部分代码很多,先想一下如何构造恶意rmi服务。
正常rmi服务,在注册中心注册后,在注册中心保留存根,在调用lookup查询类的时候,获得的是存根对象。
所以,我们要做的是,将这个存根对象改为Referencr对象(在学习ldap时已经知道,如果lookup获取的是Reference对象,就会远程加载并初始化指定类),这样在lookup时,就会导致命令执行。
搭建测试
网上找了份代码,学习下,如图搭建rmi服务端。
恶意类evil如图,将它编译为class,再用python开启一个文件服务器。
客户端如图,运行就打开了计算器。
看来不手工解析rmi的流量协议也可以构造恶意rmi服务端,猜测这个工具这么做事想,更清晰的获取到rmi调用信息。如图,这个是java.rmi.registry.LocateRegistry类不能实现的。
原理分析
重点看evil类。之前ldap部分 分析过,恶意类被加载后会被转为ObjectFactory类型,并调用getObjectInstance方法(本例就用这种方式命令执行)。
如图
下面分析下命令执行过程,调用栈如图。
在decodeObject方法中,获取到了Reference对象,就是var3,然后调用NamingManager.getObjectInstance实例化对象
这里很熟悉,回想一下ldap的命令执行过程:DirectoryManager的getObjectInstance方法 --> NamingManager的getObjectFactoryFromReference方法导致命令执行
看下NamingManager.getObjectInstance方法,如果调用了NamingManager的getObjectFactoryFromReference方法就会导致命令执行
如图,果然
后面的就不分析了,和上面分析ldap的一样
总结
ldap和rmi利用方式类似
rmi利用链是lookup --> ...... --> RegisterContext.decodeObjecct --> NamingManger.getObjectInstance --> NamingManger.getObjectFactoryFromReference
ldap利用链是lookup --> ...... --> DirectoryManager.getObjectInstance -> NamingManger.getObjectFactoryFromReference
可见问题根源就在NamingManger.getObjectFactoryFromReference方法
它们的利用方式都是在查询时,收到一个Reference对象,Reference对象的codebase指向恶意类,客户端加载恶意类,导致命令执行
ldap和rmi在Reference对象处理时,都会使用Class.forName方法加载Reference对象指向的远程.class文件,,而且会将它强转为ObjectFactory类型,并调用getObjectInstance方法
所以构造恶意.class文件时,可以使用静态代码块,也可以继承ObjectFactory,重写getObjectInstance方法
ldap服务搭建,上面演示过
rmi服务搭建,可以直接调用java原生的rmi相关的类搭建,很方便,也可以学习本例工具中的方式,手工解析rmi流量(这种方式更灵活)
不足
可以通过ldap或rmi执行任意代码,但不足之处就是,没有回显。实战中可以写内存马、反弹shell、上线cs等操作,但如果命令执行或代码出问题,但又没回显,会很难排查问题。后面会继续分析,如何写一个带回显的jndi注入工具
这两种方式都属于JNDI Reference 利用,对jdk版本有限制
下面是绕过高版本限制的几种方式,第十五篇 JNDI注入Plus(JNDI注入的绕过方式总结) 暂时搁置
大概有以下几种,通过这些关键字网上就能找到很详细的分析
使用BeanFactory
ldap反序列化触发本地gadget
利用JRMP触发本地gadget