diff --git a/RuleParser.cpp b/RuleParser.cpp index 04cfb12..a3fcfe9 100644 --- a/RuleParser.cpp +++ b/RuleParser.cpp @@ -54,12 +54,12 @@ RuleParser::RuleParser() { libsqliRule.id = 17; libsqliRule.scores.emplace_back("$LIBINJECTION_SQL", 8); - libsqliRule.action = BLOCK; + libsqliRule.action = LOG; libxssRule.id = 18; libxssRule.logMsg = "Libinjection XSS"; libxssRule.scores.emplace_back("$LIBINJECTION_XSS", 8); - libxssRule.action = BLOCK; + libxssRule.action = LOG; } unsigned int RuleParser::parseMainRules(vector &ruleLines, string errorMsg) { diff --git a/RuntimeScanner.cpp b/RuntimeScanner.cpp index 28a9004..24c7887 100644 --- a/RuntimeScanner.cpp +++ b/RuntimeScanner.cpp @@ -5,6 +5,7 @@ * |_| |_| |_|\___/ \__,_|___\__,_|\___|_| \___|_| |_|\__,_|\___|_| * |_____| * Copyright (c) 2017 Annihil + * Copyright (c) 2019 Jérémie Jourdin - Advens * Released under the GPLv3 */ @@ -696,12 +697,12 @@ void RuntimeScanner::addHeader(char *key, char *val) { } // Store Content-Type for further processing else if (k == "content-type") { - if (v == "application/x-www-form-urlencoded") { + if (v.substr(0, 33) == "application/x-www-form-urlencoded") { contentType = CONTENT_TYPE_URL_ENC; } else if (v.substr(0, 20) == "multipart/form-data;") { contentType = CONTENT_TYPE_MULTIPART; rawContentType = string(val); // important: need to keep the case! - } else if (v == "application/json") { + } else if (v.substr(0,16) == "application/json") { contentType = CONTENT_TYPE_APP_JSON; } } @@ -954,7 +955,13 @@ void RuntimeScanner::writeJSONLearningLog() { jsonlog << "\"protocol\":\"" << protocol << "\","; jsonlog << "\"unparsed_uri\":\"" << fullUri << "\","; unique_data << "" << uri; - jsonlog << "\"context_id\":\"" << unique_data.str() << "\""; + /* + o This may generate HUGE string - thus causing problem when inserting into databases + o Better approach: Use HASH + */ + std::hash hash_fn; + size_t str_hash = hash_fn(unique_data.str()); + jsonlog << "\"context_id\":\"" << str_hash << "\""; jsonlog << "}" << endl; streamToFile(jsonlog, learningJSONLogFile); diff --git a/mod_defender.cpp b/mod_defender.cpp index d957fde..e4bd765 100644 --- a/mod_defender.cpp +++ b/mod_defender.cpp @@ -133,6 +133,141 @@ static int write_log(void *thefile, const void *buf, size_t *nbytes) { return apr_file_write((apr_file_t *) thefile, buf, nbytes); } +/** + * Function from Apache httpd, used to convert 2 chars string into hex int + * Example: "2a" => 0x2a + */ +static char x2c(const char *what) +{ + char digit; + +#if !APR_CHARSET_EBCDIC + digit = ((what[0] >= 'A') ? ((what[0] & 0xdf) - 'A') + 10 + : (what[0] - '0')); + digit *= 16; + digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A') + 10 + : (what[1] - '0')); +#else /*APR_CHARSET_EBCDIC*/ + char xstr[5]; + xstr[0]='0'; + xstr[1]='x'; + xstr[2]=what[0]; + xstr[3]=what[1]; + xstr[4]='\0'; + digit = apr_xlate_conv_byte(ap_hdrs_from_ascii, + 0xFF & strtol(xstr, NULL, 16)); +#endif /*APR_CHARSET_EBCDIC*/ + return (digit); +} + +/** + * Function from Apache httpd, used to urldecode string + * The particularity of this function, instead of Apache's, is that the %00 is not interpreted + */ +static int unescape_url(char *url, const char *forbid, const char *reserved) +{ + int badesc, badpath; + char *x, *y; + + badesc = 0; + badpath = 0; + /* Initial scan for first '%'. Don't bother writing values before + * seeing a '%' */ + y = strchr(url, '%'); + if (y == NULL) { + return OK; + } + for (x = y; *y; ++x, ++y) { + if (*y != '%') { + *x = *y; + } + else { + if (!apr_isxdigit(*(y + 1)) || !apr_isxdigit(*(y + 2))) { + badesc = 1; + *x = '%'; + } + else { + char decoded; + decoded = x2c(y + 1); + if( decoded == '\0' ) { + /* Copy-Paste the %00 - don't interpret ! */ + *x++ = *y++; + *x++ = *y++; + *x = *y; + badpath = 1; + } else if( forbid && ap_strchr_c(forbid, decoded) ) { + badpath = 1; + *x = decoded; + y += 2; + } + else if (reserved && ap_strchr_c(reserved, decoded)) { + *x++ = *y++; + *x++ = *y++; + *x = *y; + } + else { + *x = decoded; + y += 2; + } + } + } + } + *x = '\0'; + if (badesc) { + return HTTP_BAD_REQUEST; + } + else if (badpath) { + return HTTP_NOT_FOUND; + } + else { + return OK; + } +} + +/** + * Function from Apache httpd, used too convert get params as string + * into an apr_table_t struct filled in with key, value pairs GET params + */ +static void argstring_to_table(char *str, apr_table_t *parms) +{ + char *key; + char *value; + char *strtok_state; + + if (str == NULL) { + return; + } + + key = apr_strtok(str, "&", &strtok_state); + while (key) { + value = strchr(key, '='); + if (value) { + *value = '\0'; /* Split the string in two */ + value++; /* Skip passed the = */ + } + else { + value = (char*)"1"; + } + /* Verify return ? */ + unescape_url(key, SLASHES, NULL); + unescape_url(value, SLASHES, NULL); + apr_table_set(parms, key, value); + key = apr_strtok(NULL, "&", &strtok_state); + } +} + +/** + * Function from Apache httpd, used to convert request GET arguments + * into an apr_table_t struct filled-in with key, value pairs + */ +void args_to_table(request_rec *r, apr_table_t **table) +{ + apr_table_t *t = apr_table_make(r->pool, 10); + argstring_to_table(apr_pstrdup(r->pool, r->args), t); + *table = t; +} + + /** * This routine gives our module another chance to examine the request * headers and to take special action. This is the first phase whose @@ -254,7 +389,7 @@ static int header_parser(request_rec *r) { // Pass GET parameters apr_table_t *getTable = NULL; - ap_args_to_table(r, &getTable); + args_to_table(r, &getTable); const apr_array_header_t *getParams = apr_table_elts(getTable); apr_table_entry_t *getParam = (apr_table_entry_t *) getParams->elts; for (int i = 0; i < getParams->nelts; i++) diff --git a/mod_defender.hpp b/mod_defender.hpp index 0cad713..a6a6834 100644 --- a/mod_defender.hpp +++ b/mod_defender.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include "RuleParser.h" #include "RuntimeScanner.hpp" @@ -45,34 +46,40 @@ extern module AP_MODULE_DECLARE_DATA defender_module; /** * \def MAX_BB_SIZE - * The length of the 403 response body, in bytes + * The maximum length of post body processed */ #define MAX_BB_SIZE 0x7FFFFFFF /** * \def CHUNK_CAPACITY - * The length of the 403 response body, in bytes + * The maximum length of a chunk */ #define CHUNK_CAPACITY 8192 /** * \def IF_STATUS_NONE - * The length of the 403 response body, in bytes + * The status of the body to be processed */ #define IF_STATUS_NONE 0 /** * \def IF_STATUS_WANTS_TO_RUN - * The length of the 403 response body, in bytes + * The status of the body to be processed */ #define IF_STATUS_WANTS_TO_RUN 1 /** * \def IF_STATUS_COMPLETE - * The length of the 403 response body, in bytes + * The status of the body to be processed */ #define IF_STATUS_COMPLETE 2 +/** + * \def SLASHES + * The slash as string, used to urlencode/decode + */ +#define SLASHES "/" + /**************/ /* Structures */ diff --git a/tests/core.sh b/tests/core.sh index 077aba6..829f4ca 100755 --- a/tests/core.sh +++ b/tests/core.sh @@ -3,85 +3,107 @@ source ./test.sh declare -a tests=( - # " -d a=blah" 0 - ) + # " -d a=blah" 0 + ) # BODY BODY_NAME URL ARGS ARGS_NAME $HEADERS_VAR:Cookie declare -a core_rules_tests=( - # SQL Injections IDs:1000-1099 - "blah" 0 0 0 0 0 0 - "select+from" 1 1 1 1 1 1 - "selected+fromage" 0 0 0 0 0 0 - "\"" 1 1 1 1 1 1 - "0x0x0x0x" 1 1 1 1 1 1 - "/*" 1 1 1 1 1 1 - "*/" 1 1 1 1 1 1 - "|" 1 1 1 1 1 1 - "&&" 1 1 1 1 1 1 - "----" 1 1 1 1 1 1 - ";" 1 1 1 1 1 0 - "====" 1 1 0 1 1 0 - "(" 1 1 1 1 1 1 - ")" 1 1 1 1 1 1 - "'" 1 1 1 1 1 1 - ",," 1 1 1 1 1 1 - "##" 1 1 1 1 1 1 - "@@@@" 1 1 1 1 1 1 + # SQL Injections IDs:1000-1099 + "blah" 0 0 0 0 0 0 0 0 0 0 0 + "select+from" 1 1 1 1 1 1 1 1 1 1 1 + "selected+fromage" 0 0 0 0 0 0 0 0 0 0 0 + "\\\"" 1 1 1 1 1 1 1 1 1 1 1 + "0x0x0x0x" 1 1 1 1 1 1 1 1 1 1 1 + "/*" 1 1 1 1 1 1 1 1 1 1 1 + "*/" 1 1 1 1 1 1 1 1 1 1 1 + "|" 1 1 1 1 1 1 1 1 1 1 1 + "&&" 1 1 1 1 1 1 0 0 1 0 0 + "----" 1 1 1 1 1 1 1 1 1 1 1 + ";" 1 1 1 1 1 0 1 1 1 1 1 + "====" 1 1 0 1 1 0 1 1 0 1 1 + "(" 1 1 1 1 1 1 1 1 1 1 1 + ")" 1 1 1 1 1 1 1 1 1 1 1 + "'" 1 1 1 1 1 1 1 1 1 1 1 + ",," 1 1 1 1 1 1 1 1 1 1 1 + "##" 1 1 1 1 1 1 1 1 0 0 0 + "@@@@" 1 1 1 1 1 1 1 1 1 1 1 - # OBVIOUS RFI IDs:1100-1199 - "http://" 1 1 0 1 1 1 - "https://" 1 1 0 1 1 1 - "ftp://" 1 1 0 1 1 1 - "sftp://" 1 1 0 1 1 1 - "zlib://" 1 1 0 1 1 1 - "data://" 1 1 0 1 1 1 - "glob://" 1 1 0 1 1 1 - "phar://" 1 1 0 1 1 1 - "file://" 1 1 0 1 1 1 - "gopher://" 1 1 0 1 1 1 + # OBVIOUS RFI IDs:1100-1199 + "http://" 1 1 0 1 1 1 1 1 0 1 1 + "https://" 1 1 0 1 1 1 1 1 0 1 1 + "ftp://" 1 1 0 1 1 1 1 1 0 1 1 + "sftp://" 1 1 0 1 1 1 1 1 0 1 1 + "zlib://" 1 1 0 1 1 1 1 1 0 1 1 + "data://" 1 1 0 1 1 1 1 1 0 1 1 + "glob://" 1 1 0 1 1 1 1 1 0 1 1 + "phar://" 1 1 0 1 1 1 1 1 0 1 1 + "file://" 1 1 0 1 1 1 1 1 0 1 1 + "gopher://" 1 1 0 1 1 1 1 1 0 1 1 - # Directory traversal IDs:1200-1299 - "...." 1 1 1 1 1 1 - "/etc/passwd" 1 1 1 1 1 1 - "c:\\" 1 1 1 1 1 1 - "cmd.exe" 1 1 1 1 1 1 - "\\" 1 1 1 1 1 1 + # Directory traversal IDs:1200-1299 + "...." 1 1 1 1 1 1 1 1 1 1 1 + "/etc/passwd" 1 1 1 1 1 1 1 1 1 1 1 + "c:\\\\" 1 1 1 1 1 1 1 1 1 1 1 + "cmd.exe" 1 1 1 1 1 1 1 1 1 1 1 + "\\\\" 1 1 1 1 1 1 1 1 1 1 1 - # Cross Site Scripting IDs:1300-1399 - "<" 1 1 1 1 1 1 - ">" 1 1 1 1 1 1 - "[[" 1 1 1 1 1 1 - "]]" 1 1 1 1 1 1 - "~~" 1 1 1 1 1 1 - "\`" 1 1 1 1 1 1 - "%20" 1 1 1 1 1 1 + # Cross Site Scripting IDs:1300-1399 + "<" 1 1 1 1 1 1 1 1 1 1 1 + ">" 1 1 1 1 1 1 1 1 1 1 1 + "[[" 1 1 1 1 1 1 1 1 1 1 1 + "]]" 1 1 1 1 1 1 1 1 1 1 1 + "~~" 1 1 1 1 1 1 1 1 1 1 1 + "\\\`" 1 1 1 1 1 1 1 1 1 1 1 + "%20" 1 1 1 1 1 1 0 0 0 0 0 + "%00" 1 1 1 1 1 1 1 1 0 1 1 + + # Evading tricks IDs: 1400-1500 + "&#" 1 1 1 1 1 1 0 0 0 0 0 + "%U" 1 1 1 1 1 1 1 1 400 1 1 + ) - # Evading tricks IDs: 1400-1500 - "&#" 1 1 1 1 1 1 - "%U" 1 1 1 1 1 1 - ) -for ((i=0; i<${#core_rules_tests[@]}; i+=7)); do - pattern=${core_rules_tests[$i]} - tests+=(" --data-urlencode x=$pattern" ${core_rules_tests[$i+1]}) - tests+=(" -d $(url_encode "$pattern")=x" ${core_rules_tests[$i+2]}) - tests+=($(url_encode "$pattern") ${core_rules_tests[$i+3]}) - tests+=("?x="$(url_encode "$pattern") ${core_rules_tests[$i+4]}) - tests+=("?$(url_encode "$pattern")=x" ${core_rules_tests[$i+5]}) - tests+=(" -b x=$pattern" ${core_rules_tests[$i+6]}) -done -tests_size=${#tests[@]} -test_count=$((tests_size / 2)) +test_count=0 test_passed=0 -for ((i=0; i<$tests_size; i+=2)); do - req="curl $HOST/${tests[$i]}" - expected_action=${tests[$i+1]} - status_code=`$req $curl_ret` - test_msg=`check_block $status_code $expected_action` - test_passed=$((test_passed + $?)) - printf "%-60s %s\n" "$req" "$status_code $test_msg" +check_url() { + # URL = $1 + # OPTIONS = $2 + # Expected action = $3 + req="curl \"$HOST/$1\" $2" + expected_action="$3" + status_code=$(echo "$req $curl_ret" | bash) + # If expected code is not 0 or 1 -> it is an http code + if ([ $expected_action -ne 0 ] && [ $expected_action -ne 1 ]) + then + test_msg=$(check_status_code $status_code $expected_action) + else + test_msg=$(check_block $status_code $expected_action) + fi + test_passed=$((test_passed + $?)) + test_count=$((test_count + 1)) + printf "%-60s %s\n" "$req" "$status_code $test_msg" +} + +for ((i=0; i<${#core_rules_tests[@]}; i+=12)); do + pattern=${core_rules_tests[$i]} + # URL encoded + check_url "" " --data-urlencode \"x=$pattern\"" ${core_rules_tests[$i+1]} + no_escaped="$(echo "$pattern" | sed 's/\\\(.\{1\}\)/\1/g')" + check_url "" " --data-raw \"$(url_encode "$no_escaped")=x\"" ${core_rules_tests[$i+2]} + check_url "$(url_encode "$no_escaped")" "" ${core_rules_tests[$i+3]} + check_url "?x=$(url_encode "$no_escaped")" "" ${core_rules_tests[$i+4]} + check_url "?$(url_encode "$no_escaped")=x" "" ${core_rules_tests[$i+5]} + check_url "" " -b \"x=$pattern\"" ${core_rules_tests[$i+6]} + # Do NOT URL encode + check_url "" " --data-raw \"x=$pattern\"" ${core_rules_tests[$i+7]} + check_url "" " --data-raw \"$pattern=x\"" ${core_rules_tests[$i+8]} + check_url "$pattern" " -g " ${core_rules_tests[$i+9]} + check_url "?x=$pattern" " -g " ${core_rules_tests[$i+10]} + check_url "?$pattern=x" " -g " ${core_rules_tests[$i+11]} done +# Print results echo $test_passed/$test_count "tests passed" \($(((test_passed * 100) / test_count))%\) -exit $(($test_passed != $test_count)) \ No newline at end of file +exit $(($test_passed != $test_count)) + diff --git a/tests/core_https.sh b/tests/core_https.sh old mode 100755 new mode 100644 diff --git a/tests/internal.sh b/tests/internal.sh index 3e1b373..30fb5f2 100755 --- a/tests/internal.sh +++ b/tests/internal.sh @@ -18,7 +18,7 @@ test_count=$((test_count + 1)) echo -e "sent 2MB @ 350kb/s " "$req" "$status_code $test_msg" status_code=$(printf %1000s | tr " " "a" | curl $HOST --data-binary @- -H "Transfer-Encoding: chunked" $curl_ret) -test_msg=`check_block $status_code 0` +test_msg=`check_status_code $status_code 501` test_passed=$((test_passed + $?)) test_count=$((test_count + 1)) echo -e "sent 1kB with transfer-encoding: chunked " "$req" "$status_code $test_msg" @@ -55,4 +55,4 @@ test_count=$((test_count + 1)) echo -e "<200*a>+select+from=x " "$req" "$status_code $test_msg" echo $test_passed/$test_count "tests passed" \($(((test_passed * 100) / test_count))%\) -exit $(($test_passed != $test_count)) +exit $(($test_passed != $test_count)) \ No newline at end of file diff --git a/tests/internal_https.sh b/tests/internal_https.sh old mode 100755 new mode 100644 diff --git a/tests/test.sh b/tests/test.sh index 6d0af8a..ac7b496 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -6,7 +6,6 @@ if [ "$#" -ne 1 ]; then fi HOST=$1 curl_ret="-s -o /dev/null -w %{http_code}" -ca_path="/etc/apache2/ssl/ca.crt" PASS_MESSAGE="[ \033[0;32mPASS\033[0m ]" FAIL_MESSAGE="[ \033[0;31mFAIL\033[0m ]"