owasp-modsecurity / ModSecurity

ModSecurity is an open source, cross platform web application firewall (WAF) engine for Apache, IIS and Nginx. It has a robust event-based programming language which provides protection from a range of attacks against web applications and allows for HTTP traffic monitoring, logging and real-time analysis.
https://www.modsecurity.org
Apache License 2.0
7.67k stars 1.54k forks source link

base64decode behaviour #3103

Closed emiliocamposmartin closed 2 months ago

emiliocamposmartin commented 2 months ago

I realized that some transformations are not working as understood reading the documentation, see the following request HTTP://172.16.100.100/QUIT and the following simple rule that blocks the request works:

begin conf

SecAuditEngine on
SecAuditLog /var/log/waf_audit.log
SecAuditLogFormat json
SecRequestBodyAccess on
SecResponseBodyAccess on
SecRuleEngine on
SecDefaultAction "pass,log,auditlog,logdata:'client:%{REMOTE_ADDR}',phase:1"
SecDefaultAction "pass,log,auditlog,logdata:'client:%{REMOTE_ADDR}',phase:2"
SecDefaultAction "pass,log,auditlog,logdata:'client:%{REMOTE_ADDR}',phase:3"
SecDefaultAction "pass,log,auditlog,logdata:'client:%{REMOTE_ADDR}',phase:4"

SecRule REQUEST_URI "@rx QUIT" \
    "id:50001,\
    phase:1,\
    block,\
    t:none,\
    msg:'QUIT found in URI',\
    logdata:'Matched Data: %{TX.0} found within %{MATCHED_VAR_NAME}: %{MATCHED_VAR}',\
    auditlog"

end conf

The audit result is the following:


{
    "transaction": {
        "client_ip": "192.168.2.21", 
        "client_port": 45126, 
        "host_ip": "172.16.100.100", 
        "host_port": 80, 
        "messages": [
            {
                "details": {
                    "accuracy": "0", 
                    "data": "client:192.168.2.21", 
                    "file": "/usr/local/skudonet/config/ipds/waf/sets/AUDIT.conf", 
                    "lineNumber": "15", 
                    "match": "Matched \"Operator `Rx' with parameter `QUIT' against variable `REQUEST_URI' (Value: `/QUIT' )", 
                    "maturity": "0", 
                    "reference": "o1,4v4,5", 
                    "rev": "", 
                    "ruleId": "50001", 
                    "severity": "0", 
                    "tags": [], 
                    "ver": ""
                }, 
                "message": "QUIT found in URI"
            }
        ], 
        "producer": {
            "components": [
                "OWASP_CRS/4.0.1-dev\""
            ], 
            "connector": "", 
            "modsecurity": "ModSecurity v3.0.4 (Linux)", 
            "secrules_engine": "Enabled"
        }, 
        "request": {
            "body": "", 
            "headers": {
                "Accept": "*/*", 
                "Host": "www.skudonet.com", 
                "User-Agent": "curl/7.84.0"
            }, 
            "http_version": 1.1, 
            "method": "GET", 
            "uri": "/QUIT"
        }, 
        "response": {
            "headers": {}, 
            "http_code": 200
        }, 
        "server_id": "b766c40958ee9301f89c65d57e0c75d06bf88037", 
        "time_stamp": "Tue Mar  5 17:34:20 2024", 
        "unique_id": "170965646087.632863"
    }
}

Now I wanted to send the QUIT string in the URI but in base64 Encoded, the string QUIT in base64Encoded is UVVJVEU= but now I add the following transformation to the rule:

I send the following URL HTTP://172.16.100.100/UVVJVEU= and the rule is changed as follow:

begin conf

SecAuditEngine on
SecAuditLog /var/log/waf_audit.log
SecAuditLogFormat json
SecRequestBodyAccess on
SecResponseBodyAccess on
SecRuleEngine on
SecDefaultAction "pass,log,auditlog,logdata:'client:%{REMOTE_ADDR}',phase:1"
SecDefaultAction "pass,log,auditlog,logdata:'client:%{REMOTE_ADDR}',phase:2"
SecDefaultAction "pass,log,auditlog,logdata:'client:%{REMOTE_ADDR}',phase:3"
SecDefaultAction "pass,log,auditlog,logdata:'client:%{REMOTE_ADDR}',phase:4"

SecRule REQUEST_URI "@rx QUIT" \
    "id:50001,\
    phase:1,\
    block,\
    t:base64Decode,\
    msg:'QUIT found in URI',\
    logdata:'Matched Data: %{TX.0} found within %{MATCHED_VAR_NAME}: %{MATCHED_VAR}',\
    auditlog"

end conf

Now the request is not stopped and passes to the nginx backend server, see the logs:

{
    "transaction": {
        "client_ip": "192.168.2.21", 
        "client_port": 45886, 
        "host_ip": "172.16.100.100", 
        "host_port": 80, 
        "messages": [], 
        "producer": {
            "components": [
                "OWASP_CRS/4.0.1-dev\""
            ], 
            "connector": "", 
            "modsecurity": "ModSecurity v3.0.4 (Linux)", 
            "secrules_engine": "Enabled"
        }, 
        "request": {
            "body": "", 
            "headers": {
                "Accept": "*/*", 
                "Host": "www.skudonet.com", 
                "User-Agent": "curl/7.84.0"
            }, 
            "http_version": 1.1, 
            "method": "GET", 
            "uri": "/QUIT"
        }, 
        "response": {
            "headers": {
                "Connection": "keep-alive", 
                "Content-Length": "169", 
                "Content-Type": "text/html", 
                "Date": "Tue, 05 Mar 2024 16:37:38 GMT", 
                "Server": "nginx/1.14.2"
            }, 
            "http_code": 404
        }, 
        "server_id": "b766c40958ee9301f89c65d57e0c75d06bf88037", 
        "time_stamp": "Tue Mar  5 17:37:38 2024", 
        "unique_id": "170965665888.420056"
    }
}

I appreciate any comments

airween commented 2 months ago

Hi @emiliocamposmartin,

thanks for reporting.

First of all:

Now I wanted to send the QUIT string in the URI but in base64 Encoded, the string QUIT in base64Encoded is UVVJVEU=

Sorry to ask, but are you sure?

echo "QUIT" | base64         
UVVJVAo=
echo "UVVJVEU=" | base64 --decode
QUITE

But just for sure take another test with Python:

>>> import base64
>>> base64.b64encode(b"QUIT")
b'UVVJVA=='
>>> base64.b64decode(b"UVVJVEU=")
b'QUITE'

Also please note, that REQUEST_URI contains the leading /, so if you pass that variable to base64Decode transformation, that will receive the /UVVJVA== string. Therefore you have to split/remove the leading /, eg. with a chained rule or with a previous rule.

Whit this combo it worked for me:

SecRule REQUEST_URI "@rx ^/(.*)" \
    "id:5000,\
    phase:1,\
    pass,\
    capture"

SecRule TX:1 "@rx QUIT" \
    "id:50001,\
    phase:1,\
    block,\
    t:base64Decode,\
    msg:'QUIT found in URI',\
    logdata:'Matched Data: %{TX.0} found within %{MATCHED_VAR_NAME}: %{MATCHED_VAR}',\
    auditlog"

After the first rule the capture action will push the matched regex parts into TX collection (see TX and capture in reference). Your rule is modified, that checks the TX:1, which is a "cleaned" format of your REQUEST_URI - so it's that only UVVJVA==. t:base64Decode can decodes this string as you expect.

So whit this request:

curl -v "http://127.0.0.1/UVVJVA=="

I get:

2024/03/07 20:36:29 [info] 42724#42724: *1 ModSecurity: Warning. Matched "Operator `Rx' with parameter `QUIT' against variable `TX:1' (Value: `UVVJVA==' ) [file "/etc/nginx/modsecurity.conf"] [line "21"] [id "50001"] [rev ""] [msg "QUIT found in URI"] [data "Matched Data: /UVVJVA== found within TX:1: QUIT"] [severity "0"] [ver ""] [maturity "0"] [accuracy "0"] [hostname "127.0.0.1"] [uri "/UVVJVA=="] [unique_id "170984018951.252711"] [ref "o0,4t:base64Decode"], client: 127.0.0.1, server: _, request: "GET /UVVJVA== HTTP/1.1", host: "127.0.0.1"

Let us know if you have any questions, or feel free to close the issue.

airween commented 2 months ago

@emiliocamposmartin - any news?

emiliocamposmartin commented 2 months ago

Clarified and it works now! Thanks for your help.