Learning Man's Blog

s2-048 漏洞分析

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

影响版本:

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