1-漏洞分析——tomcat任意文件写入漏洞分析
Posted songly_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了1-漏洞分析——tomcat任意文件写入漏洞分析相关的知识,希望对你有一定的参考价值。
tomcat8.0.45写入任意文件漏洞的编号为CVE_2017-12615,该漏洞的利用场景为当tomcat运行在windows下,允许PUT方式的http请求并且tomcat的web.xml配置文件中readonly值为false,攻击者就可以向服务器上传恶意jsp文件或webshell文件。
漏洞环境:
apache-tomcat-8.0.45
apache-tomcat-8.0.45-src
漏洞复现
在复现漏洞之前,先修改tomcat的conf目录下的web.xml配置文件:
为什么要修改readonly?因为tomcat服务器配置默认情况下readonly值为true无法触发漏洞,需要修改为false才能上传文件。
修改完配置后重启tomcat服务器,然后打开burpSuite开启抓包,在右边Target一栏中点击修改,将host修改为127.0.0.1,port修改为8080,然后在Repeater模块中点击Go发送payload,可以看到Response中成功返回了201状态码,说明漏洞利用成功。
在浏览器中访问test.jsp文件,可以看到页面返回了我们写入的数据,说明test.jsp文件成功上传到webapp/ROOT目录
漏洞分析
简单介绍一下Servelt:
Servelt是一种服务端程序,当浏览器客户端向tomcat服务器发起http请求时,tomcat会读取web.xml配置文件创建指定的servelt程序来处理浏览器的请求,客户端的所有请求都会交给servelt程序来处理,本次漏洞分析会用到tomcat服务器的DefaultServlet和JspServlet。DefaultServlet是tomcat默认的servlet,主要用于处理静态资源,如html,css,js,图片文件等,一般来说只有在其他servlet都匹配不到时才会匹配到DefaultServlet,而JspServlet是负责处理所有的JSP请求。
DefaultServlet程序针对每一种http请求方式都有对应的方法,漏洞复现中使用的payload中请求头为put方式,因此会被DefaultServlet程序的doPut方法拦截到,因此doPut方法是我们分析的入口。
现在我们来分析一下漏洞的利用过程,使用Idea工具打开tomcat的源码项目apache-tomcat-8.0.45-src,搜索DefaultServlet并定位到doPut,使用debug模式运行tomcat项目,如下所示:
doPut方法就是用于处理PUT请求的,该方法会接收两个参数,分别代表http请求对象和http响应对象,具体实现如下
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (readOnly) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
//获取请求的uri
String path = getRelativePath(req);
//获取请求uri资源对象
WebResource resource = resources.getResource(path);
Range range = parseContentRange(req, resp);
InputStream resourceInputStream = null;
try {
// Append data specified in ranges to existing content for this
// resource - create a temp. file on the local filesystem to
// perform this operation
// Assume just one range is specified for now
if (range != null) {
File contentFile = executePartialPut(req, range, path);
resourceInputStream = new FileInputStream(contentFile);
} else {
resourceInputStream = req.getInputStream();
}
//调用write方法生成jsp文件
if (resources.write(path, resourceInputStream, true)) {
if (resource.exists()) {
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
} else {
resp.setStatus(HttpServletResponse.SC_CREATED);
}
} else {
resp.sendError(HttpServletResponse.SC_CONFLICT);
}
} finally {
if (resourceInputStream != null) {
try {
resourceInputStream.close();
} catch (IOException ioe) {
// Ignore
}
}
}
}
doPut方法中首先会判断readOnly,如果readOnly值为true会发送SC_FORBIDDEN错误并直接返回(SC_FORBIDDEN定义在HttpServletResponse接口中,表示403错误代码)。只有当readOnly值为false才会继续往下执行,那么readOnly值是从哪来的?其实readOnly的值就是从web.xml文件中读取的,这也是为什么之前漏洞复现时修改readOnly的原因。
getRelativePath方法用于获取http请求对象req中的请求uri路径(/1.jsp/),getResource方法主要返回一个请求的uri路径的资源对象,接着调用了write方法写入文件,传入了三个参数,我们只关心前2个参数。
write方法中的path就是请求的uri路径,is为uri资源对象,其数据类型为InputStream
public boolean write(String path, InputStream is, boolean overwrite) {
path = validate(path);
if (!overwrite && preResourceExists(path)) {
return false;
}
//生成jsp文件
boolean writeResult = main.write(path, is, overwrite);
if (writeResult && isCachingAllowed()) {
// Remove the entry from the cache so the new resource is visible
cache.removeCacheEntry(path);
}
return writeResult;
}
write方法实现如下
public boolean write(String path, InputStream is, boolean overwrite) {
//检查uri路径
checkPath(path);
if (is == null) {
throw new NullPointerException(
sm.getString("dirResourceSet.writeNpe"));
}
if (isReadOnly()) {
return false;
}
File dest = null;
String webAppMount = getWebAppMount();
if (path.startsWith(webAppMount)) {
//这里调用了file方法
dest = file(path.substring(webAppMount.length()), false);
if (dest == null) {
return false;
}
} else {
return false;
}
if (dest.exists() && !overwrite) {
return false;
}
try {
if (overwrite) {
//这里又调用了Files.copy方法生成jsp文件
Files.copy(is, dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
} else {
Files.copy(is, dest.toPath());
}
} catch (IOException ioe) {
return false;
}
return true;
}
write方法内部调用了一个file方法并返回了一个dest变量,接着调用了Files.copy方法
file方法具体实现如下
protected final File file(String name, boolean mustExist) {
//判断rui路径
if (name.equals("/")) {
name = "";
}
//创建文件
File file = new File(fileBase, name);
if (!mustExist || file.canRead()) {
if (getRoot().getAllowLinking()) {
return file;
}
// 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)
return null;
if (!canPath.startsWith(canonicalBase)) {
return null;
}
// Case sensitivity check
// Note: We know the resource is located somewhere under base at
// point. The purpose of this code is to check in a case
// sensitive manner, the path to the resource under base
// agrees with what was requested
String fileAbsPath = file.getAbsolutePath();
if (fileAbsPath.endsWith("."))
fileAbsPath = fileAbsPath + '/';
String absPath = normalize(fileAbsPath);
if ((absoluteBase.length() < absPath.length())
&& (canonicalBase.length() < canPath.length())) {
absPath = absPath.substring(absoluteBase.length() + 1);
if (absPath.equals(""))
absPath = "/";
canPath = canPath.substring(canonicalBase.length() + 1);
if (canPath.equals(""))
canPath = "/";
if (!canPath.equals(absPath))
return null;
}
} else {
return null;
}
//返回文件
return file;
}
file方法接收两个参数,name为uri路径,mustExist参数一般为false,调用了equals方法判断请求的uri路径中是否有“/”,如果有那么name参数就不会置为空,然后创建了一个file对象并传入了2个参数,参数fileBase就是tomcat的webapp/ROOT路径,全路径为:D:\\ProgramFiles\\apache-tomcat-8.0.45\\webapps\\ROOT,也就是说,new file返回的file对象的内容为D:\\ProgramFiles\\apache-tomcat-8.0.45\\webapps\\ROOT\\1.jsp,注意:这里在生成文件名时把最后的“/”斜杠字符去掉了。
说白了new file操作就是创建了一个文件,然后将返回的文件赋值给变量dest,接着就调用copy方法,将变量dest作为参数传给copy方法。
继续跟进copy方法:
public static long copy(InputStream in, Path target, CopyOption... options)
throws IOException
{
// ensure not null before opening file
Objects.requireNonNull(in);
// check for REPLACE_EXISTING
boolean replaceExisting = false;
for (CopyOption opt: options) {
if (opt == StandardCopyOption.REPLACE_EXISTING) {
replaceExisting = true;
} else {
if (opt == null) {
throw new NullPointerException("options contains 'null'");
} else {
throw new UnsupportedOperationException(opt + " not supported");
}
}
}
// attempt to delete an existing file
SecurityException se = null;
if (replaceExisting) {
try {
deleteIfExists(target);
} catch (SecurityException x) {
se = x;
}
}
// attempt to create target file. If it fails with
// FileAlreadyExistsException then it may be because the security
// manager prevented us from deleting the file, in which case we just
// throw the SecurityException.
OutputStream ostream;
try {
//生成jsp文件
ostream = newOutputStream(target, StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE);
} catch (FileAlreadyExistsException x) {
if (se != null)
throw se;
// someone else won the race and created the file
throw x;
}
// do the copy
//写入数据
try (OutputStream out = ostream) {
return copy(in, out);
}
}
copy方法内部调用了一个newOutputStream方法,传入了一个参数target,这个参数tomcat容器生成的文件存储路径D:\\ProgramFiles\\apache-tomcat-8.0.45\\webapps\\ROOT,也就是说我们上传的1.jsp文件会存在这个路径。
继续跟进newOutputStream方法,该方法内部又调用了一个newOutputStream方法
public static OutputStream newOutputStream(Path path, OpenOption... options) throws IOException {
//生成jsp文件并返回
return provider(path).newOutputStream(path, options);
}
newOutputStream返回时,在webapp/ROOT目录下会创建1.jsp文件,不过此时的1.jsp文件没有内容。
接着再回到copy方法调用处,这里又调用了一次copy,写入数据
private static long copy(InputStream source, OutputStream sink) throws IOException {
long nread = 0L;
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = source.read(buf)) > 0) {
//写入数据
sink.write(buf, 0, n);
nread += n;
}
return nread;
}
参数source就是PUT请求中的数据,read函数会从source读取数据,然后写入到1.jsp文件
tomcat任意文件写入漏洞通过一些错误配置,然后再构造特定的uri请求来绕过JspServlet,利用DefaultServlet程序的漏洞上传文件,如何绕过JspServlet我们可以查看web.xml文件中JspServlet和DefaultServlet的uri映射规则
<!-- 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>
当请求uri路径为/1.jsp或/1.jspx时会匹配到JspServlet,如果请求uri为/1.jsp/时不会匹配到JspServlet,而是会匹配DefaultServlet,从而绕过JspServlet。
漏洞修复:
升级tomcat高版本或者在web.xml配置文件中设置readonly值为true
以上是关于1-漏洞分析——tomcat任意文件写入漏洞分析的主要内容,如果未能解决你的问题,请参考以下文章
Java安全-Tomcat任意文件写入(CVE-2017-12615)漏洞复现
Tomcat任意写入文件漏洞(CVE-2017-12615) 复现
Tomcat任意文件写入(CVE-2017-12615)漏洞复现