从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
中存储的对象建议为如下四种类型:
Referenceable
objectsReference
objectsjava.io.Serializable
objectsDirContext
objects
此外还可以是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