概要:

  • EXE部署版,内嵌 Tomcat 通过 Tomcat.start() 启动,只添加了特定的 SCI, JasperInitializer 没有被初始化,无法解析 JSP。
  • 服务器部署版,通过 Bootstrap.start() 启动,会通过 SPI 自动加载所有的 SCI,包括 JasperInitializer,JSP 能够被正常解析。

问题描述

根据七月火师傅的提示,范围缩小到 EXE 部署版

启动 FineReport:

1
FineReport/bin/designer.bat

访问 *.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
61
HTTP状态 500 - 内部服务器错误

类型 异常报告

消息 org.apache.jasper.JasperException: 无法为JSP编译类

描述 服务器遇到一个意外的情况,阻止它完成请求。

例外情况

org.apache.jasper.JasperException: org.apache.jasper.JasperException: 无法为JSP编译类
	org.apache.jasper.servlet.JspServletWrapper.handleJspException(JspServletWrapper.java:589)
	org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:425)
	org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:379)
	org.apache.jasper.servlet.JspServlet.service(JspServlet.java:327)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:623)
	com.fr.decision.webservice.BackupActivator$1.doFilter(Unknown Source)
	com.fr.decision.webservice.filter.TenantFilter.doFilter(Unknown Source)
	com.fr.decision.webservice.filter.URICheckFilter.doFilterInternal(Unknown Source)
	com.fr.third.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	com.fr.third.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
	com.fr.third.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)

根本原因。

org.apache.jasper.JasperException: 无法为JSP编译类
	org.apache.jasper.JspCompilationContext.compile(JspCompilationContext.java:619)
	org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:399)
	org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:379)
	org.apache.jasper.servlet.JspServlet.service(JspServlet.java:327)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:623)
	com.fr.decision.webservice.BackupActivator$1.doFilter(Unknown Source)
	com.fr.decision.webservice.filter.TenantFilter.doFilter(Unknown Source)
	com.fr.decision.webservice.filter.URICheckFilter.doFilterInternal(Unknown Source)
	com.fr.third.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	com.fr.third.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
	com.fr.third.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)

根本原因。

java.lang.NullPointerException
	org.apache.jasper.compiler.Validator$ValidateVisitor.<init>(Validator.java:527)
	org.apache.jasper.compiler.Validator.validateExDirectives(Validator.java:1869)
	org.apache.jasper.compiler.Compiler.generateJava(Compiler.java:225)
	org.apache.jasper.compiler.Compiler.compile(Compiler.java:392)
	org.apache.jasper.compiler.Compiler.compile(Compiler.java:368)
	org.apache.jasper.compiler.Compiler.compile(Compiler.java:352)
	org.apache.jasper.JspCompilationContext.compile(JspCompilationContext.java:603)
	org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:399)
	org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:379)
	org.apache.jasper.servlet.JspServlet.service(JspServlet.java:327)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:623)
	com.fr.decision.webservice.BackupActivator$1.doFilter(Unknown Source)
	com.fr.decision.webservice.filter.TenantFilter.doFilter(Unknown Source)
	com.fr.decision.webservice.filter.URICheckFilter.doFilterInternal(Unknown Source)
	com.fr.third.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	com.fr.third.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
	com.fr.third.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)

):注意 主要问题的全部 stack 信息可以在 server logs 里查看
Apache Tomcat/9.0.85

根因分析

空指针异常

根据异常堆栈信息,将断点设置在 org.apache.jasper.compiler.Validator.ValidateVisitor 构造方法处,再次访问 JSP,成功触发断点:

1
2
3
4
5
6
ValidateVisitor(Compiler compiler) {
    this.pageInfo = compiler.getPageInfo();
    this.err = compiler.getErrorDispatcher();
    this.loader = compiler.getCompilationContext().getClassLoader();
    this.expressionFactory = JspFactory.getDefaultFactory().getJspApplicationContext(compiler.getCompilationContext().getServletContext()).getExpressionFactory();
}

运行时信息显示 JspFactory.getDefaultFactory()null,并尝试调用其 getJspApplicationContext() 方法,导致空指针异常。

image-20240930175039627

JspFactory.getDefaultFactory() 为 null

JspFactory.getDefaultFactory() 返回静态变量 deflt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public abstract class JspFactory {
    private static volatile JspFactory deflt = null;

    public JspFactory() {
    }

    public static void setDefaultFactory(JspFactory deflt) {
        JspFactory.deflt = deflt;
    }

    public static JspFactory getDefaultFactory() {
        return deflt;
    }
  ...

此时 defltnull,说明在启动时没有调用 javax.servlet.jsp.JspFactory#setDefaultFactory 进行赋值,反之另一种部署方式会进行赋值。

为了验证这一点,安装服务器部署版,在 setDefaultFactory 处设置断点,参考以下 JVM 调试配置:

1
2
3
4
cd C:\FineReport\tomcat-win64\bin>
set JPDA_ADDRESS=5005
set JPDA_SUSPEND=y
catalina.bat jpda start

使用 IDEA 进行远程调试,成功触发 setDefaultFactory 断点,验证了猜想。

image-20240930202232323

通过 SPI 在加载 JasperInitializer 时触发静态代码中调用 setDefaultFactory 给静态变量 deflt 赋值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class JasperInitializer implements ServletContainerInitializer {
    private static final String MSG = "org.apache.jasper.servlet.JasperInitializer";
    private final Log log = LogFactory.getLog(JasperInitializer.class);

    public JasperInitializer() {
    }

    public void onStartup(Set<Class<?>> types, ServletContext context) throws ServletException {
    	...
    }

    protected TldScanner newTldScanner(ServletContext context, boolean namespaceAware, boolean validate, boolean blockExternal) {
        return new TldScanner(context, namespaceAware, validate, blockExternal);
    }

    static {
        JspFactoryImpl factory = new JspFactoryImpl();
        SecurityClassLoad.securityClassLoad(factory.getClass().getClassLoader());
        if (JspFactory.getDefaultFactory() == null) {
            JspFactory.setDefaultFactory(factory);
        }

    }
}

完整调用栈:

 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
setDefaultFactory, JspFactory (javax.servlet.jsp)
<clinit>, JasperInitializer (org.apache.jasper.servlet)
forName0, Class (java.lang)
forName, Class (java.lang)
loadServices, WebappServiceLoader (org.apache.catalina.startup)
load, WebappServiceLoader (org.apache.catalina.startup)
processServletContainerInitializers, ContextConfig (org.apache.catalina.startup)
webConfig, ContextConfig (org.apache.catalina.startup)
configureStart, ContextConfig (org.apache.catalina.startup)
lifecycleEvent, ContextConfig (org.apache.catalina.startup)
fireLifecycleEvent, LifecycleBase (org.apache.catalina.util)
startInternal, StandardContext (org.apache.catalina.core)
start, LifecycleBase (org.apache.catalina.util)
addChildInternal, ContainerBase (org.apache.catalina.core)
addChild, ContainerBase (org.apache.catalina.core)
addChild, StandardHost (org.apache.catalina.core)
deployDirectory, HostConfig (org.apache.catalina.startup)
run, HostConfig$DeployDirectory (org.apache.catalina.startup)
call, Executors$RunnableAdapter (java.util.concurrent)
run, FutureTask (java.util.concurrent)
execute, InlineExecutorService (org.apache.tomcat.util.threads)
submit, AbstractExecutorService (java.util.concurrent)
deployDirectories, HostConfig (org.apache.catalina.startup)
deployApps, HostConfig (org.apache.catalina.startup)
start, HostConfig (org.apache.catalina.startup)
lifecycleEvent, HostConfig (org.apache.catalina.startup)
fireLifecycleEvent, LifecycleBase (org.apache.catalina.util)
setStateInternal, LifecycleBase (org.apache.catalina.util)
setState, LifecycleBase (org.apache.catalina.util)
startInternal, ContainerBase (org.apache.catalina.core)
startInternal, StandardHost (org.apache.catalina.core)
start, LifecycleBase (org.apache.catalina.util)
call, ContainerBase$StartChild (org.apache.catalina.core)
call, ContainerBase$StartChild (org.apache.catalina.core)
run, FutureTask (java.util.concurrent)
execute, InlineExecutorService (org.apache.tomcat.util.threads)
submit, AbstractExecutorService (java.util.concurrent)
startInternal, ContainerBase (org.apache.catalina.core)
startInternal, StandardEngine (org.apache.catalina.core)
start, LifecycleBase (org.apache.catalina.util)
startInternal, StandardService (org.apache.catalina.core)
start, LifecycleBase (org.apache.catalina.util)
startInternal, StandardServer (org.apache.catalina.core)
start, LifecycleBase (org.apache.catalina.util)
start, Catalina (org.apache.catalina.startup)
invoke0, NativeMethodAccessorImpl (sun.reflect)
invoke, NativeMethodAccessorImpl (sun.reflect)
invoke, DelegatingMethodAccessorImpl (sun.reflect)
invoke, Method (java.lang.reflect)
start, Bootstrap (org.apache.catalina.startup)
main, Bootstrap (org.apache.catalina.startup)

EXE 部署版的启动流程

FineReport/bin/designer.bat 可知运行的主类为 com.fr.start.Designer,在 com.fr.start.Designer#main 处设置断点,并设置客户端连接时才开始调试。

触发断点:

image-20240930203629545

后续会从 designer-startup.xml 中读取配置进行处理:

1
2
3
4
5
6
<designer-startup activator="com.fr.start.module.DesignerStartup" invoke-subs-strategy="custom" role="root">
    ...
    <emb-server activator="com.fr.start.server.FineEmbedServerActivator" invoke-subs-strategy="custom" auto-invoke-by-parent="false" binding-workspace="local">
        <server ref="../server/server.xml"/>
    </emb-server>
</designer-startup>

内嵌 Tomcat,通过 Tomcat#start() 启动:

  • com.fr.start.server.FineEmbedServerActivator#start
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public synchronized void start() {
    try {
        FineEmbedServerMonitor.getInstance().reset();
        this.initTomcat();
      	// org.apache.catalina.startup.Tomcat#start
        this.tomcat.start();
    } catch (LifecycleException var5) {
        FineLoggerFactory.getLogger().error(var5.getMessage(), var5);
    } finally {
        FineEmbedServerMonitor.getInstance().setComplete();
    }

}

服务器部署版是通过 Bootstrap.start() 启动的。

在内嵌 Tomcat 的初始化过程中,只添加了对指定的 ServletContainerInitializer (SCI) 的支持:

  • com.fr.start.server.FineEmbedServerActivator#initTomcat
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private void initTomcat() {
    this.tomcat = new Tomcat();
  	...
    Context var3 = this.tomcat.addContext(var2, var1);
  	...
    SpringServletContainerInitializer var4 = new SpringServletContainerInitializer();
    HashSet var5 = new HashSet();
    var5.add(FineWebApplicationInitializer.class);
    var3.addServletContainerInitializer(var4, var5);
}

image-20240930222138579

而在 Bootstrap.start() 中,会通过 SPI 的方式自动添加所有的 SCI,包括 JasperInitializer

  • org.apache.catalina.startup.ContextConfig#processServletContainerInitializers

image-20240930221005778

解决方案

刚好去年有挖过另一个任意文件写的利用方式,把 class 写到 /classes/ 下后进行类加载

image-20240930223106503

相应代码

  • com.fr.web.controller.common.FileRequestService#getFile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Controller("fileRequestService")
@RequestMapping({"/file"})
@LoginStatusChecker(
        required = false
)
public class FileRequestService {
    // ...
    
    public void getFile(HttpServletRequest var1, HttpServletResponse var2, @RequestParam("path") String var3, @RequestParam(value = "type",required = false) String var4, @RequestParam(value = "parser",required = false) String var5) throws Exception {
        if (!WebServiceUtils.containSQLChars(var3) && !WebServiceUtils.containSQLChars(var4) && !WebServiceUtils.containSQLChars(var5) && !WebServiceUtils.invalidFilePath(var3)) {
            // ...
            try {
                FileType var7 = FileType.parse(var4);
                PrintWriter var11;
                if (var7 == FileType.PLAIN) {
                    //...
                } else if (var7 == FileType.CLASS) {
                    String var19 = ServerConfig.getInstance().getServerCharset();
                    TextGenerator var20 = (TextGenerator)Reflect.on(ClassFactory.getInstance().classForName(var3)).create().get();
                    //...

构造请求如下,成功解决该问题

1
/webroot/decision/file?path=org.apache.jasper.servlet.JasperInitializer&type=class&parser=plain

image-20240930223851768