Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion lib/msf/core/db_manager/import/metasploit_framework/zip.rb
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ def import_msf_zip(args={}, &block)
end

def is_child_of?(target_dir, target)
target.downcase.start_with?(target_dir.downcase)
target_dir = File.expand_path(target_dir)
target = File.expand_path(target)

if Gem.win_platform?
target_dir = target_dir.downcase
target = target.downcase
end

target == target_dir || target.start_with?("#{target_dir}#{File::SEPARATOR}")
end
end
12 changes: 11 additions & 1 deletion lib/msf/core/rpc/v10/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ module RPC

class Service

MAX_REQUEST_SIZE = 10 * 1024 * 1024

attr_accessor :service, :srvhost, :srvport, :uri, :options
attr_accessor :handlers, :default_handler, :tokens, :users, :framework
attr_accessor :dispatcher_timeout, :token_timeout, :debug, :str_encoding
Expand Down Expand Up @@ -57,6 +59,7 @@ def start
self.options[:comm],
self.options[:cert]
)
self.service.max_request_body_size = MAX_REQUEST_SIZE

self.service.add_resource(self.uri, {
'Proc' => Proc.new { |cli, req| on_request_uri(cli, req) },
Expand Down Expand Up @@ -113,6 +116,10 @@ def process(req)
raise ArgumentError, "Invalid Content Type"
end

if req.body.bytesize > MAX_REQUEST_SIZE
raise ArgumentError, "RPC request body is too large (maximum #{MAX_REQUEST_SIZE} bytes)"
end

msg = MessagePack.unpack(req.body)

unless (msg && msg.kind_of?(::Array) && msg.length > 0)
Expand All @@ -121,7 +128,10 @@ def process(req)

msg.map { |a| a.respond_to?(:force_encoding) ? a.force_encoding(self.str_encoding) : a }

group, funct = msg.shift.split(".", 2)
method_name = msg.shift
raise ArgumentError, 'Invalid API method' unless method_name.kind_of?(::String)

group, funct = method_name.split(".", 2)

unless self.handlers[group]
raise ArgumentError, "Unknown API Group: '#{group.inspect}'"
Expand Down
8 changes: 8 additions & 0 deletions lib/rex/proto/http/packet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,21 @@ def parse(buf, opts={})

# Continue on to the body if the header was processed
if(self.state == ParseState::ProcessingBody)
max_body_size = opts[:max_body_size]
if max_body_size && !transfer_chunked && body_bytes_left > max_body_size
raise ArgumentError, "HTTP body is too large (maximum #{max_body_size} bytes)"
end

# Chunked encoding sets the parsing state on its own.
# HEAD requests can return immediately.
orig_method = opts.fetch(:orig_method) { '' }
if (self.body_bytes_left == 0 && (!self.transfer_chunked || orig_method == 'HEAD'))
self.state = ParseState::Completed
else
parse_body
if max_body_size && body.bytesize > max_body_size
raise ArgumentError, "HTTP body is too large (maximum #{max_body_size} bytes)"
end
end
end
rescue
Expand Down
4 changes: 3 additions & 1 deletion lib/rex/proto/http/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ module Http
###
class Server

attr_accessor :max_request_body_size

include Proto

#
Expand Down Expand Up @@ -247,7 +249,7 @@ def on_client_data(cli)
raise ::EOFError if not data
raise ::EOFError if data.empty?

case cli.request.parse(data)
case cli.request.parse(data, max_body_size: max_request_body_size)
when Packet::ParseCode::Completed
dispatch_request(cli, cli.request)
cli.reset_cli
Expand Down
62 changes: 53 additions & 9 deletions lib/snmp/mib.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
#

require 'snmp/varbind'
require 'snmp/python_literal_parser'
require 'fileutils'
require 'open3'
require 'yaml'

module SNMP
Expand All @@ -24,6 +26,10 @@ class MIB
MODULE_EXT = 'yaml'

class ModuleNotLoadedError < RuntimeError; end
class InvalidMIBError < RuntimeError; end

MAX_CONVERTER_OUTPUT_SIZE = 10 * 1024 * 1024
MODULE_NAME_PATTERN = /\A[A-Za-z][A-Za-z0-9-]*\z/

class << self
##
Expand Down Expand Up @@ -55,11 +61,18 @@ class << self
def import_module(module_file, mib_dir=DEFAULT_MIB_PATH)
raise "smidump tool must be installed" unless import_supported?
FileUtils.makedirs mib_dir
mib_hash = `smidump -f python #{module_file}`
mib_hash, error, status = capture_command('smidump', '-f', 'python', module_file.to_s)
unless status.success?
warn "*** Import failed for: #{module_file}: #{error.strip} ***"
return nil
end
mib = eval_mib_data(mib_hash)
if mib
module_name = mib["moduleName"]
raise "#{module_file}: invalid file format; no module name" unless module_name
unless module_name.is_a?(String) && MODULE_NAME_PATTERN.match?(module_name)
raise InvalidMIBError, "#{module_file}: invalid module name"
end
if mib["nodes"]
oid_hash = {}
mib["nodes"].each { |key, value| oid_hash[key] = value["oid"] }
Expand Down Expand Up @@ -118,14 +131,45 @@ def list_imported(regex=//, mib_dir=DEFAULT_MIB_PATH)
private

def eval_mib_data(mib_hash)
ruby_hash = mib_hash.
gsub(':', '=>'). # fix hash syntax
gsub('(', '[').gsub(')', ']'). # fix tuple syntax
sub('FILENAME =', 'filename ='). # get rid of constants
sub('MIB =', 'mib =')
mib = nil
eval(ruby_hash)
mib
raise InvalidMIBError, 'MIB converter output is too large' if mib_hash.bytesize > MAX_CONVERTER_OUTPUT_SIZE

assignment = mib_hash.match(/(?:\A|\n)\s*MIB\s*=\s*/)
raise InvalidMIBError, 'MIB converter output does not contain a MIB assignment' unless assignment

SNMP::PythonLiteralParser.new(mib_hash, offset: assignment.end(0)).parse
rescue SNMP::PythonLiteralParser::ParseError => e
raise InvalidMIBError, e.message
end

def capture_command(*command)
output = String.new
error = String.new
status = nil

Open3.popen3(*command) do |stdin, stdout, stderr, wait_thread|
stdin.close
streams = { stdout => [output, MAX_CONVERTER_OUTPUT_SIZE], stderr => [error, 65_536] }

until streams.empty?
IO.select(streams.keys)&.first&.each do |stream|
begin
chunk = stream.read_nonblock(16_384)
buffer, limit = streams.fetch(stream)
if buffer.bytesize + chunk.bytesize > limit
Process.kill('KILL', wait_thread.pid)
raise InvalidMIBError, 'MIB converter output is too large'
end
buffer << chunk
rescue EOFError
streams.delete(stream)
stream.close
end
end
end
status = wait_thread.value
end

[output, error, status]
end
end # class methods

Expand Down
185 changes: 185 additions & 0 deletions lib/snmp/python_literal_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# frozen_string_literal: true

module SNMP
# Parses the primitive Python literal subset emitted by `smidump -f python`.
class PythonLiteralParser
MAX_DEPTH = 64

class ParseError < StandardError; end

# @param source [String] Python literal source
# @param offset [Integer] offset at which the literal starts
def initialize(source, offset: 0)
@source = source
@offset = offset
end

# @return [Hash] parsed MIB dictionary
# @raise [ParseError] if source contains anything except primitive literals
def parse
value = parse_value(0)
skip_ignored
raise ParseError, 'Unexpected content after MIB data' unless eof?
raise ParseError, 'MIB data must be a dictionary' unless value.is_a?(Hash)

value
end

private

def parse_value(depth)
raise ParseError, 'MIB data is nested too deeply' if depth > MAX_DEPTH

skip_ignored
case current
when '{' then parse_hash(depth + 1)
when '[', '(' then parse_sequence(depth + 1)
when "'", '"' then parse_string
when '-', '0'..'9' then parse_integer
else parse_keyword
end
end

def parse_hash(depth)
consume('{')
result = {}
skip_ignored
return consume('}') && result if current == '}'

loop do
key = parse_value(depth)
raise ParseError, 'MIB dictionary keys must be strings' unless key.is_a?(String)

skip_ignored
consume(':')
result[key] = parse_value(depth)
skip_ignored
break if current == '}'

consume(',')
skip_ignored
break if current == '}'
end
consume('}')
result
end

def parse_sequence(depth)
closer = current == '[' ? ']' : ')'
@offset += 1
result = []
skip_ignored
return consume(closer) && result if current == closer

loop do
result << parse_value(depth)
skip_ignored
break if current == closer

consume(',')
skip_ignored
break if current == closer
end
consume(closer)
result
end

def parse_string
quote = current
consume(quote)
result = String.new
until eof?
char = take
return result if char == quote

unless char == '\\'
result << char
next
end

raise ParseError, 'Unterminated escape sequence' if eof?

result << unescape(take)
end
raise ParseError, 'Unterminated string'
end

def unescape(escaped)
return escaped if ['\\', "'", '"'].include?(escaped)
return [read_digits(2, 16, '\\x')].pack('U') if escaped == 'x'
return [read_digits(4, 16, '\\u')].pack('U') if escaped == 'u'
return [read_octal(escaped)].pack('U') if escaped.match?(/[0-7]/)

replacements = { 'n' => "\n", 'r' => "\r", 't' => "\t", 'b' => "\b", 'f' => "\f", 'v' => "\v", 'a' => "\a" }
replacements.fetch(escaped) { raise ParseError, "Unsupported escape sequence \\#{escaped}" }
end

def read_digits(length, base, prefix)
digits = @source[@offset, length]
unless digits&.length == length && digits.match?(base == 16 ? /\A[0-9A-Fa-f]+\z/ : /\A[0-7]+\z/)
raise ParseError, "Invalid #{prefix} escape sequence"
end

@offset += length
digits.to_i(base)
end

def read_octal(first_digit)
digits = first_digit
2.times do
break unless current&.match?(/[0-7]/)

digits << take
end
digits.to_i(8)
end

def parse_integer
token = @source[@offset..].match(/\A-?\d+/)&.[](0)
raise ParseError, 'Invalid integer' unless token

@offset += token.length
Integer(token, 10)
end

def parse_keyword
token = @source[@offset..].match(/\A[A-Za-z_][A-Za-z0-9_]*/)&.[](0)
@offset += token.length if token
return nil if token == 'None'
return true if token == 'True'
return false if token == 'False'

raise ParseError, "Unsupported MIB expression #{token.inspect}"
end

def skip_ignored
loop do
@offset += 1 while !eof? && @source[@offset].match?(/\s/)
break unless current == '#'

@offset += 1 until eof? || current == "\n"
end
end

def consume(expected)
raise ParseError, "Expected #{expected.inspect}" unless current == expected

@offset += 1
true
end

def take
char = current
@offset += 1
char
end

def current
@source[@offset]
end

def eof?
@offset >= @source.length
end
end
end
Loading