Configuration

Options are chainable by using dots (.) and string parameters must be quoted, while booleans and integers aren’t.

Comments are prefixed either with #, or ;.

Some rules apply in a specific function (context) on a specific variable (data), like disable_function. Others can only be enabled/disabled, like harden_random.

Warning

If you configure Snuffleupagus incorrectly, you could break your website. It’s up to you to understand the features, read the present documentation about how to configure them, evaluate your threat model and write your configuration file accordingly.

Most of the features can be used in simulation mode by appending the .simulation() option to them (eg. sp.readonly_exec.simulation().enable();) to see whether or not they could break your website. The simulation mode won’t block the request, but will write a warning in the log.

The rules are evaluated in the order that they are written, the first one to match will terminate the evaluation (except for rules in simulation mode).

Configuration file format

Since PHP ini-like configuration model isn’t flexible enough, Snuffleupagus is using its own format in the file specified by the directive sp.configuration_file in your php.ini file, like sp.configuration_file=/etc/php/conf.d/snuffleupagus.rules.

You can use the , separator to include multiple configuration files: sp.configuration_file=/etc/php/conf.d/snuffleupagus.rules,/etc/php/conf.d/sp_wordpress.rules.

We’re also also supporting glob, so you can write something like: sp.configuration_file=/etc/php/conf.d/*.rules,/etc/php/conf.d/extra/test.rules.

To sum up, you should put this in your php.ini:

module=snuffleupagus.so
sp.configuration_file=/path/to/your/snufflepagus/rules/file.rules

And the snuffleupagus rules into the .rules files.

Miscellaneous

global

This configuration variable contains parameters that are used by multiple features:

  • secret_key: A secret key used by various cryptographic features, like cookies protection or unserialize protection, please ensure the length and complexity is sufficient. You can generate it with functions such as: head -c 256 /dev/urandom | tr -dc 'a-zA-Z0-9'.
sp.global.secret_key("44239bd400aa82e125337c9d4eb8315767411ccd");

Bugclass-killer features

global_strict

global_strict, disabled by default, will enable the strict mode globally, forcing PHP to throw a TypeError exception if an argument type being passed to a function does not match its corresponding declared parameter type.

It can either be enabled or disabled.

sp.global_strict.disable();
sp.global_strict.enable();

harden_random

harden_random, enabled by default, will silently replace the insecure rand and mt_rand functions with the secure PRNG random_int.

It can either be enabled or disabled.

sp.harden_random.enable();
sp.harden_random.disable();

Prevent sloppy comparison

Sloppy comparison prevention, disabled by default, will prevent php type juggling (==): two values with different types will always be different.

It can either be enabled or disabled.

sp.sloppy_comparison.enable();
sp.sloppy_comparison.disable();

unserialize_hmac

unserialize_hmac, disabled by default, will add an integrity check to unserialize calls, preventing arbitrary code execution in their context.

It can either be enabled or disabled and can be used in simulation mode.

sp.unserialize_hmac.enable();
sp.unserialize_hmac.disable();

readonly_exec

readonly_exec, disabled by default, will prevent the execution of writeable PHP files.

It can either be enabled or disabled and can be used in simulation mode.

sp.readonly_exec.enable();

upload_validation

upload_validation, disabled by default, will call a given script upon a file upload, with the path to the file being uploaded as argument and various information about it in the environment:

  • SP_FILENAME: the name of the uploaded file
  • SP_FILESIZE: the size of the file being uploaded
  • SP_REMOTE_ADDR: the ip address of the uploader
  • SP_CURRENT_FILE: the current file being executed

This feature can be used, for example, to check if an uploaded file contains php code, using vld, via this script.

The upload will be allowed if the script returns the value 0. Every other value will prevent the file from being uploaded.

It can either be enabled or disabled and can be used in simulation mode.

sp.upload_validation.script("/var/www/is_valid_php.py").enable();

disable_xxe

disable_xxe, enabled by default, will prevent XXE attacks by disabling the loading of external entities (libxml_disable_entity_loader) in the XML parser.

sp.disable_xxe.enable();

Eval white and blacklist

eval_whitelist and eval_blacklist, disabled by default, allow to respectively specify functions allowed and forbidden from being called inside eval. The functions names are comma-separated.

sp.eval_blacklist.list("system,exec,shell_exec");
sp.eval_whitelist.list("strlen,strcmp").simulation();

The whitelist comes before the black one: if a function is both whitelisted and blacklisted, it’ll be allowed.

Virtual-patching

Snuffleupagus provides virtual-patching via the disable_function directive, allowing you to stop or control dangerous behaviours. In the situation where you have a call to system() that lacks proper user-input validation, this could cause issues as it would lead to an RCE. The virtual-patching would allow this to be prevented.

# Allow `id.php` to restrict system() calls to `id`
sp.disable_function.function("system").filename("id.php").param("cmd").value("id").allow();
sp.disable_function.function("system").filename("id.php").drop()

Of course, this is a trivial example, a lot can be achieved with this feature, as you will see below.

Filters

  • alias(description): human-readable description of the rule
  • cidr(ip/mask): match on the client’s cidr
  • filename(name): match in the file name
  • filename_r(regexp): the file name matching the regexp
  • function(name): match on function name
  • function_r(regexp): the function matching the regexp
  • hash(sha256): match on the file’s sha256 sum
  • line(line_number): match on the file’s line.
  • param(name): match on the function’s parameter name
  • param_r(regexp): match on the function’s parameter regexp
  • param_type(type): match on the function’s parameter type
  • ret(value): match on the function’s return value
  • ret_r(regexp): match with a regexp on the function’s return
  • ret_type(type_name): match on the type_name of the function’s return value
  • value(value): match on a literal value
  • value_r(regexp): match on a value matching the regexp
  • var(name): match on a local variable name
  • key(name): match on the presence of name as a key in the hashtable
  • key_r(regexp): match with regexp on keys in the hashtable

The type must be one of the following values:

  • FALSE: for boolean false
  • TRUE: for boolean true
  • NULL: for the null value
  • LONG: for a long (also know as integer) value
  • DOUBLE: for a double (also known as float) value
  • STRING: for a string
  • OBJECT: for a object
  • ARRAY: for an array
  • RESOURCE: for a resource

Actions

  • allow(): allow the request if the rule matches
  • drop(): drop the request if the rule matches
  • dump(directory): dump the request in the directory if it matches the rule
  • simulation(): enabled the simulation mode

Details

The function filter is able to do various dereferencing:

  • function("AwesomeClass::my_method") will match the method my_method in the class AwesomeClass
  • function("AwesomeNamespace\\my_function") will match the function my_function in the namespace AwesomeNamespace

The param filter is also able to do some dereferencing:

  • param($foo[bar]) will get a match on the value corresponding to the bar key in the hashtable foo. Remember that in PHP, almost every data structure is a hashtable. You can of course nest this like param($foo[bar][$object->array['123']][$batman]).
  • The var filter will walk the calltrace until it finds the variable name, or the end of the calltrace, allowing the filter to match global variables: .var("$_GET[\"param\"]") will match on the GET parameter param.

The filename filter requires a leading /, since paths are absolutes (like /var/www/mywebsite/lib/parse.php). If you would like to have only one configuration file for several vhost in different folders, you can use the filename_r directive to match on the filename (like /lib/parse\.php). Please do note that this filter matches on the file where the function is defined, not the one where the function is called from.

For clarity, the presence of the allow or drop action is mandatory.

Warning

When you’re writing rules, please do keep in mind that the order matters. For example, if you’re denying a call to system() and then allowing it in a more narrowed way later, the call will be denied, because it’ll match the deny first.

If you’re paranoid, we’re providing a php script to automatically generate hash of files containing dangerous functions, and blacklisting them everywhere else.

Limitations

It’s currently not possible to:

  • Hook every language construct, because each of them requires a specific implementation.
  • Hook on the return value of user-defined functions
  • Use extra-convoluted rules for matching, like ${$A}$$B->${'}[1], because if you’re writing things like this, odds are that you’re doing something wrong anyway.
  • Hooks on echo and on print are equivalent: there is no way to hook one without hooking the other, at least for now). This is why hooked print will be displayed as echo in the logs.

Examples

Evaluation order of rules

The following rules will:

  1. Allow calls to system("id")
  2. Issue a trace in the logs on calls to system with its parameters starting with ping, and pursuing evaluation of the remaining rules.
  3. Drop calls to system.
sp.disable_function.function("system").param("cmd").value("id").allow();
sp.disable_function.function("system").param("cmd").value_r("^ping").drop().simulation();
sp.disable_function.function("system").param("cmd").drop();

Miscellaneous examples

# Harden the PRNG
sp.harden_random.enable();

# Disabled XXE
sp.disable_xxe.enable();

# use SameSite on session cookie
sp.cookie.name("PHPSESSID").samesite("lax");

# Harden the `chmod` function
sp.disable_function.function("chmod").param("mode").value_r("^[0-9]{2}[67]$").drop();

# Prevent various `mail`-related vulnerabilities
sp.disable_function.function("mail").param("additional_parameters").value_r("\\-").drop();

# Since it's now burned, me might as well mitigate it publicly
sp.disable_function.function("putenv").param("setting").value_r("LD_").drop()

##Prevent various `include`-related vulnerabilities
sp.disable_function.function("require_once").value_r("\.(inc|phtml|php)$").allow();
sp.disable_function.function("include_once").value_r("\.(inc|phtml|php)$").allow();
sp.disable_function.function("require").value_r("\.(inc|phtml|php)$").allow();
sp.disable_function.function("include").value_r("\.(inc|phtml|php)$").allow();
sp.disable_function.function("require_once").drop()
sp.disable_function.function("include_once").drop()
sp.disable_function.function("require").drop()
sp.disable_function.function("include").drop()

# Prevent `system`-related injections
sp.disable_function.function("system").param("command").value_r("[$|;&`\\n]").drop();
sp.disable_function.function("shell_exec").param("command").value_r("[$|;&`\\n]").drop();
sp.disable_function.function("exec").param("command").value_r("[$|;&`\\n]").drop();
sp.disable_function.function("proc_open").param("command").value_r("[$|;&`\\n]").drop();

# Prevent runtime modification of interesting things
sp.disable_function.function("ini_set").param("var_name").value("assert.active").drop();
sp.disable_function.function("ini_set").param("var_name").value("zend.assertions").drop();
sp.disable_function.function("ini_set").param("var_name").value("memory_limit").drop();
sp.disable_function.function("ini_set").param("var_name").value("include_path").drop();
sp.disable_function.function("ini_set").param("var_name").value("open_basedir").drop();

# Detect some backdoors via environnement recon
sp.disable_function.function("ini_get").param("var_name").value("allow_url_fopen").drop();
sp.disable_function.function("ini_get").param("var_name").value("open_basedir").drop();
sp.disable_function.function("ini_get").param("var_name").value_r("suhosin").drop();
sp.disable_function.function("function_exists").param("function_name").value("eval").drop();
sp.disable_function.function("function_exists").param("function_name").value("exec").drop();
sp.disable_function.function("function_exists").param("function_name").value("system").drop();
sp.disable_function.function("function_exists").param("function_name").value("shell_exec").drop();
sp.disable_function.function("function_exists").param("function_name").value("proc_open").drop();
sp.disable_function.function("function_exists").param("function_name").value("passthru").drop();
sp.disable_function.function("is_callable").param("var").value("eval").drop();
sp.disable_function.function("is_callable").param("var").value("exec").drop();
sp.disable_function.function("is_callable").param("var").value("system").drop();
sp.disable_function.function("is_callable").param("var").value("shell_exec").drop();
sp.disable_function.function("is_callable").param("var").value("proc_open").drop();
sp.disable_function.function("is_callable").param("var").value("passthru").drop();

# Commenting sqli related stuff to improve performance.
# TODO figure out why these functions can't be hooked at startup
# Ghetto sqli hardening
# sp.disable_function.function("mysql_query").param("query").value_r("/\\*").drop();
# sp.disable_function.function("mysql_query").param("query").value_r("--").drop();
# sp.disable_function.function("mysql_query").param("query").value_r("#").drop();
# sp.disable_function.function("mysql_query").param("query").value_r(";.*;").drop();
# sp.disable_function.function("mysql_query").param("query").value_r("benchmark").drop();
# sp.disable_function.function("mysql_query").param("query").value_r("sleep").drop();
# sp.disable_function.function("mysql_query").param("query").value_r("information_schema").drop();

# sp.disable_function.function("mysqli_query").param("query").value_r("/\\*").drop();
# sp.disable_function.function("mysqli_query").param("query").value_r("--").drop();
# sp.disable_function.function("mysqli_query").param("query").value_r("#").drop();
# sp.disable_function.function("mysqli_query").param("query").value_r(";.*;").drop();
# sp.disable_function.function("mysqli_query").param("query").value_r("benchmark").drop();
# sp.disable_function.function("mysqli_query").param("query").value_r("sleep").drop();
# sp.disable_function.function("mysqli_query").param("query").value_r("information_schema").drop();

# sp.disable_function.function("PDO::query").param("query").value_r("/\\*").drop();
# sp.disable_function.function("PDO::query").param("query").value_r("--").drop();
# sp.disable_function.function("PDO::query").param("query").value_r("#").drop();
# sp.disable_function.function("PDO::query").param("query").value_r(";.*;").drop();
# sp.disable_function.function("PDO::query").param("query").value_r("benchmark\\s*\\(").drop();
# sp.disable_function.function("PDO::query").param("query").value_r("sleep\\s*\\(").drop();
# sp.disable_function.function("PDO::query").param("query").value_r("information_schema").drop();

# Ghetto sqli detection
# sp.disable_function.function("mysql_query").ret("FALSE").drop();
# sp.disable_function.function("mysqli_query").ret("FALSE").drop();
# sp.disable_function.function("PDO::query").ret("FALSE").drop();

#File upload
sp.disable_function.function("move_uploaded_file").param("destination").value_r("\\.ph").drop();
sp.disable_function.function("move_uploaded_file").param("destination").value_r("\\.ht").drop();