Artificial truth

The more you see, the less you believe.

[archives] [latest] | [homepage] | [atom/rss]

Using modsecurity from Python
Tue 16 February 2016 — download

The modsecurity project recently created Python bindings for their WAF, and despite the fact that naxsi is way better, I wanted to give it a try. To save time to fellow naive adventurer that want to do the same, this is the story of how far I went.

Modsecurity logo

Please keep in mind that those bindings are more likely an experiment than a real project for now, since I found bugs in modsec's parser (read modesec bypass) and logic/API issues during this journey.

# install modsecurity
git clone https://github.com/SpiderLabs/ModSecurity.git
cd ModSecurity
./build.sh
./configure
make
make install

# install the bindings
git clone https://github.com/SpiderLabs/ModSecurity-Python-bindings.git
cd ModSecurity-Python-bindings
make  # you may want to edit the Makefile and setup.py files
make install

And this is how to use it to load and process web requests:

import modsecurity
import os
import sys

req="""GET /docs/index.html?a=<script>alert(1)</script>&q=test HTTP/1.1
Host: 127.0.0.1
Accept-Language: en-uk
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
"""

resp="""HTTP/1.1 200 OK
Date: Sun, 16 Feb 2016 08:56:53 GMT
Server: nginx-2.2.14
Last-Modified: Sat, 20 Nov 2004 07:16:26 GMT
Accept-Ranges: bytes
Connection: close
Content-Type: text/html

<html><body><h1>It works!</h1>'<script>alert(1)</script>'</body></html>
"""

def parseRequest(req):  # parse a web request
    method = uri = version = ""
    headers = list()
    lines = req.split('\n')

    try:
        method, uri, version = lines.pop(0).split(' ')
    except ValueError:
        print 'Invalid request'
        sys.exit(0)

    while lines:
        line = lines.pop(0)
        if not line:
            break
        headers.append(line)

    return method, uri, version, headers, '\n'.join(lines)

modsec = modsecurity.msc_init()

rules = modsecurity.Rules()

rules.load('''SecRuleEngine On
SecDebugLog /tmp/debug.log
SecDebugLogLevel 9
SecRule ARGS "@rx (script)" "id:1,phase:request,deny,t:none,status:403"
''')

ret = rules.getParserError()
if ret:
    print 'Unable to parse rules: %s' % ret

transaction = modsecurity.msc_new_transaction(modsec, rules, None)

# Parse and process the request
method, uri, version, headers, data = parseRequest(req)
transaction.processURI(uri, method, version)

for header in headers:
    transaction.addRequestHeader(*header.split(':', 2))

if data:
    transaction.appendRequestBody(data, len(data))

# Parse and process the response
method, uri, version, headers, data = parseRequest(resp)
transaction.processURI(uri, method, version)

for header in headers:
    transaction.addResponseHeader(*header.split(':', 1))

if data:
    #transaction.appendResponseBody(data, len(data))  # the bindings are borken
    transaction.m_responseBody = data  # is this even working?

transaction.processRequestHeaders()
transaction.processRequestBody()
transaction.processResponseHeaders()
transaction.processResponseBody()

transaction.processLogging(200)

intervention = modsecurity.ModSecurityIntervention()
if transaction.intervention(intervention):
    print 'Bad request!'
else:
    print 'Good request!'

with open('/tmp/debug.log', 'r') as f:
    print f.read()
os.remove('/tmp/debug.log')

And this is what you'll get:

$ python modsec.py
Bad request!
[4] Initialising transaction
[4] Starting phase URI. (SecRules 0 + 1/2)
[4] Adding request argument (QUERY_STRING): name "a", value "<script>alert(1)</script>"
[4] Adding request argument (QUERY_STRING): name "q", value "test"
[4] Starting phase URI. (SecRules 0 + 1/2)
[4] Starting phase REQUEST_HEADERS.  (SecRules 1)
[9] This phase consists of 0 rule(s).
[4] Starting phase REQUEST_BODY. (SecRules 2)
[9] This phase consists of 1 rule(s).
[4] (Rule: 1) Executing operator "@rx " with param "(script)" against ARGS.
[9] Target value: "<script>alert(1)</script>" (Variable: ARGS:a)
[4] Operator completed in 0.000068 seconds
[4] Rule returned 1.
[4] Running (_non_ disruptive) action: status:403
[4] Running (disruptive) action: deny
[8] Running action deny
[9] Target value: "test" (Variable: ARGS:q)
[4] Operator completed in 0.000144 seconds
[4] Rule returned 0.
[4] Starting phase RESPONSE_HEADERS. (SecRules 3)
[9] This phase consists of 0 rule(s).
[4] Starting phase RESPONSE_BODY. (SecRules 4)
[9] This phase consists of 0 rule(s).
[4] Starting phase LOGGING. (SecRules 5)
[9] This phase consists of 0 rule(s).
[8] Checking if this request is suitable to be saved as an audit log.

Have fun parsing the logs!