1-漏洞分析——tomcat任意文件写入漏洞分析

Posted songly_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了1-漏洞分析——tomcat任意文件写入漏洞分析相关的知识,希望对你有一定的参考价值。

tomcat8.0.45写入任意文件漏洞的编号为CVE_2017-12615,该漏洞的利用场景为当tomcat运行在windows下,允许PUT方式的http请求并且tomcat的web.xml配置文件中readonly值为false,攻击者就可以向服务器上传恶意jsp文件或webshell文件。

漏洞环境:

apache-tomcat-8.0.45

apache-tomcat-8.0.45-src

漏洞复现

在复现漏洞之前,先修改tomcat的conf目录下的web.xml配置文件:

        为什么要修改readonly?因为tomcat服务器配置默认情况下readonly值为true无法触发漏洞,需要修改为false才能上传文件。

        修改完配置后重启tomcat服务器,然后打开burpSuite开启抓包,在右边Target一栏中点击修改,将host修改为127.0.0.1,port修改为8080,然后在Repeater模块中点击Go发送payload,可以看到Response中成功返回了201状态码,说明漏洞利用成功。

在浏览器中访问test.jsp文件,可以看到页面返回了我们写入的数据,说明test.jsp文件成功上传到webapp/ROOT目录

漏洞分析

简单介绍一下Servelt:

       Servelt是一种服务端程序,当浏览器客户端向tomcat服务器发起http请求时,tomcat会读取web.xml配置文件创建指定的servelt程序来处理浏览器的请求,客户端的所有请求都会交给servelt程序来处理,本次漏洞分析会用到tomcat服务器的DefaultServlet和JspServlet。DefaultServlet是tomcat默认的servlet,主要用于处理静态资源,如html,css,js,图片文件等,一般来说只有在其他servlet都匹配不到时才会匹配到DefaultServlet,而JspServlet是负责处理所有的JSP请求。

       DefaultServlet程序针对每一种http请求方式都有对应的方法,漏洞复现中使用的payload中请求头为put方式,因此会被DefaultServlet程序的doPut方法拦截到,因此doPut方法是我们分析的入口。

现在我们来分析一下漏洞的利用过程,使用Idea工具打开tomcat的源码项目apache-tomcat-8.0.45-src,搜索DefaultServlet并定位到doPut,使用debug模式运行tomcat项目,如下所示:

doPut方法就是用于处理PUT请求的,该方法会接收两个参数,分别代表http请求对象和http响应对象,具体实现如下

    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {

        if (readOnly) {
            resp.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        //获取请求的uri
        String path = getRelativePath(req);
        //获取请求uri资源对象
        WebResource resource = resources.getResource(path);

        Range range = parseContentRange(req, resp);

        InputStream resourceInputStream = null;

        try {
            // Append data specified in ranges to existing content for this
            // resource - create a temp. file on the local filesystem to
            // perform this operation
            // Assume just one range is specified for now
            if (range != null) {
                File contentFile = executePartialPut(req, range, path);
                resourceInputStream = new FileInputStream(contentFile);
            } else {
                resourceInputStream = req.getInputStream();
            }
            //调用write方法生成jsp文件
            if (resources.write(path, resourceInputStream, true)) {
                if (resource.exists()) {
                    resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
                } else {
                    resp.setStatus(HttpServletResponse.SC_CREATED);
                }
            } else {
                resp.sendError(HttpServletResponse.SC_CONFLICT);
            }
        } finally {
            if (resourceInputStream != null) {
                try {
                    resourceInputStream.close();
                } catch (IOException ioe) {
                    // Ignore
                }
            }
        }
    }

       doPut方法中首先会判断readOnly,如果readOnly值为true会发送SC_FORBIDDEN错误并直接返回(SC_FORBIDDEN定义在HttpServletResponse接口中,表示403错误代码)。只有当readOnly值为false才会继续往下执行,那么readOnly值是从哪来的?其实readOnly的值就是从web.xml文件中读取的,这也是为什么之前漏洞复现时修改readOnly的原因。

       getRelativePath方法用于获取http请求对象req中的请求uri路径(/1.jsp/),getResource方法主要返回一个请求的uri路径的资源对象,接着调用了write方法写入文件,传入了三个参数,我们只关心前2个参数。

write方法中的path就是请求的uri路径,is为uri资源对象,其数据类型为InputStream

    public boolean write(String path, InputStream is, boolean overwrite) {
        path = validate(path);

        if (!overwrite && preResourceExists(path)) {
            return false;
        }
        //生成jsp文件
        boolean writeResult = main.write(path, is, overwrite);

        if (writeResult && isCachingAllowed()) {
            // Remove the entry from the cache so the new resource is visible
            cache.removeCacheEntry(path);
        }

        return writeResult;
    }

write方法实现如下

public boolean write(String path, InputStream is, boolean overwrite) {
        //检查uri路径
        checkPath(path);

        if (is == null) {
            throw new NullPointerException(
                    sm.getString("dirResourceSet.writeNpe"));
        }

        if (isReadOnly()) {
            return false;
        }

        File dest = null;
        String webAppMount = getWebAppMount();
        if (path.startsWith(webAppMount)) {
            //这里调用了file方法
            dest = file(path.substring(webAppMount.length()), false);
            if (dest == null) {
                return false;
            }
        } else {
            return false;
        }

        if (dest.exists() && !overwrite) {
            return false;
        }

        try {
            if (overwrite) {
                 //这里又调用了Files.copy方法生成jsp文件
                Files.copy(is, dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
            } else {
                Files.copy(is, dest.toPath());
            }
        } catch (IOException ioe) {
            return false;
        }

        return true;
    }

        write方法内部调用了一个file方法并返回了一个dest变量,接着调用了Files.copy方法

file方法具体实现如下

    protected final File file(String name, boolean mustExist) {
        //判断rui路径
        if (name.equals("/")) {
            name = "";
        }
        //创建文件
        File file = new File(fileBase, name);
        if (!mustExist || file.canRead()) {

            if (getRoot().getAllowLinking()) {
                return file;
            }

            // Check that this file is located under the WebResourceSet's base
            String canPath = null;
            try {
                canPath = file.getCanonicalPath();
            } catch (IOException e) {
                // Ignore
            }
            if (canPath == null)
                return null;

            if (!canPath.startsWith(canonicalBase)) {
                return null;
            }

            // Case sensitivity check
            // Note: We know the resource is located somewhere under base at
            //       point. The purpose of this code is to check in a case
            //       sensitive manner, the path to the resource under base
            //       agrees with what was requested
            String fileAbsPath = file.getAbsolutePath();
            if (fileAbsPath.endsWith("."))
                fileAbsPath = fileAbsPath + '/';
            String absPath = normalize(fileAbsPath);
            if ((absoluteBase.length() < absPath.length())
                && (canonicalBase.length() < canPath.length())) {
                absPath = absPath.substring(absoluteBase.length() + 1);
                if (absPath.equals(""))
                    absPath = "/";
                canPath = canPath.substring(canonicalBase.length() + 1);
                if (canPath.equals(""))
                    canPath = "/";
                if (!canPath.equals(absPath))
                    return null;
            }

        } else {
            return null;
        }
        //返回文件
        return file;
    }

         file方法接收两个参数,name为uri路径,mustExist参数一般为false,调用了equals方法判断请求的uri路径中是否有“/”,如果有那么name参数就不会置为空,然后创建了一个file对象并传入了2个参数,参数fileBase就是tomcat的webapp/ROOT路径,全路径为:D:\\ProgramFiles\\apache-tomcat-8.0.45\\webapps\\ROOT,也就是说,new file返回的file对象的内容为D:\\ProgramFiles\\apache-tomcat-8.0.45\\webapps\\ROOT\\1.jsp,注意:这里在生成文件名时把最后的“/”斜杠字符去掉了。

       说白了new file操作就是创建了一个文件,然后将返回的文件赋值给变量dest,接着就调用copy方法,将变量dest作为参数传给copy方法。

继续跟进copy方法:

    public static long copy(InputStream in, Path target, CopyOption... options)
        throws IOException
    {
        // ensure not null before opening file
        Objects.requireNonNull(in);

        // check for REPLACE_EXISTING
        boolean replaceExisting = false;
        for (CopyOption opt: options) {
            if (opt == StandardCopyOption.REPLACE_EXISTING) {
                replaceExisting = true;
            } else {
                if (opt == null) {
                    throw new NullPointerException("options contains 'null'");
                }  else {
                    throw new UnsupportedOperationException(opt + " not supported");
                }
            }
        }

        // attempt to delete an existing file
        SecurityException se = null;
        if (replaceExisting) {
            try {
                deleteIfExists(target);
            } catch (SecurityException x) {
                se = x;
            }
        }

        // attempt to create target file. If it fails with
        // FileAlreadyExistsException then it may be because the security
        // manager prevented us from deleting the file, in which case we just
        // throw the SecurityException.
        OutputStream ostream;
        try {
            //生成jsp文件
            ostream = newOutputStream(target, StandardOpenOption.CREATE_NEW,
                                              StandardOpenOption.WRITE);
        } catch (FileAlreadyExistsException x) {
            if (se != null)
                throw se;
            // someone else won the race and created the file
            throw x;
        }

        // do the copy
        //写入数据
        try (OutputStream out = ostream) {
            return copy(in, out);
        }
    }

       copy方法内部调用了一个newOutputStream方法,传入了一个参数target,这个参数tomcat容器生成的文件存储路径D:\\ProgramFiles\\apache-tomcat-8.0.45\\webapps\\ROOT,也就是说我们上传的1.jsp文件会存在这个路径。

继续跟进newOutputStream方法,该方法内部又调用了一个newOutputStream方法

    public static OutputStream newOutputStream(Path path, OpenOption... options) throws IOException {
        //生成jsp文件并返回
        return provider(path).newOutputStream(path, options);
    }

        newOutputStream返回时,在webapp/ROOT目录下会创建1.jsp文件,不过此时的1.jsp文件没有内容。

接着再回到copy方法调用处,这里又调用了一次copy,写入数据

    private static long copy(InputStream source, OutputStream sink) throws IOException {
        long nread = 0L;
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = source.read(buf)) > 0) {
            //写入数据
            sink.write(buf, 0, n);
            nread += n;
        }
        return nread;
    }

参数source就是PUT请求中的数据,read函数会从source读取数据,然后写入到1.jsp文件

tomcat任意文件写入漏洞通过一些错误配置,然后再构造特定的uri请求来绕过JspServlet,利用DefaultServlet程序的漏洞上传文件,如何绕过JspServlet我们可以查看web.xml文件中JspServlet和DefaultServlet的uri映射规则

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

       当请求uri路径为/1.jsp或/1.jspx时会匹配到JspServlet,如果请求uri为/1.jsp/时不会匹配到JspServlet,而是会匹配DefaultServlet,从而绕过JspServlet。

漏洞修复:

升级tomcat高版本或者在web.xml配置文件中设置readonly值为true

以上是关于1-漏洞分析——tomcat任意文件写入漏洞分析的主要内容,如果未能解决你的问题,请参考以下文章

Java安全-Tomcat任意文件写入(CVE-2017-12615)漏洞复现

Tomcat任意写入文件漏洞(CVE-2017-12615) 复现

Tomcat任意文件写入(CVE-2017-12615)漏洞复现

Tomcat PUT方法任意写文件漏洞(CVE-2017-12615)

Tomcat一些漏洞的汇总

CVE 2020 1938