sudo-Schwachstelle geschlossen

Posted by jonas on Friday, January 29, 2021

Am 26.01.2021 war es soweit: Eine bereits seit 2011 bestehende Schwachstelle im Tool sudo, das auf so ziemlich jedem Linux-System residiert, wurde in einer koordinierten Aktion geschlossen - und das hieß damit dann auch: Nachtschicht für uns. Denn die fragliche Schwachstelle war gravierend und erlaubte jedem lokalen Benutzer des Systems, sich mit etwas Trickserei root-Privilegen zu verschaffen. Es war also Tempo angesagt!

Bekannt wurde die Lücke unter der Bezeichnung Baron Samedit, eine Anspielung auf Baron Samedi und die Komponente sudoedit, über die die Schwachstelle ausgenutzt werden konnte.

In Fällen mit großer Tragweite - so wie diesem - kommt oft eine Vorgehensweise zum Tragen, die sich Responsible disclosure nennt. Denn ist die Katze erstmal aus dem Sack, sind also Details über die Lücke bekannt, stürzen sich erwartungsgemäß viele Leute darauf, Exploits zu schreiben, um diese Schwachstelle leicht ausnutzbar zu machen - im Zweifel, um solche Exploits zu verkaufen. Vereinfacht gesagt geht es darum, die Sicherheitslücke zunächst nicht zu veröffentlichen, sondern nur den Autorinnen der Software bekannt zu machen und ihnen Gelegenheit zu geben, die Lücke zu patchen, oft im Rahmen einer gewissen Frist. Die Veröffentlichung erfolgt dann in der Regel koordiniert mit den wichtigsten Linux-Distributionen, damit mit dem Bekanntwerden der Schwachstelle auch direkt schon Updates bereitstehen um die Lücke zu schließen.

Alarm!

Dieses mal hat Leah aus unserem Operations-Team als erste von der Lücke erfahren und sofort korrekt eingeschätzt, dass wir hier handeln müssen - und zwar umgehend. Da wir bei uns eine 24/7-Bereitschaft eingerichtet haben, war es naheliegend, diese zu alarmieren. Wir nutzen hierbei PagerDuty zur Schichtplanung und Alarm-Management. “Bereitschaft” heißt dabei nicht, dass 24/7 jemand direkt vor einem Rechner sitzt (das wäre bei unserer überschaubaren Teamgröße dann doch etwas viel verlangt), aber eben im Zweifelsfall aus dem Schlaf geklingelt wird.

Normalerweise passiert das vollautomatisch und unser Monitoring (gespeist primär aus Icinga2 und Grafana) meldet Vorfälle, die sich sofort jemand anschauen muss. Es ist aber auch möglich, manuell Incidents auszulösen. Das sah dann am Dienstag so aus:

Screenshot-2021-01-28-at-17.42.18

Ab da konnte Leah sich darauf verlassen, dass jemand aus dem Uberspace-Produktteam nun alarmiert wird, per App-Notification, SMS oder Anruf, und dass der Vorgang auch eskaliert wird, wenn die eigentlich zuständige Person aus welchem Grund auch immer gerade nicht reagieren kann.

Soforthilfe

Schnell war klar: Die Nacht wird für einige von uns wohl eher lang. Per Videochat kurz koordiniert lag das Vorgehen auf der Hand: Sofort Updates installieren, wie ja auch im PagerDuty-Incident in Großbuchstaben direkt empfohlen. Nur… was tun, wenn ein yum update lediglich sagt, es seien keine Updates verfügbar? Gründe dafür kann es viele geben, selbst bei einem koordinierten Release von Fixes für eine Schwachstelle. Im Fall von CentOS als von Red Hat Enterprise Linux abgeleiteter Distribution ganz schlicht: Das von Red Hat bereitgestellte Source-Paket muss selbst erstmal für CentOS portiert werden, was Zeit kostet, und dann auf die eigene Infrastruktur verteilt werden, was Zeit kostet, und dann auf verschiedenen Mirrorserver bereitgstellt werden, was Zeit kostet, und schließlich spielt auch noch das Caching der Paketdatenbank eine Rolle, auch wenn wir das noch lokal übergehen könnten. Wie auch immer: Für den Moment war kein Update verfügbar.

Also haben wir das Zweitbeste gemacht, was in dieser Situation möglich war, nämlich sudo abgeklemmt. Und zwar mit einem beherzten chmod -x sudo, damit es für niemanden mehr ausführbar war.

Aber natürlich: Dabei gehen Dinge kaputt. Zwar wird sudo nicht für den Betrieb unserer Dienste benötigt, also Websites und Mails waren im Grunde unbeeinträchtigt; für die Konfiguration unserer Dienste (uberspace web domain add ... und Konsorten) kommt sudo aber sehr wohl zum Einsatz. Ebenfalls diverse Checks unseres Monitorings (weil der als icinga laufende Prozess für diverse Dinge mehr Rechte benötigt), und ebenfalls diverse Zugriffe, die unser eigenes Dashboard - das keine root-Rechte auf unseren Hosts hat - via sudo ausführt. Aber verglichen mit dem Risiko, dass sich mit jeder Stunde Abwarten die Exploits für die Schwachstelle verbreiten und potentiell auch bei uns ausgenutzt werden war es eine vorübergehend akzeptable Lösung. Vor allem eine die uns Zeit zum Nachdenken verschaffte, ob wir die Situation während des Wartens auf die Verfügbarkeit des Updates verbessern können. Und vor allem auch, was wir mit den verbliebenen U6-Hosts machen, über deren EOL im November letzten Jahres wir die betreffenden User bereits mehrfach informiert haben - denn für U6 war klar: Es wird kein Update kommen; da sind wir auf uns gestellt.

Workaround

Die Red-Hat-eigene Seite zu dieser Schwachstelle liefert eine interessante Mitigation. Der ganz naheliegende Gedanke, doch einfach sudoedit auszuknipsen (das - bei uns - kein Mensch braucht), funktioniert nämlich nicht. sudoedit ist lediglich ein Symlink auf sudo, das sich bei einem Aufruf über einen anderen Namen eben anders verhält. Wir könnten also natürlich zwar den Symlink entfernen, aber jede Ubernautin könnte sich ihren eigenen Symlink anlegen und es wäre nichts gewonnen.

Red Hats Vorschlag greift auf systemtap zurück. Hierbei handelt es sich um ein mächtiges Werkzeug um Analysen an einem laufenden Linux-Kernel vorzunehmen. Man kann damit aber auch eingreifen und selbst ohne die SystemTap-Sprache zu beherrschen gewinnt man schnell einen guten Eindruck, was da möglich ist:

probe process("/usr/bin/sudo").function("main") {
        command = cmdline_args(0,0,"");
        if (strpos(command, "edit") >= 0) {
                raise(9);
        }
}

SystemTap kann insofern im Hintergrund laufen und immer wenn der Kernel die main-Funktion eines Binaries namens /usr/bin/sudo ausführt, wird geprüft, ob das Binary vielleicht unter einem Namen mit dem String “edit” aufgerufen wurde - und wenn ja, wird das Programm hart abgebrochen. Auf diese Weise lässt sich sudoedit effektiv sperren - aber sudo in Betrieb halten.

Nach ein paar Tests, dass dieses Vorgehen funktioniert wie vorgesehen, konnte sich unser Entwickler luto daran machen, daraus ein Ansible-Playbook zu formulieren, was auf den betreffenden Systemen obiges Script verteilt, als systemd-Service einbindet und ihn startet. Das sieht dann so aus:

---
- name: fix 2020-01-26 sudo exploit
  hosts: all
  become: true
  gather_facts: false
  strategy: free
  serial: 15
  tasks:
    - name: install dependencies
      shell: >-
        yum -y install systemtap yum-utils kernel-devel-"$(uname -r)"
    - name: drop sudoedit-block.stap
      copy:
        dest: /root/sudoedit-block.stap
        content: |
          probe process("/usr/bin/sudo").function("main")  {
            command = cmdline_args(0,0,"");
            if (strpos(command, "edit") >= 0) {
              raise(9);
            }
          }
    - name: drop sudoedit-block.service
      copy:
        dest: /etc/systemd/system/sudoedit-block.service
        content: |
          [Unit]
          Description=block 2021-01-26 sudo exploit

          [Service]
          ExecStart=/opt/rh/devtoolset-9/root/usr/bin/stap -g /root/sudoedit-block.stap

          [Install]
          WantedBy=default.target
    - daemonreload:
    - name: start sudoedit-block.service
      systemd:
        name: sudoedit-block.service
        enabled: yes
        state: restarted

Nachdem die Änderung auf alle U7-Hosts verteilt war, konnten wir sudo wieder anknipsen und erstmal beruhigt schlafen gehen, auch ohne Update. Auf den U6-Hosts blieb sudo weiterhin ausgeschaltet mit den entsprechenden Folgen - dass im Fall von bekannten Schwachstellen hier Funktionalität ggf. eingeschränkt werden muss und nur eine Migration zu U7 eine saubere Lösung darstellt hatten wir lange zuvor kommuniziert. Nun schien es entsprechend soweit zu sein, tatsächlich Funktionalität reduzieren zu müssen.

Performanceprobleme

Es dauerte nicht lange, bis sich dann PagerDuty erneut meldete - diesmal aber getriggert vom Monitoring, das wegen allgemein hoher Last Alarm schlug. Denn was wir ad hoc vom Gefühl erstmal nur als “okay, die CPU-Last geht ein bisschen hoch” wahrgenommen hatten, sah im späteren Monitoring dann doch deutlich heftiger aus:

Screenshot-2021-01-28-at-18.33.04

Wir nehmen also als ein wichtiges Learning mit: systemtap ist zwar im Prinzip ein guter Hammer für diesen Nagel, kostet aber CPU-Leistung, und zwar im Zweifel richtig unangenehm viel. Wir haben daher dann auf unserer Statusseite https://is.uberspace.online/ einen Hinweis auf “performance lower than usual” angebracht. Sicherheitshalber mit der pessimistischen Schätzung, dass das möglicherweise für “the next few days” so sein könne. Früher entwarnen können wir ja immer noch.

Paketupdate

Und zum Glück ging es dann doch schneller als erwartet: gleich am nächsten Morgen konnten wir feststellen, dass nun auch Updates in den CentOS-7-Repos verfügbar waren. Nach dem Ausrollen dieser Updates konnten wir das SystemTap-Script wieder überall deaktivieren und die Performance-Situation normalisierte sich dann auch schnell.

Die alten U6-Hosts blieben allerdings weiterhin ein Problem. Hier haben wir schließlich kurzerhand das letzte verfügbare Source-RPM von sudo in die Hand genommen und dann selbst sichergestellt, dass die Lücke hier nicht mehr ausgenutzt werden kann:

[mockbuild@andromeda ~]$ cat ~/rpmbuild/SOURCES/sudo-1.8.6p3-CVE-2021-3156.patch
--- sudo-1.8.6p3/src/parse_args.c.orig  2012-09-18 15:57:43.000000000 +0200
+++ sudo-1.8.6p3/src/parse_args.c       2021-01-27 11:26:59.368322413 +0100
@@ -162,6 +162,12 @@ parse_args(int argc, char **argv, int *n
     /* Flags allowed when running a command */
     valid_flags = MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|
                  MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL;
+
+    /* Fix for CVE-2021-3156 */
+    if (strcmp(getprogname(), "sudoedit") == 0) {
+       valid_flags = MODE_NONINTERACTIVE;
+    }
+
     /* XXX - should fill in settings at the end to avoid dupes */
     for (;;) {
        /*

Wir benutzen dabei mock, um aus Source-RPMs Pakete in einer verlässlich sauberen Umgebung zu bauen. Wie können wir nun kontrollieren, ob es das auch gebracht hat?

Der Blogpost von Qualys hat eine FAQ unter anderem zu genau dieser Frage:

How can I test if I have vulnerable version?

To test if a system is vulnerable or not, login to the system as a non-root user.

Run command “sudoedit -s /”

If the system is vulnerable, it will respond with an error that starts with “sudoedit:”

If the system is patched, it will respond with an error that starts with “usage:”

Also schauen wir mal:

$ sudoedit -s /
usage: sudoedit [-AknS] [-r role] [-t type] [-C fd] [-D level] [-g groupname|#gid] [-p prompt] [-u user name|#uid] file ...

Wunderbar - damit können wir in unter 24h seit Bekanntwerden der Schwachstelle für alle Ubernaut:innen einen Haken an die Sache machen. Ein akutes Problem stellte die Lücke schon in der selben Nacht nicht mehr dar.

PS: Immer noch auf U6, trotz zahlreicher Erinnerungen per Mail? Bitte einmal hier entlang!