影响版本:
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.git
mvn clean package -DskipTests
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.XStream ImageIO <command>