diff options
Diffstat (limited to 'examples')
-rw-r--r-- | examples/README | 30 | ||||
-rw-r--r-- | examples/add_header.py | 2 | ||||
-rw-r--r-- | examples/change_upstream_proxy.py | 24 | ||||
-rw-r--r-- | examples/custom_contentviews.py | 68 | ||||
-rw-r--r-- | examples/dns_spoofing.py | 50 | ||||
-rw-r--r-- | examples/dup_and_replay.py | 4 | ||||
-rw-r--r-- | examples/filt.py | 16 | ||||
-rw-r--r-- | examples/flowbasic | 44 | ||||
-rw-r--r-- | examples/flowwriter.py | 20 | ||||
-rw-r--r-- | examples/har_extractor.py | 253 | ||||
-rw-r--r-- | examples/iframe_injector.py | 27 | ||||
-rw-r--r-- | examples/mitmproxywrapper.py | 166 | ||||
-rw-r--r-- | examples/modify_form.py | 5 | ||||
-rw-r--r-- | examples/modify_querystring.py | 6 | ||||
-rw-r--r-- | examples/modify_response_body.py | 18 | ||||
-rw-r--r-- | examples/nonblocking.py | 9 | ||||
-rw-r--r-- | examples/proxapp.py | 24 | ||||
-rw-r--r-- | examples/read_dumpfile | 20 | ||||
-rw-r--r-- | examples/redirect_requests.py | 22 | ||||
-rw-r--r-- | examples/sslstrip.py | 40 | ||||
-rw-r--r-- | examples/stickycookies | 42 | ||||
-rw-r--r-- | examples/stream.py | 5 | ||||
-rw-r--r-- | examples/stream_modify.py | 20 | ||||
-rw-r--r-- | examples/stub.py | 79 | ||||
-rw-r--r-- | examples/tcp_message.py | 24 | ||||
-rw-r--r-- | examples/tls_passthrough.py | 136 | ||||
-rw-r--r-- | examples/upsidedownternet.py | 17 |
27 files changed, 1171 insertions, 0 deletions
diff --git a/examples/README b/examples/README new file mode 100644 index 00000000..cf5c4d7d --- /dev/null +++ b/examples/README @@ -0,0 +1,30 @@ +Some inline scripts may require additional dependencies, which can be installed using +`pip install mitmproxy[examples]`. + + +# inline script examples +add_header.py Simple script that just adds a header to every request. +change_upstream_proxy.py Dynamically change the upstream proxy +dns_spoofing.py Use mitmproxy in a DNS spoofing scenario. +dup_and_replay.py Duplicates each request, changes it, and then replays the modified request. +filt.py Use mitmproxy's filter expressions in your script. +flowwriter.py Only write selected flows into a mitmproxy dumpfile. +iframe_injector.py Inject configurable iframe into pages. +modify_form.py Modify all form submissions to add a parameter. +modify_querystring.py Modify all query strings to add a parameters. +modify_response_body.py Replace arbitrary strings in all responses +nonblocking.py Demonstrate parallel processing with a blocking script. +proxapp.py How to embed a WSGI app in a mitmproxy server +redirect_requests.py Redirect requests or directly reply to them. +stub.py Script stub with a method definition for every event. +upsidedownternet.py Rewrites traffic to turn images upside down. + + +# mitmproxy examples +flowbasic Basic use of mitmproxy as a library. +stickycookies An example of writing a custom proxy with mitmproxy. + + +# misc +read_dumpfile Read a dumpfile generated by mitmproxy. +mitmproxywrapper.py Bracket mitmproxy run with proxy enable/disable on OS X diff --git a/examples/add_header.py b/examples/add_header.py new file mode 100644 index 00000000..cf1b53cc --- /dev/null +++ b/examples/add_header.py @@ -0,0 +1,2 @@ +def response(context, flow): + flow.response.headers["newheader"] = "foo" diff --git a/examples/change_upstream_proxy.py b/examples/change_upstream_proxy.py new file mode 100644 index 00000000..9c454897 --- /dev/null +++ b/examples/change_upstream_proxy.py @@ -0,0 +1,24 @@ +# This scripts demonstrates how mitmproxy can switch to a second/different upstream proxy +# in upstream proxy mode. +# +# Usage: mitmdump -U http://default-upstream-proxy.local:8080/ -s change_upstream_proxy.py +# +# If you want to change the target server, you should modify flow.request.host and flow.request.port + + +def proxy_address(flow): + # Poor man's loadbalancing: route every second domain through the alternative proxy. + if hash(flow.request.host) % 2 == 1: + return ("localhost", 8082) + else: + return ("localhost", 8081) + + +def request(context, flow): + if flow.request.method == "CONNECT": + # If the decision is done by domain, one could also modify the server address here. + # We do it after CONNECT here to have the request data available as well. + return + address = proxy_address(flow) + if flow.live: + flow.live.change_upstream_proxy_server(address)
\ No newline at end of file diff --git a/examples/custom_contentviews.py b/examples/custom_contentviews.py new file mode 100644 index 00000000..776ba99d --- /dev/null +++ b/examples/custom_contentviews.py @@ -0,0 +1,68 @@ +import string +import lxml.html +import lxml.etree +from mitmproxy import utils, contentviews + + +class ViewPigLatin(contentviews.View): + name = "pig_latin_HTML" + prompt = ("pig latin HTML", "l") + content_types = ["text/html"] + + def __call__(self, data, **metadata): + if utils.isXML(data): + parser = lxml.etree.HTMLParser( + strip_cdata=True, + remove_blank_text=True + ) + d = lxml.html.fromstring(data, parser=parser) + docinfo = d.getroottree().docinfo + + def piglify(src): + words = string.split(src) + ret = '' + for word in words: + idx = -1 + while word[idx] in string.punctuation and (idx * -1) != len(word): idx -= 1 + if word[0].lower() in 'aeiou': + if idx == -1: + ret += word[0:] + "hay" + else: + ret += word[0:len(word) + idx + 1] + "hay" + word[idx + 1:] + else: + if idx == -1: + ret += word[1:] + word[0] + "ay" + else: + ret += word[1:len(word) + idx + 1] + word[0] + "ay" + word[idx + 1:] + ret += ' ' + return ret.strip() + + def recurse(root): + if hasattr(root, 'text') and root.text: + root.text = piglify(root.text) + if hasattr(root, 'tail') and root.tail: + root.tail = piglify(root.tail) + + if len(root): + for child in root: + recurse(child) + + recurse(d) + + s = lxml.etree.tostring( + d, + pretty_print=True, + doctype=docinfo.doctype + ) + return "HTML", contentviews.format_text(s) + + +pig_view = ViewPigLatin() + + +def start(context, argv): + context.add_contentview(pig_view) + + +def stop(context): + context.remove_contentview(pig_view) diff --git a/examples/dns_spoofing.py b/examples/dns_spoofing.py new file mode 100644 index 00000000..7eb79695 --- /dev/null +++ b/examples/dns_spoofing.py @@ -0,0 +1,50 @@ +""" +This inline scripts makes it possible to use mitmproxy in scenarios where IP spoofing has been used to redirect +connections to mitmproxy. The way this works is that we rely on either the TLS Server Name Indication (SNI) or the +Host header of the HTTP request. +Of course, this is not foolproof - if an HTTPS connection comes without SNI, we don't +know the actual target and cannot construct a certificate that looks valid. +Similarly, if there's no Host header or a spoofed Host header, we're out of luck as well. +Using transparent mode is the better option most of the time. + +Usage: + mitmproxy + -p 443 + -s dns_spoofing.py + # Used as the target location if neither SNI nor host header are present. + -R http://example.com/ + mitmdump + -p 80 + -R http://localhost:443/ + + (Setting up a single proxy instance and using iptables to redirect to it + works as well) +""" +import re + + +# This regex extracts splits the host header into host and port. +# Handles the edge case of IPv6 addresses containing colons. +# https://bugzilla.mozilla.org/show_bug.cgi?id=45891 +parse_host_header = re.compile(r"^(?P<host>[^:]+|\[.+\])(?::(?P<port>\d+))?$") + + +def request(context, flow): + if flow.client_conn.ssl_established: + flow.request.scheme = "https" + sni = flow.client_conn.connection.get_servername() + port = 443 + else: + flow.request.scheme = "http" + sni = None + port = 80 + + host_header = flow.request.pretty_host + m = parse_host_header.match(host_header) + if m: + host_header = m.group("host").strip("[]") + if m.group("port"): + port = int(m.group("port")) + + flow.request.host = sni or host_header + flow.request.port = port
\ No newline at end of file diff --git a/examples/dup_and_replay.py b/examples/dup_and_replay.py new file mode 100644 index 00000000..9ba91d3b --- /dev/null +++ b/examples/dup_and_replay.py @@ -0,0 +1,4 @@ +def request(context, flow): + f = context.duplicate_flow(flow) + f.request.path = "/changed" + context.replay_request(f) diff --git a/examples/filt.py b/examples/filt.py new file mode 100644 index 00000000..f99b675c --- /dev/null +++ b/examples/filt.py @@ -0,0 +1,16 @@ +# This scripts demonstrates how to use mitmproxy's filter pattern in inline scripts. +# Usage: mitmdump -s "filt.py FILTER" + +from mitmproxy import filt + + +def start(context, argv): + if len(argv) != 2: + raise ValueError("Usage: -s 'filt.py FILTER'") + context.filter = filt.parse(argv[1]) + + +def response(context, flow): + if flow.match(context.filter): + print("Flow matches filter:") + print(flow) diff --git a/examples/flowbasic b/examples/flowbasic new file mode 100644 index 00000000..4a87b86a --- /dev/null +++ b/examples/flowbasic @@ -0,0 +1,44 @@ +#!/usr/bin/env python +""" + This example shows how to build a proxy based on mitmproxy's Flow + primitives. + + Heads Up: In the majority of cases, you want to use inline scripts. + + Note that request and response messages are not automatically replied to, + so we need to implement handlers to do this. +""" +from mitmproxy import flow +from mitmproxy.proxy import ProxyServer, ProxyConfig + + +class MyMaster(flow.FlowMaster): + def run(self): + try: + flow.FlowMaster.run(self) + except KeyboardInterrupt: + self.shutdown() + + def handle_request(self, f): + f = flow.FlowMaster.handle_request(self, f) + if f: + f.reply() + return f + + def handle_response(self, f): + f = flow.FlowMaster.handle_response(self, f) + if f: + f.reply() + print(f) + return f + + +config = ProxyConfig( + port=8080, + # use ~/.mitmproxy/mitmproxy-ca.pem as default CA file. + cadir="~/.mitmproxy/" +) +state = flow.State() +server = ProxyServer(config) +m = MyMaster(server, state) +m.run() diff --git a/examples/flowwriter.py b/examples/flowwriter.py new file mode 100644 index 00000000..8fb8cc60 --- /dev/null +++ b/examples/flowwriter.py @@ -0,0 +1,20 @@ +import random +import sys + +from mitmproxy.flow import FlowWriter + + +def start(context, argv): + if len(argv) != 2: + raise ValueError('Usage: -s "flowriter.py filename"') + + if argv[1] == "-": + f = sys.stdout + else: + f = open(argv[1], "wb") + context.flow_writer = FlowWriter(f) + + +def response(context, flow): + if random.choice([True, False]): + context.flow_writer.add(flow) diff --git a/examples/har_extractor.py b/examples/har_extractor.py new file mode 100644 index 00000000..4e905438 --- /dev/null +++ b/examples/har_extractor.py @@ -0,0 +1,253 @@ +""" + + This inline script utilizes harparser.HAR from + https://github.com/JustusW/harparser to generate a HAR log object. +""" +from harparser import HAR + +from datetime import datetime + + +class _HARLog(HAR.log): + # The attributes need to be registered here for them to actually be + # available later via self. This is due to HAREncodable linking __getattr__ + # to __getitem__. Anything that is set only in __init__ will just be added + # as key/value pair to self.__classes__. + __page_list__ = [] + __page_count__ = 0 + __page_ref__ = {} + + def __init__(self, page_list): + self.__page_list__ = page_list + self.__page_count__ = 0 + self.__page_ref__ = {} + + HAR.log.__init__(self, {"version": "1.2", + "creator": {"name": "MITMPROXY HARExtractor", + "version": "0.1", + "comment": ""}, + "pages": [], + "entries": []}) + + def reset(self): + self.__init__(self.__page_list__) + + def add(self, obj): + if isinstance(obj, HAR.pages): + self['pages'].append(obj) + if isinstance(obj, HAR.entries): + self['entries'].append(obj) + + def create_page_id(self): + self.__page_count__ += 1 + return "autopage_%s" % str(self.__page_count__) + + def set_page_ref(self, page, ref): + self.__page_ref__[page] = ref + + def get_page_ref(self, page): + return self.__page_ref__.get(page, None) + + def get_page_list(self): + return self.__page_list__ + + +def start(context, argv): + """ + On start we create a HARLog instance. You will have to adapt this to + suit your actual needs of HAR generation. As it will probably be + necessary to cluster logs by IPs or reset them from time to time. + """ + context.dump_file = None + if len(argv) > 1: + context.dump_file = argv[1] + else: + raise ValueError( + 'Usage: -s "har_extractor.py filename" ' + '(- will output to stdout, filenames ending with .zhar ' + 'will result in compressed har)' + ) + context.HARLog = _HARLog(['https://github.com']) + context.seen_server = set() + + +def response(context, flow): + """ + Called when a server response has been received. At the time of this + message both a request and a response are present and completely done. + """ + # Values are converted from float seconds to int milliseconds later. + ssl_time = -.001 + connect_time = -.001 + if flow.server_conn not in context.seen_server: + # Calculate the connect_time for this server_conn. Afterwards add it to + # seen list, in order to avoid the connect_time being present in entries + # that use an existing connection. + connect_time = flow.server_conn.timestamp_tcp_setup - \ + flow.server_conn.timestamp_start + context.seen_server.add(flow.server_conn) + + if flow.server_conn.timestamp_ssl_setup is not None: + # Get the ssl_time for this server_conn as the difference between + # the start of the successful tcp setup and the successful ssl + # setup. If no ssl setup has been made it is left as -1 since it + # doesn't apply to this connection. + ssl_time = flow.server_conn.timestamp_ssl_setup - \ + flow.server_conn.timestamp_tcp_setup + + # Calculate the raw timings from the different timestamps present in the + # request and response object. For lack of a way to measure it dns timings + # can not be calculated. The same goes for HAR blocked: MITMProxy will open + # a server connection as soon as it receives the host and port from the + # client connection. So the time spent waiting is actually spent waiting + # between request.timestamp_end and response.timestamp_start thus it + # correlates to HAR wait instead. + timings_raw = { + 'send': flow.request.timestamp_end - flow.request.timestamp_start, + 'wait': flow.response.timestamp_start - flow.request.timestamp_end, + 'receive': flow.response.timestamp_end - flow.response.timestamp_start, + 'connect': connect_time, + 'ssl': ssl_time + } + + # HAR timings are integers in ms, so we have to re-encode the raw timings to + # that format. + timings = dict([(key, int(1000 * value)) + for key, value in timings_raw.iteritems()]) + + # The full_time is the sum of all timings. Timings set to -1 will be ignored + # as per spec. + full_time = 0 + for item in timings.values(): + if item > -1: + full_time += item + + started_date_time = datetime.fromtimestamp( + flow.request.timestamp_start, + tz=utc).isoformat() + + request_query_string = [{"name": k, "value": v} + for k, v in flow.request.get_query()] + request_http_version = flow.request.http_version + # Cookies are shaped as tuples by MITMProxy. + request_cookies = [{"name": k.strip(), "value": v[0]} + for k, v in (flow.request.get_cookies() or {}).iteritems()] + request_headers = [{"name": k, "value": v} for k, v in flow.request.headers] + request_headers_size = len(str(flow.request.headers)) + request_body_size = len(flow.request.content) + + response_http_version = flow.response.http_version + # Cookies are shaped as tuples by MITMProxy. + response_cookies = [{"name": k.strip(), "value": v[0]} + for k, v in (flow.response.get_cookies() or {}).iteritems()] + response_headers = [{"name": k, "value": v} + for k, v in flow.response.headers] + response_headers_size = len(str(flow.response.headers)) + response_body_size = len(flow.response.content) + response_body_decoded_size = len(flow.response.get_decoded_content()) + response_body_compression = response_body_decoded_size - response_body_size + response_mime_type = flow.response.headers.get('Content-Type', '') + response_redirect_url = flow.response.headers.get('Location', '') + + entry = HAR.entries( + { + "startedDateTime": started_date_time, + "time": full_time, + "request": { + "method": flow.request.method, + "url": flow.request.url, + "httpVersion": request_http_version, + "cookies": request_cookies, + "headers": request_headers, + "queryString": request_query_string, + "headersSize": request_headers_size, + "bodySize": request_body_size, + }, + "response": { + "status": flow.response.status_code, + "statusText": flow.response.msg, + "httpVersion": response_http_version, + "cookies": response_cookies, + "headers": response_headers, + "content": { + "size": response_body_size, + "compression": response_body_compression, + "mimeType": response_mime_type}, + "redirectURL": response_redirect_url, + "headersSize": response_headers_size, + "bodySize": response_body_size, + }, + "cache": {}, + "timings": timings, + }) + + # If the current url is in the page list of context.HARLog or does not have + # a referrer we add it as a new pages object. + if flow.request.url in context.HARLog.get_page_list() or flow.request.headers.get( + 'Referer', + None) is None: + page_id = context.HARLog.create_page_id() + context.HARLog.add( + HAR.pages({ + "startedDateTime": entry['startedDateTime'], + "id": page_id, + "title": flow.request.url, + }) + ) + context.HARLog.set_page_ref(flow.request.url, page_id) + entry['pageref'] = page_id + + # Lookup the referer in the page_ref of context.HARLog to point this entries + # pageref attribute to the right pages object, then set it as a new + # reference to build a reference tree. + elif context.HARLog.get_page_ref(flow.request.headers.get('Referer')) is not None: + entry['pageref'] = context.HARLog.get_page_ref( + flow.request.headers['Referer'] + ) + context.HARLog.set_page_ref( + flow.request.headers['Referer'], entry['pageref'] + ) + + context.HARLog.add(entry) + + +def done(context): + """ + Called once on script shutdown, after any other events. + """ + from pprint import pprint + import json + + json_dump = context.HARLog.json() + compressed_json_dump = context.HARLog.compress() + + if context.dump_file == '-': + context.log(pprint.pformat(json.loads(json_dump))) + elif context.dump_file.endswith('.zhar'): + file(context.dump_file, "w").write(compressed_json_dump) + else: + file(context.dump_file, "w").write(json_dump) + context.log( + "HAR log finished with %s bytes (%s bytes compressed)" % ( + len(json_dump), len(compressed_json_dump) + ) + ) + context.log( + "Compression rate is %s%%" % str( + 100. * len(compressed_json_dump) / len(json_dump) + ) + ) + + +def print_attributes(obj, filter_string=None, hide_privates=False): + """ + Useful helper method to quickly get all attributes of an object and its + values. + """ + for attr in dir(obj): + if hide_privates and "__" in attr: + continue + if filter_string is not None and filter_string not in attr: + continue + value = getattr(obj, attr) + print("%s.%s" % ('obj', attr), value, type(value)) diff --git a/examples/iframe_injector.py b/examples/iframe_injector.py new file mode 100644 index 00000000..fc38b136 --- /dev/null +++ b/examples/iframe_injector.py @@ -0,0 +1,27 @@ +# Usage: mitmdump -s "iframe_injector.py url" +# (this script works best with --anticache) +from bs4 import BeautifulSoup +from mitmproxy.models import decoded + + +def start(context, argv): + if len(argv) != 2: + raise ValueError('Usage: -s "iframe_injector.py url"') + context.iframe_url = argv[1] + + +def response(context, flow): + if flow.request.host in context.iframe_url: + return + with decoded(flow.response): # Remove content encoding (gzip, ...) + html = BeautifulSoup(flow.response.content) + if html.body: + iframe = html.new_tag( + "iframe", + src=context.iframe_url, + frameborder=0, + height=0, + width=0) + html.body.insert(0, iframe) + flow.response.content = str(html) + context.log("Iframe inserted.") diff --git a/examples/mitmproxywrapper.py b/examples/mitmproxywrapper.py new file mode 100644 index 00000000..7ea10715 --- /dev/null +++ b/examples/mitmproxywrapper.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# +# Helper tool to enable/disable OS X proxy and wrap mitmproxy +# +# Get usage information with: +# +# mitmproxywrapper.py -h +# + +import subprocess +import re +import argparse +import contextlib +import os +import sys + + +class Wrapper(object): + + def __init__(self, port, extra_arguments=None): + self.port = port + self.extra_arguments = extra_arguments + + def run_networksetup_command(self, *arguments): + return subprocess.check_output( + ['sudo', 'networksetup'] + list(arguments)) + + def proxy_state_for_service(self, service): + state = self.run_networksetup_command( + '-getwebproxy', + service).splitlines() + return dict([re.findall(r'([^:]+): (.*)', line)[0] for line in state]) + + def enable_proxy_for_service(self, service): + print('Enabling proxy on {}...'.format(service)) + for subcommand in ['-setwebproxy', '-setsecurewebproxy']: + self.run_networksetup_command( + subcommand, service, '127.0.0.1', str( + self.port)) + + def disable_proxy_for_service(self, service): + print('Disabling proxy on {}...'.format(service)) + for subcommand in ['-setwebproxystate', '-setsecurewebproxystate']: + self.run_networksetup_command(subcommand, service, 'Off') + + def interface_name_to_service_name_map(self): + order = self.run_networksetup_command('-listnetworkserviceorder') + mapping = re.findall( + r'\(\d+\)\s(.*)$\n\(.*Device: (.+)\)$', + order, + re.MULTILINE) + return dict([(b, a) for (a, b) in mapping]) + + def run_command_with_input(self, command, input): + popen = subprocess.Popen( + command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + (stdout, stderr) = popen.communicate(input) + return stdout + + def primary_interace_name(self): + scutil_script = 'get State:/Network/Global/IPv4\nd.show\n' + stdout = self.run_command_with_input('/usr/sbin/scutil', scutil_script) + interface, = re.findall(r'PrimaryInterface\s*:\s*(.+)', stdout) + return interface + + def primary_service_name(self): + return self.interface_name_to_service_name_map()[ + self.primary_interace_name()] + + def proxy_enabled_for_service(self, service): + return self.proxy_state_for_service(service)['Enabled'] == 'Yes' + + def toggle_proxy(self): + new_state = not self.proxy_enabled_for_service( + self.primary_service_name()) + for service_name in self.connected_service_names(): + if self.proxy_enabled_for_service(service_name) and not new_state: + self.disable_proxy_for_service(service_name) + elif not self.proxy_enabled_for_service(service_name) and new_state: + self.enable_proxy_for_service(service_name) + + def connected_service_names(self): + scutil_script = 'list\n' + stdout = self.run_command_with_input('/usr/sbin/scutil', scutil_script) + service_ids = re.findall(r'State:/Network/Service/(.+)/IPv4', stdout) + + service_names = [] + for service_id in service_ids: + scutil_script = 'show Setup:/Network/Service/{}\n'.format( + service_id) + stdout = self.run_command_with_input( + '/usr/sbin/scutil', + scutil_script) + service_name, = re.findall(r'UserDefinedName\s*:\s*(.+)', stdout) + service_names.append(service_name) + + return service_names + + def wrap_mitmproxy(self): + with self.wrap_proxy(): + cmd = ['mitmproxy', '-p', str(self.port)] + if self.extra_arguments: + cmd.extend(self.extra_arguments) + subprocess.check_call(cmd) + + def wrap_honeyproxy(self): + with self.wrap_proxy(): + popen = subprocess.Popen('honeyproxy.sh') + try: + popen.wait() + except KeyboardInterrupt: + popen.terminate() + + @contextlib.contextmanager + def wrap_proxy(self): + connected_service_names = self.connected_service_names() + for service_name in connected_service_names: + if not self.proxy_enabled_for_service(service_name): + self.enable_proxy_for_service(service_name) + + yield + + for service_name in connected_service_names: + if self.proxy_enabled_for_service(service_name): + self.disable_proxy_for_service(service_name) + + @classmethod + def ensure_superuser(cls): + if os.getuid() != 0: + print('Relaunching with sudo...') + os.execv('/usr/bin/sudo', ['/usr/bin/sudo'] + sys.argv) + + @classmethod + def main(cls): + parser = argparse.ArgumentParser( + description='Helper tool for OS X proxy configuration and mitmproxy.', + epilog='Any additional arguments will be passed on unchanged to mitmproxy.') + parser.add_argument( + '-t', + '--toggle', + action='store_true', + help='just toggle the proxy configuration') +# parser.add_argument('--honeyproxy', action='store_true', help='run honeyproxy instead of mitmproxy') + parser.add_argument( + '-p', + '--port', + type=int, + help='override the default port of 8080', + default=8080) + args, extra_arguments = parser.parse_known_args() + + wrapper = cls(port=args.port, extra_arguments=extra_arguments) + + if args.toggle: + wrapper.toggle_proxy() +# elif args.honeyproxy: +# wrapper.wrap_honeyproxy() + else: + wrapper.wrap_mitmproxy() + + +if __name__ == '__main__': + Wrapper.ensure_superuser() + Wrapper.main() diff --git a/examples/modify_form.py b/examples/modify_form.py new file mode 100644 index 00000000..3e9d15c0 --- /dev/null +++ b/examples/modify_form.py @@ -0,0 +1,5 @@ +def request(context, flow): + if "application/x-www-form-urlencoded" in flow.request.headers.get("content-type", ""): + form = flow.request.get_form_urlencoded() + form["mitmproxy"] = ["rocks"] + flow.request.set_form_urlencoded(form) diff --git a/examples/modify_querystring.py b/examples/modify_querystring.py new file mode 100644 index 00000000..7f31a48f --- /dev/null +++ b/examples/modify_querystring.py @@ -0,0 +1,6 @@ + +def request(context, flow): + q = flow.request.get_query() + if q: + q["mitmproxy"] = ["rocks"] + flow.request.set_query(q) diff --git a/examples/modify_response_body.py b/examples/modify_response_body.py new file mode 100644 index 00000000..88f69e15 --- /dev/null +++ b/examples/modify_response_body.py @@ -0,0 +1,18 @@ +# Usage: mitmdump -s "modify_response_body.py mitmproxy bananas" +# (this script works best with --anticache) +from mitmproxy.models import decoded + + +def start(context, argv): + if len(argv) != 3: + raise ValueError('Usage: -s "modify-response-body.py old new"') + # You may want to use Python's argparse for more sophisticated argument + # parsing. + context.old, context.new = argv[1], argv[2] + + +def response(context, flow): + with decoded(flow.response): # automatically decode gzipped responses. + flow.response.content = flow.response.content.replace( + context.old, + context.new) diff --git a/examples/nonblocking.py b/examples/nonblocking.py new file mode 100644 index 00000000..41674b2a --- /dev/null +++ b/examples/nonblocking.py @@ -0,0 +1,9 @@ +import time +from mitmproxy.script import concurrent + + +@concurrent # Remove this and see what happens +def request(context, flow): + print("handle request: %s%s" % (flow.request.host, flow.request.path)) + time.sleep(5) + print("start request: %s%s" % (flow.request.host, flow.request.path)) diff --git a/examples/proxapp.py b/examples/proxapp.py new file mode 100644 index 00000000..4d8e7b58 --- /dev/null +++ b/examples/proxapp.py @@ -0,0 +1,24 @@ +""" +This example shows how to graft a WSGI app onto mitmproxy. In this +instance, we're using the Flask framework (http://flask.pocoo.org/) to expose +a single simplest-possible page. +""" +from flask import Flask + +app = Flask("proxapp") + + +@app.route('/') +def hello_world(): + return 'Hello World!' + + +# Register the app using the magic domain "proxapp" on port 80. Requests to +# this domain and port combination will now be routed to the WSGI app instance. +def start(context, argv): + context.app_registry.add(app, "proxapp", 80) + + # SSL works too, but the magic domain needs to be resolvable from the mitmproxy machine due to mitmproxy's design. + # mitmproxy will connect to said domain and use serve its certificate (unless --no-upstream-cert is set) + # but won't send any data. + context.app_registry.add(app, "example.com", 443) diff --git a/examples/read_dumpfile b/examples/read_dumpfile new file mode 100644 index 00000000..56746bb8 --- /dev/null +++ b/examples/read_dumpfile @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# +# Simple script showing how to read a mitmproxy dump file +# + +from mitmproxy import flow +import pprint +import sys + +with open(sys.argv[1], "rb") as logfile: + freader = flow.FlowReader(logfile) + pp = pprint.PrettyPrinter(indent=4) + try: + for f in freader.stream(): + print(f) + print(f.request.host) + pp.pprint(f.get_state()) + print("") + except flow.FlowReadError as v: + print "Flow file corrupted. Stopped loading." diff --git a/examples/redirect_requests.py b/examples/redirect_requests.py new file mode 100644 index 00000000..c0a0ccba --- /dev/null +++ b/examples/redirect_requests.py @@ -0,0 +1,22 @@ +""" +This example shows two ways to redirect flows to other destinations. +""" +from mitmproxy.models import HTTPResponse +from netlib.http import Headers + +def request(context, flow): + # pretty_host takes the "Host" header of the request into account, + # which is useful in transparent mode where we usually only have the IP + # otherwise. + + # Method 1: Answer with a locally generated response + if flow.request.pretty_host.endswith("example.com"): + resp = HTTPResponse( + "HTTP/1.1", 200, "OK", + Headers(Content_Type="text/html"), + "helloworld") + flow.reply(resp) + + # Method 2: Redirect the request to a different server + if flow.request.pretty_host.endswith("example.org"): + flow.request.host = "mitmproxy.org" diff --git a/examples/sslstrip.py b/examples/sslstrip.py new file mode 100644 index 00000000..369427a2 --- /dev/null +++ b/examples/sslstrip.py @@ -0,0 +1,40 @@ +from netlib.http import decoded +import re +from six.moves import urllib + +def start(context, argv) : + + #set of SSL/TLS capable hosts + context.secure_hosts = set() + +def request(context, flow) : + + flow.request.headers.pop('If-Modified-Since', None) + flow.request.headers.pop('Cache-Control', None) + + #proxy connections to SSL-enabled hosts + if flow.request.pretty_host in context.secure_hosts : + flow.request.scheme = 'https' + flow.request.port = 443 + +def response(context, flow) : + + with decoded(flow.response) : + flow.request.headers.pop('Strict-Transport-Security', None) + flow.request.headers.pop('Public-Key-Pins', None) + + #strip links in response body + flow.response.content = flow.response.content.replace('https://', 'http://') + + #strip links in 'Location' header + if flow.response.headers.get('Location','').startswith('https://'): + location = flow.response.headers['Location'] + hostname = urllib.parse.urlparse(location).hostname + if hostname: + context.secure_hosts.add(hostname) + flow.response.headers['Location'] = location.replace('https://', 'http://', 1) + + #strip secure flag from 'Set-Cookie' headers + cookies = flow.response.headers.get_all('Set-Cookie') + cookies = [re.sub(r';\s*secure\s*', '', s) for s in cookies] + flow.response.headers.set_all('Set-Cookie', cookies) diff --git a/examples/stickycookies b/examples/stickycookies new file mode 100644 index 00000000..8f11de8d --- /dev/null +++ b/examples/stickycookies @@ -0,0 +1,42 @@ +#!/usr/bin/env python +""" +This example builds on mitmproxy's base proxying infrastructure to +implement functionality similar to the "sticky cookies" option. + +Heads Up: In the majority of cases, you want to use inline scripts. +""" +import os +from mitmproxy import controller, proxy +from mitmproxy.proxy.server import ProxyServer + + +class StickyMaster(controller.Master): + def __init__(self, server): + controller.Master.__init__(self, server) + self.stickyhosts = {} + + def run(self): + try: + return controller.Master.run(self) + except KeyboardInterrupt: + self.shutdown() + + def handle_request(self, flow): + hid = (flow.request.host, flow.request.port) + if "cookie" in flow.request.headers: + self.stickyhosts[hid] = flow.request.headers.get_all("cookie") + elif hid in self.stickyhosts: + flow.request.headers.set_all("cookie", self.stickyhosts[hid]) + flow.reply() + + def handle_response(self, flow): + hid = (flow.request.host, flow.request.port) + if "set-cookie" in flow.response.headers: + self.stickyhosts[hid] = flow.response.headers.get_all("set-cookie") + flow.reply() + + +config = proxy.ProxyConfig(port=8080) +server = ProxyServer(config) +m = StickyMaster(server) +m.run() diff --git a/examples/stream.py b/examples/stream.py new file mode 100644 index 00000000..3adbe437 --- /dev/null +++ b/examples/stream.py @@ -0,0 +1,5 @@ +def responseheaders(context, flow): + """ + Enables streaming for all responses. + """ + flow.response.stream = True diff --git a/examples/stream_modify.py b/examples/stream_modify.py new file mode 100644 index 00000000..aa395c03 --- /dev/null +++ b/examples/stream_modify.py @@ -0,0 +1,20 @@ +""" +This inline script modifies a streamed response. +If you do not need streaming, see the modify_response_body example. +Be aware that content replacement isn't trivial: + - If the transfer encoding isn't chunked, you cannot simply change the content length. + - If you want to replace all occurences of "foobar", make sure to catch the cases + where one chunk ends with [...]foo" and the next starts with "bar[...]. +""" + + +def modify(chunks): + """ + chunks is a generator that can be used to iterate over all chunks. + """ + for chunk in chunks: + yield chunk.replace("foo", "bar") + + +def responseheaders(context, flow): + flow.response.stream = modify diff --git a/examples/stub.py b/examples/stub.py new file mode 100644 index 00000000..516b71a5 --- /dev/null +++ b/examples/stub.py @@ -0,0 +1,79 @@ +""" + This is a script stub, with definitions for all events. +""" + + +def start(context, argv): + """ + Called once on script startup, before any other events. + """ + context.log("start") + + +def clientconnect(context, root_layer): + """ + Called when a client initiates a connection to the proxy. Note that a + connection can correspond to multiple HTTP requests + """ + context.log("clientconnect") + + +def request(context, flow): + """ + Called when a client request has been received. + """ + context.log("request") + + +def serverconnect(context, server_conn): + """ + Called when the proxy initiates a connection to the target server. Note that a + connection can correspond to multiple HTTP requests + """ + context.log("serverconnect") + + +def responseheaders(context, flow): + """ + Called when the response headers for a server response have been received, + but the response body has not been processed yet. Can be used to tell mitmproxy + to stream the response. + """ + context.log("responseheaders") + + +def response(context, flow): + """ + Called when a server response has been received. + """ + context.log("response") + + +def error(context, flow): + """ + Called when a flow error has occured, e.g. invalid server responses, or + interrupted connections. This is distinct from a valid server HTTP error + response, which is simply a response with an HTTP error code. + """ + context.log("error") + + +def serverdisconnect(context, server_conn): + """ + Called when the proxy closes the connection to the target server. + """ + context.log("serverdisconnect") + + +def clientdisconnect(context, root_layer): + """ + Called when a client disconnects from the proxy. + """ + context.log("clientdisconnect") + + +def done(context): + """ + Called once on script shutdown, after any other events. + """ + context.log("done") diff --git a/examples/tcp_message.py b/examples/tcp_message.py new file mode 100644 index 00000000..c63368e4 --- /dev/null +++ b/examples/tcp_message.py @@ -0,0 +1,24 @@ +''' +tcp_message Inline Script Hook API Demonstration +------------------------------------------------ + +* modifies packets containing "foo" to "bar" +* prints various details for each packet. + +example cmdline invocation: +mitmdump -T --host --tcp ".*" -q -s examples/tcp_message.py +''' +from netlib.utils import clean_bin + +def tcp_message(ctx, tcp_msg): + modified_msg = tcp_msg.message.replace("foo", "bar") + + is_modified = False if modified_msg == tcp_msg.message else True + tcp_msg.message = modified_msg + + print("[tcp_message{}] from {} {} to {} {}:\r\n{}".format( + " (modified)" if is_modified else "", + "client" if tcp_msg.sender == tcp_msg.client_conn else "server", + tcp_msg.sender.address, + "server" if tcp_msg.receiver == tcp_msg.server_conn else "client", + tcp_msg.receiver.address, clean_bin(tcp_msg.message))) diff --git a/examples/tls_passthrough.py b/examples/tls_passthrough.py new file mode 100644 index 00000000..8c8fa4eb --- /dev/null +++ b/examples/tls_passthrough.py @@ -0,0 +1,136 @@ +""" +This inline script allows conditional TLS Interception based +on a user-defined strategy. + +Example: + + > mitmdump -s tls_passthrough.py + + 1. curl --proxy http://localhost:8080 https://example.com --insecure + // works - we'll also see the contents in mitmproxy + + 2. curl --proxy http://localhost:8080 https://example.com --insecure + // still works - we'll also see the contents in mitmproxy + + 3. curl --proxy http://localhost:8080 https://example.com + // fails with a certificate error, which we will also see in mitmproxy + + 4. curl --proxy http://localhost:8080 https://example.com + // works again, but mitmproxy does not intercept and we do *not* see the contents + +Authors: Maximilian Hils, Matthew Tuusberg +""" +from __future__ import (absolute_import, print_function, division) +import collections +import random + +from enum import Enum + +from mitmproxy.exceptions import TlsProtocolException +from mitmproxy.protocol import TlsLayer, RawTCPLayer + + +class InterceptionResult(Enum): + success = True + failure = False + skipped = None + + +class _TlsStrategy(object): + """ + Abstract base class for interception strategies. + """ + def __init__(self): + # A server_address -> interception results mapping + self.history = collections.defaultdict(lambda: collections.deque(maxlen=200)) + + def should_intercept(self, server_address): + """ + Returns: + True, if we should attempt to intercept the connection. + False, if we want to employ pass-through instead. + """ + raise NotImplementedError() + + def record_success(self, server_address): + self.history[server_address].append(InterceptionResult.success) + + def record_failure(self, server_address): + self.history[server_address].append(InterceptionResult.failure) + + def record_skipped(self, server_address): + self.history[server_address].append(InterceptionResult.skipped) + + +class ConservativeStrategy(_TlsStrategy): + """ + Conservative Interception Strategy - only intercept if there haven't been any failed attempts + in the history. + """ + + def should_intercept(self, server_address): + if InterceptionResult.failure in self.history[server_address]: + return False + return True + + +class ProbabilisticStrategy(_TlsStrategy): + """ + Fixed probability that we intercept a given connection. + """ + def __init__(self, p): + self.p = p + super(ProbabilisticStrategy, self).__init__() + + def should_intercept(self, server_address): + return random.uniform(0, 1) < self.p + + +class TlsFeedback(TlsLayer): + """ + Monkey-patch _establish_tls_with_client to get feedback if TLS could be established + successfully on the client connection (which may fail due to cert pinning). + """ + + def _establish_tls_with_client(self): + server_address = self.server_conn.address + tls_strategy = self.script_context.tls_strategy + + try: + super(TlsFeedback, self)._establish_tls_with_client() + except TlsProtocolException as e: + tls_strategy.record_failure(server_address) + raise e + else: + tls_strategy.record_success(server_address) + + +# inline script hooks below. + + +def start(context, argv): + if len(argv) == 2: + context.tls_strategy = ProbabilisticStrategy(float(argv[1])) + else: + context.tls_strategy = ConservativeStrategy() + + +def next_layer(context, next_layer): + """ + This hook does the actual magic - if the next layer is planned to be a TLS layer, + we check if we want to enter pass-through mode instead. + """ + if isinstance(next_layer, TlsLayer) and next_layer._client_tls: + server_address = next_layer.server_conn.address + + if context.tls_strategy.should_intercept(server_address): + # We try to intercept. + # Monkey-Patch the layer to get feedback from the TLSLayer if interception worked. + next_layer.__class__ = TlsFeedback + next_layer.script_context = context + else: + # We don't intercept - reply with a pass-through layer and add a "skipped" entry. + context.log("TLS passthrough for %s" % repr(next_layer.server_conn.address), "info") + next_layer_replacement = RawTCPLayer(next_layer.ctx, logging=False) + next_layer.reply(next_layer_replacement) + context.tls_strategy.record_skipped(server_address) diff --git a/examples/upsidedownternet.py b/examples/upsidedownternet.py new file mode 100644 index 00000000..b7489cb6 --- /dev/null +++ b/examples/upsidedownternet.py @@ -0,0 +1,17 @@ +import cStringIO +from PIL import Image +from mitmproxy.models import decoded + + +def response(context, flow): + if flow.response.headers.get("content-type", "").startswith("image"): + with decoded(flow.response): # automatically decode gzipped responses. + try: + s = cStringIO.StringIO(flow.response.content) + img = Image.open(s).rotate(180) + s2 = cStringIO.StringIO() + img.save(s2, "png") + flow.response.content = s2.getvalue() + flow.response.headers["content-type"] = "image/png" + except: # Unknown image types etc. + pass |