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 (Man-In-The-Middle) 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 terminalmitmweb, which also starts interception but provides visualization through a web-based UImitmdump, 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 <MITM_NAMESPACE_NAME>
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/<MITM_NAMESPACE_NAME>/ 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 <MITM_NAMESPACE_NAME> ip link set lo up
ip link set $WIFI_IFACE down
ip link set $WIFI_IFACE netns <MITM_NAMESPACE_NAME>
ip netns exec <MITM_NAMESPACE_NAME> ip link set $WIFI_IFACE up
The ip netns exec <MITM_NAMESPACE_NAME> 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 <MITM_NAMESPACE_NAME> 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
-woption) - 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 <MITM_NAMESPACE_NAME> nft add table nat
ip netns exec <MITM_NAMESPACE_NAME> nft add chain nat prerouting { type nat hook prerouting priority -100 \; }
ip netns exec <MITM_NAMESPACE_NAME> nft add rule nat prerouting iifname <WIFI_IFACE> ip daddr != <MITMPROXY_IP_ADDR> tcp dport 80 redirect to <MITMPROXY_TRANSPARENT_PORT>
ip netns exec <MITM_NAMESPACE_NAME> nft add rule nat prerouting iifname <WIFI_IFACE> ip daddr != <MITMPROXY_IP_ADDR> tcp dport 443 redirect to <MITMPROXY_TRANSPARENT_PORT>
ip netns exec <MITM_NAMESPACE_NAME> nft add rule nat prerouting iifname <WIFI_IFACE> udp dport 53 redirect to <MITMPROXY_TRANSPARENT_DNS_PORT>
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=<PATH_TO_MITMPROXY_CA_CERT> \
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 certificates | List 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<Address> 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<Double, Double> 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…