s2-048 漏洞分析
影响版本:
影响版本:
Struts 2.1.2 - Struts 2.3.33
Struts 2.5 - Struts 2.5.12
漏洞产生原因主要在于对于xml文件的反序列化
0x01 又见踩坑
终于不是部署时候的坑了,这回的坑在于了解struts.xml中的配置信息
- 此漏洞在于rest插件,所以部署rest-showcase即可
几个小tips:
- marshal: 从非文本格式到文本格式的数据转化,如java对象->xml
- unmarshal: marshal的逆过程,将xml->java对象
struts.xml
-
项目中的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> -
继承的
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
-
通过上面的配置信息,关键在于会调用
rest拦截器,通过对传入的数据包的content-type进行分析,然后使用对用的类进行内容解析 -
首先进入拦截器
org.apache.struts2.rest.ContentTypeInterceptor,在第一个断点通过解析content-type然后返相应handler,并在第二个断点进行转化
-
toObject将Reader对象中的内容转换成target对象

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

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

-
之后利用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);} -
当解析完第一个entry后,key会转换成NativeString对象,该对象的value字段为Base64Data对象,接着调用put方法将键值放入HashMap中。
key对象的转换过程只是一个填充对象字段的过程,不涉及命令执行。

ii. 命令执行
-
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);} -
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();} -
由于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;} -
执行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;}} -
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;}} -
因为
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;}}... -
进入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;}}} -
由于传入的参数分别为
byte[0]、0、0,所以通过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");}} -
进入后,经过一系列判断,最后调用serviceIterator.next()方法。(P.S. 这里有些奇怪,如果在前面某些地方打了断点后,servicelterator.next就会为null,导致无法进入方法)

-
因为next不为空,调用advance()
public T next() {if (next == null) {throw new NoSuchElementException();}T o = next;advance();return o;} -
advance会调用filter
private void advance() {while (iter.hasNext()) {T elt = iter.next();if (filter.filter(elt)) {next = elt;return;}}next = null;} -
在这里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 利用条件
- 使用漏洞版本rest
- 允许解析xml
0x04 利用方式
吐槽:我感觉自己分析构造payload会累死Orz
payload的生成:
git clone https://github.com/mbechler/marshalsec.gitmvn clean package -DskipTestsjava -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.XStream ImageIO <command>