diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index eb24bed7a..08639f6db 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -370,6 +370,21 @@ def common_options(parser): default=None, help="Forward all requests to upstream proxy server: http://host[:port]" ) + group.add_argument( + "--spoof", + action="store_true", dest="spoof_mode", default=False, + help="Use Host header to connect to HTTP servers." + ) + group.add_argument( + "--ssl-spoof", + action="store_true", dest="ssl_spoof_mode", default=False, + help="Use TLS SNI to connect to HTTPS servers." + ) + group.add_argument( + "--spoofed-port", + action="store", dest="spoofed_ssl_port", type=int, default=443, + help="Port number of upstream HTTPS servers in SSL spoof mode." + ) group = parser.add_argument_group( "Advanced Proxy Options", diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 9c1433865..9bce72063 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -1328,8 +1328,34 @@ class HTTPHandler(ProtocolHandler): # value at flow.server_conn self.c.set_server_address((request.host, request.port)) flow.server_conn = self.c.server_conn - + + elif request.form_in == "relative": + if self.c.config.mode == "spoof": + # Host header + h = request.pretty_host(hostheader=True) + if h is None: + raise http.HttpError( + 400, + "Invalid request: No host information" + ) + p = http.parse_url("http://" + h) + request.scheme = p[0] + request.host = p[1] + request.port = p[2] + self.c.set_server_address((request.host, request.port)) + flow.server_conn = self.c.server_conn + + if self.c.config.mode == "sslspoof": + # SNI is processed in server.py + if not (flow.server_conn and flow.server_conn.ssl_established): + print ":::::::::::::::" + raise http.HttpError( + 400, + "Invalid request: No host information" + ) + return None + raise http.HttpError( 400, "Invalid HTTP request form (expected: %s, got: %s)" % ( self.expected_form_in, request.form_in diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index 3f5796694..07dc5c892 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -4,7 +4,7 @@ import re from OpenSSL import SSL from netlib import http_auth, certutils, tcp from .. import utils, platform, version -from .primitives import RegularProxyMode, TransparentProxyMode, UpstreamProxyMode, ReverseProxyMode, Socks5ProxyMode +from .primitives import RegularProxyMode, SpoofMode, SSLSpoofMode, TransparentProxyMode, UpstreamProxyMode, ReverseProxyMode, Socks5ProxyMode TRANSPARENT_SSL_PORTS = [443, 8443] CONF_BASENAME = "mitmproxy" @@ -51,7 +51,8 @@ class ProxyConfig: certforward=False, ssl_version_client="secure", ssl_version_server="secure", - ssl_ports=TRANSPARENT_SSL_PORTS + ssl_ports=TRANSPARENT_SSL_PORTS, + spoofed_ssl_port=None ): self.host = host self.port = port @@ -70,6 +71,10 @@ class ProxyConfig: self.mode = ReverseProxyMode(upstream_server) elif mode == "upstream": self.mode = UpstreamProxyMode(upstream_server) + elif mode == "spoof": + self.mode = SpoofMode() + elif mode == "sslspoof": + self.mode = SSLSpoofMode(spoofed_ssl_port) else: self.mode = RegularProxyMode() @@ -126,7 +131,7 @@ def process_proxy_options(parser, options): body_size_limit = utils.parse_size(options.body_size_limit) c = 0 - mode, upstream_server = None, None + mode, upstream_server, spoofed_ssl_port = None, None, None if options.transparent_proxy: c += 1 if not platform.resolver: @@ -144,6 +149,13 @@ def process_proxy_options(parser, options): c += 1 mode = "upstream" upstream_server = options.upstream_proxy + if options.spoof_mode: + c += 1 + mode = "spoof" + if options.ssl_spoof_mode: + c += 1 + mode = "sslspoof" + spoofed_ssl_port = options.spoofed_ssl_port if c > 1: return parser.error( "Transparent, SOCKS5, reverse and upstream proxy mode " @@ -213,7 +225,8 @@ def process_proxy_options(parser, options): certforward=options.certforward, ssl_version_client=options.ssl_version_client, ssl_version_server=options.ssl_version_server, - ssl_ports=ssl_ports + ssl_ports=ssl_ports, + spoofed_ssl_port=spoofed_ssl_port ) diff --git a/libmproxy/proxy/primitives.py b/libmproxy/proxy/primitives.py index 9e7dae9af..b01ddde32 100644 --- a/libmproxy/proxy/primitives.py +++ b/libmproxy/proxy/primitives.py @@ -51,6 +51,33 @@ class RegularProxyMode(ProxyMode): return None +class SpoofMode(ProxyMode): + http_form_in = "relative" + http_form_out = "relative" + + def get_upstream_server(self, client_conn): + return None + + @property + def name(self): + return "spoof" + + +class SSLSpoofMode(ProxyMode): + http_form_in = "relative" + http_form_out = "relative" + + def __init__(self, sslport): + self.sslport = sslport + + def get_upstream_server(self, client_conn): + return None + + @property + def name(self): + return "sslspoof" + + class TransparentProxyMode(ProxyMode): http_form_in = "relative" http_form_out = "relative" diff --git a/libmproxy/proxy/server.py b/libmproxy/proxy/server.py index e1587df17..71704413a 100644 --- a/libmproxy/proxy/server.py +++ b/libmproxy/proxy/server.py @@ -117,6 +117,16 @@ class ConnectionHandler: self.server_conn.address(), "info") self.conntype = "tcp" + + elif not self.server_conn and self.config.mode == "sslspoof": + port = self.config.mode.sslport + self.set_server_address(("-", port)) + self.establish_ssl(client=True) + host = self.client_conn.connection.get_servername() + if host: + self.set_server_address((host, port)) + self.establish_server_connection() + self.establish_ssl(server=True, sni=host) # Delegate handling to the protocol handler protocol_handler( @@ -308,6 +318,9 @@ class ConnectionHandler: host = upstream_cert.cn.decode("utf8").encode("idna") if self.server_conn.sni: sans.append(self.server_conn.sni) + # for ssl spoof mode + if hasattr(self.client_conn, "sni"): + sans.append(self.client_conn.sni) ret = self.config.certstore.get_cert(host, sans) if not ret: @@ -325,6 +338,8 @@ class ConnectionHandler: if not sn: return sni = sn.decode("utf8").encode("idna") + # for ssl spoof mode + self.client_conn.sni = sni if sni != self.server_conn.sni: self.log("SNI received: %s" % sni, "debug") diff --git a/test/test_proxy.py b/test/test_proxy.py index a618ae6cc..d1e72f75c 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -90,6 +90,12 @@ class TestProcessProxyOptions: self.assert_err("expected one argument", "-U") self.assert_err("Invalid server specification", "-U", "upstream") + self.assert_noerr("--spoof") + self.assert_noerr("--ssl-spoof") + + self.assert_noerr("--spoofed-port", "443") + self.assert_err("expected one argument", "--spoofed-port") + self.assert_err("mutually exclusive", "-R", "http://localhost", "-T") def test_client_certs(self): diff --git a/test/test_server.py b/test/test_server.py index 2ab48422b..07b8a5f23 100644 --- a/test/test_server.py +++ b/test/test_server.py @@ -368,6 +368,55 @@ class TestReverse(tservers.ReverseProxTest, CommonMixin, TcpMixin): reverse = True +class TestSpoof(tservers.SpoofModeTest): + def test_http(self): + alist = ( + ("localhost", self.server.port), + ("127.0.0.1", self.server.port) + ) + for a in alist: + self.server.clear_log() + p = self.pathoc() + f = p.request("get:/p/304:h'Host'='%s:%s'" % a) + assert self.server.last_log() + assert f.status_code == 304 + l = self.master.state.view[-1] + assert l.server_conn.address + assert l.server_conn.address.host == a[0] + assert l.server_conn.address.port == a[1] + + def test_http_without_host(self): + p = self.pathoc() + f = p.request("get:/p/304:r") + assert f.status_code == 400 + + +class TestSSLSpoof(tservers.SSLSpoofModeTest): + def test_https(self): + alist = ( + ("localhost", self.server.port), + ("127.0.0.1", self.server.port) + ) + for a in alist: + self.server.clear_log() + self.config.mode.sslport = a[1] + p = self.pathoc(sni=a[0]) + f = p.request("get:/p/304") + assert self.server.last_log() + assert f.status_code == 304 + l = self.master.state.view[-1] + assert l.server_conn.address + assert l.server_conn.address.host == a[0] + assert l.server_conn.address.port == a[1] + + def test_https_without_sni(self): + a = ("localhost", self.server.port) + self.config.mode.sslport = a[1] + p = self.pathoc(sni=None) + f = p.request("get:/p/304") + assert f.status_code == 400 + + class TestHttps2Http(tservers.ReverseProxTest): @classmethod def get_proxy_config(cls): diff --git a/test/tservers.py b/test/tservers.py index dc14fb37c..c70ad68a0 100644 --- a/test/tservers.py +++ b/test/tservers.py @@ -270,6 +270,49 @@ class ReverseProxTest(ProxTestBase): return p.request(q) +class SpoofModeTest(ProxTestBase): + ssl = None + + @classmethod + def get_proxy_config(cls): + d = ProxTestBase.get_proxy_config() + d["upstream_server"] = None + d["mode"] = "spoof" + return d + + def pathoc(self, sni=None): + """ + Returns a connected Pathoc instance. + """ + p = libpathod.pathoc.Pathoc( + ("localhost", self.proxy.port), ssl=self.ssl, sni=sni, fp=None + ) + p.connect() + return p + + +class SSLSpoofModeTest(ProxTestBase): + ssl = True + + @classmethod + def get_proxy_config(cls): + d = ProxTestBase.get_proxy_config() + d["upstream_server"] = None + d["mode"] = "sslspoof" + d["spoofed_ssl_port"] = 443 + return d + + def pathoc(self, sni=None): + """ + Returns a connected Pathoc instance. + """ + p = libpathod.pathoc.Pathoc( + ("localhost", self.proxy.port), ssl=self.ssl, sni=sni, fp=None + ) + p.connect() + return p + + class ChainProxTest(ProxTestBase): """ Chain three instances of mitmproxy in a row to test upstream mode.