sichere Befehlsausführung mit Python: subprocess.Popen

Sicherheit ist für mich bei der Entwicklung des picockpit-Clients wichtig.

Das Folgende gilt für Linux-Systeme (ist aber wahrscheinlich auf alle Unix-ähnlichen Systeme anwendbar, einschließlich macOS)

Python ermöglicht die Ausführung externer Befehle mit dem Modul subprocess.

importieren subprocess

In der kommenden Version von PiCockpit werden die Benutzer in der Lage sein, ihre eigenen Schaltflächen zu erstellen (durch einfaches Bearbeiten einer JSON-Datei auf dem Pi), um Befehle auszuführen.

PiCockpit wird standardmäßig mit drei Tasten ausgeliefert:

Bild

Diese Befehle erfordern alle Root-Rechte. picockpit-client läuft standardmäßig mit Root-Rechten auf Ihrem System.

Andere Befehle, die Sie ausführen möchten, sollten jedoch als weniger privilegierter Benutzer ausgeführt werden (wir verwenden standardmäßig den Benutzer "pi"). Tatsächlich werden einige Befehle, wie der Chromium-Browser Abfall als root ausführen.

Kurzum, wir brauchen eine Lösung, um diese Prozesse unter einem anderen Benutzer laufen zu lassen.

Dies ist möglich dank der preexec_fn Parameter für popen.

Hier ist mein Code:

def thread_run(self):
     def sanitize(input_string):
         #https://maxharp3r.wordpress.com/2008/05/15/pythons-minidom-xml-and-illegal-unicode-characters/
         RE_XML_ILLEGAL = u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' + \
                          u'|' + \
                          u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])’ % \
                          (chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff),
                           chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff),
                           chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff))
         return re.sub(RE_XML_ILLEGAL, "", input_string)

    def demote(user, uid, gid):
         def demote_function():
             print("beginnend")
             print ('uid, gid = %d, %d' % (os.getuid(), os.getgid()))
             print (os.getgroups())
             #-Initgroups müssen ausgeführt werden, bevor wir das Privileg verlieren, sie zu setzen!
             os.initgroups(user, gid)
             os.setgid(gid)
             # muss zuletzt ausgeführt werden
             os.setuid(uid)
             print("Degradierung beendet")
             print('uid, gid = %d, %d' % (os.getuid(), os.getgid()))
             print (os.getgroups())
         return demote_function

    def run_cmd(cmd, cmd_def):
         return_code = 0
         return_error = ""
         return_result = ""
         timeout = Keine
         benutzer = "pi"
         wenn "timeout" in cmd_def:
             if isinstance(cmd_def["timeout"], int):
                 wenn cmd_def["timeout"] > 0:
                     timeout = cmd_def["timeout"]
         if "user" in cmd_def:
             if isinstance(cmd_def["user"], str):
                 user = cmd_def["user"]
         versuchen:
             print("picontrol :: Einrichtung von Benutzerrechten")
             uid = pwd.getpwnam(user).pw_uid
             gid = pwd.getpwnam(user).pw_gid
             print("picontrol :: angeforderter Benutzer " + user + " uid " + str(uid) + " gid " + str(gid))
             print("picontrol :: Befehl starten ... ")
             drucken(cmd)
             proc = subprocess.Popen(
                 cmd,
                 stdout=subprocess.PIPE,
                 stderr=subprocess.PIPE,
                 preexec_fn=demote(user, uid, gid)
             )
             # TODO: Zeitüberschreitung anders behandeln
                 # timeout=timeout)               
             print("picontrol :: AFTER subprocess.Popen")
         except FileNotFoundError:
             return_code = errno.ENOENT
             return_error = "Befehl nicht auf Ihrem System gefunden " + " ".join(cmd)
         # TODO: wird dies tatsächlich jemals aufgerufen werden?
         except subprocess.TimeoutExpired:
             return_code = errno.ETIME
             return_error = "Befehlszeitüberschreitung abgelaufen (" + str(cmd_def["timeout"]) + " sec)"
         außer Ausnahme wie e:
             if hasattr(e, 'errno'):
                 return_code = e.errno
             sonst:
                 return_code = 1
             if hasattr(e, 'message'):
                 return_error = str(e.message)
             sonst:
                 return_error = str(e)
         sonst:
             #-Prozess kann normal ausgeführt werden, keine Ausnahmen beim Starten
             process_running = True
             while process_running:
                 wenn proc.poll() nicht None ist:
                     process_running = False
                 if self.stop_flag.is_set():
                     print("Stop-Flag in process_running erhalten, process_running auf false gesetzt")
                     process_running = False
                     # und führen Sie danach auch Schritte zur aktiven Terminierung durch.
                 # eine halbe Sekunde der Auflösung
                 time.sleep(0.5)
             return_code = proc.returncode
             return_error = sanitize(proc.stderr.read().decode('UTF-8').rstrip("\r\n"))
             return_result = sanitize(proc.stdout.read().decode('UTF-8').rstrip("\r\n"))

        return (return_code, return_error, return_result)

Dieser Code ist noch in Arbeit, so dass Teile davon möglicherweise nicht funktionieren. Die Teile, die ich in diesem Blogbeitrag besprechen möchte, funktionieren jedoch.

Benutzer unter Linux

Einem Benutzernamen wird unter Linux eine Benutzerkennung und eine Benutzergruppe zugewiesen, und der Benutzer ist ein Mitglied von anderen Gruppen.

/etc/passwd enthält die im System definierten Benutzer:

Bild

Die Einträge können wie folgt entschlüsselt werden:

Benutzer : Passwort : Benutzerkennung (uid) : Gruppenkennung (gid) : Vollständiger Name des Benutzers (GECOS) : Heimatverzeichnis des Benutzers : Login-Shell

Interessante Nebenbemerkung: Das Speichern des Passworts in /etc/passwd (in verschlüsselter Form) ist optional (falls das Passwort nicht gespeichert wird, wird ein x wird hier gespeichert); Da diese Datei für die ganze Welt lesbar ist, entscheiden sich viele Systeme dafür, stattdessen die für root lesbare Datei /etc/shadow zu verwenden:

Bild

das tatsächlich das Kennwort enthält.

Für picockpit holen wir uns den Benutzernamen aus der JSON-Einstellungsdatei und wollen alles andere entsprechend einrichten. Also fangen wir an mit:

uid = pwd.getpwnam(user).pw_uid
gid = pwd.getpwnam(user).pw_gid

(Bitte beachten Sie pwd importieren).

Mit dem Modul pwd können wir mit Python auf die Datei /etc/passwd zugreifen. getpwnam sucht in der entsprechenden Zeile nach dem Namen des Benutzers, in unserem Fall "pi" oder "root" (oder was immer Sie wollen, was auch immer in der JSON-Datei festgelegt ist).

So erhalten wir die Benutzerkennung (uid) und die Gruppenkennung (gid) des Benutzers.

Als nächstes rufen wir subprocess.Popen auf:

proc = subprocess.Popen(
     cmd,
     stdout=subprocess.PIPE,
     stderr=subprocess.PIPE,
     preexec_fn=demote(user, uid, gid)
)

stdout und stderr sind PIPEd, damit wir sie später erfassen können.

der preexec_fn wird auf demote(user, uid, gid) gesetzt.

Wichtig!

Dies ist ein wichtiger Punkt: preexec_fn erwartet eine Funktion Handle, das er im Kontext des neuen Prozesses, den er startet, ausführt (der Prozess degradiert sich also selbst!). Diese Funktion wird vor dem eigentlichen Code ausgeführt, So können wir sicher sein, dass der Code in dem Kontext ausgeführt wird, in dem wir ihn ausführen wollen..

So wie wir hier demote übergeben, sieht es so aus, als würde die Funktion aufgerufen werden.

Aber schauen Sie sich die Definition von "degradieren" an:

def demote(user, uid, gid):
     def demote_function():
        print("beginnend")
         print ('uid, gid = %d, %d' % (os.getuid(), os.getgid()))
         print (os.getgroups())
         #-Initgroups müssen ausgeführt werden, bevor wir das Privileg verlieren, sie zu setzen!
         os.initgroups(user, gid)
         os.setgid(gid)
         # muss zuletzt ausgeführt werden
         os.setuid(uid)
         print("Degradierung beendet")
         print('uid, gid = %d, %d' % (os.getuid(), os.getgid()))
         print (os.getgroups())
     return demote_function

Diese Funktion verpackt eine andere Funktion in sich selbst, die sie zurückgibt. Mit der oben angegebenen Syntax können wir eigentlich eine Funktion zurückgeben sondern können auch Parameter an sie übergeben.

Das ist so, als ob man den Kuchen nicht nur essen, sondern auch haben kann. Schön Lächeln

Hier ist ein Screenshot, für den Fall, dass WordPress mit der obigen Syntax durcheinander kommt:

Bild

OK, jetzt erkläre ich den Code in dieser Degradierungsfunktion:

os.initgroups:

legen wir die Gruppen fest, in denen der Prozess Mitglied sein soll (ja, das müssen wir tun - sonst ist der Prozess nur Mitglied der Standardgruppe pi) - dieser Befehl erwartet den Benutzer Name

os.setgid:

Hier wird die Gruppen-ID für den neuen Prozess festgelegt.

Es gibt auch eine Funktion os.setegid(), die wie folgt beschrieben wird: "Setzen der effektiven Gruppen-ID des aktuellen Prozesses". 

Worin besteht der Unterschied?

setegid setzt die gid temporär, so dass der Prozess später zur ursprünglichen gid zurückkehren kann. Das ist nicht das, was wir wollen, da der Prozess, den wir aufrufen, seine Rechte wieder aufwerten könnte.

os.setuid(uid):

In ähnlicher Weise gibt es eine os.seteuid(euid) Funktion, die es dem ausgeführten Prozess ermöglichen würde, "zurück zu aktualisieren". Wir verwenden das also nicht.

Die Reihenfolge dieser Befehle ist wichtig! Wenn wir die Benutzerkennung zuerst festlegen würden, hätten wir nicht genügend Rechte, um z. B. die Gruppen einzurichten. Legen Sie also die Benutzerkennung zuletzt fest.

Nebenbei bemerkt: preexec_fn könnte auch zum Einrichten von Umgebungsvariablen und anderen Dingen verwendet werden, um mehr Flexibilität zu ermöglichen. Möglicherweise in einem zukünftigen Picockpit-Client-Upgrade, mal sehen.

Nebenbemerkung 2: preexec_fn schreibt in den Ausgabestrom des neuen Prozesses (denken Sie daran, dass er in seinem Kontext bereits ausgeführt wird):

Aus diesem Grund wird die Debug-Ausgabe an das Picockpit-Frontend gesendet:

Bild

Ref

Weitere Links von Interesse:

Bonus: Chrom

chromium-browser -display=:0 https://www.picockpit.com

Bild

ermöglicht es Ihnen, den Chromium-Browser von der Kommandozeile aus (in der Shell) zu starten. -display=:0 legt das X-Display fest, das Chromium für die Ausgabe verwenden soll, so dass Sie die entsprechende Umgebungsvariable nicht einrichten müssen.

Peter.sh bietet eine Vielzahl von Kommandozeilenschaltern, mit denen Sie Chromium nach Ihren Wünschen konfigurieren können (z.B. Kiosk-Modus, etc.).

Beachten Sie, dass Sie einen X-Server ausführen müssen, wenn Sie Chromium anzeigen möchten. Auf dem Raspberry Pi ist es am einfachsten, den Raspberry Pi Desktop laufen zu lassen.

Sie suchen nach professioneller Beratung und Entwicklung für die Raspberry Pi Plattform?

Wir bieten Beratungs- und Entwicklungsdienstleistungen für den Fall, dass Sie jemanden suchen, der eine sichere Chromium-basierte Kioskanwendung für Sie einrichtet. Nehmen Sie noch heute Kontakt auf.