Infinite Lives: Replaying 2FA in Pterodactyl Panel
Jan 7, 2026·6 min read
Executive Summary (TL;DR)
Pterodactyl Panel failed to track used 2FA tokens. If an attacker has your password and intercepts your 2FA code (via phishing or shoulder surfing), they can reuse that same code within ~60 seconds to log in as you. Fixed in version 1.12.0 by forcing the server to remember the last used timestamp.
A critical logic flaw in Pterodactyl Panel allowing attackers to reuse Time-based One-Time Passwords (TOTP) within their validity window, effectively bypassing the 'One-Time' aspect of 2FA.
The Hook: Game Over, Insert Token
If you run a game server—be it Minecraft, Rust, or Ark—you probably know Pterodactyl Panel. It’s the de facto standard for open-source game server management. It’s slick, it’s Docker-based, and it holds the keys to the kingdom. If you compromise a Pterodactyl instance, you don't just get a user account; you usually get Remote Code Execution (RCE) via the game console wrappers or file managers. It is the crown jewel for anyone looking to build a botnet out of high-performance gaming rigs.
Because of its high value, admins lock it down. Strong passwords. IP allowlists. And, inevitably, Two-Factor Authentication (2FA) using TOTP (Time-based One-Time Passwords). We rely on TOTP as the final barrier—the moat around the castle. Even if a hacker dumps the database or keylogs the password, they can't generate that rotating 6-digit code on your phone. Right?
Well, CVE-2025-69197 proves that even the best moats are useless if the drawbridge operator is asleep. This vulnerability isn't a complex buffer overflow or a heap spray. It's a logic error so simple it hurts: the application checked if the code was correct, but it forgot to check if it had already been used. It turns out, "One-Time Password" was more of a suggestion than a rule.
The Flaw: Déjà Vu All Over Again
To understand the screw-up, we have to look at how TOTP actually works. RFC 6238 defines TOTP as a hash-based message authentication code (HMAC) where the message is the current time, floored to a 30-second interval (the "time step").
When you type 123456, the server calculates what the code should be right now. Since clocks drift and users are slow typists, servers usually accept codes from the current 30-second window, the previous window, and sometimes the next one. This creates a valid acceptance window of 60 to 90 seconds.
In a secure implementation, once a specific time step (say, the window from 12:00:00 to 12:00:30) is used to authenticate, the server should mark that specific timestamp as "consumed." If another request comes in for that same timestamp, it should be rejected immediately. This is replay protection 101.
Pterodactyl Panel didn't do this. It treated the verification as a stateless mathematical check: "Is 123456 a valid output for Secret Key K at Time T? Yes? Come on in!" The code completely ignored the state of the user session. It’s the digital equivalent of a movie theater ticket taker looking at the date on your ticket, seeing it's for today, and letting you walk in—without ripping the stub. You can just walk out the side door, hand the ticket to your friend, and they can walk right in too.
The Code: The Smoking Gun
Let's look at the vulnerable code in app/Http/Controllers/Auth/LoginCheckpointController.php. This controller handles the 2FA challenge after a user successfully provides a password.
The Vulnerable Implementation (<= 1.11.11):
// Stateless verification
if ($this->google2FA->verifyKey(
$decrypted,
(string) ($request->input('authentication_code') ?? ''),
config('pterodactyl.auth.2fa.window')
)) {
// Login successful
return $this->sendLoginResponse($request, $user);
}The method verifyKey from the pragmarx/google2fa library performs a pure mathematical validation. It returns true if the code matches any time step within the configured window. It does not touch the database.
The Fix (v1.12.0):
The patch changes the logic to track when the user last authenticated. A new column totp_authenticated_at was added to the users table.
// Calculate the last used time step (timestamp / 30)
$oldTimestamp = $user->totp_authenticated_at
? (int) floor($user->totp_authenticated_at->unix() / $this->google2FA->getKeyRegeneration())
: null;
// Use verifyKeyNewer to enforce strictly increasing timestamps
$verified = $this->google2FA->verifyKeyNewer(
$decrypted,
$request->input('authentication_code') ?? '',
$oldTimestamp,
config('pterodactyl.auth.2fa.window') ?? 1,
);
if ($verified !== false) {
// Update the timestamp so this window cannot be used again
$user->update(['totp_authenticated_at' => Carbon::now()]);
return $this->sendLoginResponse($request, $user);
}The function verifyKeyNewer is the hero here. It requires the current token's timestamp to be greater than $oldTimestamp. If you try to replay the same token, the timestamp will be identical, and the function returns false.
The Exploit: Double Dipping
So how do we weaponize this? This isn't a magic "bypass authentication" button. We need two things: the victim's password (which is surprisingly easy to get via credential stuffing or phishing) and a valid TOTP code.
The Scenario:
Imagine an attacker, let's call him "Niko," targeting a Minecraft server admin, "Roman."
- Credential Harvesting: Niko has already dumped Roman's password from a previous breach of a forum where Roman reused his credentials.
- The Intercept: Niko sends Roman a phishing link to a fake login page that proxies requests to the real Pterodactyl panel (an Evilginx2 style attack), or simply tricks Roman into screen-sharing on Discord while logging in to "fix a config issue."
- The Race: Roman types
882 194. The real server accepts it. Roman is in. - The Replay: 15 seconds later, Niko takes his script containing Roman's username/password and sends a POST request to the login checkpoint with the code
882 194.
Because the code is valid for a 60-second window (current 30s + previous 30s for drift), and the server hasn't "burned" that window, Niko gets a valid session cookie. He is now authenticated as Roman. The "One-Time" password effectively became a "Two-Time" password.
[!NOTE] Why this matters: In many Man-in-the-Middle (MitM) setups, the attacker tries to use the token instead of the user. Here, the attacker can use it alongside the user, which is much stealthier. The user gets in successfully and suspects nothing, while the attacker quietly creates a shadow session.
The Impact: Total Control
Once inside Pterodactyl, the game is over. Pterodactyl is designed to manage processes and files. This means an authenticated user with server access can:
- Read/Write Files: Upload a malicious plugin, a webshell, or a modified server binary.
- Execute Commands: Send commands directly to the server console. If the container is privileged or misconfigured, this leads to container escape.
- Exfiltrate Data: Download the entire world database, user lists, and whitelist configs.
While the CVSS score is a modest 6.5 (due to the complexity of needing credentials and a valid token), the criticality of the asset makes this a nightmare scenario. Game server panels are often run by teenagers or volunteers who reuse passwords and share screens casually—the exact behaviors that make this vulnerability highly exploitable in the wild.
The Fix: Burning the Bridge
The remediation is straightforward: Upgrade to Pterodactyl Panel v1.12.0 immediately. The patch introduces the necessary database schema changes and logic updates to enforce stateful TOTP verification.
If you cannot upgrade immediately (perhaps you are running a heavily modified legacy fork), you must patch LoginCheckpointController.php manually.
- Add a migration to add
totp_authenticated_at(timestamp) to youruserstable. - Swap
verifyKeyforverifyKeyNewerin your controller logic. - Ensure you are updating the timestamp on every successful login.
Until then, tell your admins to stop streaming their login screens on Discord. Seriously.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Pterodactyl Panel Pterodactyl | <= 1.11.11 | 1.12.0 |
| Attribute | Detail |
|---|---|
| CWE | CWE-294: Authentication Bypass by Capture-replay |
| Attack Vector | Network (AV:N) |
| CVSS v3.1 | 6.5 (Medium) |
| EPSS Score | 0.00027 (Low) |
| Impact | Account Takeover / RCE |
| Affected Versions | <= 1.11.11 |
| Patch Commit | 032bf076d92bb2f929fa69c1bac1b89f26b8badf |
MITRE ATT&CK Mapping
The product does not verify that a one-time password (OTP) or similar secret has not been used before, allowing an attacker to replay the secret to gain access.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.