The Confluence Server vulnerability

Developers
Java

What can we learn from the recent Atlassian Confluence Server vulnerability? Learn how to avoid similar bugs in your code!

Earlier this year Meta-Inf – the vendor of Email This Issue for Jira – and Cydrill – the global training provider for corporate software security – joined forces to boost the secure coding preparedness of developers working on Atlassian apps. As part of this co-operation, Cydrill has customized its Web application security in Java training to Meta-Inf’s specific foci, and has also mapped the material to the Bugcrowd vulnerability rating taxonomy.

Continuing on this path, we decided to take a closer look at the recent Confluence Server and Data Center vulnerability of Atlassian. Below, we will drill down to the root cause of the vulnerability, providing an analysis of the bug. As always, the interesting thing is what developers can learn from all this, in order to avoid similar problems in their code in the future.

Down the OGNL rabbit hole in Confluence

Discovered by Benny Jacob via the Atlassian bug bounty program, the vulnerability behind the breach is CVE-2021-26084 (CVSS 9.8 – critical severity). Let’s take a look at it both from a top-down and bottom-up perspective!

First, let’s take it from the top, following Trend Micro’s root cause analysis. The vulnerability targets a weakness in the implementation of the Webwork web app framework that Confluence runs on. This framework maps URLs that end with .action to methods within a particular Java class, as defined in an xwork.xml file – that file also defines what HTML pages will be generated depending on the result returned by the Java methods. These generated HTML pages are not static. They use the Velocity template system, which supports expressions written in the Object-Graph Navigational Language (OGNL). OGNL is an implementation of the Jakarta Expression Language and one can theoretically use it to execute arbitrary Java code. This is what makes it especially interesting from the security point of view. Of course, there is validation in place in ognl.OgnlParser.expression() and com.opensymphony.webwork.util.SafeExpressionUtil.containsUnsafeExpression() which prevents someone from doing that through this interface… or so we think.

So how one could exploit this? Let’s take a look at the hacker perspective through a write-up
by @rootxharsh. By looking at changelogs, Atlassian has modified the following two lines in createpage-entervariables.vm (a Velocity template file) when fixing the vulnerability, giving us a hint on where an attacker would look first:

#tag ("Hidden") "name='queryString'" "value='$!queryString'")
#tag ("Hidden") "name='linkCreation'" "value='$linkCreation'")

Thus, when the attacker accessed /pages/doenterpagevariables.action on a vulnerable Confluence server (note that this did not need authentication), the server used this template to render the page. Both tags processed the appropriate user input as OGNL before putting the results into the generated HTML. At this point, $queryString contained the value doenterpagevariables.action, from the URL, thus the attacker was limited when setting its value. However, if the attacker specified a parameter called queryString in the POST request, it would end up overriding the real query string in this context, and the server code would use its value as input for the template – this issue is also known as ‘variable shadowing’.

To actually execute the injection attack, first the attacker had to break out of the value=’$!queryString’ string by injecting a single quote (‘) character. This was detected and prevented by some (weak) input validation in the OgnlParser. But since this validation function only checked for the actual ‘ character, the attacker could just use Unicode escaping on it (\u0027) to get around the protection – that character got through the check and then was decoded into a single quote, allowing the injection to happen. The same encoding trick could be applied to double quotes (\u0022).

Now that the attacker could inject arbitrary OGNL, what could they accomplish? The answer is the dreaded three characters of RCE (Remote Code Execution). OGNL injection has been around for a while, so there are several cheatsheets and guides available for this. The usual process is to use {“”.getClass()} to gain access to the instance of a class, and then use Java reflection functionality like .forName(“java.lang.Runtime”) to execute arbitrary code; optionally the # sign at the beginning of an OGNL statement can be used to access global Java objects.

Validation is good, denylists are bad

However, Confluence had a second layer of protection that tried to prevent RCE: a method called isSafeExpression(). This method checked for certain dangerous strings in an OGNL expression before executing it. We exhibit some code snippets of the function below:

public class SafeExpressionUtil {
  private static final Set SAFE_EXPRESSIONS_CACHE = Collections.newSetFromMap(new ConcurrentHashMap());
  private static final Set UNSAFE_METHOD_NAMES;
  private static final Set UNSAFE_NODE_TYPES;
  private static final Set UNSAFE_PROPERTY_TYPES;
  private static final Set UNSAFE_VARIABLE_NAMES;
  private static final Log log = LogFactory.getLog(SafeExpressionUtil.class);

  static {
    Set set = new HashSet();
    set.add("ognl.ASTStaticMethod");
    // Some other items added to the denylist…
    UNSAFE_NODE_TYPES = Collections.unmodifiableSet(set);
    Set set2 = new HashSet();
    set2.add("class");
    set2.add("classLoader");
    UNSAFE_PROPERTY_NAMES = Collections.unmodifiableSet(set2);
    // create similar denylists for UNSAFE_METHOD_NAMES and UNSAFE_VARIABLE_NAMES…
  }

  public static boolean isSafeExpression(String expression) {
    if (!SAFE_EXPRESSIONS_CACHE.contains(expression)) {
    try {
      Object parsedExpression = OgnlUtil.compile(expression);
      if (parsedExpression instanceof Node) {
        if (containsUnsafeExpression((Node) parsedExpression)) {
          log.warn("Unsafe clause found in [" + expression + "]");
        } else {
          SAFE_EXPRESSIONS_CACHE.add(expression);
        }
      }
    } catch (OgnlException ex) {
      log.debug("Cannot verify safety of OGNL expression", ex);
    }
  }   
// …

There was something fundamentally wrong with both protections so far: they applied denylists to the input. Denylists are inherently dangerous. If the attacker can encode malicious input in a format that’s not on the list, it’ll get through the validation. Just like the first layer of validation only filtered the single quote and not its escaped version, isSafeExpression() only filtered inputs that were explicitly within the UNSAFE_NODE_TYPES, UNSAFE_PROPERTY_NAMES, UNSAFE_METHOD_NAMES and UNSAFE_VARIABLE_NAMES sets. The denylist for UNSAFE_METHOD_NAMES included getClass(), so at first it looked like the attack was not going to be possible – but a creative attacker could just use “”[“class”] instead to achieve the same thing, as posted by Orange Tsai back in 2018.

Putting it all together, all an attacker had to do was to make a POST request to /pages/doenterpagevariables.action with the body consisting of a queryString parameter that had a value like


' #{""["class"].forName("java.lang.Runtime")
.getMethod("getRuntime",null)
.invoke(null,null)
.exec("touch /tmp/you_just_got_haxx0red")} '

or, after encoding the single and double quotes:


queryString=\u0027 #\u0022\u0022 [\u0022class\u0022]
.forName(\u0022java.lang.Runtime\u0022)
.getMethod(\u0022getRuntime\u0022,null).invoke(null,null)
.exec(\u0022touch /tmp/you_just_got_haxx0red\u0022)} \u0027

Of course, the real hack used a much more elaborate attack string – but the mechanism was the same.

Expressions of Power

On a basic level, injection vulnerabilities (now A03 in the brand new OWASP Top Ten 2021) are caused by a fundamental misunderstanding of the difference between data and code. In SQL injection, for instance, the code inserts the input from the user (data) into a SQL query (code), typically through some kind of string concatenation.

The Jakarta Expression Language is somewhat notorious in security circles for being vulnerable to injection. OGNL is a common implementation of the EL. The EL’s capabilities to dynamically manipulate Java objects and execute Java code are impressive. But such functionality is extremely dangerous if exposed to insufficiently validated user input.

Apache Struts, in particular, has suffered from numerous critical OGNL injection vulnerabilities over the years. Many of these are critical severity, with the same 9.8 CVSS score as the Confluence vulnerability (e.g. CVE-2020-17530, CVE-2019-0230, CVE-2017-16861, CVE-2017-14589, and CVE-2017-9791), but some less serious vulnerabilities exploiting OGNL go as far back as 2007 (CVE-2007-4556). An OGNL injection vulnerability in Struts (CVE-2017-5638) was also in the background of the (in)famous Equifax hack of 2017!

Atlassian, Confluence, OGNL, injection,

Of course, OGNL is not the only dangerous EL implementation. SpEL in Spring has also been the target of injection attacks in the past, and SpEL injections can be just as devastating (e.g., see CVE-2020-9301 in Netflix’s Spinnaker).

This isn’t an EL-specific problem, either. Many HTML templating frameworks allow evaluating expressions in dynamically-generated templates with basically identical effects. So much in fact, that there is an entire subtype of injection (server-side template injection, or SSTI) to cover these vulnerabilities. Some common targets of SSTI are Razor (.NET), Jinja2 (Python) and Twig (PHP).

Lessons learnt

As you can see from the long list of CVE entries above, injection is unfortunately still a very common problem. While SQL injection is the most discussed one, there are other types, too: HTML injection (aka Cross-site scripting, or XSS), OS command injection, XPath injection or LDAP injection. And yes, expression language injection and OGNL injection. Luckily, there are best practices. Developers just have to learn and adapt them. Injection is also an input validation issue by nature. Generally, applying an allowlist would have helped in this case as well. But remember: denylists are dangerous!

The story also highlights an unfortunate fact. Expression languages are powerful tools in solving many problems, but great power comes with great responsibility. A too-powerful expression language basically acts as an eval(), and – when not used carefully – can directly turn untrusted data provided by the user (i.e. the attacker) into code, easily leading to Remote Code Execution (RCE).

Furthermore, variable shadowing was yet another potential weakness that led to a successful exploit in this case.

All in all, the Confluence hack showcases yet another issue that one can learn how to avoid in our courses. Ask for a free Webinar to learn more about expression language injection or Server-side Template injection.

It’s all covered!