dpyproxy 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dpyproxy/__init__.py +1 -0
- dpyproxy/__main__.py +4 -0
- dpyproxy-2.2.0.dist-info/METADATA +296 -0
- dpyproxy-2.2.0.dist-info/RECORD +59 -0
- dpyproxy-2.2.0.dist-info/WHEEL +4 -0
- dpyproxy-2.2.0.dist-info/entry_points.txt +2 -0
- dpyproxy-2.2.0.dist-info/licenses/LICENSE +201 -0
- enumerators/DnsProxyMode.py +39 -0
- enumerators/DnsResolvers.py +141 -0
- enumerators/HttpMethod.py +17 -0
- enumerators/Modules.py +38 -0
- enumerators/Port.py +11 -0
- enumerators/TcpProxyMode.py +17 -0
- enumerators/TlsVersion.py +21 -0
- enumerators/__init__.py +0 -0
- exception/DnsException.py +7 -0
- exception/ParserException.py +7 -0
- exception/__init__.py +0 -0
- main.py +94 -0
- modules/Module.py +45 -0
- modules/__init__.py +0 -0
- modules/dns/DnsModeDeterminator.py +358 -0
- modules/dns/DnsModule.py +113 -0
- modules/dns/DnsProxy.py +277 -0
- modules/dns/DnsResolver.py +18 -0
- modules/dns/__init__.py +0 -0
- modules/http/HttpModule.py +69 -0
- modules/http/HttpStrategies.py +849 -0
- modules/http/HttpUtils.py +94 -0
- modules/http/__init__.py +0 -0
- modules/tls/TcpProxy.py +106 -0
- modules/tls/TlsModule.py +173 -0
- modules/tls/__init__.py +0 -0
- network/DomainResolver.py +472 -0
- network/NetworkAddress.py +10 -0
- network/WrappedSocket.py +97 -0
- network/__init__.py +0 -0
- network/protocols/Dns.py +62 -0
- network/protocols/Http.py +109 -0
- network/protocols/Socksv4.py +70 -0
- network/protocols/Socksv5.py +106 -0
- network/protocols/Tls.py +113 -0
- network/protocols/__init__.py +0 -0
- network/tcp/Forwarder.py +203 -0
- network/tcp/TcpConnectionHandler.py +264 -0
- network/tcp/WrappedTcpSocket.py +30 -0
- network/tcp/__init__.py +0 -0
- network/udp/__init__.py +0 -0
- test/Sink.py +23 -0
- test/__init__.py +0 -0
- test/test_dns.py +98 -0
- test/test_http.py +57 -0
- test/test_tls.py +63 -0
- util/DnsAutoModeRuntimeMeasurement.py +62 -0
- util/DnsReachabilityCollector.py +160 -0
- util/DnsResolversDomainResolver.py +36 -0
- util/Util.py +62 -0
- util/__init__.py +0 -0
- util/constants.py +8 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from enumerators.HttpMethod import HttpMethod
|
|
2
|
+
from exception.ParserException import ParserException
|
|
3
|
+
from network.NetworkAddress import NetworkAddress
|
|
4
|
+
from network.tcp.WrappedTcpSocket import WrappedTcpSocket
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Http:
|
|
8
|
+
"""
|
|
9
|
+
Implements methods to parse HTTP messages.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def is_valid_http(wrapped_socket: WrappedTcpSocket) -> bool:
|
|
14
|
+
try:
|
|
15
|
+
Http._parse_http_method(wrapped_socket, HttpMethod.all(), True)
|
|
16
|
+
return True
|
|
17
|
+
except ParserException:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def _parse_http_method(wrapped_socket: WrappedTcpSocket, methods: list[str], peek: bool = False) -> (str, int, str):
|
|
22
|
+
"""
|
|
23
|
+
Reads the first line of a http method to parse the domain from it.
|
|
24
|
+
:return: host and port in the method, defaults to port 80 if no port found
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
# read complete message
|
|
28
|
+
message = wrapped_socket.read_until([b"\n\n", b"\r\n\r\n"], 500, peek).decode("ASCII")
|
|
29
|
+
# extract first line, assume that \r\n are at least used coherently in the message
|
|
30
|
+
if "\r\n" in message:
|
|
31
|
+
first_line = message.split("\r\n")[0]
|
|
32
|
+
else:
|
|
33
|
+
first_line = message.split("\n")[0]
|
|
34
|
+
except UnicodeDecodeError as e:
|
|
35
|
+
raise ParserException(f"Could not decode ASCII in first line of HTTP request with exception {e}")
|
|
36
|
+
# check for any method
|
|
37
|
+
parsed_method = False
|
|
38
|
+
for method in methods:
|
|
39
|
+
if first_line.upper().startswith(f"{method.upper()} "):
|
|
40
|
+
parsed_method = True
|
|
41
|
+
if not parsed_method:
|
|
42
|
+
raise ParserException(f"Not a {methods} request")
|
|
43
|
+
if first_line.count(" ") not in [1, 2]:
|
|
44
|
+
raise ParserException("Not a valid HTTP request, could not determine target URI")
|
|
45
|
+
if first_line.count(" ") == 1:
|
|
46
|
+
# HTTP/0.9
|
|
47
|
+
_, uri = first_line.split(" ")
|
|
48
|
+
version = "HTTP/0.9"
|
|
49
|
+
else:
|
|
50
|
+
_, uri, version = first_line.split(" ")
|
|
51
|
+
if version.upper() != "HTTP/1.1" and version.upper() != "HTTP/1.0" and version.upper() != "HTTP/0.9":
|
|
52
|
+
raise ParserException("Not a valid HTTP request, only HTTP/0.9, HTTP/1.0, and HTTP/1.1 supported")
|
|
53
|
+
host, _, port = Http.parse_uri(uri)
|
|
54
|
+
return host, port, version
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def read_http_get(wrapped_socket: WrappedTcpSocket) -> (str, int, str):
|
|
58
|
+
"""
|
|
59
|
+
Reads the first line of a http get request.
|
|
60
|
+
:return: host, port, and version from the http get request.
|
|
61
|
+
"""
|
|
62
|
+
return Http._parse_http_method(wrapped_socket, ["GET"], True)
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def read_http_connect(wrapped_socket: WrappedTcpSocket) -> (str, int, str):
|
|
66
|
+
"""
|
|
67
|
+
Reads the first line of a http connect request.
|
|
68
|
+
:return: host, port, and http version from the http connect request.
|
|
69
|
+
"""
|
|
70
|
+
return Http._parse_http_method(wrapped_socket, ["CONNECT"], False)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def parse_uri(uri: str) -> (str, str, int):
|
|
74
|
+
"""
|
|
75
|
+
Parses a URI into its host, path and port components.
|
|
76
|
+
Parameters are currently not parsed.
|
|
77
|
+
:param uri: uri to parse
|
|
78
|
+
:return: host, path, port
|
|
79
|
+
"""
|
|
80
|
+
if "://" in uri:
|
|
81
|
+
# remove protocol prefix if present
|
|
82
|
+
uri = uri.split("://")[1]
|
|
83
|
+
|
|
84
|
+
if "/" in uri:
|
|
85
|
+
# split host and path
|
|
86
|
+
uri, path = uri.split("/", 1)
|
|
87
|
+
path = "/" + path
|
|
88
|
+
else:
|
|
89
|
+
path = "/"
|
|
90
|
+
|
|
91
|
+
if ":" in uri:
|
|
92
|
+
# split host and port
|
|
93
|
+
uri, port = uri.split(":")
|
|
94
|
+
port = int(port)
|
|
95
|
+
else:
|
|
96
|
+
port = 80
|
|
97
|
+
|
|
98
|
+
return uri, path, port
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def connect_message(server_address: NetworkAddress, version: str) -> bytes:
|
|
102
|
+
return (
|
|
103
|
+
f"CONNECT {server_address.host}:{server_address.port} {version}\n"
|
|
104
|
+
f"Host: {server_address.host}:{server_address.port}\n\n".encode("ASCII")
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def http_200_ok(version: str) -> bytes:
|
|
109
|
+
return f"{version} 200 OK\n\n".encode("ASCII")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from exception.ParserException import ParserException
|
|
2
|
+
from network.NetworkAddress import NetworkAddress
|
|
3
|
+
from network.tcp.WrappedTcpSocket import WrappedTcpSocket
|
|
4
|
+
from util.constants import SOCKSv4_HEADER
|
|
5
|
+
from util.Util import is_valid_ipv4_address
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Socksv4:
|
|
9
|
+
"""
|
|
10
|
+
Implements SOCKSv4a protocol
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
REQUEST_MODE = b"\01"
|
|
14
|
+
BIND_MODE = b"\02"
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def read_socks4(connection_socket: WrappedTcpSocket) -> tuple[str, int]:
|
|
18
|
+
|
|
19
|
+
version = connection_socket.recv(1)
|
|
20
|
+
if version != SOCKSv4_HEADER:
|
|
21
|
+
raise ParserException("Not a SOCKSv4 request")
|
|
22
|
+
|
|
23
|
+
mode = connection_socket.recv(1)
|
|
24
|
+
if mode == Socksv4.BIND_MODE:
|
|
25
|
+
raise ParserException("BIND mode not supported")
|
|
26
|
+
if mode != Socksv4.REQUEST_MODE:
|
|
27
|
+
raise ParserException(f"Socks mode {mode} not supported")
|
|
28
|
+
|
|
29
|
+
port = int.from_bytes(connection_socket.recv(2), byteorder="big")
|
|
30
|
+
if not 0 <= port <= 65535:
|
|
31
|
+
raise ParserException(f"Invalid port {port}")
|
|
32
|
+
|
|
33
|
+
# str ip from bytes
|
|
34
|
+
ip = ".".join(f"{c}" for c in connection_socket.recv(4))
|
|
35
|
+
|
|
36
|
+
# ignore user_id but parse bytes until null byte
|
|
37
|
+
connection_socket.read_until([b"\x00"])
|
|
38
|
+
|
|
39
|
+
# socksv4a allows for domain/ip? behind user id
|
|
40
|
+
if ip.startswith("0.0.0."):
|
|
41
|
+
ip = connection_socket.read_until([b"\x00"])[:-1].decode("utf-8")
|
|
42
|
+
|
|
43
|
+
return ip, port
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def socks4_request(server_address: NetworkAddress):
|
|
47
|
+
if not is_valid_ipv4_address(server_address.host):
|
|
48
|
+
# Socksv4a domain encoding
|
|
49
|
+
return (
|
|
50
|
+
SOCKSv4_HEADER
|
|
51
|
+
+ b"\x01"
|
|
52
|
+
+ server_address.port.to_bytes(2, byteorder="big")
|
|
53
|
+
+ "0.0.0.1".encode("'utf-8")
|
|
54
|
+
+ b"\x00"
|
|
55
|
+
+ server_address.host.encode("utf-8")
|
|
56
|
+
+ b"\x00"
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
# Socksv4 ip encoding
|
|
60
|
+
return (
|
|
61
|
+
SOCKSv4_HEADER
|
|
62
|
+
+ b"\x01"
|
|
63
|
+
+ server_address.port.to_bytes(2, byteorder="big")
|
|
64
|
+
+ bytes([int(i) for i in server_address.host.split(".")])
|
|
65
|
+
+ b"\x00"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def socks4_ok() -> bytes:
|
|
70
|
+
return b"\00\x5a\xff\xff\xff\xff\xff\xff"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
|
|
3
|
+
from exception.ParserException import ParserException
|
|
4
|
+
from network.NetworkAddress import NetworkAddress
|
|
5
|
+
from network.tcp.WrappedTcpSocket import WrappedTcpSocket
|
|
6
|
+
from util.constants import SOCKSv5_HEADER
|
|
7
|
+
from util.Util import is_valid_ipv4_address
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Socksv5:
|
|
11
|
+
"""
|
|
12
|
+
Implements SOCKSv5 protocol, only supports no auth
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
REQUEST_MODE = b"\x01"
|
|
16
|
+
BIND_MODE = b"\x02"
|
|
17
|
+
UDP_PORT = b"\x03"
|
|
18
|
+
RESERVED_BYTE = b"\x00"
|
|
19
|
+
NO_AUTH = b"\x00"
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def read_socks5(connection_socket: WrappedTcpSocket) -> tuple[str, int]:
|
|
23
|
+
|
|
24
|
+
# read connection methods
|
|
25
|
+
version = connection_socket.recv(1)
|
|
26
|
+
if version != SOCKSv5_HEADER:
|
|
27
|
+
raise ParserException("Not a SOCKSv5 request")
|
|
28
|
+
|
|
29
|
+
number_authentication_methods = int.from_bytes(connection_socket.recv(1), byteorder="big")
|
|
30
|
+
if number_authentication_methods == 0:
|
|
31
|
+
connection_socket.send(SOCKSv5_HEADER + b"\xff")
|
|
32
|
+
raise ParserException("No auth method provided")
|
|
33
|
+
|
|
34
|
+
authentication_methods = connection_socket.read(number_authentication_methods)
|
|
35
|
+
if Socksv5.NO_AUTH not in authentication_methods:
|
|
36
|
+
connection_socket.send(SOCKSv5_HEADER + b"\xff")
|
|
37
|
+
raise ParserException("No auth method not supported by client")
|
|
38
|
+
|
|
39
|
+
# always choose no auth
|
|
40
|
+
connection_socket.send(SOCKSv5_HEADER + Socksv5.NO_AUTH)
|
|
41
|
+
|
|
42
|
+
# receive destination address
|
|
43
|
+
version = connection_socket.recv(1)
|
|
44
|
+
if version != SOCKSv5_HEADER:
|
|
45
|
+
raise ParserException("Not a SOCKSv5 request")
|
|
46
|
+
|
|
47
|
+
mode = connection_socket.recv(1)
|
|
48
|
+
if mode == Socksv5.BIND_MODE:
|
|
49
|
+
raise ParserException("BIND mode not supported")
|
|
50
|
+
if mode == Socksv5.UDP_PORT:
|
|
51
|
+
raise ParserException("UDP mode not supported")
|
|
52
|
+
if mode != Socksv5.REQUEST_MODE:
|
|
53
|
+
raise ParserException(f"Socks mode {mode} not supported")
|
|
54
|
+
|
|
55
|
+
if connection_socket.recv(1) != Socksv5.RESERVED_BYTE:
|
|
56
|
+
raise ParserException("Invalid reserved byte")
|
|
57
|
+
|
|
58
|
+
host = Socksv5._read_address(connection_socket)
|
|
59
|
+
|
|
60
|
+
port = int.from_bytes(connection_socket.read(2), byteorder="big")
|
|
61
|
+
if not 0 <= port <= 65535:
|
|
62
|
+
raise ParserException(f"Invalid port {port}")
|
|
63
|
+
|
|
64
|
+
return host, port
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _read_address(connection_socket: WrappedTcpSocket) -> str:
|
|
68
|
+
address_type = connection_socket.recv(1)
|
|
69
|
+
if address_type == b"\x01":
|
|
70
|
+
# ipv4
|
|
71
|
+
host = ".".join(f"{c}" for c in connection_socket.read(4))
|
|
72
|
+
elif address_type == b"\x04":
|
|
73
|
+
# ipv6
|
|
74
|
+
host = ":".join(f"{c}" for c in connection_socket.read(16))
|
|
75
|
+
elif address_type == b"\x03":
|
|
76
|
+
# domain
|
|
77
|
+
length = int.from_bytes(connection_socket.read(1), byteorder="big")
|
|
78
|
+
host = connection_socket.read(length).decode("utf-8")
|
|
79
|
+
else:
|
|
80
|
+
raise ParserException(f"Address type {address_type} not supported")
|
|
81
|
+
return host
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def socks5_auth_methods() -> bytes:
|
|
85
|
+
return SOCKSv5_HEADER + b"\x01" + Socksv5.NO_AUTH
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def socks5_request(server_address: NetworkAddress):
|
|
89
|
+
if not is_valid_ipv4_address(server_address.host):
|
|
90
|
+
domain = server_address.host.encode("utf-8")
|
|
91
|
+
address = b"\x03" + len(domain).to_bytes() + domain
|
|
92
|
+
else:
|
|
93
|
+
address = b"\x01" + socket.inet_aton(server_address.host)
|
|
94
|
+
return (
|
|
95
|
+
SOCKSv5_HEADER
|
|
96
|
+
+ b"\x01"
|
|
97
|
+
+ Socksv5.RESERVED_BYTE
|
|
98
|
+
+ address
|
|
99
|
+
+ server_address.port.to_bytes(2, byteorder="big")
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def socks5_ok(connection_socket: WrappedTcpSocket) -> bytes:
|
|
104
|
+
host_ip, host_port = connection_socket.socket.getsockname()
|
|
105
|
+
host_address = b"\x01" + socket.inet_aton(host_ip)
|
|
106
|
+
return SOCKSv5_HEADER + b"\x00" + Socksv5.RESERVED_BYTE + host_address + host_port.to_bytes(2, byteorder="big")
|
network/protocols/Tls.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from time import time
|
|
2
|
+
|
|
3
|
+
from exception.ParserException import ParserException
|
|
4
|
+
from network.tcp.WrappedTcpSocket import WrappedTcpSocket
|
|
5
|
+
from util.constants import TLS_1_0_HEADER, TLS_1_1_HEADER, TLS_1_2_HEADER
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Tls:
|
|
9
|
+
@staticmethod
|
|
10
|
+
def read_sni(wrapped_socket: WrappedTcpSocket, timeout: int) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Attempts to read the host from the SNI extension. If the client does not send a SNI extension, None is returned.
|
|
13
|
+
:return: host of the sni extension
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
tls_message = Tls._read_tls_message(wrapped_socket, peek=True, timeout=timeout)
|
|
18
|
+
except ParserException as e:
|
|
19
|
+
raise e
|
|
20
|
+
except Exception as e:
|
|
21
|
+
raise ParserException(e)
|
|
22
|
+
|
|
23
|
+
# check if record is a client hello
|
|
24
|
+
if tls_message[0] != 0x01:
|
|
25
|
+
raise ParserException("Not a client hello")
|
|
26
|
+
|
|
27
|
+
# skip everything until SNI extension
|
|
28
|
+
p = 38
|
|
29
|
+
# session_id
|
|
30
|
+
p += 1 + int.from_bytes(tls_message[p : p + 1], byteorder="big")
|
|
31
|
+
# cipher suites
|
|
32
|
+
p += 2 + int.from_bytes(tls_message[p : p + 2], byteorder="big")
|
|
33
|
+
# compression methods
|
|
34
|
+
p += 1 + int.from_bytes(tls_message[p : p + 1], byteorder="big")
|
|
35
|
+
|
|
36
|
+
if p >= len(tls_message):
|
|
37
|
+
raise ParserException("No extensions present")
|
|
38
|
+
|
|
39
|
+
# extensions
|
|
40
|
+
p += 2
|
|
41
|
+
|
|
42
|
+
while p < len(tls_message):
|
|
43
|
+
ext_type = int.from_bytes(tls_message[p : p + 2], byteorder="big")
|
|
44
|
+
p += 2
|
|
45
|
+
ext_length = int.from_bytes(tls_message[p : p + 2], byteorder="big")
|
|
46
|
+
p += 2
|
|
47
|
+
|
|
48
|
+
if ext_type != 0:
|
|
49
|
+
# skip over not sni
|
|
50
|
+
p += ext_length
|
|
51
|
+
else:
|
|
52
|
+
# sni
|
|
53
|
+
list_len = int.from_bytes(tls_message[p : p + 2], byteorder="big")
|
|
54
|
+
p += 2
|
|
55
|
+
_list_len = p + list_len
|
|
56
|
+
while p < _list_len:
|
|
57
|
+
name_type = int.from_bytes(tls_message[p : p + 1], byteorder="big")
|
|
58
|
+
p += 1
|
|
59
|
+
name_len = int.from_bytes(tls_message[p : p + 2], byteorder="big")
|
|
60
|
+
p += 2
|
|
61
|
+
if name_type != 0:
|
|
62
|
+
# unknown name type, skip
|
|
63
|
+
p += name_len
|
|
64
|
+
else:
|
|
65
|
+
# hostname
|
|
66
|
+
hostname = tls_message[p : p + name_len]
|
|
67
|
+
return hostname.decode("ASCII")
|
|
68
|
+
raise ParserException("No SNI present")
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _read_tls_message(wrapped_socket: WrappedTcpSocket, timeout: int, peek=False) -> bytes:
|
|
72
|
+
"""
|
|
73
|
+
Reads the content of the next tls message from the socket.
|
|
74
|
+
:param: whether to peek the tls message.
|
|
75
|
+
:return: The content of the TLS message
|
|
76
|
+
"""
|
|
77
|
+
message = b""
|
|
78
|
+
buffer = b""
|
|
79
|
+
# headers
|
|
80
|
+
len_to_read = 4
|
|
81
|
+
# prevent infinite sockets
|
|
82
|
+
timestamp = time()
|
|
83
|
+
# parse records until message complete
|
|
84
|
+
while len(message) < len_to_read and int(time() - timestamp) < timeout:
|
|
85
|
+
record = Tls._read_tls_record(wrapped_socket)
|
|
86
|
+
buffer += record
|
|
87
|
+
message += record[5:]
|
|
88
|
+
if len(message) >= 4:
|
|
89
|
+
# can parse message length
|
|
90
|
+
len_to_read = int.from_bytes(message[1:4], byteorder="big")
|
|
91
|
+
if peek:
|
|
92
|
+
# re-inject all read records
|
|
93
|
+
wrapped_socket.inject(buffer)
|
|
94
|
+
return message
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _read_tls_record(
|
|
98
|
+
wrapped_socket: WrappedTcpSocket,
|
|
99
|
+
) -> bytes:
|
|
100
|
+
"""
|
|
101
|
+
Reads the content of the next tls record from the wire with headers. Throws exception if no record is received.
|
|
102
|
+
:return: The contents of the TLS record
|
|
103
|
+
"""
|
|
104
|
+
# read record header
|
|
105
|
+
data = wrapped_socket.read(5)
|
|
106
|
+
|
|
107
|
+
# check if first 3 bytes are a tls header
|
|
108
|
+
if data[:3] != TLS_1_0_HEADER and data[:3] != TLS_1_1_HEADER and data[:3] != TLS_1_2_HEADER:
|
|
109
|
+
raise ParserException("Not a TLS connection")
|
|
110
|
+
|
|
111
|
+
# read record length
|
|
112
|
+
record_length = int.from_bytes(data[3:5], byteorder="big")
|
|
113
|
+
return data + wrapped_socket.read(record_length)
|
|
File without changes
|
network/tcp/Forwarder.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
from enumerators.TlsVersion import TlsVersion
|
|
5
|
+
from modules.http.HttpStrategies import HttpStrategies
|
|
6
|
+
from network.protocols.Http import Http
|
|
7
|
+
from network.tcp.WrappedTcpSocket import WrappedTcpSocket
|
|
8
|
+
from util.constants import STANDARD_SOCKET_RECEIVE_SIZE, TLS_1_0_HEADER, TLS_1_1_HEADER, TLS_1_2_HEADER
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Forwarder:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
socket1: WrappedTcpSocket,
|
|
15
|
+
socket1_name: str,
|
|
16
|
+
socket2: WrappedTcpSocket,
|
|
17
|
+
socket2_name: str,
|
|
18
|
+
record_frag: bool = False,
|
|
19
|
+
record_version: str = TlsVersion.DEFAULT.value,
|
|
20
|
+
frag_size: int = 0,
|
|
21
|
+
http_strategy=None,
|
|
22
|
+
http_smuggling_uncensored_url: str = "",
|
|
23
|
+
):
|
|
24
|
+
self.socket1 = socket1
|
|
25
|
+
self.socket2 = socket2
|
|
26
|
+
self.socket1_name = socket1_name
|
|
27
|
+
self.socket2_name = socket2_name
|
|
28
|
+
self.record_frag = record_frag
|
|
29
|
+
self.record_version = record_version
|
|
30
|
+
self.frag_size = frag_size
|
|
31
|
+
self.http_strategy = http_strategy
|
|
32
|
+
self.http_smuggling_uncensored_url = http_smuggling_uncensored_url
|
|
33
|
+
|
|
34
|
+
def start(self):
|
|
35
|
+
# start sending thread
|
|
36
|
+
threading.Thread(
|
|
37
|
+
target=self._forward,
|
|
38
|
+
args=(
|
|
39
|
+
self.socket1,
|
|
40
|
+
self.socket2,
|
|
41
|
+
f"{self.socket1_name}->{self.socket2_name}",
|
|
42
|
+
self.record_frag,
|
|
43
|
+
self.record_version,
|
|
44
|
+
self.http_strategy,
|
|
45
|
+
),
|
|
46
|
+
).start()
|
|
47
|
+
# start receiving thread
|
|
48
|
+
threading.Thread(
|
|
49
|
+
target=self._forward,
|
|
50
|
+
args=(
|
|
51
|
+
self.socket2,
|
|
52
|
+
self.socket1,
|
|
53
|
+
f"{self.socket2_name}->{self.socket1_name}",
|
|
54
|
+
),
|
|
55
|
+
).start()
|
|
56
|
+
|
|
57
|
+
def _forward(
|
|
58
|
+
self,
|
|
59
|
+
from_socket: WrappedTcpSocket,
|
|
60
|
+
to_socket: WrappedTcpSocket,
|
|
61
|
+
direction: str,
|
|
62
|
+
record_frag=False,
|
|
63
|
+
record_version: str = TlsVersion.DEFAULT.value,
|
|
64
|
+
http_strategy=None,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Forwards data between two sockets with optional record manipulation. Falls back to forwarding if no TLS records
|
|
68
|
+
can be parsed from the connection anymore.
|
|
69
|
+
:param to_socket: Socket to receive data from.
|
|
70
|
+
:param from_socket: Socket to forward data to.
|
|
71
|
+
:param record_frag: Whether to fragment handshake records
|
|
72
|
+
:return: None
|
|
73
|
+
"""
|
|
74
|
+
alter_tls = record_frag or (record_version != TlsVersion.DEFAULT.value)
|
|
75
|
+
try:
|
|
76
|
+
while True:
|
|
77
|
+
# if record frag is configured, it takes precedence over http manipulations
|
|
78
|
+
if alter_tls:
|
|
79
|
+
# fragment TLS
|
|
80
|
+
if not self.attempt_tls_fragmentation(
|
|
81
|
+
from_socket, to_socket, direction, record_frag, record_version
|
|
82
|
+
):
|
|
83
|
+
# turn of record frag and force normal forwarding
|
|
84
|
+
alter_tls = False
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
elif http_strategy is not None:
|
|
88
|
+
if not self.attempt_http_manipulations(from_socket, to_socket, direction, http_strategy):
|
|
89
|
+
# turn of htt manipulations and force normal forwarding
|
|
90
|
+
http_strategy = None
|
|
91
|
+
continue
|
|
92
|
+
else:
|
|
93
|
+
# normal forwarding
|
|
94
|
+
data = from_socket.recv(STANDARD_SOCKET_RECEIVE_SIZE)
|
|
95
|
+
if not data:
|
|
96
|
+
self.debug("Connection closed, closing both sockets", direction)
|
|
97
|
+
to_socket.try_close()
|
|
98
|
+
break
|
|
99
|
+
else:
|
|
100
|
+
to_socket.send(data)
|
|
101
|
+
logging.info("### Cancelled forwarding ###")
|
|
102
|
+
except BrokenPipeError as e:
|
|
103
|
+
self.debug(f"Forwarding broken with {e}", direction)
|
|
104
|
+
to_socket.try_close()
|
|
105
|
+
except OSError as e:
|
|
106
|
+
if e.errno == 9:
|
|
107
|
+
# Bad file descriptor, socket closed by other forwarding queue
|
|
108
|
+
to_socket.try_close()
|
|
109
|
+
else:
|
|
110
|
+
self.debug(f"OSError while forwarding, closing sockets: {e}", direction)
|
|
111
|
+
to_socket.try_close()
|
|
112
|
+
except Exception as e:
|
|
113
|
+
self.debug(f"Exception while forwarding: {e}", direction)
|
|
114
|
+
to_socket.try_close()
|
|
115
|
+
self.info(f"{direction}: Closed connection", direction)
|
|
116
|
+
|
|
117
|
+
def attempt_http_manipulations(
|
|
118
|
+
self, from_socket: WrappedTcpSocket, to_socket: WrappedTcpSocket, direction: str, http_strategy
|
|
119
|
+
) -> bool:
|
|
120
|
+
"""
|
|
121
|
+
Attempts to parse HTTP requests and apply manipulations if an HTTP strategy is set. Sends the data.
|
|
122
|
+
|
|
123
|
+
Returns: True on successful parsing, false otherwise
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
if not Http.is_valid_http(from_socket):
|
|
127
|
+
self.debug(
|
|
128
|
+
"Received non-HTTP data, turning off HTTP manipulations for this and following requests", direction
|
|
129
|
+
)
|
|
130
|
+
return False
|
|
131
|
+
# we allow 4k bytes max
|
|
132
|
+
data = from_socket.recv(STANDARD_SOCKET_RECEIVE_SIZE)
|
|
133
|
+
self.debug("Received HTTP data and manipulating request", direction)
|
|
134
|
+
data = HttpStrategies.manipulations(
|
|
135
|
+
data, strategy=http_strategy, http_smuggling_uncensored_url=self.http_smuggling_uncensored_url
|
|
136
|
+
)
|
|
137
|
+
self.debug(f"After applying Manipulation: {data}", direction)
|
|
138
|
+
to_socket.send(data)
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
def attempt_tls_fragmentation(
|
|
142
|
+
self, from_socket: WrappedTcpSocket, to_socket: WrappedTcpSocket, direction: str, record_frag, record_version
|
|
143
|
+
) -> bool:
|
|
144
|
+
"""
|
|
145
|
+
Attempts to parse and fragment TLS handshake records. Sends data if fragmentable, does not if otherwise.
|
|
146
|
+
|
|
147
|
+
Returns: True on successful parsing, false otherwise
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
record_header = from_socket.peek(5)
|
|
151
|
+
except Exception:
|
|
152
|
+
self.debug("Could not read record_header bytes. Disabling record fragmentation", direction)
|
|
153
|
+
return False
|
|
154
|
+
base_header = record_header[:3]
|
|
155
|
+
record_len = int.from_bytes(record_header[3:], byteorder="big")
|
|
156
|
+
is_tls = base_header == TLS_1_0_HEADER or base_header == TLS_1_1_HEADER or base_header == TLS_1_2_HEADER
|
|
157
|
+
if not is_tls:
|
|
158
|
+
self.debug(
|
|
159
|
+
f"Received first non-handshake TLS record header: {record_header}. Turning off "
|
|
160
|
+
f"TLS record fragmentation for this and following records",
|
|
161
|
+
direction,
|
|
162
|
+
)
|
|
163
|
+
# did not receive tls record
|
|
164
|
+
return False
|
|
165
|
+
else:
|
|
166
|
+
self.debug("Received TLS handshake record - fragmenting", direction)
|
|
167
|
+
try:
|
|
168
|
+
record = from_socket.read(5 + record_len)
|
|
169
|
+
except Exception:
|
|
170
|
+
self.debug(f"Could not read {record_len} record bytes. Disabling record fragmentation", direction)
|
|
171
|
+
return False
|
|
172
|
+
if record_version != TlsVersion.DEFAULT.value:
|
|
173
|
+
record_version_bytes = bytes.fromhex(record_version)
|
|
174
|
+
record = self.replace_version_in_record_header(record, record_version_bytes)
|
|
175
|
+
base_header = base_header[:1] + record_version_bytes
|
|
176
|
+
if record_frag:
|
|
177
|
+
record = self.fragment_record(base_header, record_len, record[5:])
|
|
178
|
+
to_socket.send(record)
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
def fragment_record(self, record_base_header: bytes, record_length: int, record_body: bytes) -> bytes:
|
|
182
|
+
fragments = [record_body[i : i + self.frag_size] for i in range(0, record_length, self.frag_size)]
|
|
183
|
+
fragmented_message = b""
|
|
184
|
+
for fragment in fragments:
|
|
185
|
+
# construct header
|
|
186
|
+
fragmented_message += record_base_header + int.to_bytes(len(fragment), byteorder="big", length=2)
|
|
187
|
+
fragmented_message += fragment
|
|
188
|
+
return fragmented_message
|
|
189
|
+
|
|
190
|
+
def replace_version_in_record_header(self, record: bytes, version: bytes) -> bytes:
|
|
191
|
+
header_to_keep = record[:1]
|
|
192
|
+
record_length_and_body = record[3:]
|
|
193
|
+
return header_to_keep + version + record_length_and_body
|
|
194
|
+
|
|
195
|
+
# LOGGER utility functions
|
|
196
|
+
def _logger_string(self, message: str, prefix: str) -> str:
|
|
197
|
+
return f"{prefix}: {message}"
|
|
198
|
+
|
|
199
|
+
def debug(self, message: str, prefix: str):
|
|
200
|
+
logging.debug(self._logger_string(message, prefix))
|
|
201
|
+
|
|
202
|
+
def info(self, message: str, prefix: str):
|
|
203
|
+
logging.info(self._logger_string(message, prefix))
|