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…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…