作者:RicterZ
0x01 CVE-2017-12615 補丁分析
CVE-2017-12615 是 Tomcat 在設置了 readonly
為 false
狀態下,可以通過 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 的補丁好了。
同樣的,進入 DefaultServlet
的 doPut
方法,再調用到 FileDirContext
的 bind
方法,接着調用 file
方法:
protected File file(String name, boolean mustExist) { File file = new File(base, name); return validate(file, mustExist, absoluteBase); }
注意到 mustExist
為 false
:
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