├── tests ├── travis │ ├── do_run.sh │ ├── basic.sh │ └── https.sh ├── test.sh ├── core_https.sh ├── internal.sh ├── internal_https.sh └── core.sh ├── .gitignore ├── cmake └── modules │ ├── FindApr.cmake │ └── FindApache.cmake ├── deps └── libinjection │ ├── libinjection_xss.h │ ├── libinjection_html5.h │ ├── libinjection.h │ ├── libinjection_sqli.h │ ├── libinjection_xss.c │ └── libinjection_html5.c ├── .travis.yml ├── JsonValidator.hpp ├── CMakeLists.txt ├── Util.h ├── mod_defender.hpp ├── RuntimeScanner.hpp ├── README.md ├── JsonValidator.cpp ├── RuleParser.h ├── Util.cpp ├── mod_defender_body.cpp ├── mod_defender.cpp └── RuleParser.cpp /tests/travis/do_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export DEFENDER_HOME=`pwd` 4 | echo running tests/travis/$RUN, home: $DEFENDER_HOME 5 | bash $DEFENDER_HOME/tests/travis/$RUN -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | *.a 4 | .idea/ 5 | conf/ 6 | cmake-build-debug/ 7 | lib/ 8 | build/ 9 | CMakeFiles/ 10 | CMakeCache.txt 11 | cmake_install.cmake 12 | Makefile -------------------------------------------------------------------------------- /cmake/modules/FindApr.cmake: -------------------------------------------------------------------------------- 1 | find_path(APR_INC 2 | NAMES apr.h 3 | HINTS 4 | /usr/include/apr-1 5 | /usr/include/apr-1.0 6 | /usr/local/include/apr-1 7 | /usr/local/include/apr-1.0) 8 | include(FindPackageHandleStandardArgs) 9 | find_package_handle_standard_args(APR DEFAULT_MSG APR_INC) 10 | -------------------------------------------------------------------------------- /deps/libinjection/libinjection_xss.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBINJECTION_XSS 2 | #define LIBINJECTION_XSS 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | /** 9 | * HEY THIS ISN'T DONE 10 | */ 11 | 12 | /* pull in size_t */ 13 | 14 | #include 15 | 16 | int libinjection_is_xss(const char* s, size_t len, int flags); 17 | 18 | #ifdef __cplusplus 19 | } 20 | #endif 21 | #endif 22 | -------------------------------------------------------------------------------- /cmake/modules/FindApache.cmake: -------------------------------------------------------------------------------- 1 | find_path(APACHE_INC 2 | NAMES httpd.h 3 | HINTS 4 | /usr/include/apache2 5 | /usr/include 6 | /usr/local/include/apache2 7 | /usr/local/include/apache22 8 | /usr/local/include/apache24 9 | /usr/home/vlt-sys/Engine/include) 10 | include(FindPackageHandleStandardArgs) 11 | find_package_handle_standard_args(APACHE DEFAULT_MSG APACHE_INC) 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | os: linux 4 | dist: trusty 5 | 6 | language: cpp 7 | compiler: gcc 8 | 9 | addons: 10 | apt: 11 | packages: 12 | - apache2 13 | - apache2-dev 14 | - g++-6 15 | - gcc-6 16 | sources: 17 | - ubuntu-toolchain-r-test 18 | 19 | before_install: 20 | 21 | install: 22 | - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-6 90 23 | - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-6 90 24 | 25 | matrix: 26 | allow_failures: 27 | exclude: 28 | - compiler: "gcc" 29 | 30 | include: 31 | - os: linux 32 | compiler: "gcc" 33 | env: RUN="basic.sh" 34 | 35 | - os: linux 36 | compiler: "gcc" 37 | env: RUN="https.sh" 38 | 39 | script: 40 | - /bin/bash ./tests/travis/do_run.sh 41 | 42 | after_script: 43 | - sudo cat /var/log/apache2/error.log 44 | # - sudo cat /var/log/apache2/defender_match.log 45 | - sudo cat /var/log/apache2/defender_json_match.log 46 | -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 1 ]; then 4 | echo "Usage: $0 " 5 | exit 0 6 | fi 7 | HOST=$1 8 | curl_ret="-s -o /dev/null -w %{http_code}" 9 | 10 | PASS_MESSAGE="[ \033[0;32mPASS\033[0m ]" 11 | FAIL_MESSAGE="[ \033[0;31mFAIL\033[0m ]" 12 | 13 | check_block() { 14 | if ([ $2 == 0 ] && ([ $1 == 200 ] || [ $1 == 404 ])) || 15 | ([ $2 == 1 ] && [ $1 == 403 ]) && 16 | [ $1 -lt 500 ] 17 | then 18 | printf "$PASS_MESSAGE" 19 | return 1 20 | else 21 | printf "$FAIL_MESSAGE" 22 | return 0 23 | fi 24 | } 25 | 26 | check_status_code() { 27 | if ([ $1 == $2 ]) then 28 | printf "$PASS_MESSAGE" 29 | return 1 30 | else 31 | printf "$FAIL_MESSAGE" 32 | return 0 33 | fi 34 | } 35 | 36 | url_encode() { 37 | local string="$1" 38 | local strlen=${#string} 39 | local encoded="" 40 | local pos c o 41 | 42 | for ((pos=0; pos 11 | 12 | enum html5_type { 13 | DATA_TEXT 14 | , TAG_NAME_OPEN 15 | , TAG_NAME_CLOSE 16 | , TAG_NAME_SELFCLOSE 17 | , TAG_DATA 18 | , TAG_CLOSE 19 | , ATTR_NAME 20 | , ATTR_VALUE 21 | , TAG_COMMENT 22 | , DOCTYPE 23 | }; 24 | 25 | enum html5_flags { 26 | DATA_STATE 27 | , VALUE_NO_QUOTE 28 | , VALUE_SINGLE_QUOTE 29 | , VALUE_DOUBLE_QUOTE 30 | , VALUE_BACK_QUOTE 31 | }; 32 | 33 | struct h5_state; 34 | typedef int (*ptr_html5_state)(struct h5_state*); 35 | 36 | typedef struct h5_state { 37 | const char* s; 38 | size_t len; 39 | size_t pos; 40 | int is_close; 41 | ptr_html5_state state; 42 | const char* token_start; 43 | size_t token_len; 44 | enum html5_type token_type; 45 | } h5_state_t; 46 | 47 | 48 | void libinjection_h5_init(h5_state_t* hs, const char* s, size_t len, enum html5_flags); 49 | int libinjection_h5_next(h5_state_t* hs); 50 | 51 | #ifdef __cplusplus 52 | } 53 | #endif 54 | #endif 55 | -------------------------------------------------------------------------------- /JsonValidator.hpp: -------------------------------------------------------------------------------- 1 | /* _ _ __ _ 2 | * _ __ ___ ___ __| | __| | ___ / _| ___ _ __ __| | ___ _ __ 3 | * | '_ ` _ \ / _ \ / _` | / _` |/ _ \ |_ / _ \ '_ \ / _` |/ _ \ '__| 4 | * | | | | | | (_) | (_| | | (_| | __/ _| __/ | | | (_| | __/ | 5 | * |_| |_| |_|\___/ \__,_|___\__,_|\___|_| \___|_| |_|\__,_|\___|_| 6 | * |_____| 7 | * Copyright (c) 2017 Annihil 8 | * Released under the GPLv3 9 | */ 10 | 11 | #ifndef MOD_DEFENDER_JSONVALIDATOR_H 12 | #define MOD_DEFENDER_JSONVALIDATOR_H 13 | 14 | #include "Util.h" 15 | 16 | class RuntimeScanner; 17 | 18 | /* 19 | ** To avoid getting DoS'ed, define max depth 20 | ** for JSON parser, as it is recursive 21 | */ 22 | #define JSON_MAX_DEPTH 10 23 | 24 | /* 25 | ** this structure is used only for json parsing. 26 | */ 27 | typedef struct { 28 | str_t json; 29 | u_char *src; 30 | unsigned long off = 0, len = 0; 31 | u_char c; 32 | int depth = 0; 33 | str_t ckey; 34 | } json_t; 35 | 36 | class JsonValidator { 37 | friend class RuntimeScanner; 38 | private: 39 | RuntimeScanner& scanner; 40 | bool jsonObj(json_t &js); 41 | bool jsonVal(json_t &js); 42 | bool jsonArray(json_t &js); 43 | bool jsonQuoted(json_t &js, str_t *ve); 44 | bool jsonForward(json_t &js); 45 | bool jsonSeek(json_t &js, unsigned char seek); 46 | public: 47 | JsonValidator(RuntimeScanner& scanner) : scanner(scanner) {} 48 | void jsonParse(u_char *src, unsigned long len); 49 | }; 50 | 51 | #endif //MOD_DEFENDER_JSONVALIDATOR_H 52 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2) 2 | project(mod_defender) 3 | set(CMAKE_BUILD_TYPE Release) 4 | set(CMAKE_SHARED_LIBRARY_PREFIX "") 5 | 6 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules") 7 | 8 | set(CMAKE_CXX_FLAGS "-W -Wall -Wextra") 9 | 10 | message("FLAGS = ${CMAKE_CXX_FLAGS}") 11 | 12 | find_package(Apache) 13 | find_package(Apr) 14 | 15 | include_directories(deps ${APACHE_INC} ${APR_INC}) 16 | 17 | set(CMAKE_CXX_STANDARD 11) 18 | 19 | file(GLOB SOURCE_FILES *.cpp deps/libinjection/*.c) 20 | add_library(mod_defender SHARED ${SOURCE_FILES}) 21 | 22 | if (AUTO) 23 | set(STOP_APACHE_CMD sudo systemctl stop apache2) 24 | set(START_APACHE_CMD sudo systemctl start apache2) 25 | set(AP_MODS_AV /etc/apache2/mods-available) 26 | set(AP_MODS_DIR /usr/lib/apache2/modules) 27 | 28 | if (${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") 29 | set(STOP_APACHE_CMD service apache24 restart) 30 | set(START_APACHE_CMD service apache24 restart) 31 | set(AP_MODS_DIR /usr/local/libexec/apache24/) 32 | 33 | if (EXISTS "/usr/local/etc/rc.d/vulture") 34 | set(STOP_APACHE_CMD "") 35 | set(START_APACHE_CMD "") 36 | set(AP_MODS_DIR /usr/home/vlt-sys/Engine/modules/) 37 | endif () 38 | endif () 39 | 40 | add_custom_command( 41 | TARGET mod_defender 42 | POST_BUILD 43 | COMMAND ${STOP_APACHE_CMD} 44 | COMMAND cp $ ${AP_MODS_DIR} 45 | COMMAND ${START_APACHE_CMD} 46 | COMMENT "Copying module then restarting Apache") 47 | endif () 48 | -------------------------------------------------------------------------------- /tests/travis/basic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | sudo mkdir /etc/defender/ 6 | sudo wget -O /etc/defender/core.rules https://raw.githubusercontent.com/nbs-system/naxsi/master/naxsi_config/naxsi_core.rules 7 | sudo sed -i "s/select|union|update|delete|insert|table|from|ascii|hex|unhex|drop/\\\b(select|union|update|delete|insert|table|from|ascii|hex|unhex|drop)\\\b/" /etc/defender/core.rules 8 | 9 | printf \ 10 | "LoadModule defender_module /usr/lib/apache2/modules/mod_defender.so 11 | 12 | Include /etc/defender/core.rules 13 | " | sudo tee /etc/apache2/mods-available/defender.load 14 | 15 | sudo apachectl -v 16 | sudo apachectl -M 17 | sudo a2enmod defender 18 | sudo service apache2 stop 19 | 20 | printf \ 21 | " 22 | LogLevel notice 23 | ErrorLog \${APACHE_LOG_DIR}/error.log 24 | AllowEncodedSlashes On 25 | 26 | 27 | Defender On 28 | MatchLog \${APACHE_LOG_DIR}/defender_match.log 29 | JSONMatchLog \${APACHE_LOG_DIR}/defender_json_match.log 30 | RequestBodyLimit 8388608 31 | LearningMode Off 32 | ExtensiveLog Off 33 | LibinjectionSQL Off 34 | LibinjectionXSS Off 35 | CheckRule \"\$SQL >= 8\" BLOCK 36 | CheckRule \"\$RFI >= 8\" BLOCK 37 | CheckRule \"\$TRAVERSAL >= 4\" BLOCK 38 | CheckRule \"\$EVADE >= 4\" BLOCK 39 | CheckRule \"\$XSS >= 8\" BLOCK 40 | CheckRule \"\$UPLOAD >= 8\" BLOCK 41 | 42 | 43 | " | sudo tee /etc/apache2/sites-available/000-default.conf 44 | 45 | cmake -H. -Bbuild 46 | cmake --build build 47 | sudo cp build/mod_defender.so /usr/lib/apache2/modules/ 48 | sudo service apache2 start 49 | cd tests/ 50 | bash core.sh localhost 51 | bash internal.sh localhost -------------------------------------------------------------------------------- /deps/libinjection/libinjection.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012-2016 Nick Galbreath 3 | * nickg@client9.com 4 | * BSD License -- see COPYING.txt for details 5 | * 6 | * https://libinjection.client9.com/ 7 | * 8 | */ 9 | 10 | #ifndef LIBINJECTION_H 11 | #define LIBINJECTION_H 12 | 13 | #ifdef __cplusplus 14 | # define LIBINJECTION_BEGIN_DECLS extern "C" { 15 | # define LIBINJECTION_END_DECLS } 16 | #else 17 | # define LIBINJECTION_BEGIN_DECLS 18 | # define LIBINJECTION_END_DECLS 19 | #endif 20 | 21 | LIBINJECTION_BEGIN_DECLS 22 | 23 | /* 24 | * Pull in size_t 25 | */ 26 | #include 27 | 28 | /* 29 | * Version info. 30 | * 31 | * This is moved into a function to allow SWIG and other auto-generated 32 | * binding to not be modified during minor release changes. We change 33 | * change the version number in the c source file, and not regenerated 34 | * the binding 35 | * 36 | * See python's normalized version 37 | * http://www.python.org/dev/peps/pep-0386/#normalizedversion 38 | */ 39 | const char* libinjection_version(void); 40 | 41 | /** 42 | * Simple API for SQLi detection - returns a SQLi fingerprint or NULL 43 | * is benign input 44 | * 45 | * \param[in] s input string, may contain nulls, does not need to be null-terminated 46 | * \param[in] slen input string length 47 | * \param[out] fingerprint buffer of 8+ characters. c-string, 48 | * \return 1 if SQLi, 0 if benign. fingerprint will be set or set to empty string. 49 | */ 50 | int libinjection_sqli(const char* s, size_t slen, char fingerprint[]); 51 | 52 | /** ALPHA version of xss detector. 53 | * 54 | * NOT DONE. 55 | * 56 | * \param[in] s input string, may contain nulls, does not need to be null-terminated 57 | * \param[in] slen input string length 58 | * \return 1 if XSS found, 0 if benign 59 | * 60 | */ 61 | int libinjection_xss(const char* s, size_t slen); 62 | 63 | LIBINJECTION_END_DECLS 64 | 65 | #endif /* LIBINJECTION_H */ 66 | -------------------------------------------------------------------------------- /tests/core_https.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./test.sh 4 | 5 | declare -a tests=( 6 | # " -d a=blah" 0 7 | ) 8 | 9 | # BODY BODY_NAME URL ARGS ARGS_NAME $HEADERS_VAR:Cookie 10 | declare -a core_rules_tests=( 11 | # SQL Injections IDs:1000-1099 12 | "blah" 0 0 0 0 0 0 13 | "select+from" 1 1 1 1 1 1 14 | "selected+fromage" 0 0 0 0 0 0 15 | "\"" 1 1 1 1 1 1 16 | "0x0x0x0x" 1 1 1 1 1 1 17 | "/*" 1 1 1 1 1 1 18 | "*/" 1 1 1 1 1 1 19 | "|" 1 1 1 1 1 1 20 | "&&" 1 1 1 1 1 1 21 | "----" 1 1 1 1 1 1 22 | ";" 1 1 1 1 1 0 23 | "====" 1 1 0 1 1 0 24 | "(" 1 1 1 1 1 1 25 | ")" 1 1 1 1 1 1 26 | "'" 1 1 1 1 1 1 27 | ",," 1 1 1 1 1 1 28 | "##" 1 1 1 1 1 1 29 | "@@@@" 1 1 1 1 1 1 30 | 31 | # OBVIOUS RFI IDs:1100-1199 32 | "http://" 1 1 0 1 1 1 33 | "https://" 1 1 0 1 1 1 34 | "ftp://" 1 1 0 1 1 1 35 | "sftp://" 1 1 0 1 1 1 36 | "zlib://" 1 1 0 1 1 1 37 | "data://" 1 1 0 1 1 1 38 | "glob://" 1 1 0 1 1 1 39 | "phar://" 1 1 0 1 1 1 40 | "file://" 1 1 0 1 1 1 41 | "gopher://" 1 1 0 1 1 1 42 | 43 | # Directory traversal IDs:1200-1299 44 | "...." 1 1 1 1 1 1 45 | "/etc/passwd" 1 1 1 1 1 1 46 | "c:\\" 1 1 1 1 1 1 47 | "cmd.exe" 1 1 1 1 1 1 48 | "\\" 1 1 1 1 1 1 49 | 50 | # Cross Site Scripting IDs:1300-1399 51 | "<" 1 1 1 1 1 1 52 | ">" 1 1 1 1 1 1 53 | "[[" 1 1 1 1 1 1 54 | "]]" 1 1 1 1 1 1 55 | "~~" 1 1 1 1 1 1 56 | "\`" 1 1 1 1 1 1 57 | "%20" 1 1 1 1 1 1 58 | 59 | # Evading tricks IDs: 1400-1500 60 | "&#" 1 1 1 1 1 1 61 | "%U" 1 1 1 1 1 1 62 | ) 63 | for ((i=0; i<${#core_rules_tests[@]}; i+=7)); do 64 | pattern=${core_rules_tests[$i]} 65 | tests+=(" --data-urlencode x=$pattern" ${core_rules_tests[$i+1]}) 66 | tests+=(" -d $(url_encode "$pattern")=x" ${core_rules_tests[$i+2]}) 67 | tests+=($(url_encode "$pattern") ${core_rules_tests[$i+3]}) 68 | tests+=("?x="$(url_encode "$pattern") ${core_rules_tests[$i+4]}) 69 | tests+=("?$(url_encode "$pattern")=x" ${core_rules_tests[$i+5]}) 70 | tests+=(" -b x=$pattern" ${core_rules_tests[$i+6]}) 71 | done 72 | 73 | tests_size=${#tests[@]} 74 | test_count=$((tests_size / 2)) 75 | test_passed=0 76 | 77 | for ((i=0; i<$tests_size; i+=2)); do 78 | req="curl --cacert $ca_path https://$HOST/${tests[$i]}" 79 | expected_action=${tests[$i+1]} 80 | status_code=`$req $curl_ret` 81 | test_msg=`check_block $status_code $expected_action` 82 | test_passed=$((test_passed + $?)) 83 | printf "%-95s %s\n" "$req" "$status_code $test_msg" 84 | done 85 | 86 | echo $test_passed/$test_count "tests passed" \($(((test_passed * 100) / test_count))%\) 87 | exit $(($test_passed != $test_count)) -------------------------------------------------------------------------------- /tests/internal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./test.sh 4 | 5 | test_passed=0 6 | test_count=0 7 | 8 | status_code=$(printf %1000000s | tr " " "a" | curl $HOST --data-binary @- $curl_ret) 9 | test_msg=`check_block $status_code 0` 10 | test_passed=$((test_passed + $?)) 11 | test_count=$((test_count + 1)) 12 | echo -e "sent 1MB " "$req" "$status_code $test_msg" 13 | 14 | status_code=$(printf "%2000000s" | tr " " "a" | curl $HOST --data-binary @- --limit-rate 350k $curl_ret) 15 | test_msg=`check_block $status_code 0` 16 | test_passed=$((test_passed + $?)) 17 | test_count=$((test_count + 1)) 18 | echo -e "sent 2MB @ 350kb/s " "$req" "$status_code $test_msg" 19 | 20 | status_code=$(printf %1000s | tr " " "a" | curl $HOST --data-binary @- -H "Transfer-Encoding: chunked" $curl_ret) 21 | test_msg=`check_status_code $status_code 501` 22 | test_passed=$((test_passed + $?)) 23 | test_count=$((test_count + 1)) 24 | echo -e "sent 1kB with transfer-encoding: chunked " "$req" "$status_code $test_msg" 25 | 26 | status_code=$(curl $HOST -X POST -H 'Content-Length:' $curl_ret) 27 | test_msg=`check_block $status_code 1` 28 | test_passed=$((test_passed + $?)) 29 | test_count=$((test_count + 1)) 30 | echo -e "sent POST request without content-length " "$req" "$status_code $test_msg" 31 | 32 | # Not working on Travis 33 | # status_code=$(printf "%2000000s" | tr " " "a" | curl $HOST --data-binary @- --limit-rate 100k $curl_ret) 34 | # test_msg=`check_status_code $status_code 500` 35 | # test_passed=$((test_passed + $?)) 36 | # test_count=$((test_count + 1)) 37 | # echo -e "sent 2MB @ 100kb/s (timeout by mod_reqtimeout)" "$req" "$status_code $test_msg" 38 | 39 | status_code=$(printf %10000000s | tr " " "a" | curl $HOST --data-binary @- $curl_ret) 40 | test_msg=`check_block $status_code 1` 41 | test_passed=$((test_passed + $?)) 42 | test_count=$((test_count + 1)) 43 | echo -e "sent 10MB (too big) " "$req" "$status_code $test_msg" 44 | 45 | status_code=$(printf "x=%2000000s+select+from" | tr " " "a" | curl $HOST --data-binary @- $curl_ret) 46 | test_msg=`check_block $status_code 1` 47 | test_passed=$((test_passed + $?)) 48 | test_count=$((test_count + 1)) 49 | echo -e "x=<200*a>+select+from " "$req" "$status_code $test_msg" 50 | 51 | status_code=$(printf "%2000000s+select+from=x" | tr " " "a" | curl $HOST --data-binary @- $curl_ret) 52 | test_msg=`check_block $status_code 1` 53 | test_passed=$((test_passed + $?)) 54 | test_count=$((test_count + 1)) 55 | echo -e "<200*a>+select+from=x " "$req" "$status_code $test_msg" 56 | 57 | echo $test_passed/$test_count "tests passed" \($(((test_passed * 100) / test_count))%\) 58 | exit $(($test_passed != $test_count)) -------------------------------------------------------------------------------- /tests/internal_https.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./test.sh 4 | 5 | test_passed=0 6 | test_count=0 7 | 8 | curl_options="--cacert $ca_path https://$HOST" 9 | 10 | status_code=$(printf %1000000s | tr " " "a" | curl $curl_options --data-binary @- $curl_ret) 11 | test_msg=`check_block $status_code 0` 12 | test_passed=$((test_passed + $?)) 13 | test_count=$((test_count + 1)) 14 | echo -e "sent 1MB " "$req" "$status_code $test_msg" 15 | 16 | status_code=$(printf "%2000000s" | tr " " "a" | curl $curl_options --data-binary @- --limit-rate 350k $curl_ret) 17 | test_msg=`check_block $status_code 0` 18 | test_passed=$((test_passed + $?)) 19 | test_count=$((test_count + 1)) 20 | echo -e "sent 2MB @ 350kb/s " "$req" "$status_code $test_msg" 21 | 22 | status_code=$(printf %1000s | tr " " "a" | curl $curl_options --data-binary @- -H "Transfer-Encoding: chunked" $curl_ret) 23 | test_msg=`check_block $status_code 0` 24 | test_passed=$((test_passed + $?)) 25 | test_count=$((test_count + 1)) 26 | echo -e "sent 1kB with transfer-encoding: chunked " "$req" "$status_code $test_msg" 27 | 28 | status_code=$(curl $curl_options -X POST -H 'Content-Length:' $curl_ret) 29 | test_msg=`check_block $status_code 1` 30 | test_passed=$((test_passed + $?)) 31 | test_count=$((test_count + 1)) 32 | echo -e "sent POST request without content-length " "$req" "$status_code $test_msg" 33 | 34 | # Not working on Travis 35 | # status_code=$(printf "%2000000s" | tr " " "a" | curl $curl_options --data-binary @- --limit-rate 100k $curl_ret) 36 | # test_msg=`check_status_code $status_code 500` 37 | # test_passed=$((test_passed + $?)) 38 | # test_count=$((test_count + 1)) 39 | # echo -e "sent 2MB @ 100kb/s (timeout by mod_reqtimeout)" "$req" "$status_code $test_msg" 40 | 41 | status_code=$(printf %10000000s | tr " " "a" | curl $curl_options --data-binary @- $curl_ret) 42 | test_msg=`check_block $status_code 1` 43 | test_passed=$((test_passed + $?)) 44 | test_count=$((test_count + 1)) 45 | echo -e "sent 10MB (too big) " "$req" "$status_code $test_msg" 46 | 47 | status_code=$(printf "x=%2000000s+select+from" | tr " " "a" | curl $curl_options --data-binary @- $curl_ret) 48 | test_msg=`check_block $status_code 1` 49 | test_passed=$((test_passed + $?)) 50 | test_count=$((test_count + 1)) 51 | echo -e "x=<200*a>+select+from " "$req" "$status_code $test_msg" 52 | 53 | status_code=$(printf "%2000000s+select+from=x" | tr " " "a" | curl $curl_options --data-binary @- $curl_ret) 54 | test_msg=`check_block $status_code 1` 55 | test_passed=$((test_passed + $?)) 56 | test_count=$((test_count + 1)) 57 | echo -e "<200*a>+select+from=x " "$req" "$status_code $test_msg" 58 | 59 | echo $test_passed/$test_count "tests passed" \($(((test_passed * 100) / test_count))%\) 60 | exit $(($test_passed != $test_count)) -------------------------------------------------------------------------------- /tests/travis/https.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | sudo mkdir /etc/apache2/ssl 6 | cd /etc/apache2/ssl 7 | # Create the PKI used by Apache (https tests) 8 | sudo openssl genrsa -out ca.key 4096 9 | echo -e "FR\nNord\nLille\nVultureProject\nTravis tests\nAC_racine\nsupport@vultureproject.org\n\n" | sudo openssl req -sha256 -new -x509 -key ./ca.key -out ./ca.crt 10 | sudo openssl genrsa -out localhost.key 4096 11 | echo -e "FR\nNord\nLille\nVultureProject\nTravis tests\nlocalhost\nsupport@vultureproject.org\n\n" | sudo openssl req -sha256 -new -key ./localhost.key -out ./localhost.csr 12 | sudo openssl x509 -req -sha256 -days 1 -in ./localhost.csr -CA ./ca.crt -CAkey ./ca.key -CAcreateserial -out ./localhost.crt 13 | 14 | 15 | sudo mkdir /etc/defender/ 16 | sudo wget -O /etc/defender/core.rules https://raw.githubusercontent.com/nbs-system/naxsi/master/naxsi_config/naxsi_core.rules 17 | sudo sed -i "s/select|union|update|delete|insert|table|from|ascii|hex|unhex|drop/\\\b(select|union|update|delete|insert|table|from|ascii|hex|unhex|drop)\\\b/" /etc/defender/core.rules 18 | 19 | printf \ 20 | "LoadModule defender_module /usr/lib/apache2/modules/mod_defender.so 21 | 22 | Include /etc/defender/core.rules 23 | " | sudo tee /etc/apache2/mods-available/defender.load 24 | 25 | sudo apachectl -v 26 | sudo apachectl -M 27 | sudo a2enmod ssl 28 | sudo a2enmod defender 29 | sudo service apache2 stop 30 | 31 | printf \ 32 | " 33 | 34 | ServerName localhost 35 | LogLevel notice 36 | AllowEncodedSlashes On 37 | ErrorLog \${APACHE_LOG_DIR}/error.log 38 | SSLEngine on 39 | SSLCertificateFile /etc/apache2/ssl/localhost.crt 40 | SSLCertificateKeyFile /etc/apache2/ssl/localhost.key 41 | SSLCACertificateFile /etc/apache2/ssl/ca.crt 42 | 43 | 44 | Defender On 45 | MatchLog \${APACHE_LOG_DIR}/defender_match.log 46 | JSONMatchLog \${APACHE_LOG_DIR}/defender_json_match.log 47 | RequestBodyLimit 8388608 48 | LearningMode Off 49 | ExtensiveLog Off 50 | LibinjectionSQL Off 51 | LibinjectionXSS Off 52 | CheckRule \"\$SQL >= 8\" BLOCK 53 | CheckRule \"\$RFI >= 8\" BLOCK 54 | CheckRule \"\$TRAVERSAL >= 4\" BLOCK 55 | CheckRule \"\$EVADE >= 4\" BLOCK 56 | CheckRule \"\$XSS >= 8\" BLOCK 57 | CheckRule \"\$UPLOAD >= 8\" BLOCK 58 | 59 | 60 | 61 | " | sudo tee /etc/apache2/sites-available/ssl-default.conf 62 | 63 | sudo a2ensite ssl-default 64 | cd $DEFENDER_HOME 65 | cmake -H. -Bbuild 66 | cmake --build build 67 | sudo cp build/mod_defender.so /usr/lib/apache2/modules/ 68 | sudo service apache2 start 69 | cd tests/ 70 | bash core_https.sh localhost 71 | bash internal_https.sh localhost -------------------------------------------------------------------------------- /tests/core.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./test.sh 4 | 5 | declare -a tests=( 6 | # " -d a=blah" 0 7 | ) 8 | 9 | # BODY BODY_NAME URL ARGS ARGS_NAME $HEADERS_VAR:Cookie 10 | declare -a core_rules_tests=( 11 | # SQL Injections IDs:1000-1099 12 | "blah" 0 0 0 0 0 0 0 0 0 0 0 13 | "select+from" 1 1 1 1 1 1 1 1 1 1 1 14 | "selected+fromage" 0 0 0 0 0 0 0 0 0 0 0 15 | "\\\"" 1 1 1 1 1 1 1 1 1 1 1 16 | "0x0x0x0x" 1 1 1 1 1 1 1 1 1 1 1 17 | "/*" 1 1 1 1 1 1 1 1 1 1 1 18 | "*/" 1 1 1 1 1 1 1 1 1 1 1 19 | "|" 1 1 1 1 1 1 1 1 1 1 1 20 | "&&" 1 1 1 1 1 1 0 0 1 0 0 21 | "----" 1 1 1 1 1 1 1 1 1 1 1 22 | ";" 1 1 1 1 1 0 1 1 1 1 1 23 | "====" 1 1 0 1 1 0 1 1 0 1 1 24 | "(" 1 1 1 1 1 1 1 1 1 1 1 25 | ")" 1 1 1 1 1 1 1 1 1 1 1 26 | "'" 1 1 1 1 1 1 1 1 1 1 1 27 | ",," 1 1 1 1 1 1 1 1 1 1 1 28 | "##" 1 1 1 1 1 1 1 1 0 0 0 29 | "@@@@" 1 1 1 1 1 1 1 1 1 1 1 30 | 31 | # OBVIOUS RFI IDs:1100-1199 32 | "http://" 1 1 0 1 1 1 1 1 0 1 1 33 | "https://" 1 1 0 1 1 1 1 1 0 1 1 34 | "ftp://" 1 1 0 1 1 1 1 1 0 1 1 35 | "sftp://" 1 1 0 1 1 1 1 1 0 1 1 36 | "zlib://" 1 1 0 1 1 1 1 1 0 1 1 37 | "data://" 1 1 0 1 1 1 1 1 0 1 1 38 | "glob://" 1 1 0 1 1 1 1 1 0 1 1 39 | "phar://" 1 1 0 1 1 1 1 1 0 1 1 40 | "file://" 1 1 0 1 1 1 1 1 0 1 1 41 | "gopher://" 1 1 0 1 1 1 1 1 0 1 1 42 | 43 | # Directory traversal IDs:1200-1299 44 | "...." 1 1 1 1 1 1 1 1 1 1 1 45 | "/etc/passwd" 1 1 1 1 1 1 1 1 1 1 1 46 | "c:\\\\" 1 1 1 1 1 1 1 1 1 1 1 47 | "cmd.exe" 1 1 1 1 1 1 1 1 1 1 1 48 | "\\\\" 1 1 1 1 1 1 1 1 1 1 1 49 | 50 | # Cross Site Scripting IDs:1300-1399 51 | "<" 1 1 1 1 1 1 1 1 1 1 1 52 | ">" 1 1 1 1 1 1 1 1 1 1 1 53 | "[[" 1 1 1 1 1 1 1 1 1 1 1 54 | "]]" 1 1 1 1 1 1 1 1 1 1 1 55 | "~~" 1 1 1 1 1 1 1 1 1 1 1 56 | "\\\`" 1 1 1 1 1 1 1 1 1 1 1 57 | "%20" 1 1 1 1 1 1 0 0 0 0 0 58 | "%00" 1 1 1 1 1 1 1 1 0 1 1 59 | 60 | # Evading tricks IDs: 1400-1500 61 | "&#" 1 1 1 1 1 1 0 0 0 0 0 62 | "%U" 1 1 1 1 1 1 1 1 400 1 1 63 | ) 64 | 65 | 66 | test_count=0 67 | test_passed=0 68 | 69 | check_url() { 70 | # URL = $1 71 | # OPTIONS = $2 72 | # Expected action = $3 73 | req="curl \"$HOST/$1\" $2" 74 | expected_action="$3" 75 | status_code=$(echo "$req $curl_ret" | bash) 76 | # If expected code is not 0 or 1 -> it is an http code 77 | if ([ $expected_action -ne 0 ] && [ $expected_action -ne 1 ]) 78 | then 79 | test_msg=$(check_status_code $status_code $expected_action) 80 | else 81 | test_msg=$(check_block $status_code $expected_action) 82 | fi 83 | test_passed=$((test_passed + $?)) 84 | test_count=$((test_count + 1)) 85 | printf "%-60s %s\n" "$req" "$status_code $test_msg" 86 | } 87 | 88 | for ((i=0; i<${#core_rules_tests[@]}; i+=12)); do 89 | pattern=${core_rules_tests[$i]} 90 | # URL encoded 91 | check_url "" " --data-urlencode \"x=$pattern\"" ${core_rules_tests[$i+1]} 92 | no_escaped="$(echo "$pattern" | sed 's/\\\(.\{1\}\)/\1/g')" 93 | check_url "" " --data-raw \"$(url_encode "$no_escaped")=x\"" ${core_rules_tests[$i+2]} 94 | check_url "$(url_encode "$no_escaped")" "" ${core_rules_tests[$i+3]} 95 | check_url "?x=$(url_encode "$no_escaped")" "" ${core_rules_tests[$i+4]} 96 | check_url "?$(url_encode "$no_escaped")=x" "" ${core_rules_tests[$i+5]} 97 | check_url "" " -b \"x=$pattern\"" ${core_rules_tests[$i+6]} 98 | # Do NOT URL encode 99 | check_url "" " --data-raw \"x=$pattern\"" ${core_rules_tests[$i+7]} 100 | check_url "" " --data-raw \"$pattern=x\"" ${core_rules_tests[$i+8]} 101 | check_url "$pattern" " -g " ${core_rules_tests[$i+9]} 102 | check_url "?x=$pattern" " -g " ${core_rules_tests[$i+10]} 103 | check_url "?$pattern=x" " -g " ${core_rules_tests[$i+11]} 104 | done 105 | 106 | # Print results 107 | echo $test_passed/$test_count "tests passed" \($(((test_passed * 100) / test_count))%\) 108 | exit $(($test_passed != $test_count)) 109 | 110 | -------------------------------------------------------------------------------- /Util.h: -------------------------------------------------------------------------------- 1 | /* _ _ __ _ 2 | * _ __ ___ ___ __| | __| | ___ / _| ___ _ __ __| | ___ _ __ 3 | * | '_ ` _ \ / _ \ / _` | / _` |/ _ \ |_ / _ \ '_ \ / _` |/ _ \ '__| 4 | * | | | | | | (_) | (_| | | (_| | __/ _| __/ | | | (_| | __/ | 5 | * |_| |_| |_|\___/ \__,_|___\__,_|\___|_| \___|_| |_|\__,_|\___|_| 6 | * |_____| 7 | * Copyright (c) 2017 Annihil 8 | * Released under the GPLv3 9 | */ 10 | 11 | #ifndef MOD_DEFENDER_UTIL_H 12 | #define MOD_DEFENDER_UTIL_H 13 | 14 | #define UNESCAPE_URI 1 15 | #define UNESCAPE_REDIRECT 2 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | 31 | using std::vector; 32 | using std::string; 33 | using std::stringstream; 34 | using std::ostringstream; 35 | using std::endl; 36 | using std::istringstream; 37 | using std::pair; 38 | 39 | 40 | // Shell colors 41 | #define KNRM "\x1B[0m" 42 | #define KRED "\x1B[31m" 43 | #define KGRN "\x1B[32m" 44 | #define KYEL "\x1B[33m" 45 | #define KBLU "\x1B[34m" 46 | #define KMAG "\x1B[35m" 47 | #define KCYN "\x1B[36m" 48 | #define KWHT "\x1B[37m" 49 | 50 | typedef struct { 51 | size_t len = 0; 52 | u_char *data; 53 | } str_t; 54 | 55 | namespace Util { 56 | inline string <rim(string &s) { // trim from start 57 | s.erase(s.begin(), std::find_if(s.begin(), s.end(), std::not1(std::ptr_fun(std::isspace)))); 58 | return s; 59 | } 60 | 61 | inline string &rtrim(string &s) { // trim from end 62 | s.erase(std::find_if(s.rbegin(), s.rend(), std::not1(std::ptr_fun(std::isspace))).base(), s.end()); 63 | return s; 64 | } 65 | 66 | inline string &trim(string &s) { // trim from both ends 67 | return ltrim(rtrim(s)); 68 | } 69 | 70 | inline unsigned long countSubstring(const string &str, const string &sub) { 71 | if (sub.length() == 0) return 0; 72 | unsigned long count = 0; 73 | for (size_t offset = str.find(sub); offset != std::string::npos; 74 | offset = str.find(sub, offset + sub.length())) { 75 | ++count; 76 | } 77 | return count; 78 | } 79 | 80 | inline unsigned long countSubstring(const char *str, size_t len, const char *pattern, size_t patternLen) { 81 | char *p; 82 | unsigned long count = 0; 83 | unsigned long idx = 0; 84 | while ((p = (char *) memmem(str + idx, len - idx, pattern, patternLen)) != NULL) { 85 | count++; 86 | idx = (p - str) + patternLen; 87 | } 88 | return count; 89 | } 90 | 91 | inline unsigned long countSubstring(const char *str, const char *pattern, size_t patternLen) { 92 | unsigned long count = 0; 93 | char *p = (char *) str; 94 | while ((p = strstr(p, pattern)) != NULL) { 95 | count++; 96 | p += patternLen; 97 | } 98 | return count; 99 | } 100 | 101 | inline bool caseEqual(const string &str1, const string &str2) { 102 | if (str1.size() != str2.size()) { 103 | return false; 104 | } 105 | for (string::const_iterator c1 = str1.begin(), c2 = str2.begin(); c1 != str1.end(); ++c1, ++c2) { 106 | if (tolower(*c1) != tolower(*c2)) { 107 | return false; 108 | } 109 | } 110 | return true; 111 | } 112 | 113 | int naxsi_unescape_uri(u_char **dst, u_char **src, size_t size, unsigned int type); 114 | 115 | /* unescape routine, returns number of nullbytes present */ 116 | inline int naxsi_unescape(str_t *str) { 117 | u_char *dst, *src; 118 | u_int nullbytes = 0, bad = 0, i; 119 | 120 | dst = str->data; 121 | src = str->data; 122 | 123 | bad = (u_int) naxsi_unescape_uri(&src, &dst, str->len, 0); 124 | str->len = src - str->data; 125 | //tmp hack fix, avoid %00 & co (null byte) encoding :p 126 | for (i = 0; i < str->len; i++) 127 | if (str->data[i] == 0x0) { 128 | nullbytes++; 129 | str->data[i] = '0'; 130 | } 131 | return (nullbytes + bad); 132 | } 133 | 134 | inline char *strnchr(const char *s, int c, unsigned long len) { 135 | unsigned long cpt = 0; 136 | for (cpt = 0; cpt < len && s[cpt]; cpt++) 137 | if (s[cpt] == c) 138 | return ((char *) s + cpt); 139 | return (NULL); 140 | } 141 | 142 | vector split(const string &s, char delim); 143 | pair splitAtFirst(const string &s, string delim); 144 | std::vector 145 | parseRawDirective(std::string raw_directive); 146 | vector splitToInt(string &s, char delimiter); 147 | string apacheTimeFmt(); 148 | string naxsiTimeFmt(); 149 | string formatLog(int loglevel, const string &clientIp); 150 | string escapeQuotes(const string &before); 151 | string unescape(const string &s); 152 | } 153 | 154 | #endif //MOD_DEFENDER_UTIL_H 155 | -------------------------------------------------------------------------------- /mod_defender.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * \file mod_defender.hpp 3 | * \author Kevin Guillemot 4 | * \version 1.0 5 | * \date 30/03/2018 6 | * \license GPLv3 7 | * \brief Header file of the mod_defender module 8 | */ 9 | 10 | #ifndef MOD_DEFENDER_HPP 11 | #define MOD_DEFENDER_HPP 12 | 13 | 14 | /*************************/ 15 | /* Inclusion of .H files */ 16 | /*************************/ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include "RuleParser.h" 26 | #include "RuntimeScanner.hpp" 27 | 28 | 29 | /*************/ 30 | /* Constants */ 31 | /*************/ 32 | 33 | /*---------------------------*/ 34 | /* MODULE-part needed macros */ 35 | /*---------------------------*/ 36 | 37 | /** 38 | * Extra Apache 2.4+ C++ module declaration. 39 | * Needed cause of C++ use. 40 | */ 41 | #ifdef APLOG_USE_MODULE 42 | APLOG_USE_MODULE(defender); 43 | #endif 44 | 45 | extern module AP_MODULE_DECLARE_DATA defender_module; 46 | 47 | /** 48 | * \def MAX_BB_SIZE 49 | * The maximum length of post body processed 50 | */ 51 | #define MAX_BB_SIZE 0x7FFFFFFF 52 | 53 | /** 54 | * \def CHUNK_CAPACITY 55 | * The maximum length of a chunk 56 | */ 57 | #define CHUNK_CAPACITY 8192 58 | 59 | /** 60 | * \def IF_STATUS_NONE 61 | * The status of the body to be processed 62 | */ 63 | #define IF_STATUS_NONE 0 64 | 65 | /** 66 | * \def IF_STATUS_WANTS_TO_RUN 67 | * The status of the body to be processed 68 | */ 69 | #define IF_STATUS_WANTS_TO_RUN 1 70 | 71 | /** 72 | * \def IF_STATUS_COMPLETE 73 | * The status of the body to be processed 74 | */ 75 | #define IF_STATUS_COMPLETE 2 76 | 77 | /** 78 | * \def SLASHES 79 | * The slash as string, used to urlencode/decode 80 | */ 81 | #define SLASHES "/" 82 | 83 | 84 | /**************/ 85 | /* Structures */ 86 | /**************/ 87 | 88 | /** 89 | * \struct dir_config_t mod_defender.h 90 | * Regroup all server directives in a structure 91 | */ 92 | typedef struct { 93 | RuleParser *parser; 94 | vector> tmpCheckRules; 95 | vector tmpBasicRules; 96 | char *loc_path; 97 | apr_file_t *matchlog_file; 98 | apr_file_t *jsonmatchlog_file; 99 | unsigned long requestBodyLimit; 100 | bool libinjection_sql; 101 | bool libinjection_xss; 102 | bool defender; 103 | bool learning; 104 | bool extensive; 105 | bool useenv; 106 | } dir_config_t; 107 | 108 | /** 109 | * \struct chunk_t mod_defender.h 110 | * Chunk structure used to save/restore brigades 111 | */ 112 | typedef struct { 113 | char *data; 114 | apr_size_t length; 115 | unsigned int is_permanent; 116 | } chunk_t; 117 | 118 | /** 119 | * \struct defender_t mod_defender.h 120 | * Defender structure used to save/restore brigades 121 | */ 122 | typedef struct { 123 | int fixups_done; 124 | int body_error; 125 | const char *body_error_msg; 126 | unsigned int status; 127 | unsigned int started_forwarding; 128 | unsigned int stream_changed; 129 | apr_size_t stream_input_length; 130 | char *stream_input_data; 131 | unsigned int if_seen_eos; 132 | int body_chunk_position; 133 | unsigned int body_chunk_offset; 134 | apr_pool_t *body_pool; 135 | apr_array_header_t *body_chunks; 136 | chunk_t *body_chunk; 137 | apr_size_t body_length; 138 | chunk_t *body_chunk_current; 139 | char *body_buffer; 140 | unsigned int body_should_exist; 141 | unsigned int body_read; 142 | } defender_t; 143 | 144 | /** 145 | * \struct defender_config_t mod_defender.h 146 | * Custom definition to hold any configuration data we may need. 147 | */ 148 | typedef struct { 149 | RuntimeScanner *vpRuntimeScanner; 150 | defender_t *def; 151 | } defender_config_t; 152 | 153 | 154 | /************************/ 155 | /* Functions signatures */ 156 | /************************/ 157 | 158 | /** 159 | * \brief Initialize all variables used to forward request body. 160 | * \param def Defender structure. 161 | * \param char** Error message pointer. 162 | * \param r Apache request structure to work on. 163 | * \return apr_status_t Return status code of function. 164 | */ 165 | apr_status_t body_retrieve_start(defender_t *def, char **error_msg, request_rec *r); 166 | 167 | /** 168 | * \brief Retrieve stocked chunk of request body and return it. 169 | * \param def Defender structure. 170 | * \param chunk_t** List of chunks to add the chunk onto. 171 | * \param nbytes Chunk max bytes length. 172 | * \param char** Error message pointer. 173 | * \param r Apache request structure to work on. 174 | * \return apr_status_t Return status code of function. 175 | */ 176 | apr_status_t body_retrieve(defender_t *def, chunk_t **chunk, long int nbytes, char **error_msg, request_rec *r); 177 | 178 | /** 179 | * \brief Initialize all variables used to forward request body. 180 | * \param def Defender structure. 181 | * \param char** Error message pointer. 182 | * \param r Apache request structure to work on. 183 | * \param body_limit Value of requestBodyLimit directive, to not exceed. 184 | * \return apr_status_t Return status code of function. 185 | */ 186 | apr_status_t read_request_body(defender_t *def, char **error_msg, request_rec *r, unsigned long body_limit); 187 | 188 | /** 189 | * \brief Initialize all variables used to forward request body. 190 | * \param data Defender structure, as void*, called by apache hook. 191 | * \return apr_status_t Return status code of function. 192 | */ 193 | apr_status_t body_clear(void *data); 194 | 195 | 196 | #endif //MOD_DEFENDER_HPP 197 | -------------------------------------------------------------------------------- /RuntimeScanner.hpp: -------------------------------------------------------------------------------- 1 | /* _ _ __ _ 2 | * _ __ ___ ___ __| | __| | ___ / _| ___ _ __ __| | ___ _ __ 3 | * | '_ ` _ \ / _ \ / _` | / _` |/ _ \ |_ / _ \ '_ \ / _` |/ _ \ '__| 4 | * | | | | | | (_) | (_| | | (_| | __/ _| __/ | | | (_| | __/ | 5 | * |_| |_| |_|\___/ \__,_|___\__,_|\___|_| \___|_| |_|\__,_|\___|_| 6 | * |_____| 7 | * Copyright (c) 2017 Annihil 8 | * Released under the GPLv3 9 | */ 10 | 11 | #ifndef RUNTIMESCANNER_HPP 12 | #define RUNTIMESCANNER_HPP 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include "RuleParser.h" 25 | #include "JsonValidator.hpp" 26 | 27 | //#define DEBUG_RUNTIME_PROCESSRULE 28 | #ifdef DEBUG_RUNTIME_PROCESSRULE 29 | #define DEBUG_RUNTIME_PR(x) do { std::cerr << x; } while (0) 30 | #else 31 | #define DEBUG_RUNTIME_PR(x) 32 | #endif 33 | 34 | //#define DEBUG_RUNTIME_BASESTR_RULE_SET 35 | #ifdef DEBUG_RUNTIME_BASESTR_RULE_SET 36 | #define DEBUG_RUNTIME_BRS(x) do { std::cerr << x; } while (0) 37 | #else 38 | #define DEBUG_RUNTIME_BRS(x) 39 | #endif 40 | 41 | #define PASS -1 42 | #define STOP 403 43 | /* used for reading input blocks */ 44 | #define READ_BLOCKSIZE 2048 45 | 46 | using namespace Util; 47 | using std::pair; 48 | using std::make_pair; 49 | using std::vector; 50 | using std::set; 51 | using std::string; 52 | using std::cerr; 53 | using std::stringstream; 54 | using std::endl; 55 | using std::regex; 56 | using std::sregex_iterator; 57 | using std::regex_match; 58 | using std::distance; 59 | using std::unordered_map; 60 | using std::transform; 61 | using std::function; 62 | 63 | const std::string empty = string(); 64 | 65 | enum METHOD { 66 | METHOD_GET = 0, 67 | METHOD_POST, 68 | METHOD_PUT, 69 | UNSUPPORTED_METHOD, 70 | }; 71 | 72 | enum CONTENT_TYPE { 73 | CONTENT_TYPE_UNSUPPORTED = 0, 74 | CONTENT_TYPE_URL_ENC, // application/x-www-form-urlencoded 75 | CONTENT_TYPE_MULTIPART, // multipart/form-data 76 | CONTENT_TYPE_APP_JSON, // application/json 77 | }; 78 | 79 | enum TRANSFER_ENCODING { 80 | TRANSFER_ENCODING_UNSUPPORTED = 0, 81 | TRANSFER_ENCODING_CHUNKED 82 | }; 83 | 84 | enum LOG_LVL { 85 | LOG_LVL_EMERG = 0, 86 | LOG_LVL_ALERT, 87 | LOG_LVL_CRIT, 88 | LOG_LVL_ERR, 89 | LOG_LVL_WARNING, 90 | LOG_LVL_NOTICE, 91 | LOG_LVL_INFO, 92 | LOG_LVL_DEBUG 93 | }; 94 | 95 | typedef struct { 96 | string zone; 97 | set ruleId; 98 | string varname; 99 | string content; 100 | } match_info_t; 101 | 102 | class RuntimeScanner { 103 | friend class JsonValidator; 104 | private: 105 | RuleParser& parser; 106 | stringstream matchVars; 107 | unsigned int rulesMatchedCount = 0; 108 | string uri; 109 | vector> headers; 110 | vector> get; 111 | string rawContentType; 112 | 113 | public: 114 | METHOD method = UNSUPPORTED_METHOD; 115 | CONTENT_TYPE contentType = CONTENT_TYPE_UNSUPPORTED; 116 | TRANSFER_ENCODING transferEncoding = TRANSFER_ENCODING_UNSUPPORTED; 117 | bool transferEncodingProvided = false; 118 | unsigned long contentLength = 0; 119 | bool contentLengthProvided = false; 120 | string body; 121 | unsigned long bodyLimit = 0; 122 | bool bodyLimitExceeded = false; 123 | 124 | int pid = 0; 125 | long connectionId = 0; 126 | string threadId; 127 | string clientIp; 128 | string requestedHost; 129 | string serverHostname; 130 | string fullUri; 131 | string protocol; 132 | string softwareVersion; 133 | 134 | LOG_LVL logLevel = LOG_LVL_EMERG; 135 | void *errorLogFile; 136 | void *learningLogFile; 137 | void *learningJSONLogFile; 138 | 139 | bool learning; 140 | bool extensiveLearning; 141 | bool libinjSQL; 142 | bool libinjXSS; 143 | 144 | unordered_map matchScores; 145 | unordered_map matchInfos; 146 | 147 | bool block = false; 148 | bool drop = false; 149 | bool allow = false; 150 | bool log = false; 151 | 152 | function writeLogFn; 153 | 154 | RuntimeScanner(RuleParser &parser) : parser(parser) {} 155 | void setUri(char *uri); 156 | void addHeader(char* key, char* val); 157 | void addGETParameter(char* key, char* val); 158 | void streamToFile(const stringstream &ss, void *file); 159 | int processHeaders(); 160 | int processBody(); 161 | void logg(int priority, void *file, const char *fmt, ...); 162 | void applyRuleAction(const rule_action_t &rule_action); 163 | void checkLibInjection(MATCH_ZONE zone, const string &name, const string &value); 164 | void basestrRuleset(MATCH_ZONE zone, const string &name, const string &value, 165 | const vector &rules); 166 | bool processRuleBuffer(const string &str, const http_rule_t &rl, unsigned long &nbMatch); 167 | void applyCheckRule(const http_rule_t &rule, unsigned long nbMatch, const string &name, const string &value, 168 | MATCH_ZONE zone, bool targetName); 169 | void applyRuleMatch(const http_rule_t &rule, unsigned long nbMatch, MATCH_ZONE zone, const string &name, 170 | const string &value, bool targetName); 171 | void writeLearningLog(); 172 | void writeExtensiveLog(const http_rule_t &rule, MATCH_ZONE zone, const string &name, 173 | const string &value, bool targetName); 174 | void writeJSONLearningLog(); 175 | bool parseFormDataBoundary(unsigned char **boundary, unsigned long *boundary_len); 176 | void multipartParse(u_char *src, unsigned long len); 177 | bool contentDispositionParser(unsigned char *str, unsigned char *line_end, 178 | unsigned char **fvarn_start, unsigned char **fvarn_end, 179 | unsigned char **ffilen_start, unsigned char **ffilen_end); 180 | int processAction(); 181 | bool splitUrlEncodedRuleset(char *str, const vector &rules, MATCH_ZONE zone); 182 | }; 183 | 184 | #endif /* RUNTIMESCANNER_HPP */ 185 | 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![ModDefender logo](https://i.imgur.com/EIHE0dS.png) 2 | [![travis-ci](https://travis-ci.org/Annihil/mod_defender.svg?branch=master)](https://travis-ci.org/Annihil/mod_defender) 3 | Mod Defender is an Apache2 module aiming to block attacks thanks to a whitelist policy 4 | It is an almost complete replication of [NAXSI](https://github.com/nbs-system/naxsi), which is for Nginx 5 | It uses the same configs format and is thus fully compatible with [NXAPI/NXTOOL](https://github.com/nbs-system/naxsi/tree/master/nxapi) 6 | 7 | - Input 8 | - [MainRule](https://github.com/nbs-system/naxsi/blob/master/naxsi_config/naxsi_core.rules) 9 | - [BasicRule](https://github.com/nbs-system/naxsi/wiki/whitelists-bnf) 10 | - [CheckRule](https://github.com/nbs-system/naxsi/wiki/checkrules-bnf) 11 | - Output 12 | - [Learning log](https://github.com/nbs-system/naxsi/wiki/naxsilogs#naxsi_fmt) 13 | - [Extensive learning log](https://github.com/nbs-system/naxsi/wiki/naxsilogs#naxsi_exlog) 14 | 15 | ## Advantages 16 | - Human readable log: colored output to watch Mainrules and Basicrules processing 17 | - JSON match log: easier parsing and more compact logs 18 | - Combined log: regular and extensive match log are mixed so that content and name of variable in question are presents on the same line 19 | 20 | ## Required packages 21 | * apache2 dev package to provide Apache2 headers 22 | * apr package to provide Apache Portal Runtime library and headers 23 | * gcc & g++ >= 4.9 (for std::regex) 24 | * GNU make 25 | * cmake >= 3.2 26 | 27 | ## Installation 28 | ### Debian 29 | 1. Install required packages 30 | ```sh 31 | sudo apt-get install apache2-dev make gcc g++ cmake 32 | ``` 33 | 34 | 1. Compile the source 35 | ```sh 36 | cmake -H. -Bbuild 37 | cmake --build build -- -j4 38 | ``` 39 | 40 | 1. Install the module 41 | ```sh 42 | sudo cp build/mod_defender.so /usr/lib/apache2/modules/ 43 | ``` 44 | 45 | 1. Create its module load file 46 | ```sh 47 | cat << EOF | sudo tee /etc/apache2/mods-available/defender.load > /dev/null 48 | LoadModule defender_module /usr/lib/apache2/modules/mod_defender.so 49 | 50 | Include /etc/defender/core.rules 51 | 52 | EOF 53 | ``` 54 | 55 | 1. Add mod_defender settings in the desired location / directory / proxy blocks 56 | ``` 57 | 58 | ServerName ... 59 | DocumentRoot ... 60 | 61 | 62 | 63 | # Defender toggle 64 | Defender On 65 | # Match log path 66 | MatchLog ${APACHE_LOG_DIR}/defender_match.log 67 | # JSON Match log path 68 | JSONMatchLog ${APACHE_LOG_DIR}/defender_json_match.log 69 | # Request body limit 70 | RequestBodyLimit 8388608 71 | # Learning mode toggle 72 | LearningMode On 73 | # Extensive Learning log toggle 74 | ExtensiveLog Off 75 | # Libinjection SQL toggle 76 | LibinjectionSQL Off 77 | # Libinjection XSS toggle 78 | LibinjectionXSS Off 79 | ## Score action 80 | CheckRule "$SQL >= 8" BLOCK 81 | CheckRule "$RFI >= 8" BLOCK 82 | CheckRule "$TRAVERSAL >= 4" BLOCK 83 | CheckRule "$EVADE >= 4" BLOCK 84 | CheckRule "$XSS >= 8" BLOCK 85 | CheckRule "$UPLOAD >= 8" BLOCK 86 | 87 | # Whitelists (BasicRule) 88 | Include /etc/defender/my_whitelist.rules 89 | 90 | 91 | 92 | ``` 93 | 94 | 1. Create Mod Defender conf directory 95 | ```sh 96 | sudo mkdir /etc/defender/ 97 | ``` 98 | 99 | 1. Populate it with the core rules 100 | ```sh 101 | sudo wget -O /etc/defender/core.rules \ 102 | https://raw.githubusercontent.com/nbs-system/naxsi/master/naxsi_config/naxsi_core.rules 103 | ``` 104 | 105 | 1. Enable the module 106 | ```sh 107 | sudo a2enmod defender 108 | ``` 109 | 110 | 1. Restart Apache2 to take effect 111 | ```sh 112 | sudo service apache2 restart 113 | ``` 114 | 115 | ### FreeBSD 116 | 1. Install required packages 117 | ```sh 118 | pkg install apr make gcc cmake 119 | ``` 120 | 121 | 1. Compile the source 122 | ```sh 123 | cmake -H. -Bbuild 124 | cmake --build build -- -j4 125 | ``` 126 | 127 | 1. Install the module 128 | ```sh 129 | cp build/mod_defender.so /usr/local/libexec/apache24/ 130 | ``` 131 | 132 | 1. Create its module load file 133 | ```sh 134 | cat << EOF | tee /usr/local/etc/apache24/modules.d/250_defender.conf > /dev/null 135 | LoadModule defender_module libexec/apache24/mod_defender.so 136 | 137 | Include etc/defender/core.rules 138 | 139 | EOF 140 | ``` 141 | 142 | 1. Add mod_defender settings in the desired location / directory / proxy blocks 143 | ``` 144 | 145 | ServerName ... 146 | DocumentRoot ... 147 | 148 | 149 | 150 | # Defender toggle 151 | Defender On 152 | # Match log path 153 | MatchLog /var/log/defender_match.log 154 | # JSON Match log path 155 | JSONMatchLog /var/log/defender_json_match.log 156 | # Request body limit 157 | RequestBodyLimit 8388608 158 | # Learning mode toggle 159 | LearningMode On 160 | # Extensive Learning log toggle 161 | ExtensiveLog Off 162 | # Libinjection SQL toggle 163 | LibinjectionSQL Off 164 | # Libinjection XSS toggle 165 | LibinjectionXSS Off 166 | ## Score action 167 | CheckRule "$SQL >= 8" BLOCK 168 | CheckRule "$RFI >= 8" BLOCK 169 | CheckRule "$TRAVERSAL >= 4" BLOCK 170 | CheckRule "$EVADE >= 4" BLOCK 171 | CheckRule "$XSS >= 8" BLOCK 172 | CheckRule "$UPLOAD >= 8" BLOCK 173 | 174 | # Whitelists (BasicRule) 175 | Include etc/defender/my_whitelist.rules 176 | 177 | 178 | 179 | ``` 180 | 181 | 1. Create Mod Defender conf directory 182 | ```sh 183 | mkdir /usr/local/etc/defender/ 184 | ``` 185 | 186 | 1. Populate it with the core rules 187 | ```sh 188 | wget -O /usr/local/etc/defender/core.rules \ 189 | https://raw.githubusercontent.com/nbs-system/naxsi/master/naxsi_config/naxsi_core.rules 190 | ``` 191 | 192 | 1. Restart Apache2 to take effect 193 | ```sh 194 | service apache24 restart 195 | ``` 196 | 197 | ## Configuration hierarchy 198 | ### Top (above <VirtualHost>) 199 | ``` 200 | # Score rules 201 | Include /etc/defender/core.rules 202 | MainRule "..." 203 | ``` 204 | 205 | ### <Location> / <Directory> / <Proxy> blocks 206 | ``` 207 | # Action rules 208 | CheckRule "..." 209 | 210 | # Whitelist rules 211 | BasicRule "..." 212 | ``` 213 | 214 | ## Credits 215 | [NAXSI's team](https://github.com/orgs/nbs-system/people) from nbs-system 216 | -------------------------------------------------------------------------------- /deps/libinjection/libinjection_sqli.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012-2016 Nick Galbreath 3 | * nickg@client9.com 4 | * BSD License -- see `COPYING.txt` for details 5 | * 6 | * https://libinjection.client9.com/ 7 | * 8 | */ 9 | 10 | #ifndef LIBINJECTION_SQLI_H 11 | #define LIBINJECTION_SQLI_H 12 | 13 | #ifdef __cplusplus 14 | extern "C" { 15 | #endif 16 | 17 | /* 18 | * Pull in size_t 19 | */ 20 | #include 21 | 22 | enum sqli_flags { 23 | FLAG_NONE = 0 24 | , FLAG_QUOTE_NONE = 1 /* 1 << 0 */ 25 | , FLAG_QUOTE_SINGLE = 2 /* 1 << 1 */ 26 | , FLAG_QUOTE_DOUBLE = 4 /* 1 << 2 */ 27 | 28 | , FLAG_SQL_ANSI = 8 /* 1 << 3 */ 29 | , FLAG_SQL_MYSQL = 16 /* 1 << 4 */ 30 | }; 31 | 32 | enum lookup_type { 33 | LOOKUP_WORD = 1 34 | , LOOKUP_TYPE = 2 35 | , LOOKUP_OPERATOR = 3 36 | , LOOKUP_FINGERPRINT = 4 37 | }; 38 | 39 | struct libinjection_sqli_token { 40 | #ifdef SWIG 41 | %immutable; 42 | #endif 43 | char type; 44 | char str_open; 45 | char str_close; 46 | 47 | /* 48 | * position and length of token 49 | * in original string 50 | */ 51 | size_t pos; 52 | size_t len; 53 | 54 | /* count: 55 | * in type 'v', used for number of opening '@' 56 | * but maybe used in other contexts 57 | */ 58 | int count; 59 | 60 | char val[32]; 61 | }; 62 | 63 | typedef struct libinjection_sqli_token stoken_t; 64 | 65 | /** 66 | * Pointer to function, takes c-string input, 67 | * returns '\0' for no match, else a char 68 | */ 69 | struct libinjection_sqli_state; 70 | typedef char (*ptr_lookup_fn)(struct libinjection_sqli_state*, int lookuptype, const char* word, size_t len); 71 | 72 | struct libinjection_sqli_state { 73 | #ifdef SWIG 74 | %immutable; 75 | #endif 76 | 77 | /* 78 | * input, does not need to be null terminated. 79 | * it is also not modified. 80 | */ 81 | const char *s; 82 | 83 | /* 84 | * input length 85 | */ 86 | size_t slen; 87 | 88 | /* 89 | * How to lookup a word or fingerprint 90 | */ 91 | ptr_lookup_fn lookup; 92 | void* userdata; 93 | 94 | /* 95 | * 96 | */ 97 | int flags; 98 | 99 | /* 100 | * pos is the index in the string during tokenization 101 | */ 102 | size_t pos; 103 | 104 | #ifndef SWIG 105 | /* for SWIG.. don't use this.. use functional API instead */ 106 | 107 | /* MAX TOKENS + 1 since we use one extra token 108 | * to determine the type of the previous token 109 | */ 110 | struct libinjection_sqli_token tokenvec[8]; 111 | #endif 112 | 113 | /* 114 | * Pointer to token position in tokenvec, above 115 | */ 116 | struct libinjection_sqli_token *current; 117 | 118 | /* 119 | * fingerprint pattern c-string 120 | * +1 for ending null 121 | * Minimum of 8 bytes to add gcc's -fstack-protector to work 122 | */ 123 | char fingerprint[8]; 124 | 125 | /* 126 | * Line number of code that said decided if the input was SQLi or 127 | * not. Most of the time it's line that said "it's not a matching 128 | * fingerprint" but there is other logic that sometimes approves 129 | * an input. This is only useful for debugging. 130 | * 131 | */ 132 | int reason; 133 | 134 | /* Number of ddw (dash-dash-white) comments 135 | * These comments are in the form of 136 | * '--[whitespace]' or '--[EOF]' 137 | * 138 | * All databases treat this as a comment. 139 | */ 140 | int stats_comment_ddw; 141 | 142 | /* Number of ddx (dash-dash-[notwhite]) comments 143 | * 144 | * ANSI SQL treats these are comments, MySQL treats this as 145 | * two unary operators '-' '-' 146 | * 147 | * If you are parsing result returns FALSE and 148 | * stats_comment_dd > 0, you should reparse with 149 | * COMMENT_MYSQL 150 | * 151 | */ 152 | int stats_comment_ddx; 153 | 154 | /* 155 | * c-style comments found /x .. x/ 156 | */ 157 | int stats_comment_c; 158 | 159 | /* '#' operators or MySQL EOL comments found 160 | * 161 | */ 162 | int stats_comment_hash; 163 | 164 | /* 165 | * number of tokens folded away 166 | */ 167 | int stats_folds; 168 | 169 | /* 170 | * total tokens processed 171 | */ 172 | int stats_tokens; 173 | 174 | }; 175 | 176 | typedef struct libinjection_sqli_state sfilter; 177 | 178 | struct libinjection_sqli_token* libinjection_sqli_get_token( 179 | struct libinjection_sqli_state* sqlistate, int i); 180 | 181 | /* 182 | * Version info. 183 | * 184 | * This is moved into a function to allow SWIG and other auto-generated 185 | * binding to not be modified during minor release changes. We change 186 | * change the version number in the c source file, and not regenerated 187 | * the binding 188 | * 189 | * See python's normalized version 190 | * http://www.python.org/dev/peps/pep-0386/#normalizedversion 191 | */ 192 | const char* libinjection_version(void); 193 | 194 | /** 195 | * 196 | */ 197 | void libinjection_sqli_init(struct libinjection_sqli_state* sql_state, 198 | const char* s, size_t slen, 199 | int flags); 200 | 201 | /** 202 | * Main API: tests for SQLi in three possible contexts, no quotes, 203 | * single quote and double quote 204 | * 205 | * \param sql_state core data structure 206 | * 207 | * \return 1 (true) if SQLi, 0 (false) if benign 208 | */ 209 | int libinjection_is_sqli(struct libinjection_sqli_state* sql_state); 210 | 211 | /* FOR HACKERS ONLY 212 | * provides deep hooks into the decision making process 213 | */ 214 | void libinjection_sqli_callback(struct libinjection_sqli_state* sql_state, 215 | ptr_lookup_fn fn, 216 | void* userdata); 217 | 218 | 219 | /* 220 | * Resets state, but keeps initial string and callbacks 221 | */ 222 | void libinjection_sqli_reset(struct libinjection_sqli_state* sql_state, 223 | int flags); 224 | 225 | /** 226 | * 227 | */ 228 | 229 | /** 230 | * This detects SQLi in a single context, mostly useful for custom 231 | * logic and debugging. 232 | * 233 | * \param sql_state Main data structure 234 | * \param flags flags to adjust parsing 235 | * 236 | * \returns a pointer to sfilter.fingerprint as convenience 237 | * do not free! 238 | * 239 | */ 240 | const char* libinjection_sqli_fingerprint(struct libinjection_sqli_state* sql_state, 241 | int flags); 242 | 243 | /** 244 | * The default "word" to token-type or fingerprint function. This 245 | * uses a ASCII case-insensitive binary tree. 246 | */ 247 | char libinjection_sqli_lookup_word(struct libinjection_sqli_state* sql_state, 248 | int lookup_type, 249 | const char* s, 250 | size_t slen); 251 | 252 | /* Streaming tokenization interface. 253 | * 254 | * sql_state->current is updated with the current token. 255 | * 256 | * \returns 1, has a token, keep going, or 0 no tokens 257 | * 258 | */ 259 | int libinjection_sqli_tokenize(struct libinjection_sqli_state * sql_state); 260 | 261 | /** 262 | * parses and folds input, up to 5 tokens 263 | * 264 | */ 265 | int libinjection_sqli_fold(struct libinjection_sqli_state * sql_state); 266 | 267 | /** The built-in default function to match fingerprints 268 | * and do false negative/positive analysis. This calls the following 269 | * two functions. With this, you over-ride one part or the other. 270 | * 271 | * return libinjection_sqli_blacklist(sql_state) && 272 | * libinjection_sqli_not_whitelist(sql_state); 273 | * 274 | * \param sql_state should be filled out after libinjection_sqli_fingerprint is called 275 | */ 276 | int libinjection_sqli_check_fingerprint(struct libinjection_sqli_state * sql_state); 277 | 278 | /* Given a pattern determine if it's a SQLi pattern. 279 | * 280 | * \return TRUE if sqli, false otherwise 281 | */ 282 | int libinjection_sqli_blacklist(struct libinjection_sqli_state* sql_state); 283 | 284 | /* Given a positive match for a pattern (i.e. pattern is SQLi), this function 285 | * does additional analysis to reduce false positives. 286 | * 287 | * \return TRUE if SQLi, false otherwise 288 | */ 289 | int libinjection_sqli_not_whitelist(struct libinjection_sqli_state * sql_state); 290 | 291 | #ifdef __cplusplus 292 | } 293 | #endif 294 | 295 | #endif /* LIBINJECTION_SQLI_H */ 296 | -------------------------------------------------------------------------------- /JsonValidator.cpp: -------------------------------------------------------------------------------- 1 | /* _ _ __ _ 2 | * _ __ ___ ___ __| | __| | ___ / _| ___ _ __ __| | ___ _ __ 3 | * | '_ ` _ \ / _ \ / _` | / _` |/ _ \ |_ / _ \ '_ \ / _` |/ _ \ '__| 4 | * | | | | | | (_) | (_| | | (_| | __/ _| __/ | | | (_| | __/ | 5 | * |_| |_| |_|\___/ \__,_|___\__,_|\___|_| \___|_| |_|\__,_|\___|_| 6 | * |_____| 7 | * Copyright (c) 2017 Annihil 8 | * Released under the GPLv3 9 | */ 10 | 11 | #include "JsonValidator.hpp" 12 | #include "RuntimeScanner.hpp" 13 | 14 | bool JsonValidator::jsonForward(json_t &js) { 15 | while ((*(js.src + js.off) == ' ' || 16 | *(js.src + js.off) == '\t' || 17 | *(js.src + js.off) == '\n' || 18 | *(js.src + js.off) == '\r') && js.off < js.len) { 19 | js.off++; 20 | } 21 | js.c = *(js.src + js.off); 22 | return true; 23 | } 24 | 25 | /* 26 | ** used to fast forward in json POSTS, 27 | ** we skip whitespaces/tab/CR/LF 28 | */ 29 | bool JsonValidator::jsonSeek(json_t &js, unsigned char seek) { 30 | jsonForward(js); 31 | return js.c == seek; 32 | } 33 | 34 | /* 35 | ** extract a quoted strings, 36 | ** JSON spec only supports double-quoted strings, 37 | ** so do we. 38 | */ 39 | bool JsonValidator::jsonQuoted(json_t &js, str_t *ve) { 40 | u_char *vn_start, *vn_end = NULL; 41 | 42 | if (*(js.src + js.off) != '"') 43 | return false; 44 | js.off++; 45 | vn_start = js.src + js.off; 46 | /* extract varname inbetween "..."*/ 47 | while (js.off < js.len) { 48 | /* skip next character if backslashed */ 49 | if (*(js.src + js.off) == '\\') { 50 | js.off += 2; 51 | if (js.off >= js.len) break; 52 | } 53 | if (*(js.src + js.off) == '"') { 54 | vn_end = js.src + js.off; 55 | js.off++; 56 | break; 57 | } 58 | js.off++; 59 | } 60 | if (!vn_start || !vn_end) 61 | return false; 62 | if (!*vn_start || !*vn_end) 63 | return false; 64 | ve->data = vn_start; 65 | ve->len = vn_end - vn_start; 66 | return true; 67 | } 68 | 69 | /* 70 | ** an array is values separated by ',' 71 | */ 72 | bool JsonValidator::jsonArray(json_t &js) { 73 | bool rc; 74 | 75 | js.c = *(js.src + js.off); 76 | if (js.c != '[' || js.depth > JSON_MAX_DEPTH) 77 | return false; 78 | js.off++; 79 | do { 80 | rc = jsonVal(js); 81 | /* if we cannot extract the value, 82 | we may have reached array end. */ 83 | if (!rc) 84 | break; 85 | jsonForward(js); 86 | if (js.c == ',') { 87 | js.off++; 88 | jsonForward(js); 89 | } else break; 90 | } while (true); 91 | return js.c == ']'; 92 | } 93 | 94 | 95 | bool JsonValidator::jsonVal(json_t &js) { 96 | str_t val; 97 | bool ret; 98 | 99 | val.data = NULL; 100 | val.len = 0; 101 | 102 | jsonForward(js); 103 | if (js.c == '"') { 104 | ret = jsonQuoted(js, &val); 105 | if (ret) { 106 | /* parse extracted values. */ 107 | string jsckey = string((char *) js.ckey.data, js.ckey.len); 108 | string value = string((char *) val.data, val.len); 109 | transform(jsckey.begin(), jsckey.end(), jsckey.begin(), tolower); 110 | transform(value.begin(), value.end(), value.begin(), tolower); 111 | scanner.basestrRuleset(BODY, jsckey, value, bodyRules); 112 | scanner.logg(LOG_LVL_DEBUG, scanner.errorLogFile, "JSON '%s' : '%s'\n", (char *) js.ckey.data, 113 | (char *) val.data); 114 | } 115 | return ret; 116 | } 117 | if ((js.c >= '0' && js.c <= '9') || js.c == '-') { 118 | val.data = js.src + js.off; 119 | while (((*(js.src + js.off) >= '0' && *(js.src + js.off) <= '9') || 120 | *(js.src + js.off) == '.' || *(js.src + js.off) == '-') && js.off < js.len) { 121 | val.len++; 122 | js.off++; 123 | } 124 | /* parse extracted values. */ 125 | string jsckey = string((char *) js.ckey.data, js.ckey.len); 126 | string value = string((char *) val.data, val.len); 127 | transform(jsckey.begin(), jsckey.end(), jsckey.begin(), tolower); 128 | transform(value.begin(), value.end(), value.begin(), tolower); 129 | scanner.basestrRuleset(BODY, jsckey, value, bodyRules); 130 | scanner.logg(LOG_LVL_DEBUG, scanner.errorLogFile, "JSON '%s' : '%s'\n", (char *) js.ckey.data, 131 | (char *) val.data); 132 | return true; 133 | } 134 | if (!strncasecmp((const char *) (js.src + js.off), (const char *) "true", 4) || 135 | !strncasecmp((const char *) (js.src + js.off), (const char *) "false", 5) || 136 | !strncasecmp((const char *) (js.src + js.off), (const char *) "null", 4)) { 137 | js.c = *(js.src + js.off); 138 | /* we don't check static values, do we ?! */ 139 | val.data = js.src + js.off; 140 | if (js.c == 'F' || js.c == 'f') { 141 | js.off += 5; 142 | val.len = 5; 143 | } else { 144 | js.off += 4; 145 | val.len = 4; 146 | } 147 | /* parse extracted values. */ 148 | string jsckey = string((char *) js.ckey.data, js.ckey.len); 149 | string value = string((char *) val.data, val.len); 150 | transform(jsckey.begin(), jsckey.end(), jsckey.begin(), tolower); 151 | transform(value.begin(), value.end(), value.begin(), tolower); 152 | scanner.basestrRuleset(BODY, jsckey, value, bodyRules); 153 | 154 | scanner.logg(LOG_LVL_DEBUG, scanner.errorLogFile, "JSON '%s' : '%s'\n", (char *) js.ckey.data, 155 | (char *) val.data); 156 | return true; 157 | } 158 | 159 | if (js.c == '[') { 160 | ret = jsonArray(js); 161 | if (js.c != ']') 162 | return false; 163 | js.off++; 164 | return (ret); 165 | } 166 | if (js.c == '{') { 167 | /* 168 | ** if sub-struct, parse key without value : 169 | ** "foobar" : { "bar" : [1,2,3]} => "foobar" parsed alone. 170 | ** this is to avoid "foobar" left unparsed, as we won't have 171 | ** key/value here with "foobar" as a key. 172 | */ 173 | string jsckey = string((char *) js.ckey.data, js.ckey.len); 174 | transform(jsckey.begin(), jsckey.end(), jsckey.begin(), tolower); 175 | scanner.basestrRuleset(BODY, jsckey, empty, bodyRules); 176 | 177 | ret = jsonObj(js); 178 | jsonForward(js); 179 | if (js.c != '}') 180 | return false; 181 | js.off++; 182 | return (ret); 183 | } 184 | return false; 185 | } 186 | 187 | 188 | bool JsonValidator::jsonObj(json_t &js) { 189 | js.c = *(js.src + js.off); 190 | 191 | if (js.c != '{' || js.depth > JSON_MAX_DEPTH) 192 | return false; 193 | js.off++; 194 | 195 | do { 196 | jsonForward(js); 197 | /* check subs (arrays, objects) */ 198 | switch (js.c) { 199 | case '[': /* array */ 200 | js.depth++; 201 | jsonArray(js); 202 | if (!jsonSeek(js, ']')) 203 | return false; 204 | js.off++; 205 | js.depth--; 206 | break; 207 | case '{': /* sub-object */ 208 | js.depth++; 209 | jsonObj(js); 210 | if (js.c != '}') 211 | return false; 212 | js.off++; 213 | js.depth--; 214 | break; 215 | case '"': /* key : value, extract and parse. */ 216 | if (!jsonQuoted(js, &(js.ckey))) 217 | return false; 218 | if (!jsonSeek(js, ':')) 219 | return false; 220 | js.off++; 221 | jsonForward(js); 222 | if (!jsonVal(js)) 223 | return false; 224 | default: 225 | break; 226 | } 227 | jsonForward(js); 228 | /* another element ? */ 229 | if (js.c == ',') { 230 | js.off++; 231 | jsonForward(js); 232 | continue; 233 | 234 | } else if (js.c == '}') { 235 | js.depth--; 236 | /* or maybe we just finished parsing this object */ 237 | return true; 238 | } else { 239 | /* nothing we expected, die. */ 240 | return false; 241 | } 242 | } while (js.off < js.len); 243 | 244 | return false; 245 | } 246 | 247 | /* 248 | ** Parse a JSON request 249 | */ 250 | void JsonValidator::jsonParse(u_char *src, unsigned long len) { 251 | json_t js; 252 | js.json.data = js.src = src; 253 | js.json.len = js.len = len; 254 | 255 | if (!jsonSeek(js, '{')) { 256 | scanner.applyRuleMatch(scanner.parser.invalidJson, 1, BODY, "missing opening brace", empty, 257 | false); 258 | return; 259 | } 260 | if (!jsonObj(js)) { 261 | scanner.applyRuleMatch(scanner.parser.invalidJson, 1, BODY, "malformed json object", empty, 262 | false); 263 | scanner.logg(LOG_LVL_NOTICE, scanner.errorLogFile, "jsonObj returned error, apply invalid json.\n"); 264 | return; 265 | } 266 | /* we are now on closing bracket, check for garbage. */ 267 | js.off++; 268 | jsonForward(js); 269 | if (js.off != js.len) { 270 | scanner.applyRuleMatch(scanner.parser.invalidJson, 1, BODY, "garbage after the closing brace", 271 | empty, false); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /RuleParser.h: -------------------------------------------------------------------------------- 1 | /* _ _ __ _ 2 | * _ __ ___ ___ __| | __| | ___ / _| ___ _ __ __| | ___ _ __ 3 | * | '_ ` _ \ / _ \ / _` | / _` |/ _ \ |_ / _ \ '_ \ / _` |/ _ \ '__| 4 | * | | | | | | (_) | (_| | | (_| | __/ _| __/ | | | (_| | __/ | 5 | * |_| |_| |_|\___/ \__,_|___\__,_|\___|_| \___|_| |_|\__,_|\___|_| 6 | * |_____| 7 | * Copyright (c) 2017 Annihil 8 | * Released under the GPLv3 9 | */ 10 | 11 | #ifndef MOD_DEFENDER_RULEPARSER_H 12 | #define MOD_DEFENDER_RULEPARSER_H 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include "Util.h" 21 | #include 22 | #include 23 | 24 | //#define DEBUG_CONFIG_MAINRULE 25 | #ifdef DEBUG_CONFIG_MAINRULE 26 | #define DEBUG_CONF_MR(x) do { std::cerr << x; } while (0) 27 | #else 28 | #define DEBUG_CONF_MR(x) 29 | #endif 30 | 31 | //#define DEBUG_CONFIG_CHECKRULE 32 | #ifdef DEBUG_CONFIG_CHECKRULE 33 | #define DEBUG_CONF_CR(x) do { std::cerr << x; } while (0) 34 | #else 35 | #define DEBUG_CONF_CR(x) 36 | #endif 37 | 38 | //#define DEBUG_CONFIG_BASICRULE 39 | #ifdef DEBUG_CONFIG_BASICRULE 40 | #define DEBUG_CONF_BR(x) do { std::cerr << x; } while (0) 41 | #else 42 | #define DEBUG_CONF_BR(x) 43 | #endif 44 | 45 | //#define DEBUG_CONFIG_ACTION 46 | #ifdef DEBUG_CONFIG_ACTION 47 | #define DEBUG_CONF_ACTN(x) do { std::cerr << x; } while (0) 48 | #else 49 | #define DEBUG_CONF_ACTN(x) 50 | #endif 51 | 52 | //#define DEBUG_CONFIG_MATCHZONE 53 | #ifdef DEBUG_CONFIG_MATCHZONE 54 | #define DEBUG_CONF_MZ(x) do { std::cerr << x; } while (0) 55 | #else 56 | #define DEBUG_CONF_MZ(x) 57 | #endif 58 | 59 | //#define DEBUG_CONFIG_HASHTABLES 60 | #ifdef DEBUG_CONFIG_HASHTABLES 61 | #define DEBUG_CONF_HT(x) do { std::cerr << x; } while (0) 62 | #else 63 | #define DEBUG_CONF_HT(x) 64 | #endif 65 | 66 | //#define DEBUG_CONFIG_WLRFIND 67 | #ifdef DEBUG_CONFIG_WLRFIND 68 | #define DEBUG_CONF_WLRF(x) do { std::cerr << x << endl; } while (0) 69 | #else 70 | #define DEBUG_CONF_WLRF(x) 71 | #endif 72 | 73 | //#define DEBUG_CONFIG_WL 74 | #ifdef DEBUG_CONFIG_WL 75 | #define DEBUG_CONF_WL(x) do { std::cerr << x << endl; } while (0) 76 | #else 77 | #define DEBUG_CONF_WL(x) 78 | #endif 79 | 80 | using namespace Util; 81 | using std::pair; 82 | using std::vector; 83 | using std::string; 84 | using std::cerr; 85 | using std::stringstream; 86 | using std::endl; 87 | using std::istream_iterator; 88 | using std::istringstream; 89 | using std::regex; 90 | using std::sregex_iterator; 91 | using std::regex_match; 92 | using std::distance; 93 | using std::unordered_map; 94 | 95 | typedef enum { 96 | SUP_OR_EQUAL, 97 | SUP, 98 | INF_OR_EQUAL, 99 | INF 100 | } comparator_t; 101 | 102 | typedef enum { 103 | ALLOW = 0, 104 | BLOCK, 105 | DROP, 106 | LOG 107 | } rule_action_t; 108 | 109 | typedef struct { 110 | comparator_t comparator; 111 | unsigned long limit; 112 | rule_action_t action = ALLOW; 113 | } check_rule_t; 114 | 115 | /* 116 | ** struct used to store a specific match zone 117 | ** in conf : MATCH_ZONE:[GET_VAR|HEADER|POST_VAR]:VAR_NAME: 118 | */ 119 | typedef struct { 120 | bool bodyVar = false; // match in [name] var of body 121 | bool headersVar = false; // match in [name] var of headers 122 | bool argsVar = false; // match in [name] var of args 123 | bool specificUrl = false; // match on URL [name] 124 | string target; // to be used for string match zones 125 | regex targetRx; // to be used for regexed match zones 126 | } custom_rule_location_t; 127 | 128 | /* 129 | ** WhiteList Rules Definition : 130 | ** A whitelist contains : 131 | ** - an URI 132 | ** 133 | ** - one or several sets containing : 134 | ** - an variable name ('foo') associated with a zone ($GET_VAR:foo) 135 | ** - one or several rules id to whitelist 136 | */ 137 | typedef struct { 138 | bool body = false; // match in full body (POST DATA) 139 | bool bodyVar = false; // match in [name] var of body 140 | bool headers = false; // match in all headers 141 | bool headersVar = false; // match in [name] var of headers 142 | bool url = false; // match in URI 143 | bool args = false; // match in args (bla.php?) 144 | bool argsVar = false; // match in [name] var of args 145 | bool flags = false; // match on a global flag : weird_request, big_body etc. 146 | bool fileExt = false; // match on file upload extension 147 | /* set if defined "custom" match zone (GET_VAR/POST_VAR/...) */ 148 | vector wlIds; 149 | string target; 150 | } whitelist_location_t; 151 | 152 | /* 153 | ** basic rule can have 4 (so far) kind of matching mechanisms 154 | ** RX 155 | ** STR 156 | ** LIBINJ_XSS 157 | ** LIBINJ_SQL 158 | */ 159 | enum DETECT_MECHANISM { 160 | NONE = -1, 161 | RX, 162 | STR, 163 | LIBINJ_XSS, 164 | LIBINJ_SQL 165 | }; 166 | 167 | enum MATCH_TYPE { 168 | URI_ONLY = 0, 169 | NAME_ONLY, 170 | MIXED 171 | }; 172 | 173 | enum MATCH_ZONE { 174 | HEADERS = 0, 175 | URL, 176 | ARGS, 177 | BODY, 178 | RAW_BODY, 179 | FILE_EXT, 180 | UNKNOWN 181 | }; 182 | 183 | #if defined(RUNTIME_SCANNER_DEF) || defined(DEBUG_CONFIG_WL) 184 | static const char *match_zones[] = { 185 | "HEADERS", 186 | "URL", 187 | "ARGS", 188 | "BODY", 189 | "RAW_BODY", 190 | "FILE_EXT", 191 | "UNKNOWN", 192 | NULL 193 | }; 194 | #endif 195 | 196 | /* 197 | ** this struct is used to aggregate all whitelist 198 | ** that point to the same URI or the same VARNAME 199 | ** all the "subrules" will then be stored in the "whitelist_locations" 200 | */ 201 | typedef struct { 202 | vector whitelistLocations; 203 | MATCH_ZONE zone; // zone to wich the WL applies 204 | bool uriOnly = false; // if the "name" is only an url, specify it 205 | bool targetName = false; // does the rule targets the name instead of the content 206 | string name; // hash key [#]URI#VARNAME 207 | vector ids; 208 | } whitelist_rule_t; 209 | 210 | typedef struct { 211 | bool active = false; // to check if there is a basic rule or not 212 | regex rx; 213 | string str; 214 | /* 215 | ** basic rule can have 4 (so far) kind of matching mechanisms : 216 | ** RX, STR, LIBINJ_XSS, LIBINJ_SQL 217 | */ 218 | enum DETECT_MECHANISM match_type; 219 | bool rxMz = false; 220 | MATCH_ZONE zone; 221 | bool bodyMz = false; 222 | bool rawBodyMz = false; 223 | bool bodyVarMz = false; 224 | bool headersMz = false; 225 | bool headersVarMz = false; 226 | bool urlMz = false; 227 | bool specificUrlMz = false; 228 | bool argsMz = false; 229 | bool argsVarMz = false; 230 | bool fileExtMz = false; 231 | bool customLocation = false; // set if defined "custom" match zone (GET_VAR/POST_VAR/...) 232 | bool targetName = false; // does the rule targets variable name instead ? 233 | bool negative = false; 234 | vector customLocations; 235 | } basic_rule_t; 236 | 237 | enum RULE_TYPE { 238 | MAIN_RULE = 0, 239 | BASIC_RULE 240 | }; 241 | 242 | /* TOP level rule structure */ 243 | typedef struct { 244 | RULE_TYPE type; // type of the rule 245 | bool whitelist = false; // simply put a flag if it's a wlr, wl_id array will be used to store the whitelisted IDs 246 | vector wlIds; 247 | /* "common" data for all rules */ 248 | unsigned long id; 249 | string logMsg; // a specific log message 250 | /* List of scores increased on rule match. */ 251 | vector> scores; 252 | rule_action_t action = ALLOW; 253 | basic_rule_t br; // specific rule stuff 254 | } http_rule_t; 255 | 256 | extern vector tmpMainRules; 257 | 258 | extern vector getRules; 259 | extern vector bodyRules; 260 | extern vector rawBodyRules; 261 | extern vector headerRules; 262 | extern vector genericRules; // URL 263 | 264 | class RuleParser { 265 | private: 266 | vector whitelistRules; // raw array of whitelist rules 267 | bool isRuleWhitelistedRx(const http_rule_t &rule, const string uri, const string &name, MATCH_ZONE zone, bool targetName); 268 | bool isWhitelistAdapted(whitelist_rule_t &wlrule, MATCH_ZONE zone, const http_rule_t &rule, 269 | MATCH_TYPE type, bool targetName); 270 | 271 | public: 272 | unordered_map checkRules; 273 | 274 | vector tmpWlr; // raw array of transformed whitelists 275 | vector rxMzWlr; // raw array of regex-mz whitelists 276 | 277 | unordered_map wlUrlHash; // hash table of whitelisted URL rules 278 | unordered_map wlArgsHash; // hash table of whitelisted ARGS rules 279 | unordered_map wlBodyHash; // hash table of whitelisted BODY rules 280 | unordered_map wlHeadersHash; // hash table of whitelisted HEADERS rules 281 | vector disabled_rules; // rules that are globally disabled in one location 282 | http_rule_t bigRequest; 283 | http_rule_t uncommonHexEncoding; 284 | http_rule_t uncommonContentType; 285 | http_rule_t uncommonUrl; 286 | http_rule_t uncommonPostFormat; 287 | http_rule_t uncommonPostBoundary; 288 | http_rule_t invalidJson; 289 | http_rule_t emptyPostBody; 290 | http_rule_t libsqliRule; 291 | http_rule_t libxssRule; 292 | 293 | RuleParser(); 294 | static unsigned int parseMainRules(vector &ruleLines, string errorMsg); 295 | void parseCheckRule(vector> &rulesArray, string errorMsg); 296 | unsigned int parseBasicRules(vector &ruleLines, string errorMsg); 297 | static void parseAction(string action, rule_action_t& rule_action); 298 | static void parseMatchZone(http_rule_t &rule, string &rawMatchZone, stringstream &err); 299 | static string parseCode(std::regex_constants::error_type etype); 300 | void generateHashTables(); 301 | void wlrIdentify(const http_rule_t &curr, MATCH_ZONE &zone, int &uri_idx, int &name_idx); 302 | void wlrFind(const http_rule_t &curr, whitelist_rule_t &father_wlr, MATCH_ZONE &zone, int &uriIndex, int &name_idx); 303 | bool checkIds(unsigned long matchId, const vector &wlIds); 304 | bool findWlInHash(whitelist_rule_t &wlRule, const string &key, MATCH_ZONE zone); 305 | bool isRuleWhitelisted(const http_rule_t &rule, const string& uri, const string &name, MATCH_ZONE zone, bool targetName); 306 | }; 307 | 308 | 309 | #endif //MOD_DEFENDER_RULEPARSER_H 310 | -------------------------------------------------------------------------------- /Util.cpp: -------------------------------------------------------------------------------- 1 | /* _ _ __ _ 2 | * _ __ ___ ___ __| | __| | ___ / _| ___ _ __ __| | ___ _ __ 3 | * | '_ ` _ \ / _ \ / _` | / _` |/ _ \ |_ / _ \ '_ \ / _` |/ _ \ '__| 4 | * | | | | | | (_) | (_| | | (_| | __/ _| __/ | | | (_| | __/ | 5 | * |_| |_| |_|\___/ \__,_|___\__,_|\___|_| \___|_| |_|\__,_|\___|_| 6 | * |_____| 7 | * Copyright (c) 2017 Annihil 8 | * Released under the GPLv3 9 | */ 10 | 11 | #include "Util.h" 12 | #include "RuntimeScanner.hpp" 13 | 14 | static const char *logLevels[] = {"emerg", "alert", "crit", "error", "warn", "notice", "info", "debug", NULL}; 15 | 16 | namespace Util { 17 | vector split(const string &s, char delimiter) { 18 | vector v; 19 | size_t last = 0; 20 | size_t next = 0; 21 | string token; 22 | while ((next = s.find(delimiter, last)) != string::npos) { 23 | token = s.substr(last, next - last); 24 | if (!token.empty()) 25 | v.push_back(token); 26 | last = next + 1; 27 | } 28 | token = s.substr(last); 29 | if (!token.empty()) 30 | v.push_back(token); 31 | 32 | return v; 33 | } 34 | 35 | vector splitToInt(string &s, char delimiter) { 36 | vector v; 37 | size_t pos = 0; 38 | string token; 39 | while ((pos = s.find(delimiter)) != string::npos) { 40 | token = s.substr(0, pos); 41 | if (token.size() > 0) 42 | v.push_back(std::stoi(token)); 43 | s.erase(0, pos + 1); 44 | } 45 | v.push_back(std::stoi(s)); 46 | 47 | return v; 48 | } 49 | 50 | pair splitAtFirst(const string &s, string delim) { 51 | pair p; 52 | unsigned long delimpos = s.find(delim); 53 | p.first = s.substr(0, delimpos); 54 | p.second = s.substr(delimpos + delim.length(), s.size()); 55 | return p; 56 | } 57 | 58 | std::vector parseRawDirective(std::string raw_directive) { 59 | std::size_t semicolon_pos = raw_directive.rfind(';'); 60 | if (semicolon_pos != std::string::npos) { 61 | raw_directive = raw_directive.substr(0, semicolon_pos); 62 | raw_directive = rtrim(raw_directive); 63 | } 64 | std::vector parts; 65 | bool in_quotes = false; 66 | std::string part; 67 | unsigned int backslash = 0; 68 | for (size_t i = 0; i < raw_directive.length(); i++) { 69 | const char &c = raw_directive[i]; 70 | bool char_added = false; 71 | if (in_quotes || (c != ' ')) { 72 | part.push_back(c); 73 | if (in_quotes && backslash % 2 == 1 && c == '"') { 74 | 75 | } else if ((c == '"' && !in_quotes)) 76 | in_quotes = true; 77 | else if (c == '"') 78 | in_quotes = false; 79 | char_added = true; 80 | } 81 | if (in_quotes && c == '\\') 82 | backslash++; 83 | else 84 | backslash = 0; 85 | if (!part.empty() && (!char_added || (i == raw_directive.length() - 1))) { 86 | if (part.front() == '\"' && part.back() == '\"') { 87 | part.erase(0, 1); // remove leading and 88 | part.pop_back(); // trailing double quotes 89 | } 90 | parts.push_back(unescape(part)); 91 | part.clear(); 92 | } 93 | } 94 | return parts; 95 | } 96 | 97 | string apacheTimeFmt() { 98 | time_t timer; 99 | char date[20]; 100 | struct tm *tm_info; 101 | time(&timer); 102 | tm_info = localtime(&timer); 103 | strftime(date, 20, "%a %b %d %T", tm_info); 104 | 105 | struct timespec tp; 106 | clock_gettime(CLOCK_REALTIME, &tp); 107 | long mic = tp.tv_nsec / 1000; 108 | 109 | std::ostringstream oss; 110 | oss << date << "." << mic << " "; 111 | 112 | char year[5]; 113 | strftime(year, 5, "%Y", tm_info); 114 | oss << year; 115 | return oss.str(); 116 | } 117 | 118 | string naxsiTimeFmt() { 119 | time_t timer; 120 | char buffer[26]; 121 | struct tm *tm_info; 122 | time(&timer); 123 | tm_info = localtime(&timer); 124 | strftime(buffer, 26, "%Y/%m/%d %T", tm_info); 125 | return string(buffer); 126 | } 127 | 128 | string formatLog(int loglevel, const string &clientIp) { 129 | stringstream ss; 130 | ss << "[" << apacheTimeFmt() << "] "; 131 | ss << "[defender:" << logLevels[loglevel] << "] "; 132 | // ss << "[pid " << getpid() << "] "; 133 | if (!clientIp.empty()) 134 | ss << "[client " << clientIp << "] "; 135 | return ss.str(); 136 | } 137 | 138 | int naxsi_unescape_uri(u_char **dst, u_char **src, size_t size, unsigned int type) { 139 | u_char *d, *s, ch, c, decoded; 140 | int bad = 0; 141 | 142 | enum { 143 | sw_usual = 0, 144 | sw_quoted, 145 | sw_quoted_second 146 | } state; 147 | 148 | d = *dst; 149 | s = *src; 150 | 151 | state = sw_usual; 152 | decoded = 0; 153 | 154 | while (size--) { 155 | ch = *s++; 156 | switch (state) { 157 | case sw_usual: 158 | if (ch == '?' 159 | && (type & (UNESCAPE_URI | UNESCAPE_REDIRECT))) { 160 | *d++ = ch; 161 | goto done; 162 | } 163 | 164 | if (ch == '%') { 165 | state = sw_quoted; 166 | break; 167 | } 168 | 169 | *d++ = ch; 170 | break; 171 | case sw_quoted: 172 | if (ch >= '0' && ch <= '9') { 173 | decoded = (u_char) (ch - '0'); 174 | state = sw_quoted_second; 175 | break; 176 | } 177 | 178 | c = (u_char) (ch | 0x20); 179 | if (c >= 'a' && c <= 'f') { 180 | decoded = (u_char) (c - 'a' + 10); 181 | state = sw_quoted_second; 182 | break; 183 | } 184 | 185 | /* the invalid quoted character */ 186 | bad++; 187 | state = sw_usual; 188 | *d++ = '%'; 189 | *d++ = ch; 190 | break; 191 | 192 | case sw_quoted_second: 193 | state = sw_usual; 194 | if (ch >= '0' && ch <= '9') { 195 | ch = (u_char) ((decoded << 4) + ch - '0'); 196 | 197 | if (type & UNESCAPE_REDIRECT) { 198 | if (ch > '%' && ch < 0x7f) { 199 | *d++ = ch; 200 | break; 201 | } 202 | 203 | *d++ = '%'; 204 | *d++ = *(s - 2); 205 | *d++ = *(s - 1); 206 | 207 | break; 208 | } 209 | 210 | *d++ = ch; 211 | 212 | break; 213 | } 214 | 215 | c = (u_char) (ch | 0x20); 216 | if (c >= 'a' && c <= 'f') { 217 | ch = (u_char) ((decoded << 4) + c - 'a' + 10); 218 | 219 | if (type & UNESCAPE_URI) { 220 | if (ch == '?') { 221 | *d++ = ch; 222 | goto done; 223 | } 224 | 225 | *d++ = ch; 226 | break; 227 | } 228 | 229 | if (type & UNESCAPE_REDIRECT) { 230 | if (ch == '?') { 231 | *d++ = ch; 232 | goto done; 233 | } 234 | 235 | if (ch > '%' && ch < 0x7f) { 236 | *d++ = ch; 237 | break; 238 | } 239 | 240 | *d++ = '%'; 241 | *d++ = *(s - 2); 242 | *d++ = *(s - 1); 243 | break; 244 | } 245 | 246 | *d++ = ch; 247 | 248 | break; 249 | } 250 | /* the invalid quoted character */ 251 | /* as it happened in the 2nd part of quoted character, 252 | we need to restore the decoded char as well. */ 253 | *d++ = '%'; 254 | *d++ = (u_char) ((0 >= decoded && decoded < 10) ? decoded + '0' : decoded - 10 + 'a'); 255 | *d++ = ch; 256 | bad++; 257 | break; 258 | } 259 | } 260 | 261 | done: 262 | 263 | *dst = d; 264 | *src = s; 265 | 266 | return bad; 267 | } 268 | 269 | string escapeQuotes(const string &before) { 270 | string after; 271 | after.reserve(before.length() + 4); 272 | for (string::size_type i = 0; i < before.length(); ++i) { 273 | switch (before[i]) { 274 | case '"': 275 | case '\\': 276 | after += '\\'; 277 | default: 278 | after += before[i]; 279 | } 280 | } 281 | return after; 282 | } 283 | 284 | /* 285 | * Similar to Apache directive args parsing: 286 | * \\ -> \ 287 | * \ -> \ 288 | * \" -> " 289 | * \ -> \ 290 | */ 291 | string unescape(const string &s) { 292 | string res; 293 | string::const_iterator it = s.begin(); 294 | while (it != s.end()) { 295 | char c = *it++; 296 | if (c == '\\' && it != s.end()) { 297 | char next = *it++; 298 | switch (next) { 299 | case '\\': 300 | c = '\\'; 301 | break; 302 | case '"': 303 | c = '"'; 304 | break; 305 | default: 306 | res += c; 307 | res += next; 308 | continue; 309 | } 310 | } 311 | res += c; 312 | } 313 | return res; 314 | } 315 | } -------------------------------------------------------------------------------- /deps/libinjection/libinjection_xss.c: -------------------------------------------------------------------------------- 1 | 2 | #include "libinjection.h" 3 | #include "libinjection_xss.h" 4 | #include "libinjection_html5.h" 5 | 6 | #include 7 | #include 8 | 9 | typedef enum attribute { 10 | TYPE_NONE 11 | , TYPE_BLACK /* ban always */ 12 | , TYPE_ATTR_URL /* attribute value takes a URL-like object */ 13 | , TYPE_STYLE 14 | , TYPE_ATTR_INDIRECT /* attribute *name* is given in *value* */ 15 | } attribute_t; 16 | 17 | 18 | static attribute_t is_black_attr(const char* s, size_t len); 19 | static int is_black_tag(const char* s, size_t len); 20 | static int is_black_url(const char* s, size_t len); 21 | static int cstrcasecmp_with_null(const char *a, const char *b, size_t n); 22 | static int html_decode_char_at(const char* src, size_t len, size_t* consumed); 23 | static int htmlencode_startswith(const char* prefix, const char *src, size_t n); 24 | 25 | 26 | typedef struct stringtype { 27 | const char* name; 28 | attribute_t atype; 29 | } stringtype_t; 30 | 31 | 32 | static const int gsHexDecodeMap[256] = { 33 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 34 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 35 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 36 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 37 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 256, 256, 38 | 256, 256, 256, 256, 256, 10, 11, 12, 13, 14, 15, 256, 39 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 40 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 41 | 256, 10, 11, 12, 13, 14, 15, 256, 256, 256, 256, 256, 42 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 43 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 44 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 45 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 46 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 47 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 48 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 49 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 50 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 51 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 52 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 53 | 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 54 | 256, 256, 256, 256 55 | }; 56 | 57 | static int html_decode_char_at(const char* src, size_t len, size_t* consumed) 58 | { 59 | int val = 0; 60 | size_t i; 61 | int ch; 62 | 63 | if (len == 0 || src == NULL) { 64 | *consumed = 0; 65 | return -1; 66 | } 67 | 68 | *consumed = 1; 69 | if (*src != '&' || len < 2) { 70 | return (unsigned char)(*src); 71 | } 72 | 73 | 74 | if (*(src+1) != '#') { 75 | /* normally this would be for named entities 76 | * but for this case we don't actually care 77 | */ 78 | return '&'; 79 | } 80 | 81 | if (*(src+2) == 'x' || *(src+2) == 'X') { 82 | ch = (unsigned char) (*(src+3)); 83 | ch = gsHexDecodeMap[ch]; 84 | if (ch == 256) { 85 | /* degenerate case '&#[?]' */ 86 | return '&'; 87 | } 88 | val = ch; 89 | i = 4; 90 | while (i < len) { 91 | ch = (unsigned char) src[i]; 92 | if (ch == ';') { 93 | *consumed = i + 1; 94 | return val; 95 | } 96 | ch = gsHexDecodeMap[ch]; 97 | if (ch == 256) { 98 | *consumed = i; 99 | return val; 100 | } 101 | val = (val * 16) + ch; 102 | if (val > 0x1000FF) { 103 | return '&'; 104 | } 105 | ++i; 106 | } 107 | *consumed = i; 108 | return val; 109 | } else { 110 | i = 2; 111 | ch = (unsigned char) src[i]; 112 | if (ch < '0' || ch > '9') { 113 | return '&'; 114 | } 115 | val = ch - '0'; 116 | i += 1; 117 | while (i < len) { 118 | ch = (unsigned char) src[i]; 119 | if (ch == ';') { 120 | *consumed = i + 1; 121 | return val; 122 | } 123 | if (ch < '0' || ch > '9') { 124 | *consumed = i; 125 | return val; 126 | } 127 | val = (val * 10) + (ch - '0'); 128 | if (val > 0x1000FF) { 129 | return '&'; 130 | } 131 | ++i; 132 | } 133 | *consumed = i; 134 | return val; 135 | } 136 | } 137 | 138 | 139 | /* 140 | * view-source: 141 | * data: 142 | * javascript: 143 | */ 144 | static stringtype_t BLACKATTR[] = { 145 | { "ACTION", TYPE_ATTR_URL } /* form */ 146 | , { "ATTRIBUTENAME", TYPE_ATTR_INDIRECT } /* SVG allow indirection of attribute names */ 147 | , { "BY", TYPE_ATTR_URL } /* SVG */ 148 | , { "BACKGROUND", TYPE_ATTR_URL } /* IE6, O11 */ 149 | , { "DATAFORMATAS", TYPE_BLACK } /* IE */ 150 | , { "DATASRC", TYPE_BLACK } /* IE */ 151 | , { "DYNSRC", TYPE_ATTR_URL } /* Obsolete img attribute */ 152 | , { "FILTER", TYPE_STYLE } /* Opera, SVG inline style */ 153 | , { "FORMACTION", TYPE_ATTR_URL } /* HTML 5 */ 154 | , { "FOLDER", TYPE_ATTR_URL } /* Only on A tags, IE-only */ 155 | , { "FROM", TYPE_ATTR_URL } /* SVG */ 156 | , { "HANDLER", TYPE_ATTR_URL } /* SVG Tiny, Opera */ 157 | , { "HREF", TYPE_ATTR_URL } 158 | , { "LOWSRC", TYPE_ATTR_URL } /* Obsolete img attribute */ 159 | , { "POSTER", TYPE_ATTR_URL } /* Opera 10,11 */ 160 | , { "SRC", TYPE_ATTR_URL } 161 | , { "STYLE", TYPE_STYLE } 162 | , { "TO", TYPE_ATTR_URL } /* SVG */ 163 | , { "VALUES", TYPE_ATTR_URL } /* SVG */ 164 | , { "XLINK:HREF", TYPE_ATTR_URL } 165 | , { NULL, TYPE_NONE } 166 | }; 167 | 168 | /* xmlns */ 169 | /* `xml-stylesheet` > , */ 170 | 171 | /* 172 | static const char* BLACKATTR[] = { 173 | "ATTRIBUTENAME", 174 | "BACKGROUND", 175 | "DATAFORMATAS", 176 | "HREF", 177 | "SCROLL", 178 | "SRC", 179 | "STYLE", 180 | "SRCDOC", 181 | NULL 182 | }; 183 | */ 184 | 185 | static const char* BLACKTAG[] = { 186 | "APPLET" 187 | /* , "AUDIO" */ 188 | , "BASE" 189 | , "COMMENT" /* IE http://html5sec.org/#38 */ 190 | , "EMBED" 191 | /* , "FORM" */ 192 | , "FRAME" 193 | , "FRAMESET" 194 | , "HANDLER" /* Opera SVG, effectively a script tag */ 195 | , "IFRAME" 196 | , "IMPORT" 197 | , "ISINDEX" 198 | , "LINK" 199 | , "LISTENER" 200 | /* , "MARQUEE" */ 201 | , "META" 202 | , "NOSCRIPT" 203 | , "OBJECT" 204 | , "SCRIPT" 205 | , "STYLE" 206 | /* , "VIDEO" */ 207 | , "VMLFRAME" 208 | , "XML" 209 | , "XSS" 210 | , NULL 211 | }; 212 | 213 | 214 | static int cstrcasecmp_with_null(const char *a, const char *b, size_t n) 215 | { 216 | char ca; 217 | char cb; 218 | /* printf("Comparing to %s %.*s\n", a, (int)n, b); */ 219 | while (n-- > 0) { 220 | cb = *b++; 221 | if (cb == '\0') continue; 222 | 223 | ca = *a++; 224 | 225 | if (cb >= 'a' && cb <= 'z') { 226 | cb -= 0x20; 227 | } 228 | /* printf("Comparing %c vs %c with %d left\n", ca, cb, (int)n); */ 229 | if (ca != cb) { 230 | return 1; 231 | } 232 | } 233 | 234 | if (*a == 0) { 235 | /* printf(" MATCH \n"); */ 236 | return 0; 237 | } else { 238 | return 1; 239 | } 240 | } 241 | 242 | /* 243 | * Does an HTML encoded binary string (const char*, length) start with 244 | * a all uppercase c-string (null terminated), case insensitive! 245 | * 246 | * also ignore any embedded nulls in the HTML string! 247 | * 248 | * return 1 if match / starts with 249 | * return 0 if not 250 | */ 251 | static int htmlencode_startswith(const char *a, const char *b, size_t n) 252 | { 253 | size_t consumed; 254 | int cb; 255 | int first = 1; 256 | /* printf("Comparing %s with %.*s\n", a,(int)n,b); */ 257 | while (n > 0) { 258 | if (*a == 0) { 259 | /* printf("Match EOL!\n"); */ 260 | return 1; 261 | } 262 | cb = html_decode_char_at(b, n, &consumed); 263 | b += consumed; 264 | n -= consumed; 265 | 266 | if (first && cb <= 32) { 267 | /* ignore all leading whitespace and control characters */ 268 | continue; 269 | } 270 | first = 0; 271 | 272 | if (cb == 0) { 273 | /* always ignore null characters in user input */ 274 | continue; 275 | } 276 | 277 | if (cb == 10) { 278 | /* always ignore vertical tab characters in user input */ 279 | /* who allows this?? */ 280 | continue; 281 | } 282 | 283 | if (cb >= 'a' && cb <= 'z') { 284 | /* upcase */ 285 | cb -= 0x20; 286 | } 287 | 288 | if (*a != (char) cb) { 289 | /* printf(" %c != %c\n", *a, cb); */ 290 | /* mismatch */ 291 | return 0; 292 | } 293 | a++; 294 | } 295 | 296 | return (*a == 0) ? 1 : 0; 297 | } 298 | 299 | static int is_black_tag(const char* s, size_t len) 300 | { 301 | const char** black; 302 | 303 | if (len < 3) { 304 | return 0; 305 | } 306 | 307 | black = BLACKTAG; 308 | while (*black != NULL) { 309 | if (cstrcasecmp_with_null(*black, s, len) == 0) { 310 | /* printf("Got black tag %s\n", *black); */ 311 | return 1; 312 | } 313 | black += 1; 314 | } 315 | 316 | /* anything SVG related */ 317 | if ((s[0] == 's' || s[0] == 'S') && 318 | (s[1] == 'v' || s[1] == 'V') && 319 | (s[2] == 'g' || s[2] == 'G')) { 320 | /* printf("Got SVG tag \n"); */ 321 | return 1; 322 | } 323 | 324 | /* Anything XSL(t) related */ 325 | if ((s[0] == 'x' || s[0] == 'X') && 326 | (s[1] == 's' || s[1] == 'S') && 327 | (s[2] == 'l' || s[2] == 'L')) { 328 | /* printf("Got XSL tag\n"); */ 329 | return 1; 330 | } 331 | 332 | return 0; 333 | } 334 | 335 | static attribute_t is_black_attr(const char* s, size_t len) 336 | { 337 | stringtype_t* black; 338 | 339 | if (len < 2) { 340 | return TYPE_NONE; 341 | } 342 | 343 | /* JavaScript on.* */ 344 | if ((s[0] == 'o' || s[0] == 'O') && (s[1] == 'n' || s[1] == 'N')) { 345 | /* printf("Got JavaScript on- attribute name\n"); */ 346 | return TYPE_BLACK; 347 | } 348 | 349 | 350 | if (len >= 5) { 351 | /* XMLNS can be used to create arbitrary tags */ 352 | if (cstrcasecmp_with_null("XMLNS", s, 5) == 0 || cstrcasecmp_with_null("XLINK", s, 5) == 0) { 353 | /* printf("Got XMLNS and XLINK tags\n"); */ 354 | return TYPE_BLACK; 355 | } 356 | } 357 | 358 | black = BLACKATTR; 359 | while (black->name != NULL) { 360 | if (cstrcasecmp_with_null(black->name, s, len) == 0) { 361 | /* printf("Got banned attribute name %s\n", black->name); */ 362 | return black->atype; 363 | } 364 | black += 1; 365 | } 366 | 367 | return TYPE_NONE; 368 | } 369 | 370 | static int is_black_url(const char* s, size_t len) 371 | { 372 | 373 | static const char* data_url = "DATA"; 374 | static const char* viewsource_url = "VIEW-SOURCE"; 375 | 376 | /* obsolete but interesting signal */ 377 | static const char* vbscript_url = "VBSCRIPT"; 378 | 379 | /* covers JAVA, JAVASCRIPT, + colon */ 380 | static const char* javascript_url = "JAVA"; 381 | 382 | /* skip whitespace */ 383 | while (len > 0 && (*s <= 32 || *s >= 127)) { 384 | /* 385 | * HEY: this is a signed character. 386 | * We are intentionally skipping high-bit characters too 387 | * since they are not ASCII, and Opera sometimes uses UTF-8 whitespace. 388 | * 389 | * Also in EUC-JP some of the high bytes are just ignored. 390 | */ 391 | ++s; 392 | --len; 393 | } 394 | 395 | if (htmlencode_startswith(data_url, s, len)) { 396 | return 1; 397 | } 398 | 399 | if (htmlencode_startswith(viewsource_url, s, len)) { 400 | return 1; 401 | } 402 | 403 | if (htmlencode_startswith(javascript_url, s, len)) { 404 | return 1; 405 | } 406 | 407 | if (htmlencode_startswith(vbscript_url, s, len)) { 408 | return 1; 409 | } 410 | return 0; 411 | } 412 | 413 | int libinjection_is_xss(const char* s, size_t len, int flags) 414 | { 415 | h5_state_t h5; 416 | attribute_t attr = TYPE_NONE; 417 | 418 | libinjection_h5_init(&h5, s, len, (enum html5_flags) flags); 419 | while (libinjection_h5_next(&h5)) { 420 | if (h5.token_type != ATTR_VALUE) { 421 | attr = TYPE_NONE; 422 | } 423 | 424 | if (h5.token_type == DOCTYPE) { 425 | return 1; 426 | } else if (h5.token_type == TAG_NAME_OPEN) { 427 | if (is_black_tag(h5.token_start, h5.token_len)) { 428 | return 1; 429 | } 430 | } else if (h5.token_type == ATTR_NAME) { 431 | attr = is_black_attr(h5.token_start, h5.token_len); 432 | } else if (h5.token_type == ATTR_VALUE) { 433 | /* 434 | * IE6,7,8 parsing works a bit differently so 435 | * a whole