最新消息:图 床

Abuse Cache of WinNTFileSystem : Yet Another Bypass of Tomcat CVE-2017-12615

COOL IAM 332浏览 0评论

作者:RicterZ

0x01 CVE-2017-12615 補丁分析

CVE-2017-12615 是 Tomcat 在設置了 readonlyfalse 狀態下,可以通過 PUT 創建一個“.jsp ”的文件。由於後綴名非 .jsp.jspx,所以 Tomcat 在處理的時候經由 DefaultServlet 處理而不是 JspServlet,又由於 Windows 不允許文件名為空格結尾,所以可以成功創建一個 JSP 文件,以達到 RCE 的結果。

龍哥在周五敲我說,在高併發的情況下,還是可以成功寫入一個 JSP 文件;同時微博上的一個小夥伴也告訴我,在一定的條件下還是可以成功創建文件。

測試發現,對於 7.0.81 可以成功復現,但是對於 8.5.21 失敗。如下代碼分析是基於 Apache Tomcat 7.0.81 的。經過分析,我發現這兩種情況其實本質是相同的。不過在此之前,首先看一下 Tomcat 對於 CVE-2017-12615 的補丁好了。

同樣的,進入 DefaultServletdoPut 方法,再調用到 FileDirContextbind 方法,接着調用 file 方法:

protected File file(String name, boolean mustExist) {
    File file = new File(base, name);
    return validate(file, mustExist, absoluteBase);
}

注意到 mustExistfalse

protected File validate(File file, boolean mustExist, String absoluteBase) {

    if (!mustExist || file.exists() && file.canRead()) { // !mustExist = true,進入 if
        ...
        try {
            canPath = file.getCanonicalPath(); 
            // 此處,對路徑進行規範化,調用的是 java.io.File 內的方法
            // 之前的 Payload 中結尾為空格,那麼這個方法就會去掉空格
        } catch (IOException e) {

        }
        ...
        if ((absoluteBase.length() < absPath.length())
            && (absoluteBase.length() < canPath.length())) {
            ...
            // 判斷規範化的路徑以及傳入的路徑是否相等,由於 canPath 沒有空格,return null
            if (!canPath.equals(absPath))
                return null;
        }
    } else {
        return null;
    }

經過上述的判斷,導致我們無法通過空格來創建 JSP 文件。

但是之前提到,在高併發或者另外一種情況下,卻又能創建 JSP 文件,也就是說 canPath.equals(absPath)true。通過深入分析,找出了其原因。

0x02 WinNTFileSystem.canonicalize

上述代碼中,對於路徑的規範化是調用的 file.getCanonicalPath()

public String getCanonicalPath() throws IOException {
    if (isInvalid()) {
        throw new IOException("Invalid file path");
    }
    return fs.canonicalize(fs.resolve(this));
}

也就是調用 FS 的 canonicalize 方法,對於 Windows,調用的是 WinNTFileSystem.canonicalize。這個 Bypass 的鍋也就出在 WinNTFileSystem.canonicalize 里,下面為其代碼,我已去處掉了無關代碼可以更清晰的了解原因。

@Override
public String canonicalize(String path) throws IOException {
    ...
    if (!useCanonCaches) { // !useCanonCaches = false
        return canonicalize0(path);
    } else {
        // 進入此處分支
        String res = cache.get(path);
        if (res == null) {
            String dir = null;
            String resDir = null;
            if (useCanonPrefixCache) {
                dir = parentOrNull(path);
                if (dir != null) {
                    resDir = prefixCache.get(dir);
                    if (resDir != null) {
                        String filename = path.substring(1 + dir.length());
                        // 此處 canonicalizeWithPrefix 不會去掉尾部空格
                        res = canonicalizeWithPrefix(resDir, filename);
                        cache.put(dir + File.separatorChar + filename, res);
                    }
                }
            }
            if (res == null) {
                // 此處的 canonicalize0 會將尾部空格去掉
                res = canonicalize0(path);
                cache.put(path, res);
                if (useCanonPrefixCache && dir != null) {
                    resDir = parentOrNull(res);
                    if (resDir != null) {
                        File f = new File(res);
                        if (f.exists() && !f.isDirectory()) {
                            prefixCache.put(dir, resDir);
                        }
                    }
                }
            }
        }
        // 返迴路徑
        return res;
    }
}

上述代碼有一個非常非常神奇的地方:

  • canonicalizeWithPrefix(resDir, filename) 不會去掉路徑尾部空格

  • canonicalize0(path) 會去掉路徑尾部空格

為了滿足進入存在 canonicalizeWithPrefix 的分支,需要通過兩個判斷:

  • String res = cache.get(path); 應為 null,此處 PUT 一個從未 PUT 過的文件名即可
  • resDir = prefixCache.get(dir); 應不為 null

可以發現,對於 prefixCache 進行添加元素的操作在下方存在 canonicalize0 的 if 分支:

        if (res == null) {
            res = canonicalize0(path);
            cache.put(path, res);
            if (useCanonPrefixCache && dir != null) {
                resDir = parentOrNull(res);
                if (resDir != null) {
                    File f = new File(res);
                    if (f.exists() && !f.isDirectory()) { // 需要滿足條件
                        prefixCache.put(dir, resDir); // 進行 put 操作

通過代碼可知,如果想在 prefixCache 存入數據,需要滿足文件存在文件不是目錄的條件。

prefixCache 存放的是什麼數據呢?通過單步調試可以發現:

转载请注明:IAMCOOL » Abuse Cache of WinNTFileSystem : Yet Another Bypass of Tomcat CVE-2017-12615

0 0 vote
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x