GHSA-5W8W-26CH-V5CW

Vulnerability from github – Published: 2026-05-05 19:08 – Updated: 2026-05-13 14:19
VLAI
Summary
AVideo: Password Hash Leak in MobileManager OAuth Redirect URL Enables Account Takeover
Details

Summary

plugin/MobileManager/oauth2.php completes an OAuth login by sending an HTTP 302 Location: oauth2Success.php?user=<email>&pass=<HASH> where <HASH> is the victim's stored password hash (md5(hash("whirlpool", sha1(password)))) read directly from the users table. AVideo's own login endpoint (objects/login.json.php) accepts an encodedPass=1 flag that bypasses hashing and performs a direct string comparison between the supplied value and the stored hash. Anyone who captures the redirect URL — via server logs, referrer leakage, or browser history — therefore obtains a credential equivalent to the plaintext password and can fully take over the account, including admin accounts.

Details

Sink: hash inlined in a GET redirect

plugin/MobileManager/oauth2.php:98-102:

$pass = rand();
$users_id = User::createUserIfNotExists($user, $pass, $name, $email, $photoURL);
$adapter->disconnect();
$userObject = new User($users_id);
header("Location: oauth2Success.php?user=" . $userObject->getUser() . "&pass=" . $userObject->getPassword());

$userObject->getPassword() returns the raw database column (objects/user.php:159-162):

public function getPassword()
{
    return strip_tags($this->password);
}

The returned value is the stored password hash for the account (existing or freshly-created). It is transported to the browser as a query-string parameter in the Location: header, so it is written to:

  • Web-server access logs (combined / main log formats record the full request line including query string).
  • Upstream proxy / CDN / WAF logs.
  • Any error monitoring / APM that captures request URLs (Sentry, Datadog, New Relic defaults).
  • The victim's browser history (persistent local artifact).
  • The Referer header on subsequent navigation from the rendered oauth2Success.php page if the page or its assets load any external origin and the browser's Referrer-Policy is not strict.

Hash equals plaintext for login

objects/login.json.php:182-209:

if (!empty($_GET['user'])) {
    $_POST['user'] = $_GET['user'];
}
if (!empty($_GET['pass'])) {
    $_POST['pass'] = $_GET['pass'];
}
if (!empty($_GET['encodedPass'])) {
    $_POST['encodedPass'] = $_GET['encodedPass'];
}
...
$user = new User(0, $_POST['user'], $_POST['pass']);
...
$resp = $user->login(false, @$_POST['encodedPass']);

objects/user.php:1272-1279 passes $encodedPass to find():

if (strtolower($encodedPass) === 'false') {
    $encodedPass = false;
}
...
$user = $this->find($this->user, $this->password, true, $encodedPass);

objects/user.php:1785-1794:

if ($pass !== false) {
    if (!encryptPasswordVerify($pass, $result['password'], $encodedPass)) {
        ...
        return false;
    }
}

objects/functions.php:2312-2331:

function encryptPasswordVerify(#[\SensitiveParameter] $password, $hash, $encodedPass = false)
{
    global $advancedCustom, $global;
    if (!$encodedPass || $encodedPass === 'false') {
        $passwordSalted  = encryptPassword($password);
        $passwordUnSalted = encryptPassword($password, true);
    } else {
        $passwordSalted  = $password;   // <- direct use, no hashing
        $passwordUnSalted = $password;
    }
    $isValid = $passwordSalted === $hash || $passwordUnSalted === $hash;
    ...
}

When encodedPass is truthy, the supplied value is compared as-is against the stored hash. The captured redirect parameter pass=<HASH> is therefore a valid login credential when replayed with encodedPass=1.

Compounding factors

  • The redirect is a raw Location: (GET), not a POST — the secret is placed in a URL which is by definition non-confidential transport.
  • No CSRF token, no state parameter tied to the session, and no single-use token is used on /plugin/MobileManager/oauth2.php.
  • login.json.php does not require a CSRF token or captcha on the first attempt (checkLoginAttempts() at objects/user.php:1282 only rate-limits after failures, and the attacker succeeds on the first try).
  • By contrast, the non-plugin flow in objects/login.json.php:144-145 already sets session state server-side ($userObject->login(true)), demonstrating the project already has a safer pattern available.

PoC

Prerequisites: MobileManager plugin enabled and at least one supported login provider (e.g. LoginGoogle) configured with valid keys — both are common production settings for this product.

  1. Victim initiates the mobile OAuth flow:

GET /plugin/MobileManager/oauth2.php?type=Google

  1. After the victim authorizes at the provider, the server sends:

HTTP/1.1 302 Found Location: oauth2Success.php?user=victim%40example.com&pass=9d7ab4...stored-hash...

This request-line — including the password hash — is written to the web server's access log (default combined format) and to any upstream proxy/CDN log. It also appears in the victim's browser history.

  1. Attacker obtains <HASH> from any of those channels.

  2. Attacker logs in as the victim without knowing the plaintext password:

curl -i -c cookies.txt \ 'https://target.example.com/objects/login.json.php?user=victim@example.com&pass=<HASH>&encodedPass=1'

Expected response: 200 OK with JSON containing id, user, PHPSESSID, isAdmin, email, and a Set-Cookie: PHPSESSID=... that grants full account access. The attacker can now browse, upload, modify the victim's channel, or — if the victim is an admin — access /mvideos and all admin endpoints.

Impact

  • Full account takeover of any user who has ever logged in through the MobileManager OAuth endpoint.
  • If the victim is an administrator, the attacker gains administrative control of the AVideo instance (user management, plugin config, site-wide content).
  • The exposed hash works indefinitely: it remains valid for as long as the victim does not change their password, so a one-time log/history/referrer capture yields a persistent credential.
  • Passes silently — from the application's perspective, the attacker is just a legitimate login with encodedPass=1 (a flag the product itself uses for mobile-app "remember me" flows).

Recommended Fix

  1. Never place the password hash (or any credential-equivalent material) in a URL. In plugin/MobileManager/oauth2.php, mirror what objects/login.json.php:143-146 already does for the web flow — establish the session server-side and redirect to a URL with no credentials:

php $userObject = new User(0, $user, $pass); $userObject->login(true); // server-side session header("Location: oauth2Success.php");

  1. Additionally, remove or hard-restrict the encodedPass branch in objects/functions.php:2319-2329. If a "hash-equivalent" credential must exist for the mobile app, replace it with a short-lived, single-use, server-issued bearer token bound to the session, rather than the persistent database hash.

  2. Add a state parameter and CSRF protection on /plugin/MobileManager/oauth2.php so the redirect cannot be initiated from a third-party origin.

  3. For defense-in-depth, strip query strings containing pass= from access-log formats and ensure oauth2Success.php sets Referrer-Policy: no-referrer while it is being deprecated.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "wwbn/avideo"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "29.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-43875"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-598"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-05T19:08:45Z",
    "nvd_published_at": "2026-05-11T22:22:11Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\n`plugin/MobileManager/oauth2.php` completes an OAuth login by sending an HTTP 302 `Location: oauth2Success.php?user=\u003cemail\u003e\u0026pass=\u003cHASH\u003e` where `\u003cHASH\u003e` is the victim\u0027s stored password hash (`md5(hash(\"whirlpool\", sha1(password)))`) read directly from the `users` table. AVideo\u0027s own login endpoint (`objects/login.json.php`) accepts an `encodedPass=1` flag that bypasses hashing and performs a direct string comparison between the supplied value and the stored hash. Anyone who captures the redirect URL \u2014 via server logs, referrer leakage, or browser history \u2014 therefore obtains a credential equivalent to the plaintext password and can fully take over the account, including admin accounts.\n\n## Details\n\n### Sink: hash inlined in a GET redirect\n\n`plugin/MobileManager/oauth2.php:98-102`:\n\n```php\n$pass = rand();\n$users_id = User::createUserIfNotExists($user, $pass, $name, $email, $photoURL);\n$adapter-\u003edisconnect();\n$userObject = new User($users_id);\nheader(\"Location: oauth2Success.php?user=\" . $userObject-\u003egetUser() . \"\u0026pass=\" . $userObject-\u003egetPassword());\n```\n\n`$userObject-\u003egetPassword()` returns the raw database column (`objects/user.php:159-162`):\n\n```php\npublic function getPassword()\n{\n    return strip_tags($this-\u003epassword);\n}\n```\n\nThe returned value is the stored password hash for the account (existing or freshly-created). It is transported to the browser as a query-string parameter in the `Location:` header, so it is written to:\n\n* Web-server access logs (`combined` / `main` log formats record the full request line including query string).\n* Upstream proxy / CDN / WAF logs.\n* Any error monitoring / APM that captures request URLs (Sentry, Datadog, New Relic defaults).\n* The victim\u0027s browser history (persistent local artifact).\n* The `Referer` header on subsequent navigation from the rendered `oauth2Success.php` page if the page or its assets load any external origin and the browser\u0027s `Referrer-Policy` is not strict.\n\n### Hash equals plaintext for login\n\n`objects/login.json.php:182-209`:\n\n```php\nif (!empty($_GET[\u0027user\u0027])) {\n    $_POST[\u0027user\u0027] = $_GET[\u0027user\u0027];\n}\nif (!empty($_GET[\u0027pass\u0027])) {\n    $_POST[\u0027pass\u0027] = $_GET[\u0027pass\u0027];\n}\nif (!empty($_GET[\u0027encodedPass\u0027])) {\n    $_POST[\u0027encodedPass\u0027] = $_GET[\u0027encodedPass\u0027];\n}\n...\n$user = new User(0, $_POST[\u0027user\u0027], $_POST[\u0027pass\u0027]);\n...\n$resp = $user-\u003elogin(false, @$_POST[\u0027encodedPass\u0027]);\n```\n\n`objects/user.php:1272-1279` passes `$encodedPass` to `find()`:\n\n```php\nif (strtolower($encodedPass) === \u0027false\u0027) {\n    $encodedPass = false;\n}\n...\n$user = $this-\u003efind($this-\u003euser, $this-\u003epassword, true, $encodedPass);\n```\n\n`objects/user.php:1785-1794`:\n\n```php\nif ($pass !== false) {\n    if (!encryptPasswordVerify($pass, $result[\u0027password\u0027], $encodedPass)) {\n        ...\n        return false;\n    }\n}\n```\n\n`objects/functions.php:2312-2331`:\n\n```php\nfunction encryptPasswordVerify(#[\\SensitiveParameter] $password, $hash, $encodedPass = false)\n{\n    global $advancedCustom, $global;\n    if (!$encodedPass || $encodedPass === \u0027false\u0027) {\n        $passwordSalted  = encryptPassword($password);\n        $passwordUnSalted = encryptPassword($password, true);\n    } else {\n        $passwordSalted  = $password;   // \u003c- direct use, no hashing\n        $passwordUnSalted = $password;\n    }\n    $isValid = $passwordSalted === $hash || $passwordUnSalted === $hash;\n    ...\n}\n```\n\nWhen `encodedPass` is truthy, the supplied value is compared as-is against the stored hash. The captured redirect parameter `pass=\u003cHASH\u003e` is therefore a valid login credential when replayed with `encodedPass=1`.\n\n### Compounding factors\n\n* The redirect is a raw `Location:` (GET), not a POST \u2014 the secret is placed in a URL which is by definition non-confidential transport.\n* No CSRF token, no `state` parameter tied to the session, and no single-use token is used on `/plugin/MobileManager/oauth2.php`.\n* `login.json.php` does not require a CSRF token or captcha on the first attempt (`checkLoginAttempts()` at `objects/user.php:1282` only rate-limits after failures, and the attacker succeeds on the first try).\n* By contrast, the non-plugin flow in `objects/login.json.php:144-145` already sets session state server-side (`$userObject-\u003elogin(true)`), demonstrating the project already has a safer pattern available.\n\n## PoC\n\nPrerequisites: `MobileManager` plugin enabled and at least one supported login provider (e.g. `LoginGoogle`) configured with valid keys \u2014 both are common production settings for this product.\n\n1. Victim initiates the mobile OAuth flow:\n\n   ```\n   GET /plugin/MobileManager/oauth2.php?type=Google\n   ```\n\n2. After the victim authorizes at the provider, the server sends:\n\n   ```\n   HTTP/1.1 302 Found\n   Location: oauth2Success.php?user=victim%40example.com\u0026pass=9d7ab4...stored-hash...\n   ```\n\n   This request-line \u2014 including the password hash \u2014 is written to the web server\u0027s access log (default `combined` format) and to any upstream proxy/CDN log. It also appears in the victim\u0027s browser history.\n\n3. Attacker obtains `\u003cHASH\u003e` from any of those channels.\n\n4. Attacker logs in as the victim without knowing the plaintext password:\n\n   ```\n   curl -i -c cookies.txt \\\n     \u0027https://target.example.com/objects/login.json.php?user=victim@example.com\u0026pass=\u003cHASH\u003e\u0026encodedPass=1\u0027\n   ```\n\n   Expected response: `200 OK` with JSON containing `id`, `user`, `PHPSESSID`, `isAdmin`, `email`, and a `Set-Cookie: PHPSESSID=...` that grants full account access. The attacker can now browse, upload, modify the victim\u0027s channel, or \u2014 if the victim is an admin \u2014 access `/mvideos` and all admin endpoints.\n\n## Impact\n\n* Full account takeover of any user who has ever logged in through the MobileManager OAuth endpoint.\n* If the victim is an administrator, the attacker gains administrative control of the AVideo instance (user management, plugin config, site-wide content).\n* The exposed hash works indefinitely: it remains valid for as long as the victim does not change their password, so a one-time log/history/referrer capture yields a persistent credential.\n* Passes silently \u2014 from the application\u0027s perspective, the attacker is just a legitimate login with `encodedPass=1` (a flag the product itself uses for mobile-app \"remember me\" flows).\n\n## Recommended Fix\n\n1. Never place the password hash (or any credential-equivalent material) in a URL. In `plugin/MobileManager/oauth2.php`, mirror what `objects/login.json.php:143-146` already does for the web flow \u2014 establish the session server-side and redirect to a URL with no credentials:\n\n   ```php\n   $userObject = new User(0, $user, $pass);\n   $userObject-\u003elogin(true);   // server-side session\n   header(\"Location: oauth2Success.php\");\n   ```\n\n2. Additionally, remove or hard-restrict the `encodedPass` branch in `objects/functions.php:2319-2329`. If a \"hash-equivalent\" credential must exist for the mobile app, replace it with a short-lived, single-use, server-issued bearer token bound to the session, rather than the persistent database hash.\n\n3. Add a `state` parameter and CSRF protection on `/plugin/MobileManager/oauth2.php` so the redirect cannot be initiated from a third-party origin.\n\n4. For defense-in-depth, strip query strings containing `pass=` from access-log formats and ensure `oauth2Success.php` sets `Referrer-Policy: no-referrer` while it is being deprecated.",
  "id": "GHSA-5w8w-26ch-v5cw",
  "modified": "2026-05-13T14:19:29Z",
  "published": "2026-05-05T19:08:45Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-5w8w-26ch-v5cw"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-43875"
    },
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/commit/977cd6930a97571a26da4239e25c8096dd4ecbc1"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/WWBN/AVideo"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "AVideo: Password Hash Leak in MobileManager OAuth Redirect URL Enables Account Takeover"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…