A hands-on lab for learning how a Web Application Firewall (WAF) works in practice. It covers deploying ModSecurity with the OWASP Core Rule Set (CRS) on Apache, then testing it against real attacks on an intentionally vulnerable application (DVWA).
Disclaimer: This project is for educational purposes only. DVWA contains real vulnerabilities. Never expose it to the internet, and only run attack tools against systems you own or are authorized to test.
- Objectives
- Lab Environment
- Repository Structure
- Setup
- Experiments and Results
- Tuning: Paranoia Level and False Positives
- Custom Rule
- Key Takeaways
- References
- Deploy ModSecurity and the OWASP CRS on Apache
- Understand the difference between
DetectionOnlyandOnengine modes - Read WAF logs and understand Anomaly Scoring
- Write custom ModSecurity rules
- Configure CRS Paranoia Levels
- Identify and resolve false positives with narrowly scoped exclusions
- Validate protection using sqlmap and compare results with and without blocking
| Component | Details |
|---|---|
| Operating system | Ubuntu (virtual machine) |
| Web server | Apache 2.4 (listening on port 8081) |
| WAF engine | ModSecurity v2 (libapache2-mod-security2) |
| Rule set | OWASP Core Rule Set (cloned from GitHub) |
| Target application | DVWA (security level: Low) |
| Attack tool | sqlmap |
WAF-lab/
├── README.md
├── config/
│ ├── 000-default.conf # Apache VirtualHost (port 8081)
│ ├── ports.conf # Apache listening port
│ ├── security2.conf # Loads ModSecurity, CRS, and exclusions
│ ├── modsecurity-engine.txt # ModSecurity engine mode (SecRuleEngine)
│ └── crs-setup-active.conf # Active CRS settings (Paranoia Level)
└── rules/
├── custom.conf # Custom rule (id:10001)
└── exclusions-after-crs.conf # False-positive exclusions
sudo apt update
sudo apt install apache2 libapache2-mod-security2 -y
sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
sudo sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/modsecurity/modsecurity.confcd /etc/modsecurity
sudo git clone https://github.com/coreruleset/coreruleset.git
cd coreruleset
sudo cp crs-setup.conf.example crs-setup.confEdit /etc/apache2/mods-enabled/security2.conf (see config/security2.conf):
<IfModule security2_module>
SecDataDir /var/cache/modsecurity
IncludeOptional /etc/modsecurity/*.conf
Include /etc/modsecurity/coreruleset/crs-setup.conf
Include /etc/modsecurity/coreruleset/rules/*.conf
Include /etc/modsecurity/exclusions-after-crs.conf
# IncludeOptional /usr/share/modsecurity-crs/*.load # disabled to avoid loading CRS twice
</IfModule>Exclusions must be loaded after the CRS rules, otherwise they have no effect.
Nginx was already bound to port 80 on the VM, so Apache runs on 8081:
sudo sed -i 's/Listen 80/Listen 8081/' /etc/apache2/ports.conf
sudo sed -i 's/<VirtualHost \*:80>/<VirtualHost *:8081>/' /etc/apache2/sites-available/000-default.confsudo apachectl configtest
sudo systemctl restart apache2sudo apt install -y php libapache2-mod-php php-mysqli php-gd mariadb-server git
cd /var/www/html
sudo git clone https://github.com/digininja/DVWA.git dvwa
sudo cp dvwa/config/config.inc.php.dist dvwa/config/config.inc.php
sudo mysql -e "CREATE DATABASE dvwa; CREATE USER 'dvwa'@'localhost' IDENTIFIED BY 'p@ssw0rd'; GRANT ALL ON dvwa.* TO 'dvwa'@'localhost'; FLUSH PRIVILEGES;"
sudo chown -R www-data:www-data /var/www/html/dvwa
sudo systemctl restart apache2Then open http://<VM-IP>:8081/dvwa/setup.php, create the database, and set DVWA Security to Low.
curl -i "http://<VM-IP>:8081/?q=<script>alert(1)</script>" # XSS
curl -i "http://<VM-IP>:8081/?id=1' OR '1'='1" # SQL injection
curl -i "http://<VM-IP>:8081/?file=../../etc/passwd" # Path traversalResult: all three requests return 403 Forbidden.
Switch the engine mode:
# Log only, do not block
sudo sed -i 's/SecRuleEngine On/SecRuleEngine DetectionOnly/' /etc/modsecurity/modsecurity.conf
sudo systemctl restart apache2
# Log and block
sudo sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/modsecurity/modsecurity.conf
sudo systemctl restart apache2Manual SQL injection (1' OR '1'='1) on the DVWA SQL Injection page:
| Engine mode | Outcome |
|---|---|
DetectionOnly |
Attack succeeds and all usernames are returned |
On |
403 Forbidden |
sqlmap -u "http://<VM-IP>:8081/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="PHPSESSID=<SESSION>; security=low" --random-agent --batchEngine = On |
Engine = DetectionOnly |
|
|---|---|---|
| sqlmap verdict | not injectable |
is vulnerable |
| Blocked requests | 144 returned 403 |
none |
| Findings | none | time-based blind and UNION query injection |
| Information leaked | none | DBMS type, OS, web server version |
Important note: without --random-agent, sqlmap was blocked on the very first request because of its User-Agent (scanner-detection rules, 913xxx). That does not prove SQL injection protection. Using a random User-Agent ensures the blocking comes from the SQL injection rules (942xxx).
Conclusion: the WAF does not fix the vulnerability, it prevents it from being reached (virtual patching).
Paranoia Level was raised to 2 in crs-setup.conf (see config/crs-setup-active.conf):
SecAction \
"id:900000,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
setvar:tx.blocking_paranoia_level=2"Higher levels enable more detection rules, which increases both protection and the likelihood of false positives.
This legitimate request was blocked:
curl -i "http://<VM-IP>:8081/dvwa/?comment=please+select+one+from+the+list+and+union+of+items"Log analysis:
| Rule ID | Description | Score |
|---|---|---|
920350 |
Host header is a numeric IP address | 3 |
942200 |
MySQL comment/space-obfuscated injection detection | 5 |
942260 |
Basic SQL authentication bypass attempt | 5 |
949110 |
Inbound Anomaly Score exceeded (total 13 vs. threshold 5) | block |
A narrowly scoped exclusion in rules/exclusions-after-crs.conf:
SecRuleUpdateTargetById 942200 "!ARGS:comment"
SecRuleUpdateTargetById 942260 "!ARGS:comment"This excludes one parameter from two rules instead of disabling them site-wide.
| Test | Result |
|---|---|
Legitimate text in comment |
Allowed (302 redirect to login) |
SQL injection on id |
403 Forbidden |
SQL injection inside comment |
403 Forbidden (other rules still inspect it) |
Prefer SecRuleUpdateTargetById over SecRuleRemoveById, which disables a rule everywhere.
SecRule ARGS:test "@contains hacker" "id:10001,phase:2,deny,status:403,msg:'My first rule'"| Part | Meaning |
|---|---|
ARGS:test |
Inspect the request parameter named test |
@contains hacker |
Match if the value contains the string hacker |
id:10001 |
Unique rule ID (custom rules use IDs above 10000) |
phase:2 |
Evaluate after request arguments and body are parsed |
deny,status:403 |
Block the request with HTTP 403 |
msg |
Message written to the logs |
curl -i "http://<VM-IP>:8081/?test=hacker" # 403 Forbidden
curl -i "http://<VM-IP>:8081/?test=hello" # 200 OK# Check the current engine mode
grep SecRuleEngine /etc/modsecurity/modsecurity.conf
# Validate Apache configuration
sudo apachectl configtest
# See which rules fired, ranked by frequency
sudo grep -oE 'id "[0-9]+"' /var/log/apache2/error.log | sort | uniq -c | sort -rn | head
# Full audit log
sudo tail -n 50 /var/log/apache2/modsec_audit.log| Range | Category |
|---|---|
913xxx |
Scanner and tool detection |
920xxx |
Protocol enforcement |
930xxx |
Local file inclusion / path traversal |
941xxx |
Cross-site scripting (XSS) |
942xxx |
SQL injection |
949110 |
Inbound anomaly score evaluation (blocking decision) |
980170 |
Anomaly score summary |
DetectionOnlyobserves attacks,Onblocks them. The vulnerability itself remains in both cases.- CRS uses anomaly scoring: individual rules add points, and the request is blocked when the total exceeds the threshold.
- Tuning is the real work of operating a WAF: reducing false positives without weakening protection.
- Exclusions should be as narrow as possible (one parameter, specific rules).
- A WAF is an additional defensive layer. It does not replace fixing vulnerabilities in the application.
Provided for educational use. ModSecurity, OWASP CRS, DVWA, and sqlmap are independent projects with their own licenses.