Web Application Firewall and CRS


During my stay at CfgMgmtCamp I attended the presentation of Franziska Bühler (@bufrasch) titled Web Application Firewall - Friend of your DevOps pipeline?. She talked about Web Application Firewalls (WAF) and the Core Rule Set (CRS) for owasp

Being into security and stuff like that myself, I decided I wanted to try to get the web application with ModSecurity up and running in my own test environment.

My test environment consists of a CentOS8 machine with NGINX and it turned out to be a little trickier than I thought.

The ModSecurity modules are standard available for the Apache webserver, so I could have used that. But I like a good challenge, so CentOS8 and NGINX it is.

Searching the web I found a couple of resources that helped me along, not completely, but it got me going.

Enable the PowerTools repository

As all configuration takes place on CentOS8, all development tools need to be made available, so the PowerTools repository needs to be enabled.

dnf config-manager --set-enabled PowerTools

And a lot of development tools need to be present

dnf -y install        \
    autoconf          \
    automake          \
    GeoIP-devel       \
    bison             \
    bison-devel       \
    curl              \
    curl-devel        \
    doxygen           \
    flex              \
    gcc               \
    gcc-c++           \
    git               \
    libcurl-devel     \
    libxml2-devel     \
    lmdb-devel        \
    lua-devel         \
    openssl-devel     \
    ssdeep-devel      \
    yajl              \
    yajl-devel        \
    zlib-devel

Get all sources

Now that all software is in place, setup an environment to work in. Be aware, as this is a testing envіronment no sudo is used, as everyting is done as root.

cd
mkdir owasp
cd owasp

and download all sources. GeoIP2 s needed, because GeoIP will soon be deprecated, thus download it from the link given and install with NGINX

wget https://nginx.org/download/nginx-1.17.8.tar.gz
wget https://github.com/SpiderLabs/ModSecurity/releases/download/v3.0.4/modsecurity-v3.0.4.tar.gz
wget https://github.com/SpiderLabs/owasp-modsecurity-crs/archive/v3.2.0.tar.gz -O CRS_v3.2.0.tar.gz
git clone --recursive https://github.com/maxmind/libmaxminddb
git clone             https://github.com/SpiderLabs/ModSecurity
git clone             https://github.com/SpiderLabs/ModSecurity-nginx
git clone             https://github.com/leev/ngx_http_geoip2_module

MaxMin library

The ModSecurity library and the ModSecurity module both need libmaxmin, so that is first

cd libmaxminddb
./bootstrap
./configure
make
make check
make install
cd ..

Configure and install modsecurity library

And after the libmaxmin the modsecurity library

tar -xvf modsecurity-v3.0.4.tar.gz
cd modsecurity-v3.0.4
./configure --with-lmdb --with-maxmind=/usr/local
make
make install
cd ..

Configure and install modsecurity module

Now that the modsecurity library is available, create the module

cd ModSecurity
sh build.sh
git submodule init
git submodule update
./configure --with-lmdb --with-maxmind=/usr/local
make
make install
cd ..

The tricky bit

For NGINX to accept the modules and load them, they have to be compiled with exactly the same configure options as the installed NGXINX. These can be determined with the nginx -V command.

But somehow I could not get this working. I tried all options I could find, but I kept running into binairy incompatibility errors. So I decided to compile NGINX from scratch as well. This has, of course the disadvantage that NGINX cannot be upgraded through the packagemanager anymore, but was already rendered impossible with the strict match between the modules and the nginx binary. But I wanted to make sure the self-build nginx stuff would not interfere with the rest of the system, so I place everything in /usr/local/nginx. As a starting point I took the configuration option the installed NGINX gave me and ended up with:

tar -xvf nginx-1.17.8.tar.gz
cd nginx-1.17.8
./configure                                                      \
    --prefix=/usr/local/nginx                                    \
    --sbin-path=/usr/local/nginx/sbin/nginx                      \
    --modules-path=/usr/local/nginx/modules                      \
    --conf-path=/usr/local/nginx/etc/nginx.conf                  \
    --error-log-path=/var/log/nginx/error.log                    \
    --http-log-path=/var/log/nginx/access.log                    \
    --pid-path=/var/run/nginx.pid                                \
    --lock-path=/var/run/nginx.lock                              \
    --http-client-body-temp-path=/var/cache/nginx/client_temp    \
    --http-proxy-temp-path=/var/cache/nginx/proxy_temp           \
    --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp       \
    --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp           \
    --http-scgi-temp-path=/var/cache/nginx/scgi_temp             \
    --user=nginx                                                 \
    --group=nginx                                                \
    --with-file-aio                                              \
    --with-threads                                               \
    --with-http_addition_module                                  \
    --with-http_auth_request_module                              \
    --with-http_dav_module                                       \
    --with-http_flv_module                                       \
    --with-http_gunzip_module                                    \
    --with-http_gzip_static_module                               \
    --with-http_mp4_module                                       \
    --with-http_random_index_module                              \
    --with-http_realip_module                                    \
    --with-http_secure_link_module                               \
    --with-http_slice_module                                     \
    --with-http_ssl_module                                       \
    --with-http_stub_status_module                               \
    --with-http_sub_module                                       \
    --with-mail                                                  \
    --with-mail_ssl_module                                       \
    --with-stream                                                \
    --with-stream_ssl_module                                     \
    --with-compat                                                \
    --with-cc-opt='-g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -DNGX_HTTP_HEADERS' \
    --with-ld-opt='-Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -Wl,--as-needed'i \
    --add-dynamic-module=../ModSecurity-nginx                    \
    --add-dynamic-module=../ngx_http_geoip2_module
make
make install
cd ..
useradd -m -c'nginx' nginx
mkdir -p /var/cache/nginx/client_temp
chown nginx:nginx /var/cache/nginx/client_temp

Configure ModSecurity

Once nginx is compiled and installed, on to modsec The creator of ModSec (SpiderLabs) has a default configuration available for download, so let’s start with that.

mkdir -p /usr/local/nginx/etc/modsec
wget
https://raw.githubusercontent.com/SpiderLabs/ModSecurity/v3/master/modsecurity.conf-recommended \
    -O /usr/local/nginx/etc/modsec/modsecurity.conf
cp -p /root/owasp/modsecurity-v3.0.4/unicode.mapping /usr/local/nginx/etc/modsec/unicode.mapping

sed -i 's/^SecRuleEngine.*/SecRuleEngine On/' /usr/local/nginx/etc/modsec/modsecurity.conf
cat <<- '@EOF' > /usr/local/nginx/etc/modsec/main.conf
    Include "/usr/local/nginx/etc/modsec/modsecurity.conf"

    # Basic test rule
    SecRule ARGS:blogtest "@contains test" "id:1111,deny,status:403"
    SecRule REQUEST_URI "@beginsWith /admin"
    "phase:2,t:lowercase,id:2222,deny,msg:'block admin'"
@EOF

And configure nginx to use the ModSec module.

worker_processes  1;
load_module modules/ngx_http_modsecurity_module.so;
load_module modules/ngx_http_geoip2_module.so;
load_module modules/ngx_stream_geoip2_module.so;
events {
    worker_connections  1024;
}
http {
    include            mime.types;
    default_type       application/octet-stream;
    sendfile           on;
    keepalive_timeout  65;
    server {
        listen         80;
        server_name    localhost;
        modsecurity    on;
        modsecurity_rules_file /usr/local/nginx/etc/modsec/main.conf;
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page  500 502 503 504 /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

To verify it’s not all completely broken, run /usr/local/nginx/sbin/nginx -t and ensure everything is in working order. Check if ModЅecurity is working with:

curl http://localhost/adminaccess
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>

And the /var/log/nginx/error.log file shows:

2020/02/07 16:04:08 [error] 17871#17871: *3 [client 127.0.0.1]
ModSecurity: Access denied with code 403 (phase 2). Matched "Operator
`BeginsWith' with parameter `/admin' against variable `REQUEST_URI'
(Value: `/adminaccess' ) [file "/usr/local/nginx/etc/modsec/main.conf"]
[line "7"] [id "2222"] [rev ""] [msg "block admin"] [data ""] [severity
"0"] [ver ""] [maturity "0"] [accuracy "0"] [hostname "127.0.0.1"] [uri
"/adminaccess"] [unique_id "158108784890.091254"] [ref
"o0,6v4,12t:lowercase"], client: 127.0.0.1, server: localhost, request:
"GET /adminaccess HTTP/1.1", host: "localhost"

Almost there

At this point ModSecurity is running with NGINX, so all that’s needed is the Core Rule Set. Ans that is a breeze once you get this far.

cd /usr/local/nginx/etc
tar -xvf ~/owasp/CRS_v3.2.0.tar.gz
ln -s owasp-modsecurity-crs-3.2.0 owasp-crs
cp -p /usr/local/nginx/etc/owasp-crs/crs-setup.conf.example /usr/local/nginx/etc/owasp-crs/crs-setup.conf

And add these lines to: /usr/local/nginx/etc/modsec/main.conf

Include "/usr/local/nginx/etc/owasp-crs/crs-setup.conf"
Include "/usr/local/nginx/etc/owasp-crs/rules/*.conf"

Also make sure that the file /usr/local/nginx/etc/owasp-crs/crs-setup.conf contains the lines

SecDefaultAction "phase:1,log,auditlog,deny,status:403"
SecDefaultAction "phase:2,log,auditlog,deny,status:403"

If a normal curl is issued, e.g. curl http://localhost/trololo_singer.html this does not trigger any security rules and a plain 404 error is displayed:

<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>

But, if a curl command is requesting a protected file, like the .htaccess file, the Core Rule Set is triggered and issues an access denied error.

<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>

And the /var/log/nginx/error.log file shows:

2020/02/07 16:17:28 [error] 2724#2724: *8 [client 127.0.0.1]
ModSecurity: Access denied with code 403 (phase 2). Matched "Operator
`PmFromFile' with parameter `restricted-files.data' against variable
`REQUEST_FILENAME' (Value: `/.htaccess' ) [file
"/usr/local/nginx/etc/owasp-crs/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"]
[line "104"] [id "930130"] [rev ""] [msg "Restricted File Access
Attempt"] [data "Matched Data: .htaccess found within REQUEST_FILENAME:
/.htaccess"] [severity "2"] [ver "OWASP_CRS/3.2.0"] [maturity "0"]
[accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag
"platform-multi"] [tag "attack-lfi"] [tag "OWASP_CRS"] [tag
"OWASP_CRS/WEB_ATTACK/FILE_INJECTION"] [tag "WASCTC/WASC-33"] [tag
"OWASP_TOP_10/A4"] [tag "PCI/6.5.4"] [hostname "127.0.0.1"] [uri
"/.htaccess"] [unique_id "15813292242.837003"] [ref
"o1,9v4,10t:utf8toUnicode,t:urlDecodeUni,t:normalizePathWin,t:lowercase"],
client: 127.0.0.1, server: localhost, request: "GET /.htaccess
HTTP/1.1", host: "localhost"

Which surely is a message that the request was blocked by the Core Rule Set.

See also