# mitmproxy for fun and profit: Interception and Analysis of Application Traffic

A solid understanding of the protocols used by applications is a necessary prerequisite when assessing application security. In recent projects, we have had to intercept various types of network traffic across different platforms, including Linux, Android, and iOS. The purpose of this article is to introduce the mitmproxy tool and how to use it, as well as the different techniques that can be implemented to effectively intercept these communications, while taking into account the specific characteristics of each environment.

Looking to improve your skills? Discover our **trainings** sessions! Learn more.

## Tool Overview

`mitmproxy` is an open-source tool, primarily developed in Python, that allows analyzing, modifying, and replaying network communications at various protocol layers (mainly _HTTP_ and _HTTPS_), but it can also operate directly at the transport layer with protocols such as _TCP_ and _UDP_. It belongs to the category of MITM ( **M** an- **I** n- **T** he- **M** iddle) tools by positioning itself between a client and a server. The tool can be used in _explicit_ mode, where the proxy is configured directly on the device, or in _transparent_ mode, in which the device is not aware of the proxy’s presence.

In _explicit_ mode, the proxy is configured directly on the target device (workstation, smartphone, emulator, etc.) through the settings specific to the type of connection. The application or operating system is therefore aware that a proxy is being used, and all relevant traffic is intentionally redirected to `mitmproxy`. This mode is generally the easiest to implement, as it requires very little configuration on the device. In this mode, applications that do not follow the device’s proxy configuration will not have their traffic routed through `mitmproxy`, unless firewall rules are used to force the redirection of these packets.

Conversely, _transparent_ mode does not require any prior configuration on the client side. Traffic is then redirected to the `mitmproxy` instance using network mechanisms such as routing or redirection rules (for example, using tools like `iptables` or `nftables`). The target device therefore has no awareness of the proxy’s presence. This mode is particularly useful when configuring an explicit proxy is not possible or when you want to observe application behavior without modifying the client environment. In transparent mode, `mitmproxy` relies on the TLS _ClientHello_ SNI extension to dynamically determine the domain for which to forge the certificate. However, it has the drawback of allowing applications to use any protocol they wish for outbound traffic, which leads to the use of protocols such as _QUIC_ (based on _UDP_), a protocol widely used on mobile devices.

In both modes, and except in specific cases (for example, when there is no TLS validation on the client side), installing the root certificate generated by `mitmproxy` on the device is required in order to access the content of encrypted exchanges. This is the critical point when analyzing network communications, regardless of the operating system. However, some applications implement _Certificate Pinning_ mechanisms, strictly limiting the trusted certificates accepted for one or more domains. In such cases, interception becomes impossible without implementing specific countermeasures aimed at bypassing or neutralizing these protections…

From a network perspective, interception can be performed at different levels:

– At the transport layer, by intercepting TCP or UDP traffic.
– At the application layer, by acting as an explicit or transparent HTTP proxy.
– At the TLS layer, enabling the decryption of encrypted communications, particularly those based on HTTPS.

The configuration and use of `mitmproxy` are intended to be relatively straightforward. The tool can be deployed directly on the host or run within an OCI image provided by the developers.

Three main tools are provided when installing the package:

– `mitmproxy`, allowing you to start interception interactively directly from the command line with an interface integrated into the terminal
– `mitmweb`, which also starts interception but provides visualization through a web-based UI
– `mitmdump`, the non-interactive variant that allows script injection and dumping the different network flows in a format specific to the tool.

Among its key features, `mitmproxy` also provides a scripting API that enables the development of custom modules for fine-grained manipulation of requests and responses at different protocol layers. This capability is one of the tool’s main strengths and will be leveraged several times throughout this article.

## Network setup for interception

> This section focuses on setting up a lab environment on Linux using features available in the kernel and therefore does not directly apply to other operating systems such as Windows or macOS.

Our network environment is based on the use of _network namespaces_, a mechanism available in the Linux kernel since version **2.6.24 (January 2008)**. Setting up these namespaces makes it possible to isolate the network stack (routing tables, firewall, interfaces, etc.) between processes launched within the namespace and those launched outside of it. Creating them is very simple using the `ip` utility and requires _root_ privileges.

“`
Using ip utility… ip netns add
“`

For example, it is possible to have a different version of the `resolv.conf` file within the namespace by creating it in the `/etc/netns//` directory.

Once our namespace has been instantiated, we can attach all the interfaces required for its proper operation, namely the _loopback_ interface ( `lo`) as well as the Wi-Fi interface (named `wlan0` in our example). The latter will allow us to connect mobile devices to the same network as our proxy. It is worth noting that, depending on the configuration of the system on which the commands are executed, it may be necessary to adjust certain settings to prevent services such as `NetworkManager` or `wpa_supplicant` from taking back control of the interface.

“`
WIFI_IFACE=wlan0 ip netns exec ip link set lo up ip link set $WIFI_IFACE down ip link set $WIFI_IFACE netns ip netns exec ip link set $WIFI_IFACE up
“`

The `ip netns exec ` command allows you to run commands inside the network namespace rather than directly on the host.

From this point on, our Wi-Fi interface is no longer accessible from the main namespace and is available only within our new namespace.

We can now create our Wi-Fi access point using the lnxrouter script, which allows the creation of the access point, the management of firewall rules, and routing to a specified outbound interface.

Launching the utility is fairly straightforward:

“`
AP_NAME=mitm_lab AP_PASS=VerySecurePasswd OUTPUT_IFACE=tun0 ip netns exec lnxrouter –ap $WIFI_IFACE $AP_NAME -p $AP_PASS -o $OUTPUT_IFACE –dhcp-range=10.10.0.10,10.10.0.100,255.255.255.0,24h
“`

From the perspective of the outbound interface, the only requirement is that it must exist within the network namespace. Many different scenarios can be envisioned, depending on your use case:

– Outbound traffic through a controlled VPS via a VPN tunnel ( `wireguard`, `openvpn`…)
– Outbound traffic through a controlled VPS via an SSH tunnel (using the `-w` option)
– Outbound traffic through the host’s Ethernet interface
– …

The DHCP server setup is also handled by `lnxrouter`, and some parameters can be configured directly through the available options.

It is also necessary to add firewall rules when running `mitmproxy` in _transparent_ mode. These rules make it possible to redirect all _HTTP_ and _HTTPS_ traffic via DNAT to our `mitmproxy` instance.

“`
ip netns exec nft add table nat ip netns exec nft add chain nat prerouting { type nat hook prerouting priority -100 ; } ip netns exec nft add rule nat prerouting iifname ip daddr != tcp dport 80 redirect to ip netns exec nft add rule nat prerouting iifname ip daddr != tcp dport 443 redirect to ip netns exec nft add rule nat prerouting iifname udp dport 53 redirect to
“`

Regarding the launch of the `mitmproxy` tool:

“`
mitmweb -s scripts/inspection.py –web-open-browser –mode [regular|transparent] –set web_host=$MITMWEB_HOST –set web_port=$MITMWEB_PORT –ignore-hosts=”$IGNORE_HOSTS_REGEXP”
“`

After presenting the tool, its features, and the network environment we will rely on for the rest of the study, we will now move on to interception examples on the different platforms mentioned in the introduction.

## Alteration of application traffic on Linux

This part of the study will focus on visualizing and manipulating the network traffic generated by the `git` tool when cloning a project.

With our newly created network namespace, we can now launch the `mitmweb` tool inside it (listening on _localhost:8080_ for demonstration purposes).

We can also run a _git clone_ of an arbitrary project within it; for our example, we will use the nmap project. To force the use of the proxy, `git` respects the _HTTP_PROXY_ and _HTTPS_PROXY_ environment variables, which we can set to `http://127.0.0.1:8080`. Since the CA generated by our `mitmproxy` is not present in the list of trusted root certificates on our system, it is necessary to specify to `git` the path to our root certificate. This can be done using the `GIT_SSL_CAINFO` environment variable, although we could also have completely disabled certificate verification by setting `GIT_SSL_NO_VERIFY=true`. This attack assumes that the user does not verify commit or tag signatures, as these are not automatically validated by default.

Cloning our Git project can then be performed directly within the network namespace in order to take advantage of the configured default route, any `resolv.conf` file specific to the namespace, as well as the appropriate firewall rules for our processing.

“`
HTTP_PROXY=http://127.0.0.1:8080 HTTPS_PROXY=http://127.0.0.1:8080 GIT_SSL_CAINFO= git clone https://github.com/nmap/nmap
“`

We typically observe three main requests in our `mitmproxy`:

– The first allows the client to retrieve information about the server and the repository
– The second enumerates the list of references present on the server, such as branches or tags
– The third transfers the source code in the form of Git objects

From a security perspective, we will focus on intercepting the last two requests in order to make git download a completely different code repository, for example Magisk. It would also have been entirely possible to return a modified version of the _nmap_ project, for instance one including a backdoor. The attack does not modify the Git protocol itself but simply redirects the HTTP requests to another repository, leaving the remote server to handle the Git protocol negotiation as usual.

“`
from mitmproxy import ctx, http SRC_REPO_PATH = “/nmap/nmap” DST_REPO_PATH = “/topjohnwu/Magisk” def request(flow: http.HTTPFlow): req = flow.request # We only want to alter github domains if req.host != “github.com”: return # info/refs?service=git-upload-pack if req.path.startswith(f”{SRC_REPO_PATH}/info/refs”): req.path = req.path.replace(SRC_REPO_PATH, DST_REPO_PATH) ctx.log.info(f”[info/refs] {SRC_REPO_PATH} → {DST_REPO_PATH}”) # git-upload-pack (ls-refs, fetch) elif req.path.startswith(f”{SRC_REPO_PATH}/git-upload-pack”): req.path = req.path.replace(SRC_REPO_PATH, DST_REPO_PATH) ctx.log.info(f”[git-upload-pack] {SRC_REPO_PATH} → {DST_REPO_PATH}”)
“`

By taking a closer look at the output of the `git` commands in the code directory:

“`
$ git remote -v # nothing suspicious… origin https://github.com/nmap/nmap (fetch) origin https://github.com/nmap/nmap (push) $ git log HEAD^1..HEAD # suspicious output showing latest magisk commit…
“`

This interception clearly demonstrates the ease and possibilities offered by `mitmproxy` when it comes to altering network traffic. In our example, the environment is fully controlled, but one could imagine applying the same type of attack to a victim workstation with a rather “permissive” git configuration.

## Interception and modification of responses on an Android device

The issue of accepting root certificates remains the same for mobile devices, with the additional difficulty that they are not easily modifiable. On Android, certificate management relies on two concepts: **user** certificates and **system** certificates.

Since Android 7 (Nougat), applications no longer trust user certificates by default, which makes it necessary to install them at the system level in order to intercept certain applications[1].

System **certificates** are available in the `/system/etc/security/cacerts` directory and are accessible in read-only mode only. They are also visible from the System settings under _Security_ > _More security & privacy_ > _Encryption and Credentials_ > _Trusted Credentials_.

It is therefore necessary to have a rooted device (using Magisk, for example) in order to modify the contents of this partition in read/write mode as early as possible in the device’s boot chain. _Magisk_ makes it easy to install “modules”, and one of them, called Cert-Fixer, copies all _user_ certificates into the _system_ certificates directory. This method therefore allows our `mitmproxy` certificate to be recognized as a certificate with the highest level of trust on our device.

List of user certificatesList of system certificates

We will now use `mitmproxy` to visualize and modify a request sent to a Google server. We chose to intercept requests to the `geomobileservices-pa.googleapis.com` domain, which is regularly contacted over _gRPC_ by the `com.google.android.gms` package in order to resolve a location in the form of `(latitude, longitude)` into a “textual” address.

The request is made to the full URL https://geomobileservices-pa.googleapis.com/google.internal.maps.geomob…

Like many requests made on mobile platforms, the format of the exchanged data relies on `protobuf`, a serialization format developed by Google.

We will therefore develop a `mitmproxy` script to parse the content of this request using the associated description file, and then modify the `latitude` and `longitude` fields in order to spoof our real position to Google’s servers.

First of all, in order to easily reproduce this request, the following code will be executed on the phone as _system_server_:

“`
public class GeocodeCallback extends IGeocodeCallback.Stub { private static final String TAG = “GeocodeCallback”; public GeocodeCallback() { } @Override public void onError(String arg0) throws RemoteException { Log.e(TAG, “got error: ” + arg0); } @Override public void onResults(List

arg0) throws RemoteException { Logger.info(“got reverse location result:”); for (Address addr : arg0) { Log.i(TAG, “tcountry=” + addr.getCountryName()); Log.i(TAG, “tcountryCode=” + addr.getCountryCode()); Log.i(TAG, “taddress=” + addr.getAddressLine(0)); } } } public class Main { public static void main(String[] args) { try { IBinder locationService = ServiceManager .getService(Context.LOCATION_SERVICE); ILocationManager locationManager = ILocationManager.Stub.asInterface(locationService); /* Eiffel Tower coordinates */ Pair coordinates = Pair.create(48.636093, -1.511457); ReverseGeocodeRequest request = new ReverseGeocodeRequest.Builder( coordinates.first, coordinates.second, 1, /* max results in response */ Locale.US, 1000, /* system_server UID */ “android” /* system_server package name */ ).build(); } catch (Exception e) { Logger.error(“got exception: ” + e); } } }
“`

Running this PoC in the proper context allows us to directly visualize the traffic in the `mitmproxy` interface and infer the expected request format. The “exact” format of this data can also be found by analyzing the Android source code, in particular the _Java_ class `ReverseGeocodeRequest`.

The request is structured as follows:

And the response:

Once this information has been intercepted, we can infer the following `protobuf` message format:

“`
syntax = “proto3”; message ReverseGeocodeRequest { Location location = 1; string locale = 3; int64 max_results = 6; string package_name = 7; message Location { double latitude = 1; double longitude = 2; } }
“`

This schema can then be “compiled” into Python using the protobuf compiler `protoc`. This makes it easier to integrate into our `mitmproxy` script:

“`
protoc –proto_path=. –python_out=. reverse_geocode.proto
“`

We can then develop our `mitmproxy` script to modify on the fly the coordinates sent by our device:

“`
GRPC_FRAMING_LEN = 5 def should_intercept(request: http.Request) -> bool: content_type = request.headers.get(“content-type”, “”) host = request.host path = request.path return ( content_type.lower().startswith(“application/grpc”) and host == “geomobileservices-pa.googleapis.com” and path.endswith(“/ReverseGeocode”) ) def request(flow: http.HTTPFlow): if not should_intercept(flow.request): # likely not the content we want to intercept return uncompressed_content = flow.request.content if not uncompressed_content: ctx.log.error(“error while getting uncompressed content”) return # gRPC framing: # 1 byte -> compression # 4 bytes -> message length (big endian) # N bytes -> message if len(uncompressed_content) < GRPC_FRAMING_LEN: return # assuming here that data isn’t sent compressed message_length = int.from_bytes( uncompressed_content[1:GRPC_FRAMING_LEN], byteorder=”big” ) message = uncompressed_content[GRPC_FRAMING_LEN : GRPC_FRAMING_LEN + message_length] geocode_request = ReverseGeocodeRequest() geocode_request.ParseFromString(message) # Alter request content geocode_request.location.latitude = 48.8584 geocode_request.location.longitude = 2.2945 # ……… # Serialize back data (gRPC-framed) new_payload = geocode_request.SerializeToString() new_payload_data = bytes([0]) + len(new_payload).to_bytes(4, byteorder=”big”) + new_payload # ……… flow.request.content = new_payload_data ctx.log.info(“Successfully replaced payload”)
“`

And in the end, the “legitimate” location requests made by the phone (and in particular those made by the `com.android.phone` package) are now resolved as coming from the Eiffel Tower rather than from our offices in Paris 🙂.

Intercepted request:

And the response received from Google’s servers:

This is obviously not the only method used by Google to retrieve user location, and this study would need to be conducted across many more (domain / endpoint) pairs in order to be exhaustive. The objective of this proof of concept is to demonstrate how easy it is to modify data using a `mitmproxy` instance placed in interception mode, even when dealing with protocols such as _gRPC_.

## Analysis of the Mumble application on iOS

This final part of the study will focus on network analysis and interception on _iOS_, and in particular on the passive _dumping_ of text messages exchanged with a server by the `Mumble` application.

### Server setup

The server code is open-source and is available in most Linux package distributions. For those who do not wish to install the package, it is also possible to deploy it using a Docker image (the solution we will use for this study). The following _bash_ script allows the container to be launched:

“`
#! /bin/bash set -e MUMBLE_DATA_FOLDER=$(pwd)/data mkdir ${MUMBLE_DATA_FOLDER} || true podman run –rm –name mumble-server –userns=keep-id -u “$(id -u):$(id -g)” –publish 64738:64738/tcp –publish 64738:64738/udp –volume ${MUMBLE_DATA_FOLDER}:/data –restart on-failure -e MUMBLE_CHOWN_DATA=false ghcr.io/mumble-voip/mumble-server:latest
“`

### Protocol aspects and data encapsulation

The application communicates with the server over _TCP_, encrypting the application data using the _TLS_ protocol. Strong verification through _certificate pinning_ is not mandatory since the list of servers is not known in advance or fixed (which makes sense, as servers can be self-hosted). The messages are not additionally encrypted within the TLS exchanges, which makes it possible to recover the data in clear text in case of compromise of the communication channel (for example, by accepting a self-signed TLS certificate or one that is not valid for the contacted domain). The application protocol used is `protobuf`, and all schemas are available on the project’s GitHub. In our case, the Mumble.proto schema is of interest, and in particular the `TextMessage` message type containing the actual message as well as certain metadata:

“`
message TextMessage { optional uint32 actor = 1; repeated uint32 session = 2; repeated uint32 channel_id = 3; repeated uint32 tree_id = 4; required string message = 5; }
“`

These elements allow us to set up our `mitmproxy` instance in `reverse:tls` mode. Our instance listens on Mumble’s “standard” port (namely _64738_), and once the data is intercepted, it forwards it to our Mumble server (over _TLS_) listening on port _64000_.

“`
MUMBLE_HOST=192.168.12.1 MUMBLE_PORT=64000 MITMPROXY_PORT=64738 mitmweb –ssl-insecure –mode reverse:tls://$(MUMBLE_HOST):$(MUMBLE_PORT) –listen-port $(MITMPROXY_PORT) –set web_port=9008 –set web_host=192.168.20.2 -s intercept_mumble_messages.py
“`

The interception script `intercept_mumble_messages.py` is based on the prior compilation of the protobuf file:

“`
protoc –proto_path=. –python_out=. Mumble.proto
“`

In the end, it is fairly simple. TCP data buffering is implemented to handle messages arriving across multiple TCP packets. For this proof of concept, the data is kept in memory per TCP session and for the entire lifetime of the program.

“`
from mitmproxy import tcp import struct import Mumble_pb2 # Interesting message types MESSAGE_TYPES = { 9: Mumble_pb2.UserState, 11: Mumble_pb2.TextMessage, } class MumbleMessageLogger: def __init__(self): self.buffers = {} def tcp_start(self, flow: tcp.TCPFlow): self.buffers[flow.id] = b”” def tcp_message(self, flow: tcp.TCPFlow): data = flow.messages[-1].content buffer = self.buffers.get(flow.id, b””) + data offset = 0 while True: if len(buffer[offset:]) < 6: # not enough data to read header break # 2 bytes type, 4 bytes length msg_type = struct.unpack(“>H”, buffer[offset:offset+2])[0] length = struct.unpack(“>I”, buffer[offset+2:offset+6])[0] if len(buffer[offset+6:]) < length: # incomplete payload, waiting for another message break payload = buffer[offset+6:offset+6+length] if msg_type in MESSAGE_TYPES: try: msg = MESSAGE_TYPES[msg_type]() msg.ParseFromString(payload) if isinstance(msg, Mumble_pb2.TextMessage): print(f”Actor {msg.actor} sent {msg.message}”) elif isinstance(msg, UserState): print(f”[USER JOIN] -> {msg.name or “unknown”}”) except Exception as e: print(“Protobuf parse error:”, e) offset += 6 + length # keep remaining bytes for next segment self.buffers[flow.id] = buffer[offset:] def tcp_end(self, flow: tcp.TCPFlow): self.buffers.pop(flow.id, None) addons = [ MumbleMessageLogger() ]
“`

We then see the messages displayed in our terminal:

“`
[USER JOIN] -> phone2 [MESSAGE] -> Welcome to the server @phone2 [MESSAGE] -> hello world 🙂
“`

This POC can be enhanced to include displaying the username of the message sender, users who have left the server, and more. Many other types of protobuf messages can be sent by clients and the server, all of which are available in the _Mumble.proto_ file.

## Conclusion

This article demonstrated how easily `mitmproxy` allows the interception and analysis of application-level traffic on Linux, Android, and iOS. We saw that mastering the network layers is essential to understand and manipulate communications, whether for protocol exploration or security testing. Despite encryption, interception remains possible with the correct certificates and knowledge of the protocols, though protections like _certificate pinning_ or application-level encryption can limit this capability.

[1] Android Privacy and Security: https://developer.android.com/privacy-and-security/security-config#base…