Attacker Value
Moderate
(1 user assessed)
Exploitability
Moderate
(1 user assessed)
User Interaction
Unknown
Privileges Required
Unknown
Attack Vector
Unknown
0

CVE-2018-20434 - LibreNMS Addhost Command Injection

Disclosure Date: April 24, 2019 Last updated February 13, 2020
Add MITRE ATT&CK tactics and techniques that apply to this CVE.

Description

LibreNMS 1.46 allows remote attackers to execute arbitrary OS commands by using the $_POST['community'] parameter to html/pages/addhost.inc.php during creation of a new device, and then making a /ajax_output.php?id=capture&format=text&type=snmpwalk&hostname=localhost request that triggers html/includes/output/capture.inc.php command mishandling.

Add Assessment

1
Ratings
  • Attacker Value
    Medium
  • Exploitability
    Medium
Technical Analysis

Useful exploit with a caveat. This exploit takes more effort to execute given that authentication is required first.

According to the CVE listing on NVD, shell commands can be passed through the _POST['community'] parameter to html/pages/addhost.inc.php, which deals with the creation of new devices. After successfully creating a device, a request can be sent to ajax_output.php, which triggers the actual execution of code through html/includes/output/capture.inc.php.

If the community parameter is set when a request is made to addhost.inc.php, then community is passed to the clean() function with the second argument set to false.

if ($_POST['community']) 
{
   $config['snmp']['community'] = array(clean($_POST['community'], false));
}

The clean() function is located in includes/common.php. Here’s what it looks like in version 1.46:

function clean($value, $strip_tags = true)
{
    if ($strip_tags === true) {
        return strip_tags(mres($value));
    } else {
        return mres($value);
    }
}

In this particular call to clean(), the $strip_tags value is set to false, meaning that the community parameter is acted upon by the mres() function, then returned. The mres() function:

function mres($string)
{
    return $string;
    
    global $database_link;
    return mysqli_real_escape_string($database_link, $string);
}

The community parameter is simply returned without any modifications.

From here, we can see that the community parameter is set through a POST request to addhost.inc.php, and it is unsanitized. Assuming that unwanted input is passed into the community parameter, now the goal is to see how a request to ajax_output.php will trigger code execution.

In ajax_output.php, the id is checked and is used to require another file. In this case, that would be capture.inc.php.

if (isset($id))
{
    require $config['install_dir'] . "/html/includes/output/$id.inc.php";
}

The functionality in capture.inc.php runs a command that is determined by the type parameter and either prints the output of the command or saves the output to a file. Initially, the type parameter is checked against three different values. If the type parameter is snmp walk, the command becomes the output of the gen_snmp_walk() function.

$type = $_REQUEST['type'];

switch ($type)
{
    case 'poller':
        ...
    case 'snmpwalk':
        $device = device_by_name(mres($hostname));
        $cmd = gen_snmpwalk_cmd($device, '.', ' -OUneb');
    	...
    case 'discovery':
        ...
    default:
        ...
}

The gen_snmp_walk() function is located in the snmp.inc.php file. gen_snmp_walk() first checks the version that was passed in the addhost POST request earlier and then returns the result of calling gen_snmp_cmd(). The gen_snmp_cmd() also resides in the snmp.inc.php file, and this function is where the bulk of the command used in capture.inc.php is created.

The first addition to $cmd is set to the result of calling the snmp_gen_auth() function.

function gen_snmp_cmd($cmd, $device, $oids, $options = null, $mib = null, $mibdir = null)
{
    ...
    $cmd .= snmp_gen_auth($device);
    ...
}

snmp_gen_auth() further builds the $cmd variable by checking snmpver. If that value is either v2c or v1, then the unsanitized community parameter is added to the command and then returned.

} elseif ($device['snmpver'] === 'v2c' or $device['snmpver'] === 'v1') {
        $cmd  = " -".$device['snmpver'];
        $cmd .= " -c '".$device['community']."'";
...
return $cmd;

Now that there is a command that contains unsanitized input, code execution is the last step.

As was stated previously, the functionality in capture.inc.php generates a command to run and either prints the output of that command or saves the output to a file. The functionality that runs the command checks the format parameter passed in the request made to ajax_output.php described earlier. If the format parameter is set to text, then this code block will be executed:

if ($_GET['format'] == 'text') {
    header("Content-type: text/plain");
    header('X-Accel-Buffering: no');

    if (($fp = popen($cmd, "r"))) {
        while (!feof($fp)) {
            $line = stream_get_line($fp, 1024, PHP_EOL);
            echo preg_replace('/\033\[[\d;]+m/', '', $line) . PHP_EOL;
            ob_flush();
            flush(); // you have to flush buffer
        }
        fclose($fp);
    }

The $cmd variable that now contains the unsanitized community parameter gets passed to the popen() function and executed.

General Information

Additional Info

Technical Analysis