blob: 6e0eb91f7b6e457bed4111d577cebadc4f6b7598 [file] [log] [blame] [raw]
_ _ _
| \ | | __ ___ _____(_)
| \| |/ _` \ \/ / __| |
| |\ | (_| |> <\__ \ |
|_| \_|\__,_/_/\_\___/_|
v0.1 alpha
[[[!!! PLEASE REFER TO THE WIKI INSTEAD !!!]]]
Yes, this readme is for v0.1 alpha, now is 0.46-1
|--{ Introduction }------------------------------------------------------------|
NAXSI is a module for nginx, the famous webserver/reverse-proxy/...
Its goal is to help people to secure their web application against attacks
such as SQL Injection, Cross Site Scripting, Cross Site Request Forgery,
Local & Remote file inclusions and such.
The difference with most WAF (Web Applicative Firewalls) out there is that
it does not rely on signatures to detect attacks. It is using a simpler model,
where instead of trying to detect "known" attacks, it will detect unexpected
characters in the HTTP request/arguments. Each kind of unusual character will
increase the score of the request. If the request reaches a score that's
considered "too high", the request will be denied, and the user will be
redirected to a "forbidden" page. Yes, it works a bit like a spam system.
|--{ Why is it different ? }--------------------------------------------------|
NAXSI is different, because it works on a learning mode (read whitelist).
Set the module in learning mode, scroll your site, and it will generate the
necessaries whitelists to avoid false positives !
NAXSI doesn't relies on signatures, so it should be capable of defeating
complex/unknown/obfuscated attack
patterns.
|--{ How does it work }------------------------------------------------------|
NAXSI relies on two separate configuration parts.
* Core Rules : Located at HTTP server level configuration.
* WhiteLists & Specific Rules : Located at the HTTP location
level configuration.
The first one is what we called 'core rules'.
It's a set of rules that will contain all characters or regular expression
that will increase the score of the request, for exemple :
MainRule "rx:<|>" "msg:html tag ?" "mz:ARGS|URL|BODY" "s:$XSS:8" id:1302;
This rule, will match on both < and > characters (rx:<|>), and will increase
the score associated to the XSS threat (s:$XSS:8). This pattern will be matched
against various zones of the request : ARGS (GET arguments),
URL (the full URI), and BODY (POST arguments). Each rule is associated to
unique ID (here, 1302), that is used for whitelisting.
There is not "many" core rules (34 at the time of writting), and this set
should normally not evolve.
On the other hand, we have a "local" configuration, which is to be defined
"per site" (as NAXSI main goal is to work with NGINX as a RP), and which will
define "how strict" the security policy of the site will be, as well as
putting exceptions (whitelists) according the site specificities :
-----------------------------------8<-------------------------------------------
# Define to which "location" the user will be redirected when a request is
# denied.
DeniedUrl "/RequestDenied";
# Whitelist '|', as it's used on the /report/ page, in argument 'd'
BasicRule wl:1005 "mz:$URL:/report/|$ARGS_VAR:d";
# Whitelist ',' on URL zone as it's massively used for URL rewritting !
BasicRule wl:1008 "mz:URL";
# Check rules
CheckRule "$SQL >= 8" BLOCK;
CheckRule "$RFI >= 8" BLOCK;
CheckRule "$TRAVERSAL >= 4" BLOCK;
CheckRule "$XSS >= 8" BLOCK;
-----------------------------------8<-------------------------------------------
Let's see this configuration again to clarify things :
* 'BasicRule' : This directive is used here to whitelist some rules.
As you can see (and expect !) you can be more or less laxist.
For exemple, there is a directive (BasicRule wl:1008 ...) that will totally
disable the rule 1008 checking against URL. This rule is normally matching
the ',' character.
On the other hand, you can make extremly precise rules, as this one :
BasicRule wl:1005 "mz:$URL:/report/|$ARGS_VAR:d";
In the last exemple, we are whitelisting a rule, but only on one specific
argument of one specific webpage (the argument named 'd' of the page
'/report/').
As stated earlier, "local" configuration is also used to decide how
"strict" one site (or one part of one site) will be, that is, what is
the maximal tolerated page score before a request is denied :
CheckRule "$SQL >= 8" BLOCK;
This directive tells NAXSI that every request that has a 'SQL' score superior
or equal to 8 will be denied.
|--{ Denied requests and whitelist generation }------------------------------|
When a user's request is denied, he will be (internally) redirected to the page
defined in the configuration :
DeniedUrl "/RequestDenied";
This page will receive detailed informations on the rules that matched
(it's logued in log files too), as well as both the request and the user
context, without giving information to the user (thanks to nginx internal
redirects). Actually the page at /RequestDenied will be called with arguments,
like :
server=xxxx&uri=/vulnerable.php&ip=127.0.0.1&zone0=ARGS&id0=1010&var_name0=foo1&zone1=ARGS&id1=1011&...
If you have a closer look to the URL above, you will understand that the
following information will be transmitted to the forbidden page :
- Which rule matched on which argument, in which part of the request (ARGS,
BODY, URL, HEADERS ...)
- Original URL and hostname
- Client IP adress
Those information are given for both statistics generation purpose, as well as
for whitelist generation purposes.
Yes, actually that's a very important aspect of NAXSI : As it is heavily
relying on whitelist for configuration, the easiest the whitelist generation is
, the easier the configuration is ! The thing being that the DeniedUrl page
receives enough information to generate a set of whitelisting rules that will
allow the request that was blocked to be allowed in the future.
To make it clear : you can generate the whole NAXSI configuration, only by
naviguating to the site, or even better, by using a clever crawler !
When you will do a navigation session on the website you want to create rules
for, if you are in "LearningMode", some requests might be (and will be) tagued
as blocked. They should be blocked, because, for example, the developpers
(damn!) decided to massively use '|' for URL rewritting, and NAXSI
will dislike this.
So, as you are in learning mode (but the idea is the same when LearningMode is
disabled), NAXSI will log the fact that the request was blocked because of the
presence of multiples '|' in the URL. Then, with a simple script (a python
script is provided), you can parse the log files and generate the appropriate
whiterules to allow legitimate (false positive) requests.
The more tricky part when talking about NAXSI and its whitelist, is when we
come to sites that allows a LOT of wide user input : Comments, Registration
forms and things like this. For this, either a carefull navigation, submitting
real content is required (so that NAXSI will trigger every plausible rule on
each kind of form field) or the usage of a clever crawler is required.
In the worst, case, it will require to do a real navigation session to generate
the appropriate whitelists.
|--{ Statistics, Reporting and so on ... }-----------------------------------|
This is a crucial part of any WAF, and this is not done yet !
But the good point is that, thanks to the principle of calling an external page
that receives all the informations about every denied request, it is fairly
simple to write your custom <insert your favorite language here> webpage to
take care of the statistics / reporting.
|--{ 3 .. 2 .. 1 .. practice ! }---------------------------------------------|
Ok, now, let's have a look at the practice ! Let's admit we want to create a
setup for a website. I won't cover the basics of setting up nginx as a reverse
proxy, but rather focus on NAXSI configuration. If you have a "normal" web
site, with no fancy URL rewritting or strange things, the default configuration
should do the work, but let's have a look at website with fancy rewritting,
and complex user forms.
To make things easier, a good point is that we can 'fool' nginx and the OS into
thinking that he is already the reverse proxy for the website, so that we can
setup the configuration without any risk of impacting the production servers,
so here we go :
/etc/nginx/site-enabled/default:
-----------------------------------8<-------------------------------------------
server {
proxy_set_header Proxy-Connection "";
resolver X.Y.Z;
listen *:80;
access_log /tmp/nginx_access.log;
error_log /tmp/nginx_error.log debug;
location / {
# specific site config
LearningMode;
SecRulesEnabled;
DeniedUrl "/RequestDenied";
## check rules
CheckRule "$SQL >= 8" BLOCK;
CheckRule "$RFI >= 8" BLOCK;
CheckRule "$TRAVERSAL >= 4" BLOCK;
CheckRule "$XSS >= 8" BLOCK;
# /specific site config
proxy_pass http://xx.xx.xx.xx;
proxy_set_header Host "www.xxx.com";
}
location /RequestDenied {
proxy_pass http://127.0.0.1:4242/denied_page.php;
}
}
-----------------------------------8<-------------------------------------------
Our configuration file is extremly similar to any nginx configuration, except:
- We defined a NAXSI "per site" configuration. This is what will determine how
NAXSI will behave for this site.
- We define a location that will be used to redirect fobidden pages. Here,
I have an apache instance listening on lo:4242
The NAXSI "per location" configuration, simply defines :
- CheckRule : The maximum score for each kind of "threat"
- The "LearningMode" directive is here to make the learning easier. By default,
NAXSI will stop processing a request as soon as it hits one of the 'BLOCK'
scores. With this directive, it will go through every rules, making
whitelist generation easier, while allowing the request to pass, even if
tagued as "BLOCKED", to make learning easier.
- The 'SecRulesEnabled' directives tells that NAXSI should be activated for
this location. In this way, you can decide to active / desactivate it easily
for location X or Y. (For exemple, you might not want a WAF on your
back-office ?)
- DeniedUrl : We tell NAXSI were to redirect the user when a request is blocked.
and we need to add this line in the http section of nginx.conf :
--------------------------------8<--------------------------------------
include /etc/nginx/sec-rules/core.rules;
--------------------------------8<--------------------------------------
The "core.rules" file is provided with NAXSI, and contains all the "patterns".
So, here we go ! Let's start
We can now fool the OS into thinking that xxx.com is on 127.0.0.1, edit /etc/hosts. We are ready for configuration ! Fire up your favorite browser at xxx.com and start navigation.
<roll roll>
As you should be in "learningmode", NAXSI will allow all the request, even if
they reach a blocking score. As well as letting them pass, it will, as well,
"forward" the request (like nginx's post_action directive) to your DeniedUrl,
as well as the original blocked URL (in headers) and the generated blocking
details. In this way, the web backend is abble to generate the white-lists,
and reload nginx 'on the fly' with the new generated whitelist rules ;)
The 'web' part is not written yet (I suck at html), but you can yet proceed
in a different way :
In your nginx's log file (if set as debug), you will see a lot of lines like
this one appear :
-----------------------------------8<-------------------------------------------
2011/07/11 17:12:27 [debug] 18653#0: *7 NAXSI_FMT: server=&uri=/skin/frontend/default/xxx/images/interface/fleche-grise.gif&ip=127.0.0.1&zone0=HEADERS&id0=1005&var_name0=cookie&zone1=HEADERS&id1=1008&var_name1=cookie&zone2=HEADERS&id2=1009&var_name2=cookie&zone3=HEADERS&id3=1010&var_name3=cookie&zone4=HEADERS&id4=1011&var_name4=cookie&zone5=HEADERS&id5=1308&var_name5=cookie&zone6=HEADERS&id6=1309&var_name6=cookie&zone7=HEADERS&id7=1313&var_name7=cookie
-----------------------------------8<-------------------------------------------
So, once you think you've done a reasonable crawling on your site, you can
launch the "rules generator" [destination rules file] [nginx's log file]:
-----------------------------------8<-------------------------------------------
bui@zeroed:~$ ./rules_generator.py
RANGE for ID=1000,ZONE=URL, range=0-4
# for rule 1000, we have 4 elements in zone URL
#duplicate for id 1000, delete (4 elems)
RANGE for ID=1002,ZONE=URL, range=1-89
...
{'id': '1313', 'uri': '', 'var_name': 'cookie', 'zone': 'HEADERS'}]
-----------------------------------8<-------------------------------------------
Rules generator default output file is /tmp/RT_naxsi.tmp, and looks like :
-----------------------------------8<-------------------------------------------
bui@zeroed:/home/bui/secdev/nginx/web: cat /tmp/RT_naxsi.tmp | grep ^Basic
BasicRule wl:1000 "mz:$URL:/skin/frontend/default/lepape/images/titre_notre_selection2.gif|URL";
BasicRule wl:1002 "mz:URL";
BasicRule wl:1008 "mz:URL";
BasicRule wl:1005 "mz:$HEADERS_VAR:cookie";
BasicRule wl:1008 "mz:$HEADERS_VAR:cookie";
BasicRule wl:1009 "mz:$HEADERS_VAR:cookie";
BasicRule wl:1010 "mz:$HEADERS_VAR:cookie";
BasicRule wl:1011 "mz:$HEADERS_VAR:cookie";
BasicRule wl:1308 "mz:$HEADERS_VAR:cookie";
BasicRule wl:1309 "mz:$HEADERS_VAR:cookie";
BasicRule wl:1313 "mz:$HEADERS_VAR:cookie";
-----------------------------------8<-------------------------------------------
What happened just here ? The Rule Generator parsed nginx log file,
extracted all the "denied urls", and generated (and then factorized)
whitelist rules from your session ! If you did an exhaustive enough
crawling / browsing session, your ruleset might be complete enough and
ready for production !
|--{ Detail configuration }--------------------------------------------------|
Here, I will go through all the directives supported by NAXSI.
|--{ Location configuration }------------------------------------------------|
LearningMode : By default, NAXSI will stop parsing a request as soon
as it reaches a score that will block the request. When
LearningMode is enabled, the request will be matched
against every possible rules. This directive should be
used when configuring NAXSI ONLY (a.k.a : generating
whitelists)
SecRulesEnabled : If the directive is not present, NAXSI will not
inspect anything. SecRulesDisabled : If the directive is present,
NAXSI will not inspect anything. (The two rules above are here for
easier usage in production environnment, when you want to be abble to
simply enable / disable the module)
DeniedUrl "/location" : This directive is used to tell NAXSI where the
user should be redirected when a request is
blocked. The location should be present in
NGINX configuration for this to work, as we
are relying on nginx's internal redirect
feature.
BasicRule : The 'main' thing you should care about. This can be used,
either to add some whitelist, or to create some specific
rules for a location (might be usefull if you have super
crappy websites). Here are some examples of possible
BasicRule syntaxes :
- BasicRule wl:1005 "mz:$URL:/bar/|$ARGS_VAR:foo" : Whitelist rule 1005 on
the GET "foo" arg of page /bar/
- BasicRule wl:1005 "mz:$URL:/bar/|ARGS" : Whitelist rule 1005 on the every
GET arg of page /bar/
- BasicRule wl:1005 : Globally disable rule 1005 on the location
- BasicRule wl:1005 "mz:HEADERS" : Whitelist rule 1005 for all HEADERS
- mz supports keywords like ARGS (global GET args), BODY (global POST args),
URL (url, rly), $ARGS_VAR (named GET arg), $BODY_VAR (named POST arg),
HEADERS (HTTP headers), $HEADERS_VAR (named header var)
CheckRule : Used to determine when the request should be blocked, for
example : CheckRule "$SQL >= 8" BLOCK : If the $SQL score is superior
or equal to 8, the request will be blocked.
|--{ Main configuration }----------------------------------------------------|
In main configuration, a.k.a core rules, there is only one directive :
MainRule "str:/*" "msg:mysql comment (/*)" "mz:BODY|URL|ARGS" "s:$SQL:8" id:1003;
This specific rule for example, will be matched against GET,POST
arguments, as well as the URL. If the '/*' string is found, the $SQL
score will be increased by 8. Sounds pretty simple right ? But you
can as well do some tricky things !
MainRule negative "rx:application/x-www-form-urlencoded|multipart/form-data" "msg:foobar test pattern" "mz:$HEADER_VAR:Content-type" "s:$SQL:42" id:1999;
This one is a negative rule (means that the score will be applied if the rule
DOES NOT match). This one is used to prevent strange content types on POSTs.
To litteraly translate it, it means : If a "Content-type" HTTP header is
present, check weither it's 'application/x-www-form-urlencoded' or
'multipart/form-data' (you noticed the rx: keyword, yes it means that it's a
regular expression). If the content-type is different, then increase $SQL score
by 42.