上一篇写了readObject的简单利用,这篇写一下RMI/JNDI这些java特有的特性以及JNDI注入。有一些java反序列化漏洞主要就是利用JNDI注入,反序列化只是个触发点。
RMI(Java Remote Method Invocation)是Java里一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。简单来说这个功能就是调用其他机器上的java程序,比如客户端C要调用服务端S上的java代码,RMI作为注册中心连接两者。打个比方就是C要买S卖的东西,但S需要先在RMI那里注册(类似淘宝开店),然后C向RMI(淘宝)查询S,就这么简单。
RMI技术是让客户端和服务端在不同jvm上执行java代码。比如客户端只有一个对象接口而没有具体的实现,需要调用服务端的对象。这里有两种传递对象的方法:
1、传递远程对象(对象实现Remote接口),得到的是一个带着ip地址和内存地址的指针。两个JVM拥有同一个对象。
2、传递序列化对象(对象实现serializable接口),得到的服务端对象的副本,两个JVM中的对象不同。
来看一个例子,尝试用RMI实现一个买东西问价的功能,首先在client端和server端各定义一个相同的shop类接口,为了远程传递这个接口要实现Remote接口
1 | import java.rmi.*; |
然后在server端实现这个接口,远程对象实现时需要继承UnicastRemoteObject这个类。这个实现类的功能就是用hashmap实现很简单的查询。
1 | import java.rmi.RemoteException; |
然后创建rmi服务并且注册
1 | import java.rmi.Naming; |
接下来就是客户端连接这个rmi中心获取内容
1 | import javax.naming.NamingException; |
调用过程很清晰,就几行,这就是rmi的作用,类似于socket。RMI其实是一个基于序列化的java远程方法调用机制,所以如果服务端存在漏洞组件版本,是可能被利用导致RCE的。但并不是很常见的攻击手段。常用手法是把rmi和jndi注入结合。
接下来介绍下JNDI,JNDI(Java Naming and DirectoryInterface)是一个用来查找资源的技术。通过将名字和对象绑定,可以通过名字检索指定的对象。JNDI一般用来代替jdbc连数据库,它的好处是复用性更高,更加灵活(一般来说灵活就意味着不安全),比如我们把上面的代码从纯RMI版本改写为JNDI版本:
1 | //server |
虽然看上去没有太大区别,只是使用的从Naming类换成了javax.naming类。但JNDI的灵活性在于,可以切换不同的协议(rmi、ldap、corba),并且除了直接绑定对象之外,还可以通过References类来绑定一个外部的远程对象。
JNDI注入实际上就是在客户端的InitialContext.lookup(URI)这里发生,如果URL是可控的,攻击者可以控制URI参数为恶意的RMI服务地址,如:rmi://hacker_rmi_server//name;
攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类,这样
目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例,达到RCE的效果。
这也就解释了为什么RCE却不是在RMI服务端发生的,实际上服务端也执行了代码,但只返回了Reference,具体的代码是在客户端执行的。实际上受害者是RMI客户端,攻击者是RMI服务端。
可以看这个图方便理解
某种意义上和ssrf的302跳转有点像
把上面的服务端代码改成jndi注入的版本,需要在jdk<8u121下才能正常运行。
1 | //恶意class |
RMI客户端(被攻击的服务器)运行后弹出计算器。这是RMI客户端绑定地址可控导致的,当然如果是RMI服务端的reference参数可控也是一样的,相当于正常的服务端就变成恶意服务端了。
接下来看下反序列化和JNDI的结合,一个实际的漏洞,Spring的反序列化漏洞,参考https://zerothoughts.tumblr.com/post/137769010389/fun-with-jndi-remote-code-injection
获取作者的源码后,切到client和server目录下,分别执行
1 | #server |
服务端会执行ExportObject中的代码
简单分析下调用过程,服务端实际上只是用socket获取了一个object,然后调用了它的readObject方法,做了一次反序列化。
1 | public class ExploitableServer { |
然后是客户端,其实和上面的过程差不多,我写了下注释
1 | public class ExploitClient { |
该漏洞发生在 org.springframework.transaction.jta.JtaTransactionManager类中,直接看这个类的readObject方法
1 | private void readObject(ObjectInputStream ois) |
再看看initUserTransactionAndTransactionManager这个方法
1 | protected void initUserTransactionAndTransactionManager() |
看到了lookup,参数是this.userTransactionName,也就是我们通过setUserTransactionName传入的恶意类地址,再跟进lookupUserTransaction这个函数
1 | protected TransactionManager lookupTransactionManager(String transactionManagerName) |
这里获取到了一个JndiTemplate,然后调用了它的lookup方法,参数还是我们传入的地址,也就是这里存在JNDI注入。
但是前面的利用都有个前提,就是jdk版本限制在JDK 6u141、7u131、8u121之前,那么对于新一点的jdk,还可以用ldap代替rmi,这种方法可以适用到JDK 11.0.1、8u191、7u201、6u211,看代码:
1 | //server |
然后是被攻击的ldap客户端,写法和rmi一样
1 | import javax.naming.Context; |
需要准备一个unboundid-ldapsdk.jar
1 | javac -cp unboundid-ldapsdk.jar LdapServer.java |
本地版本8u121,运行客户端后后弹计算器
这个服务端的写法其实是固定的,也就没有去研究,可以用现成的工具https://github.com/welk1n/JNDI-Injection-Exploit
至于JDK 6u141、7u131、8u121之后的版本,限制了ldap调用reference,暂时没有通用解决方法,只能从受害机本机上寻找利用链,比如利用CommonsCollections漏洞等。
参考链接
https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf
https://www.freebuf.com/column/189835.html
https://www.mi1k7ea.com/2019/09/15/%E6%B5%85%E6%9E%90JNDI%E6%B3%A8%E5%85%A5
https://zerothoughts.tumblr.com/post/137769010389/fun-with-jndi-remote-code-injection
https://www.aqniu.com/threat-alert/13382.html