0x00 前言
Apache Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理功能,可为任何应用提供安全保障 – 从命令行应用、移动应用到大型网络及企业应用。Shiro为解决应用安全的如下四要素提供了相应的API:
-
认证 – 用户身份识别,常被称为用户“登录”;
-
授权 – 访问控制;
-
密码加密 – 保护或隐藏数据防止被偷窥;
-
会话管理 – 用户相关的时间敏感的状态。
Shiro还支持一些辅助特性,如Web应用安全、单元测试和多线程,它们的存在强化了这四个要素。本文重点分析2015年11月19号报告的1.2.4版本中存在的一个反序列化导致的远程代码执行的漏洞。
0x01 分析
根据SHIRO-550(https://issues.apache.org/jira/browse/SHIRO-550)报告中的描述,默认情况下,shiro使用CookieRememberMeManager类对用户的身份信息的进行序列化,加密以及编码。因此,当系统收到一个未认证的用户的请求时,将会按照下面的过程来寻找已记住的身份信息:
-
获取rememberMe cookie的值
-
Base64解码
-
使用AES解密
-
使用ObjectInputStream进行反序列化
然而,默认的AES加密的密钥却是硬编码在源码里。这就意味着,任何能够看到源代码的人都知道默认的密钥什么。一旦攻击者构造了一个恶意的对象,利用上面处理过程的反过程(序列化-AES加密-Base64编码)将恶意代码作为cookie发送至服务器端这就造成了由反序列化引起的远程代码执行的漏洞。
下面我将重点分析一下这个漏洞造成的过程。
从报告描述中可以发现这个漏洞主要是因为CookieRememberMeManager类引起的,找到github上shiro 1.2.4源码。
CookieRememberMeManager.java:
public class CookieRememberMeManager extends AbstractRememberMeManager { ... /** * Base64-encodes the specified serialized byte array and sets that base64-encoded String as the cookie value. * <p/> * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair * so an HTTP cookie can be set on the outgoing response. If it is not a {@code WebSubject} or that * {@code WebSubject} does not have an HTTP Request/Response pair, this implementation does nothing. * * @param subject the Subject for which the identity is being serialized. * @param serialized the serialized bytes to be persisted. */ protected void rememberSerializedIdentity(Subject subject, byte[] serialized) { if (!WebUtils.isHttp(subject)) { if (log.isDebugEnabled()) { String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " + "request and response in order to set the rememberMe cookie. Returning immediately and " + "ignoring rememberMe operation."; log.debug(msg); } return; } HttpServletRequest request = WebUtils.getHttpRequest(subject); HttpServletResponse response = WebUtils.getHttpResponse(subject); //base 64 encode it and store as a cookie: String base64 = Base64.encodeToString(serialized); Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies Cookie cookie = new SimpleCookie(template); cookie.setValue(base64); cookie.saveTo(request, response); } ... /** * Returns a previously serialized identity byte array or {@code null} if the byte array could not be acquired. * This implementation retrieves an HTTP cookie, Base64-decodes the cookie value, and returns the resulting byte * array. * <p/> * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP * Request/Response pair so an HTTP cookie can be retrieved from the incoming request. If it is not a * {@code WebSubjectContext} or that {@code WebSubjectContext} does not have an HTTP Request/Response pair, this * implementation returns {@code null}. * * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation, that * is being used to construct a {@link Subject} instance. To be used to assist with data * lookup. * @return a previously serialized identity byte array or {@code null} if the byte array could not be acquired. */ 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); // Browsers do not always remove cookies immediately (SHIRO-183) // ignore cookies that are scheduled for removal 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 { //no cookie set - new site visitor? return null; } }
|
分析这个类后,我们发现CookieRememberMeManager类实际上继承了父类AbstractRememberMeManager并且正如上面描述的过程使用getRememberedSerializedIdentity方法对获取到的请求进行Base64解码返回序列化对象。
而AbstractRememberMeManager类直接将AES加密的密钥写在源码里,并且调用DefaultSerializer类来实现序列化操作
AbstractRememberMeManager.java:
public abstract class AbstractRememberMeManager implements RememberMeManager { /** * private inner log instance. */ private static final Logger log = LoggerFactory.getLogger(AbstractRememberMeManager.class); /** * The following Base64 string was generated by auto-generating an AES Key: * <pre> * AesCipherService aes = new AesCipherService(); * byte[] key = aes.generateNewKey().getEncoded(); * String base64 = Base64.encodeToString(key); * </pre> * The value of 'base64' was copied-n-pasted here: */ private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");... ... /** * Default constructor that initializes a {@link DefaultSerializer} as the {@link #getSerializer() serializer} and * an {@link AesCipherService} as the {@link #getCipherService() cipherService}. */ public AbstractRememberMeManager() { this.serializer = new DefaultSerializer<PrincipalCollection>(); this.cipherService = new AesCipherService(); setCipherKey(DEFAULT_CIPHER_KEY_BYTES); }
继续分析DefaultSerializer类,在反序列化方法deserialize里,我们看到了熟悉的readObject(),这也正是远程代码执行漏洞产生的原因。
DefaultSerializer.java:
public class DefaultSerializer<T> implements Serializer<T> { /** * This implementation serializes the Object by using an {@link ObjectOutputStream} backed by a * {@link ByteArrayOutputStream}. The {@code ByteArrayOutputStream}'s backing byte array is returned. * * @param o the Object to convert into a byte[] array. * @return the bytes representing the serialized object using standard JVM serialization. * @throws SerializationException wrapping a {@link IOException} if something goes wrong with the streams. */ public byte[] serialize(T o) throws SerializationException { if (o == null) { String msg = "argument cannot be null."; throw new IllegalArgumentException(msg); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); BufferedOutputStream bos = new BufferedOutputStream(baos); try { ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(o); oos.close(); return baos.toByteArray(); } catch (IOException e) { String msg = "Unable to serialize object [" + o + "]. " + "In order for the DefaultSerializer to serialize this object, the [" + o.getClass().getName() + "] " + "class must implement java.io.Serializable."; throw new SerializationException(msg, e); } } /** * This implementation deserializes the byte array using a {@link ObjectInputStream} using a source * {@link ByteArrayInputStream} constructed with the argument byte array. * * @param serialized the raw data resulting from a previous {@link #serialize(Object) serialize} call. * @return the deserialized/reconstituted object based on the given byte array * @throws SerializationException if anything goes wrong using the streams. */ 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); } }}
|
总结一下漏洞产生的过程如下:
-
CookieRememberMeManager类接收到客户端的rememberMe cookie的请求
-
使用getRememberedSerializedIdentity方法对获取到的请求进行Base64解码返回序列化对象
-
调用AbstractRememberMeManager类并使用硬编码的密钥对序列化对象进行AES解密
-
调用DefaultSerializer类中的deserialize方法实现反序列化操作,从而造成远程代码执行
0x02 利用
2.1 搭建实验环境
首先,从Github上下载Shiro 1.2.4的源代码:
git clone https://github.com/apache/shiro.git cd shiro git checkout shiro-root-1.2.4cd samples/web
接着,编辑pom.xml文件,添加存在漏洞的jar包如下:
<!-- 设置maven的编译环境 --> <properties> <maven.compiler.source>1.6</maven.compiler.source> <maven.compiler.target>1.6</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <!-- 此处需设置版本为1.2 --> <version>1.2</version> <scope>runtime</scope> </dependency> ... <!-- 添加存在漏洞的commons-collections包 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency> </dependencies>
然后,安装和配置maven并设置maven的编译环境。可参考http://shiro-user.582556.n2.nabble.com/Help-td7580772.html,新建文件”~/.m2/toolchains.xml”包含以下内容:
<toolchains> <toolchain> <type>jdk</type> <provides> <version>1.6</version> <vendor>sun</vendor> </provides> <configuration> <!-- this can be anything 1.6+, I tested with java 1.8 on a mac --> <jdkHome>/absolute/path/to/java/home</jdkHome> </configuration> </toolchain></toolchains>
编译存在漏洞环境为war包:
mvn package
|
编译成功后,将target目录下生成的war文件部署到你的web服务器上(如:tomcat)如下图所示:
2.2 编写漏洞利用
根据以上的分析,我编写了如下的工具可用于检测是否存在漏洞。
单个网址检测:
hackUtils.py -o http://www.shiro.com/
批量网址检测:
hackUtils.py -o urls.txt
0x03 修补方案
升级到Shiro 1.2.5 或者 2.0.0 版本。
参考
https://issues.apache.org/jira/browse/SHIRO-550
作者博客:http://avfisher.win
转载自:http://avfisher.win/archives/584
版权声明:该科普文章属于WhiteCellClub团队的安全小飞侠原创,转载须申明!
本文始发于微信公众号(WhiteCellClub):Apache Shiro 1.2.4 远程代码执行分析与利用
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论