Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

CookieJar cleanup #15165

Merged
merged 2 commits into from
May 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 109 additions & 21 deletions lib/msf/core/exploit/remote/http/http_cookie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,61 @@ module Msf
class Exploit
class Remote
module HTTP
# Acts as a wrapper for the 3rd party Cookie (http-cookie)
# This class is a representation of a Http Cookie with some built in convenience methods.
# Acts as a wrapper for the +HTTP::Cookie+ (https://www.rubydoc.info/gems/http-cookie/1.0.3/HTTP/Cookie) class .
class HttpCookie
include Comparable

# Returns a new +HttpCookie+.
#
# Name can either be a string or an instance of +::HTTP::Cookie+.
# - If an instance of +::HTTP::Cookie+, all future method calls will return values and act on values of said
# +::HTTP::Cookie+.
# - If a +String+, the name of the cookie is set to the passed +name+.
# - If a +String+ is passed as a +name+, the cookie is set as a session cookie.
#
# Value can be a +String+ or +nil+.
# - If a +String+, the value of the cookie is set as the passed +cookie+.
# - If +nil+, the value of the cookie is set as an empty +String+ +''+ and the cookie is set to expire at
# +UNIX_EPOCH+
#
# +attr_hash+ can be used to set the values of +domain+, +path+, +max_age+, +expires+, +secure+, +httponly+,
# +accessed_at+, +created_at+.
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
elsif value
@cookie = ::HTTP::Cookie.new(name, value)
else
@cookie = ::HTTP::Cookie.new(name)
end

attr_hash.each_pair do |k, v|
if respond_to?("#{k}=".to_sym)
send("#{k}=".to_sym, v)
end
end
end

# Returns the name of cookie of type +String+.
def name
@cookie.name
end

# Sets the cookie name.
def name=(name)
@cookie.name = name.to_s
end

# Returns the value of cookie of type +String+.
def value
@cookie.value
end

# Sets the cookie value.
#
# Passed +value+ must be +nil+, an instance of +String+, or an object that can be converted successfully to
# a +String+ with +to_s+.
def value=(value)
if value.nil? || value.is_a?(String)
@cookie.value = value
Expand All @@ -40,10 +67,17 @@ def value=(value)
end
end

# Returns the value of max_age.
#
# max_age is the number of seconds until a cookie expires.
def max_age
@cookie.max_age
end

# Sets the cookie max_age of type +Integer+.
#
# Passed +max_age+ must be +nil+, an +Integer+, or an object that can be converted successfully to an
# +Integer+ with +Integer(max_age)+.
def max_age=(max_age)
if max_age.nil? || max_age.is_a?(Integer)
@cookie.max_age = max_age
Expand All @@ -52,10 +86,17 @@ def max_age=(max_age)
end
end

# Returns the value of cookie expires of type +Time+.
#
# expires is the date and time at which a cookie expires.
def expires
@cookie.expires
end

# Sets the cookie expires value.
#
# Passed +expires+ must be +nil+, an instance of +Time+, or an object that can be converted successfully to
# an +Time+ with +Time.parse(expires)+.
def expires=(expires)
if expires.nil? || expires.is_a?(Time)
@cookie.expires = expires
Expand All @@ -65,14 +106,17 @@ def expires=(expires)
end
end

def expired?(time = Time.now)
@cookie.expired?(time)
end

# Returns the cookie path of type +String+.
#
# path is the URL for which the cookie is valid.
def path
@cookie.path
end

# Sets the cookie path.
#
# Passed +path+ must be +nil+, an instance of +String+, or an object that can be converted successfully to a
# +String+ with +to_s+.
def path=(path)
if path.nil? || path.is_a?(String)
@cookie.path = path
Expand All @@ -81,38 +125,70 @@ def path=(path)
end
end

# Returns the cookie secure value of type +Boolean+.
#
# secure is a boolean that indicates if the cookie should be limited to the scope of secure channels as
# defined by the user agent.
def secure
@cookie.secure
end

# Sets the cookie secure value.
#
# Passed +secure+ is converted to a Boolean with +!!secure+ and set.
def secure=(secure)
@cookie.secure = !!secure
end

# Returns the cookie httponly value of type +Boolean+.
#
# httponly is a +Boolean+ that indicates if client-side scripts should be prevented from accessing data.
def httponly
@cookie.httponly
end

# Sets the cookie httponly value.
#
# Passed +httponly+ is converted to a Boolean with +!!httponly+ and set.
def httponly=(httponly)
@cookie.httponly = !!httponly
end

# Returns the cookie domain of type +String+.
#
# If omitted, defaults to the host of the current document URL, not including subdomains. Leading dots in
# domain names (.example.com) are ignored. Multiple host/domain values are not allowed, but if a domain is
# specified, then subdomains are always included.
def domain
@cookie.domain
if @cookie.domain.nil?
nil
else
@cookie.domain.to_s
end
end

# Sets the cookie domain.
#
# Passed +domain+ must be +nil+, an instance of +String+, or an object that can be converted successfully to
# an +String+ with +to_s+.
def domain=(domain)
if domain.nil? || domain.is_a?(DomainName)
if domain.nil?
@cookie.domain = domain
else
@cookie.domain = domain.to_s
end
end

# Returns the cookie accessed_at value of type +Time+. accessed_at indicates when a cookie was last interacted
# with.
def accessed_at
@cookie.accessed_at
end

# Sets the cookie accessed_at time.
#
# Passed +time+ must be +nil+, an instance of +Time+, or an object that can be converted successfully to an
# +Time+ with +Time.parse+.
def accessed_at=(time)
if time.nil? || time.is_a?(Time)
@cookie.accessed_at = time
Expand All @@ -121,10 +197,15 @@ def accessed_at=(time)
end
end

# Returns the cookie created_at value of type +Time+. created_at indicates when a cookie was created.
def created_at
@cookie.created_at
end

# Sets the cookie accessed_at time.
#
# Passed +time+ must be +nil+, an instance of +Time+, or an object that can be converted successfully to an
# +Time+ with +Time.parse+.
def created_at=(time)
if time.nil? || time.is_a?(Time)
@cookie.created_at = time
Expand All @@ -133,25 +214,39 @@ def created_at=(time)
end
end

# Returns a string representation of the cookie for use in a cookie header.
# Comes in format "#{name}=#{value}".
def cookie_value
@cookie.cookie_value
end
alias to_s cookie_value

# Returns a boolean indicating if the cookie will have expired by the date and time represented by +time+.
# +time+ defaults to +Time.now+, so the method can return a different value after enough calls.
def expired?(time = Time.now)
@cookie.expired?(time)
end

# Returns a boolean indicating if the cookie is a Session Cookie.
def session?
@cookie.session?
end

# Tests if it is OK to accept this cookie. If either domain or path is missing an ArgumentError is raised.
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.
# Returns a boolean indicating if the cookie can be sent to the passed +uri+.
# Raises an ArgumentError if domain is nil (unset).
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`.
# Tests if it is OK to accept this cookie if it is sent from the passed +uri+.
def acceptable_from_uri?(uri)
return false if uri.nil?

Expand All @@ -161,13 +256,6 @@ def acceptable_from_uri?(uri)
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
Expand Down
50 changes: 38 additions & 12 deletions lib/msf/core/exploit/remote/http/http_cookie_jar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,39 @@
require 'http/cookie_jar'
require 'http/cookie'

# This class is a collection of Http Cookies with some built in convenience methods.
# Acts as a wrapper for the +::HTTP::CookieJar+ (https://www.rubydoc.info/gems/http-cookie/1.0.2/HTTP/CookieJar) class.

module Msf
class Exploit
class Remote
module HTTP
# Acts as a wrapper for the 3rd party CookieJar (http-cookie)
class HttpCookieJar

# Returns a new instance of +HttpCookieJar+.
def initialize
@cookie_jar = ::HTTP::CookieJar.new({
store: HashStoreWithoutAutomaticExpiration
})
end

# Adds +cookie+ to the jar.
#
# +cookie+ must be an instance or subclass of +Msf::Exploit::Remote::HTTP::HttpCookie+, or a `TypeError`
# will be raised.
#
# Returns +self+.
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

# Will remove any cookie from the jar that has the same +name+, +domain+ and +path+ as the passed +cookie+.
#
# Returns +self+.
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)
Expand All @@ -30,46 +44,58 @@ def delete(cookie)
self
end

# Iterates over all cookies that are not expired in no particular
# order.
# Returns an unordered array of all cookies stored in the jar.
def cookies
@cookie_jar.cookies
end

# Will remove all cookies from the jar.
#
# Returns +nil+.
def clear
@cookie_jar.clear
self
end

# Removes expired cookies and returns self. If `session` is true,
# all session cookies are removed as well.
# Will remove all expired cookies. If +expire_all+ is set as true, all session cookies are removed as well.
#
# Returns +self+.
def cleanup(expire_all = false)
@cookie_jar.cleanup(expire_all)
self
end

# Returns +true+ if the jar contains no cookies, else +false+.
def empty?
@cookie_jar.empty?
end

# Parses a Set-Cookie header value +set_cookie_header+ assuming that it is sent from a source URI/URL
# +origin_url+, and returns an array of +::HTTP::Cookie+ (https://www.rubydoc.info/gems/http-cookie/1.0.3/HTTP/Cookie)
# objects. Parts (separated by commas) that are malformed or considered unacceptable are silently ignored.
def parse(set_cookie_header, origin_url, options = nil)
::HTTP::Cookie.parse(set_cookie_header, origin_url, options)
end

# Same as +parse+, but each +::HTTP::Cookie+ (https://www.rubydoc.info/gems/http-cookie/1.0.3/HTTP/Cookie) is
# converted to +HttpCookie+ (https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/exploit/remote/http/http_cookie.rb)
# and added to the jar.
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

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

Expand Down
Loading