环境搭配
tomcat 9.0.80
推荐看如下教程去搭建debug环境
https://gitee.com/appleyk/tomact9-markdown
jsp解析
Tomcat处理jsp的核心逻辑是它实现了一个处理jsp的Servletorg.apache.jasper.servlet.JspServlet
,这个Servlet处理所有以jsp和jspx为后缀的请求。
编译jsp的过程如下
- 将jsp文件包装成java文件
- 将java文件编译为class文件
- 通过newInstance加载编译的class文件
主要编译代码从这里开始org.apache.jasper.JspCompilationContext#compile()
跟入 jspCompiler.compile();
可以看到两行重要代码,先生成java文件,然后根据java文件生成class文件
1 | Map<String,SmapStratum> smaps = generateJava(); |
编译后就通过反射去获取该servlet
最后会将实例赋值给成员变量
后面你访问的jsp,其实就相当于在访问这个servlet
详细步骤需要自己调试
空文件jsp
根据上述原理,其实只要可以直接写入该class文件,那么jsp的内容应该是无关紧要的
根据调试,发现编译出来的class文件位于
但是如果直接去写入这个class文件是没有用的,经过我个人测试
- 只修改class文件,然后访问那个jsp,会被重新编译
- 只修改java文件,不会被继续编译,class还是最初的
回到刚刚的tomcat编译jsp的源码
注意到这个1
jspCompiler.isOutDated()
这个函数用于检测你的jsp是否有修改过,如果修改过就会重新编译,那么来看看内部细节是怎么检测的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38File targetFile;
if (checkClass) {
targetFile = new File(ctxt.getClassFileName());
} else {
targetFile = new File(ctxt.getServletJavaFileName());
}
if (!targetFile.exists()) {
return true;
}
long targetLastModified = targetFile.lastModified();
if (checkClass && jsw != null) {
jsw.setServletClassLastModifiedTime(targetLastModified);
}
Long jspRealLastModified = ctxt.getLastModified(ctxt.getJspFile());
if (jspRealLastModified.longValue() < 0) {
// Something went wrong - assume modification
return true;
}
if (targetLastModified != jspRealLastModified.longValue()) {
if (log.isDebugEnabled()) {
log.debug("Compiler: outdated: " + targetFile + " "
+ targetLastModified);
}
return true;
}
// determine if source dependent files (e.g. includes using include
// directives) have been changed.
if (jsw == null) {
return false;
}
Map<String,Long> depends = jsw.getDependants();
if (depends == null) {
return false;
}
看到上面的代码,可以知道他会拿jsp的时间戳与class的时间戳进行比对,如果不一致就会重新编译
在重新编译后会将class文件的时间戳设置为jsp的时间戳
这个 depends 应该是include里面需要编译的文件,没测试过
那么修改class以后就会改变时间戳,所以导致刚刚测试会重新编译
这里还有一个需要注意的点就是
刚刚分析代码的时候提到过
servlet会将实例赋值给成员变量,你之后在不更改jsp的情况下,访问的都是已经加载在内存中的servlet
所以如果想写一个jsp,然后去写入该jsp的class文件,在把自身写入的东西清楚掉,伪代码类似下面1
2
3写入恶意class
清空自身文件内容
改变class文件和自身jsp为相同的时间戳
然后在去访问该jsp,你会发现这个jsp还是之前的功能,并不会是恶意的class文件的功能,因为他已经把之前的class内容加载到内存中了
那么这里我能想到有两个解决方案
- 重启tomcat,让他重新加载class文件
- 写入其他jsp,比如用一个a.jsp写入b.jsp和b_jsp.class
最终代码
我直接将哥斯拉的jsp生成的class文件复制出来,没有特别去生成
这样下面的代码在不涉及任何反射和rce函数,只操控文件的方法下完成了哥斯拉jsp🐴的注入
1 | <%@ page import="java.io.*" %> |