/secimport

Secure import for python modules using dtrace

Primary LanguagePythonMIT LicenseMIT

secimport

secimport

Secure import for python modules using dtrace under the hood.
Medium Article

secimport can be used to:

  • Confine/Restrict specific python modules inside your production environment.
    • Open Source, 3rd party from unstrusted sources.
    • Audit the flow of your python application at user-space/os/kernel level.
  • Run an entire python application under unified configuration
    • Like seccomp but not limited to Linux kernels. Cross platform.

Quick Start

secimport can be used out of the box in the following ways:

  1. Inside your code using module = secimport.secure_import('module_name', ...).
    • Replacing the regular import statement with secure_import
    • Only modules that were imported with secure_import will be traced.
  2. As a sandbox, by specifying the modules and their policies.
    • Use this repository to:
      • Generate a YAML policy from your code
      • Compile that YAML to dscript.
    • Use dtrace command to run your main python application, with your tailor-made sandbox.
      • No need for secure_import, you can keep using regular imports

For the full list of examples, see EXAMPLES.md.

Pickle Example

How pickle can be exploited in your 3rd party packages:

>>> import pickle
>>> class Demo:
...     def __reduce__(self):
...         return (eval, ("__import__('os').system('echo Exploited!')",))
... 
>>> pickle.dumps(Demo())
b"\x80\x04\x95F\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x04eval\x94\x93\x94\x8c*__import__('os').system('echo Exploited!')\x94\x85\x94R\x94."
>>> pickle.loads(b"\x80\x04\x95F\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x04eval\x94\x93\x94\x8c*__import__('os').system('echo Exploited!')\x94\x85\x94R\x94.")
Exploited!
0

With secimport, you can control such action to do whatever you want:

In [1]: import secimport
In [2]: pickle = secimport.secure_import("pickle")
In [3]: pickle.loads(b"\x80\x04\x95F\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x04eval\x94\x93\x94\x8c*__import__('os').system('echo Exploited!')\x94\x85\x94R\x94.")

[1]    28027 killed     ipython

A log file is automatically created, containing everything you need to know:

$ less /tmp/.secimport/sandbox_pickle.log

  @posix_spawn from /Users/avilumelsky/Downloads/Python-3.10.0/Lib/threading.py
    DETECTED SHELL:
        depth=8
        sandboxed_depth=0
        sandboxed_module=/Users/avilumelsky/Downloads/Python-3.10.0/Lib/pickle.py  

    TERMINATING SHELL:
        libsystem_kernel.dylib`__posix_spawn+0xa
        ...
                libsystem_kernel.dylib`__posix_spawn+0xa
                libsystem_c.dylib`system+0x18b
                python.exe`os_system+0xb3
    KILLED
:

YAML Template Example

For a full tutorial, see YAML Profiles Usage

# An example yaml template for a sandbox.

modules:
  requests:
    destructive: true
    syscall_allowlist:
      - write
      - ioctl
      ...
      - stat64
  fastapi:
    destructive: true
    syscall_allowlist:
      - bind
      - fchmod
      ...
      - stat64
  uvicorn:
    destructive: true
    syscall_allowlist:
      - getpeername
      - getpgrp
      ...
      - stat64

Python Processing Example

Python 3.10.0 (default, May  2 2022, 21:43:20) [Clang 13.0.0 (clang-1300.0.27.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.

# Let's import subprocess module, limiting it's syscall access.
>>> import secimport
>>> subprocess = secimport.secure_import("subprocess")

# Let's import os 
>>> import os
>>> os.system("ps")
  PID TTY           TIME CMD
 2022 ttys000    0:00.61 /bin/zsh -l
50092 ttys001    0:04.66 /bin/zsh -l
75860 ttys001    0:00.13 python
0
# It worked as expected, returning exit code 0.


# Now, let's try to invoke the same logic using a different module, "subprocess", that was imported using secure_import:
>>> subprocess.check_call('ps')
[1]    75860 killed     python

# Damn! That's cool.
  • The dtrace profile for the module is saved under:
    • /tmp/.secimport/sandbox_subprocess.d:
  • The log file for this module is under
    • /tmp/.secimport/sandbox_subprocess.log:
      ...
      
      (OPENING SHELL using posix_spawn): (pid 75860) (thread 344676) (user 501) (python module: <stdin>) (probe mod=, name=entry, prov=syscall func=posix_spawn) /bin/sh 
          #posix_spawn,
      
      (TOUCHING FILESYSTEM): write(140339021606912) from thread 344676
                  libsystem_kernel.dylib`__fork+0xb
                  _posixsubprocess.cpython-310-darwin.so`do_fork_exec+0x29
                  _posixsubprocess.cpython-310-darwin.so`subprocess_fork_exec+0x71f
                  python.exe`cfunction_call+0x86
      killing...
      killed.

Shell Blocking Example

# example.py - Executes code upon import;
import os;

os.system('Hello World!');
# production.py - Your production code
from secimport import secure_import 

example = secure_import('example', allow_shells=False)

Let's run the and see what happens:

(root) sh-3.2#  export PYTHONPATH=$(pwd)/src:$(pwd)/examples:$(pwd):$PYTHONPATH
(root) sh-3.2#  python examples/production.py 
Successfully compiled dtrace profile:  /tmp/.secimport/sandbox_example.d
Killed: 9
  • We imported example with limited capabilities.
  • If a syscall like spawn/exec/fork/forkexec will be executed
    • The process will be killed with -9 signal.

Network Blocking Example

>>> import requests
>>> requests.get('https://google.com')
<Response [200]>
  

>>> from secimport import secure_import
>>> requests = secure_import('requests', allow_networking=False)

# The next call should kill the process,
# because we disallowed networking for the requests module.
>>> requests.get('https://google.com')
[1]    86664 killed

Requirements

The only requirement is a Python interpreter that was built with --with-dtrace.

  • See INSTALL.md for a detailed setup from scratch.
  • pip
    • python3 -m pip install secimport
  • Poetry
    • python3 -m pip install poetry && python3 -m poetry build

Tests

python -m pytest

Log4Shell as an example

Not related for python, but for the sake of explanation (Equivilant Demo soon).

  • Log4Shell - CVE-2021-44228
    • Let's say we want to block log4j from doing crazy things.
    • In the following import we deny log4j from opening an LDAP connection / shell:
      • log4j = secure_import('log4j', allow_shells=False, allow_networking=False)
    • This would disable log4j from opening sockets and execute commands, IN THE KERNEL.
    • You can choose any policy you like for any module.

Useful References

TODO:

  • ✔️ Allow/Block list configuration
  • ✔️ Create a .yaml configuration per module in the code
    • ✔️ Use secimport to compile that yml
    • ✔️ Create a single dcript policy
    • ✔️ Run an application with that policy using dtrace, without using secure_import
  • Node support (dtrace hooks)
  • Go support (dtrace hooks)
  • Use current_module_str together with thread ID