s2-048 漏洞分析

影响版本:

本页目录
  1. 0x01 又见踩坑
  2. struts.xml
  3. 0x02 分析
  4. 跟进分析
  5. 流程梳理
  6. 0x03 利用条件
  7. 0x04 利用方式
  8. 参考资料

影响版本:

Struts 2.1.2 - Struts 2.3.33
Struts 2.5 - Struts 2.5.12

漏洞产生原因主要在于对于xml文件的反序列化

0x01 又见踩坑

终于不是部署时候的坑了,这回的坑在于了解struts.xml中的配置信息

  1. 此漏洞在于rest插件,所以部署rest-showcase即可

几个小tips:

  1. marshal: 从非文本格式到文本格式的数据转化,如java对象->xml
  2. unmarshal: marshal的逆过程,将xml->java对象

struts.xml

  1. 项目中的struts.xml,注意到这里的rest-default

    <struts>
    ...
    <package name="rest-showcase" extends="rest-default">
    <global-allowed-methods>index,show,create,update,destroy,deleteConfirm,edit,editNew</global-allowed-methods>
    </package>
    </struts>
  2. 继承的rest-default配置位于struts2-rest-plugin-2.5.8.jar!/struts-plugin.xml

    s2-048 漏洞分析 · 图示

    其中关键的配置信息如下,需要理清其中的调用关系

    <struts>
    <bean type="com.opensymphony.xwork2.ActionProxyFactory" name="rest" class="org.apache.struts2.rest.RestActionProxyFactory" />
    <bean type="org.apache.struts2.dispatcher.mapper.ActionMapper" name="rest" class="org.apache.struts2.rest.RestActionMapper" />
    <bean type="org.apache.struts2.rest.ContentTypeHandlerManager" class="org.apache.struts2.rest.DefaultContentTypeHandlerManager" />
    <bean type="org.apache.struts2.rest.handler.ContentTypeHandler" name="xml" class="org.apache.struts2.rest.handler.XStreamHandler" /> <!-- 5 -->
<constant name="struts.actionProxyFactory" value="rest" />
<constant name="struts.mapper.class" value="rest" />
<constant name="struts.mapper.idParameterName" value="id" />
<constant name="struts.action.extension" value="xhtml,,xml,json" />
<package name="rest-default" extends="struts-default"> <!-- 1 -->
<interceptors>
<interceptor name="rest" class="org.apache.struts2.rest.ContentTypeInterceptor"/> <!-- 4 -->
<interceptor name="restWorkflow" class="org.apache.struts2.rest.RestWorkflowInterceptor"/>
<interceptor name="messages" class="org.apache.struts2.interceptor.MessageStoreInterceptor" />
<interceptor-stack name="restDefaultStack"> <!-- 3 -->
<interceptor-ref name="rest" />
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="restDefaultStack"/> <!-- 2 -->
<default-class-ref class="org.apache.struts2.rest.RestActionSupport"/>
<global-allowed-methods>execute,input,back,cancel,browse,save,delete,list,index,show,create,update,destroy,edit,editNew</global-allowed-methods>
</package>
</struts>
```

0x02 分析

跟进分析

i. 数据解析

注意发包时候content-type修改为application/xml

  1. 通过上面的配置信息,关键在于会调用rest拦截器,通过对传入的数据包的content-type进行分析,然后使用对用的类进行内容解析

  2. 首先进入拦截器org.apache.struts2.rest.ContentTypeInterceptor,在第一个断点通过解析content-type然后返相应handler,并在第二个断点进行转化

    s2-048 漏洞分析 · 图示

  3. toObject将Reader对象中的内容转换成target对象

    s2-048 漏洞分析 · 图示

  4. 到达start开始解析xml内容,type对应顶层标签,即java.util.Map,然后进入convertAnother

    s2-048 漏洞分析 · 图示

  5. convertAnother,第一个断点查找该类对应的具体实现类(跟入后会调用defaultImplementationOf,很奇怪,官方文档看不到对这个的详细解释),将java.util.Map转为java.util.HashMap类,然后在第二个断点去找相应的转化器为MapConverter

    s2-048 漏洞分析 · 图示

  6. 然后进入转化器的unmarshal方法进行内容解析

    s2-048 漏洞分析 · 图示

  7. 之后利用populateMap真正开始解析内容,期间会调用putCurrentEntryIntoMap再调用readItem读取键值,readItem内调用上面的convertAnother,实现深层遍历。

    protected void populateMap(HierarchicalStreamReader reader, UnmarshallingContext context, Map map, Map target) {
    while(reader.hasMoreChildren()) {
    reader.moveDown();
    this.putCurrentEntryIntoMap(reader, context, map, target);
    reader.moveUp();
    }
    }
    protected void putCurrentEntryIntoMap(HierarchicalStreamReader reader, UnmarshallingContext context, Map map, Map target) {
    reader.moveDown();
    Object key = this.readItem(reader, context, map);
    reader.moveUp();
    reader.moveDown();
    Object value = this.readItem(reader, context, map);
    reader.moveUp();
    target.put(key, value);
    }
    protected Object readItem(HierarchicalStreamReader reader, UnmarshallingContext context, Object current) {
    Class type = HierarchicalStreams.readClassType(reader, this.mapper());
    return context.convertAnother(current, type);
    }
  8. 当解析完第一个entry后,key会转换成NativeString对象,该对象的value字段为Base64Data对象,接着调用put方法将键值放入HashMap中。

    key对象的转换过程只是一个填充对象字段的过程,不涉及命令执行。

    s2-048 漏洞分析 · 图示

ii. 命令执行

  1. HashMap中的put方法会对key进行hash运算

    /**
    * Associates the specified value with the specified key in this map.
    * If the map previously contained a mapping for the key, the old
    * value is replaced.
    *
    * @param key key with which the specified value is to be associated
    * @param value value to be associated with the specified key
    * @return the previous value associated with <tt>key</tt>, or
    * <tt>null</tt> if there was no mapping for <tt>key</tt>.
    * (A <tt>null</tt> return can also indicate that the map
    * previously associated <tt>null</tt> with <tt>key</tt>.)
    */
    public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
    }

    hash运算会调用key.hashCode()方法,即NativeString.hashCode()

    static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  2. NativeString.hashCode方法会调用getStringValue方法获取key对应value的字符串值

    public int hashCode() {
    return this.getStringValue().hashCode();
    }
    private String getStringValue() {
    return this.value instanceof String ? (String)this.value : this.value.toString();
    }
  3. 由于value是Base64Data类型,调用Base64Data.toString()转换成String对象,其通过get()方法获取数据内容

    public String toString() {
    this.get();
    return DatatypeConverterImpl._printBase64Binary(this.data, 0, this.dataLen);
    }
    public byte[] get() {
    if (this.data == null) {
    try {
    ByteArrayOutputStreamEx baos = new ByteArrayOutputStreamEx(1024);
    InputStream is = this.dataHandler.getDataSource().getInputStream();
    baos.readFrom(is);
    is.close();
    this.data = baos.getBuffer();
    this.dataLen = baos.size();
    } catch (IOException var3) {
    this.dataLen = 0;
    }
    }
    return this.data;
    }
  4. 执行ByteArrayOutputStreamEx.readFrom()方法,其内会调用is.read()

    public void readFrom(InputStream is) throws IOException {
    while(true) {
    if (this.count == this.buf.length) {
    byte[] data = new byte[this.buf.length * 2];
    System.arraycopy(this.buf, 0, data, 0, this.buf.length);
    this.buf = data;
    }
    int sz = is.read(this.buf, this.count, this.buf.length - this.count);
    if (sz < 0) {
    return;
    }
    this.count += sz;
    }
    }
  5. is为CipherInputStream类,所以is.readFrom()实际为CipherInputSream.readFrom()

    public void readFrom(InputStream is) throws IOException {
    while(true) {
    if (this.count == this.buf.length) {
    byte[] data = new byte[this.buf.length * 2];
    System.arraycopy(this.buf, 0, data, 0, this.buf.length);
    this.buf = data;
    }
    int sz = is.read(this.buf, this.count, this.buf.length - this.count);
    if (sz < 0) {
    return;
    }
    this.count += sz;
    }
    }
  6. 因为ostart == ofinish == 0所以调用getMoreData()

    s2-048 漏洞分析 · 图示

    public int read(byte[] var1, int var2, int var3) throws IOException {
    int var4;
    if (this.ostart >= this.ofinish) {
    for(var4 = 0; var4 == 0; var4 = this.getMoreData()) {
    ;
    }
    if (var4 == -1) {
    return -1;
    }
    }
    ...
  7. 进入getMoreData后,在上面可见this.done为false,进入this.input.read(),而ibuffer实际长度为0的byte数组,所以返回为0进入else,这里调用cipher.update()方法

    private int getMoreData() throws IOException {
    if (this.done) {
    return -1;
    } else {
    int var1 = this.input.read(this.ibuffer);
    if (var1 == -1) {
    ...
    } else {
    try {
    this.obuffer = this.cipher.update(this.ibuffer, 0, var1);
    } catch (IllegalStateException var4) {
    this.obuffer = null;
    throw var4;
    }
    this.ostart = 0;
    if (this.obuffer == null) {
    this.ofinish = 0;
    } else {
    this.ofinish = this.obuffer.length;
    }
    return this.ofinish;
    }
    }
    }
  8. 由于传入的参数分别为byte[0]、0、0,所以通过if判断,调用chooseFirstProvider()

    public final byte[] update(byte[] var1, int var2, int var3) {
    this.checkCipherState();
    if (var1 != null && var2 >= 0 && var3 <= var1.length - var2 && var3 >= 0) {
    this.chooseFirstProvider();
    return var3 == 0 ? null : this.spi.engineUpdate(var1, var2, var3);
    } else {
    throw new IllegalArgumentException("Bad arguments");
    }
    }
  9. 进入后,经过一系列判断,最后调用serviceIterator.next()方法。(P.S. 这里有些奇怪,如果在前面某些地方打了断点后,servicelterator.next就会为null,导致无法进入方法)

    s2-048 漏洞分析 · 图示

  10. 因为next不为空,调用advance()

    public T next() {
    if (next == null) {
    throw new NoSuchElementException();
    }
    T o = next;
    advance();
    return o;
    }
  11. advance会调用filter

    private void advance() {
    while (iter.hasNext()) {
    T elt = iter.next();
    if (filter.filter(elt)) {
    next = elt;
    return;
    }
    }
    next = null;
    }
  12. 在这里method为java.lang.ProcessBuilder.start(),elt为ProcessBuilder对象,因此method.invoke(elt)相当于 ProcessBuilder.start()调用,实现命令执行

    public boolean filter(Object elt) {
    try {
    return contains((String[])method.invoke(elt), name);
    } catch (Exception e) {
    return false;
    }
    }

流程梳理

吐槽:实在不想写流程了,想的都想吐,佩服挖洞的人,有空再自己写,先放上未然实验室的

toObject, XStreamHandler
fromXML, XStream
...
start, TreeUnmarshaller // 真正开始解析XML,识别类并转化成对象
...
unmarshal, MapConverter // 开始解析顶层的Map对象
populateMap, MapConverter
putCurrentEntryInfoMap, MapConverter // 解析第一对Entry,即<key, value>结构
key = readItem // 生成jdk.nashorn.internal.objects.NativeString对象
readClassType // 读取key的类型,即jdk.nashorn.internal.objects.NativeString
ConvertAnother // 递归解析对象
.....
value = readItem
put(key, value), HashMap // 将解析的key,value对象添加到HashMap中
putVal, HashMap
hash(key), HashMap // 对key计算hash
key.hashCode, NativeString
getStringValue, NativeString
toString, Base64Data //调用value的toString方法
get, Base64Data
readFrom, ByteArrayOutputStreamEx
read, CipherInputStream
getMoreData, CipherInputStream
update, NullCipher
chooseFirstProvider, NullCipher
next, FilterIterator
advance, FilterIterator
filter, FilterIterator
method.invoke // ProcessBuilder.start()

0x03 利用条件

  1. 使用漏洞版本rest
  2. 允许解析xml

0x04 利用方式

吐槽:我感觉自己分析构造payload会累死Orz

payload的生成:

Terminal window
git clone https://github.com/mbechler/marshalsec.git
mvn clean package -DskipTests
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.XStream ImageIO <command>

参考资料

  1. http://www.freebuf.com/vuls/147170.html
  2. https://kingx.me/Struts2-S2-052.html
  3. https://yq.aliyun.com/articles/197926