欢迎关注我的微信公众号《壳中之魂》,查看更多网安文章
漏洞产生原因为web.xml里将readonly设置为了false(默认为true),导致了可以通过PUT写入任意文件
经过实际测试,Tomcat 7.x 版本内 web.xml 配置文件内默认配置无 readonly 参数,需要手工添加,默认配置条件下不受此漏洞影响
复现
搭建环境: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 删除。