PPy - privileged python wrapper

26 dec 2009

Summary

The thing is very simple - a C binary, designed as a drop-in replacement for python binary in shebangs or command line for the scripts that need elevated (or just different) privileges then the ones they've been started with. Essentially, it's a remake of suid perl wrapper, but based on capabilities(7) instead of suid.

POSIX Caps fragment root privileges into many subsets and prevent unrestricted propagnation via exec. In a nutshell, when you need to, say, backup whole file system, you have two choices:

  • SUID root, which will allow you to access fs, but should someone replace something like /bin/date, with malicious binary, and you'll run it, he'll get unrestricted root as well, that's not to mention the ever-present holes in the backup script/binary itself.
  • Second choice is to set cap_dac_read_search capability, which not only won't allow anything except just reading fs, but also won't propagnate to any binaries you execute, and you can drop it any time you don't need it, either for good, or retaining capability to pick it up later.

Just scan through man 7 capabilities and you'll see how nice they are ;)

This wrapper allows scripts with posix caps set on them (and fairly secure mode) to retain these caps as permitted, but not immediately-effective, regardless of fs flag.

Note that the wrapper itself need to be either suid root or inherit capabilities via some other way (all+ep, for instance) to get full permitted-set, and needs python binary with used caps set as inherited ('all=i' + root:priveleged_group:0750 setup should be fairly secure, you can clear inheritable set to be on the safe side).

To build the wrapper you'll need libcap-ng (use gcc ppy.c -o ppy -lcap-ng). I'd suggest also having userspace tools for caps manipulation from libcap, since the ones that come with libcap-ng suck.

Also you'll need fairly recent kernel (>=2.6.25) with CONFIG_SECURITY_FILE_CAPABILITIES=y and CONFIG_(your_fs_of_choice)_FS_SECURITY=y.

Usage

setcap 'cap_dac_read_search=i' /path/to/capped/script.py

Setting dac_read_search capability to a file (via libcap binary).

#!/usr/bin/ppy
from capng import *

capng_get_caps_process()
capng_update(CAPNG_ADD,
	CAPNG_EFFECTIVE,
	CAP_DAC_READ_SEARCH)
capng_apply(CAPNG_SELECT_CAPS)

import os
print os.listdir('/root')
		

Should give you listing of the "/root" (normally not readable by user) if you've set dac_read_search cap onto the script file (see above).

ppy /path/to/capped/script.py

Invocation from the command line.

Operation

Well, I bet it's easier to look at the source itself to understand it's operation 'cause it's quite straightforward, but since I've decided to pull this page together, might as well explain it here... in fact, it's just a couple of checks:

  • Simple check that the next argument is not a py binary parameter (starting with '-' or ppy binary itself).
  • Check that this argument (script file) actually exists and is not writable by anyone but user.
  • Get the script uid/gid/caps, copy inheritable set from a file to inheritable set of a process.
  • Check that both ppy and capped-py binaries aren't group- or world-writable and belong to root.
  • Drop privileges to script's uid/gid, retaining capability set; clear bounding set; drop supplement groups.
  • Launch capped-py binary with all the given arguments.

It's a whole lot less checks than in suid perl, but I'm just not that paranoid :P

There are no python-specific quirks in the wrapper itself, so it can be used with pretty much any interpreter, or just any app for that matter.

Also note that to make use of the capabilities, you'll need to set them in the effective set. I have a rant on how weird are python wrappers for these and my own implementation here.

Source is here. Use gcc ppy.c -o ppy -lcap-ng to build it.