Learning Man's Blog

s2-048 漏洞分析

字数统计: 2k阅读时长: 10 min
2018/10/11

影响版本:

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

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

     <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,并在第二个断点进行转化

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

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

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

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

  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对象的转换过程只是一个填充对象字段的过程,不涉及命令执行。

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()

     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]00,所以通过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,导致无法进入方法)

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

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

    private void advance() {
        while (iter.hasNext()) {
            T elt = iter.next();
            if (filter.filter(elt)) {
                next = elt;
                return;
            }
        }
        next = null;
    }
  1. 在这里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的生成:

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
CATALOG
  1. 1. 0x01 又见踩坑
    1. 1.1. struts.xml
  2. 2. 0x02 分析
    1. 2.1. 跟进分析
      1. 2.1.1. i. 数据解析
      2. 2.1.2. ii. 命令执行
    2. 2.2. 流程梳理
  3. 3. 0x03 利用条件
  4. 4. 0x04 利用方式
  5. 5. 参考资料