深入fastjson源码命令执行调试

之前写过一篇fastjson漏洞文章,但是当时在复现利用链的过程中一直没有弹出计算器,而且利用链的代码单步调试也没有给出来,这次我要通过底层代码把漏洞实现过程展现出来。

fastjson漏洞demo

上次不是没有弹出计算器吗,这次我先把可以弹出计算器的漏洞demo先讲解一下。

  • 创建一个普通类Person
import java.util.Properties;

//创建一个普通类
public class Person {
    private String name;
    private int age;
    private String sex;
    private Properties properties;

    public Person() {
        System.out.println("构造方法");
    }

    //Setter Getter方法
    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("getAge");
        return age;
    }

    public String getSex(){
        System.out.println("getAddress");
        return sex;
    }

    public Properties getProperties() throws Exception {
        System.out.println("getProperties");
        Runtime.getRuntime().exec("open -a /System/Applications/Calculator.app");
        return properties;
    }
}
  • 反序列化测试
import com.alibaba.fastjson.JSON;

public class Demo {
    public static void main(String[] args){
        String jsonstring ="{\"@type\":\"com.FastjsonDemo.Person\",\"age\":18,\"name\":\"lili\",\"address\":\"china\",\"properties\":{}}";
        //JSON.parseObject() 方法将 jsonstring 字符串解析并转换为一个通用的 Object 对象
        Object obj = JSON.parseObject(jsonstring, Object.class);
        System.out.println(obj);
    }
}

  • 测试结果

1687937316_649be124cc1a2eef0df3e.png!small

代码调试讲解

  1. 我们在JSON.parseObject位置处打上断点

1687937342_649be13ef0c5cd18a34cb.png!small


  1. f7步入,进入到parseObject(String text, Class clazz)方法中,text为要解析的JSON字符串;clazz为要转换的目标类型,Class中的T为范型参数,表示要转换成的具体类型。在这个方法的实现中,它再次调用自己,通过递归无限循环调用自身。
public static  T parseObject(String text, Class clazz) {
    return parseObject(text, clazz);
}

1687937363_649be1531fc289d742fdc.png!small


  1. f7继续步入,进入parseObject(String json, Class clazz, Feature… features)方法,这里又是一个重载方法,接受了三个参数:一个JSON字符串json、目标类型的Class对象clazz和可选的特性(features)数组。在方法的实现中,它掉用了另一个重载的\’parseObject\’方法,将解析过程委托给它。这个新的重载方法接受了更多的参数,包括:ParserConfig对象、ParseProcess对象以及默认的解析特性(DEFAULT_PARSER_FEATURE)。
public static  T parseObject(String json, Class clazz, Feature... features) {
    return parseObject(json, clazz, ParserConfig.global, (ParseProcess)null, DEFAULT_PARSER_FEATURE, features);
}

1687937388_649be16c9b766b3612ff6.png!small

  1. f7步入到 public static T parseObject(String input, Type clazz, ParserConfig config, ParseProcess processor, int featureValues, Feature… features) {…}方法中

1687937405_649be17d73e3f75a24438.png!small

  1. 我们详细看一下这段代码,目的是将输入的input字符串解析为指定类型 \’clazz\’ 的java对象。
public static  T parseObject(String input, Type clazz, ParserConfig config, ParseProcess processor, int featureValues, Feature... features) {
  	//input为{"@type":"com.FastjsonDemo.Person","age":18,"name":"lili","address":"china","properties":{}}
    if (input == null) {
        return null;
    } else {
      	//features为Feature[0]@497
        if (features != null) {
            Feature[] var6 = features;
          	//定义特性数组长度var7
            int var7 = features.length;

            for(int var8 = 0; var8 < var7;   var8) {
              	//获取当前索引var8位置的特性对象,并将其复制给变量feature
                Feature feature = var6[var8];
              	//使用按位或操作符(|=)将当前特性的掩码(feature.mask)与featureValues进行按位或运算,并将结果重新赋值给featureValues
                featureValues |= feature.mask;
            }
        }
      	//创建一个DefaultJSONParser对象parser,用于解析JSON字符串。这个对象使用传入的输入字符串、配置对象config和特性值featureValues进行初始化。
        DefaultJSONParser parser = new DefaultJSONParser(input, config, featureValues);
      	//处理解析过程中的处理器processor,此时processor为null
        if (processor != null) {
            if (processor instanceof ExtraTypeProvider) {
                parser.getExtraTypeProviders().add((ExtraTypeProvider)processor);
            }

            if (processor instanceof ExtraProcessor) {
                parser.getExtraProcessors().add((ExtraProcessor)processor);
            }

            if (processor instanceof FieldTypeResolver) {
                parser.setFieldTypeResolver((FieldTypeResolver)processor);
            }
        }
      	//解析JSON字符串并得到解析结果对象
        T value = parser.parseObject(clazz, (Object)null);
      	//处理解析结果中的后续任务
        parser.handleResovleTask(value);
      	//关闭解析器
        parser.close();
      	//返回解析结果对象
        return value;
    }
}
  1. 跟着f8步过代码
  • 首先执行循环
for(int var8 = 0; var8 < var7;   var8) {
    Feature feature = var6[var8];
    featureValues |= feature.mask;
}
  • 随后跳转创建DefaultJSONParser对象
DefaultJSONParser parser = new DefaultJSONParser(input, config, featureValues);

1687937426_649be192bdf7d20aea2f2.png!small

  • 继续执行processor的判断,因为processor为空,跳过判断
if (processor != null) {...}
  • 随后进行解析JSON字符串并得到解析结果对象

这里是使用解析器parser对JSON字符串进行解析,并将解析结果赋值给变量value。解析的目标类型由参数clazz指定,该方法返回了一个泛型类型T的对象。

T value = parser.parseObject(clazz, (Object)null);
  • 直接f8跳转下一步,可以发现计算器被弹出来,造成命令执行

1687937451_649be1ab335e64a288968.png!small

  1. 重点调试parser.parseObject(clazz, (Object)null)代码
T value = parser.parseObject(clazz, (Object)null);
  • f7步入代码

1687937468_649be1bcbd81d8fb44dfd.png!small

public  T parseObject(Type type, Object fieldName) {
  	//获取当前JSON的token类型
    int token = this.lexer.token();
    if (token == 8) {//如果当前token是JSON的null值(8代表null)
        this.lexer.nextToken();//跳过null值解析
        return null;
    } else {
        if (token == 4) {//如果当前token是JSON的字符串值(4代表字符串)
            if (type == byte[].class) {//判断期望类型是否为 \'byte[]\'
                byte[] bytes = this.lexer.bytesValue();//如果是,获取字节数组值
                this.lexer.nextToken();//跳过字符串值的解析
                return bytes;//返回字节数组值
            }

            if (type == char[].class) {//判断其我想是否为 \'char[]\'
                String strVal = this.lexer.stringVal();
                this.lexer.nextToken();
                return strVal.toCharArray();//将字符串转换为字符数组并返回
            }
        }
      	//以上都不满足,根据期望类型获取相应的\'ObjectDeserializer\',并使用它来解析
        ObjectDeserializer derializer = this.config.getDeserializer(type);

        try {
            return derializer.deserialze(this, type, fieldName);//通过config(ParserConfig对象)获取相应类型的反序列化器,并调用deserialze方法进行解析。该方法会根据给定的JSON类型和字段名,将当前解析器作为参数,返回解析后的Java对象。
        } catch (JSONException var6) {
            throw var6;
        } catch (Throwable var7) {
            throw new JSONException(var7.getMessage(), var7);
        }
    }
}
  • 初始化token值为12

1687937487_649be1cfdddaa35c28dae.png!small

  • 不满足if循环。会步入ObjectDeserializer derializer = this.config.getDeserializer(type);

1687937506_649be1e228bb9e77e936c.png!small

  1. 分析这段代码实现细节
ObjectDeserializer derializer = this.config.getDeserializer(type);
  • f7步入,这段代码是 \’ParseConfig\’ 类中的 \’getDeserializer(Type type)\’方法,目的是根据给定的类型(Type)获取相应的反序列化器(ObjectDeserializer)。初始 derializers 为默认值IdentityHashMap@659,derializer的值为null。随后进行一个缓存类型的判断:(1)如果类型type属于 \’Class\’ 类型,调用 getDeserializer(Class, Type)方法,传入类型(Class)和类型(Type)进行处理;(2)如果类型(Type)是ParameterizedType类型,获取原始类型(RawType)。随后做了一个三元运算符判断:如果原始类型是Class类型就调用getDeserializer(Class, Type)方法,对传入原始类型(Class)和类型(Type)进行处理;否则就递归调用getDeserializer(Type)方法,对传入原始类型(RawType)进行处理。(3)其他情况就返回默认的反序列化器 \’JavaObjectDeserializer.instance\’。

1687937526_649be1f63d5dfc461fc30.png!small

  • f7继续步入后是一段基于身份比较hash映射的 \’get\’ 方法

1687937545_649be2098a637e5a484fe.png!small

  • 最终获取的derializer值为JavaObjectDeserializer@660

1687937561_649be21924eec7e17bac3.png!small

  • 返回反序列化器具体值:当前对象DefaultJSONParser@542、类型type为Object对象、fieldName字段名为null、config属性值为ParseConfig@540

1687937578_649be22a3181634b60767.png!small

  • 返回值的具体实现可以f7继续步入查看

1687937595_649be23befa5d3db58b9d.png!small

  • 底层是一个JSON解析器的 \’parse\’ 方法,lexer的初始化默认值为JSONScanner@655
public Object parse(Object fieldName) {
    JSONLexer lexer = this.lexer;
    switch(lexer.token()) {
    case 1:
    case 5:
    case 10:
    case 11:
    case 13:
    case 15:
    case 16:
    case 17:
    case 18:
    case 19:
    default:
        throw new JSONException("syntax error, "   lexer.info());
    case 2:
        Number intValue = lexer.integerValue();
        lexer.nextToken();
        return intValue;
    case 3:
        Object value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
        lexer.nextToken();
        return value;
    case 4:
        String stringLiteral = lexer.stringVal();
        lexer.nextToken(16);
        if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
            JSONScanner iso8601Lexer = new JSONScanner(stringLiteral);

            try {
                if (iso8601Lexer.scanISO8601DateIfMatch()) {
                    Date var11 = iso8601Lexer.getCalendar().getTime();
                    return var11;
                }
            } finally {
                iso8601Lexer.close();
            }
        }

        return stringLiteral;
    case 6:
        lexer.nextToken();
        return Boolean.TRUE;
    case 7:
        lexer.nextToken();
        return Boolean.FALSE;
    case 8:
        lexer.nextToken();
        return null;
    case 9:
        lexer.nextToken(18);
        if (lexer.token() != 18) {
            throw new JSONException("syntax error");
        }

        lexer.nextToken(10);
        this.accept(10);
        long time = lexer.integerValue().longValue();
        this.accept(2);
        this.accept(11);
        return new Date(time);
    case 12:
        JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
        return this.parseObject((Map)object, fieldName);
    case 14:
        JSONArray array = new JSONArray();
        this.parseArray((Collection)array, (Object)fieldName);
        if (lexer.isEnabled(Feature.UseObjectArray)) {
            return array.toArray();
        }

        return array;
    case 20:
        if (lexer.isBlankInput()) {
            return null;
        }

        throw new JSONException("unterminated json string, "   lexer.info());
    case 21:
        lexer.nextToken();
        HashSet set = new HashSet();
        this.parseArray((Collection)set, (Object)fieldName);
        return set;
    case 22:
        lexer.nextToken();
        TreeSet treeSet = new TreeSet();
        this.parseArray((Collection)treeSet, (Object)fieldName);
        return treeSet;
    case 23:
        lexer.nextToken();
        return null;
    }
}
  • f8步过,进入到case12。这里首先创建了一个新的JSONObject对象,并启用了Feature.OrderedField特性(这个特性是用于指示是否保留JSON对象中字段的顺序);随后调用了当前对象的parseObject方法,将刚刚创建的object对象作为参数传递进去,并传递fileName作为另一个参数。
case 12:
    JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
    return this.parseObject((Map)object, fieldName);

1687937621_649be2557609d8bd6b304.png!small

  • 继续f7步入,查看parseObject反序列化的具体实现

1687937638_649be2661f477f3ae3cd3.png!small

  • 底层对key做了个判断

1687937659_649be27b4ae64330f43f7.png!small

  • 继续步过可以在底层发现从当前对象的配置中获取一个用于指定类 \'clazz\' 的反序列化器,这个clazz就是我们要反序列化的Person类。
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);

1687937707_649be2ab4ca2056ff197f.png!small

  1. 回到getDeserializer代码的实现位置,这里我们重新带入要反序列化的type(Person类)

1687937723_649be2bbb78f3011e7864.png!small

  • f8步过,依然是要从当前对象的配置中获取与指定类 \'clazz\'相关联的反序列化器 ObjectDeserializer

1687937739_649be2cbbdacbf8317390.png!small

  • f8步过查看 thisObj = deserializer.deserialze(this, clazz, fieldName); 的具体实现
public  T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
        return this.deserialze(parser, type, fieldName, 0);
    }

public  T deserialze(DefaultJSONParser parser, Type type, Object fieldName, int features) {
        return this.deserialze(parser, type, fieldName, (Object)null, features);
    }
  • 相当于对Person类做一遍和Object类一样的调试,这里的token值为16

1687937757_649be2ddc1bba19ba32ea.png!small

  • f8步过token=16的循环体

1687937783_649be2f7307263c3af1de.png!small

  • 继续步过走到自增fieldIndex

1687937809_649be311a31ab9eaef38f.png!small

  • 通过多次递增,直到fileIndex为5

1687937832_649be32871d8efdcca30f.png!small

1687937864_649be3489d454e94368c4.png!small

......

1687937882_649be35a80d859b7490fc.png!small

  • 再看这段代码,判断当前解析操作是否成功,只要解析成功即可弹出计算器
boolean match = this.parseField(parser, key, object, type, fieldValues);
  • 仔细查看代码的实现过程
public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType, Map<String, Object> fieldValues) {
    //创建了一个JSONLexer对象,并将其初始化为 \'parser\'对象的 \'lexer\'属性
		JSONLexer lexer = parser.lexer;
    //smartMatch方法用于根据提供的字段名key匹配对应的字段解析器
		FieldDeserializer fieldDeserializer = this.smartMatch(key);
  	//创建一个mask的整型变量,并设置为 \'Feature.SupportNonPublicField.mask\'的值
    int mask = Feature.SupportNonPublicField.mask;
    if (fieldDeserializer == null && (parser.lexer.isEnabled(mask) || (this.beanInfo.parserFeatures & mask) != 0)) {
        if (this.extraFieldDeserializers == null) {
            ConcurrentHashMap extraFieldDeserializers = new ConcurrentHashMap(1, 0.75F, 1);
            Field[] fields = this.clazz.getDeclaredFields();
            Field[] var11 = fields;
            int var12 = fields.length;
          	//遍历当前类声明的字段,对每个字段进行一下操作:(1)获取字段名并检查是否已存在对应的字段解析器;(2)检查字段的修饰符,判断是否为非公共字段和非静态字段;(3)如果满足条件,则将字段添加到extraFieldDeserializers中。
            for(int var13 = 0; var13 < var12;   var13) {
                Field field = var11[var13];
                String fieldName = field.getName();
                if (this.getFieldDeserializer(fieldName) == null) {
                    int fieldModifiers = field.getModifiers();
                    if ((fieldModifiers & 16) == 0 && (fieldModifiers & 8) == 0) {
                        extraFieldDeserializers.put(fieldName, field);
                    }
                }
            }

            this.extraFieldDeserializers = extraFieldDeserializers;
        }

        Object deserOrField = this.extraFieldDeserializers.get(key);
        if (deserOrField != null) {
            if (deserOrField instanceof FieldDeserializer) {
                fieldDeserializer = (FieldDeserializer)deserOrField;
            } else {
                Field field = (Field)deserOrField;
                field.setAccessible(true);
                FieldInfo fieldInfo = new FieldInfo(key, field.getDeclaringClass(), field.getType(), field.getGenericType(), field, 0, 0, 0);
                fieldDeserializer = new DefaultFieldDeserializer(parser.getConfig(), this.clazz, fieldInfo);
                this.extraFieldDeserializers.put(key, fieldDeserializer);
            }
        }
    }
  	//如果fieldDeserializer为空,继续判断:
    if (fieldDeserializer == null) {//如果lexer未启用Feature.IgnoreNotMatch,抛出异常
        if (!lexer.isEnabled(Feature.IgnoreNotMatch)) {
            throw new JSONException("setter not found, class "   this.clazz.getName()   ", property "   key);
        } else {//否则调用\'parser.parseExtra(object,key)\'方法,将额外的字段解析给 object
            parser.parseExtra(object, key);
            return false;//false表示未成功解析字段
        }
    } else {//filedDeserializer不为空,通过下列方法将解析器移到字段值的冒号位置,并解析字段值
        lexer.nextTokenWithColon(((FieldDeserializer)fieldDeserializer).getFastMatchToken());
        ((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues);
        return true;//返回true表示成功解析
    }
}
  • 此时fieldDeserializer不为空

1687937908_649be374d92b4791d8cc9.png!small

  • 跟进 ((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues); 最后使用字段值的反序列化器从解析器中解析字段的值

1687937927_649be3878876a960d1fb7.png!small

  • 继续步过,将解析得到的字段值 value 设置到目标对象 object 的对应字段上

1687937955_649be3a3ca8f30bead06d.png!small

  • 跟进setValue,可以清晰的看见通过反射调用了Person类的getProperties() throws Exception

1687937974_649be3b66336d9ef07807.png!small

写作感悟

  1. 调试的过程,底层其实就是先对Object进行反序列化,再对具体的Person类进行反序列化,中间有一些重复的过程,当然底层有很多过程其实很难一个个去分析出来,重点一定要先自己先调试完,自己多走几遍可能就知道它流程了。
  2. 这篇文章写了大概得有四五天,其实调试什么的就是跟着代码往下走,不会的代码就查询。这里就是给出了我在调试过程中遇到的情况,也是想告诉大家代码审计、代码调试就是一个耐心的活。

内容出处:FreeBuf.COM,

声明:本网站所收集的部分公开资料来源于互联网,转载的目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。如果您发现网站上有侵犯您的知识产权的作品,请与我们取得联系,我们会及时修改或删除。文章链接:http://www.yixao.com/procedure/30587.html

发表评论

登录后才能评论