Sudo 1.9: accessing terminal data from Python

Sudo 1.9 is now feature complete. One of the new features is Python support, meaning that you can easily extend sudo functionality using Python scripts. It supports the very same APIs as the regular C plugin API, only the language is different. One of the more interesting APIs is the IO logging API, which provides access to terminal data in real-time, both input and output. This way you can check if a sudo user is accessing data that he should not, or analyze the commands entered and terminate a session before a disaster occurs. In this blog you will find two simple examples for the above use cases. Both are over simplified but functional. You can use them to test out the new functionality, or as a basis for your own code.

Before you begin

In order to test sudo’s Python support, you will need both the sudo 1.9.0 package and the new sudo-python package. Sudo binary packages are available for many platforms, but the sudo-python package is currently only distributed for Linux. If you are using a different platform you will need to compile sudo yourself. It has a few dependencies, but the process is relatively easy and straightforward. I described it for RHEL / CentOS in my previous blog in depth. In addition to the version number, another important thing has changed: Python support is now available as a sub-package. Once you have installed the packages, you are almost ready to begin testing the new features. Before you begin, make sure that you can login as root without using sudo. Yes, even on Ubuntu. When you are experimenting it is easy to create a configuration, or broken Python code, that prevents sudo from working correctly.

Checking terminal output

Any data appearing on the terminal of a sudo session can be accessed from Python code. Our first example is a very simple one. Sudo sends data to the Python code in small chunks, sometimes even character by character, as it appears on the terminal. In this case we will list a directory, so data is sent in larger chunks to the Python code. Thus there is no need to concatenate the incoming data.

Setting up the test environment is easy. Login as root (or use “su”, just make sure that you can become root without sudo). Create a new directory under root’s home, and create an empty file in it:

mkdir /root/DoNotEnter
touch /root/DoNotEnter/MySecret

Now, open your favorite text editor and create a new Python plugin. It can be anywhere on the file system, but must be readable only by root. For simplicity’s sake I created it under the /root/ directory, which is accessible only by root, and gave it the name kick.py as it terminates the session (kicks the user out) if a given text string appears on the terminal.

import sudo

class MyIOPlugin(sudo.Plugin):
    def log_ttyout(self, buf):
        if "MySecret" in buf:
          sudo.log_info("Don't look at my secret!")
          return sudo.RC.REJECT

As you can see, the script starts by importing the sudo module. This does not exist as a separate file on your file system, only within the sudo Python plugin itself.

Next, we define a class. You can name it however you like, the only important thing is that it is inherited from the sudo.Plugin class. You will use the name of the class in the sudo.conf file, where you configure sudo plugins.

The IO Plugin has many methods. For this example we are only concerned with log_ttyout(). For a complete list, see the documentation at https://www.sudo.ws/man/1.9.0/sudo_plugin_python.man.html. The log_ttyout() method is called by sudo any time when new data appears on the terminal. This data is available in the buf parameter of the method. Any time the method is called, it checks if MySecret appears in buf. If it is there, sudo prints a message on screen and terminates the session by returning sudo.RC.REJECT.

Next, open /etc/sudo.conf in your favorite text editor and add the following line to it:

Plugin python_io python_plugin.so ModulePath=/root/kick.py ClassName=MyIOPlugin

Of course, make sure that ModulePath matches your actual setup.

Now, let’s login as a user and start a shell using sudo. Change to the /root/ directory and list it. You can see an interesting looking directory name: DoNotEnter. Obviously, you enter it and try to list it to see why it has this name. Boom. Your session is terminated, even before MySecret could appear on the terminal. Here is what you should see:

[czanik@centos7sudo ~]$ sudo -s
Password: 
[root@centos7sudo czanik]# cd /root/
[root@centos7sudo ~]# ls
DoNotEnter        kick.py
[root@centos7sudo ~]# cd DoNotEnter/
[root@centos7sudo DoNotEnter]# ls
Don't look at my secret!
                        Hangup

Checking input

Unless you copy and paste large chunks of text to your terminal, checking user input is a bit more complicated. Input arrives character by character, as the user types it, so you need to concatenate it before checking the content.

Note: The following sample code is a very naive implementation, as it only does exact matches. As soon as someone uses backspace to fix a spelling mistake or uses a different order of command line parameters, it cannot detect the command.

Lacking creativity I saved the following code in /root/kick_v2.py (see the previous section for an explanation):

import sudo

class MyIOPlugin(sudo.Plugin):
    def __init__(self, version: str, plugin_options, **kwargs):
        self.collected_buf = ''
        
    def log_ttyin(self, buf):
        self.collected_buf += buf
        if "rm -fr" in self.collected_buf:
          sudo.log_info("\n\nOops. 'rm -fr' is dangerous! Kicking you out...")
          return sudo.RC.REJECT
        # drop all the string until last enter:
        last_enter_pos = self.collected_buf.rfind("\n")
        if last_enter_pos >= 0:
          self.collected_buf = ''

As you can see, the script starts by importing the sudo module. This does not exist as a separate file on your file system, only within the sudo Python plugin itself. Next, we define a class. Name it however you like, the only important thing is that it is inherited from the sudo.Plugin class. You will use the name of the class in the sudo.conf file, where you configure sudo plugins.

Here we use the log_ttyin() method of the IO Plugin, but first I initialize a variable in __init__() to collect the keystrokes. When the log_ttyin() method receives data from sudo, it first appends it to collected_buf. Next, it checks, if rm -fr appears in the variable. If it is present, a warning message is displayed and the session is terminated by returning sudo.RC.REJECT. When you hit the Enter key, the content of collected_buf is reset.

Next, open /etc/sudo.conf in your favorite text editor and add the following line to it:

Plugin python_io python_plugin.so ModulePath=/root/kick_v2.py ClassName=MyIOPlugin

Now let’s login as a user and start a shell using sudo. Start to enter rm -fr on the command line. You should be kicked out before the final “r” appears on the screen:

[czanik@centos7sudo ~]$ sudo -s
Password: 
[root@centos7sudo czanik]# rm -f

                                Oops. 'rm -fr' is dangerous! Kicking you out...
                                                                               Hangup
[czanik@centos7sudo ~]$ 

What is next?

From this blog you can learn the basics of dealing with terminal input and output from Python code. Before going on to write your own Python code, I recommend reading the sudo plugin documentation. Both:

If you would like to be notified about new posts and sudo news, sign up for the sudo blog announcement mailing list.