Problem
Web applications can be a source of very frequent vulnerabilities. These vulnerabilities can stem from bugs in the program itself as well as the libraries and frameworks upon which it depends. These vulnerabilities are often used as the entry-point for an attacker to upload malicious software onto a system. This software can contain root-kits, bots, data collection agents, or other malicious applications.

Goal
Prevent an attacker from uploading and executing malicious software through vulnerabilities in the web application.

Approach
I’ll use SELinux to confine the web application. This will involve building an SELinux domain for the web application to run in that cannot execute anything it writes to disk or access the network directly. The strictest policy I could write would not allow the domain to write anything to disk at all, but that is often not feasible for web apps, as part of their function often involves receiving uploads. By eliminating the ability to execute something that has been uploaded, I can significantly reduce the attackers ability to utilize previously generated malicious code such as a root-kit. However, the attacker may still be able to send malicious code by taking over the web app process itself. While I cannot prevent this without having a bullet-proof web app, I can limit what the attacker can do by preventing all direct access to the network. This prevents the attacker from using a compromised web app process as a bot. Additionally, I’ll limit the access that the web app has to only what is necessary for it to function in order to prevent the compromised app from scouring the system for information or further vulnerabilities.

I wrote a quick python script to use as my example application. It basically just receives uploads and stores them on disk. It also includes a couple of attacks for testing if my policy is working. I’ll have Apache execute this script as a cgi. Note that this will not work if I was using mod_python, as mod_python executes the script with the apache process. Consequently I’d be forced to try to confine apache itself, and I clearly can’t place all these restrictions on apache.

I created my SELinux policy with SLIDE, which filled in much of the details for me through a basic wizard. This wizard created a type for my application (bb_t) and its corresponding executable (bb_exec_t) and granted some basic accesses that almost everything needs (like access to shared libraries and locale). I then really only needed to add four things to make the application work.

type bb_uploads_t;
files_type(bb_uploads_t)

This creates a type for the uploads directory and all the files that are uploaded. This type cannot be executed by the web app or apache.

corecmd_exec_bin(bb_t)

This simply gives the web app the ability to execute common binaries on the system (mostly in /bin and /usr/bin). In particular, this is necessary to execute python.

manage_files_pattern(bb_t, bb_uploads_t, bb_uploads_t)

This allows the web app the ability to create, read, write, delete, etc. (but not execute) files and directories in the uploads directory.

apache_cgi_domain(bb_t, bb_exec_t)1

This does a couple of things. First, it ensures that when Apache executes our web app, the web app runs in the proper domain (bb_t). Secondly, it allows our web app to talk to Apache.

The full source code of the policy and the test web app are available here.

Results
My example python script includes two exploits. The first allows an attacker to execute something that’s been uploaded. The second assumes that the attacker has taken over the web app itself, and attempts to bind to a network port to await instructions from the attacker. Below, I execute both attacks twice - once with SELinux disabled and once with SELinux enabled.

SELinux disabled

SELinux enabled

To test executing uploaded files, I upload a script that simply prints “You have been hacked!” Note that the SELinux policy prevents this from being executed.

SELinux disabled

SELinux enabled

To test accessing the network, my web app attempts to bind to tcp port 1337 and responds on that port to anything that connects to it. Note again that SELinux policy prevents this access, as well as any other network access it may attempt.

1 This policy is built on the upstream Fedora 7 targeted SELinux policy with one exception. The apache_cgi_domain() interface does not exist there yet. When I started working on this blog entry, this interface did not exist. So, I wrote it and upstreamed it to the Reference Policy project (which serves as upstream for most distros including Fedora). As of today, this interface hasn’t made it into Fedora yet, but it shouldn’t be long. For the above example, I backported this interface into the current Fedora policy.