WAF(Web application firewall) for My Website

Lately, I’ve been investigating ways to improve this website/blog’s security and I’ve remembered about WAF. I did some reading about this back in the day when I was working daily with load-balancers (F5/Netscalers). But those solutions were for big websites and expensive.

Then, the blog being hosted in the cloud, I thought to see what WAF solutions cloud providers offer. It seems everybody has a WAF product to offer(GCP, AWS, Azure, Oracle, Cloudflare, etc), but again it is too expensive for a small website/blog like mine.

Now, my website is built with WordPress . So what security plugins have some WAF features? It seems there are some. Some even have some free features. Not bad.

But I’ve ended up choosing something open-source: Modsecurity.
Why? Well, because I like to get my hands dirty and try to learn something new.
Now, what is ModSecurity?
ModSecurity started as a module for Apache, but now it is a library that supports different web servers like Apache, Nginx, and IIS. It provides a rule configuration language known as ‘SecRules’ for real-time monitoring, logging, and filtering of Hypertext Transfer Protocol communications based on user-defined rules.

Its most commonly deployed to protect against generic classes of vulnerabilities using the OWASP ModSecurity Core Rule Set (CRS). The CRS provides protection against many common attack categories, including SQL Injection, Cross Site Scripting, Local File Inclusion, etc. If you search the Internet, you can even find commercial sets of rules that an enterprise can use. But it is not my case.
Also, many companies use this Core Rule Set in their WAF products and/or provide a commercial set of rules: Akamai, Oracle, Fastly, Microsoft, etc.
Now I am not going to explain how this library works. First, because there is enough info if you search it, and second because I am a new user of this WAF.

Here is a good start:
How to install on Nginx:
https://www.linode.com/docs/guides/securing-nginx-with-modsecurity/
CRS documentation:
https://coreruleset.org/docs/
Writing ModSecurity rules:
https://malware.expert/tutorial/writing-modsecurity-rules/
https://www.feistyduck.com/books/modsecurity-handbook/modsecurity-rule-writing-workshop.pdf

After you install and start to use the WAF, you will probably hit some false positives that might make your site not work. In my case, I found a config file for WordPress, which is “whitelisting”/fixes sites that were built with WordPress. But still, I was hitting some false positives. To clear, them I had to write some rules.
Here is an example:

SecRule REQUEST_URI "@endsWith /ipinfo/" \
    "id:9002833,\
    phase:4,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveById=953100,\
    ver:'OWASP_CRS/3.3.4'

Basically, this rule bypasses rule id 953100 which was causing my issues for the URI that ends with “/ipinfo/”.

To get the id that you need to bypass, you will have to check the logs(modsec_audit.log).
Here is an example where you can see that rule id “953100” from rule file RESPONSE-953-DATA-LEAKAGES-PHP.conf is denying access (403) because its anomaly score is equal to the threshold.

Nov 4 11:24:53 server1 modsec_audit.log ModSecurity: Warning. Matched "Operator `PmFromFile' with parameter `php-errors.data' against variable `RESPONSE_BODY' (Value: `<!-- This page is cached by the Hummingbird Performance plugin v3.3.6 - https://wordpress.org/plugin (257345 characters omitted)' ) [file "/usr/local/modsecurity-crs/rules/RESPONSE-953-DATA-LEAKAGES-PHP.conf"] [line "26"] [id "953100"] [rev ""] [msg "PHP Information Leakage"] [data "Matched Data: The function found within RESPONSE_BODY"] [severity "3"] [ver "OWASP_CRS/4.0.0-rc1"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-php"] [tag "platform-multi"] [tag "attack-disclosure"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "capec/1000/118/116"] [tag "PCI/6.5.6"] [hostname "10.156.0.11"] [uri "/2020/11/23/ipinfo/"] [unique_id "166755389328.223317"] [ref "o137433,12v648,248124"]
Nov 4 11:24:53 server1 modsec_audit.log ModSecurity: Access denied with code 403 (phase 4). Matched "Operator `Ge' with parameter `4' against variable `TX:BLOCKING_OUTBOUND_ANOMALY_SCORE' (Value: `4' ) [file "/usr/local/modsecurity-crs/rules/RESPONSE-959-BLOCKING-EVALUATION.conf"] [line "186"] [id "959100"] [rev ""] [msg "Outbound Anomaly Score Exceeded (Total Score: 4)"] [data ""] [severity "0"] [ver "OWASP_CRS/4.0.0-rc1"] [maturity "0"] [accuracy "0"] [tag "anomaly-evaluation"] [hostname "10.156.0.11"] [uri "/2020/11/23/ipinfo/"] [unique_id "166755389328.223317"] [ref ""]

After fixing it, it was time to test the WAF.
Test for UNIX-shell:

curl -vs https://latebits.com/ipinfo/?exec=/bin/bash
< HTTP/2 403
< server: nginx
< date: Mon, 07 Nov 2022 14:22:47 GMT
< content-type: text/html
< content-length: 146
<
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

Then try a bad user agent (scanners):

curl -sv -H "User-Agent: zmeu" https://latebits.com/ipinfo/
< HTTP/2 403
< server: nginx
< date: Mon, 07 Nov 2022 14:25:11 GMT
< content-type: text/html
< content-length: 146
<
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

Next, let’s try to block something. For example, a URI that ends with test.php / test.html. For this I’ve created a new file (it is better not to change the default rules) under the rule folder, that contains:

SecRule REQUEST_URI "@rx (test)\.(php|html)$" \
    "id:101,\
    phase:1,\
    deny,\
    status:403,\
    t:none,\
    log,\
    msg:'Test sites not allowed',\
    ver:'OWASP_CRS/3.3.4'

In the logs you will see it like this:

modsec_audit.log ModSecurity: Access denied with code 403 (phase 1). Matched "Operator `Rx' with parameter `(test)\.(php|html)$' against variable `REQUEST_URI' (Value: `/test.html' ) [file "/usr/local/modsecurity-crs/rules/REQUEST-100-CUSTOM-RULES.conf"] [line "1"] [id "101"] [rev ""] [msg "Test sites not allowed"] [data ""] [severity "0"] [ver "OWASP_CRS/3.3.4"] [maturity "0"] [accuracy "0"] [hostname "10.156.0.11"] [uri "/test.html"] [unique_id "16678340461.206583"] [ref "o1,9o1,4o6,4v4,10"]

After some tests, I’ve also created a graph so I can visualize better how many attacks have been blocked and the URIs that were the target:

These are only some basic rules. There are a lot of things you can do with ModSecurity. That’s why you will find companies selling a set of rules that you can add to your WAF.

Now, let’s see how an attack looks in my monitoring solutions and validate that the WAF works.
Below is an attack on one of my blog posts. It had almost 2K requests in total.

All these requests were served a 403 response:

In web server logs:

Nov 23 08:52:58 server12 latebits.com.access.log 78.128.113.166 - - [23/Nov/2022:06:52:58 +0000] "/2021/08/17/observium-reporting-with-ansible/ HTTP/1.1" 403 146 "-" "Mozilla/5.0 (Windows; U; Windows NT 6.1; nl; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 ( .NET CLR 3.5.30729)"
Nov 23 08:52:58 server12 latebits.com.access.log 78.128.113.166 - - [23/Nov/2022:06:52:58 +0000] "/2021/08/17/observium-reporting-with-ansible/ HTTP/1.1" 403 146 "-" "Mozilla/5.0 (Windows; U; Windows NT 6.1; nl; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 ( .NET CLR 3.5.30729)"
Nov 23 08:52:59 server12 latebits.com.access.log 78.128.113.166 - - [23/Nov/2022:06:52:58 +0000] " /2021/08/17/observium-reporting-with-ansible/ HTTP/1.1" 403 146 "-" "Mozilla/5.0 (Windows; U; Windows NT 6.1; nl; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 ( .NET CLR 3.5.30729)"
Nov 23 08:52:59 server12 latebits.com.access.log 78.128.113.166 - - [23/Nov/2022:06:52:59 +0000] " /2021/08/17/observium-reporting-with-ansible/ HTTP/1.1" 403 146 "-" "Mozilla/5.0 (Windows; U; Windows NT 6.1; nl; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 ( .NET CLR 3.5.30729)"
Nov 23 08:52:59 server12 latebits.com.access.log 78.128.113.166 - - [23/Nov/2022:06:52:59 +0000] "/2021/08/17/observium-reporting-with-ansible/ HTTP/1.1" 403 146 "-" "Mozilla/5.0 (Windows; U; Windows NT 6.1; nl; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 ( .NET CLR 3.5.30729)"
Nov 23 08:53:00 server12 latebits.com.access.log 78.128.113.166 - - [23/Nov/2022:06:53:00 +0000] " /2021/08/17/observium-reporting-with-ansible/ HTTP/1.1" 403 146 "-" "Mozilla/5.0 (Windows; U; Windows NT 6.1; nl; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 ( .NET CLR 3.5.30729)"

In the WAF logs, you can see what type of attacks:

23 08:53:07 server12 modsec_audit.log ModSecurity: Warning. detected SQLi using libinjection. [file "/usr/local/modsecurity-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf"] [line "46"] [id "942100"] [rev ""] [msg "SQL Injection Attack Detected via libinjection"] [data "Matched Data: sUEvo found within ARGS:utm_campaign: observium-reporting-with-ansible%' UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL#"] [severity "2"] [ver "OWASP_CRS/4.0.0-rc1"] [maturity "0"] [accuracy "0"] [hostname "10.158.0.102"] [uri "/2021/08/17/observium-reporting-with-ansible/"] [unique_id "166918638774.077351"] [ref "v578,102"]
23 08:53:06 server12 modsec_audit.log ModSecurity: Warning. Matched "Operator `Rx' with parameter `(?i)(?:\b(?:u(?:nion(?:[\w(\s]*?select|\sselect\s@)|ser\s*?\([^\)]*?)|(?:c(?:onnection_id|urrent_user)|database)\s*?\([^\)]*?|s(?:chema\s*?\([^\)]*?|elect.*?\w?user\()|into[\s+]+(?:dump|out)file\s*?[\ (203 characters omitted)' against variable `ARGS:utm_campaign' (Value: `observium-reporting-with-ansible%' UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL#' ) [file "/usr/local/modsecurity-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf"] [line "206"] [id "942190"] [rev ""] [msg "Detects MSSQL code execution and information gathering attempts"] [data "Matched Data: ' UNION ALL S found within ARGS:utm_campaign: observium-reporting-with-ansible%' UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL"] [severity "2"] [ver "OWASP_CRS/4.0.0-rc1"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-sqli"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "capec/1000/152/248/66"] [tag "PCI/6.5.2"] [hostname "10.158.0.101"] [uri "/2021/08/17/observium-reporting-with-ansible/"] [unique_id "166918638648.240727"] [ref "o33,13v578,97t:urlDecodeUni,t:removeCommentsChar"]

About the author

Mihai is a Senior Network Engineer with more than 15 years of experience