Land #14831, Add CookieJar support to http_client

This commit is contained in:
adfoster-r7 2021-04-30 14:08:04 +01:00 committed by GitHub
commit 6c6d7699ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1273 additions and 112 deletions

View File

@ -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)
@ -193,6 +196,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)

View File

@ -0,0 +1,175 @@
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 domain
@cookie.domain
end
def domain=(domain)
if domain.nil? || domain.is_a?(DomainName)
@cookie.domain = domain
else
@cookie.domain = domain.to_s
end
end
def accessed_at
@cookie.accessed_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
@cookie.created_at
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

View File

@ -0,0 +1,91 @@
# 3rd party gems
require 'http/cookie_jar/hash_store'
require 'http/cookie_jar'
require 'http/cookie'
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: HashStoreWithoutAutomaticExpiration
})
end
def add(cookie)
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
end
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}" unless cookie.is_a?(Msf::Exploit::Remote::HTTP::HttpCookie)
@cookie_jar.delete(cookie)
self
end
# Iterates over all cookies that are not expired in no particular
# order.
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
def empty?
@cookie_jar.empty?
end
def parse(set_cookie_header, origin_url, options = nil)
::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
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)
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|
yield cookie
end
end
end
end
self
end
end
end
end
end
end

View File

@ -91,7 +91,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 = Msf::Exploit::Remote::HTTP::HttpCookieJar.new
end
def deregister_http_client_options
@ -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.
@ -373,22 +372,34 @@ module Exploit::Remote::HttpClient
# Connects to the server, creates a request, sends the request,
# reads the response
#
# 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 +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)
if cookie_jar.any?
opts = { 'cookie' => cookie_jar.to_a.join(' ') }.merge(opts)
if opts.has_key?('cookie')
if opts['cookie'].is_a?(Msf::Exploit::Remote::HTTP::HttpCookieJar)
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 unless opts['expire_cookies'] == false
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?
# XXX: CGI::Cookie (get_cookies_parsed) is hella broken
cookie_jar.merge(res.get_cookies.split(' '))
cookie_jar.parse_and_merge(res.headers['Set-Cookie'], "http#{ssl ? 's' : ''}://#{vhost}:#{rport}")
end
res
@ -890,10 +901,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

View File

@ -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

View File

@ -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

View File

@ -57,19 +57,20 @@ 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 +90,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 +97,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 +113,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 +137,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 +158,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 +166,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 +195,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 +223,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 +243,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 <a href="http://.*#{create_issue_path}/(\d+)">redirected</a>}, 1]
issue.merge({
@ -267,7 +263,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 +292,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 +312,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 +321,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 +328,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 +538,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

View File

@ -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,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

View File

@ -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',

View File

@ -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

View File

@ -0,0 +1,247 @@
require 'spec_helper'
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 random_cookie
Msf::Exploit::Remote::HTTP::HttpCookie.new(
random_string,
random_string,
max_age: Faker::Number.within(range: 1..100),
path: '/' + random_string,
secure: Faker::Boolean.boolean,
httponly: Faker::Boolean.boolean,
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 = random_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 true when populated cookie_jar has been cleared' do
c = random_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 = []
Faker::Number.within(range: 1..10).times do
c = random_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
end
describe 'add' do
it 'unacceptable cookie without a domain will throw an 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
it 'unacceptable cookie without a path will throw an ArgumentError' do
c = Msf::Exploit::Remote::HTTP::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 = random_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 = random_cookie
c_dup = random_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).to match_array([c_dup])
end
it 'variable not a subclass of ::HttpCookie will raise TypeError' do
int = 1
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(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 = random_cookie
c_dup = random_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 = 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)
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 = Faker::Number.within(range: 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(random_cookie)
int = Faker::Number.within(range: 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
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 = [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.each { |c| cookie_jar.add(c) }
expired_cookies.each { |c| cookie_jar.add(c) }
cookie_jar.cleanup
expect(cookie_jar.cookies).to match_array(cookies)
end
it 'will remove expired cookies with expires value' do
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 = [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.each { |c| cookie_jar.add(c) }
expired_cookies.each { |c| cookie_jar.add(c) }
cookie_jar.cleanup
expect(cookie_jar.cookies).to match_array(cookies)
end
end
end

View File

@ -0,0 +1,696 @@
require 'spec_helper'
require 'faker'
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
let(:cookie) { described_class.new(random_string, random_string) }
before(:each) do
Timecop.freeze(Time.local(2008, 9, 5, 10, 5, 30))
end
after(:each) do
Timecop.return
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 do
cookie.name = n
end.to raise_error(ArgumentError)
end
end
describe 'nil' do
it 'assigned to name throws an ArgumentError' do
n = nil
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
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 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)]
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)
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 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)
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
clazz = Class.new(String) do
end
str = "/#{random_string(0)}"
my_str = clazz.new(str)
cookie.path = my_str
expect(cookie.path).to eql(str)
end
end
describe 'nil' do
it 'assigned to expired 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 '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 '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 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)
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 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)
end
end
end
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 to true when nil is assigned to max_age' do
max_age = nil
cookie.max_age = max_age
expect(cookie.session?).to eq(true)
end
it 'is set to false when an integer > 0 is assigned to max_age' do
max_age = Faker::Number.within(range: 1..100)
cookie.max_age = max_age
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
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? 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(true)
end
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.expires = t
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 = Time.local(2008, 9, 5, 10, 5, 30) - 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 = Time.local(2008, 9, 5, 10, 5, 30) - 5.seconds
expect(cookie.expired?).to eql(false)
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 '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 eq(false)
end
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 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
describe 'valid_for_uri?' do
it "will return false when domain hasn't been set" do
# domain set as nil in before
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
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 matches 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

View File

@ -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