从0开始的JNDI学习
21年12月初,Log4j的JNDI注入一石激起千层浪,由于其使用广泛、漏洞利用简单,很快便“风靡全球”,攻击者们在各种地方尝试着jndi。直到12月的最后一周也在Log4j配置文件RCE的喧嚣中过去,这起事件也算是告一段落了。不难看出,JNDI便是此次事件的“罪魁祸首”,恰逢新年伊始,今日便让我们从0开始了解下何为JNDI以及其攻击原理。
JNDI:起源
JNDI全称是Java Naming and Directory Interface ,单从名称来看,其由Naming与Directory所构成:
命名服务: 名称与实体对象绑定,并可以通过名称查找到对象。例如DNS( Internet Domain Name System ),可以从域名查找到对应的IP。

目录服务:是一种特殊的命名服务,它可以对对象的属性进行操作
所以JNDI就是为应用程序提供命名和目录服务的接口,这里举个例子来实际感受下其作用:
在操作数据库的时候往往需要如下几步:
- 加载数据库驱动程序(
Class.forName("数据库驱动类");) - 连接数据库(
Connection con = DriverManager.getConnection();) - 操作数据库(
PreparedStatement stat = con.prepareStatement(sql);stat.executeQuery();) - 关闭数据库,释放连接(
con.close();)
当需要操作多个数据库时就会显得繁琐,这时就可以配置多个是数据源,其实就是把数据库信息绑定到命名空间中
为每一个数据源命名后就可以通过这个名称来查找数据源
1 | //1、初始化名称查找上下文 |
上例的java:comp/env/是JNDI的节点,可以在其中找到JavaEE的属性,所以只能在Web环境中使用。为了方便调试,这里给出一段更简易的代码
1 | Context ctx = new InitialContext(); |
这里是发起ldap请求,也可以使用rmi,JNDI默认支持的协议有

它会根据schema来识别是什么协议,并实例化对应的类

LDAP
上文中也有提及,JNDI支持LDAP协议,在结构上二者也很相似,而且由于攻击时利用的一些限制,本文主要从LDAP的角度来进行分析。

我们可以使用lookup查找一个在LDAP服务中的对象,此外doLookup也可以达成相似的效果:
1 | ctx.lookup("cn=Rosanna Lee,ou=People"); |

在LDAP中存储的对象建议为如下四种类型:
ReferenceableobjectsReferenceobjectsjava.io.SerializableobjectsDirContextobjects
此外还可以是java.rmi.Remote与CORBA objects,它们都以javaObject的子类形式存储在LDAP中,且一般需要有这些属性
| LDAP Attribute Name | Content |
|---|---|
javaClassName |
类名 |
javaCodebase |
类的class文件所在的位置 |
对于不同的类型也有不同的属性和返回值,例如:
Reference Object
| LDAP Attribute Name | Content |
|---|---|
javaFactory |
要查找的类的名称 |
当LDAP读到该类型时将返回一个JavaCodebase指定地址的,以javaFactory为名的Class文件所实例化的对象。
Serializable Object
| LDAP Attribute Name | Content |
|---|---|
javaSerializedData |
序列化后的对象 |
它将返回一个javaSerializedData反序列化后的对象
到这里其实问题已经很明确了,一方面我们可以通过CodeBase指定恶意地址从而加载恶意类,另一方面可以传入恶意序列化数据,从而任意代码执行。
CodeBase
使用如下代码当成服务端并启动:
1 | package com.longofo; |
本地开启一个Web服务,把弹出计算器的类calc编译后的class文件放置Web目录下后
1 | Context ctx = new InitialContext(); |
在11.0.1、8u191、7u201、6u211 之前版本可以直接弹出计算器,跟进代码到com.sun.jndi.ldap.LdapCtx#doSearch,这里像ldap服务器发送了查询请求,并获取到了查询结果:

在服务端设置的javaCodeBase与javaFactory已经出现了,继续往下跟进至com.sun.jndi.ldap.Obj#decodeObject

这里只有objectClass为javaNamingReference时能进入下一步,这也是服务端如此设置的原因。最后进入到javax.naming.spi.NamingManager#getObjectFactoryFromReference通过CodeBase和factoryName加载恶意类,完成整个利用流程。高版本则是在最后loadClass的时候检测了trustURLCodebase是否为true,其默认为false

DeSerialized
上文中就提到了LDAP的返回对象中有serialized object,所以存在反序列化其实是理所当然的。同样是先来看看查找后的结果

这次只有一个javaClassName以及javaserializeddata很明显的可以看到序列化的数据,下一步便是反序列化执行命令了,关于反序列化暂且填个坑,留待以后补充。
Log4j
回到一开始的Log4j,关于漏洞原理这里不再赘述,直接来到漏洞触发点org.apache.logging.log4j.core.lookup.JndiLookup#lookup

此处的key就是${jndi:xxx}中的xxx的值,直接搭建恶意ldap服务器返回序列化数据即可达成命令执行。
两个没什么用的tips:
1 | ldap://ip:port/urldecode 查询的DN可以使用URL编码,ldap查询前会自动解码 |
参考文献
- https://docs.oracle.com/javase/tutorial/jndi/
- https://paper.seebug.org/1091/
- https://www.cnblogs.com/xdp-gacl/p/3951952.html
- https://stackoverflow.com/questions/11631839/what-is-javacomp-env
- https://www.ietf.org/rfc/rfc2713.txt
- https://evilpan.com/2021/12/13/jndi-injection/#remote-class
- https://www.freebuf.com/company-information/312180.html