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.
This commit is contained in:
Steve Pinkham 2012-09-12 17:06:51 -04:00
parent a655d5853c
commit c9d5b74896
39 changed files with 2551 additions and 544 deletions

View File

@ -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:
--------------

View File

@ -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

29
README
View File

@ -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!
-------------------------------------

View File

@ -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)"
};

View File

@ -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 ~

98
doc/authentication.txt Normal file
View File

@ -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...]

152
doc/signatures.txt Normal file
View File

@ -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:[!]"<string>"
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:<int>
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 <title> 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

13
signatures/apps.sigs Normal file
View File

@ -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()</title><meta name="; depth:2048; memo:"phpinfo() page";
# A phpmyadmin page
id:11002; sev:3; content:'<title>phpMyAdmin </title>'; depth:1024; content:'<a href="http://www.phpmyadmin.net" target="_blank" class="logo">'; depth:2048; memo:"phpMyAdmin";
id:11003; sev:3; content:"<title>Parallels Plesk Panel"; depth:1024; content:'action="/login_up.php3" method="post"'; memo:"Plesk administrative interface";

24
signatures/context.sigs Normal file
View File

@ -0,0 +1,24 @@
###############################
# CONTEXT SPECIFIC SIGNATURES
#
# During some injection tests we might not be able to determine that
# the server is vulnerable. However, some server messages can give away
# the fact that the underlying logic can likely be abused. The signatures
# in this file look for such messages and are all linked to individual
# injection tests. For example: we look for I/O errors during local file
# disclosure attacks.
#####################################################
# Signatures for check 8: File inclusion / disclosure
# PHP errors for include(), fopen(), include_once(), file_get_contents(), etc
id:51001; sev:2; check:8; content:"[<a href='function."; content:"</a>]: failed to open stream:"; depth:100; memo:"PHP inclusion errors (during traversal tests)";
id:51002; sev:2; check:8; content:"Warning: "; content:": failed to open stream: "; depth:100; memo:"PHP inclusion errors (during traversal tests)";
# Java IO exception
id:51003; sev:2; check:8; content:"java.io.FileNotFoundException: "; content:"at java.io."; depth:200; memo:"Java FileNotFound exception (during traversal tests)";
# Python IO exception
id:51004; sev:2; check:8; content:"Traceback (most recent call last):"; content:"No such file or directory: "; depth:512; memo:"Python IO backtrace (during traversal tests)";

42
signatures/files.sigs Normal file
View File

@ -0,0 +1,42 @@
####################################
# INTERESTING PAGES / FILES
# Detect private keys
id:31001; sev:2; mime:"text/plain"; content:"-----BEGIN DSA PRIVATE KEY-----"; depth:100; memo:"DSA private key";
id:31002; sev:2; mime:"text/plain"; content:"-----BEGIN RSA PRIVATE KEY-----"; depth:100; memo:"RSA private key";
id:31003; sev:3; content:'ADDRESS=(PROTOCOL='; memo:"SQL configuration or logs";
id:31004; sev:3; content:";pwd="; content:";database="; depth:512; memo:"ODBC connect string";
id:31005; sev:3; content:"Data Source="; content:";Password="; depth:512; memo:"ODBC connect string";
id:31006; sev:3; content:"Provider="; content:";Password="; depth:512; memo:"ODBC connect string";
id:31007; sev:3; content:"Driver="; content:";Pwd="; depth:512; memo:"ODBC connect string";
# Typical crossdomain / access policy files
id:31008; sev:3; content:"<cross-domain-policy>"; depth:512; memo:"Flash crossdomain file";
id:31009; sev:3; content:"<access-policy>"; depth:512; memo:"Silverlight cross-domain policy";
# Web.xml config file
id:31010; sev:3; content:"<web-app"; depth:512; memo:"web.xml config file";
# SVN RCS data
id:31011; sev:3; content:"svn:special svn"; depth:256; memo:"SVN RCS data";
id:31012; sev:3; content:"SVN RCS data"; depth:256; memo:"SVN RCS data";
# Log files
id:31013; sev:3; content:"0] \"GET /"; depth:1024; memo:"Apache access log";
id:31014; sev:3; content:"[error] [client "; depth:1024; memo:"Apache error log";
id:31015; sev:3; content:"0, GET, /"; depth:1024; memo:"Microsoft IIS access log";
# Source code and scripts
id:32001; sev:3; content:"\nimport java."; depth:512; memo:"Java source";
id:32002; sev:3; content:"\n#include"; depth:512; memo:"C/C++ source";
id:32003; sev:3; content:"#!/"; depth:1; memo:"Shell script";
id:32004; sev:3; content:!"# ?>" content:!"<?import"; content:"<?"; content:!"xml"; depth:1; content:"?>"; memo:"PHP source";
id:32005; sev:3; content:"<%@"; content:"%>"; memo:"JSP source";
id:32006; sev:3; content:"<%"; content:"%>"; memo:"ASP source";
# These two need to be improved!
id:32007; sev:3; content:"@echo "; depth:256; memo:"DOS batch script";
id:32008; sev:3; content:"(\"Wscript."; depth:256; memo:"Windows shell script";

45
signatures/messages.sigs Normal file
View File

@ -0,0 +1,45 @@
#####################################
# INTERESTING SERVER ERRORS
# SQL related error strings
id:21001; prob:40402; content:"<b>Warning</b>: MySQL: "; memo:"MySQL error string";
id:21002; prob:40402; content:"Unclosed quotation mark"; memo:"SQL error string";
id:21003; prob:40402; content:"java.sql.SQLException:"; memo:"Java SQL exception";
id:21004; prob:40402; content:"SqlClient.SqlException: Syntax error"; memo:"SqlClient exception";
id:21005; prob:40402; content:"PostgreSQL query failed"; memo:"PostgreSQL query failed";
id:21006; prob:40402; content:"Dynamic SQL Error"; memo:"SQL error string";
id:21007; prob:40402; content:"unable to perform query"; memo:"Possible SQL error string";
id:21008; prob:40402; content:"Microsoft OLE DB Provider for ODBC Drivers</font>"; memo:"OLE SQL error";
id:21009; prob:40402; content:"[Microsoft][ODBC SQL Server Driver]"; memo:"Microsoft SQL error";
id:21010; prob:40402; content:"Syntax error in string in query expression"; memo:"SQL syntax string";
id:21011; prob:40402; content:"You have an error in your SQL syntax; "; memo:"SQL syntax error";
id:21012; prob:40402; content:"Incorrect syntax near"; memo:"SQL syntax error";
id:21013; prob:40402; content:"[DM_QUERY_E_SYNTAX]"; memo:"SQL syntax error";
# Stacktraces and server errors
id:22001; prob:40402; content:"<span><H1>Server Error in '"; memo:"ASP.NET Yellow Screen of Death";
id:22002; prob:40402; content:"<font face=\"Arial\" size=2>error '"; memo:"Microsoft runtime error";
id:22003; prob:40402; content:"[an error occurred while processing"; memo:"SHTML error";
id:22004; prob:40402; content:"Traceback (most recent call last):"; memo:"Python error";
id:22005; prob:40402; content:"<title>JRun Servlet Error</title>"; memo:"JRun servlet error";
# Java exceptions
id:22006; prob:40402; content:"Stacktrace:"; content:"javax.servlet."; content:"<b>note</b> <u>The full stack trace"; memo:"Java server stacktrace";
id:22007; prob:40402; content:"at java.lang.Thread.run"; content:".java:"; memo:"Java runtime stacktrace";
id:22020; prob:40402; content:"<b>type</b> Exception report</p><p>"; content:"<p><b>description</b> <u>The server "; depth:512; memo:"Java server exception";
# PHP HTML and text errors. The text and HTML sigs can perhaps be merged,
id:22008; prob:40402; content:"<b>Fatal error</b>: "; content:"</b> on line <b>"; depth:512; memo:"PHP error (HTML)";
id:22009; prob:40402; content:"Fatal error: "; content:" on line "; depth:512; memo:"PHP error (text)";
id:22010; prob:40402; content:"<b>Parse error</b>: "; content:"</b> on line <b>"; depth:512; memo:"PHP parse error (HTML)";
id:22011; prob:40402; content:"Parse error: "; content:" on line "; depth:512; memo:"PHP parse error (text)";
id:22012; prob:40402; content:"<b>Notice</b>: "; content:"</b> on line <b>"; depth:512; memo:"PHP notice (HTML)";
id:22013; prob:40402; content:"Notice: "; content:" on line "; depth:512; memo:"PHP notice (text)";
id:22014; prob:40402; content:"<b>Strict Standards</b>: "; content:"</b> on line <b>"; depth:512; memo:"PHP warning (HTML)";
id:22015; prob:40402; content:"Strict Standards: "; content:" on line "; depth:512; memo:"PHP warning (text)";
id:22016; prob:40402; content:"<b>Catchable fatal error</b>: "; content:"</b> on line <b>"; depth:512; memo:"PHP error (HTML)";
id:22017; prob:40402; content:"Catchable fatal error: "; content:" on line "; depth:512; memo:"PHP error (text)";
id:22018; prob:40402; content:"<b>Warning</b>: "; content:"</b> on line <b>"; depth:512; memo:"PHP warning (HTML)";
id:22019; prob:40402; content:"Warning: "; content:" on line "; depth:512; memo:"PHP warning (text)";

13
signatures/mime.sigs Normal file
View File

@ -0,0 +1,13 @@
####################################
# INTERESTING MIME TYPES
id:41001; sev:4; mime:"application/vnd.ms-excel"; memo:"Microsoft Excel spreadsheet (mime)";
id:41002; sev:4; mime:"application/vnd.ms-project"; memo:"Microsoft Project file (mime)";
id:41003; sev:4; mime:"application/x-httpd-php-source"; memo:"PHP source file (mime)";
id:41004; sev:4; mime:"application/x-msdos-program"; memo:"DOS executable or script (mime)";
id:41005; sev:4; mime:"application/x-msi"; memo:"MSI installer (mime)";
id:41006; sev:3; mime:"application/x-python-code"; memo:"Python code (mime)";
id:41007; sev:3; mime:"application/x-shellscript"; memo:"Shell script (mime)";

View File

@ -0,0 +1,28 @@
#############################################
##
## Master signature file.
##
# The mime signatures warn about server responses that have an interesting
# mime. For example anything that is presented as php-source will likely
# be interesting
include signatures/mime.sigs
# The files signature will use the content to determine if a response
# is an interesting file. For example, a SVN file.
include signatures/files.sigs
# The messages signatures look for interesting server messages. Most
# are based on errors, such as caused by incorrect SQL queries or PHP
# execution failures.
include signatures/messages.sigs
# The apps signatures will help to find pages and applications who's
# functionality is a security risk by default. For example, phpinfo()
# pages that leak information or CMS admin interfaces.
include signatures/apps.sigs
# Context signatures are linked to injection tests. They look for strings
# that are relevant to the current injection test and help to highlight
# potential vulnerabilities.
include signatures/context.sigs

View File

@ -29,6 +29,8 @@
#include "database.h"
#include "crawler.h"
#include "analysis.h"
#include "signatures.h"
#include "pcre.h"
u8 no_parse, /* Disable HTML link detection */
warn_mixed, /* Warn on mixed content */
@ -448,10 +450,10 @@ static u8 maybe_xsrf(u8* token) {
/* Another helper for scrape_response(): examines all <input> tags
up until </form>, then adds them as parameters to current request. */
static void collect_form_data(struct http_request* req,
struct http_request* orig_req,
struct http_response* orig_res,
u8* cur_str, u8 is_post) {
void collect_form_data(struct http_request* req,
struct http_request* orig_req,
struct http_response* orig_res,
u8* cur_str, u8 is_post) {
u8 has_xsrf = 0, pass_form = 0, file_form = 0;
u32 tag_cnt = 0;
@ -637,6 +639,70 @@ static u8 is_mostly_ascii(struct http_response* res) {
}
struct http_request* make_form_req(struct http_request *req,
struct http_request *base,
u8* cur_str, u8* target) {
u8 *method, *clean_url;
u8 *dirty_url;
struct http_request* n;
u8 parse_form = 1;
FIND_AND_MOVE(dirty_url, cur_str, "action=");
FIND_AND_MOVE(method, cur_str, "method=");
/* See if we need to POST this form or not. */
if (method && *method) {
if (strchr("\"'", *method)) method++;
if (tolower(method[0]) == 'p') parse_form = 2;
}
/* If a form target is specified, we need to use that */
if (target) {
dirty_url = ck_strdup(target);
} else if (!dirty_url || !*dirty_url || !prefix(dirty_url, "\"\"") ||
!prefix(dirty_url, "''")) {
/* Forms with no URL submit to current location. */
dirty_url = serialize_path(req, 1, 0);
} else {
/* Last, extract the URL from the tag */
EXTRACT_ALLOC_VAL(dirty_url, dirty_url);
}
clean_url = html_decode_param(dirty_url, 0);
ck_free(dirty_url);
n = ck_alloc(sizeof(struct http_request));
n->pivot = req->pivot;
if (parse_form == 2) {
ck_free(n->method);
n->method = ck_strdup((u8*)"POST");
} else {
/* On GET forms, strip existing query params to get a submission
target. */
u8* qmark = (u8*)strchr((char*)clean_url, '?');
if (qmark) *qmark = 0;
}
if (parse_url(clean_url, n, base ? base : req)) {
DEBUG("Unable to parse_url from form: %s\n", clean_url);
ck_free(clean_url);
destroy_request(n);
return NULL;
}
ck_free(clean_url);
return n;
}
/* Analyzes response headers (Location, etc), body to extract new links,
keyword guesses. This code is designed to be simple and fast, but it
does not even try to understand the intricacies of HTML or whatever
@ -687,7 +753,8 @@ void scrape_response(struct http_request* req, struct http_response* res) {
if (*cur_str == '<' && (tag_end = (u8*)strchr((char*)cur_str + 1, '>'))) {
u32 link_type = 0;
u8 set_base = 0, parse_form = 0;
u8 set_base = 0;
u8 is_post = 0;
u8 *dirty_url = NULL, *clean_url = NULL, *meta_url = NULL,
*delete_dirty = NULL;
@ -747,25 +814,16 @@ void scrape_response(struct http_request* req, struct http_response* res) {
} else if (ISTAG(cur_str, "form")) {
u8* method;
parse_form = 1;
FIND_AND_MOVE(dirty_url, cur_str, "action=");
/* Parse the form and kick off a new pivot for further testing */
struct http_request* n = make_form_req(req, base, cur_str, NULL);
if (n) {
if (url_allowed(n) && R(100) < crawl_prob && !no_forms) {
is_post = (n->method && !strcmp((char*)n->method, "POST"));
/* See if we need to POST this form or not. */
FIND_AND_MOVE(method, cur_str, "method=");
if (method && *method) {
if (strchr("\"'", *method)) method++;
if (tolower(method[0]) == 'p') parse_form = 2;
}
/* Forms with no URL submit to current location. */
if (!dirty_url || !*dirty_url || !prefix(dirty_url, (char*)"\"\"") ||
!prefix(dirty_url, (char*)"''")) {
dirty_url = serialize_path(req, 1, 0);
delete_dirty = dirty_url;
collect_form_data(n, req, res, tag_end + 1, is_post);
maybe_add_pivot(n, NULL, 5);
}
destroy_request(n);
}
} else {
@ -806,38 +864,6 @@ void scrape_response(struct http_request* req, struct http_response* res) {
n->pivot = req->pivot;
if (!parse_url(clean_url, n, base ? base : req)) base = n;
} else if (parse_form) {
/* <form> handling... */
struct http_request* n = ck_alloc(sizeof(struct http_request));
n->pivot = req->pivot;
if (parse_form == 2) {
ck_free(n->method);
n->method = ck_strdup((u8*)"POST");
} else {
/* On GET forms, strip existing query params to get a submission
target. */
u8* qmark = (u8*)strchr((char*)clean_url, '?');
if (qmark) *qmark = 0;
}
/* Don't collect form fields, etc, if target is not within the
scope anyway. */
DEBUG("* Found form: target %s method %s\n", clean_url, n->method);
if (!parse_url(clean_url, n, base ? base : req) && url_allowed(n) &&
R(100) < crawl_prob && !no_forms) {
collect_form_data(n, req, res, tag_end + 1, (parse_form == 2));
maybe_add_pivot(n, NULL, 5);
}
destroy_request(n);
}
next_tag:
@ -1287,11 +1313,21 @@ static void check_js_xss(struct http_request* req, struct http_response* res,
while(*end_quote && end_quote++ && *end_quote != in_quot)
if(*end_quote == '\\') end_quote++;
/* Injected string is 'skip'''"fish""" */
if(end_quote && !case_prefix(end_quote + 1,"skip'''"))
problem(PROB_URL_XSS, req, res, (u8*)"injected string in JS/CSS code (single quote not escaped)", req->pivot, 0);
if(end_quote && !case_prefix(end_quote + 1,"fish\"\"\""))
problem(PROB_URL_XSS, req, res, (u8*)"injected string in JS/CSS code (double quote not escaped)", req->pivot, 0);
/* Injected string is 'skip'''"fish""" (or it's encoded variants */
if(end_quote && (!case_prefix(end_quote + 1,"skip'''") ||
!case_prefix(end_quote + 1,"fish\"\"\"")))
problem(PROB_URL_XSS, req, res, (u8*)"injected string in JS/CSS code (quote escaping issue)", req->pivot, 0);
if(end_quote && (!prefix(last_word, "on") ||
!prefix(last_word, "url") ||
!prefix(last_word, "href")) &&
(!case_prefix(end_quote + 1,"skip&apos;&apos;&apos;") ||
!case_prefix(end_quote + 1,"skip&#x27;&#x27;&#x27;") ||
!case_prefix(end_quote + 1,"skip&quot;&quot;&quot;") ||
!case_prefix(end_quote + 1,"skip&#x22;&#x22;&#x22;"))) {
problem(PROB_URL_XSS, req, res, (u8*)"injected string in JS/CSS code (html encoded)", req->pivot, 0);
}
} else if (in_quot && *text == in_quot) in_quot = 0;
@ -1430,10 +1466,13 @@ u8 content_checks(struct http_request* req, struct http_response* res) {
u8* tmp;
u32 off, tag_id, scan_id;
u8 high_risk = 0;
struct http_request* n;
DEBUG_CALLBACK(req, res);
/* CHECK 0: signature matching */
match_signatures(req, res);
/* CHECK 1: Caching header logic. */
if (req->proto == PROTO_HTTP) {
@ -1581,34 +1620,6 @@ u8 content_checks(struct http_request* req, struct http_response* res) {
if (is_javascript(res) || is_css(res)) check_js_xss(req, res, res->payload);
/* 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 (is_javascript(res) && !res->json_safe &&
(!req->method || !strcmp((char*)req->method, "GET")) &&
!inl_findstr(res->payload, (u8*)"if (", 1024) &&
!inl_findstr(res->payload, (u8*)"if(", 1024) &&
!inl_findstr(res->payload, (u8*)"for (", 1024) &&
!inl_findstr(res->payload, (u8*)"for(", 1024) &&
!inl_findstr(res->payload, (u8*)"while (", 1024) &&
!inl_findstr(res->payload, (u8*)"while(", 1024) &&
!inl_findstr(res->payload, (u8*)"function ", 1024) &&
!inl_findstr(res->payload, (u8*)"function(", 1024))
problem(PROB_JS_XSSI, req, res, NULL, req->pivot, 0);
/* If the response resembles javascript and a callback parameter does
not exist, we'll add this parameter in an attempt to catch JSONP
issues */
if(is_javascript(res) && !GET_PAR((u8*)"callback", &req->par)) {
n = req_copy(RPREQ(req), req->pivot, 1);
SET_PAR((u8*)"callback",(u8*)"hello",&n->par);
maybe_add_pivot(n, NULL, 2);
destroy_request(n);
}
tmp = res->payload;
do {
@ -1655,9 +1666,10 @@ u8 content_checks(struct http_request* req, struct http_response* res) {
/* Name followed by '='? Grab value. */
u8 quote = 0;
if (*tmp == '=') {
u32 vlen;
u8 save, quote = 0;
u8 save;
tmp++;
@ -1676,6 +1688,11 @@ u8 content_checks(struct http_request* req, struct http_response* res) {
tmp += vlen + quote;
}
/* CHECK X.X: Unquoted value can allow parameter XSS */
if (!quote && clean_val &&
!case_prefix(clean_val, "skipfish:"))
problem(PROB_TAG_XSS, req, res, tag_name, req->pivot, 0);
if (!strcasecmp((char*)tag_name, "script") &&
!strcasecmp((char*)param_name, "src")) remote_script = 1;
@ -1688,6 +1705,11 @@ u8 content_checks(struct http_request* req, struct http_response* res) {
strcasecmp((char*)tag_name, "input")) ||
!strcasecmp((char*)param_name, "codebase")) && clean_val) {
/* Check links with the javascript scheme */
if (!case_prefix(clean_val, "javascript:") ||
!case_prefix(clean_val, "vbscript:"))
check_js_xss(req, res, clean_val);
if (!case_prefix(clean_val, "skipfish:"))
problem(PROB_URL_XSS, req, res, tag_name, req->pivot, 0);
@ -2306,76 +2328,6 @@ static void check_for_stuff(struct http_request* req,
/* Assorted interesting error messages. */
if (strstr((char*)res->payload, "<font face=\"Arial\" size=2>error '")) {
problem(PROB_ERROR_POI, req, res, (u8*)"Microsoft runtime error",
req->pivot, 0);
return;
}
if (strstr((char*)res->payload, "<span><H1>Server Error in '")) {
problem(PROB_ERROR_POI, req, res, (u8*)
"ASP.NET Yellow Screen of Death", req->pivot, 0);
return;
}
if (strstr((char*)sniffbuf, "<title>JRun Servlet Error</title>")) {
problem(PROB_ERROR_POI, req, res, (u8*)"JRun servlet error", req->pivot, 0);
return;
}
if (strstr((char*)res->payload, "Exception in thread \"") ||
strstr((char*)res->payload, "at java.lang.") ||
(strstr((char*)res->payload, "\tat ") &&
(strstr((char*)res->payload, ".java:")))) {
problem(PROB_ERROR_POI, req, res, (u8*)"Java exception trace", req->pivot, 0);
return;
}
if ((tmp = (u8*)strstr((char*)res->payload, " on line "))) {
u32 off = 512;
while (tmp - 1 > res->payload && !strchr("\r\n", tmp[-1])
&& off--) tmp--;
if (off && (!prefix(tmp, "Warning: ") || !prefix(tmp, "Notice: ") ||
!prefix(tmp, "Fatal error: ") || !prefix(tmp, "Parse error: ") ||
!prefix(tmp, "Deprecated: ") ||
!prefix(tmp, "Strict Standards: ") ||
!prefix(tmp, "Catchable fatal error: "))) {
problem(PROB_ERROR_POI, req, res, (u8*)"PHP error (text)", req->pivot, 0);
return;
}
if (off && !prefix(tmp, "<b>") && (!prefix(tmp + 3, "Warning</b>: ") ||
!prefix(tmp + 3, "Notice</b>: ") ||
!prefix(tmp + 3, "Fatal error</b>: ") ||
!prefix(tmp + 3, "Parse error</b>: ") ||
!prefix(tmp + 3, "Deprecated</b>: ") ||
!prefix(tmp + 3, "Strict Standards</b>: ") ||
!prefix(tmp + 3, "Catchable fatal error</b>: "))) {
problem(PROB_ERROR_POI, req, res, (u8*)"PHP error (HTML)", req->pivot, 0);
return;
}
}
if (strstr((char*)res->payload, "<b>Warning</b>: MySQL: ") ||
strstr((char*)res->payload, "Unclosed quotation mark") ||
strstr((char*)res->payload, "Syntax error in string in query expression") ||
strstr((char*)res->payload, "java.sql.SQLException") ||
strstr((char*)res->payload, "SqlClient.SqlException: Syntax error") ||
strstr((char*)res->payload, "Incorrect syntax near") ||
strstr((char*)res->payload, "PostgreSQL query failed") ||
strstr((char*)res->payload, "Dynamic SQL Error") ||
strstr((char*)res->payload, "unable to perform query") ||
strstr((char*)res->payload, "Microsoft OLE DB Provider for ODBC Drivers</font>") ||
strstr((char*)res->payload, "[Microsoft][ODBC SQL Server Driver]") ||
strstr((char*)res->payload, "You have an error in your SQL syntax; ") ||
strstr((char*)res->payload, "[DM_QUERY_E_SYNTAX]")) {
problem(PROB_ERROR_POI, req, res, (u8*)"SQL server error", req->pivot, 0);
return;
}
if (((tmp = (u8*)strstr((char*)res->payload, "ORA-")) ||
(tmp = (u8*)strstr((char*)res->payload, "FRM-"))) &&
isdigit(tmp[4]) && tmp[9] == ':') {
@ -2383,54 +2335,6 @@ static void check_for_stuff(struct http_request* req,
return;
}
if (strstr((char*)res->payload, "[an error occurred while processing")) {
problem(PROB_ERROR_POI, req, res, (u8*)"SHTML error", req->pivot, 0);
return;
}
if (strstr((char*)res->payload, "Traceback (most recent call last):")) {
problem(PROB_ERROR_POI, req, res, (u8*)"Python error", req->pivot, 0);
return;
}
/* Interesting files. */
if (strstr((char*)res->payload, "ADDRESS=(PROTOCOL=")) {
problem(PROB_FILE_POI, req, res, (u8*)"SQL configuration or logs", req->pivot, 0);
return;
}
if (inl_strcasestr(res->payload, (u8*)";database=") &&
inl_strcasestr(res->payload, (u8*)";pwd=")) {
problem(PROB_FILE_POI, req, res, (u8*)"ODBC connect string", req->pivot, 0);
return;
}
if (strstr((char*)sniffbuf, "<cross-domain-policy>")) {
problem(PROB_FILE_POI, req, res, (u8*)
"Flash cross-domain policy", req->pivot, 0);
/*
if (strstr((char*)res->payload, "domain=\"*\""))
problem(PROB_CROSS_WILD, req, res, (u8*)
"Cross-domain policy with wildcard rules", req->pivot, 0);
*/
return;
}
if (strstr((char*)sniffbuf, "<access-policy>")) {
problem(PROB_FILE_POI, req, res, (u8*)"Silverlight cross-domain policy",
req->pivot, 0);
/*
if (strstr((char*)res->payload, "uri=\"*\""))
problem(PROB_CROSS_WILD, req, res, (u8*)
"Cross-domain policy with wildcard rules", req->pivot, 0);
*/
return;
}
if (inl_strcasestr(sniffbuf, (u8*)"\nAuthType ") ||
(inl_strcasestr(sniffbuf, (u8*)"\nOptions ") && (
@ -2481,11 +2385,6 @@ static void check_for_stuff(struct http_request* req,
return;
}
if (strstr((char*)sniffbuf, "<web-app")) {
problem(PROB_FILE_POI, req, res, (u8*)"web.xml config file", req->pivot, 0);
return;
}
/* Add more directory signatures here... */
if (strstr((char*)sniffbuf, "<A HREF=\"?N=D\">") ||
@ -2493,6 +2392,9 @@ static void check_for_stuff(struct http_request* req,
strstr((char*)sniffbuf, "<h1>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, "<wc-entries") ||
strstr((char*)sniffbuf, "svn:special svn:")) {
problem(PROB_FILE_POI, req, res, (u8*)"SVN RCS data", req->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, "<?") && strstr((char*)res->payload, "?>") &&
!strstr((char*)sniffbuf, "<?xml") && !strstr((char*)res->payload, "# ?>") &&
!strstr((char*)res->payload, "<?import")) {
problem(PROB_FILE_POI, req, res, (u8*)"PHP source", req->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;
}
}

View File

@ -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 <input> tags up until </form>, 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 */

279
src/auth.c Normal file
View File

@ -0,0 +1,279 @@
/*
skipfish - form authentication
------------------------------
Author: Niels Heinen <heinenn@google.com>
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 <string.h>
#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*)"<form");
if (!form) break;
if (auth_form_target)
vurl = ck_strdup(auth_form_target);
n = make_form_req(req, NULL, form, vurl);
if (!n)
FATAL("No auth form found\n");
is_post = (n->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; i<n->par.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;
}

74
src/auth.h Normal file
View File

@ -0,0 +1,74 @@
/*
skipfish - form authentication matching
----------------------------------------
Author: Niels Heinen <heinenn@google.com>
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 */

View File

@ -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; i++) {
SAY(" -- [%2d] %s \t%s\n", i-cb_handle_off, cb_handles[i].name,
SAY(" -- [%2d] %-25s %s\n", i-cb_handle_off, cb_handles[i].name,
cb_handles[i].skip ? "(disabled)" : "");
}
SAY("\n");
}
/* Disable tests by parsing a comma separated list which we received
from the command-line */
from the command-line. */
void toggle_injection_checks(u8* str, u32 enable) {
void toggle_injection_checks(u8* str, u32 enable, u8 user) {
u32 tnr;
u8* ptr;
ptr = (u8*)strtok((char*)str, ",");
/* If this is user input, we only allow check manipulation. Else,
we also allow other tests, such as for stability to be toggled */
u32 offset = user ? cb_handle_off : 0;
/* Copy the string for manipulation */
u8* ids = ck_strdup(str);
ptr = (u8*)strtok((char*)ids, ",");
for (; ptr != NULL ;){
tnr = atoi((char*)ptr);
@ -197,20 +239,20 @@ void toggle_injection_checks(u8* str, u32 enable) {
if (tnr > 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; i<req->pivot->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; i<req->pivot->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; i<req->pivot->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;i<global_http_par.c;i++) {
if (global_http_par.t[i] != PARAM_COOKIE)
@ -919,6 +1215,8 @@ static u8 inject_inclusion_check(struct http_request* req,
DEBUG_MISC_CALLBACK(req, res);
u32 not_found = 0;
/*
Perform directory traveral and file inclusion tests.
@ -948,7 +1246,7 @@ static u8 inject_inclusion_check(struct http_request* req,
} else if (inl_findstr(MRES(4)->payload, (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*)"<servlet-mapping>", 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, "&apos;skip&apos;&apos;&apos;&quot;fish&quot;&quot;&quot;");
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; i<req->pivot->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. */

View File

@ -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);

View File

@ -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 */

View File

@ -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;
}

View File

@ -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: */

View File

@ -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; i<pv->issue_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);
}

View File

@ -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 */
};

View File

@ -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 */

View File

@ -35,6 +35,7 @@
#include <time.h>
#include <openssl/ssl.h>
#include <openssl/x509v3.h>
#include <openssl/err.h>
#include <idna.h>
#include <zlib.h>
@ -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 && i<acnt; i++) {
const GENERAL_NAME *name = sk_GENERAL_NAME_value(altnames, i);
if (name->type != 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;

View File

@ -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

View File

@ -206,12 +206,18 @@ static void maybe_add_sig(struct pivot_desc* pv) {
/* See if a matching signature already exists. */
for (i=0;i<p_sig_cnt;i++)
for (i=0;i<p_sig_cnt;i++)
if (p_sig[i].type == pv->type && 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;c<i_samp_cnt;c++)
for (c=0;c<i_samp_cnt;c++)
if (i_samp[c].type == pv->issue[i].type) break;
if (c == i_samp_cnt) {

570
src/signatures.c Normal file
View File

@ -0,0 +1,570 @@
/*
skipfish - Signature Matching
----------------------------------------
Author: Niels Heinen <heinenn@google.com>,
Sebastian Roschke <s.roschke@gmail.com>
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 <stdio.h>
#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; i<len; i++) {
if(str[i] == '\\')
continue;
ret[k++] = str[i];
}
return ret;
}
/* Checks the signature and returns 1 if there is an error. This function is
* expected to give warnings that help to resolve the signature error */
static u8 check_signature(struct signature *sig) {
u8 ret = 0;
if (sig->severity < 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 && i<sig_list[j]->content_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; i<sig->content_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; i<sig->content_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);
}

148
src/signatures.h Normal file
View File

@ -0,0 +1,148 @@
/*
skipfish - signature matching
----------------------------------------
Author: Niels Heinen <heinenn@google.com>
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 */

View File

@ -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 <lcamtuf@google.com>.\n", argv0,
"Send comments and complaints to <heinenn@google.com>.\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 <lcamtuf@google.com>\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();

View File

@ -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;
}