From c9d5b74896dd6fdf4581847c57455c774d272cdf Mon Sep 17 00:00:00 2001 From: Steve Pinkham Date: Wed, 12 Sep 2012 17:06:51 -0400 Subject: [PATCH] Version 2.08b: Many changes including dir refactor - 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. --- ChangeLog | 38 ++ Makefile | 36 +- README | 29 +- assets/index.html | 8 +- dictionaries/extensions-only.wl | 2 - doc/authentication.txt | 98 +++ .../README-FIRST => doc/dictionaries.txt | 0 doc/signatures.txt | 152 +++++ skipfish.1 => doc/skipfish.1 | 0 signatures/apps.sigs | 13 + signatures/context.sigs | 24 + signatures/files.sigs | 42 ++ signatures/messages.sigs | 45 ++ signatures/mime.sigs | 13 + signatures/signatures.conf | 28 + alloc-inl.h => src/alloc-inl.h | 0 analysis.c => src/analysis.c | 395 ++++-------- analysis.h => src/analysis.h | 17 + src/auth.c | 279 +++++++++ src/auth.h | 74 +++ checks.c => src/checks.c | 532 +++++++++++++--- checks.h => src/checks.h | 41 +- config.h => src/config.h | 9 +- crawler.c => src/crawler.c | 27 +- crawler.h => src/crawler.h | 1 + database.c => src/database.c | 67 +- database.h => src/database.h | 23 +- debug.h => src/debug.h | 2 +- http_client.c => src/http_client.c | 98 ++- http_client.h => src/http_client.h | 12 + report.c => src/report.c | 12 +- report.h => src/report.h | 0 same_test.c => src/same_test.c | 0 src/signatures.c | 570 ++++++++++++++++++ src/signatures.h | 148 +++++ skipfish.c => src/skipfish.c | 225 ++++--- string-inl.h => src/string-inl.h | 35 +- types.h => src/types.h | 0 sfscandiff => tools/sfscandiff | 0 39 files changed, 2551 insertions(+), 544 deletions(-) create mode 100644 doc/authentication.txt rename dictionaries/README-FIRST => doc/dictionaries.txt (100%) create mode 100644 doc/signatures.txt rename skipfish.1 => doc/skipfish.1 (100%) create mode 100644 signatures/apps.sigs create mode 100644 signatures/context.sigs create mode 100644 signatures/files.sigs create mode 100644 signatures/messages.sigs create mode 100644 signatures/mime.sigs create mode 100644 signatures/signatures.conf rename alloc-inl.h => src/alloc-inl.h (100%) rename analysis.c => src/analysis.c (87%) rename analysis.h => src/analysis.h (94%) create mode 100644 src/auth.c create mode 100644 src/auth.h rename checks.c => src/checks.c (75%) rename checks.h => src/checks.h (65%) rename config.h => src/config.h (97%) rename crawler.c => src/crawler.c (99%) rename crawler.h => src/crawler.h (99%) rename database.c => src/database.c (96%) rename database.h => src/database.h (95%) rename debug.h => src/debug.h (99%) rename http_client.c => src/http_client.c (96%) rename http_client.h => src/http_client.h (97%) rename report.c => src/report.c (98%) rename report.h => src/report.h (100%) rename same_test.c => src/same_test.c (100%) create mode 100644 src/signatures.c create mode 100644 src/signatures.h rename skipfish.c => src/skipfish.c (73%) rename string-inl.h => src/string-inl.h (88%) rename types.h => src/types.h (100%) rename sfscandiff => tools/sfscandiff (100%) 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