From fc55d74b8093c8396b7d5f7b9e8a9c6d65c4970c Mon Sep 17 00:00:00 2001 From: A Galway Date: Wed, 24 Feb 2021 17:01:57 +0000 Subject: [PATCH 1/5] http-client cookie jar support and tests --- Gemfile.lock | 5 + .../core/exploit/remote/http/http_cookie.rb | 195 +++++ .../exploit/remote/http/http_cookie_jar.rb | 104 +++ lib/msf/core/exploit/remote/http_client.rb | 35 +- metasploit-framework.gemspec | 2 + .../multi/http/gitea_git_hooks_rce.rb | 39 +- .../multi/http/gitlab_file_read_rce.rb | 48 +- .../exploits/multi/http/gogs_git_hooks_rce.rb | 66 +- .../multi/http/horizontcms_upload_exec.rb | 7 +- .../remote/http/http_cookie_jar_spec.rb | 270 ++++++ .../remote/remote/http/http_cookie_spec.rb | 821 ++++++++++++++++++ spec/spec_helper.rb | 4 + 12 files changed, 1486 insertions(+), 110 deletions(-) create mode 100644 lib/msf/core/exploit/remote/http/http_cookie.rb create mode 100644 lib/msf/core/exploit/remote/http/http_cookie_jar.rb create mode 100644 spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb create mode 100644 spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 7925673e04..abb87b94fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -21,6 +21,7 @@ PATH faye-websocket filesize hrr_rb_ssh (= 0.3.0.pre2) + http-cookie irb jsobfu json @@ -158,6 +159,8 @@ GEM dnsruby (1.61.5) simpleidn (~> 0.1) docile (1.3.5) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) ed25519 (1.2.4) em-http-request (1.1.7) addressable (>= 2.3.4) @@ -189,6 +192,8 @@ GEM hashery (2.1.2) hrr_rb_ssh (0.3.0.pre2) ed25519 (~> 1.2) + http-cookie (1.0.3) + domain_name (~> 0.5) http_parser.rb (0.6.0) i18n (1.8.10) concurrent-ruby (~> 1.0) diff --git a/lib/msf/core/exploit/remote/http/http_cookie.rb b/lib/msf/core/exploit/remote/http/http_cookie.rb new file mode 100644 index 0000000000..843511efb6 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/http_cookie.rb @@ -0,0 +1,195 @@ +require 'http/cookie' + +module Msf + class Exploit + class Remote + module HTTP + # Acts as a wrapper for the 3rd party Cookie (http-cookie) + class HttpCookie + include Comparable + + def initialize(name, value = nil, **attr_hash) + if name.is_a?(::HTTP::Cookie) + @cookie = name + elsif name && value && attr_hash + @cookie = ::HTTP::Cookie.new(name, value, **attr_hash) + elsif name && value + @cookie = ::HTTP::Cookie.new(name, value) + else + @cookie = ::HTTP::Cookie.new(name) + end + end + + def name + @cookie.name + end + + def name=(name) + @cookie.name = name.to_s + end + + def value + @cookie.value + end + + def value=(value) + if value.nil? || value.is_a?(String) + @cookie.value = value + else + @cookie.value = value.to_s + end + end + + def max_age + @cookie.max_age + end + + def max_age=(max_age) + if max_age.nil? || max_age.is_a?(Integer) + @cookie.max_age = max_age + else + @cookie.max_age = Integer(max_age) + end + end + + def expires + @cookie.expires + end + + def expires=(expires) + if expires.nil? || expires.is_a?(Time) + @cookie.expires = expires + else + t = Time.parse(expires) + @cookie.expires = t + end + end + + def expired?(time = Time.now) + @cookie.expired?(time) + end + + def path + @cookie.path + end + + def path=(path) + if path.nil? || path.is_a?(String) + @cookie.path = path + else + @cookie.path = path.to_s + end + end + + def secure + @cookie.secure + end + + def secure=(secure) + @cookie.secure = !!secure + end + + def httponly + @cookie.httponly + end + + def httponly=(httponly) + @cookie.httponly = !!httponly + end + + # def for_domain + # @cookie.for_domain + # end + # + # def for_domain= for_domain + # @cookie.for_domain= !!for_domain + # end + + # def origin + # @cookie.origin + # end + # + # def origin= origin + # if origin.kind_of?(URI) + # @cookie.origin= origin + # else + # @cookie.origin= origin.to_s + # end + # end + + def domain + @cookie.domain + end + + def domain=(domain) + if domain.nil? || domain.is_a?(DomainName) + @cookie.domain = domain + else + @cookie.domain = DomainName(domain.to_s) + end + end + + def accessed_at + @cookie.accessed_at + end + + def created_at + @cookie.created_at + end + + def accessed_at=(time) + if time.nil? || time.is_a?(Time) + @cookie.accessed_at = time + else + @cookie.accessed_at = Time.parse(time) + end + end + + def created_at=(time) + if time.nil? || time.is_a?(Time) + @cookie.created_at = time + else + @cookie.created_at = Time.parse(time) + end + end + + def session? + @cookie.session? + end + + def acceptable? + @cookie.acceptable? + end + + # Tests if it is OK to send this cookie to a given `uri`. An + # ArgumentError is raised if the cookie's domain is unknown. + def valid_for_uri?(uri) + return false if uri.nil? + raise ArgumentError, 'cannot tell if this cookie is valid as domain is nil' if domain.nil? + + @cookie.valid_for_uri?(uri) + end + + # Tests if it is OK to accept this cookie if it is sent from a given + # URI/URL, `uri`. + def acceptable_from_uri?(uri) + return false if uri.nil? + + @cookie.acceptable_from_uri?(uri) + end + + def <=>(other) + @cookie <=> other + end + + # Returns a string for use in the Cookie header, i.e. `name=value` + # or `name="value"`. + def cookie_value + @cookie.cookie_value + end + alias to_s cookie_value + end + end + end + end +end diff --git a/lib/msf/core/exploit/remote/http/http_cookie_jar.rb b/lib/msf/core/exploit/remote/http/http_cookie_jar.rb new file mode 100644 index 0000000000..a99df923fb --- /dev/null +++ b/lib/msf/core/exploit/remote/http/http_cookie_jar.rb @@ -0,0 +1,104 @@ +# 3rd party gems +require 'http/cookie_jar/hash_store' +require 'http/cookie_jar' +require 'http/cookie' +require 'domain_name' + +module Msf + class Exploit + class Remote + module HTTP + # Acts as a wrapper for the 3rd party CookieJar (http-cookie) + class HttpCookieJar + def initialize + @cookie_jar = ::HTTP::CookieJar.new({ + store: HashStoreWithoutAutomaticExpiry + }) + end + + def add(cookie) + raise TypeError, "Passed cookie is of class '#{cookie.class}' and not a subclass of '#{HttpCookie.class}" unless cookie.is_a?(HttpCookie) + + @cookie_jar.add(cookie) + self + end + + def delete(cookie) + return if @cookie_jar.cookies.empty? + raise TypeError, "Passed cookie is of class '#{cookie.class}' and not a subclass of '#{HttpCookie.class}" unless cookie.is_a?(HttpCookie) + + @cookie_jar.delete(cookie) + self + end + + # Iterates over all cookies that are not expired in no particular + # order. + # + # `uri` specifies a URI/URL indicating the destination of the cookies + # being selected. Every cookie yielded should be good to send to the + # given URI, i.e. cookie.valid_for_uri?(uri) evaluates to true. + def cookies(uri = nil) + @cookie_jar.cookies(uri) + end + + def [](c) + @cookie_jar.cookies(uri)[c] + end + + def clear + @cookie_jar.clear + end + + def cleanup(expire_all = false) + @cookie_jar.cleanup(expire_all) + end + + def empty?(url = nil) + @cookie_jar.empty?(url) + end + + def parse(set_cookie_header, origin_url, options = nil) + parsed_cookies = HTTP::Cookie.parse(set_cookie_header, origin_url, options) + parsed_cookies.each { |c| add(HttpCookie.new(c)) } + end + + end + + class HashStoreWithoutAutomaticExpiry < ::HTTP::CookieJar::HashStore + + # +::HTTP::CookieJar::HashStore+ expires cookies as it iterates. We don't want that. + def each(uri = nil) + if uri + thost = DomainName.new(uri.host) + tpath = uri.path + @jar.each do |domain, paths| + next unless thost.cookie_domain?(domain) + + paths.each do |path, hash| + next unless HTTP::Cookie.path_match?(path, tpath) + + hash.each do |_name, cookie| + if cookie.valid_for_uri?(uri) + yield cookie + end + end + end + end + else + synchronize do + @jar.each do |_domain, paths| + paths.each do |_path, hash| + hash.each do |_name, cookie| + yield cookie + end + end + end + end + end + self + end + end + end + end + end +end diff --git a/lib/msf/core/exploit/remote/http_client.rb b/lib/msf/core/exploit/remote/http_client.rb index da98994c76..0046c557ab 100644 --- a/lib/msf/core/exploit/remote/http_client.rb +++ b/lib/msf/core/exploit/remote/http_client.rb @@ -2,6 +2,7 @@ require 'uri' require 'digest' +require 'msf/core/exploit/remote/http/http_cookie_jar' module Msf @@ -91,7 +92,7 @@ module Exploit::Remote::HttpClient register_autofilter_services(%W{ http https }) # Initialize an empty cookie jar to keep cookies - self.cookie_jar = Set.new + self.cookie_jar = HttpCookieJar.new end def deregister_http_client_options @@ -373,22 +374,36 @@ module Exploit::Remote::HttpClient # Connects to the server, creates a request, sends the request, # reads the response # + # If a +HTTP::CookieJar+ instance is passed in the +opts+ dict under a 'cookie' key, said CookieJar will be used in + # the request instead of the module +cookie_jar+ + # # Passes `opts` through directly to {Rex::Proto::Http::Client#request_cgi}. # Set `opts['keep_cookies']` to keep cookies from responses for reuse in requests. + # Cookies returned by the server will be stored in +cookie_jar+ + # + # +expire_cookies+ will control if +cleanup+ is called on any passed +::HttpCookieJar+ or the client cookiejar # # @return (see Rex::Proto::Http::Client#send_recv)) - def send_request_cgi(opts = {}, timeout = 20, disconnect = true) - if cookie_jar.any? - opts = { 'cookie' => cookie_jar.to_a.join(' ') }.merge(opts) + def send_request_cgi(opts = {}, timeout = 20, disconnect = true, expire_cookies = true) + origin_url = "http#{ssl ? 's' : ''}://#{peer}" + + if opts.has_key?('cookie') + if opts['cookie'].is_a?(HttpCookieJar) + cookie_jar.cleanup if expire_cookies + opts.merge({ 'cookie' => opts['cookie'].cookies.join('; ') }) + else + opts.merge({ 'cookie' => opts['cookie'].to_s }) + end + elsif !cookie_jar.empty?(origin_url) + cookie_jar.cleanup if expire_cookies + opts = opts.merge({ 'cookie' => cookie_jar.cookies(origin_url).join('; ') }) end res = send_request_raw(opts.merge(cgi: true), timeout, disconnect) - return unless res if opts['keep_cookies'] && res.headers['Set-Cookie'].present? - # XXX: CGI::Cookie (get_cookies_parsed) is hella broken - cookie_jar.merge(res.get_cookies.split(' ')) + cookie_jar.parse(res.headers['Set-Cookie'], origin_url) end res @@ -890,10 +905,12 @@ module Exploit::Remote::HttpClient } end + attr_reader :cookie_jar + protected - attr_accessor :client - attr_accessor :cookie_jar + private + attr_writer :cookie_jar end end diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index c5c41554c9..39c047e7e5 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -117,6 +117,8 @@ Gem::Specification.new do |spec| if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.3.0') spec.add_runtime_dependency 'xmlrpc' end + # Gem for handling Cookies + spec.add_runtime_dependency 'http-cookie' # # File Parsing Libraries diff --git a/modules/exploits/multi/http/gitea_git_hooks_rce.rb b/modules/exploits/multi/http/gitea_git_hooks_rce.rb index 4dc9b4a235..ca580545e8 100644 --- a/modules/exploits/multi/http/gitea_git_hooks_rce.rb +++ b/modules/exploits/multi/http/gitea_git_hooks_rce.rb @@ -123,7 +123,8 @@ class MetasploitModule < Msf::Exploit::Remote def check res = send_request_cgi( 'method' => 'GET', - 'uri' => normalize_uri(target_uri.path) + 'uri' => normalize_uri(target_uri.path), + 'keep_cookies' => true ) unless res return CheckCode::Unknown('Target did not respond to check.') @@ -198,7 +199,8 @@ class MetasploitModule < Msf::Exploit::Remote vprint_status('Get "csrf" value') res = send_request_cgi( 'method' => 'GET', - 'uri' => normalize_uri(uri) + 'uri' => normalize_uri(uri), + 'keep_cookies' => true ) unless res fail_with(Failure::Unreachable, 'Unable to get the CSRF token') @@ -240,7 +242,7 @@ class MetasploitModule < Msf::Exploit::Remote def gitea_create_repo uri = normalize_uri(datastore['TARGETURI'], '/repo/create') - res = send_request_cgi('method' => 'GET', 'uri' => uri) + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'keep_cookies' => true) unless res fail_with(Failure::Unreachable, "Unable to reach #{uri}") end @@ -305,7 +307,7 @@ class MetasploitModule < Msf::Exploit::Remote uri = normalize_uri(datastore['USERNAME'], @repo_name, '/_new/master') filename = "#{Rex::Text.rand_text_alpha(4..8)}.txt" - res = send_request_cgi('method' => 'GET', 'uri' => uri) + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'keep_cookies' => true) unless res fail_with(Failure::Unreachable, "Unable to reach #{uri}") end @@ -332,35 +334,6 @@ class MetasploitModule < Msf::Exploit::Remote nil end - # Hook the HTTP client method to add specific cookie management logic - def send_request_cgi(opts, timeout = 20) - res = super - - return unless res - - # HTTP client does not handle cookies with the same name correctly. It adds - # them instead of substituing the old value with the new one. - unless res.get_cookies.empty? - cookie_jar_hash = cookie_jar_to_hash - cookies_from_response = cookie_jar_to_hash(res.get_cookies.split(' ')) - cookie_jar_hash.merge!(cookies_from_response) - cookie_jar_updated = cookie_jar_hash.each_with_object(Set.new) do |cookie, set| - set << "#{cookie[0]}=#{cookie[1]}" - end - cookie_jar.clear - cookie_jar.merge(cookie_jar_updated) - end - - res - end - - def cookie_jar_to_hash(jar = cookie_jar) - jar.each_with_object({}) do |cookie, cookie_hash| - name, value = cookie.split('=') - cookie_hash[name] = value - end - end - def cleanup super return unless @need_cleanup diff --git a/modules/exploits/multi/http/gitlab_file_read_rce.rb b/modules/exploits/multi/http/gitlab_file_read_rce.rb index aa13e52c83..bb2cbc1954 100644 --- a/modules/exploits/multi/http/gitlab_file_read_rce.rb +++ b/modules/exploits/multi/http/gitlab_file_read_rce.rb @@ -57,19 +57,21 @@ class MetasploitModule < Msf::Exploit::Remote class GitLabClient def initialize(http_client) @http_client = http_client - @cookie_jar = {} end def sign_in(username, password) + @http_client.cookie_jar.clear + sign_in_path = '/users/sign_in' + csrf_token = extract_csrf_token( path: sign_in_path, regex: %r{action="/users/sign_in".*name="authenticity_token"\s+value="([^"]+)"} ) - res = http_client.send_request_cgi({ + res = @http_client.send_request_cgi({ 'method' => 'POST', 'uri' => '/users/sign_in', - 'cookie' => cookie, + 'keep_cookies' => true, 'vars_post' => { 'utf8' => '✓', 'authenticity_token' => csrf_token, @@ -89,8 +91,6 @@ class MetasploitModule < Msf::Exploit::Remote raise GitLabClientException, 'Login not successful. The account may need activated. Verify login works manually.' end - merge_cookie_jar(res) - current_user end @@ -98,7 +98,7 @@ class MetasploitModule < Msf::Exploit::Remote res = http_client.send_request_cgi({ 'method' => 'GET', 'uri' => '/api/v4/user', - 'cookie' => cookie + 'keep_cookies' => true }) if res.nil? || res.body.nil? @@ -114,7 +114,7 @@ class MetasploitModule < Msf::Exploit::Remote res = http_client.send_request_cgi({ 'method' => 'GET', 'uri' => '/api/v4/version', - 'cookie' => cookie + 'keep_cookies' => true }) if res.nil? || res.body.nil? @@ -138,7 +138,7 @@ class MetasploitModule < Msf::Exploit::Remote res = http_client.send_request_cgi({ 'method' => 'POST', 'uri' => create_project_path, - 'cookie' => cookie, + 'keep_cookies' => true, 'vars_post' => { 'utf8' => '✓', 'authenticity_token' => csrf_token, @@ -159,8 +159,6 @@ class MetasploitModule < Msf::Exploit::Remote raise GitLabClientException, "Unexpected HTTP #{res.code} response." end - merge_cookie_jar(res) - project(user: user, project_name: project_name) end @@ -169,7 +167,7 @@ class MetasploitModule < Msf::Exploit::Remote res = http_client.send_request_cgi({ 'method' => 'GET', 'uri' => project_path, - 'cookie' => cookie + 'keep_cookies' => true }) if res.nil? || res.body.nil? raise GitLabClientException, 'Empty response. Please validate RHOST' @@ -198,7 +196,7 @@ class MetasploitModule < Msf::Exploit::Remote res = http_client.send_request_cgi({ 'method' => 'POST', 'uri' => delete_project_path, - 'cookie' => cookie, + 'keep_cookies' => true, 'vars_post' => { 'utf8' => '✓', 'authenticity_token' => csrf_token, @@ -226,7 +224,7 @@ class MetasploitModule < Msf::Exploit::Remote res = http_client.send_request_cgi({ 'method' => 'POST', 'uri' => create_issue_path, - 'cookie' => cookie, + 'keep_cookies' => true, 'vars_post' => { 'utf8' => '✓', 'authenticity_token' => csrf_token, @@ -246,7 +244,6 @@ class MetasploitModule < Msf::Exploit::Remote raise GitLabClientException, "Unexpected HTTP #{res.code} response." end - merge_cookie_jar(res) issue_id = res.body[%r{You are being redirected}, 1] issue.merge({ @@ -267,7 +264,7 @@ class MetasploitModule < Msf::Exploit::Remote res = http_client.send_request_cgi({ 'method' => 'POST', 'uri' => move_issue_path, - 'cookie' => cookie, + 'keep_cookies' => true, 'ctype' => 'application/json', 'headers' => { 'X-CSRF-Token' => csrf_token, @@ -296,7 +293,7 @@ class MetasploitModule < Msf::Exploit::Remote res = http_client.send_request_cgi({ 'method' => 'GET', 'uri' => "#{project['path']}/#{path}", - 'cookie' => cookie + 'keep_cookies' => true }) if res.nil? || res.body.nil? @@ -316,7 +313,7 @@ class MetasploitModule < Msf::Exploit::Remote res = http_client.send_request_cgi({ 'method' => 'GET', 'uri' => path, - 'cookie' => cookie + 'keep_cookies' => true }) if res.nil? || res.body.nil? @@ -325,7 +322,6 @@ class MetasploitModule < Msf::Exploit::Remote raise GitLabClientException, "Unexpected HTTP #{res.code} response." end - merge_cookie_jar(res) token = res.body[regex, 1] if token.nil? raise GitLabClientException, 'Could not successfully extract CSRF token' @@ -333,17 +329,6 @@ class MetasploitModule < Msf::Exploit::Remote token end - - def cookie - return nil if @cookie_jar.empty? - - @cookie_jar.map { |(k, v)| "#{k}=#{v}" }.join(' ') - end - - def merge_cookie_jar(res) - new_cookies = Hash[res.get_cookies.split(' ').map { |x| x.split('=') }] - @cookie_jar.merge!(new_cookies) - end end def initialize(info = {}) @@ -554,11 +539,12 @@ class MetasploitModule < Msf::Exploit::Remote secret_key_base = read_secret_key_base payload = build_payload - signed_cookie = sign_payload(secret_key_base, payload) + signed_cookie_value = sign_payload(secret_key_base, payload) + send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'GET', - 'cookie' => "experimentation_subject_id=#{signed_cookie}" + 'cookie' => "experimentation_subject_id=#{signed_cookie_value}" }) end end diff --git a/modules/exploits/multi/http/gogs_git_hooks_rce.rb b/modules/exploits/multi/http/gogs_git_hooks_rce.rb index 9ec7d69289..e6a4f990c0 100644 --- a/modules/exploits/multi/http/gogs_git_hooks_rce.rb +++ b/modules/exploits/multi/http/gogs_git_hooks_rce.rb @@ -121,7 +121,8 @@ class MetasploitModule < Msf::Exploit::Remote def check res = send_request_cgi( 'method' => 'GET', - 'uri' => normalize_uri(target_uri.path) + 'uri' => normalize_uri(target_uri.path), + 'keep_cookies' => true ) unless res return CheckCode::Unknown('Target did not respond to check.') @@ -188,7 +189,8 @@ class MetasploitModule < Msf::Exploit::Remote vprint_status('Get "csrf" value') res = send_request_cgi( 'method' => 'GET', - 'uri' => normalize_uri(uri) + 'uri' => normalize_uri(uri), + 'keep_cookies' => true ) unless res fail_with(Failure::Unreachable, 'Unable to get the CSRF token') @@ -230,7 +232,7 @@ class MetasploitModule < Msf::Exploit::Remote def gogs_create_repo uri = normalize_uri(datastore['TARGETURI'], '/repo/create') - res = send_request_cgi('method' => 'GET', 'uri' => uri) + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'keep_cookies' => true) unless res fail_with(Failure::Unreachable, "Unable to reach #{uri}") end @@ -292,7 +294,7 @@ class MetasploitModule < Msf::Exploit::Remote uri = normalize_uri(datastore['USERNAME'], @repo_name, '/_new/master') filename = "#{Rex::Text.rand_text_alpha(4..8)}.txt" - res = send_request_cgi('method' => 'GET', 'uri' => uri) + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'keep_cookies' => true) unless res fail_with(Failure::Unreachable, "Unable to reach #{uri}") end @@ -319,34 +321,34 @@ class MetasploitModule < Msf::Exploit::Remote nil end - # Hook the HTTP client method to add specific cookie management logic - def send_request_cgi(opts, timeout = 20) - res = super - - return unless res - - # HTTP client does not handle cookies with the same name correctly. It adds - # them instead of substituing the old value with the new one. - unless res.get_cookies.empty? - cookie_jar_hash = cookie_jar_to_hash - cookies_from_response = cookie_jar_to_hash(res.get_cookies.split(' ')) - cookie_jar_hash.merge!(cookies_from_response) - cookie_jar_updated = cookie_jar_hash.each_with_object(Set.new) do |cookie, set| - set << "#{cookie[0]}=#{cookie[1]}" - end - cookie_jar.clear - cookie_jar.merge(cookie_jar_updated) - end - - res - end - - def cookie_jar_to_hash(jar = cookie_jar) - jar.each_with_object({}) do |cookie, cookie_hash| - name, value = cookie.split('=') - cookie_hash[name] = value - end - end + # # Hook the HTTP client method to add specific cookie management logic + # def send_request_cgi(opts, timeout = 20) + # res = super + # + # return unless res + # + # # HTTP client does not handle cookies with the same name correctly. It adds + # # them instead of substituing the old value with the new one. + # unless res.get_cookies.empty? + # cookie_jar_hash = cookie_jar_to_hash + # cookies_from_response = cookie_jar_to_hash(res.get_cookies.split(' ')) + # cookie_jar_hash.merge!(cookies_from_response) + # cookie_jar_updated = cookie_jar_hash.each_with_object(Set.new) do |cookie, set| + # set << "#{cookie[0]}=#{cookie[1]}" + # end + # cookie_jar.clear + # cookie_jar.merge(cookie_jar_updated) + # end + # + # res + # end + # + # def cookie_jar_to_hash(jar = cookie_jar) + # jar.each_with_object({}) do |cookie, cookie_hash| + # name, value = cookie.split('=') + # cookie_hash[name] = value + # end + # end def cleanup super diff --git a/modules/exploits/multi/http/horizontcms_upload_exec.rb b/modules/exploits/multi/http/horizontcms_upload_exec.rb index c2ce33e4c9..7be7db5c01 100644 --- a/modules/exploits/multi/http/horizontcms_upload_exec.rb +++ b/modules/exploits/multi/http/horizontcms_upload_exec.rb @@ -132,7 +132,7 @@ class MetasploitModule < Msf::Exploit::Remote return CheckCode::Safe("Target is HorizontCMS with version #{version}") end - return CheckCode::Appears("Target is HorizontCMS with version #{version}") + CheckCode::Appears("Target is HorizontCMS with version #{version}") end def login @@ -142,6 +142,7 @@ class MetasploitModule < Msf::Exploit::Remote end # try to authenticate + # Cookies from this request will overwrite the cookies currently in the jar from the +check+ method res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'admin', 'login'), @@ -163,10 +164,6 @@ class MetasploitModule < Msf::Exploit::Remote fail_with(Failure::UnexpectedReply, 'Unexpected response received while trying to authenticate.') end - # keep only the newly added cookies, otherwise subsequent requests will fail - auth_cookies = cookie_jar.to_a[2..3] - self.cookie_jar = auth_cookies.to_set - # using send_request_cgi! does not work so we have to follow the redirect manually res = send_request_cgi({ 'method' => 'GET', diff --git a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb new file mode 100644 index 0000000000..13345ea182 --- /dev/null +++ b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb @@ -0,0 +1,270 @@ +require 'spec_helper' +require 'msf/core/exploit/remote/http/http_cookie_jar' +require 'msf/core/exploit/remote/http/http_cookie' + +RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do + def random_string(min_len = 1, max_len = 12) + str = Faker::Alphanumeric.alpha(number: max_len) + + str[0, rand(min_len..max_len)] + end + + def cookie + Msf::Exploit::Remote::HTTP::HttpCookie.new( + random_string, + random_string, + max_age: rand(1..100), + path: '/' + random_string, + secure: rand(0..1) == 1, + httponly: rand(0..1) == 1, + domain: random_string + ) + end + describe 'add' do + before(:each) do + @cookie_jar = described_class.new + end + + describe 'empty?' do + it 'will return true when no cookies are in a cookie_jar' do + # cookie_jar made in before + + e = @cookie_jar.empty? + + expect(e).to eq(true) + end + + it 'will return false when a cookie has been added to a cookie_jar' do + c = cookie + + @cookie_jar.add(c) + e = @cookie_jar.empty? + + expect(e).to eq(false) + end + end + + describe 'clear' do + it 'will make no changes to an empty cookiejar' do + # empty cookie_jar made in before + + @cookie_jar.clear + + expect(@cookie_jar.empty?).to eq(true) + end + + it 'will return false when a cookie has been added to a cookie_jar' do + c = cookie + + @cookie_jar.add(c) + @cookie_jar.clear + + expect(@cookie_jar.empty?).to eq(true) + end + end + + describe 'cookies' do + it 'will return an empty array when no cookies have been added to the jar' do + # cookie_jar made in before + + c_array = @cookie_jar.cookies + + expect(c_array.class).to eq(Array) + expect(c_array.empty?).to eq(true) + end + + it 'will return an array of all cookies added to the cookie_jar when called with no url param' do + c_array = [] + rand(1..10).times do + c = cookie + + c_array.append(c) + @cookie_jar.add(c) + end + + jar_array = @cookie_jar.cookies + + expect(c_array.sort).to eq(jar_array.sort) + end + + it 'will return an array of all cookies that have parent paths to the url passed' do + c_array = [cookie, cookie, cookie, cookie] + c_array[0].secure = false + c_array[1].secure = false + c_array[1].domain = c_array[0].domain + c_array[1].path = '/' + c_array.map { |c| @cookie_jar.add(c) } + + filtered_cookies = @cookie_jar.cookies("http://#{c_array[0].domain}#{c_array[0].path}") + + expect(filtered_cookies.length).to eq(2) + expect(filtered_cookies.include?(c_array[0])).to eq(true) + expect(filtered_cookies.include?(c_array[1])).to eq(true) + expect(filtered_cookies.include?(c_array[2])).to eq(false) + expect(filtered_cookies.include?(c_array[3])).to eq(false) + end + end + + describe 'add' do + it 'unacceptable cookie missing path throws ArgumentError' do + c = HttpCookie.new(random_string, random_string, + path: '/' + random_string) + + expect do + @cookie_jar.add(c) + end.to raise_error(ArgumentError) + end + + it 'unacceptable cookie missing domain throws ArgumentError' do + c = HttpCookie.new(random_string, random_string, + domain: random_string) + + expect do + @cookie_jar.add(c) + end.to raise_error(ArgumentError) + end + + it 'acceptable cookie added to cookie_jar successfully' do + c = cookie + + @cookie_jar.add(c) + + expect(@cookie_jar.cookies[0] == c) + end + + it 'acceptable cookie added to cookie_jar containing cookie with the same name, domain, and path will result in an overwrite' do + c = cookie + c_dup = cookie + c_dup.name = c.name + c_dup.domain = c.domain + c_dup.path = c.path + + @cookie_jar.add(c) + @cookie_jar.add(c_dup) + + expect(@cookie_jar.cookies[0]).to eq(c_dup) + expect(@cookie_jar.cookies[0]).to_not eq(c) + end + + it 'variable not a subclass of ::HttpCookie will raise TypeError' do + int = rand(1..100) + + expect do + @cookie_jar.add(int) + end.to raise_error(TypeError) + end + end + + describe 'delete' do + it 'used on an empty jar will return nil' do + # cookie_jar made in before + + n = @cookie_jar.delete(cookie) + + expect(n).to eq(nil) + end + + it 'passed cookie with same name, domain, and path as cookie in jar, will delete cookie in jar' do + c = cookie + c_dup = cookie + c_dup.name = c.name + c_dup.domain = c.domain + c_dup.path = c.path + + @cookie_jar.add(c) + @cookie_jar.delete(c_dup) + + expect(@cookie_jar.empty?).to eq(true) + end + + it 'passed a cookie different name, domain, and path as cookie in jar, will not delete cookie in jar' do + c = cookie + c_dup = cookie + c_dup.name = c.name + random_string(1, 1) + c_dup.domain = c.domain + random_string(1, 1) + c_dup.path = c.path + random_string(1, 1) + + @cookie_jar.add(c) + @cookie_jar.delete(c_dup) + + expect(@cookie_jar.cookies.length).to eq(1) + expect(@cookie_jar.cookies[0]).to eql(c) + end + + it 'variable not a subclass of ::HttpCookie will not raise TypeError when the cookie_jar is empty' do + int = rand(1..100) + + n = @cookie_jar.delete(int) + + expect(n).to eq(nil) + expect(@cookie_jar.empty?).to eq(true) + end + + it 'variable not a subclass of ::HttpCookie will raise TypeError when the cookie_jar is not empty' do + @cookie_jar.add(cookie) + int = rand(1..100) + + expect do + @cookie_jar.delete(int) + end.to raise_error(TypeError) + end + end + + describe 'cleanup' do + it 'will make no changes to an empty cookiejar' do + # empty cookie_jar made in before + + @cookie_jar.cleanup + + expect(@cookie_jar.empty?).to eq(true) + end + + it 'will remove expired cookies with max_age value' do + freeze_time = Time.local(2008, 9, 5, 10, 5, 30) + Timecop.freeze(freeze_time) + expired_cookies = [cookie, cookie] + expired_cookies[0].max_age = 1 + expired_cookies[1].max_age = 1 + expired_cookies[0].created_at = Time.local(2008, 9, 5, 10, 5, 1) + expired_cookies[1].created_at = Time.local(2008, 9, 5, 10, 5, 1) + cookies = [cookie, cookie] + cookies[0].max_age = 10000 + cookies[1].max_age = 10000 + cookies[0].created_at = freeze_time + cookies[1].created_at = freeze_time + + cookies.map { |c| @cookie_jar.add(c) } + expired_cookies.map { |c| @cookie_jar.add(c) } + @cookie_jar.cleanup + + expect(@cookie_jar.cookies.length).to eq(2) + expect(@cookie_jar.cookies.include?(expired_cookies[0])).to eq(false) + expect(@cookie_jar.cookies.include?(expired_cookies[1])).to eq(false) + expect(@cookie_jar.cookies.include?(cookies[0])).to eq(true) + expect(@cookie_jar.cookies.include?(cookies[1])).to eq(true) + Timecop.return + end + + it 'will remove expired cookies with expires value' do + Timecop.freeze(Time.local(2008, 9, 5, 10, 5, 0)) + expired_cookies = [cookie, cookie] + expired_cookies[0].expires = Time.local(2008, 9, 3, 10, 5, 0) + expired_cookies[1].expires = Time.local(2008, 9, 4, 10, 5, 0) + cookies = [cookie, cookie] + cookies[0].expires = Time.local(2008, 9, 6, 10, 5, 0) + cookies[1].expires = Time.local(2008, 9, 7, 10, 5, 0) + + cookies.map { |c| @cookie_jar.add(c) } + expired_cookies.map { |c| @cookie_jar.add(c) } + @cookie_jar.cleanup + + expect(@cookie_jar.cookies.length).to eq(2) + expect(@cookie_jar.cookies.include?(expired_cookies[0])).to eq(false) + expect(@cookie_jar.cookies.include?(expired_cookies[1])).to eq(false) + expect(@cookie_jar.cookies.include?(cookies[0])).to eq(true) + expect(@cookie_jar.cookies.include?(cookies[1])).to eq(true) + Timecop.return + end + end + end +end diff --git a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb new file mode 100644 index 0000000000..623347b4e6 --- /dev/null +++ b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb @@ -0,0 +1,821 @@ +require 'spec_helper' +require 'msf/core/exploit/remote/http/http_cookie' +require 'faker' + +RSpec.describe HttpCookie do + def random_string(min_len=1, max_len=12) + str = Faker::Alphanumeric.alpha(number: max_len) + + str[0, rand(min_len..max_len)] + end + + before do + @cookie = HttpCookie.new('Name', 'Value') + end + + describe "name" do + describe "String" do + it "is assigned to name successfully" do + n = random_string + + @cookie.name = n + + expect(@cookie.name).to eql(n) + expect(@cookie.name.class).to eql(String) + end + + it "that is empty is passed to name and throws an ArgumentError" do + n = '' + + expect { + @cookie.name = n + }.to raise_error(ArgumentError) + end + end + + describe 'nil' do + it "assigned to name throws an ArgumentError" do + n = nil + + expect { + @cookie.name = n + }.to raise_error(ArgumentError) + end + end + end + + describe "value" do + describe "String" do + it "is assigned to value successfully" do + v = random_string + + @cookie.value = v + + expect(@cookie.value).to eql(v) + expect(@cookie.value.class).to eql(String) + end + + it "that is empty is passed to value successfully" do + v = '' + + @cookie.value = v + + expect(@cookie.value).to eql(v) + expect(@cookie.value.class).to eql(String) + end + end + + describe 'nil' do + it "assigned to value results in it being set to an empty string & expires is set UNIX_EPOCH" do + v = nil + + @cookie.value = v + + expect(@cookie.value).to eql('') + expect(@cookie.value.class).to eql(String) + expect(@cookie.expires).to eql(Time.at(0)) + end + end + end + + describe "max_age" do + describe "Integer" do + it "assigns an integer of 0 to max_age successfully" do + int = 0 + + @cookie.max_age = int + + expect(@cookie.max_age).to eql(int) + end + + it "assigns a positive integer to max_age successfully" do + pos_val = rand(1..100) + + @cookie.max_age = pos_val + + expect(@cookie.max_age).to eql(pos_val) + end + + it "assigns a negative integer to max_age successfully" do + neg_val = rand(-1..-100) + + @cookie.max_age = neg_val + + expect(@cookie.max_age).to eql(neg_val) + end + end + + describe "String" do + it "assigns a String of 0 to max_age successfully" do + str = '0' + + @cookie.max_age = str + + expect(@cookie.max_age).to eql(str.to_i) + end + + it "assigns a String of a positive Integer to max_age successfully" do + pos_val = rand(1..100) + + @cookie.max_age = pos_val.to_s + + expect(@cookie.max_age).to eql(pos_val) + end + + it "assigns a String of a negative Integer to max_age successfully" do + neg_val = rand(-100..-1) + + @cookie.max_age = neg_val.to_s + + expect(@cookie.max_age).to eql(neg_val) + end + + it "throws an ArgumentError with a String that cannot be converted into an Integer" do + invalid_str = random_string + + expect { + @cookie.max_age = invalid_str + }.to raise_error(ArgumentError) + end + end + + describe 'Complex Object' do + it "throws an TypeError with a Complex Object that cannot be converted into an Integer" do + obj = [rand(9),rand(9),rand(9)] + + expect { + @cookie.max_age = obj + }.to raise_error(TypeError) + end + end + + describe 'nil' do + it "assigns a nil to max_age successfully" do + @cookie.max_age = nil + expect(@cookie.max_age).to eql(nil) + end + end + end + + describe "expires" do + describe "Time" do + it "instance is assigned to expires successfully" do + t = Time.now + + @cookie.expires = t + + expect(@cookie.expires).to eql(t) + expect(@cookie.expires.class).to eql(Time) + end + end + + describe 'Non-Time' do + it "which can be parsed to a Time object, will be assigned to expires successfully" do + obj = Time.now.to_s + + @cookie.expires = obj + + expect(@cookie.expires).to eql(Time.parse(obj)) + end + + it "which cannot be parsed to a Time object, when assigned to expires, will throw a TypeError" do + obj = [rand(9),rand(9),rand(9)] + + expect { + @cookie.expires = obj + }.to raise_error(TypeError) + end + end + + describe 'nil' do + it "assigned to expires successfully" do + @cookie.expires = nil + expect(@cookie.expires).to eql(nil) + end + end + end + + describe "path" do + describe "String" do + it "beginning with \"/\" is passed to \"path\" successfully" do + p = '/' + random_string + + @cookie.path = p + + expect(@cookie.path).to eql(p) + expect(@cookie.path.class).to eql(String) + end + + it "not beginning with \"/\" is passed to \"path\" and is set as \"/\"" do + p = random_string + + @cookie.path = p + + expect(@cookie.path).to eql('/') + expect(@cookie.path.class).to eql(String) + end + + it "that is empty is passed to \"path\" and is set as \"/\"" do + p = '' + + @cookie.path = p + + expect(@cookie.path).to eql('/') + expect(@cookie.path.class).to eql(String) + end + end + + # If the Object A responds to to_str with a truthy Object that responds true to start_with?('/'), + # then path is set to A + describe 'Complex Object' do + it "which is a kind of String will be passed to path successfully" do + class MyString < String + end + str = "/#{random_string(0)}" + my_str = MyString.new(str) + + @cookie.path = my_str + + expect(@cookie.path).to eql(str) + end + end + + describe 'nil' do + it "assigned to path successfully" do + n = nil + + @cookie.expires = n + + expect(@cookie.expires).to eql(n) + end + end + end + + describe "domain" do + describe "DomainName" do + it "assigned to domain when origin is set will result in a domain based on origin.host" do + d = DomainName(random_string) + + @cookie.domain = d + + expect(@cookie.domain).to eql(d.hostname) + end + end + + describe 'nil' do + it "assigned to domain when origin is not set will result in a nil domain" do + n = nil + + @cookie.domain = n + + expect(@cookie.domain).to eql(n) + end + end + + describe 'String' do + it "assigned to domain will be converted to a DomainName and assigned" do + s = random_string + + @cookie.domain = s + + expect(@cookie.domain).to eql(DomainName(s).domain) + end + end + end + + # describe "origin" do + # before do + # @cookie = HttpCookie.new('Name', 'Value') + # end + # + # describe 'Origin already set with value' do + # it "when being set with any value will throw an ArgumentError" do + # n = '' + # + # @cookie.origin = n + # + # expect { + # @cookie.origin = n + # }.to raise_error(ArgumentError) + # end + # end + # + # describe "String" do + # it "of URL without protocol is assigned to origin without altering domain OR path" do + # url_str = random_string + # + # @cookie.origin = url_str + # + # expect(@cookie.origin.kind_of?(URI)).to eql(true) + # expect(@cookie.origin).to eql(URI(url_str)) + # expect(@cookie.domain).to eql(nil) + # expect(@cookie.path).to eql(nil) + # end + # + # it "of HTTP URL with path is assigned to origin, broken out, and domain is populated correctly" do + # domain = "#{random_string}.#{random_string(2,2)}" + # url_str = "http://#{domain}" + # + # @cookie.origin = url_str + # + # expect(@cookie.origin.instance_of?(URI::HTTP)).to eql(true) + # expect(@cookie.origin).to eql(URI(url_str)) + # expect(@cookie.domain).to eql(domain.downcase) + # expect(@cookie.path).to eql('/') + # end + # + # it "of HTTP URL without path is assigned to origin, and domain & path are populated correctly" do + # path = "/#{random_string}/#{random_string}/" + # domain = "#{random_string}.#{random_string(2,2)}" + # url_str = "http://#{domain}#{path}#{random_string}" + # + # @cookie.origin = url_str + # + # expect(@cookie.origin.instance_of?(URI::HTTP)).to eql(true) + # expect(@cookie.origin).to eql(URI(url_str)) + # expect(@cookie.domain).to eql(domain.downcase) + # expect(@cookie.path).to eql(path) + # end + # + # it "of HTTPS URL with path is assigned to origin, broken out, and domain is populated correctly" do + # domain = "#{random_string}.#{random_string(2,2)}" + # url_str = "https://#{domain}" + # + # @cookie.origin = url_str + # + # expect(@cookie.origin.instance_of?(URI::HTTPS)).to eql(true) + # expect(@cookie.origin).to eql(URI(url_str)) + # expect(@cookie.domain).to eql(domain.downcase) + # expect(@cookie.path).to eql('/') + # end + # + # it "of HTTPS URL without path is assigned to origin, and domain & path are populated correctly" do + # path = "/#{random_string}/#{random_string}/" + # domain = "#{random_string}.#{random_string(2,2)}" + # url_str = "https://#{domain}#{path}#{random_string}" + # + # @cookie.origin = url_str + # + # expect(@cookie.origin.instance_of?(URI::HTTPS)).to eql(true) + # expect(@cookie.origin).to eql(URI(url_str)) + # expect(@cookie.domain).to eql(domain.downcase) + # expect(@cookie.path).to eql(path) + # end + # end + # + # describe 'URI' do + # it "instance of URI::Generic will be passed to origin successfully" do + # url = URI(random_string) + # + # @cookie.origin = url + # + # expect(@cookie.origin).to eql(url) + # end + # + # it "instance of URI::HTTP will be passed to origin successfully" do + # url = URI('http://' + random_string) + # + # @cookie.origin = url + # + # expect(@cookie.origin).to eql(url) + # end + # + # it "URI::HTTPS will be passed to origin successfully" do + # url = URI('https://' + random_string) + # + # @cookie.origin = url + # + # expect(@cookie.origin).to eql(url) + # end + # end + # end + + describe "httponly" do + describe "Truthy" do + it "empty string passed to httponly is set as true" do + str = '' + + @cookie.httponly = str + + expect(@cookie.httponly).to eql(true) + end + + it "populated string passed to httponly is set as true" do + str = random_string + + @cookie.httponly = str + + expect(@cookie.httponly).to eql(true) + end + + it "integer passed to httponly is set as true" do + int = rand(0..10) + + @cookie.httponly = int + + expect(@cookie.httponly).to eql(true) + end + + it "true passed to httponly is set as true" do + t = true + + @cookie.httponly = t + + expect(@cookie.httponly).to eql(true) + end + end + + describe "Falsey" do + it "nil passed to httponly is set as false" do + n = nil + + @cookie.httponly = n + + expect(@cookie.httponly).to eql(false) + end + + it "false passed to httponly is set as false" do + f = false + + @cookie.httponly = f + + expect(@cookie.httponly).to eql(false) + end + end + end + + # describe 'for_domain' do + # before do + # @cookie = HttpCookie.new('Name', 'Value') + # end + # + # describe "Truthy" do + # it "empty string passed to for_domain is set as true" do + # str = '' + # + # @cookie.for_domain = str + # + # expect(@cookie.for_domain).to eql(true) + # end + # + # it "populated string passed to for_domain is set as true" do + # str = random_string + # + # @cookie.for_domain = str + # + # expect(@cookie.for_domain).to eql(true) + # end + # + # it "integer passed to for_domain is set as true" do + # int = rand(0..10) + # + # @cookie.for_domain = int + # + # expect(@cookie.for_domain).to eql(true) + # end + # + # it "true passed to for_domain is set as true" do + # t = true + # + # @cookie.for_domain = t + # + # expect(@cookie.for_domain).to eql(true) + # end + # end + # + # describe "Falsey" do + # it "nil passed to for_domain is set as false" do + # n = nil + # + # @cookie.for_domain = n + # + # expect(@cookie.for_domain).to eql(false) + # end + # + # it "false passed to for_domain is set as false" do + # f = false + # + # @cookie.for_domain = f + # + # expect(@cookie.for_domain).to eql(false) + # end + # end + # end + + describe "secure" do + describe "Truthy" do + it "empty string passed to secure is set as true" do + str = '' + + @cookie.secure = str + + expect(@cookie.secure).to eql(true) + end + + it "populated string passed to secure is set as true" do + str = random_string + + @cookie.secure = str + + expect(@cookie.secure).to eql(true) + end + + it "integer passed to secure is set as true" do + int = rand(0..10) + + @cookie.secure = int + + expect(@cookie.secure).to eql(true) + end + + it "true passed to secure is set as true" do + t = true + + @cookie.secure = t + + expect(@cookie.secure).to eql(true) + end + end + + describe "Falsey" do + it "nil passed to secure is set as false" do + n = nil + + @cookie.secure = n + + expect(@cookie.secure).to eql(false) + end + + it "false passed to secure is set as false" do + f = false + + @cookie.secure = f + + expect(@cookie.secure).to eql(false) + end + end + end + + describe "created_at" do + describe "Time" do + it "is returned by created_at after the cookie is initialized" do + #cookie created in before + + c = @cookie.created_at + + expect(c.class).to eql(Time) + end + + it "instance is assigned to created_at successfully" do + t = Time.now + + @cookie.created_at = t + + expect(@cookie.created_at).to eql(t) + expect(@cookie.created_at.class).to eql(Time) + end + end + + describe 'Non-Time' do + it "which can be parsed to a Time object, will be assigned to created_at successfully" do + obj = Time.now.to_s + + @cookie.created_at = obj + + expect(@cookie.created_at).to eql(Time.parse(obj)) + end + + it "which cannot be parsed to a Time object, when assigned to created_at, will throw a TypeError" do + obj = [rand(9),rand(9),rand(9)] + + expect { + @cookie.created_at = obj + }.to raise_error(TypeError) + end + end + + describe 'nil' do + it "assigned to created_at successfully" do + @cookie.created_at = nil + expect(@cookie.created_at).to eql(nil) + end + end + end + + describe "accessed_at" do + describe "Time" do + it "is returned by accessed_at after the cookie is initialized" do + #cookie created in before + + c = @cookie.accessed_at + + expect(c.class).to eql(Time) + end + + it "instance is assigned to accessed_at successfully" do + t = Time.now + + @cookie.accessed_at = t + + expect(@cookie.accessed_at).to eql(t) + expect(@cookie.accessed_at.class).to eql(Time) + end + end + + describe 'Non-Time' do + it "which can be parsed to a Time object, will be assigned to accessed_at successfully" do + obj = Time.now.to_s + + @cookie.accessed_at = obj + + expect(@cookie.accessed_at).to eql(Time.parse(obj)) + end + + it "which cannot be parsed to a Time object, when assigned to accessed_at, will throw a TypeError" do + obj = [rand(9),rand(9),rand(9)] + + expect { + @cookie.accessed_at = obj + }.to raise_error(TypeError) + end + end + + describe 'nil' do + it "assigned to accessed_at successfully" do + @cookie.accessed_at = nil + expect(@cookie.accessed_at).to eql(nil) + end + end + end + + describe "session" do + describe "Session" do + it "is set as True when initialized with no max_age or expires args and a nil value" do + #cookie created in before + + s = @cookie.session? + + expect(s).to eql(true ) + end + + it "is set as the nil? value of max_age when a valid value is assigned to max_age" do + max_age = rand(0..1) == 1 ? 1 : nil + + @cookie.max_age= max_age + + expect(@cookie.session?).to eql(max_age.nil?) + end + + it "is set as the nil? value of expires when a valid value is assigned to expires" do + expires = rand(0..1) == 1 ? Time.now : nil + + @cookie.expires= expires + + expect(@cookie.session?).to eql(expires.nil?) + end + end + end + + describe "expired?" do + describe "nil" do + it "assigned to expires causes expired? to return false" do + n = nil + + @cookie.expires = n + + expect(@cookie.expired?).to eql(false) + end + end + + describe "No-Arg" do + it "call of expired? returns false when expires & max_age are set to nil" do + n = nil + + @cookie.expires = n + + expect(@cookie.expired?).to eql(false) + end + + + it "call of expired? with a valid expires value will return True/False depending on if the value is before Time.now" do + expired = rand(0..1) == 1 + t = expired ? Faker::Time.backward(days: 1) : Faker::Time.forward(days: 1) + + @cookie.expires = t + + expect(@cookie.expired?).to eql(expired) + end + + it "call of expired? with a valid max_age value will return True/False depending on if the value is before Time.now" do + expired = rand(0..1) == 1 + m = expired ? 1 : 1000 + + @cookie.max_age = m + sleep(1) + + expect(@cookie.expired?).to eql(expired) + end + end + + describe "Time" do + it "call of expired? with any Time value returns false when expires is set to nil" do + n = nil + + @cookie.expires = n + @cookie.max_age = n + + expect(@cookie.expired?(Time.now)).to eql(false) + end + + + it "passed to expired? with expires set to valid Time value, will return True/False depending on if the passed Time value is after expires" do + expired = rand(0..1) == 1 + t = expired ? Faker::Time.forward(days: 1) : Faker::Time.backward(days: 1) + + @cookie.expires = Time.now + + expect(@cookie.expired?(t)).to eql(expired) + end + + it "passed to expired? with max_age set to a valid Integer, will return True/False depending on if created_at + max_age is after expires" do + expired = rand(0..1) == 1 + t = expired ? Faker::Time.forward(days: 1) : Faker::Time.backward(days: 1) + + @cookie.max_age = 1 + + expect(@cookie.expired?(t)).to eql(expired) + end + end + end + + # if @domain.nil? + # raise "cannot tell if this cookie is valid because the domain is unknown" + # end + # uri = URI(uri) + # # RFC 6265 5.4 + # return false if secure? && !(URI::HTTPS === uri) + # acceptable_from_uri?(uri) && HTTP::Cookie.path_match?(@path, uri.path) + describe "valid_for_uri?" do + it "will return false when domain hasn't been set" do + #domain set as nil in before + + expect { + @cookie.valid_for_uri?(URI(random_string)) + }.to raise_error(ArgumentError) + end + + it "will return false if secure is set as true and the passed cookie is a https url" do + @cookie.domain = random_string + + v = @cookie.valid_for_uri?(URI("https://#{random_string}")) + + expect(v).to eq(false) + end + end + + describe "acceptable_from_uri?" do + it "will return false when passed nil" do + n = nil + + a = @cookie.acceptable_from_uri?(n) + + expect(a).to eq(false) + end + + it "will return false if url without http(s) protocol is passed" do + generic_uri = random_string + + v = @cookie.acceptable_from_uri?(generic_uri) + + expect(v).to eq(false) + end + + it "will return false if url with http(s) protocol is passed but has no host" do + protcol = 'http://' + + v = @cookie.acceptable_from_uri?(protcol) + + expect(URI(protcol).is_a?(::URI::HTTP)).to eq(true) + expect(v).to eq(false) + end + + it "will return true if url with http(s) protocol is passed with a domain that watches the url domain" do + host = random_string + uri = "http://#{host}/#{random_string}" + @cookie.domain = host + + v = @cookie.acceptable_from_uri?(uri) + + expect(v).to eq(true) + end + + it "will return domain.nil? if url with http(s) protocol is passed with a domain that doesn't match the url domain" do + uri = "http://#{random_string}/#{random_string}" + @cookie.domain = rand(0..1) == 1 ? nil : random_string + + v = @cookie.acceptable_from_uri?(uri) + + expect(v).to eq(@cookie.domain.nil?) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9a0cdf5de6..0f20a8f02e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -84,6 +84,10 @@ RSpec.configure do |config| # as the one that triggered the failure. Kernel.srand config.seed + # Implemented to avoid regression issue with code calling Faker not being deterministic + # https://github.com/faker-ruby/faker/issues/2281 + Faker::Config.random = Random.new(config.seed) + config.expect_with :rspec do |expectations| # Enable only the newer, non-monkey-patching expect syntax. expectations.syntax = :expect From 88f17c512808fde8579336153ac1d36adaa15119 Mon Sep 17 00:00:00 2001 From: A Galway Date: Fri, 16 Apr 2021 13:08:38 +0100 Subject: [PATCH 2/5] cleanup and removes cookies filtering --- .../core/exploit/remote/http/http_cookie.rb | 8 +- .../exploit/remote/http/http_cookie_jar.rb | 62 +- lib/msf/core/exploit/remote/http_client.rb | 17 +- .../multi/http/gitlab_file_read_rce.rb | 1 - .../exploits/multi/http/gogs_git_hooks_rce.rb | 29 - .../remote/http/http_cookie_jar_spec.rb | 436 +++++++------ .../remote/remote/http/http_cookie_spec.rb | 608 +++++++++--------- 7 files changed, 556 insertions(+), 605 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/http_cookie.rb b/lib/msf/core/exploit/remote/http/http_cookie.rb index 843511efb6..489a1ab2fd 100644 --- a/lib/msf/core/exploit/remote/http/http_cookie.rb +++ b/lib/msf/core/exploit/remote/http/http_cookie.rb @@ -133,10 +133,6 @@ module Msf @cookie.accessed_at end - def created_at - @cookie.created_at - end - def accessed_at=(time) if time.nil? || time.is_a?(Time) @cookie.accessed_at = time @@ -145,6 +141,10 @@ module Msf end end + def created_at + @cookie.created_at + end + def created_at=(time) if time.nil? || time.is_a?(Time) @cookie.created_at = time diff --git a/lib/msf/core/exploit/remote/http/http_cookie_jar.rb b/lib/msf/core/exploit/remote/http/http_cookie_jar.rb index a99df923fb..d32f0dd733 100644 --- a/lib/msf/core/exploit/remote/http/http_cookie_jar.rb +++ b/lib/msf/core/exploit/remote/http/http_cookie_jar.rb @@ -2,7 +2,6 @@ require 'http/cookie_jar/hash_store' require 'http/cookie_jar' require 'http/cookie' -require 'domain_name' module Msf class Exploit @@ -12,12 +11,12 @@ module Msf class HttpCookieJar def initialize @cookie_jar = ::HTTP::CookieJar.new({ - store: HashStoreWithoutAutomaticExpiry + store: HashStoreWithoutAutomaticExpiration }) end def add(cookie) - raise TypeError, "Passed cookie is of class '#{cookie.class}' and not a subclass of '#{HttpCookie.class}" unless cookie.is_a?(HttpCookie) + raise TypeError, "Passed cookie is of class '#{cookie.class}' and not a subclass of '#{Msf::Exploit::Remote::HTTP::HttpCookie.class}" unless cookie.is_a?(HttpCookie) @cookie_jar.add(cookie) self @@ -25,7 +24,7 @@ module Msf def delete(cookie) return if @cookie_jar.cookies.empty? - raise TypeError, "Passed cookie is of class '#{cookie.class}' and not a subclass of '#{HttpCookie.class}" unless cookie.is_a?(HttpCookie) + raise TypeError, "Passed cookie is of class '#{cookie.class}' and not a subclass of '#{Msf::Exploit::Remote::HTTP::HttpCookie.class}" unless cookie.is_a?(HttpCookie) @cookie_jar.delete(cookie) self @@ -33,22 +32,16 @@ module Msf # Iterates over all cookies that are not expired in no particular # order. - # - # `uri` specifies a URI/URL indicating the destination of the cookies - # being selected. Every cookie yielded should be good to send to the - # given URI, i.e. cookie.valid_for_uri?(uri) evaluates to true. - def cookies(uri = nil) - @cookie_jar.cookies(uri) - end - - def [](c) - @cookie_jar.cookies(uri)[c] + def cookies + @cookie_jar.cookies end def clear @cookie_jar.clear end + # Removes expired cookies and returns self. If `session` is true, + # all session cookies are removed as well. def cleanup(expire_all = false) @cookie_jar.cleanup(expire_all) end @@ -59,38 +52,27 @@ module Msf def parse(set_cookie_header, origin_url, options = nil) parsed_cookies = HTTP::Cookie.parse(set_cookie_header, origin_url, options) - parsed_cookies.each { |c| add(HttpCookie.new(c)) } + parsed_cookies.each { |c| add(Msf::Exploit::Remote::HTTP::HttpCookie.new(c)) } end - end - class HashStoreWithoutAutomaticExpiry < ::HTTP::CookieJar::HashStore - - # +::HTTP::CookieJar::HashStore+ expires cookies as it iterates. We don't want that. + class HashStoreWithoutAutomaticExpiration < ::HTTP::CookieJar::HashStore + # On top of iterating over every item in the store, +::HTTP::CookieJar::HashStore+ also deletes any expired cookies + # and has the option to filter cookies based on whether they are parent of a passed url. + # + # We've removed the extraneous features in the overwritten method. + # - The deletion of cookies while you're iterating over them complicated simple cookie management. It also + # prevented sending expired cookies if needed for an exploit + # - Any URL passed for filtering could be resolved to nil if it was improperly formed or resolved to a eTLD, + # which was too brittle for our uses def each(uri = nil) - if uri - thost = DomainName.new(uri.host) - tpath = uri.path - @jar.each do |domain, paths| - next unless thost.cookie_domain?(domain) - - paths.each do |path, hash| - next unless HTTP::Cookie.path_match?(path, tpath) + raise ArgumentError, "HashStoreWithoutAutomaticExpiration.each doesn't support url filtering" if uri + synchronize do + @jar.each do |_domain, paths| + paths.each do |_path, hash| hash.each do |_name, cookie| - if cookie.valid_for_uri?(uri) - yield cookie - end - end - end - end - else - synchronize do - @jar.each do |_domain, paths| - paths.each do |_path, hash| - hash.each do |_name, cookie| - yield cookie - end + yield cookie end end end diff --git a/lib/msf/core/exploit/remote/http_client.rb b/lib/msf/core/exploit/remote/http_client.rb index 0046c557ab..870e62b35c 100644 --- a/lib/msf/core/exploit/remote/http_client.rb +++ b/lib/msf/core/exploit/remote/http_client.rb @@ -2,7 +2,6 @@ require 'uri' require 'digest' -require 'msf/core/exploit/remote/http/http_cookie_jar' module Msf @@ -92,7 +91,7 @@ module Exploit::Remote::HttpClient register_autofilter_services(%W{ http https }) # Initialize an empty cookie jar to keep cookies - self.cookie_jar = HttpCookieJar.new + self.cookie_jar = Msf::Exploit::Remote::HTTP::HttpCookieJar.new end def deregister_http_client_options @@ -374,36 +373,34 @@ module Exploit::Remote::HttpClient # Connects to the server, creates a request, sends the request, # reads the response # - # If a +HTTP::CookieJar+ instance is passed in the +opts+ dict under a 'cookie' key, said CookieJar will be used in + # If a +Msf::Exploit::Remote::HTTP::HttpCookieJar+ instance is passed in the +opts+ dict under a 'cookie' key, said CookieJar will be used in # the request instead of the module +cookie_jar+ # # Passes `opts` through directly to {Rex::Proto::Http::Client#request_cgi}. # Set `opts['keep_cookies']` to keep cookies from responses for reuse in requests. # Cookies returned by the server will be stored in +cookie_jar+ # - # +expire_cookies+ will control if +cleanup+ is called on any passed +::HttpCookieJar+ or the client cookiejar + # +expire_cookies+ will control if +cleanup+ is called on any passed +Msf::Exploit::Remote::HTTP::HttpCookieJar+ or the client cookiejar # # @return (see Rex::Proto::Http::Client#send_recv)) def send_request_cgi(opts = {}, timeout = 20, disconnect = true, expire_cookies = true) - origin_url = "http#{ssl ? 's' : ''}://#{peer}" - if opts.has_key?('cookie') - if opts['cookie'].is_a?(HttpCookieJar) + if opts['cookie'].is_a?(Msf::Exploit::Remote::HTTP::HttpCookieJar) cookie_jar.cleanup if expire_cookies opts.merge({ 'cookie' => opts['cookie'].cookies.join('; ') }) else opts.merge({ 'cookie' => opts['cookie'].to_s }) end - elsif !cookie_jar.empty?(origin_url) + elsif !cookie_jar.empty? cookie_jar.cleanup if expire_cookies - opts = opts.merge({ 'cookie' => cookie_jar.cookies(origin_url).join('; ') }) + opts = opts.merge({ 'cookie' => cookie_jar.cookies.join('; ') }) end res = send_request_raw(opts.merge(cgi: true), timeout, disconnect) return unless res if opts['keep_cookies'] && res.headers['Set-Cookie'].present? - cookie_jar.parse(res.headers['Set-Cookie'], origin_url) + cookie_jar.parse(res.headers['Set-Cookie'], "http#{ssl ? 's' : ''}://#{peer}") end res diff --git a/modules/exploits/multi/http/gitlab_file_read_rce.rb b/modules/exploits/multi/http/gitlab_file_read_rce.rb index bb2cbc1954..d56aca4d1b 100644 --- a/modules/exploits/multi/http/gitlab_file_read_rce.rb +++ b/modules/exploits/multi/http/gitlab_file_read_rce.rb @@ -63,7 +63,6 @@ class MetasploitModule < Msf::Exploit::Remote @http_client.cookie_jar.clear sign_in_path = '/users/sign_in' - csrf_token = extract_csrf_token( path: sign_in_path, regex: %r{action="/users/sign_in".*name="authenticity_token"\s+value="([^"]+)"} diff --git a/modules/exploits/multi/http/gogs_git_hooks_rce.rb b/modules/exploits/multi/http/gogs_git_hooks_rce.rb index e6a4f990c0..45140690b9 100644 --- a/modules/exploits/multi/http/gogs_git_hooks_rce.rb +++ b/modules/exploits/multi/http/gogs_git_hooks_rce.rb @@ -321,35 +321,6 @@ class MetasploitModule < Msf::Exploit::Remote nil end - # # Hook the HTTP client method to add specific cookie management logic - # def send_request_cgi(opts, timeout = 20) - # res = super - # - # return unless res - # - # # HTTP client does not handle cookies with the same name correctly. It adds - # # them instead of substituing the old value with the new one. - # unless res.get_cookies.empty? - # cookie_jar_hash = cookie_jar_to_hash - # cookies_from_response = cookie_jar_to_hash(res.get_cookies.split(' ')) - # cookie_jar_hash.merge!(cookies_from_response) - # cookie_jar_updated = cookie_jar_hash.each_with_object(Set.new) do |cookie, set| - # set << "#{cookie[0]}=#{cookie[1]}" - # end - # cookie_jar.clear - # cookie_jar.merge(cookie_jar_updated) - # end - # - # res - # end - # - # def cookie_jar_to_hash(jar = cookie_jar) - # jar.each_with_object({}) do |cookie, cookie_hash| - # name, value = cookie.split('=') - # cookie_hash[name] = value - # end - # end - def cleanup super return unless @need_cleanup diff --git a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb index 13345ea182..3a024bd3d7 100644 --- a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb +++ b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb @@ -5,7 +5,6 @@ require 'msf/core/exploit/remote/http/http_cookie' RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do def random_string(min_len = 1, max_len = 12) str = Faker::Alphanumeric.alpha(number: max_len) - str[0, rand(min_len..max_len)] end @@ -20,251 +19,260 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do domain: random_string ) end + + let(:cookie_jar) { described_class.new } + + before(:each) do + Timecop.freeze(Time.local(2008, 9, 5, 10, 5, 30)) + end + + after(:each) do + Timecop.return + end + + describe 'empty?' do + it 'will return true when no cookies are in a cookie_jar' do + # cookie_jar made in before + + e = cookie_jar.empty? + + expect(e).to eq(true) + end + + it 'will return false when a cookie has been added to a cookie_jar' do + c = cookie + + cookie_jar.add(c) + e = cookie_jar.empty? + + expect(e).to eq(false) + end + end + + describe 'clear' do + it 'will make no changes to an empty cookiejar' do + # empty cookie_jar made in before + + cookie_jar.clear + + expect(cookie_jar.empty?).to eq(true) + end + + it 'will return false when a cookie has been added to a cookie_jar' do + c = cookie + + cookie_jar.add(c) + cookie_jar.clear + + expect(cookie_jar.empty?).to eq(true) + end + end + + describe 'cookies' do + it 'will return an empty array when no cookies have been added to the jar' do + # cookie_jar made in before + + c_array = cookie_jar.cookies + + expect(c_array.class).to eq(Array) + expect(c_array.empty?).to eq(true) + end + + it 'will return an array of all cookies added to the cookie_jar when called with no url param' do + c_array = [] + rand(1..10).times do + c = cookie + + c_array.append(c) + cookie_jar.add(c) + end + + jar_array = cookie_jar.cookies + + expect(c_array.sort).to eq(jar_array.sort) + end + + # it 'will return an array of all cookies that have parent paths to the url passed' do + # c_array = [cookie, cookie, cookie, cookie] + # c_array[0].secure = false + # c_array[1].secure = false + # c_array[1].domain = c_array[0].domain + # c_array[1].path = '/' + # c_array.map { |c| cookie_jar.add(c) } + # + # require 'pry' + # binding.pry + # + # filtered_cookies = cookie_jar.cookies("http://#{c_array[0].domain}#{c_array[0].path}") + # + # expect(filtered_cookies.length).to eq(2) + # expect(filtered_cookies.include?(c_array[0])).to eq(true) + # expect(filtered_cookies.include?(c_array[1])).to eq(true) + # expect(filtered_cookies.include?(c_array[2])).to eq(false) + # expect(filtered_cookies.include?(c_array[3])).to eq(false) + # end + end + describe 'add' do - before(:each) do - @cookie_jar = described_class.new + it 'unacceptable cookie missing path throws ArgumentError' do + c = Msf::Exploit::Remote::HTTP::HttpCookie.new( + random_string, + random_string, + path: '/' + random_string + ) + + expect do + cookie_jar.add(c) + end.to raise_error(ArgumentError) end - describe 'empty?' do - it 'will return true when no cookies are in a cookie_jar' do - # cookie_jar made in before + it 'unacceptable cookie missing domain throws ArgumentError' do + c = Msf::Exploit::Remote::HTTP::HttpCookie.new( + random_string, + random_string, + domain: random_string + ) - e = @cookie_jar.empty? - - expect(e).to eq(true) - end - - it 'will return false when a cookie has been added to a cookie_jar' do - c = cookie - - @cookie_jar.add(c) - e = @cookie_jar.empty? - - expect(e).to eq(false) - end + expect do + cookie_jar.add(c) + end.to raise_error(ArgumentError) end - describe 'clear' do - it 'will make no changes to an empty cookiejar' do - # empty cookie_jar made in before + it 'acceptable cookie added to cookie_jar successfully' do + c = cookie - @cookie_jar.clear + cookie_jar.add(c) - expect(@cookie_jar.empty?).to eq(true) - end - - it 'will return false when a cookie has been added to a cookie_jar' do - c = cookie - - @cookie_jar.add(c) - @cookie_jar.clear - - expect(@cookie_jar.empty?).to eq(true) - end + expect(cookie_jar.cookies[0] == c) end - describe 'cookies' do - it 'will return an empty array when no cookies have been added to the jar' do - # cookie_jar made in before + it 'acceptable cookie added to cookie_jar containing cookie with the same name, domain, and path will result in an overwrite' do + c = cookie + c_dup = cookie + c_dup.name = c.name + c_dup.domain = c.domain + c_dup.path = c.path - c_array = @cookie_jar.cookies + cookie_jar.add(c) + cookie_jar.add(c_dup) - expect(c_array.class).to eq(Array) - expect(c_array.empty?).to eq(true) - end - - it 'will return an array of all cookies added to the cookie_jar when called with no url param' do - c_array = [] - rand(1..10).times do - c = cookie - - c_array.append(c) - @cookie_jar.add(c) - end - - jar_array = @cookie_jar.cookies - - expect(c_array.sort).to eq(jar_array.sort) - end - - it 'will return an array of all cookies that have parent paths to the url passed' do - c_array = [cookie, cookie, cookie, cookie] - c_array[0].secure = false - c_array[1].secure = false - c_array[1].domain = c_array[0].domain - c_array[1].path = '/' - c_array.map { |c| @cookie_jar.add(c) } - - filtered_cookies = @cookie_jar.cookies("http://#{c_array[0].domain}#{c_array[0].path}") - - expect(filtered_cookies.length).to eq(2) - expect(filtered_cookies.include?(c_array[0])).to eq(true) - expect(filtered_cookies.include?(c_array[1])).to eq(true) - expect(filtered_cookies.include?(c_array[2])).to eq(false) - expect(filtered_cookies.include?(c_array[3])).to eq(false) - end + expect(cookie_jar.cookies[0]).to eq(c_dup) + expect(cookie_jar.cookies[0]).to_not eq(c) end - describe 'add' do - it 'unacceptable cookie missing path throws ArgumentError' do - c = HttpCookie.new(random_string, random_string, - path: '/' + random_string) + it 'variable not a subclass of ::HttpCookie will raise TypeError' do + int = rand(1..100) - expect do - @cookie_jar.add(c) - end.to raise_error(ArgumentError) - end + expect do + cookie_jar.add(int) + end.to raise_error(TypeError) + end + end - it 'unacceptable cookie missing domain throws ArgumentError' do - c = HttpCookie.new(random_string, random_string, - domain: random_string) + describe 'delete' do + it 'used on an empty jar will return nil' do + # cookie_jar made in before - expect do - @cookie_jar.add(c) - end.to raise_error(ArgumentError) - end + n = cookie_jar.delete(cookie) - it 'acceptable cookie added to cookie_jar successfully' do - c = cookie - - @cookie_jar.add(c) - - expect(@cookie_jar.cookies[0] == c) - end - - it 'acceptable cookie added to cookie_jar containing cookie with the same name, domain, and path will result in an overwrite' do - c = cookie - c_dup = cookie - c_dup.name = c.name - c_dup.domain = c.domain - c_dup.path = c.path - - @cookie_jar.add(c) - @cookie_jar.add(c_dup) - - expect(@cookie_jar.cookies[0]).to eq(c_dup) - expect(@cookie_jar.cookies[0]).to_not eq(c) - end - - it 'variable not a subclass of ::HttpCookie will raise TypeError' do - int = rand(1..100) - - expect do - @cookie_jar.add(int) - end.to raise_error(TypeError) - end + expect(n).to eq(nil) end - describe 'delete' do - it 'used on an empty jar will return nil' do - # cookie_jar made in before + it 'passed cookie with same name, domain, and path as cookie in jar, will delete cookie in jar' do + c = cookie + c_dup = cookie + c_dup.name = c.name + c_dup.domain = c.domain + c_dup.path = c.path - n = @cookie_jar.delete(cookie) + cookie_jar.add(c) + cookie_jar.delete(c_dup) - expect(n).to eq(nil) - end - - it 'passed cookie with same name, domain, and path as cookie in jar, will delete cookie in jar' do - c = cookie - c_dup = cookie - c_dup.name = c.name - c_dup.domain = c.domain - c_dup.path = c.path - - @cookie_jar.add(c) - @cookie_jar.delete(c_dup) - - expect(@cookie_jar.empty?).to eq(true) - end - - it 'passed a cookie different name, domain, and path as cookie in jar, will not delete cookie in jar' do - c = cookie - c_dup = cookie - c_dup.name = c.name + random_string(1, 1) - c_dup.domain = c.domain + random_string(1, 1) - c_dup.path = c.path + random_string(1, 1) - - @cookie_jar.add(c) - @cookie_jar.delete(c_dup) - - expect(@cookie_jar.cookies.length).to eq(1) - expect(@cookie_jar.cookies[0]).to eql(c) - end - - it 'variable not a subclass of ::HttpCookie will not raise TypeError when the cookie_jar is empty' do - int = rand(1..100) - - n = @cookie_jar.delete(int) - - expect(n).to eq(nil) - expect(@cookie_jar.empty?).to eq(true) - end - - it 'variable not a subclass of ::HttpCookie will raise TypeError when the cookie_jar is not empty' do - @cookie_jar.add(cookie) - int = rand(1..100) - - expect do - @cookie_jar.delete(int) - end.to raise_error(TypeError) - end + expect(cookie_jar.empty?).to eq(true) end - describe 'cleanup' do - it 'will make no changes to an empty cookiejar' do - # empty cookie_jar made in before + it 'passed a cookie different name, domain, and path as cookie in jar, will not delete cookie in jar' do + c = cookie + c_dup = cookie + c_dup.name = c.name + random_string(1, 1) + c_dup.domain = c.domain + random_string(1, 1) + c_dup.path = c.path + random_string(1, 1) - @cookie_jar.cleanup + cookie_jar.add(c) + cookie_jar.delete(c_dup) - expect(@cookie_jar.empty?).to eq(true) - end + expect(cookie_jar.cookies.length).to eq(1) + expect(cookie_jar.cookies[0]).to eql(c) + end - it 'will remove expired cookies with max_age value' do - freeze_time = Time.local(2008, 9, 5, 10, 5, 30) - Timecop.freeze(freeze_time) - expired_cookies = [cookie, cookie] - expired_cookies[0].max_age = 1 - expired_cookies[1].max_age = 1 - expired_cookies[0].created_at = Time.local(2008, 9, 5, 10, 5, 1) - expired_cookies[1].created_at = Time.local(2008, 9, 5, 10, 5, 1) - cookies = [cookie, cookie] - cookies[0].max_age = 10000 - cookies[1].max_age = 10000 - cookies[0].created_at = freeze_time - cookies[1].created_at = freeze_time + it 'variable not a subclass of ::HttpCookie will not raise TypeError when the cookie_jar is empty' do + int = rand(1..100) - cookies.map { |c| @cookie_jar.add(c) } - expired_cookies.map { |c| @cookie_jar.add(c) } - @cookie_jar.cleanup + n = cookie_jar.delete(int) - expect(@cookie_jar.cookies.length).to eq(2) - expect(@cookie_jar.cookies.include?(expired_cookies[0])).to eq(false) - expect(@cookie_jar.cookies.include?(expired_cookies[1])).to eq(false) - expect(@cookie_jar.cookies.include?(cookies[0])).to eq(true) - expect(@cookie_jar.cookies.include?(cookies[1])).to eq(true) - Timecop.return - end + expect(n).to eq(nil) + expect(cookie_jar.empty?).to eq(true) + end - it 'will remove expired cookies with expires value' do - Timecop.freeze(Time.local(2008, 9, 5, 10, 5, 0)) - expired_cookies = [cookie, cookie] - expired_cookies[0].expires = Time.local(2008, 9, 3, 10, 5, 0) - expired_cookies[1].expires = Time.local(2008, 9, 4, 10, 5, 0) - cookies = [cookie, cookie] - cookies[0].expires = Time.local(2008, 9, 6, 10, 5, 0) - cookies[1].expires = Time.local(2008, 9, 7, 10, 5, 0) + it 'variable not a subclass of ::HttpCookie will raise TypeError when the cookie_jar is not empty' do + cookie_jar.add(cookie) + int = rand(1..100) - cookies.map { |c| @cookie_jar.add(c) } - expired_cookies.map { |c| @cookie_jar.add(c) } - @cookie_jar.cleanup + expect do + cookie_jar.delete(int) + end.to raise_error(TypeError) + end + end - expect(@cookie_jar.cookies.length).to eq(2) - expect(@cookie_jar.cookies.include?(expired_cookies[0])).to eq(false) - expect(@cookie_jar.cookies.include?(expired_cookies[1])).to eq(false) - expect(@cookie_jar.cookies.include?(cookies[0])).to eq(true) - expect(@cookie_jar.cookies.include?(cookies[1])).to eq(true) - Timecop.return - end + describe 'cleanup' do + it 'will make no changes to an empty cookiejar' do + # empty cookie_jar made in before + + cookie_jar.cleanup + + expect(cookie_jar.empty?).to eq(true) + end + + it 'will remove expired cookies with max_age value' do + expired_cookies = [cookie, cookie] + expired_cookies[0].max_age = 1 + expired_cookies[1].max_age = 1 + expired_cookies[0].created_at = Time.local(2008, 9, 5, 10, 5, 1) + expired_cookies[1].created_at = Time.local(2008, 9, 5, 10, 5, 1) + cookies = [cookie, cookie] + cookies[0].max_age = 10000 + cookies[1].max_age = 10000 + cookies[0].created_at = Time.now + cookies[1].created_at = Time.now + + cookies.map { |c| cookie_jar.add(c) } + expired_cookies.map { |c| cookie_jar.add(c) } + cookie_jar.cleanup + + expect(cookie_jar.cookies.length).to eq(2) + expect(cookie_jar.cookies.include?(expired_cookies[0])).to eq(false) + expect(cookie_jar.cookies.include?(expired_cookies[1])).to eq(false) + expect(cookie_jar.cookies.include?(cookies[0])).to eq(true) + expect(cookie_jar.cookies.include?(cookies[1])).to eq(true) + end + + it 'will remove expired cookies with expires value' do + expired_cookies = [cookie, cookie] + expired_cookies[0].expires = Time.local(2008, 9, 3, 10, 5, 0) + expired_cookies[1].expires = Time.local(2008, 9, 4, 10, 5, 0) + cookies = [cookie, cookie] + cookies[0].expires = Time.local(2008, 9, 6, 10, 5, 0) + cookies[1].expires = Time.local(2008, 9, 7, 10, 5, 0) + + cookies.map { |c| cookie_jar.add(c) } + expired_cookies.map { |c| cookie_jar.add(c) } + cookie_jar.cleanup + + expect(cookie_jar.cookies.length).to eq(2) + expect(cookie_jar.cookies.include?(expired_cookies[0])).to eq(false) + expect(cookie_jar.cookies.include?(expired_cookies[1])).to eq(false) + expect(cookie_jar.cookies.include?(cookies[0])).to eq(true) + expect(cookie_jar.cookies.include?(cookies[1])).to eq(true) end end end diff --git a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb index 623347b4e6..4a04855c36 100644 --- a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb +++ b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb @@ -1,301 +1,297 @@ require 'spec_helper' -require 'msf/core/exploit/remote/http/http_cookie' require 'faker' -RSpec.describe HttpCookie do - def random_string(min_len=1, max_len=12) +RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do + def random_string(min_len = 1, max_len = 12) str = Faker::Alphanumeric.alpha(number: max_len) - str[0, rand(min_len..max_len)] end - before do - @cookie = HttpCookie.new('Name', 'Value') - end + let(:cookie) { described_class.new(random_string, random_string) } - describe "name" do - describe "String" do - it "is assigned to name successfully" do + describe 'name' do + describe 'String' do + it 'is assigned to name successfully' do n = random_string - @cookie.name = n + cookie.name = n - expect(@cookie.name).to eql(n) - expect(@cookie.name.class).to eql(String) + expect(cookie.name).to eql(n) + expect(cookie.name.class).to eql(String) end - it "that is empty is passed to name and throws an ArgumentError" do + it 'that is empty is passed to name and throws an ArgumentError' do n = '' - expect { - @cookie.name = n - }.to raise_error(ArgumentError) + expect do + cookie.name = n + end.to raise_error(ArgumentError) end end describe 'nil' do - it "assigned to name throws an ArgumentError" do + it 'assigned to name throws an ArgumentError' do n = nil - expect { - @cookie.name = n - }.to raise_error(ArgumentError) + expect do + cookie.name = n + end.to raise_error(ArgumentError) end end end - describe "value" do - describe "String" do - it "is assigned to value successfully" do + describe 'value' do + describe 'String' do + it 'is assigned to value successfully' do v = random_string - @cookie.value = v + cookie.value = v - expect(@cookie.value).to eql(v) - expect(@cookie.value.class).to eql(String) + expect(cookie.value).to eql(v) + expect(cookie.value.class).to eql(String) end - it "that is empty is passed to value successfully" do + it 'that is empty is passed to value successfully' do v = '' - @cookie.value = v + cookie.value = v - expect(@cookie.value).to eql(v) - expect(@cookie.value.class).to eql(String) + expect(cookie.value).to eql(v) + expect(cookie.value.class).to eql(String) end end describe 'nil' do - it "assigned to value results in it being set to an empty string & expires is set UNIX_EPOCH" do + it 'assigned to value results in it being set to an empty string & expires is set UNIX_EPOCH' do v = nil - @cookie.value = v + cookie.value = v - expect(@cookie.value).to eql('') - expect(@cookie.value.class).to eql(String) - expect(@cookie.expires).to eql(Time.at(0)) + expect(cookie.value).to eql('') + expect(cookie.value.class).to eql(String) + expect(cookie.expires).to eql(Time.at(0)) end end end - describe "max_age" do - describe "Integer" do - it "assigns an integer of 0 to max_age successfully" do + describe 'max_age' do + describe 'Integer' do + it 'assigns an integer of 0 to max_age successfully' do int = 0 - @cookie.max_age = int + cookie.max_age = int - expect(@cookie.max_age).to eql(int) + expect(cookie.max_age).to eql(int) end - it "assigns a positive integer to max_age successfully" do + it 'assigns a positive integer to max_age successfully' do pos_val = rand(1..100) - @cookie.max_age = pos_val + cookie.max_age = pos_val - expect(@cookie.max_age).to eql(pos_val) + expect(cookie.max_age).to eql(pos_val) end - it "assigns a negative integer to max_age successfully" do + it 'assigns a negative integer to max_age successfully' do neg_val = rand(-1..-100) - @cookie.max_age = neg_val + cookie.max_age = neg_val - expect(@cookie.max_age).to eql(neg_val) + expect(cookie.max_age).to eql(neg_val) end end - describe "String" do - it "assigns a String of 0 to max_age successfully" do + describe 'String' do + it 'assigns a String of 0 to max_age successfully' do str = '0' - @cookie.max_age = str + cookie.max_age = str - expect(@cookie.max_age).to eql(str.to_i) + expect(cookie.max_age).to eql(str.to_i) end - it "assigns a String of a positive Integer to max_age successfully" do + it 'assigns a String of a positive Integer to max_age successfully' do pos_val = rand(1..100) - @cookie.max_age = pos_val.to_s + cookie.max_age = pos_val.to_s - expect(@cookie.max_age).to eql(pos_val) + expect(cookie.max_age).to eql(pos_val) end - it "assigns a String of a negative Integer to max_age successfully" do + it 'assigns a String of a negative Integer to max_age successfully' do neg_val = rand(-100..-1) - @cookie.max_age = neg_val.to_s + cookie.max_age = neg_val.to_s - expect(@cookie.max_age).to eql(neg_val) + expect(cookie.max_age).to eql(neg_val) end - it "throws an ArgumentError with a String that cannot be converted into an Integer" do + it 'throws an ArgumentError with a String that cannot be converted into an Integer' do invalid_str = random_string - expect { - @cookie.max_age = invalid_str - }.to raise_error(ArgumentError) + expect do + cookie.max_age = invalid_str + end.to raise_error(ArgumentError) end end describe 'Complex Object' do - it "throws an TypeError with a Complex Object that cannot be converted into an Integer" do - obj = [rand(9),rand(9),rand(9)] + it 'throws an TypeError with a Complex Object that cannot be converted into an Integer' do + obj = [rand(9), rand(9), rand(9)] - expect { - @cookie.max_age = obj - }.to raise_error(TypeError) + expect do + cookie.max_age = obj + end.to raise_error(TypeError) end end describe 'nil' do - it "assigns a nil to max_age successfully" do - @cookie.max_age = nil - expect(@cookie.max_age).to eql(nil) + it 'assigns a nil to max_age successfully' do + cookie.max_age = nil + expect(cookie.max_age).to eql(nil) end end end - describe "expires" do - describe "Time" do - it "instance is assigned to expires successfully" do + describe 'expires' do + describe 'Time' do + it 'instance is assigned to expires successfully' do t = Time.now - @cookie.expires = t + cookie.expires = t - expect(@cookie.expires).to eql(t) - expect(@cookie.expires.class).to eql(Time) + expect(cookie.expires).to eql(t) + expect(cookie.expires.class).to eql(Time) end end describe 'Non-Time' do - it "which can be parsed to a Time object, will be assigned to expires successfully" do + it 'which can be parsed to a Time object, will be assigned to expires successfully' do obj = Time.now.to_s - @cookie.expires = obj + cookie.expires = obj - expect(@cookie.expires).to eql(Time.parse(obj)) + expect(cookie.expires).to eql(Time.parse(obj)) end - it "which cannot be parsed to a Time object, when assigned to expires, will throw a TypeError" do - obj = [rand(9),rand(9),rand(9)] + it 'which cannot be parsed to a Time object, when assigned to expires, will throw a TypeError' do + obj = [rand(9), rand(9), rand(9)] - expect { - @cookie.expires = obj - }.to raise_error(TypeError) + expect do + cookie.expires = obj + end.to raise_error(TypeError) end end describe 'nil' do - it "assigned to expires successfully" do - @cookie.expires = nil - expect(@cookie.expires).to eql(nil) + it 'assigned to expires successfully' do + cookie.expires = nil + expect(cookie.expires).to eql(nil) end end end - describe "path" do - describe "String" do - it "beginning with \"/\" is passed to \"path\" successfully" do + describe 'path' do + describe 'String' do + it 'beginning with "/" is passed to "path" successfully' do p = '/' + random_string - @cookie.path = p + cookie.path = p - expect(@cookie.path).to eql(p) - expect(@cookie.path.class).to eql(String) + expect(cookie.path).to eql(p) + expect(cookie.path.class).to eql(String) end - it "not beginning with \"/\" is passed to \"path\" and is set as \"/\"" do + it 'not beginning with "/" is passed to "path" and is set as "/"' do p = random_string - @cookie.path = p + cookie.path = p - expect(@cookie.path).to eql('/') - expect(@cookie.path.class).to eql(String) + expect(cookie.path).to eql('/') + expect(cookie.path.class).to eql(String) end - it "that is empty is passed to \"path\" and is set as \"/\"" do + it 'that is empty is passed to "path" and is set as "/"' do p = '' - @cookie.path = p + cookie.path = p - expect(@cookie.path).to eql('/') - expect(@cookie.path.class).to eql(String) + expect(cookie.path).to eql('/') + expect(cookie.path.class).to eql(String) end end # If the Object A responds to to_str with a truthy Object that responds true to start_with?('/'), # then path is set to A describe 'Complex Object' do - it "which is a kind of String will be passed to path successfully" do + it 'which is a kind of String will be passed to path successfully' do class MyString < String end str = "/#{random_string(0)}" my_str = MyString.new(str) - @cookie.path = my_str + cookie.path = my_str - expect(@cookie.path).to eql(str) + expect(cookie.path).to eql(str) end end describe 'nil' do - it "assigned to path successfully" do + it 'assigned to path successfully' do n = nil - @cookie.expires = n + cookie.expires = n - expect(@cookie.expires).to eql(n) + expect(cookie.expires).to eql(n) end end end - describe "domain" do - describe "DomainName" do - it "assigned to domain when origin is set will result in a domain based on origin.host" do + describe 'domain' do + describe 'DomainName' do + it 'assigned to domain when origin is set will result in a domain based on origin.host' do d = DomainName(random_string) - @cookie.domain = d + cookie.domain = d - expect(@cookie.domain).to eql(d.hostname) + expect(cookie.domain).to eql(d.hostname) end end describe 'nil' do - it "assigned to domain when origin is not set will result in a nil domain" do + it 'assigned to domain when origin is not set will result in a nil domain' do n = nil - @cookie.domain = n + cookie.domain = n - expect(@cookie.domain).to eql(n) + expect(cookie.domain).to eql(n) end end describe 'String' do - it "assigned to domain will be converted to a DomainName and assigned" do + it 'assigned to domain will be converted to a DomainName and assigned' do s = random_string - @cookie.domain = s + cookie.domain = s - expect(@cookie.domain).to eql(DomainName(s).domain) + expect(cookie.domain).to eql(DomainName(s).domain) end end end # describe "origin" do # before do - # @cookie = HttpCookie.new('Name', 'Value') + # cookie = Msf::Exploit::Remote::HTTP::HttpCookie.new('Name', 'Value') # end # # describe 'Origin already set with value' do # it "when being set with any value will throw an ArgumentError" do # n = '' # - # @cookie.origin = n + # cookie.origin = n # # expect { - # @cookie.origin = n + # cookie.origin = n # }.to raise_error(ArgumentError) # end # end @@ -304,24 +300,24 @@ RSpec.describe HttpCookie do # it "of URL without protocol is assigned to origin without altering domain OR path" do # url_str = random_string # - # @cookie.origin = url_str + # cookie.origin = url_str # - # expect(@cookie.origin.kind_of?(URI)).to eql(true) - # expect(@cookie.origin).to eql(URI(url_str)) - # expect(@cookie.domain).to eql(nil) - # expect(@cookie.path).to eql(nil) + # expect(cookie.origin.kind_of?(URI)).to eql(true) + # expect(cookie.origin).to eql(URI(url_str)) + # expect(cookie.domain).to eql(nil) + # expect(cookie.path).to eql(nil) # end # # it "of HTTP URL with path is assigned to origin, broken out, and domain is populated correctly" do # domain = "#{random_string}.#{random_string(2,2)}" # url_str = "http://#{domain}" # - # @cookie.origin = url_str + # cookie.origin = url_str # - # expect(@cookie.origin.instance_of?(URI::HTTP)).to eql(true) - # expect(@cookie.origin).to eql(URI(url_str)) - # expect(@cookie.domain).to eql(domain.downcase) - # expect(@cookie.path).to eql('/') + # expect(cookie.origin.instance_of?(URI::HTTP)).to eql(true) + # expect(cookie.origin).to eql(URI(url_str)) + # expect(cookie.domain).to eql(domain.downcase) + # expect(cookie.path).to eql('/') # end # # it "of HTTP URL without path is assigned to origin, and domain & path are populated correctly" do @@ -329,24 +325,24 @@ RSpec.describe HttpCookie do # domain = "#{random_string}.#{random_string(2,2)}" # url_str = "http://#{domain}#{path}#{random_string}" # - # @cookie.origin = url_str + # cookie.origin = url_str # - # expect(@cookie.origin.instance_of?(URI::HTTP)).to eql(true) - # expect(@cookie.origin).to eql(URI(url_str)) - # expect(@cookie.domain).to eql(domain.downcase) - # expect(@cookie.path).to eql(path) + # expect(cookie.origin.instance_of?(URI::HTTP)).to eql(true) + # expect(cookie.origin).to eql(URI(url_str)) + # expect(cookie.domain).to eql(domain.downcase) + # expect(cookie.path).to eql(path) # end # # it "of HTTPS URL with path is assigned to origin, broken out, and domain is populated correctly" do # domain = "#{random_string}.#{random_string(2,2)}" # url_str = "https://#{domain}" # - # @cookie.origin = url_str + # cookie.origin = url_str # - # expect(@cookie.origin.instance_of?(URI::HTTPS)).to eql(true) - # expect(@cookie.origin).to eql(URI(url_str)) - # expect(@cookie.domain).to eql(domain.downcase) - # expect(@cookie.path).to eql('/') + # expect(cookie.origin.instance_of?(URI::HTTPS)).to eql(true) + # expect(cookie.origin).to eql(URI(url_str)) + # expect(cookie.domain).to eql(domain.downcase) + # expect(cookie.path).to eql('/') # end # # it "of HTTPS URL without path is assigned to origin, and domain & path are populated correctly" do @@ -354,12 +350,12 @@ RSpec.describe HttpCookie do # domain = "#{random_string}.#{random_string(2,2)}" # url_str = "https://#{domain}#{path}#{random_string}" # - # @cookie.origin = url_str + # cookie.origin = url_str # - # expect(@cookie.origin.instance_of?(URI::HTTPS)).to eql(true) - # expect(@cookie.origin).to eql(URI(url_str)) - # expect(@cookie.domain).to eql(domain.downcase) - # expect(@cookie.path).to eql(path) + # expect(cookie.origin.instance_of?(URI::HTTPS)).to eql(true) + # expect(cookie.origin).to eql(URI(url_str)) + # expect(cookie.domain).to eql(domain.downcase) + # expect(cookie.path).to eql(path) # end # end # @@ -367,119 +363,119 @@ RSpec.describe HttpCookie do # it "instance of URI::Generic will be passed to origin successfully" do # url = URI(random_string) # - # @cookie.origin = url + # cookie.origin = url # - # expect(@cookie.origin).to eql(url) + # expect(cookie.origin).to eql(url) # end # # it "instance of URI::HTTP will be passed to origin successfully" do # url = URI('http://' + random_string) # - # @cookie.origin = url + # cookie.origin = url # - # expect(@cookie.origin).to eql(url) + # expect(cookie.origin).to eql(url) # end # # it "URI::HTTPS will be passed to origin successfully" do # url = URI('https://' + random_string) # - # @cookie.origin = url + # cookie.origin = url # - # expect(@cookie.origin).to eql(url) + # expect(cookie.origin).to eql(url) # end # end # end - describe "httponly" do - describe "Truthy" do - it "empty string passed to httponly is set as true" do + describe 'httponly' do + describe 'Truthy' do + it 'empty string passed to httponly is set as true' do str = '' - @cookie.httponly = str + cookie.httponly = str - expect(@cookie.httponly).to eql(true) + expect(cookie.httponly).to eql(true) end - it "populated string passed to httponly is set as true" do + it 'populated string passed to httponly is set as true' do str = random_string - @cookie.httponly = str + cookie.httponly = str - expect(@cookie.httponly).to eql(true) + expect(cookie.httponly).to eql(true) end - it "integer passed to httponly is set as true" do + it 'integer passed to httponly is set as true' do int = rand(0..10) - @cookie.httponly = int + cookie.httponly = int - expect(@cookie.httponly).to eql(true) + expect(cookie.httponly).to eql(true) end - it "true passed to httponly is set as true" do + it 'true passed to httponly is set as true' do t = true - @cookie.httponly = t + cookie.httponly = t - expect(@cookie.httponly).to eql(true) + expect(cookie.httponly).to eql(true) end end - describe "Falsey" do - it "nil passed to httponly is set as false" do + describe 'Falsey' do + it 'nil passed to httponly is set as false' do n = nil - @cookie.httponly = n + cookie.httponly = n - expect(@cookie.httponly).to eql(false) + expect(cookie.httponly).to eql(false) end - it "false passed to httponly is set as false" do + it 'false passed to httponly is set as false' do f = false - @cookie.httponly = f + cookie.httponly = f - expect(@cookie.httponly).to eql(false) + expect(cookie.httponly).to eql(false) end end end # describe 'for_domain' do # before do - # @cookie = HttpCookie.new('Name', 'Value') + # cookie = Msf::Exploit::Remote::HTTP::HttpCookie.new('Name', 'Value') # end # # describe "Truthy" do # it "empty string passed to for_domain is set as true" do # str = '' # - # @cookie.for_domain = str + # cookie.for_domain = str # - # expect(@cookie.for_domain).to eql(true) + # expect(cookie.for_domain).to eql(true) # end # # it "populated string passed to for_domain is set as true" do # str = random_string # - # @cookie.for_domain = str + # cookie.for_domain = str # - # expect(@cookie.for_domain).to eql(true) + # expect(cookie.for_domain).to eql(true) # end # # it "integer passed to for_domain is set as true" do # int = rand(0..10) # - # @cookie.for_domain = int + # cookie.for_domain = int # - # expect(@cookie.for_domain).to eql(true) + # expect(cookie.for_domain).to eql(true) # end # # it "true passed to for_domain is set as true" do # t = true # - # @cookie.for_domain = t + # cookie.for_domain = t # - # expect(@cookie.for_domain).to eql(true) + # expect(cookie.for_domain).to eql(true) # end # end # @@ -487,263 +483,261 @@ RSpec.describe HttpCookie do # it "nil passed to for_domain is set as false" do # n = nil # - # @cookie.for_domain = n + # cookie.for_domain = n # - # expect(@cookie.for_domain).to eql(false) + # expect(cookie.for_domain).to eql(false) # end # # it "false passed to for_domain is set as false" do # f = false # - # @cookie.for_domain = f + # cookie.for_domain = f # - # expect(@cookie.for_domain).to eql(false) + # expect(cookie.for_domain).to eql(false) # end # end # end - describe "secure" do - describe "Truthy" do - it "empty string passed to secure is set as true" do + describe 'secure' do + describe 'Truthy' do + it 'empty string passed to secure is set as true' do str = '' - @cookie.secure = str + cookie.secure = str - expect(@cookie.secure).to eql(true) + expect(cookie.secure).to eql(true) end - it "populated string passed to secure is set as true" do + it 'populated string passed to secure is set as true' do str = random_string - @cookie.secure = str + cookie.secure = str - expect(@cookie.secure).to eql(true) + expect(cookie.secure).to eql(true) end - it "integer passed to secure is set as true" do + it 'integer passed to secure is set as true' do int = rand(0..10) - @cookie.secure = int + cookie.secure = int - expect(@cookie.secure).to eql(true) + expect(cookie.secure).to eql(true) end - it "true passed to secure is set as true" do + it 'true passed to secure is set as true' do t = true - @cookie.secure = t + cookie.secure = t - expect(@cookie.secure).to eql(true) + expect(cookie.secure).to eql(true) end end - describe "Falsey" do - it "nil passed to secure is set as false" do + describe 'Falsey' do + it 'nil passed to secure is set as false' do n = nil - @cookie.secure = n + cookie.secure = n - expect(@cookie.secure).to eql(false) + expect(cookie.secure).to eql(false) end - it "false passed to secure is set as false" do + it 'false passed to secure is set as false' do f = false - @cookie.secure = f + cookie.secure = f - expect(@cookie.secure).to eql(false) + expect(cookie.secure).to eql(false) end end end - describe "created_at" do - describe "Time" do - it "is returned by created_at after the cookie is initialized" do - #cookie created in before + describe 'created_at' do + describe 'Time' do + it 'is returned by created_at after the cookie is initialized' do + # cookie created in before - c = @cookie.created_at + c = cookie.created_at expect(c.class).to eql(Time) end - it "instance is assigned to created_at successfully" do + it 'instance is assigned to created_at successfully' do t = Time.now - @cookie.created_at = t + cookie.created_at = t - expect(@cookie.created_at).to eql(t) - expect(@cookie.created_at.class).to eql(Time) + expect(cookie.created_at).to eql(t) + expect(cookie.created_at.class).to eql(Time) end end describe 'Non-Time' do - it "which can be parsed to a Time object, will be assigned to created_at successfully" do + it 'which can be parsed to a Time object, will be assigned to created_at successfully' do obj = Time.now.to_s - @cookie.created_at = obj + cookie.created_at = obj - expect(@cookie.created_at).to eql(Time.parse(obj)) + expect(cookie.created_at).to eql(Time.parse(obj)) end - it "which cannot be parsed to a Time object, when assigned to created_at, will throw a TypeError" do - obj = [rand(9),rand(9),rand(9)] + it 'which cannot be parsed to a Time object, when assigned to created_at, will throw a TypeError' do + obj = [rand(9), rand(9), rand(9)] - expect { - @cookie.created_at = obj - }.to raise_error(TypeError) + expect do + cookie.created_at = obj + end.to raise_error(TypeError) end end describe 'nil' do - it "assigned to created_at successfully" do - @cookie.created_at = nil - expect(@cookie.created_at).to eql(nil) + it 'assigned to created_at successfully' do + cookie.created_at = nil + expect(cookie.created_at).to eql(nil) end end end - describe "accessed_at" do - describe "Time" do - it "is returned by accessed_at after the cookie is initialized" do - #cookie created in before + describe 'accessed_at' do + describe 'Time' do + it 'is returned by accessed_at after the cookie is initialized' do + # cookie created in before - c = @cookie.accessed_at + c = cookie.accessed_at expect(c.class).to eql(Time) end - it "instance is assigned to accessed_at successfully" do + it 'instance is assigned to accessed_at successfully' do t = Time.now - @cookie.accessed_at = t + cookie.accessed_at = t - expect(@cookie.accessed_at).to eql(t) - expect(@cookie.accessed_at.class).to eql(Time) + expect(cookie.accessed_at).to eql(t) + expect(cookie.accessed_at.class).to eql(Time) end end describe 'Non-Time' do - it "which can be parsed to a Time object, will be assigned to accessed_at successfully" do + it 'which can be parsed to a Time object, will be assigned to accessed_at successfully' do obj = Time.now.to_s - @cookie.accessed_at = obj + cookie.accessed_at = obj - expect(@cookie.accessed_at).to eql(Time.parse(obj)) + expect(cookie.accessed_at).to eql(Time.parse(obj)) end - it "which cannot be parsed to a Time object, when assigned to accessed_at, will throw a TypeError" do - obj = [rand(9),rand(9),rand(9)] + it 'which cannot be parsed to a Time object, when assigned to accessed_at, will throw a TypeError' do + obj = [rand(9), rand(9), rand(9)] - expect { - @cookie.accessed_at = obj - }.to raise_error(TypeError) + expect do + cookie.accessed_at = obj + end.to raise_error(TypeError) end end describe 'nil' do - it "assigned to accessed_at successfully" do - @cookie.accessed_at = nil - expect(@cookie.accessed_at).to eql(nil) + it 'assigned to accessed_at successfully' do + cookie.accessed_at = nil + expect(cookie.accessed_at).to eql(nil) end end end - describe "session" do - describe "Session" do - it "is set as True when initialized with no max_age or expires args and a nil value" do - #cookie created in before + describe 'session' do + describe 'Session' do + it 'is set as True when initialized with no max_age or expires args and a nil value' do + # cookie created in before - s = @cookie.session? + s = cookie.session? - expect(s).to eql(true ) + expect(s).to eql(true) end - it "is set as the nil? value of max_age when a valid value is assigned to max_age" do + it 'is set as the nil? value of max_age when a valid value is assigned to max_age' do max_age = rand(0..1) == 1 ? 1 : nil - @cookie.max_age= max_age + cookie.max_age = max_age - expect(@cookie.session?).to eql(max_age.nil?) + expect(cookie.session?).to eql(max_age.nil?) end - it "is set as the nil? value of expires when a valid value is assigned to expires" do + it 'is set as the nil? value of expires when a valid value is assigned to expires' do expires = rand(0..1) == 1 ? Time.now : nil - @cookie.expires= expires + cookie.expires = expires - expect(@cookie.session?).to eql(expires.nil?) + expect(cookie.session?).to eql(expires.nil?) end end end - describe "expired?" do - describe "nil" do - it "assigned to expires causes expired? to return false" do + describe 'expired?' do + describe 'nil' do + it 'assigned to expires causes expired? to return false' do n = nil - @cookie.expires = n + cookie.expires = n - expect(@cookie.expired?).to eql(false) + expect(cookie.expired?).to eql(false) end end - describe "No-Arg" do - it "call of expired? returns false when expires & max_age are set to nil" do + describe 'No-Arg' do + it 'call of expired? returns false when expires & max_age are set to nil' do n = nil - @cookie.expires = n + cookie.expires = n - expect(@cookie.expired?).to eql(false) + expect(cookie.expired?).to eql(false) end - - it "call of expired? with a valid expires value will return True/False depending on if the value is before Time.now" do + it 'call of expired? with a valid expires value will return True/False depending on if the value is before Time.now' do expired = rand(0..1) == 1 t = expired ? Faker::Time.backward(days: 1) : Faker::Time.forward(days: 1) - @cookie.expires = t + cookie.expires = t - expect(@cookie.expired?).to eql(expired) + expect(cookie.expired?).to eql(expired) end - it "call of expired? with a valid max_age value will return True/False depending on if the value is before Time.now" do + it 'call of expired? with a valid max_age value will return True/False depending on if the value is before Time.now' do expired = rand(0..1) == 1 m = expired ? 1 : 1000 - @cookie.max_age = m + cookie.max_age = m sleep(1) - expect(@cookie.expired?).to eql(expired) + expect(cookie.expired?).to eql(expired) end end - describe "Time" do - it "call of expired? with any Time value returns false when expires is set to nil" do + describe 'Time' do + it 'call of expired? with any Time value returns false when expires is set to nil' do n = nil - @cookie.expires = n - @cookie.max_age = n + cookie.expires = n + cookie.max_age = n - expect(@cookie.expired?(Time.now)).to eql(false) + expect(cookie.expired?(Time.now)).to eql(false) end - - it "passed to expired? with expires set to valid Time value, will return True/False depending on if the passed Time value is after expires" do + it 'passed to expired? with expires set to valid Time value, will return True/False depending on if the passed Time value is after expires' do expired = rand(0..1) == 1 t = expired ? Faker::Time.forward(days: 1) : Faker::Time.backward(days: 1) - @cookie.expires = Time.now + cookie.expires = Time.now - expect(@cookie.expired?(t)).to eql(expired) + expect(cookie.expired?(t)).to eql(expired) end - it "passed to expired? with max_age set to a valid Integer, will return True/False depending on if created_at + max_age is after expires" do + it 'passed to expired? with max_age set to a valid Integer, will return True/False depending on if created_at + max_age is after expires' do expired = rand(0..1) == 1 t = expired ? Faker::Time.forward(days: 1) : Faker::Time.backward(days: 1) - @cookie.max_age = 1 + cookie.max_age = 1 - expect(@cookie.expired?(t)).to eql(expired) + expect(cookie.expired?(t)).to eql(expired) end end end @@ -755,67 +749,67 @@ RSpec.describe HttpCookie do # # RFC 6265 5.4 # return false if secure? && !(URI::HTTPS === uri) # acceptable_from_uri?(uri) && HTTP::Cookie.path_match?(@path, uri.path) - describe "valid_for_uri?" do + describe 'valid_for_uri?' do it "will return false when domain hasn't been set" do - #domain set as nil in before + # domain set as nil in before - expect { - @cookie.valid_for_uri?(URI(random_string)) - }.to raise_error(ArgumentError) + expect do + cookie.valid_for_uri?(URI(random_string)) + end.to raise_error(ArgumentError) end - it "will return false if secure is set as true and the passed cookie is a https url" do - @cookie.domain = random_string + it 'will return false if secure is set as true and the passed cookie is a https url' do + cookie.domain = random_string - v = @cookie.valid_for_uri?(URI("https://#{random_string}")) + v = cookie.valid_for_uri?(URI("https://#{random_string}")) expect(v).to eq(false) end end - describe "acceptable_from_uri?" do - it "will return false when passed nil" do + describe 'acceptable_from_uri?' do + it 'will return false when passed nil' do n = nil - a = @cookie.acceptable_from_uri?(n) + a = cookie.acceptable_from_uri?(n) expect(a).to eq(false) end - it "will return false if url without http(s) protocol is passed" do + it 'will return false if url without http(s) protocol is passed' do generic_uri = random_string - v = @cookie.acceptable_from_uri?(generic_uri) + v = cookie.acceptable_from_uri?(generic_uri) expect(v).to eq(false) end - it "will return false if url with http(s) protocol is passed but has no host" do + it 'will return false if url with http(s) protocol is passed but has no host' do protcol = 'http://' - v = @cookie.acceptable_from_uri?(protcol) + v = cookie.acceptable_from_uri?(protcol) expect(URI(protcol).is_a?(::URI::HTTP)).to eq(true) expect(v).to eq(false) end - it "will return true if url with http(s) protocol is passed with a domain that watches the url domain" do + it 'will return true if url with http(s) protocol is passed with a domain that watches the url domain' do host = random_string uri = "http://#{host}/#{random_string}" - @cookie.domain = host + cookie.domain = host - v = @cookie.acceptable_from_uri?(uri) + v = cookie.acceptable_from_uri?(uri) expect(v).to eq(true) end it "will return domain.nil? if url with http(s) protocol is passed with a domain that doesn't match the url domain" do uri = "http://#{random_string}/#{random_string}" - @cookie.domain = rand(0..1) == 1 ? nil : random_string + cookie.domain = rand(0..1) == 1 ? nil : random_string - v = @cookie.acceptable_from_uri?(uri) + v = cookie.acceptable_from_uri?(uri) - expect(v).to eq(@cookie.domain.nil?) + expect(v).to eq(cookie.domain.nil?) end end end From 5df0f0b1649e2def37d27cda97f32dff662c0057 Mon Sep 17 00:00:00 2001 From: A Galway Date: Mon, 19 Apr 2021 15:13:42 +0100 Subject: [PATCH 3/5] improvements to tests and api --- .../core/exploit/remote/http/http_cookie.rb | 22 +- .../exploit/remote/http/http_cookie_jar.rb | 8 +- lib/msf/core/exploit/remote/http_client.rb | 7 +- .../remote/http/http_cookie_jar_spec.rb | 103 +++---- .../remote/remote/http/http_cookie_spec.rb | 271 +++++------------- 5 files changed, 114 insertions(+), 297 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/http_cookie.rb b/lib/msf/core/exploit/remote/http/http_cookie.rb index 489a1ab2fd..ac0f77225b 100644 --- a/lib/msf/core/exploit/remote/http/http_cookie.rb +++ b/lib/msf/core/exploit/remote/http/http_cookie.rb @@ -97,26 +97,6 @@ module Msf @cookie.httponly = !!httponly end - # def for_domain - # @cookie.for_domain - # end - # - # def for_domain= for_domain - # @cookie.for_domain= !!for_domain - # end - - # def origin - # @cookie.origin - # end - # - # def origin= origin - # if origin.kind_of?(URI) - # @cookie.origin= origin - # else - # @cookie.origin= origin.to_s - # end - # end - def domain @cookie.domain end @@ -125,7 +105,7 @@ module Msf if domain.nil? || domain.is_a?(DomainName) @cookie.domain = domain else - @cookie.domain = DomainName(domain.to_s) + @cookie.domain = domain.to_s end end diff --git a/lib/msf/core/exploit/remote/http/http_cookie_jar.rb b/lib/msf/core/exploit/remote/http/http_cookie_jar.rb index d32f0dd733..62c2823550 100644 --- a/lib/msf/core/exploit/remote/http/http_cookie_jar.rb +++ b/lib/msf/core/exploit/remote/http/http_cookie_jar.rb @@ -16,7 +16,7 @@ module Msf end def add(cookie) - raise TypeError, "Passed cookie is of class '#{cookie.class}' and not a subclass of '#{Msf::Exploit::Remote::HTTP::HttpCookie.class}" unless cookie.is_a?(HttpCookie) + raise TypeError, "Passed cookie is of class '#{cookie.class}' and not a subclass of '#{Msf::Exploit::Remote::HTTP::HttpCookie}" unless cookie.is_a?(Msf::Exploit::Remote::HTTP::HttpCookie) @cookie_jar.add(cookie) self @@ -24,7 +24,7 @@ module Msf def delete(cookie) return if @cookie_jar.cookies.empty? - raise TypeError, "Passed cookie is of class '#{cookie.class}' and not a subclass of '#{Msf::Exploit::Remote::HTTP::HttpCookie.class}" unless cookie.is_a?(HttpCookie) + raise TypeError, "Passed cookie is of class '#{cookie.class}' and not a subclass of '#{Msf::Exploit::Remote::HTTP::HttpCookie}" unless cookie.is_a?(Msf::Exploit::Remote::HTTP::HttpCookie) @cookie_jar.delete(cookie) self @@ -46,8 +46,8 @@ module Msf @cookie_jar.cleanup(expire_all) end - def empty?(url = nil) - @cookie_jar.empty?(url) + def empty? + @cookie_jar.empty? end def parse(set_cookie_header, origin_url, options = nil) diff --git a/lib/msf/core/exploit/remote/http_client.rb b/lib/msf/core/exploit/remote/http_client.rb index 870e62b35c..147aa433f6 100644 --- a/lib/msf/core/exploit/remote/http_client.rb +++ b/lib/msf/core/exploit/remote/http_client.rb @@ -108,7 +108,6 @@ module Exploit::Remote::HttpClient end - # # This method is meant to be overriden in the exploit module to specify a set of regexps to # attempt to match against. A failure to match any of them results in a RuntimeError exception # being raised. @@ -383,16 +382,16 @@ module Exploit::Remote::HttpClient # +expire_cookies+ will control if +cleanup+ is called on any passed +Msf::Exploit::Remote::HTTP::HttpCookieJar+ or the client cookiejar # # @return (see Rex::Proto::Http::Client#send_recv)) - def send_request_cgi(opts = {}, timeout = 20, disconnect = true, expire_cookies = true) + def send_request_cgi(opts = {}, timeout = 20, disconnect = true) if opts.has_key?('cookie') if opts['cookie'].is_a?(Msf::Exploit::Remote::HTTP::HttpCookieJar) - cookie_jar.cleanup if expire_cookies + cookie_jar.cleanup unless opts['expire_cookies'] == false opts.merge({ 'cookie' => opts['cookie'].cookies.join('; ') }) else opts.merge({ 'cookie' => opts['cookie'].to_s }) end elsif !cookie_jar.empty? - cookie_jar.cleanup if expire_cookies + cookie_jar.cleanup unless opts['expire_cookies'] == false opts = opts.merge({ 'cookie' => cookie_jar.cookies.join('; ') }) end diff --git a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb index 3a024bd3d7..bfe7a7aba5 100644 --- a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb +++ b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb @@ -1,6 +1,4 @@ require 'spec_helper' -require 'msf/core/exploit/remote/http/http_cookie_jar' -require 'msf/core/exploit/remote/http/http_cookie' RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do def random_string(min_len = 1, max_len = 12) @@ -8,22 +6,24 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do str[0, rand(min_len..max_len)] end - def cookie + def random_cookie Msf::Exploit::Remote::HTTP::HttpCookie.new( random_string, random_string, - max_age: rand(1..100), + max_age: Faker::Number.within(range: 1..100), path: '/' + random_string, - secure: rand(0..1) == 1, - httponly: rand(0..1) == 1, + secure: Faker::Boolean.boolean, + httponly: Faker::Boolean.boolean, domain: random_string ) end let(:cookie_jar) { described_class.new } + NOW = Time.local(2008, 9, 5, 10, 5, 30) + before(:each) do - Timecop.freeze(Time.local(2008, 9, 5, 10, 5, 30)) + Timecop.freeze(NOW) end after(:each) do @@ -40,7 +40,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do end it 'will return false when a cookie has been added to a cookie_jar' do - c = cookie + c = random_cookie cookie_jar.add(c) e = cookie_jar.empty? @@ -58,8 +58,8 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do expect(cookie_jar.empty?).to eq(true) end - it 'will return false when a cookie has been added to a cookie_jar' do - c = cookie + it 'will return true when populated cookie_jar has been cleared' do + c = random_cookie cookie_jar.add(c) cookie_jar.clear @@ -80,8 +80,8 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do it 'will return an array of all cookies added to the cookie_jar when called with no url param' do c_array = [] - rand(1..10).times do - c = cookie + Faker::Number.within(range: 1..10).times do + c = random_cookie c_array.append(c) cookie_jar.add(c) @@ -91,30 +91,10 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do expect(c_array.sort).to eq(jar_array.sort) end - - # it 'will return an array of all cookies that have parent paths to the url passed' do - # c_array = [cookie, cookie, cookie, cookie] - # c_array[0].secure = false - # c_array[1].secure = false - # c_array[1].domain = c_array[0].domain - # c_array[1].path = '/' - # c_array.map { |c| cookie_jar.add(c) } - # - # require 'pry' - # binding.pry - # - # filtered_cookies = cookie_jar.cookies("http://#{c_array[0].domain}#{c_array[0].path}") - # - # expect(filtered_cookies.length).to eq(2) - # expect(filtered_cookies.include?(c_array[0])).to eq(true) - # expect(filtered_cookies.include?(c_array[1])).to eq(true) - # expect(filtered_cookies.include?(c_array[2])).to eq(false) - # expect(filtered_cookies.include?(c_array[3])).to eq(false) - # end end describe 'add' do - it 'unacceptable cookie missing path throws ArgumentError' do + it 'unacceptable cookie without a domain will throw an ArgumentError' do c = Msf::Exploit::Remote::HTTP::HttpCookie.new( random_string, random_string, @@ -126,7 +106,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do end.to raise_error(ArgumentError) end - it 'unacceptable cookie missing domain throws ArgumentError' do + it 'unacceptable cookie without a path will throw an ArgumentError' do c = Msf::Exploit::Remote::HTTP::HttpCookie.new( random_string, random_string, @@ -139,7 +119,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do end it 'acceptable cookie added to cookie_jar successfully' do - c = cookie + c = random_cookie cookie_jar.add(c) @@ -147,8 +127,8 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do end it 'acceptable cookie added to cookie_jar containing cookie with the same name, domain, and path will result in an overwrite' do - c = cookie - c_dup = cookie + c = random_cookie + c_dup = random_cookie c_dup.name = c.name c_dup.domain = c.domain c_dup.path = c.path @@ -156,12 +136,11 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do cookie_jar.add(c) cookie_jar.add(c_dup) - expect(cookie_jar.cookies[0]).to eq(c_dup) - expect(cookie_jar.cookies[0]).to_not eq(c) + expect(cookie_jar.cookies).to match_array([c_dup]) end it 'variable not a subclass of ::HttpCookie will raise TypeError' do - int = rand(1..100) + int = 1 expect do cookie_jar.add(int) @@ -173,14 +152,14 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do it 'used on an empty jar will return nil' do # cookie_jar made in before - n = cookie_jar.delete(cookie) + n = cookie_jar.delete(random_cookie) expect(n).to eq(nil) end it 'passed cookie with same name, domain, and path as cookie in jar, will delete cookie in jar' do - c = cookie - c_dup = cookie + c = random_cookie + c_dup = random_cookie c_dup.name = c.name c_dup.domain = c.domain c_dup.path = c.path @@ -192,8 +171,8 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do end it 'passed a cookie different name, domain, and path as cookie in jar, will not delete cookie in jar' do - c = cookie - c_dup = cookie + c = random_cookie + c_dup = random_cookie c_dup.name = c.name + random_string(1, 1) c_dup.domain = c.domain + random_string(1, 1) c_dup.path = c.path + random_string(1, 1) @@ -206,7 +185,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do end it 'variable not a subclass of ::HttpCookie will not raise TypeError when the cookie_jar is empty' do - int = rand(1..100) + int = Faker::Number.within(range: 1..100) n = cookie_jar.delete(int) @@ -215,8 +194,8 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do end it 'variable not a subclass of ::HttpCookie will raise TypeError when the cookie_jar is not empty' do - cookie_jar.add(cookie) - int = rand(1..100) + cookie_jar.add(random_cookie) + int = Faker::Number.within(range: 1..100) expect do cookie_jar.delete(int) @@ -234,45 +213,37 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do end it 'will remove expired cookies with max_age value' do - expired_cookies = [cookie, cookie] + expired_cookies = [random_cookie, random_cookie] expired_cookies[0].max_age = 1 expired_cookies[1].max_age = 1 expired_cookies[0].created_at = Time.local(2008, 9, 5, 10, 5, 1) expired_cookies[1].created_at = Time.local(2008, 9, 5, 10, 5, 1) - cookies = [cookie, cookie] + cookies = [random_cookie, random_cookie] cookies[0].max_age = 10000 cookies[1].max_age = 10000 cookies[0].created_at = Time.now cookies[1].created_at = Time.now - cookies.map { |c| cookie_jar.add(c) } - expired_cookies.map { |c| cookie_jar.add(c) } + cookies.each { |c| cookie_jar.add(c) } + expired_cookies.each { |c| cookie_jar.add(c) } cookie_jar.cleanup - expect(cookie_jar.cookies.length).to eq(2) - expect(cookie_jar.cookies.include?(expired_cookies[0])).to eq(false) - expect(cookie_jar.cookies.include?(expired_cookies[1])).to eq(false) - expect(cookie_jar.cookies.include?(cookies[0])).to eq(true) - expect(cookie_jar.cookies.include?(cookies[1])).to eq(true) + expect(cookie_jar.cookies).to match_array(cookies) end it 'will remove expired cookies with expires value' do - expired_cookies = [cookie, cookie] + expired_cookies = [random_cookie, random_cookie] expired_cookies[0].expires = Time.local(2008, 9, 3, 10, 5, 0) expired_cookies[1].expires = Time.local(2008, 9, 4, 10, 5, 0) - cookies = [cookie, cookie] + cookies = [random_cookie, random_cookie] cookies[0].expires = Time.local(2008, 9, 6, 10, 5, 0) cookies[1].expires = Time.local(2008, 9, 7, 10, 5, 0) - cookies.map { |c| cookie_jar.add(c) } - expired_cookies.map { |c| cookie_jar.add(c) } + cookies.each { |c| cookie_jar.add(c) } + expired_cookies.each { |c| cookie_jar.add(c) } cookie_jar.cleanup - expect(cookie_jar.cookies.length).to eq(2) - expect(cookie_jar.cookies.include?(expired_cookies[0])).to eq(false) - expect(cookie_jar.cookies.include?(expired_cookies[1])).to eq(false) - expect(cookie_jar.cookies.include?(cookies[0])).to eq(true) - expect(cookie_jar.cookies.include?(cookies[1])).to eq(true) + expect(cookie_jar.cookies).to match_array(cookies) end end end diff --git a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb index 4a04855c36..15e68058db 100644 --- a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb +++ b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb @@ -9,6 +9,16 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do let(:cookie) { described_class.new(random_string, random_string) } + NOW = Time.local(2008, 9, 5, 10, 5, 30) + + before(:each) do + Timecop.freeze(NOW) + end + + after(:each) do + Timecop.return + end + describe 'name' do describe 'String' do it 'is assigned to name successfully' do @@ -225,10 +235,10 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do # then path is set to A describe 'Complex Object' do it 'which is a kind of String will be passed to path successfully' do - class MyString < String + clazz = Class.new(String) do end str = "/#{random_string(0)}" - my_str = MyString.new(str) + my_str = clazz.new(str) cookie.path = my_str @@ -237,7 +247,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do end describe 'nil' do - it 'assigned to path successfully' do + it 'assigned to expired successfully' do n = nil cookie.expires = n @@ -279,113 +289,6 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do end end - # describe "origin" do - # before do - # cookie = Msf::Exploit::Remote::HTTP::HttpCookie.new('Name', 'Value') - # end - # - # describe 'Origin already set with value' do - # it "when being set with any value will throw an ArgumentError" do - # n = '' - # - # cookie.origin = n - # - # expect { - # cookie.origin = n - # }.to raise_error(ArgumentError) - # end - # end - # - # describe "String" do - # it "of URL without protocol is assigned to origin without altering domain OR path" do - # url_str = random_string - # - # cookie.origin = url_str - # - # expect(cookie.origin.kind_of?(URI)).to eql(true) - # expect(cookie.origin).to eql(URI(url_str)) - # expect(cookie.domain).to eql(nil) - # expect(cookie.path).to eql(nil) - # end - # - # it "of HTTP URL with path is assigned to origin, broken out, and domain is populated correctly" do - # domain = "#{random_string}.#{random_string(2,2)}" - # url_str = "http://#{domain}" - # - # cookie.origin = url_str - # - # expect(cookie.origin.instance_of?(URI::HTTP)).to eql(true) - # expect(cookie.origin).to eql(URI(url_str)) - # expect(cookie.domain).to eql(domain.downcase) - # expect(cookie.path).to eql('/') - # end - # - # it "of HTTP URL without path is assigned to origin, and domain & path are populated correctly" do - # path = "/#{random_string}/#{random_string}/" - # domain = "#{random_string}.#{random_string(2,2)}" - # url_str = "http://#{domain}#{path}#{random_string}" - # - # cookie.origin = url_str - # - # expect(cookie.origin.instance_of?(URI::HTTP)).to eql(true) - # expect(cookie.origin).to eql(URI(url_str)) - # expect(cookie.domain).to eql(domain.downcase) - # expect(cookie.path).to eql(path) - # end - # - # it "of HTTPS URL with path is assigned to origin, broken out, and domain is populated correctly" do - # domain = "#{random_string}.#{random_string(2,2)}" - # url_str = "https://#{domain}" - # - # cookie.origin = url_str - # - # expect(cookie.origin.instance_of?(URI::HTTPS)).to eql(true) - # expect(cookie.origin).to eql(URI(url_str)) - # expect(cookie.domain).to eql(domain.downcase) - # expect(cookie.path).to eql('/') - # end - # - # it "of HTTPS URL without path is assigned to origin, and domain & path are populated correctly" do - # path = "/#{random_string}/#{random_string}/" - # domain = "#{random_string}.#{random_string(2,2)}" - # url_str = "https://#{domain}#{path}#{random_string}" - # - # cookie.origin = url_str - # - # expect(cookie.origin.instance_of?(URI::HTTPS)).to eql(true) - # expect(cookie.origin).to eql(URI(url_str)) - # expect(cookie.domain).to eql(domain.downcase) - # expect(cookie.path).to eql(path) - # end - # end - # - # describe 'URI' do - # it "instance of URI::Generic will be passed to origin successfully" do - # url = URI(random_string) - # - # cookie.origin = url - # - # expect(cookie.origin).to eql(url) - # end - # - # it "instance of URI::HTTP will be passed to origin successfully" do - # url = URI('http://' + random_string) - # - # cookie.origin = url - # - # expect(cookie.origin).to eql(url) - # end - # - # it "URI::HTTPS will be passed to origin successfully" do - # url = URI('https://' + random_string) - # - # cookie.origin = url - # - # expect(cookie.origin).to eql(url) - # end - # end - # end - describe 'httponly' do describe 'Truthy' do it 'empty string passed to httponly is set as true' do @@ -440,64 +343,6 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do end end - # describe 'for_domain' do - # before do - # cookie = Msf::Exploit::Remote::HTTP::HttpCookie.new('Name', 'Value') - # end - # - # describe "Truthy" do - # it "empty string passed to for_domain is set as true" do - # str = '' - # - # cookie.for_domain = str - # - # expect(cookie.for_domain).to eql(true) - # end - # - # it "populated string passed to for_domain is set as true" do - # str = random_string - # - # cookie.for_domain = str - # - # expect(cookie.for_domain).to eql(true) - # end - # - # it "integer passed to for_domain is set as true" do - # int = rand(0..10) - # - # cookie.for_domain = int - # - # expect(cookie.for_domain).to eql(true) - # end - # - # it "true passed to for_domain is set as true" do - # t = true - # - # cookie.for_domain = t - # - # expect(cookie.for_domain).to eql(true) - # end - # end - # - # describe "Falsey" do - # it "nil passed to for_domain is set as false" do - # n = nil - # - # cookie.for_domain = n - # - # expect(cookie.for_domain).to eql(false) - # end - # - # it "false passed to for_domain is set as false" do - # f = false - # - # cookie.for_domain = f - # - # expect(cookie.for_domain).to eql(false) - # end - # end - # end - describe 'secure' do describe 'Truthy' do it 'empty string passed to secure is set as true' do @@ -645,30 +490,44 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do end describe 'session' do - describe 'Session' do - it 'is set as True when initialized with no max_age or expires args and a nil value' do - # cookie created in before + it 'is set as True when initialized with no max_age or expires args and a nil value' do + # cookie created in before - s = cookie.session? + s = cookie.session? - expect(s).to eql(true) - end + expect(s).to eql(true) + end - it 'is set as the nil? value of max_age when a valid value is assigned to max_age' do - max_age = rand(0..1) == 1 ? 1 : nil + it 'is set to true when nil is assigned to max_age' do + max_age = nil - cookie.max_age = max_age + cookie.max_age = max_age - expect(cookie.session?).to eql(max_age.nil?) - end + expect(cookie.session?).to eq(true) + end - it 'is set as the nil? value of expires when a valid value is assigned to expires' do - expires = rand(0..1) == 1 ? Time.now : nil + it 'is set to false when an integer > 0 is assigned to max_age' do + max_age = Faker::Number.within(range: 1..100) - cookie.expires = expires + cookie.max_age = max_age - expect(cookie.session?).to eql(expires.nil?) - end + expect(cookie.session?).to eq(false) + end + + it 'is set to true when nil is assigned to expires' do + expires = nil + + cookie.expires = expires + + expect(cookie.session?).to eq(true) + end + + it 'is set to false when a valid Time is assigned to expires' do + expires = Time.now + + cookie.expires = expires + + expect(cookie.session?).to eq(false) end end @@ -692,23 +551,38 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do expect(cookie.expired?).to eql(false) end - it 'call of expired? with a valid expires value will return True/False depending on if the value is before Time.now' do - expired = rand(0..1) == 1 - t = expired ? Faker::Time.backward(days: 1) : Faker::Time.forward(days: 1) + it 'call of expired? will return true when expires is set to a Time in the past' do + t = Faker::Time.backward(days: 1) cookie.expires = t - expect(cookie.expired?).to eql(expired) + expect(cookie.expired?).to eql(true) end - it 'call of expired? with a valid max_age value will return True/False depending on if the value is before Time.now' do - expired = rand(0..1) == 1 - m = expired ? 1 : 1000 + it 'call of expired? will return false when expires is set to a Time in the future' do + t = Faker::Time.forward(days: 1) - cookie.max_age = m - sleep(1) + cookie.expires = t - expect(cookie.expired?).to eql(expired) + expect(cookie.expired?).to eql(false) + end + + it 'call of expired? will return true when max_age in seconds plus cookie.created_at is before Time.now' do + max_age = 1 + + cookie.max_age = max_age + cookie.created_at = NOW - 5.seconds + + expect(cookie.expired?).to eql(true) + end + + it 'call of expired? will return false when max_age in seconds plus cookie.created_at is after Time.now' do + max_age = 10 + + cookie.max_age = max_age + cookie.created_at = NOW - 5.seconds + + expect(cookie.expired?).to eql(false) end end @@ -742,13 +616,6 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do end end - # if @domain.nil? - # raise "cannot tell if this cookie is valid because the domain is unknown" - # end - # uri = URI(uri) - # # RFC 6265 5.4 - # return false if secure? && !(URI::HTTPS === uri) - # acceptable_from_uri?(uri) && HTTP::Cookie.path_match?(@path, uri.path) describe 'valid_for_uri?' do it "will return false when domain hasn't been set" do # domain set as nil in before @@ -793,7 +660,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do expect(v).to eq(false) end - it 'will return true if url with http(s) protocol is passed with a domain that watches the url domain' do + it 'will return true if url with http(s) protocol is passed with a domain that matches the url domain' do host = random_string uri = "http://#{host}/#{random_string}" cookie.domain = host From 1b02344b55c266ae137021eb1158008946f24d33 Mon Sep 17 00:00:00 2001 From: A Galway Date: Tue, 20 Apr 2021 15:02:41 +0100 Subject: [PATCH 4/5] consider vhost & expand tests --- .../exploit/remote/http/http_cookie_jar.rb | 7 +++- lib/msf/core/exploit/remote/http_client.rb | 2 +- .../remote/http/http_cookie_jar_spec.rb | 4 +- .../remote/remote/http/http_cookie_spec.rb | 40 +++++++++++++------ 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/http_cookie_jar.rb b/lib/msf/core/exploit/remote/http/http_cookie_jar.rb index 62c2823550..acec7dff15 100644 --- a/lib/msf/core/exploit/remote/http/http_cookie_jar.rb +++ b/lib/msf/core/exploit/remote/http/http_cookie_jar.rb @@ -51,8 +51,13 @@ module Msf end def parse(set_cookie_header, origin_url, options = nil) - parsed_cookies = HTTP::Cookie.parse(set_cookie_header, origin_url, options) + ::HTTP::Cookie.parse(set_cookie_header, origin_url, options) + end + + def parse_and_merge(set_cookie_header, origin_url, options = nil) + parsed_cookies = ::HTTP::Cookie.parse(set_cookie_header, origin_url, options) parsed_cookies.each { |c| add(Msf::Exploit::Remote::HTTP::HttpCookie.new(c)) } + parsed_cookies end end diff --git a/lib/msf/core/exploit/remote/http_client.rb b/lib/msf/core/exploit/remote/http_client.rb index 147aa433f6..c9b995898c 100644 --- a/lib/msf/core/exploit/remote/http_client.rb +++ b/lib/msf/core/exploit/remote/http_client.rb @@ -399,7 +399,7 @@ module Exploit::Remote::HttpClient return unless res if opts['keep_cookies'] && res.headers['Set-Cookie'].present? - cookie_jar.parse(res.headers['Set-Cookie'], "http#{ssl ? 's' : ''}://#{peer}") + cookie_jar.parse_and_merge(res.headers['Set-Cookie'], "http#{ssl ? 's' : ''}://#{vhost}:#{rport}") end res diff --git a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb index bfe7a7aba5..6f63e3bb72 100644 --- a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb +++ b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_jar_spec.rb @@ -20,10 +20,8 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookieJar do let(:cookie_jar) { described_class.new } - NOW = Time.local(2008, 9, 5, 10, 5, 30) - before(:each) do - Timecop.freeze(NOW) + Timecop.freeze(Time.local(2008, 9, 5, 10, 5, 30)) end after(:each) do diff --git a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb index 15e68058db..00fa77f42d 100644 --- a/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb +++ b/spec/lib/msf/core/exploit/remote/remote/http/http_cookie_spec.rb @@ -9,10 +9,8 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do let(:cookie) { described_class.new(random_string, random_string) } - NOW = Time.local(2008, 9, 5, 10, 5, 30) - before(:each) do - Timecop.freeze(NOW) + Timecop.freeze(Time.local(2008, 9, 5, 10, 5, 30)) end after(:each) do @@ -571,7 +569,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do max_age = 1 cookie.max_age = max_age - cookie.created_at = NOW - 5.seconds + cookie.created_at = Time.local(2008, 9, 5, 10, 5, 30) - 5.seconds expect(cookie.expired?).to eql(true) end @@ -580,7 +578,7 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do max_age = 10 cookie.max_age = max_age - cookie.created_at = NOW - 5.seconds + cookie.created_at = Time.local(2008, 9, 5, 10, 5, 30) - 5.seconds expect(cookie.expired?).to eql(false) end @@ -596,22 +594,38 @@ RSpec.describe Msf::Exploit::Remote::HTTP::HttpCookie do expect(cookie.expired?(Time.now)).to eql(false) end - it 'passed to expired? with expires set to valid Time value, will return True/False depending on if the passed Time value is after expires' do - expired = rand(0..1) == 1 - t = expired ? Faker::Time.forward(days: 1) : Faker::Time.backward(days: 1) + it 'before "cookie.expires" is passed to expired? will return false' do + t = Faker::Time.backward(days: 1) cookie.expires = Time.now - expect(cookie.expired?(t)).to eql(expired) + expect(cookie.expired?(t)).to eq(false) end - it 'passed to expired? with max_age set to a valid Integer, will return True/False depending on if created_at + max_age is after expires' do - expired = rand(0..1) == 1 - t = expired ? Faker::Time.forward(days: 1) : Faker::Time.backward(days: 1) + it 'after "cookie.expires" is passed to expired? will return true' do + t = Faker::Time.forward(days: 1) + + cookie.expires = Time.now + + expect(cookie.expired?(t)).to eql(true) + end + + it 'passed to expired?, will return true when cookie.created_at + max_age.seconds is before the passed value' do + t = Faker::Time.forward(days: 1) cookie.max_age = 1 + cookie.created_at = Time.now - expect(cookie.expired?(t)).to eql(expired) + expect(cookie.expired?(t)).to eq(true) + end + + it 'passed to expired?, will return false when created_at + seconds.max_age is after the passed value' do + t = Faker::Time.backward(days: 1) + + cookie.max_age = 1 + cookie.created_at = Time.now + + expect(cookie.expired?(t)).to eq(false) end end end From 04ff0f6bd7cdd42ca66b4e39003167ea7fe8fc65 Mon Sep 17 00:00:00 2001 From: Alan Foster Date: Thu, 29 Apr 2021 21:26:02 +0100 Subject: [PATCH 5/5] Update exchange ecp dlp policy to work with new cookie jar --- modules/exploits/windows/http/exchange_ecp_dlp_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/windows/http/exchange_ecp_dlp_policy.rb b/modules/exploits/windows/http/exchange_ecp_dlp_policy.rb index 1fd1861ac1..69312d5ac4 100644 --- a/modules/exploits/windows/http/exchange_ecp_dlp_policy.rb +++ b/modules/exploits/windows/http/exchange_ecp_dlp_policy.rb @@ -164,7 +164,7 @@ class MetasploitModule < Msf::Exploit::Remote fail_with(Failure::Unreachable, 'Failed to access OWA login page') end - unless res.code == 200 && cookie_jar.grep(/^cadata/).any? + unless res.code == 200 && cookie_jar.cookies.any? { |cookie| cookie.name.start_with?('cadata') } if res.body.include?('There are too many active sessions connected to this mailbox.') fail_with(Failure::NoAccess, 'Reached active session limit for mailbox') end