| #!/usr/bin/python |
| # try to provide minimal multi-version support |
| try: |
| from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler |
| except ImportError: |
| from http.server import BaseHTTPRequestHandler, HTTPServer |
| try: |
| import urlparse |
| except ImportError: |
| import urllib.parse as urlparse |
| import sqlite3 |
| |
| import pprint |
| import hashlib |
| import sys |
| import os |
| import argparse |
| import re |
| import cgi |
| |
| |
| |
| class Handler(BaseHTTPRequestHandler): |
| def do_POST(self): |
| self.do_GET() |
| def do_GET(self): |
| message = "" |
| # if it's a background-forwarded request ... |
| if ("naxsi_sig" in self.headers.keys()): |
| if params.v > 2: |
| print ("Exception catched.") |
| print ("ExUrl: "+self.headers["naxsi_sig"]) |
| nx.eat_rule(self.headers["naxsi_sig"]) |
| nx.agreggate_rules() |
| return |
| # user wanna reload its config |
| if (self.path.startswith("/write_and_reload")): |
| if params.v > 2: |
| print ("writting rules, reloading nginx.") |
| if self.path.find("?servmd5=") != -1: |
| print ("writting and reloading specific server."+self.path[self.path.find("?servmd5=")+9:]) |
| nx.dump_rules(self.path[self.path.find("?servmd5=")+9:]) |
| else: |
| print ("writting and reloading all servers.") |
| nx.dump_rules() |
| if params.n is False: |
| os.system(params.cmd) |
| self.send_response(302) |
| self.send_header('Location', '/') |
| self.end_headers() |
| else: |
| print ("Not reloading anything as user is non-root") |
| self.send_response(200) |
| self.end_headers() |
| if sys.version_info > (3, 0): |
| self.wfile.write(bytes("Not root, not reloading anything.", 'utf-8')) |
| else: |
| self.wfile.write("Not root, not reloading anything.") |
| return |
| if params.log is not None: |
| # else, read possible log file, show ui/report |
| self.feed_from_logs(params.log) |
| message = self.ui_report() |
| self.send_response(200) |
| self.end_headers() |
| if sys.version_info > (3, 0): |
| self.wfile.write(bytes(message, 'utf-8')) |
| else: |
| self.wfile.write(message) |
| def feed_from_logs(self, logfile): |
| if nx.log_fd is None: |
| print ("Opening log file for #1 time.") |
| nx.log_fd = open(params.log) |
| else: |
| print ("log file already opened") |
| # we're at the begining of the file ... |
| if nx.log_fd.tell() == 0: |
| print ("at the beginning of file.") |
| while True: |
| tmpbuf = nx.log_fd.readline() |
| if tmpbuf == '': |
| print ("EOF") |
| return |
| if tmpbuf.find("NAXSI_FMT: ") != -1 and tmpbuf.find(", client: ") != -1: |
| sys.stdout.write("[eating rule]") |
| nx.eat_rule(tmpbuf[tmpbuf.find("NAXSI_FMT: ") + 11:tmpbuf.find(", client: ")]) |
| nx.agreggate_rules() |
| def ui_report(self): |
| nbr = nx.get_written_rules_count() |
| nbs = nx.get_exception_count() |
| message = """<html> |
| <style> |
| .nx_ok { |
| font-size: 100%; |
| color: #99CC00; |
| } |
| .nx_ko { |
| font-size: 100%; |
| color: #FF0000; |
| } |
| .lnk_ok a { |
| color:green |
| } |
| .lnk_ko a { |
| color:red |
| } |
| </style> |
| <b class=""" |
| if (nbr > 0): |
| message += "nx_ko> " |
| else: |
| message += "nx_ok> " |
| message += "You currently have "+str(nbr) |
| message += " rules generated by naxsi </b><br>" |
| message += "You have a total of "+str(nbs)+" exceptions hit.</br>" |
| if (nbr > 2): |
| message += "You should reload nginx's config.</br>" |
| message += "<a href='/write_and_reload' style=nx_ko>Write rules and reload for <b>ALL</b> sites.</a></br>" |
| message += nx.display_written_rules() |
| message += "</html>" |
| return (message) |
| |
| class NaxsiDB: |
| def read_text(self): |
| try: |
| fd = open(params.rules, "r") |
| except IOError: |
| print ("Unable to open rules file : "+params.rules) |
| return |
| for rules in fd: |
| rid = re.search('id:([0-9]+)', rules) |
| if rid is None: |
| continue |
| ptr = re.search('str:([^"]+)', rules) |
| if ptr is None: |
| continue |
| self.static[str(rid.group(1))] = cgi.escape(ptr.group(1)) |
| fd.close() |
| def dump_rules(self, server=None): |
| if server is None: |
| fd = open(params.dst, "a+") |
| else: |
| fd = open(params.dst+"."+hashlib.md5(server.encode('utf-8')).hexdigest(), "a+") |
| cur = self.con.cursor() |
| if server is None: |
| cur.execute("SELECT id, uri, zone, var_name, " |
| "server from tmp_rules where written = 0") |
| else: |
| cur.execute("SELECT id, uri, zone, var_name, " |
| "server from tmp_rules where written = 0 and server = ?", [server]) |
| rr = cur.fetchall() |
| print ("Writting "+str(len(rr))+" rules.") |
| for i in range(len(rr)): |
| tmprule = "BasicRule wl:"+str(rr[i][0])+" \"mz:" |
| if len(rr[i][1]) > 0: |
| tmprule += "$URL:"+rr[i][1]+"|" |
| if len(rr[i][3]) > 0: |
| tmprule += "$"+rr[i][2]+"_VAR:"+rr[i][3]+"\" " |
| else: |
| tmprule += rr[i][2]+"\" " |
| tmprule += "; #"+rr[i][4]+"\n" |
| cur.execute("UPDATE tmp_rules SET written=1 WHERE id=? and " |
| "uri=? and zone=? and var_name=? and server=?", |
| [rr[i][0], rr[i][1], rr[i][2], rr[i][3], rr[i][4]]) |
| self.con.commit() |
| fd.write(tmprule) |
| if params.v > 2: |
| print ("Generated Rule : "+tmprule) |
| fd.close() |
| def gen_write(self, mid, uri, zone, var_name, server): |
| cur = self.con.cursor() |
| cur.execute("SELECT count(*) from tmp_rules where id=? and uri=? " |
| "and zone=? and var_name=? and server=?", |
| [mid, uri, zone, var_name, server]) |
| ra = cur.fetchone() |
| if (ra[0] >= 1): |
| if params.v > 2: |
| print ("already present in tmp_rules ...") |
| return |
| cur.execute("INSERT INTO tmp_rules (id, uri, zone, var_name, " |
| "server, written) VALUES (?, ?, ?, ?, ?, 0)", |
| [mid, uri, zone, var_name, server]) |
| self.con.commit() |
| def agreggate_rules(self, mid=0, zone="", var_name=""): |
| cur = self.con.cursor() |
| cur.execute("SELECT id,uri,zone,var_name,server FROM received_sigs" |
| " GROUP BY zone,var_name,id ORDER BY zone,var_name,id") |
| rr = cur.fetchall() |
| for i in range(len(rr)): |
| if len(rr[i][2]) > 0 and len(rr[i][3]) > 0: |
| self.gen_write(rr[i][0], "", rr[i][2], rr[i][3], rr[i][4]) |
| continue |
| if len(rr[i][3]) <= 0: |
| self.gen_write(rr[i][0], rr[i][1], rr[i][2], rr[i][3], rr[i][4]) |
| continue |
| def cursor(self): |
| return self.con.cursor() |
| def get_written_rules_count(self, server=None): |
| cur = self.con.cursor() |
| if server is None: |
| cur.execute("SELECT COUNT(id) FROM tmp_rules where written = 0") |
| else: |
| cur.execute("SELECT COUNT(id) FROM tmp_rules where written = 0 and server = ?", [server]) |
| ra = cur.fetchone() |
| return (ra[0]) |
| def display_written_rules(self): |
| msg = "" |
| cur = self.con.cursor() |
| cur.execute("SELECT distinct(server) " |
| " FROM tmp_rules") |
| rr = cur.fetchall() |
| pprint.pprint(rr) |
| for i in range(len(rr)): |
| print ("adding elems !") |
| if self.get_written_rules_count(rr[i][0]) > 0: |
| tmpstyle="lnk_ko" |
| else: |
| tmpstyle="lnk_ok" |
| msg += """<a style={4} href='/write_and_reload?servmd5={0}'> |
| [write&reload <b>{1}</b></a>|{2} pending rules| |
| filename:{3}]</br>""".format(rr[i][0], rr[i][0], |
| str(self.get_written_rules_count(rr[i][0])), |
| params.dst+"."+hashlib.md5(rr[i][0].encode('utf-8')).hexdigest(), |
| tmpstyle) |
| msg += "</br>" |
| cur.execute("SELECT id,uri,zone,var_name,server" |
| " FROM tmp_rules where written = 0") |
| rr = cur.fetchall() |
| if len(rr): |
| msg += "Authorizing :</br>" |
| for i in range(len(rr)): |
| pattern = "" |
| if (str(rr[i][0]) in self.static.keys()): |
| pattern = nx.static[str(rr[i][0])] |
| if len(rr[i][2]) > 0 and len(rr[i][3]) > 0: |
| msg += """<b style=nx_ok>[{0}]</b> -- pattern '{1}' |
| ({2}) authorized on URL '{3}' for argument '{4}' |
| of zone {5}</br>""".format(str(rr[i][4]), pattern, |
| str(rr[i][0]), rr[i][1], |
| rr[i][3], rr[i][2]) |
| continue |
| if len(rr[i][3]) <= 0: |
| msg += """<b style=nx_ok>[{0}]</b> -- |
| pattern '{1}' ({2}) authorized on url '{3}' |
| for zone {4}</br>""".format(str(rr[i][4]), |
| pattern, |
| str(rr[i][0]), |
| rr[i][1], |
| rr[i][2]) |
| continue |
| return msg |
| def get_exception_count(self): |
| cur = self.con.cursor() |
| cur.execute("SELECT COUNT(id) FROM received_sigs") |
| ra = cur.fetchone() |
| return (ra[0]) |
| def eat_rule(self, source): |
| currdict = {} |
| server = "" |
| uri = "" |
| ridx = '0' |
| tmpdict = urlparse.parse_qsl(source) |
| for i in range(len(tmpdict)): |
| if (tmpdict[i][0][-1] >= '0' and tmpdict[i][0][-1] <= '9' and |
| tmpdict[i][0][-1] != ridx): |
| currdict["uri"] = uri |
| currdict["server"] = server |
| if ("var_name" not in currdict): |
| currdict["var_name"] = "" |
| currdict["md5"] = hashlib.md5((currdict["uri"]+ |
| currdict["server"]+ |
| currdict["id"]+ |
| currdict["zone"]+ |
| currdict["var_name"]).encode('utf-8')).hexdigest() |
| # print ('#1 here:'+currdict["md5"]) |
| self.fatdict.append(currdict) |
| currdict={} |
| ridx = tmpdict[i][0][-1] |
| if (tmpdict[i][0].startswith("server")): |
| server = tmpdict[i][1] |
| if (tmpdict[i][0].startswith("uri")): |
| uri = tmpdict[i][1] |
| if (tmpdict[i][0].startswith("id")): |
| currdict["id"] = tmpdict[i][1] |
| if (tmpdict[i][0].startswith("zone")): |
| currdict["zone"] = tmpdict[i][1] |
| if (tmpdict[i][0].startswith("var_name")): |
| currdict["var_name"] = tmpdict[i][1] |
| currdict["uri"] = uri |
| currdict["server"] = server |
| if ("var_name" not in currdict): |
| currdict["var_name"] = "" |
| currdict["md5"] = hashlib.md5((currdict["uri"]+currdict["server"]+ |
| currdict["id"]+currdict["zone"]+ |
| currdict["var_name"]).encode('utf-8')).hexdigest() |
| self.fatdict.append(currdict) |
| self.push_to_db(self.fatdict) |
| def push_to_db(self, dd): |
| cur = self.con.cursor() |
| for i in range(len(dd)): |
| cur.execute("""SELECT count(id) FROM received_sigs WHERE md5=?""", [dd[i]["md5"]]) |
| ra = cur.fetchone() |
| if (ra[0] >= 1): |
| print ("Already present in db.") |
| continue |
| if params.v > 2: |
| print ("Pushing to db :") |
| pprint.pprint(dd[i]) |
| cur.execute("INSERT INTO received_sigs (md5, server, id, uri, zone, var_name) VALUES ("+ |
| "?, ?, ?, ?, ?, ?)", [dd[i]["md5"], dd[i]["server"], dd[i]["id"], dd[i]["uri"], |
| dd[i]["zone"], dd[i]["var_name"]]) |
| self.con.commit() |
| def dbcreate(self): |
| if params.v > 2: |
| print ("Creating (new) database.") |
| cur = self.con.cursor() |
| cur.execute("CREATE TABLE received_sigs (md5 text, server text, id int, uri text, zone text, var_name text)") |
| cur.execute("CREATE TABLE tmp_rules (id int, uri text, zone text, var_name text, written int, server text)") |
| self.con.commit() |
| print ("Finished DB creation.") |
| os.system("touch %s" % params.dst) |
| if params.v > 2: |
| print ("Touched TMP rules file.") |
| def dbinit(self): |
| if (self.con is None): |
| self.con = sqlite3.connect(params.db) |
| cur = self.con.cursor() |
| cur.execute("SELECT name FROM sqlite_master WHERE name='received_sigs'") |
| ra = cur.fetchone() |
| if (ra is None): |
| self.dbcreate() |
| if params.v > 2: |
| print ("done.") |
| def __init__(self): |
| self.con = None |
| self.fatdict = [] |
| self.static = {} |
| self.dbinit() |
| self.log_fd = None |
| return |
| |
| class Params(object): |
| pass |
| |
| params = Params() |
| |
| if __name__ == '__main__': |
| parser = argparse.ArgumentParser(description="Naxsi's learning-mode HTTP server.\n"+ |
| "Should be run as root (yes scarry), as it will need to perform /etc/init.d/nginx reload.\n"+ |
| "Runs fine as non-root, but you'll have to manually restart nginx", |
| formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| parser.add_argument('--dst', type=str, default='/tmp/naxsi_rules.tmp', help='''Full path to the temp rule file. |
| This file should be included in your naxsi's location configuration file.''') |
| parser.add_argument('--db', type=str, default='naxsi_tmp.db', help='''SQLite database file to use.''') |
| parser.add_argument('--rules', type=str, default='/etc/nginx/naxsi_core.rules', help='''Path to your core rules file.''') |
| parser.add_argument('--cmd', type=str, default='/etc/init.d/nginx reload', help='''Command that will be |
| called to reload nginx's config file''') |
| parser.add_argument('--port', type=int, default=4242, help='''The port the HTTP server will listen to''') |
| parser.add_argument('--log', type=str, default=None, help='''Pickup false positives from log file as well.(ie. /var/log/nginx_error.log)''') |
| parser.add_argument('-n', action='store_true', default=False, |
| help='''Run the daemon as non-root, don't try to reload nginx.''') |
| parser.add_argument('-v', type=int, default=1, help='''Verbosity level 0-3''') |
| args = parser.parse_args(namespace=params) |
| server = HTTPServer(('localhost', params.port), Handler) |
| nx = NaxsiDB() |
| nx.read_text() |
| print ('Starting server, use <Ctrl-C> to stop') |
| try: |
| server.serve_forever() |
| except KeyboardInterrupt: |
| server.shutdown() |
| sys.exit(1) |