CVE-2025-69197

Infinite Lives: Replaying 2FA in Pterodactyl Panel

Alon Barad
Alon Barad
Software Engineer

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."

  1. Credential Harvesting: Niko has already dumped Roman's password from a previous breach of a forum where Roman reused his credentials.
  2. 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."
  3. The Race: Roman types 882 194. The real server accepts it. Roman is in.
  4. 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.

  1. Add a migration to add totp_authenticated_at (timestamp) to your users table.
  2. Swap verifyKey for verifyKeyNewer in your controller logic.
  3. Ensure you are updating the timestamp on every successful login.

Until then, tell your admins to stop streaming their login screens on Discord. Seriously.

Fix Analysis (1)

Technical Appendix

CVSS Score
6.5/ 10
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
EPSS Probability
0.03%
Top 100% most exploited

Affected Systems

Pterodactyl Panel

Affected Versions Detail

Product
Affected Versions
Fixed Version
Pterodactyl Panel
Pterodactyl
<= 1.11.111.12.0
AttributeDetail
CWECWE-294: Authentication Bypass by Capture-replay
Attack VectorNetwork (AV:N)
CVSS v3.16.5 (Medium)
EPSS Score0.00027 (Low)
ImpactAccount Takeover / RCE
Affected Versions<= 1.11.11
Patch Commit032bf076d92bb2f929fa69c1bac1b89f26b8badf
CWE-294
Authentication Bypass by Capture-replay

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.

Vulnerability Timeline

Fix commit pushed to repository
2025-12-30
Vulnerability publicly disclosed
2026-01-06
CVE-2025-69197 Assigned
2026-01-06

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.