add TCP message injection

This commit is contained in:
Maximilian Hils 2021-03-12 20:46:52 +01:00
parent 5921c590e3
commit c331d2eeb2
5 changed files with 79 additions and 18 deletions

View File

@ -6,8 +6,9 @@ from mitmproxy import command, controller, ctx, flow, http, log, master, options
from mitmproxy.flow import Error from mitmproxy.flow import Error
from mitmproxy.proxy import commands, events from mitmproxy.proxy import commands, events
from mitmproxy.proxy import server from mitmproxy.proxy import server
from mitmproxy.proxy.layers.tcp import TcpMessageInjected
from mitmproxy.proxy.layers.websocket import WebSocketMessageInjected from mitmproxy.proxy.layers.websocket import WebSocketMessageInjected
from mitmproxy.utils import asyncio_utils, human from mitmproxy.utils import asyncio_utils, human, strutils
from wsproto.frame_protocol import Opcode from wsproto.frame_protocol import Opcode
@ -151,24 +152,23 @@ class Proxyserver:
raise ValueError("Flow is not from a live connection.") raise ValueError("Flow is not from a live connection.")
self._connections[flow.client_conn.peername].server_event(event) self._connections[flow.client_conn.peername].server_event(event)
@command.command("inject.text") @command.command("inject")
def inject(self, flows: Sequence[flow.Flow], from_client: bool, message: str): def inject(self, flows: Sequence[flow.Flow], from_client: bool, message: str):
message_bytes = strutils.escaped_str_to_bytes(message)
event: events.MessageInjected
for f in flows: for f in flows:
if isinstance(f, http.HTTPFlow): if isinstance(f, http.HTTPFlow):
if f.websocket: if f.websocket:
event = WebSocketMessageInjected( msg = websocket.WebSocketMessage(Opcode.TEXT, from_client, message_bytes)
f, event = WebSocketMessageInjected(f, msg)
websocket.WebSocketMessage(
Opcode.TEXT, from_client, message.encode()
)
)
else: else:
ctx.log.warn("Cannot inject messages into HTTP connections.") ctx.log.warn("Cannot inject messages into HTTP connections.")
continue continue
elif isinstance(f, tcp.TCPFlow): elif isinstance(f, tcp.TCPFlow):
raise NotImplementedError event = TcpMessageInjected(f, tcp.TCPMessage(from_client, message_bytes))
else: else:
raise NotImplementedError ctx.log.warn(f"Cannot inject message into {f.__class__.__name__}, skipping.")
try: try:
self.inject_event(f, event) self.inject_event(f, event)
except ValueError as e: except ValueError as e:

View File

@ -589,14 +589,22 @@ class HttpLayer(layer.Layer):
yield from self.event_to_child(stream, event) yield from self.event_to_child(stream, event)
elif isinstance(event, events.MessageInjected): elif isinstance(event, events.MessageInjected):
# For injected messages we pass the HTTP stacks entirely and directly address the stream. # For injected messages we pass the HTTP stacks entirely and directly address the stream.
conn = self.connections[event.flow.server_conn] try:
conn = self.connections[event.flow.server_conn]
except KeyError:
# We have a miss for the server connection, which means we're looking at a connection object
# that is tunneled over another connection (for example: over an upstream HTTP proxy).
# We now take the stream associated with the client connection. That won't work for HTTP/2,
# but it's good enough for HTTP/1.
conn = self.connections[event.flow.client_conn]
if isinstance(conn, HttpStream): if isinstance(conn, HttpStream):
stream_id = conn.stream_id stream_id = conn.stream_id
else: else:
# We reach to the end of the connection's child stack to get the HTTP/1 client layer, # We reach to the end of the connection's child stack to get the HTTP/1 client layer,
# which tells us which stream we are dealing with. # which tells us which stream we are dealing with.
conn = conn.context.layers[-1] conn = conn.context.layers[-1]
assert isinstance(conn, Http1Client) assert isinstance(conn, Http1Connection)
assert conn.stream_id
stream_id = conn.stream_id stream_id = conn.stream_id
yield from self.event_to_child(self.streams[stream_id], event) yield from self.event_to_child(self.streams[stream_id], event)
elif isinstance(event, events.ConnectionEvent): elif isinstance(event, events.ConnectionEvent):

View File

@ -6,6 +6,7 @@ from mitmproxy.proxy import commands, events, layer
from mitmproxy.proxy.commands import StartHook from mitmproxy.proxy.commands import StartHook
from mitmproxy.connection import ConnectionState, Connection from mitmproxy.connection import ConnectionState, Connection
from mitmproxy.proxy.context import Context from mitmproxy.proxy.context import Context
from mitmproxy.proxy.events import MessageInjected
from mitmproxy.proxy.utils import expect from mitmproxy.proxy.utils import expect
@ -45,6 +46,12 @@ class TcpErrorHook(StartHook):
flow: tcp.TCPFlow flow: tcp.TCPFlow
class TcpMessageInjected(MessageInjected[tcp.TCPMessage]):
"""
The user has injected a custom TCP message.
"""
class TCPLayer(layer.Layer): class TCPLayer(layer.Layer):
""" """
Simple TCP layer that just relays messages right now. Simple TCP layer that just relays messages right now.
@ -76,8 +83,18 @@ class TCPLayer(layer.Layer):
_handle_event = start _handle_event = start
@expect(events.DataReceived, events.ConnectionClosed) @expect(events.DataReceived, events.ConnectionClosed, TcpMessageInjected)
def relay_messages(self, event: events.ConnectionEvent) -> layer.CommandGenerator[None]: def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]:
if isinstance(event, TcpMessageInjected):
# we just spoof that we received data here and then process that regularly.
event = events.DataReceived(
self.context.client if event.message.from_client else self.context.server,
event.message.content,
)
assert isinstance(event, events.ConnectionEvent)
from_client = event.connection == self.context.client from_client = event.connection == self.context.client
send_to: Connection send_to: Connection
if from_client: if from_client:
@ -110,6 +127,8 @@ class TCPLayer(layer.Layer):
yield TcpEndHook(self.flow) yield TcpEndHook(self.flow)
else: else:
yield commands.CloseConnection(send_to, half_close=True) yield commands.CloseConnection(send_to, half_close=True)
else:
raise AssertionError(f"Unexpected event: {event}")
@expect(events.DataReceived, events.ConnectionClosed) @expect(events.DataReceived, events.ConnectionClosed)
def done(self, _) -> layer.CommandGenerator[None]: def done(self, _) -> layer.CommandGenerator[None]:

View File

@ -9,9 +9,9 @@ from mitmproxy.proxy.commands import CloseConnection, OpenConnection, SendData,
from mitmproxy.connection import ConnectionState, Server from mitmproxy.connection import ConnectionState, Server
from mitmproxy.proxy.events import ConnectionClosed, DataReceived from mitmproxy.proxy.events import ConnectionClosed, DataReceived
from mitmproxy.proxy.layers import TCPLayer, http, tls from mitmproxy.proxy.layers import TCPLayer, http, tls
from mitmproxy.proxy.layers.tcp import TcpStartHook from mitmproxy.proxy.layers.tcp import TcpMessageInjected, TcpStartHook
from mitmproxy.proxy.layers.websocket import WebsocketStartHook from mitmproxy.proxy.layers.websocket import WebsocketStartHook
from mitmproxy.tcp import TCPFlow from mitmproxy.tcp import TCPFlow, TCPMessage
from test.mitmproxy.proxy.tutils import Placeholder, Playbook, reply, reply_next_layer from test.mitmproxy.proxy.tutils import Placeholder, Playbook, reply, reply_next_layer
@ -543,6 +543,7 @@ def test_upstream_proxy(tctx, redirect, scheme):
def test_http_proxy_tcp(tctx, mode, close_first): def test_http_proxy_tcp(tctx, mode, close_first):
"""Test TCP over HTTP CONNECT.""" """Test TCP over HTTP CONNECT."""
server = Placeholder(Server) server = Placeholder(Server)
f = Placeholder(TCPFlow)
if mode == "upstream": if mode == "upstream":
tctx.options.mode = "upstream:http://proxy:8080" tctx.options.mode = "upstream:http://proxy:8080"
@ -558,7 +559,9 @@ def test_http_proxy_tcp(tctx, mode, close_first):
<< SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n")
>> DataReceived(tctx.client, b"this is not http") >> DataReceived(tctx.client, b"this is not http")
<< layer.NextLayerHook(Placeholder()) << layer.NextLayerHook(Placeholder())
>> reply_next_layer(lambda ctx: TCPLayer(ctx, ignore=True)) >> reply_next_layer(lambda ctx: TCPLayer(ctx, ignore=False))
<< TcpStartHook(f)
>> reply()
<< OpenConnection(server) << OpenConnection(server)
) )
@ -579,6 +582,12 @@ def test_http_proxy_tcp(tctx, mode, close_first):
else: else:
assert server().address == ("proxy", 8080) assert server().address == ("proxy", 8080)
assert (
playbook
>> TcpMessageInjected(f, TCPMessage(False, b"fake news from your friendly man-in-the-middle"))
<< SendData(tctx.client, b"fake news from your friendly man-in-the-middle")
)
if close_first == "client": if close_first == "client":
a, b = tctx.client, server a, b = tctx.client, server
else: else:

View File

@ -4,7 +4,8 @@ from mitmproxy.proxy.commands import CloseConnection, OpenConnection, SendData
from mitmproxy.connection import ConnectionState from mitmproxy.connection import ConnectionState
from mitmproxy.proxy.events import ConnectionClosed, DataReceived from mitmproxy.proxy.events import ConnectionClosed, DataReceived
from mitmproxy.proxy.layers import tcp from mitmproxy.proxy.layers import tcp
from mitmproxy.tcp import TCPFlow from mitmproxy.proxy.layers.tcp import TcpMessageInjected
from mitmproxy.tcp import TCPFlow, TCPMessage
from ..tutils import Placeholder, Playbook, reply from ..tutils import Placeholder, Playbook, reply
@ -122,3 +123,27 @@ def test_ignore(tctx, ignore):
else: else:
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
no_flow_hooks() no_flow_hooks()
def test_inject(tctx):
"""inject data into an open connection."""
f = Placeholder(TCPFlow)
assert (
Playbook(tcp.TCPLayer(tctx))
<< tcp.TcpStartHook(f)
>> TcpMessageInjected(f, TCPMessage(True, b"hello!"))
>> reply(to=-2)
<< OpenConnection(tctx.server)
>> reply(None)
<< tcp.TcpMessageHook(f)
>> reply()
<< SendData(tctx.server, b"hello!")
# and the other way...
>> TcpMessageInjected(f, TCPMessage(False, b"I have already done the greeting for you."))
<< tcp.TcpMessageHook(f)
>> reply()
<< SendData(tctx.client, b"I have already done the greeting for you.")
<< None
)
assert len(f().messages) == 2