diff --git a/ChangeLog b/ChangeLog index 46161ee..0355fde 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,41 @@ +Version 2.08b: + + - Added Host header XSS testing. + + - Added HTML encoding XSS tests to detect scenarios where our + injection string ends up in an attributes that execute HTML encoded + Javascript. For example: onclick. + + - Bruteforcing is now disabled for URLs that gave a directory listing. + + - Added subject alternate name checking for SSL certificates (cheers + to Matt Caroll for his feedback) + + - Added signature matching (see doc/signatures.txt) which means a lot + of the content based issues are no longer hardcoded. + + - Added active XSSI test. The passive XSSI stays (for now) but this + active check is more acurate and will remove issues detected by the + passive one if they cannot be confirmed. This reduces false positives + + - Added HTML tag XSS test which triggers when our payload is used + as a tag attribute value but without quotes (courtesy of wavsep). + + - Added javascript: scheme XSS testing (courtesy of wavsep). + + - Added form based authentication. During these authenticated + scans, skipfish will check if the session has ended and re-authenticates + if necessary. + + - Fixed a bug where in slow scans the console output could mess up + due to the high(er) refresh rate. + + - Fixed a bug where a missed response during the injection tests could + result in a crash. (courtesy of Sebastian Roschke) + + - Restructure the source package a bit by adding a src/, doc/ and + tools/ directory. + Version 2.07b: -------------- diff --git a/Makefile b/Makefile index 039065f..b824797 100644 --- a/Makefile +++ b/Makefile @@ -20,45 +20,49 @@ # PROGNAME = skipfish -VERSION = 2.07b +VERSION = 2.08b -OBJFILES = http_client.c database.c crawler.c analysis.c report.c \ - checks.c -INCFILES = alloc-inl.h string-inl.h debug.h types.h http_client.h \ +SRCDIR = src +SFILES = http_client.c database.c crawler.c analysis.c report.c \ + checks.c signatures.c auth.c +IFILES = alloc-inl.h string-inl.h debug.h types.h http_client.h \ database.h crawler.h analysis.h config.h report.h \ - checks.h + checks.h signatures.h auth.h + +OBJFILES = $(patsubst %,$(SRCDIR)/%,$(SFILES)) +INCFILES = $(patsubst %,$(SRCDIR)/%,$(IFILES)) CFLAGS_GEN = -Wall -funsigned-char -g -ggdb -I/usr/local/include/ \ -I/opt/local/include/ $(CFLAGS) -DVERSION=\"$(VERSION)\" CFLAGS_DBG = -DLOG_STDERR=1 -DDEBUG_ALLOCATOR=1 $(CFLAGS_GEN) -CFLAGS_OPT = -O3 -Wno-format $(CFLAGS_GEN) +CFLAGS_OPT = -O3 -Wno-format $(CFLAGS_GEN) LDFLAGS += -L/usr/local/lib/ -L/opt/local/lib -LIBS += -lcrypto -lssl -lidn -lz +LIBS += -lcrypto -lssl -lidn -lz -lpcre all: $(PROGNAME) -$(PROGNAME): $(PROGNAME).c $(OBJFILES) $(INCFILES) - $(CC) $(LDFLAGS) $(PROGNAME).c -o $(PROGNAME) $(CFLAGS_OPT) \ - $(OBJFILES) $(LIBS) +$(PROGNAME): $(SRCDIR)/$(PROGNAME).c $(OBJFILES) $(INCFILES) + $(CC) $(LDFLAGS) $(SRCDIR)/$(PROGNAME).c -o $(PROGNAME) \ + $(CFLAGS_OPT) $(OBJFILES) $(LIBS) @echo - @echo "See dictionaries/README-FIRST to pick a dictionary for the tool." + @echo "See doc/dictionaries.txt to pick a dictionary for the tool." @echo @echo "Having problems with your scans? Be sure to visit:" @echo "http://code.google.com/p/skipfish/wiki/KnownIssues" @echo -debug: $(PROGNAME).c $(OBJFILES) $(INCFILES) - $(CC) $(LDFLAGS) $(PROGNAME).c -o $(PROGNAME) $(CFLAGS_DBG) \ - $(OBJFILES) $(LIBS) +debug: $(SRCDIR)/$(PROGNAME).c $(OBJFILES) $(INCFILES) + $(CC) $(LDFLAGS) $(SRCDIR)/$(PROGNAME).c -o $(PROGNAME) \ + $(CFLAGS_DBG) $(OBJFILES) $(LIBS) clean: rm -f $(PROGNAME) *.exe *.o *~ a.out core core.[1-9][0-9]* *.stackdump \ LOG same_test rm -rf tmpdir -same_test: same_test.c $(OBJFILES) $(INCFILES) - $(CC) same_test.c -o same_test $(CFLAGS_DBG) $(OBJFILES) $(LDFLAGS) \ +same_test: $(SRCDIR)/same_test.c $(OBJFILES) $(INCFILES) + $(CC) $(SRCDIR)/same_test.c -o same_test $(CFLAGS_DBG) $(OBJFILES) $(LDFLAGS) \ $(LIBS) publish: clean diff --git a/README b/README index 569cabd..f068618 100644 --- a/README +++ b/README @@ -85,6 +85,9 @@ associated with web security scanners. Specific advantages include: stored XSS (path, parameters, headers), blind SQL or XML injection, or blind shell injection. + * Snort style content signatures which will highlight server errors, + information leaks or potentially dangerous web applications. + * Report post-processing drastically reduces the noise caused by any remaining false positives or server gimmicks by identifying repetitive patterns. @@ -274,23 +277,15 @@ report will be non-destructively annotated by adding red background to all new or changed nodes; and blue background to all new or changed issues found. -Some sites may require authentication; for simple HTTP credentials, you can -try: +Some sites may require authentication for which our support is described +in docs/authentication.txt. In most cases, you'll be wanting to use the +form authentication method which is capable of detecting broken sessions +in order to re-authenticate. -$ ./skipfish -A user:pass ...other parameters... - -Alternatively, if the site relies on HTTP cookies instead, log in in your -browser or using a simple curl script, and then provide skipfish with a -session cookie: - -$ ./skipfish -C name=val ...other parameters... - -Other session cookies may be passed the same way, one per each -C option. - -Certain URLs on the site may log out your session; you can combat this in two -ways: by using the -N option, which causes the scanner to reject attempts to -set or delete cookies; or with the -X parameter, which prevents matching URLs -from being fetched: +Once authenticated, certain URLs on the site may log out your session; +you can combat this in two ways: by using the -N option, which causes +the scanner to reject attempts to set or delete cookies; or with the -X +parameter, which prevents matching URLs from being fetched: $ ./skipfish -X /logout/logout.aspx ...other parameters... @@ -544,8 +539,6 @@ know: * Scheduling and management web UI. - * A database for banner / version checks or other configurable rules? - ------------------------------------- 9. Oy! Something went horribly wrong! ------------------------------------- diff --git a/assets/index.html b/assets/index.html index f50e54c..ce036f9 100644 --- a/assets/index.html +++ b/assets/index.html @@ -278,6 +278,7 @@ var issue_desc= { "10804": "Conflicting MIME / charset info (low risk)", "10901": "Numerical filename - consider enumerating", "10902": "OGNL-like parameter behavior", + "10909": "Signature match (informational)", "20101": "Resource fetch failed", "20102": "Limits exceeded, fetch suppressed", @@ -294,6 +295,7 @@ var issue_desc= { "30203": "SSL certificate host name mismatch", "30204": "No SSL certificate data found", "30205": "Weak SSL cipher negotiated", + "30206": "Host name length mismatch (name string has null byte)", "30301": "Directory listing restrictions bypassed", "30401": "Redirection to attacker-supplied URLs", "30402": "Attacker-supplied URLs in embedded content (lower risk)", @@ -305,11 +307,13 @@ var issue_desc= { "30701": "Incorrect caching directives (lower risk)", "30801": "User-controlled response prefix (BOM / plugin attacks)", "30901": "HTTP header injection vector", + "30909": "Signature match detected", "40101": "XSS vector in document body", "40102": "XSS vector via arbitrary URLs", "40103": "HTTP response header splitting", "40104": "Attacker-supplied URLs in embedded content (higher risk)", + "40105": "XSS vector via injected HTML tag attribute", "40201": "External content embedded on a page (higher risk)", "40202": "Mixed content embedded on a page (higher risk)", "40301": "Incorrect or missing MIME type (higher risk)", @@ -321,6 +325,7 @@ var issue_desc= { "40501": "Directory traversal / file inclusion possible", "40601": "Incorrect caching directives (higher risk)", "40701": "Password form submits from or to non-HTTPS page", + "40909": "Signature match detected (higher risk)", "50101": "Server-side XML injection vector", "50102": "Shell injection vector", @@ -329,7 +334,8 @@ var issue_desc= { "50105": "Integer overflow vector", "50106": "File inclusion", "50201": "SQL query or similar syntax in parameters", - "50301": "PUT request accepted" + "50301": "PUT request accepted", + "50909": "Signature match detected (high risk)" }; diff --git a/dictionaries/extensions-only.wl b/dictionaries/extensions-only.wl index 16605ed..45c183f 100644 --- a/dictionaries/extensions-only.wl +++ b/dictionaries/extensions-only.wl @@ -86,7 +86,6 @@ e 1 1 1 sql e 1 1 1 stackdump e 1 1 1 svn-base e 1 1 1 swf -e 1 1 1 swp e 1 1 1 tar e 1 1 1 tar.bz2 e 1 1 1 tar.gz @@ -107,4 +106,3 @@ e 1 1 1 xsl e 1 1 1 xslt e 1 1 1 yml e 1 1 1 zip -e 1 1 1 ~ diff --git a/doc/authentication.txt b/doc/authentication.txt new file mode 100644 index 0000000..b06210a --- /dev/null +++ b/doc/authentication.txt @@ -0,0 +1,98 @@ + + +This document describes 3 different methods you can use to run +authenticated skipfish scans. + + 1) Form authentication + 2) Cookie authentication + 3) Basic HTTP authentication + + + +----------------------- +1. Form authentication +---------------------- + +With form authentication, skipfish will submit credentials using the +given login form. The server is expected to reply with authenticated +cookies which will than be used during the rest of the scan. + +An example to login using this feature: + +$ ./skipfish --auth-form http://example.org/login \ + --auth-user myuser \ + --auth-pass mypass \ + --auth-verify-url http://example.org/profile \ + [...other options...] + +This is how it works: + +1. Upon start of the scan, the authentication form at /login will be + fetched by skipfish. We will try to complete the username and password + fields and submit the form. + +2. Once a server response is obtained, skipfish will fetch the + verification URL twice: once with the new session cookies and once + without any cookies. Both responses are expected to be different. + +3. During the scan, the verification URL will be used many times to + test whether we are authenticated. If at some point our session has + been terminated server-side, skipfish will re-authenticate using the + --auth-form (/login in our example) . + +Verifying whether the session is still active requires a good verification +URL where an authenticated request is going to get a different response +than an anonymous request. For example a 'profile' or 'my account' page. + +Troubleshooting: +---------------- + +1. Login field names not recognized + + If the username and password form fields are not recognized, skipfish + will complain. In this case, you should specify the field names using + the --auth-user-field and --auth-pass-field flags. + +2. The form is not submitted to the right location + + If the login form doesn't specify an action="" location, skipfish + will submit the form's content to the form URL. This will fail in some + occasions. For example, when the login page uses Javascript to submit + the form to a different location. + + Use the --auth-form-target flag to specify the URL where you want skipfish + to submit the form to. + +3. Skipfish keeps getting logged out + + Make sure you blacklist any URLs that will log you out. For example, + using the " -X /logout" + + +------------------------- +2. Cookie authentication +------------------------- + +Alternatively, if the site relies on HTTP cookies you can also feed these +to skipfish manually. To do this log in using your browser or using a +simple curl script, and then provide skipfish with a session cookie: + +$ ./skipfish -C name=val [...other options...] + +Other session cookies may be passed the same way, one per each -C option. + +The -N option, which causes new cookies to be rejected by skipfish, +is almost always a good choice when running cookie authenticated scans +(e.g. to avoid your precious cookies from being overwritten). + +$ ./skipfish -N -C name=val [...other options...] + +----------------------------- +3. Basic HTTP authentication +----------------------------- + +For simple HTTP credentials, you can use the -A option to pass the +credentials. + +$ ./skipfish -A user:pass [...other options...] + diff --git a/dictionaries/README-FIRST b/doc/dictionaries.txt similarity index 100% rename from dictionaries/README-FIRST rename to doc/dictionaries.txt diff --git a/doc/signatures.txt b/doc/signatures.txt new file mode 100644 index 0000000..c648bb7 --- /dev/null +++ b/doc/signatures.txt @@ -0,0 +1,152 @@ + +----------------- + 1. Introduction +----------------- + +With skipfish signatures it is possible to find interesting content, +or even vulnerabilities, in server responses. The signatures follow +a Snort-like syntax and most keywords behave similarly as well. + +Signatures focus on detecting web application vulnerabilities, information +leaks and can recognize interesting web applications, such as phpmyadmin +or phpinfo() pages. + +Signatures could also detect vulnerable software packages (e.g. old +WordPress instances) but this is a task that fits vulnerability scanners, +like Nessus and Nikto, better. + +----------------- + 2. Contributing +----------------- + +The current signature list is nice but far from complete. If you have +new signatures or can optimize existing ones, please help out by reporting +this via our issue tracker: + +https://code.google.com/p/skipfish/issues/entry?template=Content%20signatures + +----------------------- + 3. Signature keywords +----------------------- + +=== content:[!]"" + +The content keyword is used to specify a string that we try to match +against the server response. The value can either be a static string or +a regular expression (the latter requires the type:regex; modifier). + +Multiple content strings can be specified per signature and, unless the +signature specifies a mime type, there should be at least one. + +Modifiers can be specified per content keyword to influence how the string +is matches against the payload. For example, with the 'depth' keyword +you can specify how far in the payload we should look for the string. + +When ! is specified before the content string, the test is positive when +the string is NOT present. This is mainly useful in case your signature +has multiple content values. + +Note: content string modifiers should be specified _after_ the content +string to which they apply. + +=== content modifier: depth: + +With depth you can limit the amount of bytes we should search for the +string. Initially the depth is relative to the beginning of the +payload. However, when multiple 'content' strings are used, the depth +is relative to the first byte after the previous content match. + +Using the depth keyword has two advantages: increase performance and +increase signature accuracy. + +1) Performance: A signature that matches on a tag doesn't need + to be applied to the whole payload. Instead, a depth of 512 or even + 1024 bytes will help to improve performance. + +2) Accuracy: In a signature with two 'content' keywords, you can force the + second keyword to be searched within a very short depth of the previous + content match. + +=== content modifier: offset:<int> + +The content string searching will start at the given offset value. For +the first content string this is relative to the beginning of the +payload. For the following content strings, this is relative to the +first byte of the last match. + +=== content modifier: type:["regex|static"] + +Indicates whether the content string should be treated as a regular +expression or a static string. Content strings are treated as static by +default so you can leave this keyword out unless you're using a regular +expression. + +In a signature that has multiple content strings, static strings can be +mixed with regular expressions. You'll likely get the best performance +by starting with a static string before applying a regular expression. + +=== mime:"<string>" + +The given value will be compared with the MIME type specified by the +server. This is a "begins with" comparison so a partial MIME string, +like "javascript/" will match with a server value of "javascript/foo". + +=== memo:"<string>" + +The memo message is displayed in the report when the signature +matches. The content should be a short but meaningful problem title. + +=== sev:[1-4] + +The severity with which a signature match should be reported where: + + - 1 is High + - 2 is Medium + - 3 is Low + - 4 is Info (default) + +=== prob:"<string>" + +All issue types are defined in database.h and, by default, signature +matches are reported with generic (signature) issue types. + +Using the prob keyword, a signature match can be reported as any other +known issue. For example, issue 40401 stands for interesting files and +is already used for several signatures. + +The advantage of using an existing issue ID is that it's severity and +description will be used to report the signature match. + +=== check:<int> + +Injection tests have their own ID which are specified in checks.h. Using +the "check" keyword, it is possible to bind a signature to a specific +injection test. + +The idea is to allow context specific signatures to be written. Take the +following scenario as an example: During a scan, file disclosure tests +might not fully succeed to highlight a vulnerability. Errors thrown +during these tests can still reveal that there is more than likely a +file disclosure problem. While generic server error detection will +highlight these errors, it is more useful if we can detect that these +errors are related to our tests and report them as such. + +=== id:<int> + +The unique signature ID. Currently this is for documentation purpose only +but in the future we'll probably add signature chaining which requires +unique ID's as well. + + + +----------------------- + 3. Upcoming keywords +----------------------- + +Amongst other changes, it's likely that the next release will have the +following keywords implemented: + +1) nocase - for case insensitive matching +2) ssl - match against SSL responses only +3) header - match against a specific header + diff --git a/skipfish.1 b/doc/skipfish.1 similarity index 100% rename from skipfish.1 rename to doc/skipfish.1 diff --git a/signatures/apps.sigs b/signatures/apps.sigs new file mode 100644 index 0000000..663855f --- /dev/null +++ b/signatures/apps.sigs @@ -0,0 +1,13 @@ + +# ############################################## +# Detect interesting apps / pages that leak info +# and where their exposure is a security risk by +# default. + +# A phpinfo() page +id:11001; sev:3; content:"<title>phpinfo()phpMyAdmin '; depth:1024; content:'

") || @@ -2493,6 +2392,9 @@ static void check_for_stuff(struct http_request* req, strstr((char*)sniffbuf, "

Index of /") || strstr((char*)sniffbuf, ">[To Parent Directory]<")) { problem(PROB_DIR_LIST, req, res, (u8*)"Directory listing", req->pivot, 0); + + /* Since we have the listing, we'll skip bruteforcing directory */ + req->pivot->no_fuzz = 3; return; } @@ -2516,61 +2418,12 @@ static void check_for_stuff(struct http_request* req, } - if (strstr((char*)sniffbuf, "pivot, 0); - return; - } - - /* This should also cover most cases of Perl, Python, etc. */ - - if (!prefix(sniffbuf, "#!/")) { - problem(PROB_FILE_POI, req, res, (u8*)"shell script", req->pivot, 0); - return; - } - - if (strstr((char*)res->payload, "payload, "?>") && - !strstr((char*)sniffbuf, "payload, "# ?>") && - !strstr((char*)res->payload, "pivot, 0); - return; - } - - if (strstr((char*)res->payload, "<%@") && strstr((char*)res->payload, "%>")) { - problem(PROB_FILE_POI, req, res, (u8*)"JSP source", req->pivot, 0); - return; - } - - if (strstr((char*)res->payload, "<%") && strstr((char*)res->payload, "%>")) { - problem(PROB_FILE_POI, req, res, (u8*)"ASP source", req->pivot, 0); - return; - } - - if (strstr((char*)sniffbuf, "\nimport java.")) { - problem(PROB_FILE_POI, req, res, (u8*)"Java source", req->pivot, 0); - return; - } - - if (strstr((char*)sniffbuf, "\n#include")) { - problem(PROB_FILE_POI, req, res, (u8*)"C/C++ source", req->pivot, 0); - return; - } - if (strstr((char*)res->payload, "End Sub\n") || strstr((char*)res->payload, "End Sub\r")) { problem(PROB_FILE_POI, req, res, (u8*)"Visual Basic source", req->pivot, 0); return; } - if (strstr((char*)sniffbuf, "0] \"GET /")) { - problem(PROB_FILE_POI, req, res, (u8*)"Apache server logs", req->pivot, 0); - return; - } - - if (strstr((char*)sniffbuf, "0, GET, /")) { - problem(PROB_FILE_POI, req, res, (u8*)"IIS server logs", req->pivot, 0); - return; - } /* Plain text, and every line contains ;, comma, or |? */ @@ -2604,13 +2457,6 @@ static void check_for_stuff(struct http_request* req, } - /* Excel is almost always interesting on its own. */ - - if (res->sniff_mime_id == MIME_EXT_EXCEL) { - problem(PROB_FILE_POI, req, res, (u8*)"Excel spreadsheet", req->pivot, 0); - return; - } - /* This is a bit dodgy, but the most prominent sign of non-browser JS on Windows is the instantiation of obscure ActiveX objects to access local filesystem, create documents, etc. Unfortunately, some sites may also be @@ -2633,17 +2479,6 @@ static void check_for_stuff(struct http_request* req, problem(PROB_FILE_POI, req, res, (u8*)"SQL script", req->pivot, 0); return; } - - if (inl_strcasestr(sniffbuf, (u8*)"@echo ")) { - problem(PROB_FILE_POI, req, res, (u8*)"DOS batch script", req->pivot, 0); - return; - } - - if (inl_strcasestr(res->payload, (u8*)"(\"Wscript.")) { - problem(PROB_FILE_POI, req, res, (u8*)"Windows shell script", req->pivot, 0); - return; - } - } diff --git a/analysis.h b/src/analysis.h similarity index 94% rename from analysis.h rename to src/analysis.h index 8e57611..6d4dcbf 100644 --- a/analysis.h +++ b/src/analysis.h @@ -76,6 +76,23 @@ u8 content_checks(struct http_request* req, struct http_response* res); void maybe_delete_payload(struct pivot_desc* pv); + + +/* Examines all tags up until , then adds them as + parameters to current request. */ + +void collect_form_data(struct http_request* req, + struct http_request* orig_req, + struct http_response* orig_res, + u8* cur_str, u8 is_post); + + +/* Create a http_request from an HTML form structure */ + +struct http_request* make_form_req(struct http_request *req, + struct http_request *base, + u8* cur_str, u8* target); + /* MIME detector output codes: */ #define MIME_NONE 0 /* Checks missing or failed */ diff --git a/src/auth.c b/src/auth.c new file mode 100644 index 0000000..edf17e2 --- /dev/null +++ b/src/auth.c @@ -0,0 +1,279 @@ +/* + skipfish - form authentication + ------------------------------ + + Author: Niels Heinen + + Copyright 2012 by Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +#include + +#define _VIA_AUTH_C + +#include "debug.h" +#include "config.h" +#include "types.h" +#include "http_client.h" +#include "database.h" +#include "crawler.h" +#include "analysis.h" +#include "auth.h" + +u8 *auth_form; /* Auth form location */ +u8 *auth_form_target; /* Auth form submit target */ +u8 *auth_user; /* User name */ +u8 *auth_user_field; /* Username field id */ +u8 *auth_pass; /* Password */ +u8 *auth_pass_field; /* Password input field id */ +u8 *auth_verify_url; /* Auth verify URL */ +u8 auth_state; /* Stores the auth state */ + +void authenticate() { + + struct http_request* req; + + DEBUGC(L1, "*- Authentication starts\n"); + + if (!auth_form || !auth_user || !auth_pass) + return; + + struct pivot_desc *fake = ck_alloc(sizeof(struct pivot_desc)); + fake->type = PIVOT_FILE; + fake->state = PSTATE_FETCH; + + /* When in session, do nothing */ + if (auth_state != ASTATE_NONE) + return; + + auth_state = ASTATE_START; + req = ck_alloc(sizeof(struct http_request)); + + /* Create a request struct. Note that in this case, we don't care about + * whether the URL is whitelisted or not */ + if (parse_url(auth_form, req, NULL)) + FATAL("Auth form URL could not be parsed\n"); + + req->pivot = fake; + req->callback = submit_auth_form; + + async_request(req); + +} + +/* Main function to submit the authentication, login form. This function + will try find the right form and , unless form fields are specified on + command-line, try to find the right fields in order to store the username + and password. */ + +u8 submit_auth_form(struct http_request* req, + struct http_response* res) { + + u8* form; + u8 *vurl = NULL; + u8 is_post = 1; + u8 par_type = PARAM_POST; + u32 i = 0, k = 0; + struct http_request* n = NULL; + + DEBUG_CALLBACK(req, res); + + /* Loop over the forms till we get our password form */ + + do { + + form = inl_strcasestr(res->payload, (u8*)"method && !strcmp((char*)n->method, "POST")); + par_type = is_post ? PARAM_POST : PARAM_QUERY; + + n->pivot = req->pivot; + collect_form_data(n, req, res, form, is_post); + + /* If the form field was specified per command-line, we'll check if + it's present. When it's not present: move on to next form. + + Now when no form field was specified via command-line: try + to find one by using the strings from the "user_fields" array + (defined in auth.h). + */ + if (auth_user_field) + if(!get_value(par_type, auth_user_field, 0, &n->par)) + continue; + + if (auth_pass_field) + if(!get_value(par_type, auth_pass_field, 0, &n->par)) + continue; + + /* Try to find a user name-like field */ + for (i=0; ipar.c; i++) { + if (!n->par.n[i] || n->par.t[i] != par_type) continue; + + /* Find and set the user field */ + for (k=0; !auth_user_field && user_fields[k]; k++) { + if (inl_strcasestr(n->par.n[i], (u8*)user_fields[k])) { + DEBUGC(L1, "*-- Authentication - using user field: %s\n", n->par.n[i]); + if (n->par.v[i]) ck_free(n->par.v[i]); + n->par.v[i] = ck_strdup(auth_user); + auth_user_field = n->par.n[i]; + break; + } + } + /* Find and set the password field */ + for (k=0; !auth_pass_field && pass_fields[k]; k++) { + if (inl_strcasestr(n->par.n[i], (u8*)pass_fields[k])) { + DEBUGC(L1, "*-- Authentication - using pass field: %s\n", n->par.n[i]); + if (n->par.v[i]) ck_free(n->par.v[i]); + n->par.v[i] = ck_strdup(auth_pass); + auth_pass_field = n->par.n[i]; + break; + } + } + } + + /* If one of both fields is not set, there is no point in submitting + so we'll look for another form in the page */ + if (!auth_pass_field || !auth_user_field) + continue; + + n->callback = auth_form_callback; + DEBUGC(L1, "*-- Submitting authentication form\n"); +#ifdef LOG_STDERR + dump_http_request(n); +#endif + async_request(n); + auth_state = ASTATE_SEND; + break; + } while (form); + + if (auth_state != ASTATE_SEND) + DEBUGC(L1, "*-- Could not login. Please check the URL and form fields\n"); + + return 0; +} + +/* After submitting the form and receiving a response, this is called */ + +u8 auth_form_callback(struct http_request* req, + struct http_response* res) { + + DEBUG_CALLBACK(req, res); + DEBUGC(L1, "*-- Received form response\n"); + + /* Parse the payload which will make sure cookies are stored. */ + content_checks(req, res); + + /* Compare an authenticated and anonymous request to the verification URL. The + * response should be different in order to determine that we are indeed + * authenticated */ + + if (!auth_verify_url) { + auth_state = ASTATE_DONE; + return 0; + } + + auth_state = ASTATE_VERIFY; + auth_verify_tests(req->pivot); + + return 0; +} + +/* Sends two requests to the verification URL. The first request is + authenticated (or should be) while the second request is anonymous */ + +u8 auth_verify_tests(struct pivot_desc* pivot) { + + /* When we have no verification URL or the scan is no authenticated: + return */ + + DEBUG("In auth verify\n"); + + if (!auth_verify_url || (auth_state != ASTATE_DONE && + auth_state != ASTATE_VERIFY)) + return 1; + + u8* vurl = ck_strdup(auth_verify_url); + struct http_request *n = ck_alloc(sizeof(struct http_request)); + + n->pivot = pivot; + if (parse_url(vurl, n, NULL)) + FATAL("Unable to parse verification URL: %s\n", vurl); + + /* One: authenticated request */ + n->callback = auth_verify_checks; + n->user_val = 0; + async_request(n); + + /* Two: anonymous request */ + n = req_copy(n, pivot, 1); + n->no_cookies = 1; + n->user_val = 1; + n->callback = auth_verify_checks; + async_request(n); + + return 0; +} + +/* Receives two requests to the verification URL. If there is a difference, than + we'll trust that it's because one request was authenticated while the other + wasn't */ + +u8 auth_verify_checks(struct http_request* req, struct http_response* res) { + DEBUG_CALLBACK(req, res); + + if (FETCH_FAIL(res)) { + handle_error(req, res, (u8*)"during auth verification tests", 0); + return 0; + } + + req->pivot->misc_req[req->user_val] = req; + req->pivot->misc_res[req->user_val] = res; + + /* We need two responses */ + if ((++req->pivot->misc_cnt) != 2) return 1; + + /* Compare the two response. The authenticates response should be + different to the anonymous request */ + + if (same_page(&MRES(0)->sig, &MRES(1)->sig)) { + DEBUGC(L1, "*- Unable to verify authentication using provided URL.\n"); + dump_signature(&MRES(0)->sig); + dump_signature(&MRES(1)->sig); + auth_state = ASTATE_FAIL; + } + + + destroy_misc_data(req->pivot, req); + + /* Re-authenticate upon failure */ + if (auth_state == ASTATE_FAIL) { + authenticate(); + DEBUG("* Going to re-authenticate\n"); + } else { + auth_state = ASTATE_DONE; + DEBUGC(L1, "*- Authenticated\n"); + } + + return 0; +} diff --git a/src/auth.h b/src/auth.h new file mode 100644 index 0000000..41dc0c8 --- /dev/null +++ b/src/auth.h @@ -0,0 +1,74 @@ +/* + skipfish - form authentication matching + ---------------------------------------- + + Author: Niels Heinen + + Copyright 2012 by Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +#ifndef _HAVE_AUTH_H + +void authenticate(); + +u8 submit_auth_form(struct http_request* req, + struct http_response* res); + +u8 auth_form_callback(struct http_request* req, + struct http_response* res); + +u8 auth_verify_tests(struct pivot_desc* pivot); +u8 auth_verify_checks(struct http_request* req, struct http_response* res); + +extern u8 *auth_form, /* Auth form location */ + *auth_form_target, /* Auth form submit target */ + *auth_user, /* User name */ + *auth_pass, /* Password */ + *auth_user_field, /* Username field id */ + *auth_pass_field, /* Password input field id */ + *auth_verify_url; /* Auth verify URL */ + +extern u8 auth_state; + +#define ASTATE_NONE 0 +#define ASTATE_START 1 +#define ASTATE_SEND 2 +#define ASTATE_VERIFY 3 +#define ASTATE_DONE 4 +#define ASTATE_FAIL 5 + +#ifdef _VIA_AUTH_C + +/* These strings are used to find the username field */ + +static const char* user_fields[] = { + "user", + "name", + "email", + 0 +}; + +/* These strings are used to find the password field */ + +static const char* pass_fields[] = { + "pass", + "secret", + "pin", + 0 +}; + +#endif /* !_VIA_AUTH_C */ +#endif /* !_HAVE_AUTH_H */ diff --git a/checks.c b/src/checks.c similarity index 75% rename from checks.c rename to src/checks.c index 41129f8..18d332c 100644 --- a/checks.c +++ b/src/checks.c @@ -28,6 +28,7 @@ #include "analysis.h" #include "http_client.h" #include "checks.h" +#include "auth.h" @@ -46,6 +47,9 @@ static u8 inject_xss_check(struct http_request*, struct http_response*); static u8 inject_shell_tests(struct pivot_desc* pivot); static u8 inject_shell_check(struct http_request*, struct http_response*); +static u8 inject_diff_shell_tests(struct pivot_desc* pivot); +static u8 inject_diff_shell_check(struct http_request*, struct http_response*); + static u8 inject_dir_listing_tests(struct pivot_desc* pivot); static u8 inject_dir_listing_check(struct http_request*, struct http_response*); @@ -79,6 +83,8 @@ static u8 param_behavior_check(struct http_request*, struct http_response*); static u8 param_ognl_tests(struct pivot_desc* pivot); static u8 param_ognl_check(struct http_request*, struct http_response*); +static u8 xssi_tests(struct pivot_desc* pivot); +static u8 xssi_check(struct http_request*, struct http_response*); @@ -88,11 +94,12 @@ static u8 param_ognl_check(struct http_request*, struct http_response*); 1- Amount of responses expected 2- Whether to keep requests and responses before calling the check 3- Whether the check accepted pivots with res_varies set - 4- Whether we should scrape the response for links. - 5- The type of PIVOT that the test/check accepts - 6- Pointer to the function that scheduled the test(s) requests - 7- Pointer to the function that checks the result - 8- Whether to skip this test + 4- Whether the check is time sensitive + 5- Whether we should scrape the response for links. + 6- The type of PIVOT that the test/check accepts + 7- Pointer to the function that scheduled the test(s) requests + 8- Pointer to the function that checks the result + 9- Whether to skip this test At the end, inject_done() is called: - we move on with additional tests (e.g. parameter) @@ -104,67 +111,94 @@ static u8 param_ognl_check(struct http_request*, struct http_response*); */ -u32 cb_handle_cnt = 16; /* Total of checks */ -u32 cb_handle_off = 3; /* Checks after the offset are optional */ +u32 cb_handle_cnt = 19; /* Total of checks */ +u32 cb_handle_off = 4; /* Checks after the offset are optional */ static struct cb_handle cb_handles[] = { + /* Authentication check */ + { 2, 0, 0, 0, 0, 0, + CHK_SESSION, (u8*)"session check", + auth_verify_tests, auth_verify_checks, 0 }, /* Behavior checks for dirs/params */ - - { BH_CHECKS, 0, 0, 1, PIVOT_PARAM, (u8*)"param behavior", + { BH_CHECKS, 1, 0, 0, 2, PIVOT_PARAM, + CHK_BEHAVE, (u8*)"param behavior", param_behavior_tests, param_behavior_check, 0 }, - { 2, 1, 0, 1, PIVOT_PARAM, (u8*)"param OGNL", + { 2, 1, 0, 0, 2, PIVOT_PARAM, + CHK_OGNL, (u8*)"param OGNL", param_ognl_tests, param_ognl_check, 0 }, - { BH_CHECKS, 1, 0, 1, PIVOT_DIR|PIVOT_FILE, (u8*)"inject behavior", - inject_behavior_tests, inject_behavior_check, 0 }, + { BH_CHECKS, 1, 0, 0, 2, PIVOT_DIR|PIVOT_FILE, + CHK_BEHAVE, (u8*)"inject behavior", + inject_behavior_tests, inject_behavior_check, 0 }, /* All the injection tests */ + { 2, 1, 0, 0, 2, PIVOT_DIR, + CHK_IPS, (u8*)"IPS check", + dir_ips_tests, dir_ips_check, 0 }, - { 2, 1, 0, 1, PIVOT_DIR, (u8*)"IPS check", - dir_ips_tests, dir_ips_check, 0 }, + { 2, 1, 0, 0, 0, PIVOT_DIR|PIVOT_SERV, + CHK_PUT, (u8*)"PUT upload", + put_upload_tests, put_upload_check, 0 }, - { 2, 1, 0, 0, PIVOT_DIR|PIVOT_SERV, (u8*)"PUT upload", - put_upload_tests, put_upload_check, 0 }, - - { 4, 1, 0, 0, PIVOT_DIR|PIVOT_PARAM, (u8*)"dir traversal", - inject_dir_listing_tests, inject_dir_listing_check, 0 }, + { 4, 1, 0, 0, 1, PIVOT_DIR|PIVOT_PARAM, + CHK_DIR_LIST, (u8*)"dir traversal", + inject_dir_listing_tests, inject_dir_listing_check, 0 }, #ifdef RFI_SUPPORT - { 12, 1, 1, 0, 0, (u8*)"file inclusion", - inject_inclusion_tests, inject_inclusion_check, 0 }, + { 12, 1, 1, 0, 1, 0, + CHK_FI, (u8*)"file inclusion", + inject_inclusion_tests, inject_inclusion_check, 0 }, #else - { 11, 1, 1, 0, 0, (u8*)"file inclusion", - inject_inclusion_tests, inject_inclusion_check, 0 }, + { 11, 1, 1, 0, 1, 0, + CHK_FI, (u8*)"file inclusion", + inject_inclusion_tests, inject_inclusion_check, 0 }, #endif - { 3, 0, 1, 0, 0, (u8*)"XSS injection", - inject_xss_tests, inject_xss_check, 0 }, + { 4, 0, 1, 0, 1, 0, + CHK_XSS, (u8*)"XSS injection", + inject_xss_tests, inject_xss_check, 0 }, - { 0, 0, 1, 0, 0, (u8*)"prologue injection", - inject_prologue_tests, inject_prologue_check, 0 }, + { 1, 1, 1, 0, 1, 0, + CHK_XSSI, (u8*)"XSSI protection", + xssi_tests, xssi_check, 0 }, - { 2, 1, 1, 0, 0, (u8*)"Header injection", - inject_split_tests, inject_split_check, 0 }, + { 0, 0, 1, 0, 1, 0, + CHK_PROLOG, (u8*)"prologue injection", + inject_prologue_tests, inject_prologue_check, 0 }, - { 4, 1, 1, 0, PIVOT_PARAM, (u8*)"Redirect injection", - inject_redir_tests, inject_redir_check, 0 }, + { 2, 1, 1, 0, 1, 0, + CHK_RSPLIT, (u8*)"Header injection", + inject_split_tests, inject_split_check, 0 }, - { 10, 1, 0, 0, 0, (u8*)"SQL injection", - inject_sql_tests, inject_sql_check, 0 }, + { 5, 1, 1, 0, 1, PIVOT_PARAM, + CHK_REDIR, (u8*)"Redirect injection", + inject_redir_tests, inject_redir_check, 0 }, - { 2, 1, 0, 0, 0, (u8*)"XML injection", - inject_xml_tests, inject_xml_check, 0 }, + { 10, 1, 0, 0, 1, 0, + CHK_SQL, (u8*)"SQL injection", + inject_sql_tests, inject_sql_check, 0 }, - { 12, 1, 0, 0, 0, (u8*)"Shell injection", - inject_shell_tests, inject_shell_check, 0 }, + { 2, 1, 0, 0, 1, 0, + CHK_XML, (u8*)"XML injection", + inject_xml_tests, inject_xml_check, 0 }, - { 2, 1, 0, 0, 0, (u8*)"format string", - inject_format_tests, inject_format_check, 1 }, + { 12, 1, 0, 0, 1, 0, + CHK_SHELL_DIFF, (u8*)"Shell injection (diff)", + inject_diff_shell_tests, inject_diff_shell_check, 0 }, - { 9, 1, 0, 0, 0, (u8*)"integer handling", - inject_integer_tests, inject_integer_check, 1 } + { 12, 1, 1, 1, 1, 0, + CHK_SHELL_SPEC, (u8*)"Shell injection (spec)", + inject_shell_tests, inject_shell_check, 0 }, + + { 2, 1, 0, 0, 1, 0, + CHK_FORMAT, (u8*)"format string", + inject_format_tests, inject_format_check, 1 }, + + { 9, 1, 0, 0, 1, 0, + CHK_INTEGER, (u8*)"integer handling", + inject_integer_tests, inject_integer_check, 1 } }; @@ -175,21 +209,29 @@ void display_injection_checks(void) { SAY("\n[*] Available injection tests:\n\n"); for (i=cb_handle_off; i cb_handle_cnt) FATAL("Unable to parse checks toggle string"); - tnr += cb_handle_off; + tnr += offset; /* User values are array index nr + 1 */ - if (tnr > cb_handle_off && tnr < cb_handle_cnt) { - if (enable && cb_handles[tnr].skip) { - cb_handles[tnr].skip = 0; - DEBUG(" Enabled test: %d : %s\n", tnr, cb_handles[tnr].name); - } else { - cb_handles[tnr].skip = 1; - DEBUG(" Disabled test: %d : %s\n", tnr, cb_handles[tnr].name); - } + if (enable && cb_handles[tnr].skip) { + cb_handles[tnr].skip = 0; + DEBUG(" Enabled test: %d : %s\n", tnr, cb_handles[tnr].name); + } else { + cb_handles[tnr].skip = 1; + DEBUG(" Disabled test: %d : %s\n", tnr, cb_handles[tnr].name); } ptr = (u8*)strtok(NULL, ","); } + + ck_free(ids); } /* The inject state manager which uses the list ot check structs to @@ -278,6 +320,21 @@ u8 inject_state_manager(struct http_request* req, struct http_response* res) { DEBUG_STATE_CALLBACK(req, cb_handles[check].name, 1); + /* Check if we got all responses to avoid handing over NULL poiners to the + * checks() functions */ + + if (cb_handles[check].res_keep) { + for (i=0; ipivot->misc_cnt; i++) { + if (!MREQ(i) || !MRES(i)) { + problem(PROB_FETCH_FAIL, req, res, (u8*)"During injection testing", req->pivot, 0); + + /* Today, we'll give up on this test. In the next release: reschedule */ + goto content_checks; + } + + } + } + if (cb_handles[check].checks(req,res)) return 1; @@ -285,6 +342,8 @@ u8 inject_state_manager(struct http_request* req, struct http_response* res) { (cb_handles[check].res_num && ++req->pivot->misc_cnt != cb_handles[check].res_num)) return 0; +content_checks: + /* If we get here, we're done and can move on. First make sure that all responses have been checked. Than free memory and schedule the next test */ @@ -293,22 +352,25 @@ u8 inject_state_manager(struct http_request* req, struct http_response* res) { for (i=0; ipivot->misc_cnt; i++) { /* Only check content once */ - if (MRES(i)->stuff_checked) + if (!MRES(i) || !MREQ(i) || MRES(i)->stuff_checked) continue; - /* Only scrape for checks that want it */ - if (cb_handles[check].scrape) - scrape_response(MREQ(i), MRES(i)); - - /* Always do the content checks */ - content_checks(MREQ(i), MRES(i)); + /* Only scrape for checks that want it + 0 = don't scrape + 1 = check content + 2 = check content and extract links */ + if (cb_handles[check].scrape > 0) { + content_checks(MREQ(i), MRES(i)); + if (cb_handles[check].scrape == 2) + scrape_response(MREQ(i), MRES(i)); + } } } - destroy_misc_data(req->pivot, req); - schedule_tests: + destroy_misc_data(req->pivot, req); + check = ++req->pivot->check_idx; if (check < cb_handle_cnt) { @@ -316,7 +378,8 @@ schedule_tests: if (cb_handles[check].skip) goto schedule_tests; /* Move to the next test in case the page is unstable and the test doesn't want it. */ - if (req->pivot->res_varies && !cb_handles[check].allow_varies) + if ((req->pivot->res_varies && !cb_handles[check].allow_varies) || + (req->pivot->res_time_exceeds && cb_handles[check].time_sensitive)) goto schedule_tests; /* Move to the next test in case of pivot type mismatch */ @@ -326,7 +389,10 @@ schedule_tests: DEBUG_STATE_CALLBACK(req, cb_handles[check].name, 0); /* Do the tests and return upon success or move on to the next upon - a return value of 1 */ + a return value of 1. We store the ID of the check in the pivot to + allow other functions, that use the pivot, to find out the current + injection test */ + req->pivot->check_id = cb_handles[check].id; if (cb_handles[check].tests(req->pivot) == 1) goto schedule_tests; @@ -347,6 +413,69 @@ inject_done: return 0; } + +static u8 xssi_tests(struct pivot_desc* pv) { + struct http_request* n; + + DEBUG_HELPER(pv); + + /* We only want Javascript that does not have inclusion protection. This + * test, should be moved to the injection manager whenever we have more + * content specific tests (e.g. css ones) */ + + if(pv->res->js_type != 2 || pv->res->json_safe) + return 1; + + n = req_copy(pv->req, pv, 1); + n->callback = inject_state_manager; + n->no_cookies = 1; + async_request(n); + + return 0; + +} + +static u8 xssi_check(struct http_request* req, + struct http_response* res) { + + DEBUG_MISC_CALLBACK(req, res); + + /* When the response with cookie is different from the cookie-less response, + * than the content is session depended. In case of Javascript without XSSI + * protection, this is more than likely an issue. */ + + + if (!same_page(&RPRES(req)->sig, &MRES(0)->sig)) { + + /* Responses that do not contain the term "function", "if", "for", "while", etc, + are much more likely to be dynamic JSON than just static scripts. Let's + try to highlight these. */ + + if ((!req->method || !strcmp((char*)req->method, "GET")) && + !inl_findstr(res->payload, (u8*)"if (", 2048) && + !inl_findstr(res->payload, (u8*)"if(", 2048) && + !inl_findstr(res->payload, (u8*)"for (", 2048) && + !inl_findstr(res->payload, (u8*)"for(", 2048) && + !inl_findstr(res->payload, (u8*)"while (", 2048) && + !inl_findstr(res->payload, (u8*)"while(", 2048) && + !inl_findstr(res->payload, (u8*)"function ", 2048) && + !inl_findstr(res->payload, (u8*)"function(", 2048)) { + + problem(PROB_JS_XSSI, req, res, (u8*)"Cookie-less JSON is different", req->pivot, 0); + + } else { + problem(PROB_JS_XSSI, req, res, (u8*)"Cookie-less Javascript response is different", req->pivot, 0); + } + } + + /* Now this is interesting. We can lookup the issues in the pivot and if + * analysis.c thinks this page has an XSSI, we can kill that assumption */ + + remove_issue(req->pivot, PROB_JS_XSSI); + + return 0; +} + static u8 inject_behavior_tests(struct pivot_desc* pv) { struct http_request* n; u32 i; @@ -371,14 +500,11 @@ static u8 inject_behavior_check(struct http_request* req, /* pv->state may change after async_request() calls in insta-fail mode, so we should cache accordingly. */ - DEBUG_MISC_CALLBACK(req, res); + DEBUG_CALLBACK(req, res); for (i=0; ipivot->misc_cnt; i++) { - if (!same_page(&RPRES(req)->sig, &MRES(i)->sig)) { - req->pivot->res_varies = 1; problem(PROB_VARIES, MREQ(i), MRES(i), 0, MREQ(i)->pivot, 0); - /* Done, it varies so we can continue */ return 0; } } @@ -503,10 +629,168 @@ static u8 inject_xml_check(struct http_request* req, return 0; } + static u8 inject_shell_tests(struct pivot_desc* pivot) { /* Shell command injection - 12 requests. */ + u32 orig_state = pivot->state; + u8* tmp; + struct http_request* n; + + n = req_copy(pivot->req, pivot, 1); + SET_VECTOR(orig_state, n, (u8*)"`echo skip12``echo 34fish`"); + n->callback = inject_state_manager; + n->user_val = 0; + async_request(n); + + n = req_copy(pivot->req, pivot, 1); + APPEND_VECTOR(orig_state, n, (u8*)"`echo skip12``echo 34fish`"); + n->callback = inject_state_manager; + n->user_val = 1; + async_request(n); + + n = req_copy(pivot->req, pivot, 1); + SET_VECTOR(orig_state, n, (u8*)"`echo${IFS}skip12``echo${IFS}34fish`"); + n->callback = inject_state_manager; + n->user_val = 2; + async_request(n); + + n = req_copy(pivot->req, pivot, 1); + APPEND_VECTOR(orig_state, n, (u8*)"`echo${IFS}skip12``echo${IFS}34fish`"); + n->callback = inject_state_manager; + n->user_val = 3; + async_request(n); + + /* We use the measured time_base as an offset for the sleep test. The + value is limited to MAX_RES_DURATION and the result is < 10 */ + + tmp = ck_alloc(10); + sprintf((char*)tmp, (char*)"`sleep %d`", pivot->res_time_base + SLEEP_TEST_ONE); + + n = req_copy(pivot->req, pivot, 1); + APPEND_VECTOR(orig_state, n,tmp ); + n->callback = inject_state_manager; + n->user_val = 4; + async_request(n); + + n = req_copy(pivot->req, pivot, 1); + SET_VECTOR(orig_state, n, tmp); + n->callback = inject_state_manager; + n->user_val = 5; + async_request(n); + + sprintf((char*)tmp, (char*)"`sleep %d`", pivot->res_time_base + SLEEP_TEST_TWO); + + n = req_copy(pivot->req, pivot, 1); + APPEND_VECTOR(orig_state, n, tmp); + n->callback = inject_state_manager; + n->user_val = 6; + async_request(n); + + n = req_copy(pivot->req, pivot, 1); + SET_VECTOR(orig_state, n, tmp); + n->callback = inject_state_manager; + n->user_val = 7; + async_request(n); + + tmp = ck_realloc(tmp, 15); + sprintf((char*)tmp, (char*)"`sleep${IFS}%d`", pivot->res_time_base + SLEEP_TEST_ONE); + + + n = req_copy(pivot->req, pivot, 1); + APPEND_VECTOR(orig_state, n, tmp); + n->callback = inject_state_manager; + n->user_val = 8; + async_request(n); + + n = req_copy(pivot->req, pivot, 1); + SET_VECTOR(orig_state, n, tmp); + n->callback = inject_state_manager; + n->user_val = 9; + async_request(n); + + sprintf((char*)tmp, (char*)"`sleep${IFS}%d`", pivot->res_time_base + SLEEP_TEST_TWO); + + n = req_copy(pivot->req, pivot, 1); + APPEND_VECTOR(orig_state, n, tmp); + n->callback = inject_state_manager; + n->user_val = 10; + async_request(n); + + + n = req_copy(pivot->req, pivot, 1); + SET_VECTOR(orig_state, n, tmp); + n->callback = inject_state_manager; + n->user_val = 11; + async_request(n); + + ck_free(tmp); + + return 0; +} + +static u8 inject_shell_check(struct http_request* req, + struct http_response* res) { + u32 i; + + DEBUG_MISC_CALLBACK(req, res); + + /* Look in the first 4 requests to find our concatenated string */ + + for (i = 0; i < 3; i++) { + if (inl_findstr(MRES(i)->payload, (u8*)"skip1234fish", 1024)) + problem(PROB_SH_INJECT, MREQ(i), MRES(i), + (u8*)"Confirmed shell injection (echo test)", req->pivot, 0); + } + + /* Check that the request was delayed by our sleep. The sleep delay is + calculated by using the time_base in order to avoid FPs */ + + u32 test_one = req->pivot->res_time_base + SLEEP_TEST_ONE; + u32 test_two = req->pivot->res_time_base + SLEEP_TEST_TWO; + + /* Now we check if the request duration was influenced by the sleep. We + do this by testing if the total request time was longer (or equal) + to: the average request time + the sleep time (3 or 5 seconds). + + We allow the `sleep` request to take 1 second longer than + expected which is the final measure to reduce FPs. + + */ + + if ((RTIME(4) >= test_one && RTIME(4) < test_one + 1) && + (RTIME(6) >= test_two && RTIME(6) < test_two + 1)) { + problem(PROB_SH_INJECT, MREQ(4), MRES(4), + (u8*)"Confirmed shell injection (sleep test)", req->pivot, 0); + } + + if ((RTIME(5) >= test_one && RTIME(5) < test_one + 1) && + (RTIME(7) >= test_two && RTIME(7) < test_two + 1)) { + problem(PROB_SH_INJECT, MREQ(5), MRES(5), + (u8*)"Confirmed shell injection (sleep test)", req->pivot, 0); + } + + if ((RTIME(8) >= test_one && RTIME(8) < test_one + 1) && + (RTIME(10) >= test_two && RTIME(10) < test_two + 1)) { + problem(PROB_SH_INJECT, MREQ(8), MRES(8), + (u8*)"Confirmed shell injection (sleep test)", req->pivot, 0); + } + + if ((RTIME(9) >= test_one && RTIME(9) < test_one + 1) && + (RTIME(11) >= test_two && RTIME(11) < test_two + 1)) { + problem(PROB_SH_INJECT, MREQ(9), MRES(9), + (u8*)"Confirmed shell injection (sleep test)", req->pivot, 0); + } + + return 0; +} + + +static u8 inject_diff_shell_tests(struct pivot_desc* pivot) { + + /* Shell command injection - 12 requests. */ + u32 orig_state = pivot->state; struct http_request* n; @@ -585,7 +869,8 @@ static u8 inject_shell_tests(struct pivot_desc* pivot) { return 0; } -static u8 inject_shell_check(struct http_request* req, + +static u8 inject_diff_shell_check(struct http_request* req, struct http_response* res) { DEBUG_MISC_CALLBACK(req, res); @@ -613,14 +898,14 @@ static u8 inject_shell_check(struct http_request* req, to avoid errors on search fields, etc. */ if (same_page(&MRES(0)->sig, &MRES(1)->sig) && - !same_page(&MRES(0)->sig, &MRES(2)->sig)) { - problem(PROB_SH_INJECT, MREQ(0), MRES(0), + !same_page(&MRES(1)->sig, &MRES(2)->sig)) { + problem(PROB_SH_INJECT, MREQ(1), MRES(1), (u8*)"responses to `true` and `false` different than to `uname`", req->pivot, 0); } if (same_page(&MRES(3)->sig, &MRES(4)->sig) && - !same_page(&MRES(3)->sig, &MRES(5)->sig)) { + !same_page(&MRES(4)->sig, &MRES(5)->sig)) { problem(PROB_SH_INJECT, MREQ(3), MRES(3), (u8*)"responses to `true` and `false` different than to `uname`", req->pivot, 0); @@ -634,7 +919,7 @@ static u8 inject_shell_check(struct http_request* req, } if (same_page(&MRES(9)->sig, &MRES(10)->sig) && - !same_page(&MRES(9)->sig, &MRES(11)->sig)) { + !same_page(&MRES(10)->sig, &MRES(11)->sig)) { problem(PROB_SH_INJECT, MREQ(9), MRES(9), (u8*)"responses to `true` and `false` different than to `uname`", req->pivot, 0); @@ -644,6 +929,7 @@ static u8 inject_shell_check(struct http_request* req, } + static u8 inject_xss_tests(struct pivot_desc* pivot) { /* Cross-site scripting - three requests (also test common @@ -668,7 +954,7 @@ static u8 inject_xss_tests(struct pivot_desc* pivot) { n->user_val = 1; async_request(n); - /* A last one with only header injections. The User-Agent injection + /* A last ones with only header injections. The User-Agent injection doesn't seems to be very useful for reflective XSS scenario's but could reveal persistant XSS problems (i.e. in log / backend interfaces) */ @@ -681,10 +967,20 @@ static u8 inject_xss_tests(struct pivot_desc* pivot) { n->user_val = 2; async_request(n); + /* One for testing HTTP_HOST XSS types which are somewhat unlikely + but still have abuse potential (e.g. stored XSS') */ + n = req_copy(pivot->req, pivot, 1); + set_value(PARAM_HEADER, (u8*)"Host", new_xss_tag(NULL), 0, &n->par); + register_xss_tag(n); + n->callback = inject_state_manager; + n->user_val = 3; + async_request(n); + + /* Finally we tests the cookies, one by one to avoid breaking the session */ - uval = 2; + uval = 3; for (i=0;ipayload, (u8*)"127.0.0.1", 1024)) { problem(PROB_FI_LOCAL, MREQ(4), MRES(4), (u8*)"response resembles /etc/hosts (via file://)", req->pivot, 0); - } + } else not_found++; } /* Check on the /etc/passwd file disclosure */ @@ -962,21 +1260,21 @@ static u8 inject_inclusion_check(struct http_request* req, } else if (inl_findstr(MRES(9)->payload, (u8*)"root:x:0:0:root", 1024)) { problem(PROB_FI_LOCAL, MREQ(9), MRES(9), (u8*)"response resembles /etc/passwd (via file://)", req->pivot, 0); - } + } else not_found++; } /* Windows boot.ini disclosure */ if (!inl_findstr(RPRES(req)->payload, (u8*)"[boot loader]", 1024)) { - if (inl_findstr(MRES(4)->payload, (u8*)"[boot loader]", 1024)) { - problem(PROB_FI_LOCAL, MREQ(4), MRES(4), - (u8*)"response resembles c:\\boot.ini (via traversal)", req->pivot, 0); - } else if (inl_findstr(MRES(5)->payload, (u8*)"[boot loader]", 1024)) { - problem(PROB_FI_LOCAL, MREQ(5), MRES(5), - (u8*)"response resembles c:\\boot.ini (via traversal)", req->pivot, 0); - } else if (inl_findstr(MRES(10)->payload, (u8*)"[boot loader]", 1024)) { - problem(PROB_FI_LOCAL, MREQ(10), MRES(10), - (u8*)"response resembles c:\\boot.ini (via file://)", req->pivot, 0); - } + if (inl_findstr(MRES(4)->payload, (u8*)"[boot loader]", 1024)) { + problem(PROB_FI_LOCAL, MREQ(4), MRES(4), + (u8*)"response resembles c:\\boot.ini (via traversal)", req->pivot, 0); + } else if (inl_findstr(MRES(5)->payload, (u8*)"[boot loader]", 1024)) { + problem(PROB_FI_LOCAL, MREQ(5), MRES(5), + (u8*)"response resembles c:\\boot.ini (via traversal)", req->pivot, 0); + } else if (inl_findstr(MRES(10)->payload, (u8*)"[boot loader]", 1024)) { + problem(PROB_FI_LOCAL, MREQ(10), MRES(10), + (u8*)"response resembles c:\\boot.ini (via file://)", req->pivot, 0); + } else not_found++; } /* Check the web.xml disclosure */ @@ -987,9 +1285,13 @@ static u8 inject_inclusion_check(struct http_request* req, } else if (inl_findstr(MRES(7)->payload, (u8*)"", 1024)){ problem(PROB_FI_LOCAL, MREQ(7), MRES(7), (u8*)"response resembles ./WEB-INF/web.xml (via traversal)", req->pivot, 0); - } + } else not_found++; } + /* If we disclosed a file, than we can remove any present traversal + warnings, which in that case are just duplicate/noise */ + if (not_found != 4) + remove_issue(req->pivot, PROB_DIR_TRAVERSAL); #ifdef RFI_SUPPORT if (!inl_findstr(RPRES(req)->payload, (u8*)RFI_STRING, 1024) && @@ -1010,7 +1312,7 @@ static u8 inject_redir_tests(struct pivot_desc* pivot) { struct http_request* n; u32 orig_state = pivot->state; - /* XSS checks - 4 requests */ + /* XSS checks - 5 requests */ n = req_copy(pivot->req, pivot, 1); SET_VECTOR(orig_state, n, "http://skipfish.invalid/;?"); @@ -1036,6 +1338,16 @@ static u8 inject_redir_tests(struct pivot_desc* pivot) { n->user_val = 3; async_request(n); + /* Finally an encoded version which is aimed to detect injection + problems in JS handlers, such as onclick, which executes HTML encoded + strings. */ + + n = req_copy(pivot->req, pivot, 1); + SET_VECTOR(orig_state, n, "'skip'''"fish""""); + n->callback = inject_state_manager; + n->user_val = 4; + async_request(n); + return 0; } @@ -1294,7 +1606,7 @@ static u8 inject_sql_check(struct http_request* req, */ if (same_page(&MRES(0)->sig, &MRES(1)->sig) && - !same_page(&MRES(0)->sig, &MRES(2)->sig)) { + !same_page(&MRES(1)->sig, &MRES(2)->sig)) { problem(PROB_SQL_INJECT, MREQ(0), MRES(0), (u8*)"response suggests arithmetic evaluation on server side (type 1)", req->pivot, 0); @@ -1308,7 +1620,7 @@ static u8 inject_sql_check(struct http_request* req, } if (same_page(&MRES(3)->sig, &MRES(4)->sig) && - !same_page(&MRES(3)->sig, &MRES(5)->sig)) { + !same_page(&MRES(4)->sig, &MRES(5)->sig)) { problem(PROB_SQL_INJECT, MREQ(4), MRES(4), (u8*)"response to '\" different than to \\'\\\"", req->pivot, 0); } @@ -1531,7 +1843,6 @@ static u8 param_behavior_tests(struct pivot_desc* pivot) { n->user_val = i; async_request(n); } - return 0; } @@ -1539,9 +1850,36 @@ static u8 param_behavior_tests(struct pivot_desc* pivot) { static u8 param_behavior_check(struct http_request* req, struct http_response* res) { - DEBUG_CALLBACK(req, res); + u32 i; + u32 res_diff; + u32 page_diff = 0; - if (same_page(&res->sig, &RPRES(req)->sig)) { + DEBUG_MISC_CALLBACK(req, res); + + req->pivot->state = PSTATE_PAR_CHECK; + + for (i=0; ipivot->misc_cnt; i++) { + + /* Store the biggest response time */ + res_diff = MREQ(i)->end_time - MREQ(i)->start_time; + if(res_diff > req->pivot->res_time_base) + req->pivot->res_time_base = res_diff; + + /* Compare the page responses */ + if (!page_diff && !same_page(&MRES(i)->sig, &RPRES(req)->sig)) + page_diff = i; + } + + /* If the largest response time exceeded our threshold, we'll skip + the timing related tests */ + + if(req->pivot->res_time_base > MAX_RES_DURATION) { + problem(PROB_VARIES, req, res, (u8*)"Responses too slow for time sensitive tests", req->pivot, 0); + req->pivot->res_time_exceeds = 1; + } + + + if (page_diff == req->pivot->misc_cnt) { DEBUG("* Parameter seems to have no effect.\n"); req->pivot->bogus_par = 1; return 0; @@ -1572,11 +1910,9 @@ static u8 param_behavior_check(struct http_request* req, DEBUG("* Signature does not match previous responses, whoops.\n"); req->pivot->res_varies = 1; problem(PROB_VARIES, req, res, 0, req->pivot, 0); - return 0; } } - req->pivot->state = PSTATE_PAR_CHECK; return 0; } @@ -1589,7 +1925,7 @@ static u8 param_ognl_tests(struct pivot_desc* pivot) { /* All probes failed? Assume bogus parameter, what else to do... */ - if (!pivot->r404_cnt) + if (!pivot->r404_cnt) pivot->bogus_par = 1; /* If the parameter has an effect, schedule OGNL checks. */ diff --git a/checks.h b/src/checks.h similarity index 65% rename from checks.h rename to src/checks.h index c03e3e9..755ab4d 100644 --- a/checks.h +++ b/src/checks.h @@ -5,13 +5,13 @@ /* The init crawler structure which loads the test/check combos */ -void init_injection_checks(void); +void init_injection_checks(void); /* The crawler structure helper functions */ void display_injection_checks(void); void release_injection_checks(void); -void toggle_injection_checks(u8* str, u32 enable); +void toggle_injection_checks(u8* str, u32 enable, u8 user); extern u8 no_checks; @@ -21,16 +21,53 @@ extern u8 no_checks; u8 inject_state_manager(struct http_request* req, struct http_response* res); +/* Check identifiers which can be used by other parts of code to + see what the current *check* is. One specific location where this is + used is the signature matching code, */ + +#define CHK_GENERIC 0 +#define CHK_XML 1 +#define CHK_XSS 2 +#define CHK_SHELL_DIFF 3 +#define CHK_SHELL_SPEC 4 +#define CHK_SESSION 5 +#define CHK_DIR_LIST 6 +#define CHK_PUT 7 +#define CHK_FI 8 +#define CHK_RFI 9 +#define CHK_XSSI 10 +#define CHK_PROLOG 11 +#define CHK_REDIR 12 +#define CHK_SQL 13 +#define CHK_FORMAT 14 +#define CHK_INTEGER 15 +#define CHK_OGNL 16 +#define CHK_BEHAVE 17 +#define CHK_IPS 18 +#define CHK_RSPLIT 19 + #ifdef _VIA_CHECKS_C +/* Time attack knobs */ + +#define MAX_RES_DURATION 3 +#define SLEEP_TEST_ONE 3 +#define SLEEP_TEST_TWO 5 + +/* Helper for calculating the request time */ + +#define RTIME(_r) (MREQ(_r)->end_time - MREQ(_r)->start_time) + /* The test/check struct with pointers to callback functions */ struct cb_handle { u32 res_num; /* Amount of expected responses */ u32 res_keep; /* Bool for keeping req/res */ u8 allow_varies; /* Bool to accept pivots with res_varies */ + u8 time_sensitive; /* Bool for time sensitive tests */ u8 scrape; /* Scrape links, or not.. */ u32 pv_flag; /* Flag to match pivot type */ + u32 id; /* Flag to match pivot type */ u8* name; /* Name or title of the check */ u8 (*tests)(struct pivot_desc* pivot); diff --git a/config.h b/src/config.h similarity index 97% rename from config.h rename to src/config.h index a325b6e..3f37093 100644 --- a/config.h +++ b/src/config.h @@ -31,12 +31,15 @@ option in the command line. This mode will not work as expected for HTTPS requests at this time - sorry. */ -//#define PROXY_SUPPORT 1 +// #define PROXY_SUPPORT 1 /* Default paths to runtime files: */ #define ASSETS_DIR "assets" +/* Default signature file */ +#define SIG_FILE "signatures/signatures.conf" + /* Various default settings for HTTP client (cmdline override): */ #define MAX_CONNECTIONS 40 /* Simultaneous connection cap */ @@ -47,12 +50,12 @@ #define RW_TMOUT 10 /* Individual network R/W timeout */ #define RESP_TMOUT 20 /* Total request time limit */ #define IDLE_TMOUT 10 /* Connection tear down threshold */ -#define SIZE_LIMIT 200000 /* Response size cap */ +#define SIZE_LIMIT 400000 /* Response size cap */ #define MAX_GUESSES 256 /* Guess-based wordlist size limit */ /* HTTP client constants: */ -#define MAX_URL_LEN 1024 /* Maximum length of an URL */ +#define MAX_URL_LEN 2048 /* Maximum length of an URL */ #define MAX_DNS_LEN 255 /* Maximum length of a host name */ #define READ_CHUNK 4096 /* Read buffer size */ diff --git a/crawler.c b/src/crawler.c similarity index 99% rename from crawler.c rename to src/crawler.c index d5a8a75..6541a33 100644 --- a/crawler.c +++ b/src/crawler.c @@ -405,7 +405,6 @@ static u8 unknown_retrieve_check2(struct http_request*, struct http_response*); */ - static void dir_case_start(struct pivot_desc* pv) { u32 i, len; s32 last = -1; @@ -1176,6 +1175,7 @@ u8 dir_retrieve_check(struct http_request* req, struct http_response* res) { DEBUG_CALLBACK(req, res); + /* Error at this point means we should give up on other probes in this directory. */ @@ -1331,7 +1331,7 @@ schedule_next: req->pivot->r404_pending++; async_request(n); - + } ck_free(tmp); @@ -1538,13 +1538,22 @@ static void dir_dict_start(struct pivot_desc* pv) { } if (pv->no_fuzz) { - if (pv->no_fuzz == 1) - problem(PROB_LIMITS, pv->req, pv->res, - (u8*)"Recursion limit reached, not fuzzing", pv, 0); - else - problem(PROB_LIMITS, pv->req, pv->res, - (u8*)"Directory out of scope, not fuzzing", pv, 0); - param_start(pv); + switch(pv->no_fuzz) { + case 1: + problem(PROB_LIMITS, pv->req, pv->res, + (u8*)"Recursion limit reached, not fuzzing", pv, 0); + break; + + case 2: + problem(PROB_LIMITS, pv->req, pv->res, + (u8*)"Directory out of scope, not fuzzing", pv, 0); + param_start(pv); + break; + + case 3: + DEBUG("Skipping directory bruteforce (allows listing)"); + break; + } return; } diff --git a/crawler.h b/src/crawler.h similarity index 99% rename from crawler.h rename to src/crawler.h index 2ca0be7..d10a35c 100644 --- a/crawler.h +++ b/src/crawler.h @@ -33,6 +33,7 @@ void handle_error(struct http_request* req, struct http_response* res, u8* desc, void inject_done(struct pivot_desc*); void destroy_misc_data(struct pivot_desc* pv, struct http_request* self); struct pivot_desc* dir_parent(struct pivot_desc* pv); +void authenticate(); /* Internal helper macros: */ diff --git a/database.c b/src/database.c similarity index 96% rename from database.c rename to src/database.c index 3ef34a8..50b1cc3 100644 --- a/database.c +++ b/src/database.c @@ -406,6 +406,9 @@ void maybe_add_pivot(struct http_request* req, struct http_response* res, } + /* Store a reference in our the callers request struct. */ + req->pivot = cur; + /* At this point, 'cur' points to a newly created or existing node for the path element. If this element is parametric, make sure that its value is on the 'try' list. */ @@ -539,6 +542,10 @@ void maybe_add_pivot(struct http_request* req, struct http_response* res, } + /* Set the request pivot, if not set already */ + if(!req->pivot) + req->pivot = cur; + /* Done, at last! */ } @@ -571,6 +578,37 @@ u8* lookup_issue_title(u32 id) { return pstructs[i].title; } +/* Remove issue(s) of with the specified 'type' */ + +void remove_issue(struct pivot_desc *pv, u32 type) { + + u32 i, cnt = 0; + struct issue_desc *tmp = NULL; + + if (!pv->issue_cnt) return; + + for (i=0; iissue_cnt; i++) { + if (pv->issue[i].type == type) { + DEBUG("* Removing issue of type: %d\n", type); + if (pv->issue[i].req) destroy_request(pv->issue[i].req); + if (pv->issue[i].res) destroy_response(pv->issue[i].res); + } else { + tmp = ck_realloc(tmp, (cnt + 1) * sizeof(struct issue_desc)); + + tmp[cnt].type = pv->issue[i].type; + tmp[cnt].extra = pv->issue[i].extra; + tmp[cnt].req = pv->issue[i].req; + tmp[cnt].res = pv->issue[i].res; + cnt++; + } + } + + ck_free(pv->issue); + pv->issue = tmp; + pv->issue_cnt = cnt; +} + + /* Registers a problem, if not duplicate (res, extra may be NULL): */ void problem(u32 type, struct http_request* req, struct http_response* res, @@ -603,10 +641,10 @@ void problem(u32 type, struct http_request* req, struct http_response* res, #ifndef LOG_STDERR u8* url = serialize_path(req, 1, 1); u8* title = lookup_issue_title(type); - DEBUGC(L1, "\n--- NEW PROBLEM\n"); - DEBUGC(L1, " - type: %u, %s\n", type, title); - DEBUGC(L1, " - url: %s\n", url); - DEBUGC(L2, " - extra: %s\n", extra); + DEBUGC(L2, "\n--- NEW PROBLEM\n"); + DEBUGC(L2, " - type: %u, %s\n", type, title); + DEBUGC(L2, " - url: %s\n", url); + DEBUGC(L3, " - extra: %s\n", extra); ck_free(url); #endif /* LOG_STDERR */ @@ -645,7 +683,7 @@ u8 url_allowed_host(struct http_request* req) { if (pos && strlen((char*)req->host) == strlen((char*)allow_domains[i]) + (pos - req->host)) return 1; - + } else if (!strcasecmp((char*)req->host, (char*)allow_domains[i])) return 1; @@ -735,7 +773,7 @@ u8 param_allowed(u8* pname) { } -/* Compares the checksums for two responses: */ +/* Compares the fingerprints for two responses: */ u8 same_page(struct http_sig* sig1, struct http_sig* sig2) { u32 i, bucket_fail = 0; @@ -754,8 +792,7 @@ u8 same_page(struct http_sig* sig1, struct http_sig* sig2) { s32 diff = sig1->data[i] - sig2->data[i]; u32 scale = sig1->data[i] + sig2->data[i]; - if (abs(diff) > 1 + (scale * FP_T_REL / 100) || - abs(diff) > FP_T_ABS) + if (abs(diff) > 1 + (scale * FP_T_REL / 100)) if (++bucket_fail > FP_B_FAIL) return 0; total_diff += diff; @@ -807,8 +844,7 @@ void debug_same_page(struct http_sig* sig1, struct http_sig* sig2) { s32 diff = sig1->data[i] - sig2->data[i]; u32 scale = sig1->data[i] + sig2->data[i]; - if (abs(diff) > 1 + (scale * FP_T_REL / 100) || - abs(diff) > FP_T_ABS) + if (abs(diff) > 1 + (scale * FP_T_REL / 100)) DEBUG("[FAIL] "); else DEBUG("[pass] "); total_diff += diff; @@ -825,13 +861,9 @@ void debug_same_page(struct http_sig* sig1, struct http_sig* sig2) { DEBUG("(allow)\n"); - DEBUG("Total diff: %d, scale %d, allow %d. ", + DEBUG("Total diff: %d, scale %d, allow %d\n", abs(total_diff), total_scale, 1 + (u32)(total_scale * FP_T_REL / 100)); - DEBUG("Both pages have text: "); - if (sig1->has_text != sig2->has_text) - DEBUG("no\n"); else DEBUG("yes\n"); - #endif /* LOG_STDERR */ } @@ -1321,13 +1353,14 @@ void database_stats() { cGRA " Issues found : " cNOR "%u info, %u warn, %u low, %u medium, " "%u high impact\n" cGRA " Dict size : " cNOR "%u words (%u new), %u extensions, " - "%u candidates\n", + "%u candidates\n" + cGRA " Signatures : " cNOR "%u total\n", pivot_cnt, pivot_done, pivot_cnt ? ((100.0 * pivot_done) / (pivot_cnt)) : 0, pivot_pending, pivot_init, pivot_attack, pivot_bf, pivot_missing, pivot_serv, pivot_dir, pivot_file, pivot_pinfo, pivot_unknown, pivot_param, pivot_value, issue_cnt[1], issue_cnt[2], issue_cnt[3], issue_cnt[4], issue_cnt[5], keyword_total_cnt, keyword_total_cnt - - keyword_orig_cnt, wg_extension_cnt, guess_cnt); + keyword_orig_cnt, wg_extension_cnt, guess_cnt, slist_cnt); } diff --git a/database.h b/src/database.h similarity index 95% rename from database.h rename to src/database.h index c991ae6..412517c 100644 --- a/database.h +++ b/src/database.h @@ -106,6 +106,8 @@ struct pivot_desc { u8 res_varies; /* Response varies? */ u8 bad_parent; /* Parent is well-behaved? */ + u8 res_time_exceeds; /* Response time too long? */ + u32 res_time_base; /* Base response time */ /* Fuzzer and probe state data: */ @@ -127,8 +129,9 @@ struct pivot_desc { u32 r404_pending; /* ...for 404 probes */ u32 ck_pending; /* ...for behavior checks */ - s32 check_idx; /* Current injection test */ - u32 check_state; /* Current injection test */ + s32 check_idx; /* Current test index */ + u32 check_state; /* Current test state */ + u32 check_id; /* Current test id */ struct http_sig r404[MAX_404]; /* 404 response signatures */ u32 r404_cnt; /* Number of sigs collected */ @@ -162,6 +165,7 @@ struct pivot_desc { extern struct pivot_desc root_pivot; extern u32 verbosity; +extern u32 slist_cnt; /* Checks child / descendant limits. */ @@ -200,6 +204,10 @@ u8 is_c_sens(struct pivot_desc* pv); u8* lookup_issue_title(u32 id); +/* Remove issues from a pivot */ + +void remove_issue(struct pivot_desc *pv, u32 type); + /* Recorded security issues */ /* - Informational data (non-specific security-relevant notes): */ @@ -241,6 +249,8 @@ u8* lookup_issue_title(u32 id); #define PROB_FUZZ_DIGIT 10901 /* Try fuzzing file name */ #define PROB_OGNL 10902 /* OGNL-like parameter */ +#define PROB_SIG_DETECT 10909 /* Signature detected info */ + /* - Internal warnings (scan failures, etc): */ #define PROB_FETCH_FAIL 20101 /* Fetch failed. */ @@ -263,6 +273,7 @@ u8* lookup_issue_title(u32 id); #define PROB_SSL_BAD_HOST 30203 /* Certificate host mismatch */ #define PROB_SSL_NO_CERT 30204 /* No certificate data? */ #define PROB_SSL_WEAK_CIPHER 30205 /* Weak cipher negotiated */ +#define PROB_SSL_HOST_LEN 30206 /* Possible \0 in host name */ #define PROB_DIR_LIST_BYPASS 30301 /* Dir listing bypass */ @@ -282,12 +293,15 @@ u8* lookup_issue_title(u32 id); #define PROB_HEADER_INJECT 30901 /* Injected string in header */ +#define PROB_SIG_DETECT_L 30909 /* Signature detected low */ + /* - Moderate severity issues (data compromise): */ #define PROB_BODY_XSS 40101 /* Document body XSS */ #define PROB_URL_XSS 40102 /* URL-based XSS */ #define PROB_HTTP_INJECT 40103 /* Header splitting */ #define PROB_USER_URL_ACT 40104 /* Active user content */ +#define PROB_TAG_XSS 40105 /* TAG attribute XSS */ #define PROB_EXT_SUB 40201 /* External subresource */ #define PROB_MIXED_SUB 40202 /* Mixed content subresource */ @@ -306,6 +320,8 @@ u8* lookup_issue_title(u32 id); #define PROB_PASS_NOSSL 40701 /* Password form, no HTTPS */ +#define PROB_SIG_DETECT_M 40909 /* Signature detected moderate*/ + /* - High severity issues (system compromise): */ #define PROB_XML_INJECT 50101 /* Backend XML injection */ @@ -320,6 +336,8 @@ u8* lookup_issue_title(u32 id); #define PROB_PUT_DIR 50301 /* HTTP PUT accepted */ +#define PROB_SIG_DETECT_H 50909 /* Signature detected high */ + #ifdef _VIA_DATABASE_C @@ -434,6 +452,7 @@ struct pstruct pstructs[] = { struct issue_desc { u32 type; /* PROB_* */ u8* extra; /* Problem-specific string */ + u32 sid; /* Signature ID, if any */ struct http_request* req; /* HTTP request sent */ struct http_response* res; /* HTTP response seen */ }; diff --git a/debug.h b/src/debug.h similarity index 99% rename from debug.h rename to src/debug.h index 1dd0502..349a28a 100644 --- a/debug.h +++ b/src/debug.h @@ -72,7 +72,7 @@ #endif /* ^LOG_STDERR */ #define F_DEBUG(x...) fprintf(stderr,x) -#define SAY(x...) printf(x) +#define SAY(x...) printf(x) #define L1 1 /* Informative, one line messages */ #define L2 2 /* Expand the above, dump reqs, resps */ diff --git a/http_client.c b/src/http_client.c similarity index 96% rename from http_client.c rename to src/http_client.c index d80def8..3d26e03 100644 --- a/http_client.c +++ b/src/http_client.c @@ -35,6 +35,7 @@ #include #include +#include #include #include #include @@ -1099,7 +1100,7 @@ u8* build_request_data(struct http_request* req) { } } - if (ck_pos) { + if (ck_pos && !req->no_cookies) { ASD("Cookie: "); ASD(ck_buf); ASD("\r\n"); @@ -1234,7 +1235,7 @@ u8* build_request_data(struct http_request* req) { /* Internal helper for parsing lines for parse_response(), etc. */ -static u8* grab_line(u8* data, u32* cur_pos, u32 data_len) { +u8* grab_line(u8* data, u32* cur_pos, u32 data_len) { u8 *cur_ptr = data + *cur_pos, *start_ptr = cur_ptr, *end_ptr = data + data_len, @@ -1893,6 +1894,39 @@ void async_request(struct http_request* req) { } +/* A helper function to compare the CN / altname with our host name */ + +static u8 match_cert_name(char* req_host, char* host) { + + if (!host) return 0; + + /* For matching, we update our pointer from *.example.org to + .example.org */ + if (host[0] == '*' && host[1] == '.') { + host++; + + if (strlen(req_host) > strlen(host)) { + /* The cert name is a wild card which counts for the first level + * subdomain. We for comparison, strip the first section: + * + * foo.bar.example.org must not match .example.org + * bar.example.org must match .example.org + * + * */ + while(req_host && req_host[0] != '.') + req_host++; + } + } + + + if (host) DEBUG("Comparing: %s %s\n", host, req_host); + + if (!host || strcasecmp(host, req_host)) + return 0; + + return 1; +} + /* Check SSL properties, raise security alerts if necessary. We do not perform a very thorough validation - we do not check for valid root CAs, bad ciphers, @@ -1916,7 +1950,11 @@ static void check_ssl(struct conn_entry* c) { if (p) { u32 cur_time = time(0); + u32 i, acnt; char *issuer, *host, *req_host; + STACK_OF(GENERAL_NAME) *altnames; + char *buf = 0; + u8 found = 0; /* Check for certificate expiration... */ @@ -1940,21 +1978,53 @@ static void check_ssl(struct conn_entry* c) { free(issuer); - /* Extract CN= from certificate name, compare to destination host. */ + /* Extract CN= from certificate name, compare to destination host. If + it doesn't match, step 2 is to look for alternate names and compare + those to the hostname */ host = strrchr(p->name, '='); + if (host) host++; /* Strip the = */ req_host = (char*)c->q->req->host; - if (host) { - host++; - if (host[0] == '*' && host[1] == '.') { - host++; - if (strlen(req_host) > strlen(host)) - req_host += strlen(req_host) - strlen(host); + /* Step 1: compare the common name value */ + found = match_cert_name(req_host, host); + + /* Step 2: compare the alternate names */ + if (!found) { + + altnames = X509_get_ext_d2i(p, NID_subject_alt_name, NULL, NULL); + + if (altnames) { + acnt = sk_GENERAL_NAME_num(altnames); + DEBUG("*-- Certificate has %d altnames\n", acnt); + + for (i=0; !found && itype != GEN_DNS) continue; + + buf = (char*)ASN1_STRING_data(name->d.dNSName); + + /* No string, no match */ + if (!buf) continue; + + /* Not falling for the \0 trick so we only compare when the + length matches with the string */ + + if (strlen(buf) != ASN1_STRING_length(name->d.dNSName)) { + problem(PROB_SSL_HOST_LEN, c->q->req, 0, (u8*)host, + host_pivot(c->q->req->pivot), 0); + + } else { + found = match_cert_name(req_host, buf); + } + + } + GENERAL_NAMES_free(altnames); } } - if (!host || strcasecmp(host, req_host)) + if (!found) problem(PROB_SSL_BAD_HOST, c->q->req, 0, (u8*)host, host_pivot(c->q->req->pivot), 0); @@ -2066,6 +2136,9 @@ connect_error: c->write_buf = build_request_data(q->req); c->write_len = strlen((char*)c->write_buf); + /* Time the request */ + q->req->start_time = c->req_start; + } @@ -2219,6 +2292,8 @@ SSL_read_more: p_ret = parse_response(c->q->req, c->q->res, c->read_buf, c->read_len, (c->read_len > (size_limit + READ_CHUNK)) ? 0 : 1); + c->q->req->end_time = time(0); + if (!p_ret || p_ret == 3) { u8 keep; @@ -2342,11 +2417,10 @@ SSL_read_more: while (q) { u32 to_host = 0; - // enforce the max requests per seconds requirement + /* enforce the max requests per seconds requirement */ if (max_requests_sec && req_sec > max_requests_sec) { u32 diff = req_sec - max_requests_sec; - DEBUG("req_sec=%f max=%f diff=%u\n", req_sec, max_requests_sec, diff); if ((iterations_cnt++)%(diff + 1) != 0) { idle = 1; return queue_cur; diff --git a/http_client.h b/src/http_client.h similarity index 97% rename from http_client.h rename to src/http_client.h index c07292a..e5be363 100644 --- a/http_client.h +++ b/src/http_client.h @@ -126,7 +126,13 @@ struct http_request { u8* trying_key; /* Current keyword ptr */ u8 trying_spec; /* Keyword specificity info */ + /* Used by injection tests: */ + u8* fuzz_par_enc; /* Fuzz target encoding */ + u8 no_cookies; /* Don't send cookies */ + + u32 start_time; /* Request start time */ + u32 end_time; /* Request end time */ }; @@ -217,6 +223,8 @@ struct conn_entry { u32 write_off; /* Current write offset */ u32 write_len; + u8* origin; /* Connection origin */ + struct queue_entry* q; /* Current queue entry */ struct conn_entry* prev; /* Previous connection entry */ @@ -341,6 +349,10 @@ u8* build_request_data(struct http_request* req); u8 parse_response(struct http_request* req, struct http_response* res, u8* data, u32 data_len, u8 more); +/* Helper function for grabbing lines when parsing requests and responses */ + +u8* grab_line(u8* data, u32* cur_pos, u32 data_len); + /* Processes the queue. Returns the number of queue entries remaining, 0 if none. Will do a blocking select() to wait for socket state changes (or timeouts) if no data available to process. This is the main diff --git a/report.c b/src/report.c similarity index 98% rename from report.c rename to src/report.c index 656d921..23ccaf5 100644 --- a/report.c +++ b/src/report.c @@ -206,12 +206,18 @@ static void maybe_add_sig(struct pivot_desc* pv) { /* See if a matching signature already exists. */ - for (i=0;itype && p_sig[i].issue_sig == issue_sig && p_sig[i].child_sig == child_sig && same_page(p_sig[i].res_sig, &pv->res->sig)) { - pv->dupe = 1; + /* We don't mark parameters as dupes: different parameters for the + same page will get the same signature. If we mark them as duplicate, + some issues, like XSS' will only be reported once while that might + be present in two or more parameters. */ + + if (pv->type != PIVOT_PARAM && !pv->bogus_par) + pv->dupe = 1; return; } @@ -617,7 +623,7 @@ static void output_crawl_tree(struct pivot_desc* pv) { if (!pv->dupe) { u32 c; - for (c=0;cissue[i].type) break; if (c == i_samp_cnt) { diff --git a/report.h b/src/report.h similarity index 100% rename from report.h rename to src/report.h diff --git a/same_test.c b/src/same_test.c similarity index 100% rename from same_test.c rename to src/same_test.c diff --git a/src/signatures.c b/src/signatures.c new file mode 100644 index 0000000..9a95da8 --- /dev/null +++ b/src/signatures.c @@ -0,0 +1,570 @@ + +/* + skipfish - Signature Matching + ---------------------------------------- + + Author: Niels Heinen , + Sebastian Roschke + + Copyright 2011, 2012 by Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + + +#include +#include "pcre.h" + +#define _VIA_SIGNATURE_C + +#include "types.h" +#include "config.h" +#include "debug.h" +#include "alloc-inl.h" +#include "database.h" +#include "signatures.h" + +struct signature** sig_list; +u32 slist_max_cnt = 0, slist_cnt = 0; + +void dump_sig(struct signature *sig); + +/* Helper function to lookup a signature keyword. This makes it easier + switch() signature keywords for, for example, storing their values in + a struct */ + +static s32 lookup(u8* key) { + + u32 i; + for (i=0; lookuptable[i].name; i++) { + if (!strcmp((char*)key, lookuptable[i].name)) + return lookuptable[i].id; + } + return -1; +} + +/* Helper function for parse_sig which takes a string where some + characters might be escaped. It returns a copy with the \'s + removed so \"aa\" becomes "aa" */ + +static u8* unescape_str(u8* str) { + + u32 k = 0, i = 0; + u32 len = strlen((char*)str) + 1; + u8* ret = ck_alloc(len); + + for (i=0; iseverity < 0 || sig->severity > 4) { + WARN("Signature %d has an invalid severity: %d\n", sig->id, sig->severity); + ret = 1; + } + + if (!sig->content_cnt && !sig->mime) { + WARN("Signature %d has no \"content\" nor \"mime\" string\n", sig->id); + ret = 1; + } + + if (!sig->memo) { + WARN("Signature %d has no memo string\n", sig->id); + ret = 1; + } + + return ret; +} + +static u8 compile_content(struct content_struct *content) { + const char* pcre_err; + int pcre_err_offset; + + u32 options = PCRE_MULTILINE; + + if (content->type != TYPE_REGEX) + return 1; + + if (content->nocase) options |= PCRE_CASELESS; + + content->pcre_sig = pcre_compile((char*)content->match_str, options, + &pcre_err, &pcre_err_offset, 0); + + if (!content->pcre_sig) + return 1; + + /* This is extra */ + content->pcre_extra_sig = pcre_study(content->pcre_sig, 0, &pcre_err); + + return 0; + +} + +/* Parses the signature string that is given as the first parameter and returns + a signature struct */ + +struct signature* parse_sig(u8* tline) { + + u8 *val, *name, *line; + u8 in_quot = 0; + u8 no = 0; + struct content_struct *lcontent = NULL; + + line = tline = ck_strdup(tline); + + struct signature* sig = ck_alloc(sizeof(struct signature)); + while (line) { + + /* Skip spaces */ + name = line; + no = 0; + + while (name && isspace(*name)) + name++; + + /* Split on the value and, for now, return NULL when there is no + value. Later we should add value-less keywords, like 'nocase' */ + val = (u8*)index((char*)name, ':'); + + if(!val) { + ck_free(sig); + ck_free(tline); + return 0; + } + *val = 0; + + /* Check if ! is present and set 'not' */ + if (++val && *val == '!') { + no = 1; + val++; + } + + /* Move to value and check if quoted */ + if (val && (*val == '\'' || *val == '"')) { + in_quot = *val; + val++; + } + + /* Find the end of the value string */ + line = val; + while (++line) { + if(*line == '\\') { + line++; + continue; + } + + /* End of quotation? */ + if (in_quot && *line == in_quot) { + in_quot = 0; + *line = 0; + continue; + } + + /* End of the value? */ + if (!in_quot && *line == ';') { + *line = 0; + break; + } + } + + switch (lookup(name)) { + case SIG_ID: + sig->id = atoi((char*)val); + break; + case SIG_CHK: + sig->check = atoi((char*)val); + break; + case SIG_CONTENT: + + /* If we already have a content struct, try to compile it (when + regex) and create a new content struct */ + + if (lcontent) { + + /* Compile and bail out on failure */ + if (lcontent->type == TYPE_REGEX && + compile_content(lcontent)) + FATAL("Unable to compile regex in: %s", tline); + + } + + if (sig->content_cnt > MAX_CONTENT) + FATAL("Too many content's in line: %s", tline); + + sig->content[sig->content_cnt] = ck_alloc(sizeof(struct content_struct)); + lcontent = sig->content[sig->content_cnt]; + lcontent->match_str = unescape_str(val); + lcontent->match_str_len = strlen((char*)lcontent->match_str); + lcontent->no = no; + sig->content_cnt++; + break; + case SIG_MEMO: + sig->memo = unescape_str(val); + break; + case SIG_TYPE: + if (!lcontent) { + WARN("Found 'type' before 'content', skipping.."); + break; + } + /* 0 is plain to which we default so this only checks if the + rule wants a regex */ + if (!strcmp((char*)val, "regex")) + lcontent->type = (u8)TYPE_REGEX; + break; + case SIG_DIST: + if (!lcontent) { + WARN("Found 'distance' before 'content', skipping.."); + break; + } + lcontent->distance = atoi((char*)val); + break; + case SIG_OFFSET: + if (!lcontent) { + WARN("Found 'offset' before 'content', skipping.."); + break; + } + lcontent->offset = atoi((char*)val); + break; + case SIG_SEV: + sig->severity = atoi((char*)val); + break; + case SIG_PROB: + sig->prob = atoi((char*)val); + break; + case SIG_DEPTH: + if (!lcontent) { + WARN("Found 'depth' before 'content', skipping.."); + break; + } + lcontent->depth = atoi((char*)val); + break; + case SIG_CASE: + if (!lcontent) { + WARN("Found 'case' before 'content', skipping.."); + break; + } + if (!strcmp((char*)val, "no")) + lcontent->nocase = 1; + break; + case SIG_MIME: + sig->mime = unescape_str(val); + break; + case SIG_CODE: + sig->rcode = atoi((char*)val); + break; + + default: + FATAL("Unknown keyword: %s", name); + } + + /* Proceed, or stop when we're at the end of the line. Since 'line' + still points to ; , we'll increase it first */ + if(line) line++; + + /* Now if we're at EOF or EOL, we'll stop */ + if (!line || (*line == '\r' || *line == '\n')) + break; + } + + ck_free(tline); + + /* Compile the last content entry */ + if (lcontent) compile_content(lcontent); + + /* Done parsing! Now validate the signature before returning it */ + if (check_signature(sig)) { + DEBUG("Skipping signature (didn't validate)\n"); + destroy_signature(sig); + return NULL; + } + + /* Dump the signature when debugging */ + +#ifdef LOG_STDERR + dump_sig(sig); +#endif /* !LOG_STDERR */ + return sig; +} + +/* Loads the signature list from file 'fname' and parses each line. Whenever a + * line starts with #include , the file will be parsed as well to make signature + * management really easy. */ + +void load_signatures(u8* fname) { + FILE* in; + u8 tmp[MAX_SIG_LEN]; + u8 include[MAX_SIG_FNAME + 1]; + u32 in_cnt = 0; + u8 fmt[20]; + + struct signature *sig; + + in = fopen((char*)fname, "r"); + if (!in) { + PFATAL("Unable to open signature list '%s'", fname); + return; + } + + /* Create a signature list */ + if (!sig_list) + sig_list = ck_alloc(sizeof(struct signature*) * MAX_SIG_CNT); + + while (fgets((char*)tmp, MAX_SIG_LEN, in)) { + if (tmp[0] == '#') + continue; + + /* When the include directive is present, we'll follow it */ + if (!strncmp((char*)tmp, "include ", 8)) { + + /* Check the amount of files included already. This is mostly to protect + * against include loops */ + + if (in_cnt++ > MAX_SIG_INCS) + FATAL("Too many signature includes (max: %d)\n", MAX_SIG_INCS); + + sprintf((char*)fmt, "%%%u[^\x01-\x1f]", MAX_SIG_FNAME); + sscanf((char*)tmp + 8,(char*)fmt, (char*)&include); + + DEBUG("- Including signature file: %s\n", include); + load_signatures(include); + + } + + sig = parse_sig(tmp); + + if(sig == NULL) + continue; + + if (slist_cnt >= MAX_SIG_CNT) + FATAL("* Signature list is too large (max = %d)\n", MAX_SIG_CNT); + + sig_list[slist_cnt++] = sig; + } + + DEBUG("*- Signatures processed: %s (total sigs %d)\n", fname, slist_cnt); + + fclose(in); +} + +u8 match_signatures(struct http_request *req, struct http_response *res) { + + u8 pcre_ret, matches = 0; + u8 *payload, *match = NULL; + u32 ovector[PCRE_VECTOR]; + u32 pay_len, j = 0, i = 0; + + struct content_struct *content = NULL; + + for ( j = 0; j < slist_cnt; j++ ) { + + /* Check if the signature is only intended for one of the active tests. */ + if (sig_list[j]->check && (req->pivot->check_id > 0 && + req->pivot->check_id != sig_list[j]->check)) { + continue; + } + + /* Compare response code */ + if (sig_list[j]->rcode && sig_list[j]->rcode != res->code) + continue; + /* Compare the mime types */ + if (sig_list[j]->mime && res->header_mime) { + /* Skip if the mime doesn't match */ + if (strncmp((char*)res->header_mime, (char*)sig_list[j]->mime, + strlen((char*)sig_list[j]->mime))) continue; + + /* We've got a signature match with the mime is the same and no content + * string exists. This is useful for reporting interesting mime types, + * such as application/x-httpd-php-source */ + + if (!sig_list[j]->content_cnt) { + signature_problem(sig_list[j], req, res); + continue; + } + } + + + /* Nice, so here the matching will start! Unless... there are not content + * strings, or when the response is mainly binary data. */ + if (res->doc_type == 1 || !sig_list[j]->content_cnt) + continue; + + payload = res->payload; + pay_len = res->pay_len; + matches = 0; + + for (i=0; pay_len > 0 && icontent_cnt; i++) { + + content = sig_list[j]->content[i]; + + /* If there is an offset, we will apply it to the current payload + pointer */ + + if (content->offset) { + if (pay_len < content->offset) break; + + payload = payload + content->offset; + } + + /* Use the specified maximum depth to search the string. If no depth + is specified, we search the entire buffer. Note that this is relative + to the beginning of the buffer _or_ the previous content match */ + + if (content->depth) + pay_len = content->depth; + + if (content->distance && pay_len > content->distance) { + payload += content->distance; + pay_len -= content->distance; + } + + match = 0; + if (content->type == TYPE_PLAIN) { + if (content->nocase) { + match = inl_findstrcase(payload, content->match_str, pay_len); + } else { + match = inl_findstr(payload, content->match_str, pay_len); + } + + if (match && !content->no) { + /* Move the match pointer to allow offset to be applied relative + to the previous content-match */ + + payload = match + content->match_str_len; + pay_len -= content->match_str_len; + matches++; + } else if(!match && content->no) { + matches++; + } else break; + + } else if(content->type == TYPE_REGEX) { + /* Lets do the pcre matching */ + pcre_ret = (pcre_exec(content->pcre_sig, content->pcre_extra_sig, + (char*)payload, pay_len, 0, 0, (int*)ovector, PCRE_VECTOR) >= 0); + + if (!content->no && pcre_ret) { + /* We care about the first match and update the match pointer + to the first byte that follows the matching string */ + + payload = payload + ovector[1]; + pay_len -= (ovector[1] - ovector[0]); + /* pay_len is checked in the next match */ + + matches++; + } else if(!pcre_ret && content->no) { + matches++; + } else break; + } + } /* for i loop */ + + if (matches == sig_list[j]->content_cnt) + signature_problem(sig_list[j], req, res); + + } /* for j loop */ + + return 0; +} + + +/* Wrapper for reporting a signature problem */ +void signature_problem(struct signature *sig, + struct http_request *req, + struct http_response *res) { + +#ifdef _SIGNATURE_TEST + DEBUG("signature_problem() called for %d (%s)\n", sig->id, sig->memo); +#else + u8* memo = NULL; + + /* Each signature is supposed to have a memo: testing just in case */ + if (sig->memo) { + u32 len = strlen((char*)sig->memo); + memo = ck_alloc(len + 15); /* of which 5 for the ID */ + snprintf((char*)memo, len + 14, "%s (sig: %u)", (char*)sig->memo, sig->id); + } + + /* Todo: update issue_desc and add the ID in it */ + problem((sig->prob ? sig->prob : sig_serv[sig->severity]), req, res, + (memo ? memo : (u8*)""), req->pivot, 0); + + if (memo) ck_free(memo); +#endif +} + +void destroy_signature(struct signature *sig) { + u32 i; + + if (sig->memo) ck_free(sig->memo); + if (sig->mime) ck_free(sig->mime); + + for (i=0; icontent_cnt; i++) { + ck_free(sig->content[i]->match_str); + + if (sig->content[i]->pcre_sig) + free(sig->content[i]->pcre_sig); + if (sig->content[i]->pcre_extra_sig) + free(sig->content[i]->pcre_extra_sig); + + ck_free(sig->content[i]); + } + + ck_free(sig); + +} +void destroy_signature_lists() { + + u32 i; + for (i = 0; i < slist_cnt; i++) + destroy_signature(sig_list[i]); + + ck_free(sig_list); +} + +/* For debugging: dump a signature */ +void dump_sig(struct signature *sig) { + + u32 i; + + DEBUG("\n=== New signature loaded ===\n"); + DEBUG(" id = %d\n", sig->id); + DEBUG(" severity = %d\n", sig->severity); + DEBUG(" content # = %d\n", sig->content_cnt); + + for (i=0; icontent_cnt; i++) { + DEBUG(" %d. match_str = %s\n", i, sig->content[i]->match_str); + DEBUG(" %d. type = %s\n", i, sig->content[i]->type ? "REGEX" : "STRING"); + DEBUG(" %d. offset = %d\n", i, sig->content[i]->offset); + DEBUG(" %d. depth = %d\n", i, sig->content[i]->depth); + DEBUG(" %d. position = %d\n", i, sig->content[i]->distance); + DEBUG(" %d. no = %d\n", i, sig->content[i]->no); + + } + /* And now the optional fields */ + if (sig->memo) + DEBUG(" memo = %s\n", sig->memo); + if (sig->mime) + DEBUG(" mime = %s\n", sig->mime); + if (sig->rcode) + DEBUG(" code = %d\n", sig->rcode); +} diff --git a/src/signatures.h b/src/signatures.h new file mode 100644 index 0000000..9b063ca --- /dev/null +++ b/src/signatures.h @@ -0,0 +1,148 @@ + +/* + skipfish - signature matching + ---------------------------------------- + + Author: Niels Heinen + + Copyright 2011 - 2012 by Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#include "pcre.h" + +#ifndef _SIGNATURE_H +#define _SIGNATURE_H + +#define MAX_CONTENT 10 +#define PCRE_VECTOR 30 + +struct content_struct { + u8* match_str; /* The content string to find */ + u32 match_str_len; /* Length of the content string */ + pcre* pcre_sig; /* Regex: compiled */ + pcre_extra* pcre_extra_sig; /* Regex: extra */ + + u8 no; /* 1 = string should not be there */ + u8 nocase; /* 1 = case insensitive matching */ + u8 type; /* regex or static string */ + u32 depth; /* Depth of bytes to search */ + u32 distance; /* Relative distance to search */ + u32 offset; /* Search starts after offset */ +}; + +struct signature { + u32 id; /* Unique ID for documentation */ + u8* memo; /* Message displayed when found */ + u8 severity; /* Severity */ + u32 prob; /* Problem ID from analysis.h */ + u8* mime; /* Match with this mime type */ + u32 rcode; /* Match with HTTP resp code */ + u32 content_cnt; /* Amount of contenrt structs */ + u32 check; /* The check ID */ + struct content_struct* content[MAX_CONTENT]; +}; + + +/* The signature matching function */ + +u8 match_signatures(struct http_request *req, struct http_response *res); + + +/* Load the passwords from a file */ + +void load_signatures(u8* fname); + +/* Destroy the wordlists and free all memory */ + +void destroy_signature_lists(void); + +/* Wrapper for reporting a signature problem */ + +void signature_problem(struct signature *sig, struct http_request *req, struct http_response *res); + + +struct signature** sig_list; /* The one and only: signature list */ + +extern u32 slist_max_cnt; /* Allocated space in the signature lists */ +u32 slist_cnt; /* Actual elements in the signature lists */ + +#define TYPE_PLAIN 0 /* Content type: static string */ +#define TYPE_REGEX 1 /* Content type: regular expression */ + +#define MAX_SIG_LEN 2048 /* Signature line length */ +#define MAX_SIG_CNT 1024 /* Max amount of signatures to load */ +#define MAX_SIG_FNAME 512 /* Maximum signature filename */ +#define MAX_SIG_INCS 64 /* Maximum files to include. */ + +#ifdef _VIA_SIGNATURE_C + +u32 sig_serv[] = { + PROB_SIG_DETECT, /* Default: info level */ + PROB_SIG_DETECT_H, /* High risk */ + PROB_SIG_DETECT_M, /* Medium risk */ + PROB_SIG_DETECT_L, /* Low risk */ + PROB_SIG_DETECT /* info risk */ +}; + + +/* Destroy an individual signature */ + +void destroy_signature(struct signature *sig); + +#define SIG_ID 1 +#define SIG_CONTENT 2 +#define SIG_MEMO 3 +#define SIG_TYPE 4 +#define SIG_SEV 5 +#define SIG_CONST 6 +#define SIG_PROB 7 +#define SIG_TAG 8 +#define SIG_MIME 9 +#define SIG_CODE 10 +#define SIG_CASE 11 +#define SIG_DEPTH 12 +#define SIG_OFFSET 13 +#define SIG_DIST 14 +#define SIG_CHK 15 + +/* The structs below are to for helping the signature parser */ + +struct sig_key { + u32 id; + const char *name; +}; + + +struct sig_key lookuptable[] = { + { SIG_ID, "id" }, + { SIG_CONTENT, "content" }, + { SIG_MEMO, "memo" }, + { SIG_TYPE, "type" }, + { SIG_SEV, "sev" }, + { SIG_PROB, "prob" }, + { SIG_TAG, "tag" }, + { SIG_MIME, "mime" }, + { SIG_CODE, "code" }, + { SIG_CASE, "case" }, + { SIG_DEPTH, "depth" }, + { SIG_OFFSET, "offset" }, + { SIG_DIST, "distance" }, + { SIG_CHK, "check" }, + { 0, 0} +}; + +#endif /* !_VIA_SIGNATURE_C */ +#endif /* !_SIGNATURE_H */ diff --git a/skipfish.c b/src/skipfish.c similarity index 73% rename from skipfish.c rename to src/skipfish.c index 71e3ba5..72dadb4 100644 --- a/skipfish.c +++ b/src/skipfish.c @@ -44,6 +44,8 @@ #include "database.h" #include "http_client.h" #include "report.h" +#include "signatures.h" +#include "auth.h" #ifdef DEBUG_ALLOCATOR struct TRK_obj* TRK[ALLOC_BUCKETS]; @@ -73,72 +75,78 @@ static void usage(char* argv0) { "Authentication and access options:\n\n" - " -A user:pass - use specified HTTP authentication credentials\n" - " -F host=IP - pretend that 'host' resolves to 'IP'\n" - " -C name=val - append a custom cookie to all requests\n" - " -H name=val - append a custom HTTP header to all requests\n" - " -b (i|f|p) - use headers consistent with MSIE / Firefox / iPhone\n" + " -A user:pass - use specified HTTP authentication credentials\n" + " -F host=IP - pretend that 'host' resolves to 'IP'\n" + " -C name=val - append a custom cookie to all requests\n" + " -H name=val - append a custom HTTP header to all requests\n" + " -b (i|f|p) - use headers consistent with MSIE / Firefox / iPhone\n" #ifdef PROXY_SUPPORT - " -J proxy - use a specified HTTP proxy server\n" + " -J proxy - use a specified HTTP proxy server\n" #endif /* PROXY_SUPPORT */ - " -N - do not accept any new cookies\n\n" + " -N - do not accept any new cookies\n" + " --auth-form url - form authentication URL\n" + " --auth-user user - form authentication user\n" + " --auth-pass pass - form authentication password\n" + " --auth-verify-url - URL for in-session detection\n\n" "Crawl scope options:\n\n" - " -d max_depth - maximum crawl tree depth (%u)\n" - " -c max_child - maximum children to index per node (%u)\n" - " -x max_desc - maximum descendants to index per branch (%u)\n" - " -r r_limit - max total number of requests to send (%u)\n" - " -p crawl%% - node and link crawl probability (100%%)\n" - " -q hex - repeat probabilistic scan with given seed\n" - " -I string - only follow URLs matching 'string'\n" - " -X string - exclude URLs matching 'string'\n" - " -K string - do not fuzz parameters named 'string'\n" - " -D domain - crawl cross-site links to another domain\n" - " -B domain - trust, but do not crawl, another domain\n" - " -Z - do not descend into 5xx locations\n" - " -O - do not submit any forms\n" - " -P - do not parse HTML, etc, to find new links\n\n" + " -d max_depth - maximum crawl tree depth (%u)\n" + " -c max_child - maximum children to index per node (%u)\n" + " -x max_desc - maximum descendants to index per branch (%u)\n" + " -r r_limit - max total number of requests to send (%u)\n" + " -p crawl%% - node and link crawl probability (100%%)\n" + " -q hex - repeat probabilistic scan with given seed\n" + " -I string - only follow URLs matching 'string'\n" + " -X string - exclude URLs matching 'string'\n" + " -K string - do not fuzz parameters named 'string'\n" + " -D domain - crawl cross-site links to another domain\n" + " -B domain - trust, but do not crawl, another domain\n" + " -Z - do not descend into 5xx locations\n" + " -O - do not submit any forms\n" + " -P - do not parse HTML, etc, to find new links\n\n" "Reporting options:\n\n" - " -o dir - write output to specified directory (required)\n" - " -M - log warnings about mixed content / non-SSL passwords\n" - " -E - log all HTTP/1.0 / HTTP/1.1 caching intent mismatches\n" - " -U - log all external URLs and e-mails seen\n" - " -Q - completely suppress duplicate nodes in reports\n" - " -u - be quiet, disable realtime progress stats\n\n" + " -o dir - write output to specified directory (required)\n" + " -M - log warnings about mixed content / non-SSL passwords\n" + " -E - log all HTTP/1.0 / HTTP/1.1 caching intent mismatches\n" + " -U - log all external URLs and e-mails seen\n" + " -Q - completely suppress duplicate nodes in reports\n" + " -u - be quiet, disable realtime progress stats\n" + " -v - enable runtime logging (to stderr)\n\n" "Dictionary management options:\n\n" - " -W wordlist - use a specified read-write wordlist (required)\n" - " -S wordlist - load a supplemental read-only wordlist\n" - " -L - do not auto-learn new keywords for the site\n" - " -Y - do not fuzz extensions in directory brute-force\n" - " -R age - purge words hit more than 'age' scans ago\n" - " -T name=val - add new form auto-fill rule\n" - " -G max_guess - maximum number of keyword guesses to keep (%d)\n\n" + " -W wordlist - use a specified read-write wordlist (required)\n" + " -S wordlist - load a supplemental read-only wordlist\n" + " -L - do not auto-learn new keywords for the site\n" + " -Y - do not fuzz extensions in directory brute-force\n" + " -R age - purge words hit more than 'age' scans ago\n" + " -T name=val - add new form auto-fill rule\n" + " -G max_guess - maximum number of keyword guesses to keep (%d)\n\n" + " -z sigfile - load signatures from this file\n\n" "Performance settings:\n\n" - " -l max_req - max requests per second (%f)\n" - " -g max_conn - max simultaneous TCP connections, global (%u)\n" - " -m host_conn - max simultaneous connections, per target IP (%u)\n" - " -f max_fail - max number of consecutive HTTP errors (%u)\n" - " -t req_tmout - total request response timeout (%u s)\n" - " -w rw_tmout - individual network I/O timeout (%u s)\n" - " -i idle_tmout - timeout on idle HTTP connections (%u s)\n" - " -s s_limit - response size limit (%u B)\n" - " -e - do not keep binary responses for reporting\n\n" + " -g max_conn - max simultaneous TCP connections, global (%u)\n" + " -m host_conn - max simultaneous connections, per target IP (%u)\n" + " -f max_fail - max number of consecutive HTTP errors (%u)\n" + " -t req_tmout - total request response timeout (%u s)\n" + " -w rw_tmout - individual network I/O timeout (%u s)\n" + " -i idle_tmout - timeout on idle HTTP connections (%u s)\n" + " -s s_limit - response size limit (%u B)\n" + " -e - do not keep binary responses for reporting\n\n" "Safety settings:\n\n" - " -k duration - stop scanning after the given duration h:m:s\n\n" + " -l max_req - max requests per second (%f)\n" + " -k duration - stop scanning after the given duration h:m:s\n\n" - "Send comments and complaints to .\n", argv0, + "Send comments and complaints to .\n", argv0, max_depth, max_children, max_descendants, max_requests, - MAX_GUESSES, max_requests_sec, max_connections, max_conn_host, - max_fail, resp_tmout, rw_tmout, idle_tmout, size_limit); + MAX_GUESSES, max_connections, max_conn_host, + max_fail, resp_tmout, rw_tmout, idle_tmout, size_limit, max_requests_sec); exit(1); } @@ -263,10 +271,12 @@ static void read_urls(u8* fn) { int main(int argc, char** argv) { s32 opt; u32 loop_cnt = 0, purge_age = 0, seed; - u8 show_once = 0, no_statistics = 0, display_mode = 0, has_fake = 0; + u8 sig_loaded = 0, show_once = 0, no_statistics = 0, + display_mode = 0, has_fake = 0; s32 oindex = 0; u8 *wordlist = NULL, *output_dir = NULL; - u8* gtimeout_str = NULL; + u8 *sig_list_strg = NULL; + u8 *gtimeout_str = NULL; u32 gtimeout = 0; struct termios term; @@ -327,9 +337,18 @@ int main(int argc, char** argv) { {"quiet", no_argument, 0, 'u' }, {"verbose", no_argument, 0, 'v' }, {"scan-timeout", required_argument, 0, 'k'}, + {"signatures", required_argument, 0, 'z'}, {"checks", no_argument, 0, 0}, {"checks-toggle", required_argument, 0, 0}, {"no-checks", no_argument, 0, 0}, + {"fast", no_argument, 0, 0}, + {"auth-form", required_argument, 0, 0}, + {"auth-form-target", required_argument, 0, 0}, + {"auth-user", required_argument, 0, 0}, + {"auth-user-field", required_argument, 0, 0}, + {"auth-pass", required_argument, 0, 0}, + {"auth-pass-field", required_argument, 0, 0}, + {"auth-verify-url", required_argument, 0, 0}, {0, 0, 0, 0 } }; @@ -342,7 +361,7 @@ int main(int argc, char** argv) { while ((opt = getopt_long(argc, argv, "+A:B:C:D:EF:G:H:I:J:K:LMNOPQR:S:T:UW:X:YZ" - "b:c:d:ef:g:hi:k:l:m:o:p:q:r:s:t:uvw:x:", + "b:c:d:ef:g:hi:k:l:m:o:p:q:r:s:t:uvw:x:z:", long_options, &oindex)) >= 0) switch (opt) { @@ -514,6 +533,11 @@ int main(int argc, char** argv) { load_keywords((u8*)optarg, 1, 0); break; + case 'z': + load_signatures((u8*)optarg); + sig_loaded = 1; + break; + case 'b': if (optarg[0] == 'i') browser_type = BROWSER_MSIE; else if (optarg[0] == 'f') browser_type = BROWSER_FFOX; else @@ -604,17 +628,31 @@ int main(int argc, char** argv) { no_500_dir = 1; break; - case '?': - PFATAL("Unrecognized option."); - break; - case 0: - if(!strcmp( "checks", long_options[oindex].name )) + if (!strcmp("checks", long_options[oindex].name )) display_injection_checks(); - if(!strcmp( "checks-toggle", long_options[oindex].name )) - toggle_injection_checks((u8*)optarg, 1); - if(!strcmp( "no-checks", long_options[oindex].name )) + if (!strcmp("checks-toggle", long_options[oindex].name )) + toggle_injection_checks((u8*)optarg, 1, 1); + if (!strcmp("no-checks", long_options[oindex].name )) no_checks = 1; + if (!strcmp("signatures", long_options[oindex].name )) + load_signatures((u8*)optarg); + if(!strcmp("fast", long_options[oindex].name )) + toggle_injection_checks((u8*)"2,4,5,13,14,15,16", 0, 0); + if (!strcmp("auth-form", long_options[oindex].name )) + auth_form = (u8*)optarg; + if (!strcmp("auth-user", long_options[oindex].name )) + auth_user = (u8*)optarg; + if (!strcmp("auth-pass", long_options[oindex].name )) + auth_pass = (u8*)optarg; + if (!strcmp("auth-pass-field", long_options[oindex].name )) + auth_pass_field = (u8*)optarg; + if (!strcmp("auth-user-field", long_options[oindex].name )) + auth_user_field = (u8*)optarg; + if (!strcmp("auth-form-target", long_options[oindex].name )) + auth_form_target = (u8*)optarg; + if (!strcmp("auth-verify-url", long_options[oindex].name )) + auth_verify_url = (u8*)optarg; break; @@ -644,7 +682,7 @@ int main(int argc, char** argv) { "run skipfish while redirecting stderr to a file. "); - if (resp_tmout < rw_tmout) + if (resp_tmout < rw_tmout) resp_tmout = rw_tmout; if (max_connections < max_conn_host) @@ -671,15 +709,42 @@ int main(int argc, char** argv) { if (!wordlist) { wordlist = (u8*)"/dev/null"; - DEBUG("* No wordlist specified with -W defaulting to /dev/null..\n"); + DEBUG("* No wordlist specified with -W: defaulting to /dev/null\n"); } + /* If no signature files have been specified via command-line: load + the default file */ + if (!sig_loaded) + load_signatures((u8*)SIG_FILE); + load_keywords(wordlist, 0, purge_age); + /* Load the signatures list for the matching */ + if (sig_list_strg) load_signatures(sig_list_strg); + + /* Try to authenticate when the auth_user and auth_pass fields are set. */ + if (auth_user && auth_pass) { + authenticate(); + + while (next_from_queue()) { + usleep(1000); + } + + switch (auth_state) { + case ASTATE_DONE: + DEBUGC(L1, "*- Authentication succeeded!\n"); + break; + + default: + DEBUG("Auth state: %d\n", auth_state); + FATAL("Authentication failed (use -uv for more info)\n"); + break; + } + } + /* Schedule all URLs in the command line for scanning. */ while (optind < argc) { - struct http_request *req; /* Support @ notation for reading URL lists from files. */ @@ -724,6 +789,8 @@ int main(int argc, char** argv) { if (!no_statistics) SAY("\x1b[H\x1b[J"); else SAY(cLGN "[*] " cBRI "Scan in progress, please stay tuned...\n"); + u64 refresh_time = 0; + /* Enter the crawler loop */ while ((next_from_queue() && !stop_soon) || (!show_once++)) { @@ -747,32 +814,35 @@ int main(int argc, char** argv) { if (no_statistics || ((loop_cnt++ % 100) && !show_once && idle == 0)) continue; - if (clear_screen) { - SAY("\x1b[H\x1b[2J"); - clear_screen = 0; - } + if (end_time > refresh_time) { + refresh_time = (end_time + 10); - SAY(cYEL "\x1b[H" - "skipfish version " VERSION " by \n\n" - cBRI " -" cPIN " %s " cBRI "-\n\n" cNOR, + if (clear_screen) { + SAY("\x1b[H\x1b[2J"); + clear_screen = 0; + } + + SAY(cYEL "\x1b[H" + "skipfish version " VERSION " by lcamtuf@google.com\n\n" + cBRI " -" cPIN " %s " cBRI "-\n\n" cNOR, allow_domains[0]); - if (!display_mode) { - http_stats(st_time); - SAY("\n"); - database_stats(); - } else { - http_req_list(); - } + if (!display_mode) { + http_stats(st_time); + SAY("\n"); + database_stats(); + } else { + http_req_list(); + } - SAY(" \r"); + SAY(" \r"); + } if (fread(keybuf, 1, sizeof(keybuf), stdin) > 0) { display_mode ^= 1; clear_screen = 1; } - } gettimeofday(&tv, NULL); @@ -802,6 +872,7 @@ int main(int argc, char** argv) { #ifdef DEBUG_ALLOCATOR if (!stop_soon) { destroy_database(); + destroy_signature_lists(); destroy_http(); destroy_signatures(); __TRK_report(); diff --git a/string-inl.h b/src/string-inl.h similarity index 88% rename from string-inl.h rename to src/string-inl.h index b387f92..cdc2f3f 100644 --- a/string-inl.h +++ b/src/string-inl.h @@ -54,10 +54,12 @@ /* Macros for easy string prefix matching */ #define prefix(_long, _short) \ - strncmp((char*)(_long), (char*)(_short), strlen((char*)(_short))) + strncmp((const char*)(_long), (const char*)(_short), \ + strlen((const char*)(_short))) #define case_prefix(_long, _short) \ - strncasecmp((char*)(_long), (char*)(_short), strlen((char*)(_short))) + strncasecmp((const char*)(_long), (const char*)(_short), \ + strlen((const char*)(_short))) /* Modified NetBSD strcasestr() implementation (rolling strncasecmp). */ @@ -87,7 +89,6 @@ static inline u8* inl_strcasestr(const u8* haystack, const u8* needle) { } - /* Modified NetBSD memmem() implementation (rolling memcmp). */ static inline void* inl_memmem(const void* haystack, u32 h_len, @@ -136,6 +137,34 @@ static inline u8* inl_findstr(const u8* haystack, const u8* needle, u32 max_len) } +/* Distance-limited and case-insensitive strstr. */ + +static inline u8* inl_findstrcase(const u8* haystack, const u8* needle, u32 max_len) { + register u8 c, sc; + register u32 len; + + if (!haystack || !needle) return 0; + max_len++; + + if ((c = *needle++)) { + + len = strlen((char*)needle); + + do { + do { + if (!(sc = *haystack++) || !max_len--) return 0; + } while (tolower(sc) != c); + } while (strncasecmp((char*)haystack, (char*)needle, len)); + + haystack--; + + } + + return (u8*)haystack; + +} + + diff --git a/types.h b/src/types.h similarity index 100% rename from types.h rename to src/types.h diff --git a/sfscandiff b/tools/sfscandiff similarity index 100% rename from sfscandiff rename to tools/sfscandiff