From cb1221013251b4e9cba8e129edc6b8dbd8fd5145 Mon Sep 17 00:00:00 2001 From: Till Brehm Date: Mon, 25 Aug 2014 15:33:09 +0200 Subject: [PATCH] FS#3640 - Add Intrusion Detection System --- install/dist/lib/fedora.lib.php | 6 + install/dist/lib/gentoo.lib.php | 6 + install/dist/lib/opensuse.lib.php | 6 + install/lib/installer_base.lib.php | 6 + interface/lib/app.inc.php | 13 +- interface/lib/classes/IDS/.htaccess | 5 + .../lib/classes/IDS/Caching/ApcCache.php | 144 +++ .../lib/classes/IDS/Caching/CacheFactory.php | 85 ++ .../classes/IDS/Caching/CacheInterface.php | 64 ++ .../lib/classes/IDS/Caching/DatabaseCache.php | 277 ++++++ .../lib/classes/IDS/Caching/FileCache.php | 189 ++++ .../classes/IDS/Caching/MemcachedCache.php | 181 ++++ .../lib/classes/IDS/Caching/SessionCache.php | 136 +++ interface/lib/classes/IDS/Config.ini.php | 59 ++ .../lib/classes/IDS/Config/Config.ini.php | 91 ++ interface/lib/classes/IDS/Converter.php | 779 +++++++++++++++ interface/lib/classes/IDS/Event.php | 221 +++++ interface/lib/classes/IDS/Filter.php | 175 ++++ interface/lib/classes/IDS/Filter/Storage.php | 401 ++++++++ interface/lib/classes/IDS/Init.php | 174 ++++ interface/lib/classes/IDS/Monitor.php | 566 +++++++++++ interface/lib/classes/IDS/Report.php | 339 +++++++ interface/lib/classes/IDS/Version.php | 49 + interface/lib/classes/IDS/default_filter.json | 933 ++++++++++++++++++ interface/lib/classes/IDS/default_filter.xml | 787 +++++++++++++++ interface/lib/classes/IDS/license.txt | 18 + interface/lib/classes/db_mysql.inc.php | 47 + interface/lib/classes/getconf.inc.php | 18 +- interface/lib/classes/ids.inc.php | 149 +++ security/apache_directives.blacklist | 3 + security/ids.htmlfield | 5 + security/ids.whitelist | 45 + security/security_settings.ini | 9 + 33 files changed, 5979 insertions(+), 7 deletions(-) create mode 100644 interface/lib/classes/IDS/.htaccess create mode 100644 interface/lib/classes/IDS/Caching/ApcCache.php create mode 100644 interface/lib/classes/IDS/Caching/CacheFactory.php create mode 100644 interface/lib/classes/IDS/Caching/CacheInterface.php create mode 100644 interface/lib/classes/IDS/Caching/DatabaseCache.php create mode 100644 interface/lib/classes/IDS/Caching/FileCache.php create mode 100644 interface/lib/classes/IDS/Caching/MemcachedCache.php create mode 100644 interface/lib/classes/IDS/Caching/SessionCache.php create mode 100644 interface/lib/classes/IDS/Config.ini.php create mode 100644 interface/lib/classes/IDS/Config/Config.ini.php create mode 100644 interface/lib/classes/IDS/Converter.php create mode 100644 interface/lib/classes/IDS/Event.php create mode 100644 interface/lib/classes/IDS/Filter.php create mode 100644 interface/lib/classes/IDS/Filter/Storage.php create mode 100644 interface/lib/classes/IDS/Init.php create mode 100644 interface/lib/classes/IDS/Monitor.php create mode 100644 interface/lib/classes/IDS/Report.php create mode 100644 interface/lib/classes/IDS/Version.php create mode 100644 interface/lib/classes/IDS/default_filter.json create mode 100644 interface/lib/classes/IDS/default_filter.xml create mode 100644 interface/lib/classes/IDS/license.txt create mode 100644 interface/lib/classes/ids.inc.php create mode 100644 security/apache_directives.blacklist create mode 100644 security/ids.htmlfield create mode 100644 security/ids.whitelist diff --git a/install/dist/lib/fedora.lib.php b/install/dist/lib/fedora.lib.php index a7179ef8e..7dd7d9b04 100644 --- a/install/dist/lib/fedora.lib.php +++ b/install/dist/lib/fedora.lib.php @@ -1010,6 +1010,12 @@ class installer_dist extends installer_base { caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); $command = 'chown root:ispconfig '.$install_dir.'/security'; caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); + $command = 'chown root:ispconfig '.$install_dir.'/security/ids.whitelist'; + caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); + $command = 'chown root:ispconfig '.$install_dir.'/security/ids.htmlfield'; + caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); + $command = 'chown root:ispconfig '.$install_dir.'/security/apache_directives.blacklist'; + caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); //* Make the global language file directory group writable exec("chmod -R 770 $install_dir/interface/lib/lang"); diff --git a/install/dist/lib/gentoo.lib.php b/install/dist/lib/gentoo.lib.php index 153a4e019..005a2fc2e 100644 --- a/install/dist/lib/gentoo.lib.php +++ b/install/dist/lib/gentoo.lib.php @@ -903,6 +903,12 @@ class installer extends installer_base caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); $command = 'chown root:ispconfig '.$install_dir.'/security'; caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); + $command = 'chown root:ispconfig '.$install_dir.'/security/ids.whitelist'; + caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); + $command = 'chown root:ispconfig '.$install_dir.'/security/ids.htmlfield'; + caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); + $command = 'chown root:ispconfig '.$install_dir.'/security/apache_directives.blacklist'; + caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); //* Make the global language file directory group writable exec("chmod -R 770 $install_dir/interface/lib/lang"); diff --git a/install/dist/lib/opensuse.lib.php b/install/dist/lib/opensuse.lib.php index 935b81608..ea13cd259 100644 --- a/install/dist/lib/opensuse.lib.php +++ b/install/dist/lib/opensuse.lib.php @@ -1081,6 +1081,12 @@ class installer_dist extends installer_base { caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); $command = 'chown root:ispconfig '.$install_dir.'/security'; caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); + $command = 'chown root:ispconfig '.$install_dir.'/security/ids.whitelist'; + caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); + $command = 'chown root:ispconfig '.$install_dir.'/security/ids.htmlfield'; + caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); + $command = 'chown root:ispconfig '.$install_dir.'/security/apache_directives.blacklist'; + caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); //* Make the global language file directory group writable exec("chmod -R 770 $install_dir/interface/lib/lang"); diff --git a/install/lib/installer_base.lib.php b/install/lib/installer_base.lib.php index 538f21e76..a30a7ec02 100644 --- a/install/lib/installer_base.lib.php +++ b/install/lib/installer_base.lib.php @@ -1937,6 +1937,12 @@ class installer_base { caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); $command = 'chown root:ispconfig '.$install_dir.'/security'; caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); + $command = 'chown root:ispconfig '.$install_dir.'/security/ids.whitelist'; + caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); + $command = 'chown root:ispconfig '.$install_dir.'/security/ids.htmlfield'; + caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); + $command = 'chown root:ispconfig '.$install_dir.'/security/apache_directives.blacklist'; + caselog($command.' &> /dev/null', __FILE__, __LINE__, "EXECUTED: $command", "Failed to execute the command $command"); //* Make the global language file directory group writable exec("chmod -R 770 $install_dir/interface/lib/lang"); diff --git a/interface/lib/app.inc.php b/interface/lib/app.inc.php index 8832f45e1..615e39087 100755 --- a/interface/lib/app.inc.php +++ b/interface/lib/app.inc.php @@ -48,6 +48,7 @@ class app { private $_wb; private $_loaded_classes = array(); private $_conf; + private $_security_config; public $loaded_plugins = array(); @@ -109,7 +110,8 @@ class app { } $this->uses('functions'); // we need this before all others! - $this->uses('auth,plugin'); + $this->uses('auth,plugin,ini_parser,getconf'); + } public function __get($prop) { @@ -327,4 +329,13 @@ class app { //* possible future = new app($conf); $app = new app(); +// load and enable PHP Intrusion Detection System (PHPIDS) +$ids_security_config = $app->getconf->get_security_config('ids'); + +if(is_dir(ISPC_CLASS_PATH.'/IDS') && $ids_security_config['ids_enabled'] == 'yes') { + $app->uses('ids'); + $app->ids->start(); +} +unset($ids_security_config); + ?> diff --git a/interface/lib/classes/IDS/.htaccess b/interface/lib/classes/IDS/.htaccess new file mode 100644 index 000000000..878d02ab1 --- /dev/null +++ b/interface/lib/classes/IDS/.htaccess @@ -0,0 +1,5 @@ +# in case PHPIDS is placed in the web-root +deny from all + +# silence is golden +php_flag display_errors off \ No newline at end of file diff --git a/interface/lib/classes/IDS/Caching/ApcCache.php b/interface/lib/classes/IDS/Caching/ApcCache.php new file mode 100644 index 000000000..e82cc88c5 --- /dev/null +++ b/interface/lib/classes/IDS/Caching/ApcCache.php @@ -0,0 +1,144 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ + +namespace IDS\Caching; + +/** + * APC caching wrapper + * + * This class inhabits functionality to get and set cache via memcached. + * + * @category Security + * @package PHPIDS + * @author Yves Berkholz + * @copyright 2007-2009 The PHPIDS Groupoup + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + * @since Version 0.6.5 + */ +class ApcCache implements CacheInterface +{ + /** + * Caching type + * + * @var string + */ + private $type = null; + + /** + * Cache configuration + * + * @var array + */ + private $config = null; + + /** + * Flag if the filter storage has been found in memcached + * + * @var boolean + */ + private $isCached = false; + + /** + * Holds an instance of this class + * + * @var object + */ + private static $cachingInstance = null; + + /** + * Constructor + * + * @param string $type caching type + * @param array $init the IDS_Init object + * + * @return void + */ + public function __construct($type, $init) + { + $this->type = $type; + $this->config = $init->config['Caching']; + } + + /** + * Returns an instance of this class + * + * @param string $type caching type + * @param object $init the IDS_Init object + * + * @return object $this + */ + public static function getInstance($type, $init) + { + if (!self::$cachingInstance) { + self::$cachingInstance = new ApcCache($type, $init); + } + + return self::$cachingInstance; + } + + /** + * Writes cache data + * + * @param array $data the caching data + * + * @return object $this + */ + public function setCache(array $data) + { + if (!$this->isCached) { + apc_store( + $this->config['key_prefix'] . '.storage', + $data, + $this->config['expiration_time'] + ); + } + + return $this; + } + + /** + * Returns the cached data + * + * Note that this method returns false if either type or file cache is + * not set + * + * @return mixed cache data or false + */ + public function getCache() + { + $data = apc_fetch($this->config['key_prefix'] . '.storage'); + $this->isCached = !empty($data); + + return $data; + } +} diff --git a/interface/lib/classes/IDS/Caching/CacheFactory.php b/interface/lib/classes/IDS/Caching/CacheFactory.php new file mode 100644 index 000000000..c35833255 --- /dev/null +++ b/interface/lib/classes/IDS/Caching/CacheFactory.php @@ -0,0 +1,85 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +namespace IDS\Caching; + +/** + * Caching factory + * + * This class is used as a factory to load the correct concrete caching + * implementation. + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Group + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + * @since Version 0.4 + */ +class CacheFactory +{ + /** + * Factory method + * + * @param object $init the IDS_Init object + * @param string $type the caching type + * + * @return object the caching facility + */ + public static function factory($init, $type) + { + $object = false; + $wrapper = preg_replace( + '/\W+/m', + null, + ucfirst($init->config['Caching']['caching']) + ); + $class = '\\IDS\\Caching\\' . $wrapper . 'Cache'; + $path = dirname(__FILE__) . DIRECTORY_SEPARATOR . $wrapper . 'Cache.php'; + + if (file_exists($path)) { + include_once $path; + + if (class_exists($class)) { + $object = call_user_func( + array('' . $class, 'getInstance'), + $type, + $init + ); + } + } + + return $object; + } +} diff --git a/interface/lib/classes/IDS/Caching/CacheInterface.php b/interface/lib/classes/IDS/Caching/CacheInterface.php new file mode 100644 index 000000000..c1a6a6092 --- /dev/null +++ b/interface/lib/classes/IDS/Caching/CacheInterface.php @@ -0,0 +1,64 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +namespace IDS\Caching; + +/** + * Caching wrapper interface + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Group + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @since Version 0.4 + * @link http://php-ids.org/ + */ +interface CacheInterface +{ + /** + * Interface method + * + * @param array $data the cache data + * + * @return void + */ + public function setCache(array $data); + + /** + * Interface method + * + * @return void + */ + public function getCache(); +} diff --git a/interface/lib/classes/IDS/Caching/DatabaseCache.php b/interface/lib/classes/IDS/Caching/DatabaseCache.php new file mode 100644 index 000000000..09c77a87d --- /dev/null +++ b/interface/lib/classes/IDS/Caching/DatabaseCache.php @@ -0,0 +1,277 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +namespace IDS\Caching; + +/** + * + */ + +/** + * Database caching wrapper + * + * This class inhabits functionality to get and set cache via a database. + * + * Needed SQL: + * + +#create the database + +CREATE DATABASE IF NOT EXISTS `phpids` DEFAULT CHARACTER +SET utf8 COLLATE utf8_general_ci; +DROP TABLE IF EXISTS `cache`; + +#now select the created datbase and create the table + +CREATE TABLE `cache` ( +`type` VARCHAR( 32 ) NOT null , +`data` TEXT NOT null , +`created` DATETIME NOT null , +`modified` DATETIME NOT null +) ENGINE = MYISAM ; + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Groupup + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + * @since Version 0.4 + */ +class DatabaseCache implements CacheInterface +{ + + /** + * Caching type + * + * @var string + */ + private $type = null; + + /** + * Cache configuration + * + * @var array + */ + private $config = null; + + /** + * DBH + * + * @var object + */ + private $handle = null; + + /** + * Holds an instance of this class + * + * @var object + */ + private static $cachingInstance = null; + + /** + * Constructor + * + * Connects to database. + * + * @param string $type caching type + * @param object $init the IDS_Init object + * + * @return void + */ + public function __construct($type, $init) + { + $this->type = $type; + $this->config = $init->config['Caching']; + $this->handle = $this->connect(); + } + + /** + * Returns an instance of this class + * + * @static + * @param string $type caching type + * @param object $init the IDS_Init object + * + * @return object $this + */ + public static function getInstance($type, $init) + { + + if (!self::$cachingInstance) { + self::$cachingInstance = new DatabaseCache($type, $init); + } + + return self::$cachingInstance; + } + + /** + * Writes cache data into the database + * + * @param array $data the caching data + * + * @throws PDOException if a db error occurred + * @return object $this + */ + public function setCache(array $data) + { + $handle = $this->handle; + + $rows = $handle->query('SELECT created FROM `' . $this->config['table'].'`'); + + if (!$rows || $rows->rowCount() === 0) { + + $this->write($handle, $data); + } else { + + foreach ($rows as $row) { + + if ((time()-strtotime($row['created'])) > + $this->config['expiration_time']) { + + $this->write($handle, $data); + } + } + } + + return $this; + } + + /** + * Returns the cached data + * + * Note that this method returns false if either type or file cache is + * not set + * + * @throws PDOException if a db error occurred + * @return mixed cache data or false + */ + public function getCache() + { + try { + $handle = $this->handle; + $result = $handle->prepare( + 'SELECT * FROM `' . + $this->config['table'] . + '` where type=?' + ); + $result->execute(array($this->type)); + + foreach ($result as $row) { + return unserialize($row['data']); + } + + } catch (\PDOException $e) { + throw new \PDOException('PDOException: ' . $e->getMessage()); + } + + return false; + } + + /** + * Connect to database and return a handle + * + * @return object PDO + * @throws Exception if connection parameters are faulty + * @throws PDOException if a db error occurred + */ + private function connect() + { + // validate connection parameters + if (!$this->config['wrapper'] + || !$this->config['user'] + || !$this->config['password'] + || !$this->config['table']) { + + throw new \Exception('Insufficient connection parameters'); + } + + // try to connect + try { + $handle = new \PDO( + $this->config['wrapper'], + $this->config['user'], + $this->config['password'] + ); + $handle->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); + + } catch (\PDOException $e) { + throw new \PDOException('PDOException: ' . $e->getMessage()); + } + + return $handle; + } + + /** + * Write the cache data to the table + * + * @param object $handle the database handle + * @param array $data the caching data + * + * @return object PDO + * @throws PDOException if a db error occurred + */ + private function write($handle, $data) + { + try { + $handle->query('TRUNCATE ' . $this->config['table'].''); + $statement = $handle->prepare( + 'INSERT INTO `' . + $this->config['table'].'` ( + type, + data, + created, + modified + ) + VALUES ( + :type, + :data, + now(), + now() + )' + ); + + $statement->bindValue( + 'type', + $handle->quote($this->type) + ); + $statement->bindValue('data', serialize($data)); + + if (!$statement->execute()) { + throw new \PDOException($statement->errorCode()); + } + } catch (\PDOException $e) { + throw new \PDOException('PDOException: ' . $e->getMessage()); + } + } +} diff --git a/interface/lib/classes/IDS/Caching/FileCache.php b/interface/lib/classes/IDS/Caching/FileCache.php new file mode 100644 index 000000000..71f01c7dc --- /dev/null +++ b/interface/lib/classes/IDS/Caching/FileCache.php @@ -0,0 +1,189 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +namespace IDS\Caching; + +use IDS\Init; + +/** + * File caching wrapper + * + * This class inhabits functionality to get and set cache via a static flatfile. + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Group + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + * @since Version 0.4 + */ +class FileCache implements CacheInterface +{ + /** + * Caching type + * + * @var string + */ + private $type; + + /** + * Cache configuration + * + * @var array + */ + private $config; + + /** + * Path to cache file + * + * @var string + */ + private $path; + + /** + * Holds an instance of this class + * + * @var object + */ + private static $cachingInstance; + + /** + * Constructor + * + * @param string $type caching type + * @param object $init the IDS_Init object + * @throws \Exception + * + * @return void + */ + public function __construct($type, Init $init) + { + $this->type = $type; + $this->config = $init->config['Caching']; + $this->path = $init->getBasePath() . $this->config['path']; + + if (file_exists($this->path) && !is_writable($this->path)) { + throw new \Exception( + 'Make sure all files in ' . + htmlspecialchars($this->path, ENT_QUOTES, 'UTF-8') . + 'are writeable!' + ); + } + } + + /** + * Returns an instance of this class + * + * @param string $type caching type + * @param object $init the IDS_Init object + * + * @return object $this + */ + public static function getInstance($type, $init) + { + if (!self::$cachingInstance) { + self::$cachingInstance = new FileCache($type, $init); + } + + return self::$cachingInstance; + } + + /** + * Writes cache data into the file + * + * @param array $data the cache data + * + * @throws Exception if cache file couldn't be created + * @return object $this + */ + public function setCache(array $data) + { + if (!is_writable(preg_replace('/[\/][^\/]+\.[^\/]++$/', null, $this->path))) { + throw new \Exception( + 'Temp directory ' . + htmlspecialchars($this->path, ENT_QUOTES, 'UTF-8') . + ' seems not writable' + ); + } + + if (!$this->isValidFile($this->path)) { + $handle = @fopen($this->path, 'w+'); + + if (!$handle) { + throw new \Exception("Cache file couldn't be created"); + } + + $serialized = @serialize($data); + if (!$serialized) { + throw new \Exception("Cache data couldn't be serialized"); + } + + fwrite($handle, $serialized); + fclose($handle); + } + + return $this; + } + + /** + * Returns the cached data + * + * Note that this method returns false if either type or file cache is + * not set + * + * @return mixed cache data or false + */ + public function getCache() + { + // make sure filters are parsed again if cache expired + if (!$this->isValidFile($this->path)) { + return false; + } + + $data = unserialize(file_get_contents($this->path)); + + return $data; + } + + /** + * Returns true if the cache file is still valid + * + * @param string $file + * @return bool + */ + private function isValidFile($file) + { + return file_exists($file) && time() - filectime($file) <= $this->config['expiration_time']; + } +} diff --git a/interface/lib/classes/IDS/Caching/MemcachedCache.php b/interface/lib/classes/IDS/Caching/MemcachedCache.php new file mode 100644 index 000000000..b3dd5b72d --- /dev/null +++ b/interface/lib/classes/IDS/Caching/MemcachedCache.php @@ -0,0 +1,181 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +namespace IDS\Caching; + +/** + * File caching wrapper + * + * This class inhabits functionality to get and set cache via memcached. + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Groupoup + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + * @since Version 0.4 + */ +class MemcachedCache implements CacheInterface +{ + /** + * Caching type + * + * @var string + */ + private $type = null; + + /** + * Cache configuration + * + * @var array + */ + private $config = null; + + /** + * Flag if the filter storage has been found in memcached + * + * @var boolean + */ + private $isCached = false; + + /** + * Memcache object + * + * @var object + */ + private $memcache = null; + + /** + * Holds an instance of this class + * + * @var object + */ + private static $cachingInstance = null; + + /** + * Constructor + * + * @param string $type caching type + * @param array $init the IDS_Init object + * + * @return void + */ + public function __construct($type, $init) + { + + $this->type = $type; + $this->config = $init->config['Caching']; + + $this->connect(); + } + + /** + * Returns an instance of this class + * + * @param string $type caching type + * @param object $init the IDS_Init object + * + * @return object $this + */ + public static function getInstance($type, $init) + { + if (!self::$cachingInstance) { + self::$cachingInstance = new MemcachedCache($type, $init); + } + + return self::$cachingInstance; + } + + /** + * Writes cache data + * + * @param array $data the caching data + * + * @return object $this + */ + public function setCache(array $data) + { + if (!$this->isCached) { + $this->memcache->set( + $this->config['key_prefix'] . '.storage', + $data, + false, + $this->config['expiration_time'] + ); + } + + return $this; + } + + /** + * Returns the cached data + * + * Note that this method returns false if either type or file cache is + * not set + * + * @return mixed cache data or false + */ + public function getCache() + { + $data = $this->memcache->get( + $this->config['key_prefix'] . + '.storage' + ); + $this->isCached = !empty($data); + + return $data; + } + + /** + * Connect to the memcached server + * + * @throws Exception if connection parameters are insufficient + * @return void + */ + private function connect() + { + + if ($this->config['host'] && $this->config['port']) { + // establish the memcache connection + $this->memcache = new \Memcache; + $this->memcache->pconnect( + $this->config['host'], + $this->config['port'] + ); + + } else { + throw new \Exception('Insufficient connection parameters'); + } + } +} diff --git a/interface/lib/classes/IDS/Caching/SessionCache.php b/interface/lib/classes/IDS/Caching/SessionCache.php new file mode 100644 index 000000000..7168a63ce --- /dev/null +++ b/interface/lib/classes/IDS/Caching/SessionCache.php @@ -0,0 +1,136 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ + +namespace IDS\Caching; + +/** + * File caching wrapper + * + * This class inhabits functionality to get and set cache via session. + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Group + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + * @since Version 0.4 + */ +class SessionCache implements CacheInterface +{ + /** + * Caching type + * + * @var string + */ + private $type = null; + + /** + * Cache configuration + * + * @var array + */ + private $config = null; + + /** + * Holds an instance of this class + * + * @var object + */ + private static $cachingInstance = null; + + /** + * Constructor + * + * @param string $type caching type + * @param object $init the IDS_Init object + * + * @return void + */ + public function __construct($type, $init) + { + $this->type = $type; + $this->config = $init->config['Caching']; + } + + /** + * Returns an instance of this class + * + * @param string $type caching type + * @param object $init the IDS_Init object + * + * @return object $this + */ + public static function getInstance($type, $init) + { + + if (!self::$cachingInstance) { + self::$cachingInstance = new SessionCache($type, $init); + } + + return self::$cachingInstance; + } + + /** + * Writes cache data into the session + * + * @param array $data the caching data + * + * @return object $this + */ + public function setCache(array $data) + { + + $_SESSION['PHPIDS'][$this->type] = $data; + + return $this; + } + + /** + * Returns the cached data + * + * Note that this method returns false if either type or file cache is not set + * + * @return mixed cache data or false + */ + public function getCache() + { + + if ($this->type && $_SESSION['PHPIDS'][$this->type]) { + return $_SESSION['PHPIDS'][$this->type]; + } + + return false; + } +} diff --git a/interface/lib/classes/IDS/Config.ini.php b/interface/lib/classes/IDS/Config.ini.php new file mode 100644 index 000000000..f87ccc8d6 --- /dev/null +++ b/interface/lib/classes/IDS/Config.ini.php @@ -0,0 +1,59 @@ +; + +; PHPIDS Config.ini + +; General configuration settings + +[General] + + ; basic settings - customize to make the PHPIDS work at all + filter_type = xml + + base_path = /full/path/to/IDS/ + use_base_path = false + + filter_path = default_filter.xml + tmp_path = tmp + scan_keys = false + + ; in case you want to use a different HTMLPurifier source, specify it here + ; By default, those files are used that are being shipped with PHPIDS + HTML_Purifier_Cache = vendors/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer + + ; define which fields contain html and need preparation before + ; hitting the PHPIDS rules (new in PHPIDS 0.5) + ;html[] = POST.__wysiwyg + + ; define which fields contain JSON data and should be treated as such + ; for fewer false positives (new in PHPIDS 0.5.3) + ;json[] = POST.__jsondata + + ; define which fields shouldn't be monitored (a[b]=c should be referenced via a.b) + exceptions[] = GET.__utmz + exceptions[] = GET.__utmc + exceptions[] = POST.maildir_path + + ; you can use regular expressions for wildcard exceptions - example: /.*foo/i + +[Caching] + + ; caching: session|file|database|memcached|apc|none + caching = file + expiration_time = 600 + + ; file cache + path = tmp/default_filter.cache + + ; database cache + wrapper = "mysql:host=localhost;port=3306;dbname=phpids" + user = phpids_user + password = 123456 + table = cache + + ; memcached + ;host = localhost + ;port = 11211 + ;key_prefix = PHPIDS + + ; apc + ;key_prefix = PHPIDS diff --git a/interface/lib/classes/IDS/Config/Config.ini.php b/interface/lib/classes/IDS/Config/Config.ini.php new file mode 100644 index 000000000..080055298 --- /dev/null +++ b/interface/lib/classes/IDS/Config/Config.ini.php @@ -0,0 +1,91 @@ +; + +; PHPIDS Config.ini + +; General configuration settings + +[General] + + ; basic settings - customize to make the PHPIDS work at all + filter_type = xml + + base_path = /full/path/to/IDS/ + use_base_path = false + + filter_path = default_filter.xml + tmp_path = tmp + scan_keys = false + + ; in case you want to use a different HTMLPurifier source, specify it here + ; By default, those files are used that are being shipped with PHPIDS + HTML_Purifier_Cache = vendors/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer + + ; define which fields contain html and need preparation before + ; hitting the PHPIDS rules (new in PHPIDS 0.5) + ;html[] = POST.__wysiwyg + ;html[] = POST.records + ;html[] = REQUEST.records + + ; define which fields contain JSON data and should be treated as such + ; for fewer false positives (new in PHPIDS 0.5.3) + ;json[] = POST.__jsondata + + ; define which fields shouldn't be monitored (a[b]=c should be referenced via a.b) + ; exceptions[] = GET.__utmz + ; exceptions[] = GET.__utmc + ; exceptions[] = POST.maildir_path + ; exceptions[] = REQUEST.maildir_path + ; exceptions[] = REQUEST.website_path + ; exceptions[] = REQUEST.website_symlinks + ; exceptions[] = REQUEST.vhost_conf_dir + ; exceptions[] = REQUEST.vhost_conf_enabled_dir + ; exceptions[] = REQUEST.nginx_vhost_conf_dir + ; exceptions[] = REQUEST.nginx_vhost_conf_enabled_dir + ; exceptions[] = REQUEST.php_open_basedir + ; exceptions[] = REQUEST.awstats_pl + ; exceptions[] = POST.website_path + ; exceptions[] = POST.website_symlinks + ; exceptions[] = POST.vhost_conf_dir + ; exceptions[] = POST.vhost_conf_enabled_dir + ; exceptions[] = POST.nginx_vhost_conf_dir + ; exceptions[] = POST.nginx_vhost_conf_enabled_dir + ; exceptions[] = POST.php_open_basedir + ; exceptions[] = POST.awstats_pl + ; exceptions[] = REQUEST.fastcgi_starter_path + ; exceptions[] = REQUEST.fastcgi_bin + ; exceptions[] = POST.fastcgi_starter_path + ; exceptions[] = POST.fastcgi_bin + ; exceptions[] = REQUEST.jailkit_chroot_home + ; exceptions[] = POST.jailkit_chroot_home + ; exceptions[] = REQUEST.phpmyadmin_url + ; exceptions[] = REQUEST.phpmyadmin_url + ; exceptions[] = REQUEST.records.weak_password_txt + ; exceptions[] = POST.records.weak_password_txt + + + + ; you can use regular expressions for wildcard exceptions - example: /.*foo/i + +[Caching] + + ; caching: session|file|database|memcached|apc|none + caching = file + expiration_time = 600 + + ; file cache + path = tmp/default_filter.cache + + ; database cache + wrapper = "mysql:host=localhost;port=3306;dbname=phpids" + user = phpids_user + password = 123456 + table = cache + + ; memcached + ;host = localhost + ;port = 11211 + ;key_prefix = PHPIDS + + ; apc + ;key_prefix = PHPIDS + diff --git a/interface/lib/classes/IDS/Converter.php b/interface/lib/classes/IDS/Converter.php new file mode 100644 index 000000000..770d1ccc9 --- /dev/null +++ b/interface/lib/classes/IDS/Converter.php @@ -0,0 +1,779 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ + +/** + * PHPIDS specific utility class to convert charsets manually + * + * Note that if you make use of IDS_Converter::runAll(), existing class + * methods will be executed in the same order as they are implemented in the + * class tree! + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Group + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ + +namespace IDS; + +class Converter +{ + /** + * Runs all converter functions + * + * Note that if you make use of IDS_Converter::runAll(), existing class + * methods will be executed in the same order as they are implemented in the + * class tree! + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function runAll($value) + { + foreach (get_class_methods(__CLASS__) as $method) { + if (strpos($method, 'run') !== 0) { + $value = self::$method($value); + } + } + + return $value; + } + + /** + * Check for comments and erases them if available + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromCommented($value) + { + // check for existing comments + if (preg_match('/(?:\|\/\*|\*\/|\/\/\W*\w+\s*$)|(?:--[^-]*-)/ms', $value)) { + + $pattern = array( + '/(?:(?:))/ms', + '/(?:(?:\/\*\/*[^\/\*]*)+\*\/)/ms', + '/(?:--[^-]*-)/ms' + ); + + $converted = preg_replace($pattern, ';', $value); + $value .= "\n" . $converted; + } + + //make sure inline comments are detected and converted correctly + $value = preg_replace('/(<\w+)\/+(\w+=?)/m', '$1/$2', $value); + $value = preg_replace('/[^\\\:]\/\/(.*)$/m', '/**/$1', $value); + $value = preg_replace('/([^\-&])#.*[\r\n\v\f]/m', '$1', $value); + $value = preg_replace('/([^&\-])#.*\n/m', '$1 ', $value); + $value = preg_replace('/^#.*\n/m', ' ', $value); + + return $value; + } + + /** + * Strip newlines + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromWhiteSpace($value) + { + //check for inline linebreaks + $search = array('\r', '\n', '\f', '\t', '\v'); + $value = str_replace($search, ';', $value); + + // replace replacement characters regular spaces + $value = str_replace('�', ' ', $value); + + //convert real linebreaks + return preg_replace('/(?:\n|\r|\v)/m', ' ', $value); + } + + /** + * Checks for common charcode pattern and decodes them + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromJSCharcode($value) + { + $matches = array(); + + // check if value matches typical charCode pattern + if (preg_match_all('/(?:[\d+-=\/\* ]+(?:\s?,\s?[\d+-=\/\* ]+)){4,}/ms', $value, $matches)) { + $converted = ''; + $string = implode(',', $matches[0]); + $string = preg_replace('/\s/', '', $string); + $string = preg_replace('/\w+=/', '', $string); + $charcode = explode(',', $string); + + foreach ($charcode as $char) { + $char = preg_replace('/\W0/s', '', $char); + + if (preg_match_all('/\d*[+-\/\* ]\d+/', $char, $matches)) { + $match = preg_split('/(\W?\d+)/', implode('', $matches[0]), null, PREG_SPLIT_DELIM_CAPTURE); + + if (array_sum($match) >= 20 && array_sum($match) <= 127) { + $converted .= chr(array_sum($match)); + } + + } elseif (!empty($char) && $char >= 20 && $char <= 127) { + $converted .= chr($char); + } + } + + $value .= "\n" . $converted; + } + + // check for octal charcode pattern + if (preg_match_all('/(?:(?:[\\\]+\d+[ \t]*){8,})/ims', $value, $matches)) { + $converted = ''; + $charcode = explode('\\', preg_replace('/\s/', '', implode(',', $matches[0]))); + + foreach (array_map('octdec', array_filter($charcode)) as $char) { + if (20 <= $char && $char <= 127) { + $converted .= chr($char); + } + } + $value .= "\n" . $converted; + } + + // check for hexadecimal charcode pattern + if (preg_match_all('/(?:(?:[\\\]+\w+\s*){8,})/ims', $value, $matches)) { + $converted = ''; + $charcode = explode('\\', preg_replace('/[ux]/', '', implode(',', $matches[0]))); + + foreach (array_map('hexdec', array_filter($charcode)) as $char) { + if (20 <= $char && $char <= 127) { + $converted .= chr($char); + } + } + $value .= "\n" . $converted; + } + + return $value; + } + + /** + * Eliminate JS regex modifiers + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertJSRegexModifiers($value) + { + return preg_replace('/\/[gim]+/', '/', $value); + } + + /** + * Converts from hex/dec entities + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertEntities($value) + { + $converted = null; + + //deal with double encoded payload + $value = preg_replace('/&/', '&', $value); + + if (preg_match('/&#x?[\w]+/ms', $value)) { + $converted = preg_replace('/(&#x?[\w]{2}\d?);?/ms', '$1;', $value); + $converted = html_entity_decode($converted, ENT_QUOTES, 'UTF-8'); + $value .= "\n" . str_replace(';;', ';', $converted); + } + + // normalize obfuscated protocol handlers + $value = preg_replace( + '/(?:j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*:)|(d\s*a\s*t\s*a\s*:)/ms', + 'javascript:', + $value + ); + + return $value; + } + + /** + * Normalize quotes + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertQuotes($value) + { + // normalize different quotes to " + $pattern = array('\'', '`', '´', '’', '‘'); + $value = str_replace($pattern, '"', $value); + + //make sure harmless quoted strings don't generate false alerts + $value = preg_replace('/^"([^"=\\!><~]+)"$/', '$1', $value); + + return $value; + } + + /** + * Converts SQLHEX to plain text + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromSQLHex($value) + { + $matches = array(); + if (preg_match_all('/(?:(?:\A|[^\d])0x[a-f\d]{3,}[a-f\d]*)+/im', $value, $matches)) { + foreach ($matches[0] as $match) { + $converted = ''; + foreach (str_split($match, 2) as $hex_index) { + if (preg_match('/[a-f\d]{2,3}/i', $hex_index)) { + $converted .= chr(hexdec($hex_index)); + } + } + $value = str_replace($match, $converted, $value); + } + } + // take care of hex encoded ctrl chars + $value = preg_replace('/0x\d+/m', ' 1 ', $value); + + return $value; + } + + /** + * Converts basic SQL keywords and obfuscations + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromSQLKeywords($value) + { + $pattern = array( + '/(?:is\s+null)|(like\s+null)|' . + '(?:(?:^|\W)in[+\s]*\([\s\d"]+[^()]*\))/ims' + ); + $value = preg_replace($pattern, '"=0', $value); + + $value = preg_replace('/[^\w\)]+\s*like\s*[^\w\s]+/ims', '1" OR "1"', $value); + $value = preg_replace('/null([,"\s])/ims', '0$1', $value); + $value = preg_replace('/\d+\./ims', ' 1', $value); + $value = preg_replace('/,null/ims', ',0', $value); + $value = preg_replace('/(?:between)/ims', 'or', $value); + $value = preg_replace('/(?:and\s+\d+\.?\d*)/ims', '', $value); + $value = preg_replace('/(?:\s+and\s+)/ims', ' or ', $value); + + $pattern = array( + '/(?:not\s+between)|(?:is\s+not)|(?:not\s+in)|' . + '(?:xor|<>|rlike(?:\s+binary)?)|' . + '(?:regexp\s+binary)|' . + '(?:sounds\s+like)/ims' + ); + $value = preg_replace($pattern, '!', $value); + $value = preg_replace('/"\s+\d/', '"', $value); + $value = preg_replace('/(\W)div(\W)/ims', '$1 OR $2', $value); + $value = preg_replace('/\/(?:\d+|null)/', null, $value); + + return $value; + } + + /** + * Detects nullbytes and controls chars via ord() + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromControlChars($value) + { + // critical ctrl values + $search = array( + chr(0), chr(1), chr(2), chr(3), chr(4), chr(5), + chr(6), chr(7), chr(8), chr(11), chr(12), chr(14), + chr(15), chr(16), chr(17), chr(18), chr(19), chr(24), + chr(25), chr(192), chr(193), chr(238), chr(255), '\\0' + ); + + $value = str_replace($search, '%00', $value); + + //take care for malicious unicode characters + $value = urldecode( + preg_replace( + '/(?:%E(?:2|3)%8(?:0|1)%(?:A|8|9)\w|%EF%BB%BF|%EF%BF%BD)|(?:&#(?:65|8)\d{3};?)/i', + null, + urlencode($value) + ) + ); + $value = urlencode($value); + $value = preg_replace('/(?:%F0%80%BE)/i', '>', $value); + $value = preg_replace('/(?:%F0%80%BC)/i', '<', $value); + $value = preg_replace('/(?:%F0%80%A2)/i', '"', $value); + $value = preg_replace('/(?:%F0%80%A7)/i', '\'', $value); + $value = urldecode($value); + + $value = preg_replace('/(?:%ff1c)/', '<', $value); + $value = preg_replace('/(?:&[#x]*(200|820|200|820|zwn?j|lrm|rlm)\w?;?)/i', null, $value); + $value = preg_replace( + '/(?:&#(?:65|8)\d{3};?)|' . + '(?:&#(?:56|7)3\d{2};?)|' . + '(?:&#x(?:fe|20)\w{2};?)|' . + '(?:&#x(?:d[c-f])\w{2};?)/i', + null, + $value + ); + + $value = str_replace( + array( + '«', + '〈', + '<', + '‹', + '〈', + '⟨' + ), + '<', + $value + ); + $value = str_replace( + array( + '»', + '〉', + '>', + '›', + '〉', + '⟩' + ), + '>', + $value + ); + + return $value; + } + + /** + * This method matches and translates base64 strings and fragments + * used in data URIs + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromNestedBase64($value) + { + $matches = array(); + preg_match_all('/(?:^|[,&?])\s*([a-z0-9]{50,}=*)(?:\W|$)/im', $value, $matches); + + foreach ($matches[1] as $item) { + if (isset($item) && !preg_match('/[a-f0-9]{32}/i', $item)) { + $base64_item = base64_decode($item); + $value = str_replace($item, $base64_item, $value); + } + } + + return $value; + } + + /** + * Detects nullbytes and controls chars via ord() + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromOutOfRangeChars($value) + { + $values = str_split($value); + foreach ($values as $item) { + if (ord($item) >= 127) { + $value = str_replace($item, ' ', $value); + } + } + + return $value; + } + + /** + * Strip XML patterns + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromXML($value) + { + $converted = strip_tags($value); + if (!$converted || $converted === $value) { + return $value; + } else { + return $value . "\n" . $converted; + } + } + + /** + * This method converts JS unicode code points to + * regular characters + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromJSUnicode($value) + { + $matches = array(); + preg_match_all('/\\\u[0-9a-f]{4}/ims', $value, $matches); + + if (!empty($matches[0])) { + foreach ($matches[0] as $match) { + $chr = chr(hexdec(substr($match, 2, 4))); + $value = str_replace($match, $chr, $value); + } + $value .= "\n\u0001"; + } + + return $value; + } + + /** + * Converts relevant UTF-7 tags to UTF-8 + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromUTF7($value) + { + if (preg_match('/\+A\w+-?/m', $value)) { + if (function_exists('mb_convert_encoding')) { + if (version_compare(PHP_VERSION, '5.2.8', '<')) { + $tmp_chars = str_split($value); + $value = ''; + foreach ($tmp_chars as $char) { + if (ord($char) <= 127) { + $value .= $char; + } + } + } + $value .= "\n" . mb_convert_encoding($value, 'UTF-8', 'UTF-7'); + } else { + //list of all critical UTF7 codepoints + $schemes = array( + '+ACI-' => '"', + '+ADw-' => '<', + '+AD4-' => '>', + '+AFs-' => '[', + '+AF0-' => ']', + '+AHs-' => '{', + '+AH0-' => '}', + '+AFw-' => '\\', + '+ADs-' => ';', + '+ACM-' => '#', + '+ACY-' => '&', + '+ACU-' => '%', + '+ACQ-' => '$', + '+AD0-' => '=', + '+AGA-' => '`', + '+ALQ-' => '"', + '+IBg-' => '"', + '+IBk-' => '"', + '+AHw-' => '|', + '+ACo-' => '*', + '+AF4-' => '^', + '+ACIAPg-' => '">', + '+ACIAPgA8-' => '">' + ); + + $value = str_ireplace( + array_keys($schemes), + array_values($schemes), + $value + ); + } + } + + return $value; + } + + /** + * Converts basic concatenations + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromConcatenated($value) + { + //normalize remaining backslashes + if ($value != preg_replace('/(\w)\\\/', "$1", $value)) { + $value .= preg_replace('/(\w)\\\/', "$1", $value); + } + + $compare = stripslashes($value); + + $pattern = array( + '/(?:<\/\w+>\+<\w+>)/s', + '/(?:":\d+[^"[]+")/s', + '/(?:"?"\+\w+\+")/s', + '/(?:"\s*;[^"]+")|(?:";[^"]+:\s*")/s', + '/(?:"\s*(?:;|\+).{8,18}:\s*")/s', + '/(?:";\w+=)|(?:!""&&")|(?:~)/s', + '/(?:"?"\+""?\+?"?)|(?:;\w+=")|(?:"[|&]{2,})/s', + '/(?:"\s*\W+")/s', + '/(?:";\w\s*\+=\s*\w?\s*")/s', + '/(?:"[|&;]+\s*[^|&\n]*[|&]+\s*"?)/s', + '/(?:";\s*\w+\W+\w*\s*[|&]*")/s', + '/(?:"\s*"\s*\.)/s', + '/(?:\s*new\s+\w+\s*[+",])/', + '/(?:(?:^|\s+)(?:do|else)\s+)/', + '/(?:[{(]\s*new\s+\w+\s*[)}])/', + '/(?:(this|self)\.)/', + '/(?:undefined)/', + '/(?:in\s+)/' + ); + + // strip out concatenations + $converted = preg_replace($pattern, null, $compare); + + //strip object traversal + $converted = preg_replace('/\w(\.\w\()/', "$1", $converted); + + // normalize obfuscated method calls + $converted = preg_replace('/\)\s*\+/', ")", $converted); + + //convert JS special numbers + $converted = preg_replace( + '/(?:\(*[.\d]e[+-]*[^a-z\W]+\)*)|(?:NaN|Infinity)\W/ims', + 1, + $converted + ); + + if ($converted && ($compare != $converted)) { + $value .= "\n" . $converted; + } + + return $value; + } + + /** + * This method collects and decodes proprietary encoding types + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromProprietaryEncodings($value) + { + //Xajax error reportings + $value = preg_replace('//im', '$1', $value); + + //strip false alert triggering apostrophes + $value = preg_replace('/(\w)\"(s)/m', '$1$2', $value); + + //strip quotes within typical search patterns + $value = preg_replace('/^"([^"=\\!><~]+)"$/', '$1', $value); + + //OpenID login tokens + $value = preg_replace('/{[\w-]{8,9}\}(?:\{[\w=]{8}\}){2}/', null, $value); + + //convert Content and \sdo\s to null + $value = preg_replace('/Content|\Wdo\s/', null, $value); + + //strip emoticons + $value = preg_replace( + '/(?:\s[:;]-[)\/PD]+)|(?:\s;[)PD]+)|(?:\s:[)PD]+)|-\.-|\^\^/m', + null, + $value + ); + + //normalize separation char repetion + $value = preg_replace('/([.+~=*_\-;])\1{2,}/m', '$1', $value); + + //normalize multiple single quotes + $value = preg_replace('/"{2,}/m', '"', $value); + + //normalize quoted numerical values and asterisks + $value = preg_replace('/"(\d+)"/m', '$1', $value); + + //normalize pipe separated request parameters + $value = preg_replace('/\|(\w+=\w+)/m', '&$1', $value); + + //normalize ampersand listings + $value = preg_replace('/(\w\s)&\s(\w)/', '$1$2', $value); + + //normalize escaped RegExp modifiers + $value = preg_replace('/\/\\\(\w)/', '/$1', $value); + + return $value; + } + + /** + * This method removes encoded sql # comments + * + * @param string $value the value to convert + * + * @static + * @return string + */ + public static function convertFromUrlencodeSqlComment($value) + { + if (preg_match_all('/(?:\%23.*?\%0a)/im',$value,$matches)){ + $converted = $value; + foreach($matches[0] as $match){ + $converted = str_replace($match,' ',$converted); + } + $value .= "\n" . $converted; + } + return $value; + } + + /** + * This method is the centrifuge prototype + * + * @param string $value the value to convert + * @param Monitor $monitor the monitor object + * + * @static + * @return string + */ + public static function runCentrifuge($value, Monitor $monitor = null) + { + $threshold = 3.49; + if (strlen($value) > 25) { + //strip padding + $tmp_value = preg_replace('/\s{4}|==$/m', null, $value); + $tmp_value = preg_replace( + '/\s{4}|[\p{L}\d\+\-=,.%()]{8,}/m', + 'aaa', + $tmp_value + ); + + // Check for the attack char ratio + $tmp_value = preg_replace('/([*.!?+-])\1{1,}/m', '$1', $tmp_value); + $tmp_value = preg_replace('/"[\p{L}\d\s]+"/m', null, $tmp_value); + + $stripped_length = strlen( + preg_replace( + '/[\d\s\p{L}\.:,%&\/><\-)!|]+/m', + null, + $tmp_value + ) + ); + $overall_length = strlen( + preg_replace( + '/([\d\s\p{L}:,\.]{3,})+/m', + 'aaa', + preg_replace('/\s{2,}/m', null, $tmp_value) + ) + ); + + if ($stripped_length != 0 && $overall_length/$stripped_length <= $threshold) { + $monitor->centrifuge['ratio'] = $overall_length / $stripped_length; + $monitor->centrifuge['threshold'] =$threshold; + + $value .= "\n$[!!!]"; + } + } + + if (strlen($value) > 40) { + // Replace all non-special chars + $converted = preg_replace('/[\w\s\p{L},.:!]/', null, $value); + + // Split string into an array, unify and sort + $array = str_split($converted); + $array = array_unique($array); + asort($array); + + // Normalize certain tokens + $schemes = array( + '~' => '+', + '^' => '+', + '|' => '+', + '*' => '+', + '%' => '+', + '&' => '+', + '/' => '+' + ); + + $converted = implode($array); + + $_keys = array_keys($schemes); + $_values = array_values($schemes); + + $converted = str_replace($_keys, $_values, $converted); + + $converted = preg_replace('/[+-]\s*\d+/', '+', $converted); + $converted = preg_replace('/[()[\]{}]/', '(', $converted); + $converted = preg_replace('/[!?:=]/', ':', $converted); + $converted = preg_replace('/[^:(+]/', null, stripslashes($converted)); + + // Sort again and implode + $array = str_split($converted); + asort($array); + $converted = implode($array); + + if (preg_match('/(?:\({2,}\+{2,}:{2,})|(?:\({2,}\+{2,}:+)|(?:\({3,}\++:{2,})/', $converted)) { + $monitor->centrifuge['converted'] = $converted; + + return $value . "\n" . $converted; + } + } + + return $value; + } +} diff --git a/interface/lib/classes/IDS/Event.php b/interface/lib/classes/IDS/Event.php new file mode 100644 index 000000000..ebda1715e --- /dev/null +++ b/interface/lib/classes/IDS/Event.php @@ -0,0 +1,221 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +namespace IDS; + +/** + * PHPIDS event object + * + * This class represents a certain event that occured while applying the filters + * to the supplied data. It aggregates a bunch of IDS_Filter implementations and + * is a assembled in IDS_Report. + * + * Note that this class implements both Countable and IteratorAggregate + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Group + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +class Event implements \Countable, \IteratorAggregate +{ + /** + * Event name + * + * @var string + */ + protected $name = null; + + /** + * Value of the event + * + * @var mixed + */ + protected $value = null; + + /** + * List of filter objects + * + * Filter objects in this array are those that matched the events value + * + * @var Filter[]|array + */ + protected $filters = array(); + + /** + * Calculated impact + * + * Total impact of the event + * + * @var integer + */ + protected $impact = 0; + + /** + * Affecte tags + * + * @var string[]|array + */ + protected $tags = array(); + + /** + * Constructor + * + * Fills event properties + * + * @param string $name the event name + * @param mixed $value the event value + * @param Filter[]|array $filters the corresponding filters + * + * @throws \InvalidArgumentException + * @return \IDS\Event + */ + public function __construct($name, $value, array $filters) + { + if (!is_scalar($name)) { + throw new \InvalidArgumentException( + 'Expected $name to be a scalar,' . gettype($name) . ' given' + ); + } + + if (!is_scalar($value)) { + throw new \InvalidArgumentException( + 'Expected $value to be a scalar,' . gettype($value) . ' given' + ); + } + + $this->name = $name; + $this->value = $value; + + foreach ($filters as $filter) { + if (!$filter instanceof Filter) { + throw new \InvalidArgumentException( + 'Filter must be derived from IDS_Filter' + ); + } + + $this->filters[] = $filter; + } + } + + /** + * Returns event name + * + * The name of the event usually is the key of the variable that was + * considered to be malicious + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns event value + * + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns calculated impact + * + * @return integer + */ + public function getImpact() + { + if (!$this->impact) { + $this->impact = 0; + foreach ($this->filters as $filter) { + $this->impact += $filter->getImpact(); + } + } + + return $this->impact; + } + + /** + * Returns affected tags + * + * @return string[]|array + */ + public function getTags() + { + foreach ($this->getFilters() as $filter) { + $this->tags = array_merge($this->tags, $filter->getTags()); + } + + return $this->tags = array_values(array_unique($this->tags)); + } + + /** + * Returns list of filter objects + * + * @return Filter[]|array + */ + public function getFilters() + { + return $this->filters; + } + + /** + * Returns number of filters + * + * To implement interface Countable this returns the number of filters + * appended. + * + * @return integer + */ + public function count() + { + return count($this->getFilters()); + } + + /** + * IteratorAggregate iterator getter + * + * Returns an iterator to iterate over the appended filters. + * + * @return \Iterator the filter collection + */ + public function getIterator() + { + return new \ArrayIterator($this->getFilters()); + } +} diff --git a/interface/lib/classes/IDS/Filter.php b/interface/lib/classes/IDS/Filter.php new file mode 100644 index 000000000..573988f39 --- /dev/null +++ b/interface/lib/classes/IDS/Filter.php @@ -0,0 +1,175 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +namespace IDS; + +/** + * PHPIDS Filter object + * + * Each object of this class serves as a container for a specific filter. The + * object provides methods to get information about this particular filter and + * also to match an arbitrary string against it. + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Group + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + * @since Version 0.4 + */ +class Filter +{ + /** + * Filter rule + * + * @var string + */ + protected $rule; + + /** + * List of tags of the filter + * + * @var string[]|array + */ + protected $tags = array(); + + /** + * Filter impact level + * + * @var integer + */ + protected $impact = 0; + + /** + * Filter description + * + * @var string + */ + protected $description = ''; + + /** + * Constructor + * + * @param integer $id filter id + * @param string $rule filter rule + * @param string $description filter description + * @param string[]|array $tags list of tags + * @param integer $impact filter impact level + * + * @return \IDS\Filter + */ + public function __construct($id, $rule, $description, array $tags, $impact) + { + $this->id = $id; + $this->rule = $rule; + $this->tags = $tags; + $this->impact = $impact; + $this->description = $description; + } + + /** + * Matches a string against current filter + * + * Matches given string against the filter rule the specific object of this + * class represents + * + * @param string $input the string input to match + * + * @throws \InvalidArgumentException if argument is no string + * @return boolean + */ + public function match($input) + { + if (!is_string($input)) { + throw new \InvalidArgumentException( + 'Invalid argument. Expected a string, received ' . gettype($input) + ); + } + + return (bool) preg_match('/' . $this->getRule() . '/ms', strtolower($input)); + } + + /** + * Returns filter description + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Return list of affected tags + * + * Each filter rule is concerned with a certain kind of attack vectors. + * This method returns those affected kinds. + * + * @return string[]|array + */ + public function getTags() + { + return $this->tags; + } + + /** + * Returns filter rule + * + * @return string + */ + public function getRule() + { + return $this->rule; + } + + /** + * Get filter impact level + * + * @return integer + */ + public function getImpact() + { + return $this->impact; + } + + /** + * Get filter ID + * + * @return integer + */ + public function getId() + { + return $this->id; + } +} diff --git a/interface/lib/classes/IDS/Filter/Storage.php b/interface/lib/classes/IDS/Filter/Storage.php new file mode 100644 index 000000000..f17549b27 --- /dev/null +++ b/interface/lib/classes/IDS/Filter/Storage.php @@ -0,0 +1,401 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +namespace IDS\Filter; + +use IDS\Init; +use IDS\Caching\CacheFactory; + +/** + * Filter Storage + * + * This class provides various default functions for gathering filter patterns + * to be used later on by the detection mechanism. You might extend this class + * to your requirements. + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Group + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +class Storage +{ + /** + * Filter source file + * + * @var string + */ + protected $source = null; + + /** + * Holds caching settings + * + * @var array + */ + protected $cacheSettings = null; + + /** + * Cache container + * + * @var object IDS_Caching wrapper + */ + protected $cache = null; + + /** + * Filter container + * + * @var array + */ + protected $filterSet = array(); + + /** + * Constructor + * + * Loads filters based on provided IDS_Init settings. + * + * @param object $init IDS_Init instance + * + * @throws \InvalidArgumentException if unsupported filter type is given + * @return void + */ + final public function __construct(Init $init) + { + if ($init->config) { + + $caching = isset($init->config['Caching']['caching']) ? $init->config['Caching']['caching'] : 'none'; + + $type = $init->config['General']['filter_type']; + $this->source = $init->getBasePath(). $init->config['General']['filter_path']; + + if ($caching && $caching !== 'none') { + $this->cacheSettings = $init->config['Caching']; + $this->cache = CacheFactory::factory($init, 'storage'); + } + + switch ($type) { + case 'xml': + return $this->getFilterFromXML(); + case 'json': + return $this->getFilterFromJson(); + default: + throw new \InvalidArgumentException('Unsupported filter type.'); + } + } + } + + /** + * Sets the filter array + * + * @param array $filterSet array containing multiple IDS_Filter instances + * + * @return object $this + */ + final public function setFilterSet($filterSet) + { + foreach ($filterSet as $filter) { + $this->addFilter($filter); + } + + return $this; + } + + /** + * Returns registered filters + * + * @return array + */ + final public function getFilterSet() + { + return $this->filterSet; + } + + /** + * Adds a filter + * + * @param object $filter IDS_Filter instance + * + * @return object $this + */ + final public function addFilter(\IDS\Filter $filter) + { + $this->filterSet[] = $filter; + + return $this; + } + + /** + * Checks if any filters are cached + * + * @return mixed $filters cached filters or false + */ + private function isCached() + { + $filters = false; + + if ($this->cacheSettings) { + if ($this->cache) { + $filters = $this->cache->getCache(); + } + } + + return $filters; + } + + /** + * Loads filters from XML using SimpleXML + * + * This function parses the provided source file and stores the result. + * If caching mode is enabled the result will be cached to increase + * the performance. + * + * @throws \InvalidArgumentException if source file doesn't exist + * @throws \RuntimeException if problems with fetching the XML data occur + * @return object $this + */ + public function getFilterFromXML() + { + if (extension_loaded('SimpleXML')) { + + /* + * See if filters are already available in the cache + */ + $filters = $this->isCached(); + + /* + * If they aren't, parse the source file + */ + if (!$filters) { + + if (!file_exists($this->source)) { + throw new \InvalidArgumentException( + sprintf('Invalid config: %s doesn\'t exist.', $this->source) + ); + } + + if (LIBXML_VERSION >= 20621) { + $filters = simplexml_load_file($this->source, null, LIBXML_COMPACT); + } else { + $filters = simplexml_load_file($this->source); + } + } + + /* + * In case we still don't have any filters loaded and exception + * will be thrown + */ + if (empty($filters)) { + throw new \RuntimeException( + 'XML data could not be loaded.' . + ' Make sure you specified the correct path.' + ); + } + + /* + * Now the storage will be filled with IDS_Filter objects + */ + $nocache = $filters instanceof \SimpleXMLElement; + + if ($nocache) + { + // build filters and cache them for re-use on next run + $data = array(); + $filters = $filters->filter; + + foreach ($filters as $filter) { + $id = (string) $filter->id; + $rule = (string) $filter->rule; + $impact = (string) $filter->impact; + $tags = array_values((array) $filter->tags); + $description = (string) $filter->description; + + $this->addFilter( + new \IDS\Filter( + $id, + $rule, + $description, + (array) $tags[0], + (int) $impact + ) + ); + + $data[] = array( + 'id' => $id, + 'rule' => $rule, + 'impact' => $impact, + 'tags' => $tags, + 'description' => $description + ); + } + + /* + * If caching is enabled, the fetched data will be cached + */ + if ($this->cacheSettings) { + $this->cache->setCache($data); + } + + } else { + + // build filters from cached content + $this->addFiltersFromArray($filters); + } + + return $this; + } + + throw new \RuntimeException('SimpleXML is not loaded.'); + } + + /** + * Loads filters from Json file using ext/Json + * + * This function parses the provided source file and stores the result. + * If caching mode is enabled the result will be cached to increase + * the performance. + * + * @throws \RuntimeException if problems with fetching the JSON data occur + * @return object $this + */ + public function getFilterFromJson() + { + + if (extension_loaded('Json')) { + + /* + * See if filters are already available in the cache + */ + $filters = $this->isCached(); + + /* + * If they aren't, parse the source file + */ + if (!$filters) { + if (!file_exists($this->source)) { + throw new \InvalidArgumentException( + sprintf('Invalid config: %s doesn\'t exist.', $this->source) + ); + } + $filters = json_decode(file_get_contents($this->source)); + } + + if (!$filters) { + throw new \RuntimeException('JSON data could not be loaded. Make sure you specified the correct path.'); + } + + /* + * Now the storage will be filled with IDS_Filter objects + */ + $nocache = !is_array($filters); + + if ($nocache) { + + // build filters and cache them for re-use on next run + $data = array(); + $filters = $filters->filters->filter; + + foreach ($filters as $filter) { + + $id = (string) $filter->id; + $rule = (string) $filter->rule; + $impact = (string) $filter->impact; + $tags = array_values((array) $filter->tags); + $description = (string) $filter->description; + + $this->addFilter( + new \IDS\Filter( + $id, + $rule, + $description, + (array) $tags[0], + (int) $impact + ) + ); + + $data[] = array( + 'id' => $id, + 'rule' => $rule, + 'impact' => $impact, + 'tags' => $tags, + 'description' => $description + ); + } + + /* + * If caching is enabled, the fetched data will be cached + */ + if ($this->cacheSettings) { + $this->cache->setCache($data); + } + + } else { + + // build filters from cached content + $this->addFiltersFromArray($filters); + } + + return $this; + } + + throw new \RuntimeException('json extension is not loaded.'); + } + + /** + * This functions adds an array of filters to the IDS_Storage object. + * Each entry within the array is expected to be an simple array containing all parts of the filter. + * + * @param array $filters + */ + private function addFiltersFromArray(array $filters) + { + foreach ($filters as $filter) { + + $id = $filter['id']; + $rule = $filter['rule']; + $impact = $filter['impact']; + $tags = $filter['tags']; + $description = $filter['description']; + + $this->addFilter( + new \IDS\Filter( + $id, + $rule, + $description, + (array) $tags[0], + (int) $impact + ) + ); + } + } +} diff --git a/interface/lib/classes/IDS/Init.php b/interface/lib/classes/IDS/Init.php new file mode 100644 index 000000000..1b6269adc --- /dev/null +++ b/interface/lib/classes/IDS/Init.php @@ -0,0 +1,174 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +namespace IDS; + +/** + * Framework initiation + * + * This class is used for the purpose to initiate the framework and inhabits + * functionality to parse the needed configuration file. + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Groupup + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + * @since Version 0.4 + */ +class Init +{ + /** + * Holds config settings + * + * @var array + */ + public $config = array(); + + /** + * Instance of this class depending on the supplied config file + * + * @var Init[]|array + * @static + */ + private static $instances = array(); + + /** + * Constructor + * + * Includes needed classes and parses the configuration file + * + * @param array $config + * + * @return \IDS\Init $this + */ + public function __construct(array $config = array()) + { + $this->config = $config; + } + + /** + * Returns an instance of this class. Also a PHP version check + * is being performed to avoid compatibility problems with PHP < 5.1.6 + * + * @param string|null $configPath the path to the config file + * + * @throws \InvalidArgumentException + * @return self + */ + public static function init($configPath = null) + { + if (!$configPath) { + return new self(); + } + if (!isset(self::$instances[$configPath])) { + if (!file_exists($configPath) || !is_readable($configPath)) { + throw new \InvalidArgumentException("Invalid config path '$configPath'"); + } + self::$instances[$configPath] = new static(parse_ini_file($configPath, true)); + } + + return self::$instances[$configPath]; + } + + /** + * This method checks if a base path is given and usage is set to true. + * If all that tests succeed the base path will be returned as a string - + * else null will be returned. + * + * @return string|null the base path or null + */ + public function getBasePath() + { + return (!empty($this->config['General']['base_path']) + && !empty($this->config['General']['use_base_path'])) + ? $this->config['General']['base_path'] : null; + } + + /** + * Merges new settings into the exsiting ones or overwrites them + * + * @param array $config the config array + * @param boolean $overwrite config overwrite flag + * + * @return void + */ + public function setConfig(array $config, $overwrite = false) + { + if ($overwrite) { + $this->config = $this->mergeConfig($this->config, $config); + } else { + $this->config = $this->mergeConfig($config, $this->config); + } + } + + /** + * Merge config hashes recursivly + * + * The algorithm merges configuration arrays recursively. If an element is + * an array in both, the values will be appended. If it is a scalar in both, + * the value will be replaced. + * + * @param array $current The legacy hash + * @param array $successor The hash which values count more when in doubt + * @return array Merged hash + */ + protected function mergeConfig($current, $successor) + { + if (is_array($current) and is_array($successor)) { + foreach ($successor as $key => $value) { + if (isset($current[$key]) + and is_array($value) + and is_array($current[$key])) { + + $current[$key] = $this->mergeConfig($current[$key], $value); + } else { + $current[$key] = $successor[$key]; + } + } + } + + return $current; + } + + /** + * Returns the config array + * + * @return array the config array + */ + public function getConfig() + { + return $this->config; + } +} diff --git a/interface/lib/classes/IDS/Monitor.php b/interface/lib/classes/IDS/Monitor.php new file mode 100644 index 000000000..f93e748e4 --- /dev/null +++ b/interface/lib/classes/IDS/Monitor.php @@ -0,0 +1,566 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +namespace IDS; + +use IDS\Filter\Storage; + +/** + * Monitoring engine + * + * This class represents the core of the frameworks attack detection mechanism + * and provides functions to scan incoming data for malicious appearing script + * fragments. + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Group + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +class Monitor +{ + /** + * Tags to define what to search for + * + * Accepted values are xss, csrf, sqli, dt, id, lfi, rfe, spam, dos + * + * @var array + */ + private $tags = null; + + /** + * Container for filter rules + * + * Holds an instance of Storage + * + * @var Storage + */ + private $storage = null; + + /** + * Scan keys switch + * + * Enabling this property will cause the monitor to scan both the key and + * the value of variables + * + * @var boolean + */ + public $scanKeys = false; + + /** + * Exception container + * + * Using this array it is possible to define variables that must not be + * scanned. Per default, utmz google analytics parameters are permitted. + * + * @var array + */ + private $exceptions = array(); + + /** + * Html container + * + * Using this array it is possible to define variables that legally + * contain html and have to be prepared before hitting the rules to + * avoid too many false alerts + * + * @var array + */ + private $html = array(); + + /** + * JSON container + * + * Using this array it is possible to define variables that contain + * JSON data - and should be treated as such + * + * @var array + */ + private $json = array(); + + /** + * Holds HTMLPurifier object + * + * @var \HTMLPurifier + */ + private $htmlPurifier = null; + + /** + * HTMLPurifier cache directory + * + * @var string + */ + private $HTMLPurifierCache = ''; + + /** + * This property holds the tmp JSON string from the _jsonDecodeValues() callback + * + * @var string + */ + private $tmpJsonString = ''; + + /** + * Constructor + * + * @throws \InvalidArgumentException When PHP version is less than what the library supports + * @throws \Exception + * @param Init $init instance of IDS_Init + * @param array|null $tags list of tags to which filters should be applied + * @return Monitor + */ + public function __construct(Init $init, array $tags = null) + { + $this->storage = new Storage($init); + $this->tags = $tags; + $this->scanKeys = $init->config['General']['scan_keys']; + $this->exceptions = isset($init->config['General']['exceptions']) ? $init->config['General']['exceptions'] : array(); + $this->html = isset($init->config['General']['html']) ? $init->config['General']['html'] : array(); + $this->json = isset($init->config['General']['json']) ? $init->config['General']['json'] : array(); + + if (isset($init->config['General']['HTML_Purifier_Cache'])) { + $this->HTMLPurifierCache = $init->getBasePath() . $init->config['General']['HTML_Purifier_Cache']; + } + + $tmpPath = $init->getBasePath() . $init->config['General']['tmp_path']; + + if (!is_writeable($tmpPath)) { + throw new \InvalidArgumentException("Please make sure the folder '$tmpPath' is writable"); + } + } + + /** + * Starts the scan mechanism + * + * @param array $request + * @return Report + */ + public function run(array $request) + { + $report = new Report; + foreach ($request as $key => $value) { + $report = $this->iterate($key, $value, $report); + } + return $report; + } + + /** + * Iterates through given data and delegates it to IDS_Monitor::_detect() in + * order to check for malicious appearing fragments + * + * @param string $key the former array key + * @param array|string $value the former array value + * @param Report $report + * @return Report + */ + private function iterate($key, $value, Report $report) + { + if (is_array($value)) { + foreach ($value as $subKey => $subValue) { + $this->iterate("$key.$subKey", $subValue, $report); + } + } elseif (is_string($value)) { + if ($filter = $this->detect($key, $value)) { + $report->addEvent(new Event($key, $value, $filter)); + } + } + return $report; + } + + /** + * Checks whether given value matches any of the supplied filter patterns + * + * @param mixed $key the key of the value to scan + * @param mixed $value the value to scan + * + * @return Filter[] array of filter(s) that matched the value + */ + private function detect($key, $value) + { + // define the pre-filter + $preFilter = '([^\w\s/@!?\.]+|(?:\./)|(?:@@\w+)|(?:\+ADw)|(?:union\s+select))i'; + + // to increase performance, only start detection if value isn't alphanumeric + if ((!$this->scanKeys || !$key || !preg_match($preFilter, $key)) && (!$value || !preg_match($preFilter, $value))) { + return array(); + } + + // check if this field is part of the exceptions + foreach ($this->exceptions as $exception) { + $matches = array(); + if (($exception === $key) || preg_match('((/.*/[^eE]*)$)', $exception, $matches) && isset($matches[1]) && preg_match($matches[1], $key)) { + return array(); + } + } + + // check for magic quotes and remove them if necessary + if (function_exists('get_magic_quotes_gpc') && !get_magic_quotes_gpc()) { + $value = preg_replace('(\\\(["\'/]))im', '$1', $value); + } + + // if html monitoring is enabled for this field - then do it! + if (is_array($this->html) && in_array($key, $this->html, true)) { + list($key, $value) = $this->purifyValues($key, $value); + } + + // check if json monitoring is enabled for this field + if (is_array($this->json) && in_array($key, $this->json, true)) { + list($key, $value) = $this->jsonDecodeValues($key, $value); + } + + // use the converter + $value = Converter::runAll($value); + $value = Converter::runCentrifuge($value, $this); + + // scan keys if activated via config + $key = $this->scanKeys ? Converter::runAll($key) : $key; + $key = $this->scanKeys ? Converter::runCentrifuge($key, $this) : $key; + + $filterSet = $this->storage->getFilterSet(); + + if ($tags = $this->tags) { + $filterSet = array_filter( + $filterSet, + function (Filter $filter) use ($tags) { + return (bool) array_intersect($tags, $filter->getTags()); + } + ); + } + + $scanKeys = $this->scanKeys; + $filterSet = array_filter( + $filterSet, + function (Filter $filter) use ($key, $value, $scanKeys) { + return $filter->match($value) || $scanKeys && $filter->match($key); + } + ); + + return $filterSet; + } + + + /** + * Purifies given key and value variables using HTMLPurifier + * + * This function is needed whenever there is variables for which HTML + * might be allowed like e.g. WYSIWYG post bodies. It will detect malicious + * code fragments and leaves harmless parts untouched. + * + * @param mixed $key + * @param mixed $value + * @since 0.5 + * @throws \Exception + * + * @return array tuple [key,value] + */ + private function purifyValues($key, $value) + { + /* + * Perform a pre-check if string is valid for purification + */ + if ($this->purifierPreCheck($key, $value)) { + if (!is_writeable($this->HTMLPurifierCache)) { + throw new \Exception($this->HTMLPurifierCache . ' must be writeable'); + } + + /** @var $config \HTMLPurifier_Config */ + $config = \HTMLPurifier_Config::createDefault(); + $config->set('Attr.EnableID', true); + $config->set('Cache.SerializerPath', $this->HTMLPurifierCache); + $config->set('Output.Newline', "\n"); + $this->htmlPurifier = new \HTMLPurifier($config); + + $value = preg_replace('([\x0b-\x0c])', ' ', $value); + $key = preg_replace('([\x0b-\x0c])', ' ', $key); + + $purifiedValue = $this->htmlPurifier->purify($value); + $purifiedKey = $this->htmlPurifier->purify($key); + + $plainValue = strip_tags($value); + $plainKey = strip_tags($key); + + $value = $value != $purifiedValue || $plainValue ? $this->diff($value, $purifiedValue, $plainValue) : null; + $key = $key != $purifiedKey ? $this->diff($key, $purifiedKey, $plainKey) : null; + } + return array($key, $value); + } + + /** + * This method makes sure no dangerous markup can be smuggled in + * attributes when HTML mode is switched on. + * + * If the precheck considers the string too dangerous for + * purification false is being returned. + * + * @param string $key + * @param string $value + * @since 0.6 + * + * @return boolean + */ + private function purifierPreCheck($key = '', $value = '') + { + /* + * Remove control chars before pre-check + */ + $tmpValue = preg_replace('/\p{C}/', null, $value); + $tmpKey = preg_replace('/\p{C}/', null, $key); + + $preCheck = '/<(script|iframe|applet|object)\W/i'; + return !(preg_match($preCheck, $tmpKey) || preg_match($preCheck, $tmpValue)); + } + + /** + * This method calculates the difference between the original + * and the purified markup strings. + * + * @param string $original the original markup + * @param string $purified the purified markup + * @param string $plain the string without html + * @since 0.5 + * + * @return string the difference between the strings + */ + private function diff($original, $purified, $plain) + { + /* + * deal with over-sensitive alt-attribute addition of the purifier + * and other common html formatting problems + */ + $purified = preg_replace('/\s+alt="[^"]*"/m', null, $purified); + $purified = preg_replace('/=?\s*"\s*"/m', null, $purified); + $original = preg_replace('/\s+alt="[^"]*"/m', null, $original); + $original = preg_replace('/=?\s*"\s*"/m', null, $original); + $original = preg_replace('/style\s*=\s*([^"])/m', 'style = "$1', $original); + + # deal with oversensitive CSS normalization + $original = preg_replace('/(?:([\w\-]+:)+\s*([^;]+;\s*))/m', '$1$2', $original); + + # strip whitespace between tags + $original = trim(preg_replace('/>\s*<', $original)); + $purified = trim(preg_replace('/>\s*<', $purified)); + + $original = preg_replace('/(=\s*(["\'`])[^>"\'`]*>[^>"\'`]*["\'`])/m', 'alt$1', $original); + + // no purified html is left + if (!$purified) { + return $original; + } + + // calculate the diff length + $length = mb_strlen($original) - mb_strlen($purified); + + /* + * Calculate the difference between the original html input + * and the purified string. + */ + $array1 = preg_split('/(?\s*<', $diff); + + // correct over-sensitively stripped bad html elements + $diff = preg_replace('/[^<](iframe|script|embed|object|applet|base|img|style)/m', '<$1', $diff ); + + return mb_strlen($diff) >= 4 ? $diff . $plain : null; + } + + /** + * This method prepares incoming JSON data for the PHPIDS detection + * process. It utilizes _jsonConcatContents() as callback and returns a + * string version of the JSON data structures. + * + * @param string $key + * @param string $value + * @since 0.5.3 + * + * @return array tuple [key,value] + */ + private function jsonDecodeValues($key, $value) + { + $decodedKey = json_decode($key); + $decodedValue = json_decode($value); + + if ($decodedValue && is_array($decodedValue) || is_object($decodedValue)) { + array_walk_recursive($decodedValue, array($this, 'jsonConcatContents')); + $value = $this->tmpJsonString; + } else { + $this->tmpJsonString .= " " . $decodedValue . "\n"; + } + + if ($decodedKey && is_array($decodedKey) || is_object($decodedKey)) { + array_walk_recursive($decodedKey, array($this, 'jsonConcatContents')); + $key = $this->tmpJsonString; + } else { + $this->tmpJsonString .= " " . $decodedKey . "\n"; + } + + return array($key, $value); + } + + /** + * This is the callback used in _jsonDecodeValues(). The method + * concatenates key and value and stores them in $this->tmpJsonString. + * + * @param mixed $key + * @param mixed $value + * @since 0.5.3 + * + * @return void + */ + private function jsonConcatContents($key, $value) + { + if (is_string($key) && is_string($value)) { + $this->tmpJsonString .= $key . " " . $value . "\n"; + } else { + $this->jsonDecodeValues(json_encode($key), json_encode($value)); + } + } + + /** + * Sets exception array + * + * @param string[]|string $exceptions the thrown exceptions + * + * @return void + */ + public function setExceptions($exceptions) + { + $this->exceptions = (array) $exceptions; + } + + /** + * Returns exception array + * + * @return array + */ + public function getExceptions() + { + return $this->exceptions; + } + + /** + * Sets html array + * + * @param string[]|string $html the fields containing html + * @since 0.5 + * + * @return void + */ + public function setHtml($html) + { + $this->html = (array) $html; + } + + /** + * Adds a value to the html array + * + * @since 0.5 + * + * @param mixed $value + * @return void + */ + public function addHtml($value) + { + $this->html[] = $value; + } + + /** + * Returns html array + * + * @since 0.5 + * + * @return array the fields that contain allowed html + */ + public function getHtml() + { + return $this->html; + } + + /** + * Sets json array + * + * @param string[]|string $json the fields containing json + * @since 0.5.3 + * + * @return void + */ + public function setJson($json) + { + $this->json = (array) $json; + } + + /** + * Adds a value to the json array + * + * @param string $value the value containing JSON data + * @since 0.5.3 + * + * @return void + */ + public function addJson($value) + { + $this->json[] = $value; + } + + /** + * Returns json array + * + * @since 0.5.3 + * + * @return array the fields that contain json + */ + public function getJson() + { + return $this->json; + } + + /** + * Returns storage container + * + * @return array + */ + public function getStorage() + { + return $this->storage; + } +} diff --git a/interface/lib/classes/IDS/Report.php b/interface/lib/classes/IDS/Report.php new file mode 100644 index 000000000..daebfc030 --- /dev/null +++ b/interface/lib/classes/IDS/Report.php @@ -0,0 +1,339 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +namespace IDS; + +/** + * PHPIDS report object + * + * The report objects collects a number of events and thereby presents the + * detected results. It provides a convenient API to work with the results. + * + * Note that this class implements Countable, IteratorAggregate and + * a __toString() method + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Group + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +class Report implements \Countable, \IteratorAggregate +{ + /** + * Event container + * + * @var Event[]|array + */ + protected $events = array(); + + /** + * List of affected tags + * + * This list of tags is collected from the collected event objects on + * demand when IDS_Report->getTags() is called + * + * @var string[]|array + */ + protected $tags = array(); + + /** + * Impact level + * + * The impact level is calculated on demand by adding the results of the + * event objects on IDS\Report->getImpact() + * + * @var integer + */ + protected $impact = 0; + + /** + * Centrifuge data + * + * This variable - initiated as an empty array - carries all information + * about the centrifuge data if available + * + * @var array + */ + protected $centrifuge = array(); + + /** + * Constructor + * + * @param array $events the events the report should include + * + * @return Report + */ + public function __construct(array $events = null) + { + foreach ((array) $events as $event) { + $this->addEvent($event); + } + } + + /** + * Adds an IDS_Event object to the report + * + * @param Event $event IDS_Event + * + * @return self $this + */ + public function addEvent(Event $event) + { + $this->clear(); + $this->events[$event->getName()] = $event; + + return $this; + } + + /** + * Get event by name + * + * In most cases an event is identified by the key of the variable that + * contained maliciously appearing content + * + * @param string|integer $name the event name + * + * @throws \InvalidArgumentException if argument is invalid + * @return Event|null IDS_Event object or false if the event does not exist + */ + public function getEvent($name) + { + if (!is_scalar($name)) { + throw new \InvalidArgumentException('Invalid argument type given'); + } + + return $this->hasEvent($name) ? $this->events[$name] : null; + } + + /** + * Returns list of events + * + * @return string[]|array + */ + + public function getEvents() + { + return $this->events; + } + + /** + * Returns list of affected tags + * + * @return string[]|array + */ + public function getTags() + { + if (!$this->tags) { + $this->tags = array(); + + foreach ($this->events as $event) { + $this->tags = array_merge($this->tags, $event->getTags()); + } + + $this->tags = array_values(array_unique($this->tags)); + } + + return $this->tags; + } + + /** + * Returns total impact + * + * Each stored IDS_Event object and its IDS_Filter sub-object are called + * to calculate the overall impact level of this request + * + * @return integer + */ + public function getImpact() + { + if (!$this->impact) { + $this->impact = 0; + foreach ($this->events as $event) { + $this->impact += $event->getImpact(); + } + } + + return $this->impact; + } + + /** + * Checks if a specific event with given name exists + * + * @param string|integer $name the event name + * + * @throws \InvalidArgumentException if argument is illegal + * @return boolean + */ + public function hasEvent($name) + { + if (!is_scalar($name)) { + throw new \InvalidArgumentException('Invalid argument given'); + } + + return isset($this->events[$name]); + } + + /** + * Returns total amount of events + * + * @return integer + */ + public function count() + { + return count($this->events); + } + + /** + * Return iterator object + * + * In order to provide the possibility to directly iterate over the + * IDS_Event object the IteratorAggregate is implemented. One can easily + * use foreach() to iterate through all stored IDS_Event objects. + * + * @return \Iterator the event collection + */ + public function getIterator() + { + return new \ArrayIterator($this->events); + } + + /** + * Checks if any events are registered + * + * @return boolean + */ + public function isEmpty() + { + return empty($this->events); + } + + /** + * Clears calculated/collected values + * + * @return void + */ + protected function clear() + { + $this->impact = 0; + $this->tags = array(); + } + + /** + * This method returns the centrifuge property or null if not + * filled with data + * + * @return array + */ + public function getCentrifuge() + { + return $this->centrifuge; + } + + /** + * This method sets the centrifuge property + * + * @param array $centrifuge the centrifuge data + * + * @throws \InvalidArgumentException if argument is illegal + * @return void + */ + public function setCentrifuge(array $centrifuge = array()) + { + if (!$centrifuge) { + throw new \InvalidArgumentException('Empty centrifuge given'); + } + $this->centrifuge = $centrifuge; + } + + /** + * Directly outputs all available information + * + * @return string + */ + public function __toString() + { + $output = ''; + if (!$this->isEmpty()) { + $output .= vsprintf( + "Total impact: %d
\nAffected tags: %s
\n", + array( + $this->getImpact(), + implode(', ', $this->getTags()) + ) + ); + + foreach ($this->events as $event) { + $output .= vsprintf( + "
\nVariable: %s | Value: %s
\nImpact: %d | Tags: %s
\n", + array( + htmlspecialchars($event->getName()), + htmlspecialchars($event->getValue()), + $event->getImpact(), + implode(', ', $event->getTags()) + ) + ); + + foreach ($event as $filter) { + $output .= vsprintf( + "Description: %s | Tags: %s | ID %s
\n", + array( + $filter->getDescription(), + implode(', ', $filter->getTags()), + $filter->getId() + ) + ); + } + } + + $output .= '
'; + + if ($centrifuge = $this->getCentrifuge()) { + $output .= vsprintf( + "Centrifuge detection data
Threshold: %s
Ratio: %s", + array( + isset($centrifuge['threshold']) && $centrifuge['threshold'] ? $centrifuge['threshold'] : '---', + isset($centrifuge['ratio']) && $centrifuge['ratio'] ? $centrifuge['ratio'] : '---' + ) + ); + if (isset($centrifuge['converted'])) { + $output .= '
Converted: ' . $centrifuge['converted']; + } + $output .= "

\n"; + } + } + + return $output; + } +} diff --git a/interface/lib/classes/IDS/Version.php b/interface/lib/classes/IDS/Version.php new file mode 100644 index 000000000..b32438ffa --- /dev/null +++ b/interface/lib/classes/IDS/Version.php @@ -0,0 +1,49 @@ +. + * + * PHP version 5.1.6+ + * + * @category Security + * @package PHPIDS + * @author Mario Heiderich + * @author Christian Matthies + * @author Lars Strojny + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +namespace IDS; + +/** + * PHPIDS version class + * + * @category Security + * @package PHPIDS + * @author Christian Matthies + * @author Mario Heiderich + * @author Lars Strojny + * @copyright 2007-2009 The PHPIDS Group + * @license http://www.gnu.org/licenses/lgpl.html LGPL + * @link http://php-ids.org/ + */ +abstract class Version +{ + const VERSION = 'master'; +} diff --git a/interface/lib/classes/IDS/default_filter.json b/interface/lib/classes/IDS/default_filter.json new file mode 100644 index 000000000..c158ded57 --- /dev/null +++ b/interface/lib/classes/IDS/default_filter.json @@ -0,0 +1,933 @@ +{ + "filters":{ + "filter":[ + { + "id":"1", + "rule":"(?:\"[^\"]*[^-]?>)|(?:[^\\w\\s]\\s*\\\/>)|(?:>\")", + "description":"Finds html breaking injections including whitespace attacks", + "tags":{ + "tag":[ + "xss", + "csrf" + ] + }, + "impact":"4" + }, + { + "id":"2", + "rule":"(?:\"+.*[<=]\\s*\"[^\"]+\")|(?:\"\\s*\\w+\\s*=)|(?:>\\w=\\\/)|(?:#.+\\)[\"\\s]*>)|(?:\"\\s*(?:src|style|on\\w+)\\s*=\\s*\")|(?:[^\"]?\"[,;\\s]+\\w*[\\[\\(])", + "description":"Finds attribute breaking injections including whitespace attacks", + "tags":{ + "tag":[ + "xss", + "csrf" + ] + }, + "impact":"4" + }, + { + "id":"3", + "rule":"(?:^>[\\w\\s]*<\\\/?\\w{2,}>)", + "description":"Finds unquoted attribute breaking injections", + "tags":{ + "tag":[ + "xss", + "csrf" + ] + }, + "impact":"2" + }, + { + "id":"4", + "rule":"(?:[+\\\/]\\s*name[\\W\\d]*[)+])|(?:;\\W*url\\s*=)|(?:[^\\w\\s\\\/?:>]\\s*(?:location|referrer|name)\\s*[^\\\/\\w\\s-])", + "description":"Detects url-, name-, JSON, and referrer-contained payload attacks", + "tags":{ + "tag":[ + "xss", + "csrf" + ] + }, + "impact":"5" + }, + { + "id":"5", + "rule":"(?:\\W\\s*hash\\s*[^\\w\\s-])|(?:\\w+=\\W*[^,]*,[^\\s(]\\s*\\()|(?:\\?\"[^\\s\"]\":)|(?:(?]*)t(?!rong))|(?:\\)|(?:[^*]\\\/\\*|\\*\\\/[^*])|(?:(?:[\\W\\d]#|--|{)$)|(?:\\\/{3,}.*$)|(?:)", + "description":"Detects common comment types", + "tags":{ + "tag":[ + "xss", + "csrf", + "id" + ] + }, + "impact":"3" + }, + { + "id":"37", + "rule":"(?:\\~])", + "description":"Detects conditional SQL injection attempts", + "tags":{ + "tag":[ + "sqli", + "id", + "lfi" + ] + }, + "impact":"6" + }, + { + "id":"42", + "rule":"(?:\"\\s*or\\s*\"?\\d)|(?:\\\\x(?:23|27|3d))|(?:^.?\"$)|(?:(?:^[\"\\\\]*(?:[\\d\"]+|[^\"]+\"))+\\s*(?:n?and|x?or|not|\\|\\||\\&\\&)\\s*[\\w\"[+&!@(),.-])|(?:[^\\w\\s]\\w+\\s*[|-]\\s*\"\\s*\\w)|(?:@\\w+\\s+(and|or)\\s*[\"\\d]+)|(?:@[\\w-]+\\s(and|or)\\s*[^\\w\\s])|(?:[^\\w\\s:]\\s*\\d\\W+[^\\w\\s]\\s*\".)|(?:\\Winformation_schema|table_name\\W)", + "description":"Detects classic SQL injection probings 1\/2", + "tags":{ + "tag":[ + "sqli", + "id", + "lfi" + ] + }, + "impact":"6" + }, + { + "id":"43", + "rule":"(?:\"\\s*\\*.+(?:or|id)\\W*\"\\d)|(?:\\^\")|(?:^[\\w\\s\"-]+(?<=and\\s)(?<=or\\s)(?<=xor\\s)(?<=nand\\s)(?<=not\\s)(?<=\\|\\|)(?<=\\&\\&)\\w+\\()|(?:\"[\\s\\d]*[^\\w\\s]+\\W*\\d\\W*.*[\"\\d])|(?:\"\\s*[^\\w\\s?]+\\s*[^\\w\\s]+\\s*\")|(?:\"\\s*[^\\w\\s]+\\s*[\\W\\d].*(?:#|--))|(?:\".*\\*\\s*\\d)|(?:\"\\s*or\\s[^\\d]+[\\w-]+.*\\d)|(?:[()*<>%+-][\\w-]+[^\\w\\s]+\"[^,])", + "description":"Detects classic SQL injection probings 2\/2", + "tags":{ + "tag":[ + "sqli", + "id", + "lfi" + ] + }, + "impact":"6" + }, + { + "id":"44", + "rule":"(?:\\d\"\\s+\"\\s+\\d)|(?:^admin\\s*\"|(\\\/\\*)+\"+\\s?(?:--|#|\\\/\\*|{)?)|(?:\"\\s*or[\\w\\s-]+\\s*[+<>=(),-]\\s*[\\d\"])|(?:\"\\s*[^\\w\\s]?=\\s*\")|(?:\"\\W*[+=]+\\W*\")|(?:\"\\s*[!=|][\\d\\s!=+-]+.*[\"(].*$)|(?:\"\\s*[!=|][\\d\\s!=]+.*\\d+$)|(?:\"\\s*like\\W+[\\w\"(])|(?:\\sis\\s*0\\W)|(?:where\\s[\\s\\w\\.,-]+\\s=)|(?:\"[<>~]+\")", + "description":"Detects basic SQL authentication bypass attempts 1\/3", + "tags":{ + "tag":[ + "sqli", + "id", + "lfi" + ] + }, + "impact":"7" + }, + { + "id":"45", + "rule":"(?:union\\s*(?:all|distinct|[(!@]*)\\s*[([]*\\s*select)|(?:\\w+\\s+like\\s+\\\")|(?:like\\s*\"\\%)|(?:\"\\s*like\\W*[\"\\d])|(?:\"\\s*(?:n?and|x?or|not |\\|\\||\\&\\&)\\s+[\\s\\w]+=\\s*\\w+\\s*having)|(?:\"\\s*\\*\\s*\\w+\\W+\")|(?:\"\\s*[^?\\w\\s=.,;)(]+\\s*[(@\"]*\\s*\\w+\\W+\\w)|(?:select\\s*[\\[\\]()\\s\\w\\.,\"-]+from)|(?:find_in_set\\s*\\()", + "description":"Detects basic SQL authentication bypass attempts 2\/3", + "tags":{ + "tag":[ + "sqli", + "id", + "lfi" + ] + }, + "impact":"7" + }, + { + "id":"46", + "rule":"(?:in\\s*\\(+\\s*select)|(?:(?:n?and|x?or|not |\\|\\||\\&\\&)\\s+[\\s\\w+]+(?:regexp\\s*\\(|sounds\\s+like\\s*\"|[=\\d]+x))|(\"\\s*\\d\\s*(?:--|#))|(?:\"[%&<>^=]+\\d\\s*(=|or))|(?:\"\\W+[\\w+-]+\\s*=\\s*\\d\\W+\")|(?:\"\\s*is\\s*\\d.+\"?\\w)|(?:\"\\|?[\\w-]{3,}[^\\w\\s.,]+\")|(?:\"\\s*is\\s*[\\d.]+\\s*\\W.*\")", + "description":"Detects basic SQL authentication bypass attempts 3\/3", + "tags":{ + "tag":[ + "sqli", + "id", + "lfi" + ] + }, + "impact":"7" + }, + { + "id":"47", + "rule":"(?:[\\d\\W]\\s+as\\s*[\"\\w]+\\s*from)|(?:^[\\W\\d]+\\s*(?:union|select|create|rename|truncate|load|alter|delete|update|insert|desc))|(?:(?:select|create|rename|truncate|load|alter|delete|update|insert|desc)\\s+(?:(?:group_)concat|char|load_file)\\s?\\(?)|(?:end\\s*\\);)|(\"\\s+regexp\\W)|(?:[\\s(]load_file\\s*\\()", + "description":"Detects concatenated basic SQL injection and SQLLFI attempts", + "tags":{ + "tag":[ + "sqli", + "id", + "lfi" + ] + }, + "impact":"5" + }, + { + "id":"48", + "rule":"(?:@.+=\\s*\\(\\s*select)|(?:\\d+\\s*or\\s*\\d+\\s*[\\-+])|(?:\\\/\\w+;?\\s+(?:having|and|or|select)\\W)|(?:\\d\\s+group\\s+by.+\\()|(?:(?:;|#|--)\\s*(?:drop|alter))|(?:(?:;|#|--)\\s*(?:update|insert)\\s*\\w{2,})|(?:[^\\w]SET\\s*@\\w+)|(?:(?:n?and|x?or|not |\\|\\||\\&\\&)[\\s(]+\\w+[\\s)]*[!=+]+[\\s\\d]*[\"=()])", + "description":"Detects chained SQL injection attempts 1\/2", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"6" + }, + { + "id":"49", + "rule":"(?:\"\\s+and\\s*=\\W)|(?:\\(\\s*select\\s*\\w+\\s*\\()|(?:\\*\\\/from)|(?:\\+\\s*\\d+\\s*\\+\\s*@)|(?:\\w\"\\s*(?:[-+=|@]+\\s*)+[\\d(])|(?:coalesce\\s*\\(|@@\\w+\\s*[^\\w\\s])|(?:\\W!+\"\\w)|(?:\";\\s*(?:if|while|begin))|(?:\"[\\s\\d]+=\\s*\\d)|(?:order\\s+by\\s+if\\w*\\s*\\()|(?:[\\s(]+case\\d*\\W.+[tw]hen[\\s(])", + "description":"Detects chained SQL injection attempts 2\/2", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"6" + }, + { + "id":"50", + "rule":"(?:(select|;)\\s+(?:benchmark|if|sleep)\\s*?\\(\\s*\\(?\\s*\\w+)", + "description":"Detects SQL benchmark and sleep injection attempts including conditional queries", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"4" + }, + { + "id":"51", + "rule":"(?:create\\s+function\\s+\\w+\\s+returns)|(?:;\\s*(?:select|create|rename|truncate|load|alter|delete|update|insert|desc)\\s*[\\[(]?\\w{2,})", + "description":"Detects MySQL UDF injection and other data\/structure manipulation attempts", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"6" + }, + { + "id":"52", + "rule":"(?:alter\\s*\\w+.*character\\s+set\\s+\\w+)|(\";\\s*waitfor\\s+time\\s+\")|(?:\";.*:\\s*goto)", + "description":"Detects MySQL charset switch and MSSQL DoS attempts", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"6" + }, + { + "id":"53", + "rule":"(?:procedure\\s+analyse\\s*\\()|(?:;\\s*(declare|open)\\s+[\\w-]+)|(?:create\\s+(procedure|function)\\s*\\w+\\s*\\(\\s*\\)\\s*-)|(?:declare[^\\w]+[@#]\\s*\\w+)|(exec\\s*\\(\\s*@)", + "description":"Detects MySQL and PostgreSQL stored procedure\/function injections", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"7" + }, + { + "id":"54", + "rule":"(?:select\\s*pg_sleep)|(?:waitfor\\s*delay\\s?\"+\\s?\\d)|(?:;\\s*shutdown\\s*(?:;|--|#|\\\/\\*|{))", + "description":"Detects Postgres pg_sleep injection, waitfor delay attacks and database shutdown attempts", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"5" + }, + { + "id":"55", + "rule":"(?:\\sexec\\s+xp_cmdshell)|(?:\"\\s*!\\s*[\"\\w])|(?:from\\W+information_schema\\W)|(?:(?:(?:current_)?user|database|schema|connection_id)\\s*\\([^\\)]*)|(?:\";?\\s*(?:select|union|having)\\s*[^\\s])|(?:\\wiif\\s*\\()|(?:exec\\s+master\\.)|(?:union select @)|(?:union[\\w(\\s]*select)|(?:select.*\\w?user\\()|(?:into[\\s+]+(?:dump|out)file\\s*\")", + "description":"Detects MSSQL code execution and information gathering attempts", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"5" + }, + { + "id":"56", + "rule":"(?:merge.*using\\s*\\()|(execute\\s*immediate\\s*\")|(?:\\W+\\d*\\s*having\\s*[^\\s\\-])|(?:match\\s*[\\w(),+-]+\\s*against\\s*\\()", + "description":"Detects MATCH AGAINST, MERGE, EXECUTE IMMEDIATE and HAVING injections", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"5" + }, + { + "id":"57", + "rule":"(?:,.*[)\\da-f\"]\"(?:\".*\"|\\Z|[^\"]+))|(?:\\Wselect.+\\W*from)|((?:select|create|rename|truncate|load|alter|delete|update|insert|desc)\\s*\\(\\s*space\\s*\\()", + "description":"Detects MySQL comment-\/space-obfuscated injections and backtick termination", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"5" + }, + { + "id":"58", + "rule":"(?:@[\\w-]+\\s*\\()|(?:]\\s*\\(\\s*[\"!]\\s*\\w)|(?:<[?%](?:php)?.*(?:[?%]>)?)|(?:;[\\s\\w|]*\\$\\w+\\s*=)|(?:\\$\\w+\\s*=(?:(?:\\s*\\$?\\w+\\s*[(;])|\\s*\".*\"))|(?:;\\s*\\{\\W*\\w+\\s*\\()", + "description":"Detects code injection attempts 1\/3", + "tags":{ + "tag":[ + "id", + "rfe", + "lfi" + ] + }, + "impact":"7" + }, + { + "id":"59", + "rule":"(?:(?:[;]+|(<[?%](?:php)?)).*(?:define|eval|file_get_contents|include|require|require_once|set|shell_exec|phpinfo|system|passthru|preg_\\w+|execute)\\s*[\"(@])", + "description":"Detects code injection attempts 2\/3", + "tags":{ + "tag":[ + "id", + "rfe", + "lfi" + ] + }, + "impact":"7" + }, + { + "id":"60", + "rule":"(?:(?:[;]+|(<[?%](?:php)?)).*[^\\w](?:echo|print|print_r|var_dump|[fp]open))|(?:;\\s*rm\\s+-\\w+\\s+)|(?:;.*{.*\\$\\w+\\s*=)|(?:\\$\\w+\\s*\\[\\]\\s*=\\s*)", + "description":"Detects code injection attempts 3\/3", + "tags":{ + "tag":[ + "id", + "rfe", + "lfi" + ] + }, + "impact":"7" + }, + { + "id":"62", + "rule":"(?:function[^(]*\\([^)]*\\))|(?:(?:delete|void|throw|instanceof|new|typeof)[^\\w.]+\\w+\\s*[([])|([)\\]]\\s*\\.\\s*\\w+\\s*=)|(?:\\(\\s*new\\s+\\w+\\s*\\)\\.)", + "description":"Detects common function declarations and special JS operators", + "tags":{ + "tag":[ + "id", + "rfe", + "lfi" + ] + }, + "impact":"5" + }, + { + "id":"63", + "rule":"(?:[\\w.-]+@[\\w.-]+%(?:[01][\\db-ce-f])+\\w+:)", + "description":"Detects common mail header injections", + "tags":{ + "tag":[ + "id", + "spam" + ] + }, + "impact":"5" + }, + { + "id":"64", + "rule":"(?:\\.pl\\?\\w+=\\w?\\|\\w+;)|(?:\\|\\(\\w+=\\*)|(?:\\*\\s*\\)+\\s*;)", + "description":"Detects perl echo shellcode injection and LDAP vectors", + "tags":{ + "tag":[ + "lfi", + "rfe" + ] + }, + "impact":"5" + }, + { + "id":"65", + "rule":"(?:(^|\\W)const\\s+[\\w\\-]+\\s*=)|(?:(?:do|for|while)\\s*\\([^;]+;+\\))|(?:(?:^|\\W)on\\w+\\s*=[\\w\\W]*(?:on\\w+|alert|eval|print|confirm|prompt))|(?:groups=\\d+\\(\\w+\\))|(?:(.)\\1{128,})", + "description":"Detects basic XSS DoS attempts", + "tags":{ + "tag":[ + "rfe", + "dos" + ] + }, + "impact":"5" + }, + { + "id":"67", + "rule":"(?:\\({2,}\\+{2,}:{2,})|(?:\\({2,}\\+{2,}:+)|(?:\\({3,}\\++:{2,})|(?:\\$\\[!!!\\])", + "description":"Detects unknown attack vectors based on PHPIDS Centrifuge detection", + "tags":{ + "tag":[ + "xss", + "csrf", + "id", + "rfe", + "lfi" + ] + }, + "impact":"7" + }, + { + "id":"68", + "rule":"(?:[\\s\\\/\"]+[-\\w\\\/\\\\\\*]+\\s*=.+(?:\\\/\\s*>))", + "description":"Finds attribute breaking injections including obfuscated attributes", + "tags":{ + "tag":[ + "xss", + "csrf" + ] + }, + "impact":"4" + }, + { + "id":"69", + "rule":"(?:(?:msgbox|eval)\\s*\\+|(?:language\\s*=\\*vbscript))", + "description":"Finds basic VBScript injection attempts", + "tags":{ + "tag":[ + "xss", + "csrf" + ] + }, + "impact":"4" + }, + { + "id":"70", + "rule":"(?:\\[\\$(?:ne|eq|lte?|gte?|n?in|mod|all|size|exists|type|slice|or)\\])", + "description":"Finds basic MongoDB SQL injection attempts", + "tags":{ + "tag":"sqli" + }, + "impact":"4" + }, + { + "id":"71", + "rule":"(?:[\\s\\d\\\/\"]+(?:on\\w+|style|poster|background)=[$\"\\w])|(?:-type\\s*:\\s*multipart)", + "description":"Finds malicious attribute injection attempts and MHTML attacks", + "tags":{ + "tag":[ + "xss", + "csrf" + ] + }, + "impact":"6" + }, + { + "id":"72", + "rule":"(?:(sleep\\((\\s*)(\\d*)(\\s*)\\)|benchmark\\((.*)\\,(.*)\\)))", + "description":"Detects blind sqli tests using sleep() or benchmark().", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"4" + }, + { + "id":"73", + "rule":"(?:(\\%SYSTEMROOT\\%))", + "description":"An attacker is trying to locate a file to read or write.", + "tags":{ + "tag":[ + "files", + "id" + ] + }, + "impact":"4" + }, + { + "id":"75", + "rule":"(?:(((.*)\\%[c|d|i|e|f|g|o|s|u|x|p|n]){8}))", + "description":"Looking for a format string attack", + "tags":{ + "tag":"format string" + }, + "impact":"4" + }, + { + "id":"76", + "rule":"(?:(union(.*)select(.*)from))", + "description":"Looking for basic sql injection. Common attack string for mysql, oracle and others.", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"3" + }, + { + "id":"77", + "rule":"(?:^(-0000023456|4294967295|4294967296|2147483648|2147483647|0000012345|-2147483648|-2147483649|0000023456|2.2250738585072007e-308|1e309)$)", + "description":"Looking for integer overflow attacks, these are taken from skipfish, except 2.2250738585072007e-308 is the \"magic number\" crash", + "tags":{ + "tag":[ + "sqli", + "id" + ] + }, + "impact":"3" + }, + { + "id":"78", + "rule":"(?:%23.*?%0a)", + "description":"Detects SQL comment filter evasion", + "tags":{ + "tag":[ + "format string" + ] + }, + "impact":"4" + } + ] + } +} diff --git a/interface/lib/classes/IDS/default_filter.xml b/interface/lib/classes/IDS/default_filter.xml new file mode 100644 index 000000000..fdd9cefbe --- /dev/null +++ b/interface/lib/classes/IDS/default_filter.xml @@ -0,0 +1,787 @@ + + + 1 + )|(?:[^\w\s]\s*\/>)|(?:>")]]> + Finds html breaking injections including whitespace attacks + + xss + csrf + + 4 + + + 2 + \w=\/)|(?:#.+\)["\s]*>)|(?:"\s*(?:src|style|on\w+)\s*=\s*")|(?:[^"]?"[,;\s]+\w*[\[\(])]]> + Finds attribute breaking injections including whitespace attacks + + xss + csrf + + 4 + + + 3 + [\w\s]*<\/?\w{2,}>)]]> + Finds unquoted attribute breaking injections + + xss + csrf + + 2 + + + 4 + ]\s*(?:location|referrer|name)\s*[^\/\w\s-])]]> + Detects url-, name-, JSON, and referrer-contained payload attacks + + xss + csrf + + 5 + + + 5 + + Detects hash-contained xss payload attacks, setter usage and property overloading + + xss + csrf + + 5 + + + 6 + + Detects self contained xss via with(), common loops and regex to string conversion + + xss + csrf + + 5 + + + 7 + + Detects JavaScript with(), ternary operators and XML predicate attacks + + xss + csrf + + 5 + + + 8 + + Detects self-executing JavaScript functions + + xss + csrf + + 5 + + + 9 + + Detects the IE octal, hex and unicode entities + + xss + csrf + + 2 + + + 10 + + Detects basic directory traversal + + dt + id + lfi + + 5 + + + 11 + + Detects specific directory and path traversal + + dt + id + lfi + + 5 + + + 12 + + Detects etc/passwd inclusion attempts + + dt + id + lfi + + 5 + + + 13 + + Detects halfwidth/fullwidth encoded unicode HTML breaking attempts + + xss + csrf + + 3 + + + 14 + + Detects possible includes, VBSCript/JScript encodeed and packed functions + + xss + csrf + id + rfe + + 5 + + + 15 + + Detects JavaScript DOM/miscellaneous properties and methods + + xss + csrf + id + rfe + + 6 + + + 16 + + Detects possible includes and typical script methods + + xss + csrf + id + rfe + + 5 + + + 17 + + Detects JavaScript object properties and methods + + xss + csrf + id + rfe + + 4 + + + 18 + + Detects JavaScript array properties and methods + + xss + csrf + id + rfe + + 4 + + + 19 + + Detects JavaScript string properties and methods + + xss + csrf + id + rfe + + 4 + + + 20 + + Detects JavaScript language constructs + + xss + csrf + id + rfe + + 4 + + + 21 + + Detects very basic XSS probings + + xss + csrf + id + rfe + + 3 + + + 22 + + Detects advanced XSS probings via Script(), RexExp, constructors and XML namespaces + + xss + csrf + id + rfe + + 5 + + + 23 + + Detects JavaScript location/document property access and window access obfuscation + + xss + csrf + + 5 + + + 24 + + Detects basic obfuscated JavaScript script injections + + xss + csrf + + 5 + + + 25 + + Detects obfuscated JavaScript script injections + + xss + csrf + + 5 + + + 26 + + Detects JavaScript cookie stealing and redirection attempts + + xss + csrf + + 4 + + + 27 + + Detects data: URL injections, VBS injections and common URI schemes + + xss + rfe + + 5 + + + 28 + + Detects IE firefoxurl injections, cache poisoning attempts and local file inclusion/execution + + xss + rfe + lfi + csrf + + 5 + + + 29 + + Detects bindings and behavior injections + + xss + csrf + rfe + + 4 + + + 30 + + Detects common XSS concatenation patterns 1/2 + + xss + csrf + id + rfe + + 4 + + + 31 + + Detects common XSS concatenation patterns 2/2 + + xss + csrf + id + rfe + + 4 + + + 32 + + Detects possible event handlers + + xss + csrf + + 4 + + + 33 + ]*)t(?!rong))|(?:\ + Detects obfuscated script tags and XML wrapped HTML + + xss + + 4 + + + 34 + + Detects attributes in closing tags and conditional compilation tokens + + xss + csrf + + 4 + + + 35 + )|(?:[^*]\/\*|\*\/[^*])|(?:(?:[\W\d]#|--|{)$)|(?:\/{3,}.*$)|(?:)]]> + Detects common comment types + + xss + csrf + id + + 3 + + + 37 + + Detects base href injections and XML entity injections + + xss + csrf + id + + 5 + + + 38 + + Detects possibly malicious html elements including some attributes + + xss + csrf + id + rfe + lfi + + 4 + + + 39 + + Detects nullbytes and other dangerous characters + + id + rfe + xss + + 5 + + + 40 + + Detects MySQL comments, conditions and ch(a)r injections + + sqli + id + lfi + + 6 + + + 41 + ~])]]> + Detects conditional SQL injection attempts + + sqli + id + lfi + + 6 + + + 42 + + Detects classic SQL injection probings 1/2 + + sqli + id + lfi + + 6 + + + 43 + %+-][\w-]+[^\w\s]+"[^,])]]> + Detects classic SQL injection probings 2/2 + + sqli + id + lfi + + 6 + + + 44 + =(),-]\s*[\d"])|(?:"\s*[^\w\s]?=\s*")|(?:"\W*[+=]+\W*")|(?:"\s*[!=|][\d\s!=+-]+.*["(].*$)|(?:"\s*[!=|][\d\s!=]+.*\d+$)|(?:"\s*like\W+[\w"(])|(?:\sis\s*0\W)|(?:where\s[\s\w\.,-]+\s=)|(?:"[<>~]+")]]> + Detects basic SQL authentication bypass attempts 1/3 + + sqli + id + lfi + + 7 + + + 45 + + Detects basic SQL authentication bypass attempts 2/3 + + sqli + id + lfi + + 7 + + + 46 + ^=]+\d\s*(=|or))|(?:"\W+[\w+-]+\s*=\s*\d\W+")|(?:"\s*is\s*\d.+"?\w)|(?:"\|?[\w-]{3,}[^\w\s.,]+")|(?:"\s*is\s*[\d.]+\s*\W.*")]]> + Detects basic SQL authentication bypass attempts 3/3 + + sqli + id + lfi + + 7 + + + 47 + + Detects concatenated basic SQL injection and SQLLFI attempts + + sqli + id + lfi + + 5 + + + 48 + + Detects chained SQL injection attempts 1/2 + + sqli + id + + 6 + + + 49 + + Detects chained SQL injection attempts 2/2 + + sqli + id + + 6 + + + 50 + + Detects SQL benchmark and sleep injection attempts including conditional queries + + sqli + id + + 4 + + + 51 + + Detects MySQL UDF injection and other data/structure manipulation attempts + + sqli + id + + 6 + + + 52 + + Detects MySQL charset switch and MSSQL DoS attempts + + sqli + id + + 6 + + + 53 + + Detects MySQL and PostgreSQL stored procedure/function injections + + sqli + id + + 7 + + + 54 + + Detects Postgres pg_sleep injection, waitfor delay attacks and database shutdown attempts + + sqli + id + + 5 + + + 55 + + Detects MSSQL code execution and information gathering attempts + + sqli + id + + 5 + + + 56 + + Detects MATCH AGAINST, MERGE, EXECUTE IMMEDIATE and HAVING injections + + sqli + id + + 5 + + + 57 + + Detects MySQL comment-/space-obfuscated injections and backtick termination + + sqli + id + + 5 + + + 58 + )?)|(?:;[\s\w|]*\$\w+\s*=)|(?:\$\w+\s*=(?:(?:\s*\$?\w+\s*[(;])|\s*".*"))|(?:;\s*\{\W*\w+\s*\()]]> + Detects code injection attempts 1/3 + + id + rfe + lfi + + 7 + + + 59 + + Detects code injection attempts 2/3 + + id + rfe + lfi + + 7 + + + 60 + + Detects code injection attempts 3/3 + + id + rfe + lfi + + 7 + + + 62 + + Detects common function declarations and special JS operators + + id + rfe + lfi + + 5 + + + 63 + + Detects common mail header injections + + id + spam + + 5 + + + 64 + + Detects perl echo shellcode injection and LDAP vectors + + lfi + rfe + + 5 + + + 65 + + Detects basic XSS DoS attempts + + rfe + dos + + 5 + + + 67 + + Detects unknown attack vectors based on PHPIDS Centrifuge detection + + xss + csrf + id + rfe + lfi + + 7 + + + 68 + ))]]> + Finds attribute breaking injections including obfuscated attributes + + xss + csrf + + 4 + + + 69 + + Finds basic VBScript injection attempts + + xss + csrf + + 4 + + + 70 + + Finds basic MongoDB SQL injection attempts + + sqli + + 4 + + + 71 + + Finds malicious attribute injection attempts and MHTML attacks + + xss + csrf + + 6 + + + 72 + + Detects blind sqli tests using sleep() or benchmark(). + + sqli + id + + 4 + + + 73 + + An attacker is trying to locate a file to read or write. + + files + id + + 4 + + + 75 + + Looking for a format string attack + + format string + + 4 + + + 76 + + Looking for basic sql injection. Common attack string for mysql, oracle and others. + + sqli + id + + 3 + + + 77 + + Looking for integer overflow attacks, these are taken from skipfish, except 2.2250738585072007e-308 is the "magic number" crash + + sqli + id + + 3 + + + 78 + + Detects SQL comment filter evasion + + format string + + 4 + + diff --git a/interface/lib/classes/IDS/license.txt b/interface/lib/classes/IDS/license.txt new file mode 100644 index 000000000..4a3cd1b3e --- /dev/null +++ b/interface/lib/classes/IDS/license.txt @@ -0,0 +1,18 @@ + * + * The files in the folder IDS and its subfolders belong to the + * PHP Intrusion Detection System software and are licensed under LGPL. + * + * Copyright (c) 2008 PHPIDS group (https://phpids.org) + * + * PHPIDS is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * PHPIDS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with PHPIDS. If not, see . \ No newline at end of file diff --git a/interface/lib/classes/db_mysql.inc.php b/interface/lib/classes/db_mysql.inc.php index 56d43f879..b96b8d019 100644 --- a/interface/lib/classes/db_mysql.inc.php +++ b/interface/lib/classes/db_mysql.inc.php @@ -121,6 +121,52 @@ class db extends mysqli parent::query( 'SET NAMES '.$this->dbCharset); parent::query( "SET character_set_results = '".$this->dbCharset."', character_set_client = '".$this->dbCharset."', character_set_connection = '".$this->dbCharset."', character_set_database = '".$this->dbCharset."', character_set_server = '".$this->dbCharset."'"); } + + private function securityScan($string) { + global $app, $conf; + + // get security config + if(isset($app)) { + $app->uses('getconf'); + $ids_config = $app->getconf->get_security_config('ids'); + + if($ids_config['sql_scan_enabled'] == 'yes') { + + $string_orig = $string; + + //echo $string; + $chars = array(';', '#', '/*', '*/', '--', ' UNION ', '\\\'', '\\"'); + + $string = str_replace('\\\\', '', $string); + $string = preg_replace('/(^|[^\\\])([\'"])(.*?[^\\\])\\2/is', '$1', $string); + $ok = true; + + if(substr_count($string, "`") % 2 != 0 || substr_count($string, "'") % 2 != 0 || substr_count($string, '"') % 2 != 0) { + $app->log("SQL injection warning (" . $string_orig . ")",2); + $ok = false; + } else { + foreach($chars as $char) { + if(strpos($string, $char) !== false) { + $ok = false; + $app->log("SQL injection warning (" . $string_orig . ")",2); + break; + } + } + } + if($ok == true) { + return true; + } else { + if($ids_config['sql_scan_action'] == 'warn') { + // we return false in warning level. + return false; + } else { + // if sql action = 'block' or anything else then stop here. + $app->error('Possible SQL injection. All actions have been logged.'); + } + } + } + } + } public function query($queryString) { global $conf; @@ -143,6 +189,7 @@ class db extends mysqli } } } while($ok == false); + $this->securityScan($queryString); $this->queryId = parent::query($queryString); $this->updateError('DB::query('.$queryString.') -> mysqli_query'); if($this->errorNumber && $conf['demo_mode'] === false) debug_print_backtrace(); diff --git a/interface/lib/classes/getconf.inc.php b/interface/lib/classes/getconf.inc.php index 5cc223ae7..0cc282e02 100644 --- a/interface/lib/classes/getconf.inc.php +++ b/interface/lib/classes/getconf.inc.php @@ -31,6 +31,7 @@ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. class getconf { private $config; + private $security_config; public function get_server_config($server_id, $section = '') { global $app; @@ -55,15 +56,20 @@ class getconf { return ($section == '') ? $this->config['global'] : $this->config['global'][$section]; } + // Function has been moved to $app->get_security_config($section) public function get_security_config($section = '') { global $app; + + if(is_array($this->security_config)) { + return ($section == '') ? $this->security_config : $this->security_config[$section]; + } else { + $this->uses('ini_parser'); + $security_config_path = '/usr/local/ispconfig/security/security_settings.ini'; + if(!is_file($security_config_path)) $security_config_path = realpath(ISPC_ROOT_PATH.'/../security/security_settings.ini'); + $this->security_config = $this->ini_parser->parse_ini_string(file_get_contents($security_config_path)); - $app->uses('ini_parser'); - $security_config_path = '/usr/local/ispconfig/security/security_settings.ini'; - if(!is_file($security_config_path)) $security_config_path = realpath(ISPC_ROOT_PATH.'/../security/security_settings.ini'); - $security_config = $app->ini_parser->parse_ini_string(file_get_contents($security_config_path)); - - return ($section == '') ? $security_config : $security_config[$section]; + return ($section == '') ? $this->security_config : $this->security_config[$section]; + } } } diff --git a/interface/lib/classes/ids.inc.php b/interface/lib/classes/ids.inc.php new file mode 100644 index 000000000..a98b0b265 --- /dev/null +++ b/interface/lib/classes/ids.inc.php @@ -0,0 +1,149 @@ +getconf->get_security_config('ids'); + + set_include_path( + get_include_path() + . PATH_SEPARATOR + . ISPC_CLASS_PATH.'/' + ); + + require_once(ISPC_CLASS_PATH.'/IDS/Init.php'); + require_once(ISPC_CLASS_PATH.'/IDS/Monitor.php'); + require_once(ISPC_CLASS_PATH.'/IDS/Filter.php'); + require_once(ISPC_CLASS_PATH.'/IDS/Filter/Storage.php'); + require_once(ISPC_CLASS_PATH.'/IDS/Report.php'); + require_once(ISPC_CLASS_PATH.'/IDS/Event.php'); + require_once(ISPC_CLASS_PATH.'/IDS/Converter.php'); + + $ids_request = array( + 'SESSION' => $_SESSION, + 'GET' => $_GET, + 'POST' => $_POST, + 'COOKIE' => $_COOKIE + ); + + $ids_init = IDS\Init::init(ISPC_CLASS_PATH.'/IDS/Config/Config.ini.php'); + + $ids_init->config['General']['base_path'] = ISPC_CLASS_PATH.'/IDS/'; + $ids_init->config['General']['tmp_path'] = '../../../temp'; + $ids_init->config['General']['use_base_path'] = true; + $ids_init->config['Caching']['caching'] = 'none'; + $ids_init->config['Logging']['path'] = '../../../temp/ids.log'; + + $current_script_name = trim($_SERVER['SCRIPT_NAME']); + + // Get whitelist + $whitelist_path = '/usr/local/ispconfig/security/ids.whitelist'; + if(is_file('/usr/local/ispconfig/security/ids.whitelist.custom')) $whitelist_path = '/usr/local/ispconfig/security/ids.whitelist.custom'; + if(!is_file($whitelist_path)) $whitelist_path = realpath(ISPC_ROOT_PATH.'/../security/ids.whitelist'); + + $whitelist_lines = file($whitelist_path); + if(is_array($whitelist_lines)) { + foreach($whitelist_lines as $line) { + $line = trim($line); + if(substr($line,0,1) != '#') { + list($user,$path,$varname) = explode(':',$line); + if($current_script_name == $path) { + if($user = 'any' + || ($user == 'user' && ($_SESSION['s']['user']['typ'] == 'user' || $_SESSION['s']['user']['typ'] == 'admin')) + || ($user == 'admin' && $_SESSION['s']['user']['typ'] == 'admin')) { + $ids_init->config['General']['exceptions'][] = $varname; + + } + } + } + } + } + + // Get HTML fields + $htmlfield_path = '/usr/local/ispconfig/security/ids.htmlfield'; + if(is_file('/usr/local/ispconfig/security/ids.htmlfield.custom')) $htmlfield_path = '/usr/local/ispconfig/security/ids.htmlfield.custom'; + if(!is_file($htmlfield_path)) $htmlfield_path = realpath(ISPC_ROOT_PATH.'/../security/ids.htmlfield'); + + $htmlfield_lines = file($htmlfield_path); + if(is_array($htmlfield_lines)) { + foreach($htmlfield_lines as $line) { + $line = trim($line); + if(substr($line,0,1) != '#') { + list($user,$path,$varname) = explode(':',$line); + if($current_script_name == $path) { + if($user = 'any' + || ($user == 'user' && ($_SESSION['s']['user']['typ'] == 'user' || $_SESSION['s']['user']['typ'] == 'admin')) + || ($user == 'admin' && $_SESSION['s']['user']['typ'] == 'admin')) { + $ids_init->config['General']['html'][] = $varname; + } + } + } + } + } + + $ids = new IDS\Monitor($ids_init); + $ids_result = $ids->run($ids_request); + + if (!$ids_result->isEmpty()) { + + $impact = $ids_result->getImpact(); + + if($impact >= $security_config['ids_log_level']) { + $ids_log = ISPC_ROOT_PATH.'/temp/ids.log'; + if(!is_file($ids_log)) touch($ids_log); + + $user = isset($_SESSION['s']['user']['typ'])?$_SESSION['s']['user']['typ']:'any'; + + $log_lines = ''; + foreach ($ids_result->getEvents() as $event) { + $log_lines .= $user.':'.$current_script_name.':'.$event->getName()."\n"; + } + file_put_contents($ids_log,$log_lines,FILE_APPEND); + + } + + if($impact >= $security_config['ids_warn_level']) { + $app->log("PHP IDS Alert.".$ids_result, 2); + } + + if($impact >= $security_config['ids_block_level']) { + $app->error("Possible attack detected. This action has been logged.",'', true, 2); + } + + } + } + +} + +?> diff --git a/security/apache_directives.blacklist b/security/apache_directives.blacklist new file mode 100644 index 000000000..edb4b503d --- /dev/null +++ b/security/apache_directives.blacklist @@ -0,0 +1,3 @@ +/^\s*(LoadModule|LoadFile|Include)(\s+|[\\\\])/mi +/^\s*(SuexecUserGroup|suPHP_UserGroup|suPHP_PHPPath|suPHP_ConfigPath)(\s+|[\\\\])/mi +/^\s*(FCGIWrapper|FastCgiExternalServer)(\s+|[\\\\])/mi \ No newline at end of file diff --git a/security/ids.htmlfield b/security/ids.htmlfield new file mode 100644 index 000000000..2c6e92a4a --- /dev/null +++ b/security/ids.htmlfield @@ -0,0 +1,5 @@ +# Format: usertype:url_path:field +# usertype can be: any/client/admin +# Example: +# admin:/admin/language_edit.php:POST.records.weak_password_txt +admin:/admin/language_edit.php:POST.records \ No newline at end of file diff --git a/security/ids.whitelist b/security/ids.whitelist new file mode 100644 index 000000000..58a96e6ac --- /dev/null +++ b/security/ids.whitelist @@ -0,0 +1,45 @@ +# Format: usertype:url_path:field +# usertype can be: any/client/admin +# Example: +# admin:/admin/language_edit.php:POST.records.weak_password_txt +admin:/admin/server_config_edit.php:POST.maildir_path +admin:/admin/server_config_edit.php:POST.website_path +admin:/admin/server_config_edit.php:POST.website_symlinks +admin:/admin/server_config_edit.php:POST.vhost_conf_dir +admin:/admin/server_config_edit.php:POST.vhost_conf_enabled_dir +admin:/admin/server_config_edit.php:POST.nginx_vhost_conf_dir +admin:/admin/server_config_edit.php:POST.nginx_vhost_conf_enabled_dir +admin:/admin/server_config_edit.php:POST.php_open_basedir +admin:/admin/server_config_edit.php:POST.awstats_pl +admin:/admin/server_config_edit.php:POST.fastcgi_starter_path +admin:/admin/server_config_edit.php:POST.fastcgi_bin +admin:/admin/server_config_edit.php:POST.jailkit_chroot_home +admin:/admin/remote_user_edit.php:POST.remote_functions.1 +admin:/admin/firewall_edit.php:POST.tcp_port +admin:/admin/system_config_edit.php:POST.admin_dashlets_right +admin:/admin/system_config_edit.php:POST.reseller_dashlets_left +admin:/admin/system_config_edit.php:POST.reseller_dashlets_right +admin:/admin/language_edit.php:POST.records.weak_password_txt +user:/client/client_message.php:POST.message +user:/client/message_template_edit.php:POST.subject +admin:/dns/dns_template_edit.php:POST.template +admin:/nav.php:SESSION.s.module.nav.1.items.0.title +admin:/monitor/show_sys_state.php:SESSION.s.module.nav.1.items.0.title +admin:/capp.php:SESSION.s.module.nav.1.items.0.title +admin:/keepalive.php:SESSION.s.module.nav.1.items.0.title +admin:/monitor/log_list.php:SESSION.s.module.nav.1.items.0.title +admin:/monitor/datalog_list.php:SESSION.s.module.nav.1.items.0.title +admin:/monitor/show_data.php:SESSION.s.module.nav.1.items.0.title +admin:/monitor/show_sys_state.php:SESSION.s.module.nav.1.items.0.title +admin:/monitor/show_monit.php:SESSION.s.module.nav.1.items.0.title +admin:/monitor/show_munin.php:SESSION.s.module.nav.1.items.0.title +admin:/monitor/show_data.php:SESSION.s.module.nav.1.items.0.title +admin:/monitor/show_log.php:SESSION.s.module.nav.1.items.0.title +admin:/monitor/log_del.php:SESSION.s.module.nav.1.items.0.title +admin:/keepalive.php:SESSION.s.module.nav.1.items.0.title +admin:/capp.php:SESSION.s.module.nav.1.items.0.title +admin:/sites/web_vhost_subdomain_edit.php:POST.php_open_basedir +admin:/sites/web_domain_edit.php:POST.php_open_basedir +admin:/sites/web_domain_edit.php:POST.apache_directives +user:/sites/shell_user_edit.php:POST.ssh_rsa +user:/sites/cron_edit.php:POST.command \ No newline at end of file diff --git a/security/security_settings.ini b/security/security_settings.ini index 4dfe00162..43bcebf1e 100644 --- a/security/security_settings.ini +++ b/security/security_settings.ini @@ -16,6 +16,15 @@ admin_allow_software_packages=superadmin admin_allow_software_repo=superadmin remote_api_allowed=yes +[ids] +ids_enabled=yes +ids_log_level=1 +ids_warn_level=5 +ids_block_level=30 +sql_scan_enabled=yes +sql_scan_action=warn +apache_directives_scan_enabled=yes + [systemcheck] security_admin_email=root@localhost security_admin_email_subject=Security alert from server -- GitLab