None
Tipps & Tricks

Sicherer Ad-hoc Server mit Python 3

by Viktor Garske on Jan. 14, 2017, midnight

Vor zwei Wochen veröffentlichte ich einen kurzen Blogartikel zum Thema Ad-hoc Server mit Python als Beispiel. any fragte daraufhin, ob es das ganze auch mit TLS/HTTPS und Authentifizierung gibt. Ja, das gibt es – sogar mit Bordmitteln – allerdings muss man hierzu ein wenig nachhelfen. Für diesen Blogartikel setze ich auf Python 3.

Vorbereitungen

Um den Webserver HTTPS-fähig zu machen, werden ein Zertifikat und ein Private Key benötigt.

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365

Das ist nur ein kleines Beispiel, so ein Zertifikat zu erzeugen. Wie sich ganze CAs erstellen lassen, zeigt Thomas Leister in seinem Blog.

Grundlagen des HTTPS-Servers

Hierbei arbeiten wir nun nicht mehr mit Kommandozeilenbefehlen, sondern mit einem eigenständigen Skript.

from http.server import HTTPServer, BaseHTTPRequestHandler, SimpleHTTPRequestHandler
import ssl

CERTFILE = 'cert.pem'
KEYFILE = 'key.pem'

httpd = HTTPServer(('localhost', 4443), SimpleHTTPRequestHandler)

httpd.socket = ssl.wrap_socket (httpd.socket, keyfile=KEYFILE, certfile=CERTFILE, server_side=True)

httpd.serve_forever()

Vereinfacht erklärt wird der Webserver in Zeile 7 auf dem Port 4443 initalisiert. Der SimpleHTTPRequestHandler verarbeitet alle Aufrufe. In Zeile 9 wird die HTTPS-Unterstützung eingebaut. Abschließend wird der HTTPServer aktiviert.

Authentifizierung

Die eigentliche Schwierigkeit ist es, jetzt noch eine Authentifizierung einzubauen. In jedem Fall müssen wir einen eigenen Handler entwickeln, um selber vorzugeben, wie sich der Server bei Anfragen verhalten soll. Eine einfache Implementierung für die Authentifizierung ist HTTP BasicAuthentication. Hier muss für den Aufruf (GET) des geschützten Bereiches das Headerfeld "Authorization" mit gültigen Daten mitgeschickt werden. Das sieht bspw. so aus:

Authorization: Basic dmlrdG9yOnBhc3N3b3Jk

Der String hinter Basic ist Base64-codiert und würde im Beispiel viktor:password ergeben. Das ist eine Verkettung aus Benutzernamen und Passwort, getrennt durch einen Doppelpunkt. Wie wir allerdings sehen, sind die Daten zwar codiert, aber nicht chiffriert und somit abhörbar. In diesem Beispiel läuft die Verschlüsselung bereits eine Schicht tiefer über TLS ab, weshalb dieses Problem nicht so schwerwiegend ist. Trotzdem warnt die OWASP-Community vor Basic Auth, da neben unzureichender Verschlüsselung auf der HTTP-Ebene u.a. eine benutzerfreundliche Abmeldung nicht umsetzbar ist. (Zum Abmelden muss üblicherweise der Browser geschlossen werden)

Das Ergebnis

from http.server import HTTPServer, SimpleHTTPRequestHandler
import ssl
import sys
import base64

key = ''

class MyHandler(SimpleHTTPRequestHandler):
    def do_HEAD(self, authenticated):
        if authenticated:
            self.send_response(200)
        else:
            self.send_response(401)
            self.send_header('WWW-Authenticate','Basic realm=\"Secure Share\"')
        self.send_header('Content-type', 'text/html')
        self.end_headers()

    def do_GET(self):
        if self.headers.get('Authorization') == 'Basic '+key.decode('utf-8'):
            SimpleHTTPRequestHandler.do_GET(self)
            pass
        elif self.headers.get('Authorization') == None:
            self.do_HEAD(False)
            self.wfile.write(bytes('Anmeldung fehlgeschlagen', 'utf-8'))
            pass
        else:
            self.do_HEAD(False)
            self.wfile.write(bytes('Anmeldung fehlgeschlagen', 'utf-8'))
            pass

def main():
    if len(sys.argv) < 4:
        print("Usage: python3 adhocserver.py [username] [password] " +\
              "[certdir] [port (opt.)]")
        return
    
    global key
    username = sys.argv[1]
    password = sys.argv[2]
    certdir = sys.argv[3]
    port = int(sys.argv[4]) if len(sys.argv) == 5 else 4443
    credentials = '{0}:{1}'.format(username, password)
    key = base64.b64encode(bytes(credentials, 'utf-8'))
    CERTFILE = certdir + '/cert.pem'
    KEYFILE = certdir + '/key.pem'
    
    httpd = HTTPServer(('localhost', port), MyHandler)

    httpd.socket = ssl.wrap_socket (httpd.socket, keyfile=KEYFILE, certfile=CERTFILE, server_side=True)

    httpd.serve_forever()
    
if __name__ == '__main__':
    main()

So sieht der Server nach der Implementierung von Basic Authentication aus. Was ist nun passiert?

Die Authentifzierung

Neu ist die Klasse MyHandler. Sie ist der Handler für die Requests (dt. Anfragen). Mittels Vererbung, einem grundlegenden Konzept aus der objektorientierten Programmierung, greifen wir auf den bekannten SimpleHTTPRequestHandler zurück, der bisher die Abwicklung der Anfragen, die Auflistung des Verzeichnisses und den Download der Dateien übernommen hat. Lediglich die relevanten Funktionen für Basic Auth. müssen nur noch ausgetauscht werden. Das beschränkt sich auf do_GET() und do_HEAD(). Bei einer Anfrage von einem Browser wird diese in einer bedingten Verzweigung (Zeilen 19-29) auf den Authentication-Header geprüft. Es können nun drei Szenarien eintreten:

  1. Zeile 19: Ein Authorization-Header mit gültigen Anmeldedaten wurde vom Browser in der Anfrage übermittelt. Nun übernimmt die do_GET()-Funktion aus der Basisklasse und stellt die gewohnte Funktionalität (Verzeichnisanzeige, Download) bereit.
  2. Zeile 22: Es wurde kein Authorization-Header mitgeschickt. do_HEAD(authenticated=False) wird aufgerufen. Die Antwort auf die Anfrage wird den HTTP-Statuscode 401 (Unauthorized) tragen, im Feld WWW-Authenticate wird dem Browser übermittelt, dass Basic Auth. erwartet wird und der geschützte Bereich "Secure Share" heißt (Zeilen 13 und 14). Würde der Nutzer die Anmeldung abbrechen, würde im Browser der Text "Anmeldung fehlgeschlagen" stehen.
  3. Zeile 26: Es wurde kein gültiger Authorization-Header übermittelt. Dieser Fall tritt ein, wenn der Nutzer falsche Anmeldedaten eingegeben hat. In meinem Beispiel wird genau so wie bei einer ersten Anmeldung verfahren, der Nutzer erhält erneut ein Anmeldedialog. Man könnte auch schreiben, dass die Anmeldung ungültig war, allerdings bietet das für Angreifer möglicherweise Spielraum.

Fall 2 und 3 könnte man auch zusammenfassen, zur Anschaulichkeit und späteren Erweiterbarkeit habe ich diese allerdings getrennt.

Weitere Verbesserungen

Der Rest des Skripts ist eine Verschönerung des ersten Entwurfs. Nur wenn das Skript direkt ausgeführt und nicht importiert wird (Z. 53), werden die Befehle aus der main() ausgeführt.

 

Das Skript muss zwangläufig mit Argumenten aufgerufen werden. Hierunter fallen der Benutzername und das Kennwort, der Pfad der Zertifikatsdateien sowie optional ein benutzerdefinierter Port (Standard bleibt jedoch 4443). Im Beispiel müssen Zertifikat und Key in einem Verzeichnis liegen und cert.pem sowie key.pem heißen. Das ist natürlich nicht immer wünschenswert und nur zur Einfachheit so gelöst, da das Hauptaugenmerk auf der Anfragenabwicklung liegt. Weiterhin sollten Zertifikat und Schlüssel außerhalb des Working Directory liegen, da sie sonst heruntergeladen werden können, was wohl nicht im Sinne des Admins liegt.

Wichtig für den Aufruf des Skripts: das zur Verfügung gestellte Verzeichnis bleibt im Beispiel das Working Directory!

Zusammenfassung

Es ist also möglich, einen Webserver mit HTTPS (TLS) und einer einfachen Authentifizierung aufzusetzen. Ich hoffe, ich konnte ebenfalls einen kleinen Einblick in die Arbeit eines Webservers geben und möchte allerdings darauf hinweisen, dass ich nur an der Oberfläche gekratzt habe, da dieses Thema sehr vielfältig ist und den Bogen sonst überspannen würde.

Weitere Hinweise

Der GitHub-Nutzer "fxsjy" hat in einem Gist einen ähnlichen Server zur Verfügung gestellt, der allerdings auf Python 2 setzt.

 

Trotz sorgfältigen Tests (Umgebung: Python 3.6, CPython) lassen sich Fehler nicht ausschließen, insbesondere bei unterschiedlichen Python-Versionen. Deshalb übernehme ich keine Haftung für Schäden. Zertifikatsdateien und Private-Keys sind mit höchster Vorsicht zu behandeln!

Auch ich stelle unter dem Repository vnotes-scripts dieses Skript unter der MIT-Lizenz zur Verfügung. Ich freue mich auf Feedback!

Author image
Viktor Garske

Viktor Garske ist der Hauptautor des Blogs und schreibt gerne über Technologie, Panorama sowie Tipps & Tricks.

Comments (0)

Comments are not enabled for this entry.