这篇笔记我尽量少贴代码,有兴趣的可以自己去跟一下。
需要知道的背景知识:
1.在 tomcat 的 conf/web.xml 文件中配置了一个如下的 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>
这意味着,tomcat 接收到的所有的 jsp 或 jspx 的请求,都会转交给org.apache.jasper.servlet.JspServlet
来处理,由它来将请求导向至最终的位置。
2.Jsp 文件会被转换为 Java 文件,并随后被编译为 class。转换后的文件与编译后的 class 默认保存在 Tomcat 下的 work 目录中。请求最终会被导向至从 Jsp 文件编译出来的 class 的对象上。
3.Jsp 被编译并被加载实例化后,会被封装在一个 JspServletWrapper 对象中。在 Tomcat 中,每一个 Context 都对应有一个 JspRuntimeContext 对象,该对象中以 Map 的形式,以 path(如 /index.jsp) 为key 保存了当前 Context 中所有的 JspServletWrapper 对象。
4.被编译并且被 Tomcat 加载后(创建了对应的 JspServletWrapper 对象后),Jsp 文件以及转换出来的 Java 文件以及由 Java 文件编译出来的 class 文件,在一定程度上来说,都是可有可无的。
这里简述一下 Tomcat 会在什么时候对 Jsp 进行编译:
讲到这里,所谓 “隐藏任意 Jsp 文件” 的原理也就很简单了。只要在 Jsp编译完成后,删掉原有 Jsp 文件,并 “欺骗” Tomcat 让它认为文件依然存在,就可以了。
简述一下 Tomcat 接收请求的过程,当然这里只简述请求到达 JspServlet 后发生的事情,之前的事情就太多了。这里从 JspServlet 的 serviceJspFile 开始说起,代码如下:
private void serviceJspFile(HttpServletRequest request,
HttpServletResponse response, String jspUri,
boolean precompile)
throws ServletException, IOException {
JspServletWrapper wrapper = rctxt.getWrapper(jspUri);
if (wrapper == null) {
synchronized(this) {
wrapper = rctxt.getWrapper(jspUri);
if (wrapper == null) {
// Check if the requested JSP page exists, to avoid
// creating unnecessary directories and files.
if (null == context.getResource(jspUri)) {
handleMissingResource(request, response, jspUri);
return;
}
wrapper = new JspServletWrapper(config, options, jspUri,
rctxt);
rctxt.addWrapper(jspUri,wrapper);
}
}
}
try {
wrapper.service(request, response, precompile);
} catch (FileNotFoundException fnfe) {
handleMissingResource(request, response, jspUri);
}
}
它的主要作用就是检查 JspRuntimeContext 中是否已经存在与当前 Jsp 文件相对应的 JspServletWrapper(如果存在的话,说明这个文件之前已经被访问过了)。有的话就取出来,没有则检查对应的 Jsp 文件是否存在,如果存在的话就新创建一个 JspServletWrapper 并添加到 JspRuntimeContext 中去。
随后会进入 JspServletWrapper 的 service 方法,如下(我对代码进行了删减,只看与主题有关的部分):
public void service(HttpServletRequest request,
HttpServletResponse response,
boolean precompile)
throws ServletException, IOException, FileNotFoundException {
Servlet servlet;
try {
/*
* (1) Compile
*/
if (options.getDevelopment() || firstTime ) {
synchronized (this) {
firstTime = false;
// The following sets reload to true, if necessary
ctxt.compile();
}
} else {
if (compileException != null) {
// Throw cached compilation exception
throw compileException;
}
}
/*
* (2) (Re)load servlet class file
*/
servlet = getServlet();
// If a page is to be precompiled only, return.
if (precompile) {
return;
}
} catch (ServletException ex) {
.....
}
/*
* (4) Service request
*/
if (servlet instanceof SingleThreadModel) {
// sync on the wrapper so that the freshness
// of the page is determined right before servicing
synchronized (this) {
servlet.service(request, response);
}
} else {
servlet.service(request, response);
}
} catch (UnavailableException ex) {
....
}
}
可以看到,主要流程就是编译 Jsp,然后进入编译出来的 Jsp 的 service 方法,开始执行 Jsp 内的代码。这里先判断当前 Jsp 是不是第一次被访问,或者 Tomcat 是否处于 development 模式中。如果是,则会进入 ctxt.compile();
对 Jsp 进行编译。ctxt 是 JspCompilationContext 的对象,该对象内封装了与编译 Jsp 相关的所有信息,每一个 JspServletWrapper 里面都有一个自己的 JspCompilationContext。也就是在 compile 方法里面,对 Jsp 文件的更改以及删除做了检查。
而当 Tomcat 利用后台线程来对 Jsp 的更新删除做检查的时候,是不会经过这里的,而是直接进入 JspCompilationContext 的 compile 方法(也就是上文的 ctxt.compile() 方法)。代码如下:
public void compile() throws JasperException, FileNotFoundException {
createCompiler();
if (jspCompiler.isOutDated()) {
if (isRemoved()) {
throw new FileNotFoundException(jspUri);
}
try {
jspCompiler.removeGeneratedFiles();
jspLoader = null;
jspCompiler.compile();
jsw.setReload(true);
jsw.setCompilationException(null);
} catch (JasperException ex) {
// Cache compilation exception
jsw.setCompilationException(ex);
if (options.getDevelopment() && options.getRecompileOnFail()) {
// Force a recompilation attempt on next access
jsw.setLastModificationTest(-1);
}
throw ex;
} catch (FileNotFoundException fnfe) {
// Re-throw to let caller handle this - will result in a 404
throw fnfe;
} catch (Exception ex) {
JasperException je = new JasperException(
Localizer.getMessage("jsp.error.unable.compile"),
ex);
// Cache compilation exception
jsw.setCompilationException(je);
throw je;
}
}
}
JspCompilationContext 对象内有一个 Compile 对象,用它来对 Jsp 进行更新检查以及编译。jspCompile.isOutDated 方法代码如下:
public boolean isOutDated(boolean checkClass) {
if (jsw != null
&& (ctxt.getOptions().getModificationTestInterval() > 0)) {
if (jsw.getLastModificationTest()
+ (ctxt.getOptions().getModificationTestInterval() * 1000) > System
.currentTimeMillis()) {
return false;
}
jsw.setLastModificationTest(System.currentTimeMillis());
}
Long jspRealLastModified = ctxt.getLastModified(ctxt.getJspFile());
if (jspRealLastModified.longValue() < 0) {
// Something went wrong - assume modification
return true;
}
......
}
我们只需要让此方法返回 false,那么无论 Tomcat 在何时对 Jsp 文件进行编译或者更新检查,都会认为这个 JspServletWrapper 对象的 Jsp 文件没有发生任何更改,所以也就不会发现文件被删掉了。它会继续保留这个 JspServletWrapper 对象以供客户端访问。
后面就没有什么好说的了,如何进行“欺骗”,大家直接看效果吧。将 hideshell.jsp (在后面提供) 放在 webapps/ROOT 下,同目录下有传说中的 jspspy2011.jsp:
Tomcat 启动后,先访问一下 jspspy2011.jsp,目的是为了让 Tomcat 将它编译,并生成 JspServletWrapper 保存在 JspRuntimeContext 中(其实我们也可以自己用代码来编译,但是我太懒)。然后再访问 hideshell.jsp,如下图:
点击 "Hide /jspspy2011.jsp",会发现 webapps/ROOT 目录下的 jspspy2011.jsp 消失了,再访问 jspspy2011.jsp 出现了 404。Shell 被“隐藏”了,而且访问路径被更改成了 hidden-jspspy2011.jsp:
同时再回到 hideshell.jsp,它会提示 /hidden-jspspy2011.jsp 是一个疑似的隐藏文件:
hideshell.jsp 会尝试将被隐藏的 Jsp 文件与它生成的 Java 与 class 文件全部删掉。但是我发现如果 Jsp 中使用了内部类,这些内部类所编译出来的 class 不会被删掉。
“隐藏” 任意 Jsp 文件到此已经实现了。可是虽然文件看不到了,当我们在访问隐藏后的路径的时候,依然会产生日志。那么下一篇笔记,有可能分享一下在隐藏 Shell 的同时,如何隐藏掉它们产生的访问日志。
其实细心的话你会发现,你在访问这个 hideshell.jsp 的时候,如果你的日志使用的是默认配置的话,Tomcat 是不会记录你的访问日志的。:)
hideshell.jsp 我发在了 ThreatHunter 社区,https://threathunter.org/topic/59545c18a1e4d7fc5810529a