From abc639e32b7a9fd2f3966eadc25e360659549ac2 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 15 Feb 2023 17:03:35 -0500 Subject: [PATCH] Preferentially support HTTPS on Eaton PDU While Eaton does not do HTTPS by default, it can be configured to do so. Support when available. Mitigate downgrade attack by stickying the cert fingerprint. If fingerprint is present, then refuse to even think about port 80. --- .../plugins/hardwaremanagement/eatonpdu.py | 72 +++++++++++++++---- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py b/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py index c0c86b5a..fea6338c 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py @@ -17,6 +17,9 @@ import confluent.util as util import confluent.messages as msg import confluent.exceptions as exc import eventlet +import eventlet.green.socket as socket +import pyghmi.util.webclient as wc +import confluent.util as util import re import hashlib import json @@ -59,12 +62,32 @@ class WebResponse(httplib.HTTPResponse): def _check_close(self): return True -class WebConnection(httplib.HTTPConnection): +class WebConnection(wc.SecureHTTPConnection): response_class = WebResponse - def __init__(self, host): - httplib.HTTPConnection.__init__(self, host, 80) + def __init__(self, host, secure, verifycallback): + if secure: + port = 443 + else: + port = 80 + wc.SecureHTTPConnection.__init__(self, host, port, verifycallback=verifycallback) + self.secure = secure self.cookies = {} + def connect(self): + if self.secure: + return super(WebConnection, self).connect() + addrinfo = socket.getaddrinfo(self.host, self.port)[0] + # workaround problems of too large mtu, moderately frequent occurance + # in this space + plainsock = socket.socket(addrinfo[0]) + plainsock.settimeout(self.mytimeout) + try: + plainsock.setsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG, 1456) + except socket.error: + pass + plainsock.connect(addrinfo[4]) + self.sock = plainsock + def getresponse(self): try: rsp = super(WebConnection, self).getresponse() @@ -132,8 +155,18 @@ class PDUClient(object): if not target: target = self.node target = target.split('/', 1)[0] - self._wc = WebConnection(target) - self.login(self.configmanager) + verifier = util.TLSCertVerifier( + self.configmanager, self.node, 'pubkeys.tls_hardwaremanager') + try: + self._wc = WebConnection(target, secure=True, verifycallback=verifier.verify_cert) + self.login(self.configmanager) + except socket.error as e: + pkey = self.configmanager.get_node_attributes(self.node, 'pubkeys.tls_hardwaremanager') + pkey = pkey.get(self.node, {}).get('pubkeys.tls_hardwaremanager', {}).get('value', None) + if pkey: + raise + self._wc = WebConnection(target, secure=False, verifycallback=verifier.verify_cert) + self.login(self.configmanager) return self._wc def login(self, configmanager): @@ -153,18 +186,27 @@ class PDUClient(object): if not username or not passwd: raise Exception('Missing username or password') b64user = base64.b64encode(username.encode('utf8')).decode('utf8') + b64pass = base64.b64encode(passwd.encode('utf8')).decode('utf8') rsp = self.wc.grab_response('/config/gateway?page=cgi_authentication&login={}&_dc={}'.format(b64user, int(time.time()))) rsp = json.loads(sanitize_json(rsp[0])) - parms = answer_challenge(username, passwd, rsp['data'][-1]) self.sessid = rsp['data'][0] - url = '/config/gateway?page=cgi_authenticationChallenge&sessionId={}&login={}&sessionKey={}&szResponse={}&szResponseValue={}&dc={}'.format( - rsp['data'][0], - b64user, - parms['sessionKey'], - parms['szResponse'], - parms['szResponseValue'], - int(time.time()), - ) + if rsp['data'][-1] == 'password': + url = '/config/gateway?page=cgi_authenticationPassword&login={}&sessionId={}&password={}&dc={}'.format( + b64user, + rsp['data'][0], + b64pass, + int(time.time()), + ) + else: + parms = answer_challenge(username, passwd, rsp['data'][-1]) + url = '/config/gateway?page=cgi_authenticationChallenge&sessionId={}&login={}&sessionKey={}&szResponse={}&szResponseValue={}&dc={}'.format( + rsp['data'][0], + b64user, + parms['sessionKey'], + parms['szResponse'], + parms['szResponseValue'], + int(time.time()), + ) rsp = self.wc.grab_response(url) rsp = json.loads(sanitize_json(rsp[0])) if rsp['success'] != True: @@ -177,7 +219,7 @@ class PDUClient(object): return wc.grab_response(url) def logout(self): - print(repr(self.do_request('cgi_logout'))) + self.do_request('cgi_logout') def get_outlet(self, outlet): rsp = self.do_request('cgi_pdu_outlets')