├── COPYING ├── README ├── active-hosts-metrics.bro ├── country-metrics.bro ├── dump_http.bro ├── dump_http.sh ├── generate_splunk_configs.py ├── http-ext-block-exe-hosts.bro ├── http-metrics.bro ├── http-mime-metrics.bro ├── http-site-metrics.bro ├── http-size-metrics.bro ├── ipblocker.bro ├── log-external-dns.bro ├── log-external-names.bro ├── log-http-sqli.bro ├── metrics.http-ext.bro ├── metrics.smtp-ext.bro ├── notice.bro.patch ├── rdp.bro ├── rdp.sig ├── rogue-access-points.bro ├── simple-clear-passwords.bro ├── smtp-ext-count-rejects.bro ├── smtp-ext-phish-passwords.bro ├── ssh-ext-block.bro ├── subnet-admins.bro ├── subnet-helper.bro ├── tablize ├── testing └── http-watch-header-order.bro └── urlsnarf.sh /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Seth Hall and The Ohio State 2 | University. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | (1) Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | (2) Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | (3) Neither the name of The Ohio State University, nor the names of 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Bro-IDS analysis scripts 2 | ======================== 3 | All of these scripts require Bro 1.5 or greater. -------------------------------------------------------------------------------- /active-hosts-metrics.bro: -------------------------------------------------------------------------------- 1 | module Active; 2 | @load base/frameworks/metrics 3 | 4 | 5 | export { 6 | const host_tracking = LOCAL_HOSTS &redef; 7 | } 8 | 9 | event bro_init() 10 | { 11 | Metrics::add_filter("active", 12 | [$name="all", 13 | $break_interval=3600secs 14 | ]); 15 | } 16 | 17 | event connection_established(c: connection) 18 | { 19 | #taken from known-hosts.bro 20 | #I don't want to count incoming scans or anything, so just count outgoing conns. 21 | local host = c$id$orig_h; 22 | if ( c$orig$state == TCP_ESTABLISHED && 23 | c$resp$state == TCP_ESTABLISHED && 24 | addr_matches_host(host, host_tracking) ) 25 | { 26 | Metrics::add_unique("active", [$str="hosts"], cat(host)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /country-metrics.bro: -------------------------------------------------------------------------------- 1 | @load base/frameworks/metrics 2 | 3 | event bro_init() 4 | { 5 | Metrics::add_filter("country.connections", 6 | [$name="all", 7 | $break_interval=600secs 8 | ]); 9 | } 10 | 11 | event connection_established(c: connection) 12 | { 13 | if(Site::is_local_addr(c$id$orig_h)){ 14 | local loc = lookup_location(c$id$resp_h); 15 | if(loc?$country_code) { 16 | local cc = loc$country_code; 17 | Metrics::add_data("country.connections", [$str=cc], 1); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /dump_http.bro: -------------------------------------------------------------------------------- 1 | global f: file = open("http.txt"); 2 | event bro_init() 3 | { 4 | enable_raw_output(f); 5 | } 6 | 7 | event http_entity_data(c: connection, is_orig: bool, length: count, data: string) 8 | { 9 | print f, "---------------\n"; 10 | print f, fmt("%s %s %s %s %s\n", c$id$orig_h, c$id$resp_h, c$http$host, c$http$uri, c$http); 11 | print f, data; 12 | print f, "---------------\n"; 13 | } 14 | -------------------------------------------------------------------------------- /dump_http.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | FILE=$1 4 | 5 | DIR=$(mktemp -t -d bro_urlsnarf.XXXXXXXXX) 6 | 7 | cp dump_http.bro $DIR || exit 1 8 | 9 | cd $DIR || exit 1 10 | 11 | bro -f 'not ip6' -C -r $FILE dump_http.bro || true 12 | 13 | cat http.txt 14 | 15 | rm -rf $DIR 16 | -------------------------------------------------------------------------------- /generate_splunk_configs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import os 4 | import glob 5 | 6 | field_renames = { 7 | 'host': 'src', 8 | 'source': 'src', 9 | } 10 | 11 | def rename_field(f): 12 | return field_renames.get(f, f) 13 | 14 | def read_log_file(log_file): 15 | f = open(log_file) 16 | header = [f.readline().strip() for _ in range(10)] 17 | header = [l for l in header if l.startswith("#")] 18 | if not header: 19 | return 20 | fields = [ l for l in header if l.startswith("#fields")][0] 21 | fields = fields.replace("#fields\t",'').split("\t") 22 | return fields 23 | 24 | def read_log_files(log_files): 25 | logs = {} 26 | for f in log_files: 27 | info = read_log_file(f) 28 | if info: 29 | logs[f] = info 30 | return logs 31 | 32 | def generate(log_dir, out_dir): 33 | log_files = glob.glob(os.path.join(log_dir, "*.log")) 34 | 35 | data = read_log_files(log_files) 36 | 37 | i = open(os.path.join(out_dir, "inputs.conf"),'w') 38 | p = open(os.path.join(out_dir, "props.conf"),'w') 39 | t = open(os.path.join(out_dir, "transforms.conf"),'w') 40 | 41 | for fn, fields in sorted(data.items()): 42 | print fn 43 | sourcetype = "bro_" + os.path.basename(fn).replace(".log",'') 44 | fields_str = ', '.join(['"%s"' % rename_field(f) for f in fields]) 45 | 46 | i.write('[monitor://%s]\n' % fn) 47 | i.write('disabled = false\n') 48 | i.write('sourcetype = %s\n' % sourcetype ) 49 | i.write('index=security\n\n') 50 | 51 | p.write('[%s]\n' % sourcetype) 52 | p.write('KV_MODE = none\n') 53 | p.write('SHOULD_LINEMERGE = false\n') 54 | p.write('given_type = csv\n') 55 | p.write('pulldown_type = true\n') 56 | p.write('TRANSFORMS-commentsToNull = bro-ignore-comments\n') 57 | p.write('REPORT-AutoHeader = AutoHeader-%s\n\n' % sourcetype) 58 | 59 | t.write('[AutoHeader-%s]\n' % sourcetype) 60 | t.write('DELIMS = "\t"\n') 61 | t.write('FIELDS = %s\n\n' % fields_str) 62 | 63 | t.write('[bro-ignore-comments]\n') 64 | t.write('REGEX = "^#.*"\n') 65 | t.write('DEST_KEY = queue\n') 66 | t.write('FORMAT = nullQueue\n') 67 | 68 | if __name__ == "__main__": 69 | log_dir = sys.argv[1] 70 | out_dir = sys.argv[2] 71 | 72 | generate(log_dir, out_dir) 73 | -------------------------------------------------------------------------------- /http-ext-block-exe-hosts.bro: -------------------------------------------------------------------------------- 1 | @load http-ext 2 | @load ipblocker 3 | 4 | module HTTP; 5 | 6 | export { 7 | redef enum Notice += { 8 | HTTP_IncorrectFileTypeBadHost 9 | }; 10 | 11 | const bad_exec_domains = 12 | /co\.cc/ 13 | | /cx\.cc/ 14 | | /cz\.cc/ 15 | | /^www1/ 16 | &redef; 17 | 18 | const bad_exec_urls = 19 | /php.adv=/ 20 | | /http:\/\/[0-9]{1,3}\.[0-9]{1,3}.*\/index\.php\?[^=]+=[^=]+$/ #try to match http://1.2.3.4/index.php?foo=bar 21 | | /load.php/ 22 | &redef; 23 | 24 | const bad_user_agents = 25 | /Java\/1/ 26 | &redef; 27 | 28 | } 29 | 30 | redef notice_action_filters += { 31 | [HTTP_IncorrectFileTypeBadHost] = notice_exec_ipblocker_dest, 32 | }; 33 | 34 | event http_ext(id: conn_id, si: http_ext_session_info) &priority=1 35 | { 36 | if(is_local_addr(id$resp_h)) 37 | return; 38 | if(! ("identified-files" in si$tags && si$mime_type == "application/x-dosexec")) 39 | return; 40 | 41 | if( (bad_exec_domains in si$host || bad_exec_urls in si$url) 42 | ||(/\.exe/ !in si$url && bad_user_agents in si$user_agent)) { 43 | NOTICE([$note=HTTP_IncorrectFileTypeBadHost, 44 | $id=id, 45 | $msg=fmt("EXE Downloaded from bad host %s %s %s", id$orig_h, id$resp_h, si$url), 46 | $sub="http-ext" 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /http-metrics.bro: -------------------------------------------------------------------------------- 1 | @load base/frameworks/metrics 2 | 3 | event bro_init() 4 | { 5 | Metrics::add_filter("http", 6 | [$name="all", 7 | $break_interval=600secs 8 | ]); 9 | } 10 | 11 | event HTTP::log_http(rec: HTTP::Info) 12 | { 13 | if(Site::is_local_addr(rec$id$resp_h)) { 14 | Metrics::add_data("http", [$str="server_bytes"], rec$response_body_len); 15 | Metrics::add_data("http", [$str="server_hits"], 1); 16 | } else { 17 | Metrics::add_data("http", [$str="client_bytes"], rec$response_body_len); 18 | Metrics::add_data("http", [$str="client_hits"], 1); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /http-mime-metrics.bro: -------------------------------------------------------------------------------- 1 | @load base/frameworks/metrics 2 | 3 | event bro_init() 4 | { 5 | Metrics::add_filter("http.mime", 6 | [$name="all", 7 | $break_interval=600secs 8 | ]); 9 | } 10 | 11 | event HTTP::log_http(rec: HTTP::Info) 12 | { 13 | if(Site::is_local_addr(rec$id$orig_h) && rec?$mime_type) { 14 | Metrics::add_data("http.mime", [$str=rec$mime_type], rec$response_body_len); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /http-site-metrics.bro: -------------------------------------------------------------------------------- 1 | @load base/frameworks/metrics 2 | 3 | @load http-metrics 4 | 5 | function do_metric(hostname: string, size: count) 6 | { 7 | if (/youtube/ in hostname && size > 512*1024) { 8 | Metrics::add_data(HTTP_METRICS, [$str="youtube_bytes"], size); 9 | Metrics::add_data(HTTP_METRICS, [$str="youtube_views"], 1); 10 | } 11 | else if (/facebook.com|fbcdn.net/ in hostname && size > 20) { 12 | Metrics::add_data(HTTP_METRICS, [$str="facebook_bytes"], size); 13 | Metrics::add_data(HTTP_METRICS, [$str="facebook_views"], 1); 14 | } 15 | else if (/google.com/ in hostname && size > 20) { 16 | Metrics::add_data(HTTP_METRICS, [$str="google_bytes"], size); 17 | Metrics::add_data(HTTP_METRICS, [$str="google_views"], 1); 18 | } 19 | else if (/nflximg.com/ in hostname && size > 200*1024) { 20 | Metrics::add_data(HTTP_METRICS, [$str="netflix_bytes"], size); 21 | Metrics::add_data(HTTP_METRICS, [$str="netflix_views"], 1); 22 | } 23 | else if (/pandora.com/ in hostname && size > 512*1024) { 24 | Metrics::add_data(HTTP_METRICS, [$str="pandora_bytes"], size); 25 | Metrics::add_data(HTTP_METRICS, [$str="pandora_views"], 1); 26 | } 27 | else if (/gmail.com/ in hostname && size > 20) { 28 | Metrics::add_data(HTTP_METRICS, [$str="gmail_bytes"], size); 29 | Metrics::add_data(HTTP_METRICS, [$str="gmail_views"], 1); 30 | } 31 | } 32 | 33 | redef record connection += { 34 | resp_hostname: string &optional; 35 | }; 36 | 37 | event ssl_established(c: connection) 38 | { 39 | if(c?$ssl && c$ssl?$server_name) { 40 | c$resp_hostname = c$ssl$server_name; 41 | } 42 | } 43 | 44 | event connection_finished(c: connection) 45 | { 46 | if (c?$resp_hostname) 47 | do_metric(c$resp_hostname, c$resp$num_bytes_ip); 48 | } 49 | 50 | event HTTP::log_http(rec: HTTP::Info) 51 | { 52 | if(rec?$host) 53 | do_metric(rec$host, rec$response_body_len); 54 | } 55 | -------------------------------------------------------------------------------- /http-size-metrics.bro: -------------------------------------------------------------------------------- 1 | @load base/frameworks/metrics 2 | @load base/protocols/http 3 | @load base/protocols/ssl 4 | @load base/utils/site 5 | 6 | redef enum Metrics::ID += { 7 | HTTP_REQUEST_SIZE_BY_HOST, 8 | }; 9 | 10 | redef record connection += { 11 | resp_hostname: string &optional; 12 | }; 13 | 14 | event bro_init() 15 | { 16 | Metrics::add_filter(HTTP_REQUEST_SIZE_BY_HOST, 17 | [$name="all", 18 | $break_interval=600secs 19 | ]); 20 | 21 | } 22 | 23 | 24 | event connection_finished(c: connection) 25 | { 26 | if (c?$resp_hostname) { 27 | local size = c$orig$num_bytes_ip + c$resp$num_bytes_ip; 28 | Metrics::add_data(HTTP_REQUEST_SIZE_BY_HOST, [$str=c$resp_hostname], size); 29 | } 30 | } 31 | 32 | event http_header (c: connection, is_orig: bool, name: string, value: string) 33 | { 34 | if(name == "HOST") { 35 | c$resp_hostname = value; 36 | } 37 | } 38 | 39 | event ssl_established(c: connection) 40 | { 41 | if(c?$ssl && c$ssl?$server_name) { 42 | c$resp_hostname = c$ssl$server_name; 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /ipblocker.bro: -------------------------------------------------------------------------------- 1 | module Notice; 2 | 3 | export { 4 | redef enum Action += { 5 | ## Indicates that the notice should be sent to ipblocker to block 6 | ACTION_IPBLOCKER 7 | }; 8 | const ipblocker_types: set[Notice::Type] = {} &redef; 9 | ## Add a helper to the notice policy for blocking addresses 10 | redef Notice::policy += { 11 | [$pred(n: Notice::Info) = { return (n$note in Notice::ipblocker_types); }, 12 | $action = ACTION_IPBLOCKER, 13 | $priority = 10], 14 | }; 15 | } 16 | 17 | event notice(n: Notice::Info) &priority=-5 18 | { 19 | if (ACTION_IPBLOCKER !in n$actions) 20 | return; 21 | local id = n$id; 22 | 23 | # The IP to block is whichever one is not the local address. 24 | local ip: addr; 25 | if(Site::is_local_addr(id$orig_h)) 26 | ip = id$resp_h; 27 | else 28 | ip = id$orig_h; 29 | 30 | local cmd = fmt("/usr/local/bin/bro_ipblocker_block %s", ip); 31 | execute_with_notice(cmd, n); 32 | } 33 | -------------------------------------------------------------------------------- /log-external-dns.bro: -------------------------------------------------------------------------------- 1 | module DNS; 2 | 3 | export { 4 | redef record Info += { 5 | is_external: bool &default=F; 6 | ## Country code of external DNS server 7 | cc: string &log &optional; 8 | ## hostname of external DNS server 9 | #resp_h_hostname: string &log &optional; 10 | }; 11 | 12 | 13 | const ignore_external: set[subnet] = { 14 | 8.8.8.8/32, #google dns 15 | 8.8.4.4/32, #google dns 16 | 208.67.220.123/32, #opendns 17 | 208.67.222.222/32, #opendns 18 | 208.67.220.220/32, #opendns 19 | } &redef; 20 | 21 | redef enum Notice::Type += { 22 | ## Indicates that a user is using an external DNS server 23 | EXTERNAL_DNS, 24 | 25 | ## Indicates that a user is using an external DNS server in 26 | ## a foreign country. 27 | EXTERNAL_FOREIGN_DNS, 28 | }; 29 | 30 | const local_countries: set[string] = { 31 | "US", 32 | } &redef; 33 | 34 | } 35 | 36 | event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count) 37 | { 38 | local rec = c$dns; 39 | if(!rec$RD) 40 | return; 41 | 42 | local orig_h = rec$id$orig_h; 43 | local resp_h = rec$id$resp_h; 44 | 45 | #is this an outbound query 46 | if(!Site::is_local_addr(orig_h) || Site::is_local_addr(resp_h)) 47 | return; 48 | 49 | if(orig_h in ignore_external || resp_h in ignore_external) 50 | return; 51 | 52 | rec$is_external=T; 53 | 54 | local loc = lookup_location(resp_h); 55 | 56 | rec$cc = ""; 57 | if(loc?$country_code){ 58 | rec$cc = loc$country_code; 59 | } 60 | 61 | when (local hostname = lookup_addr(resp_h)) { 62 | #Doesn't work for the log, but we can use it here :-( 63 | #rec$resp_h_hostname = hostname; 64 | 65 | local note = EXTERNAL_DNS; 66 | local nmsg = fmt("An external DNS server is in use %s(%s)", resp_h, hostname); 67 | 68 | if(rec$cc !in local_countries){ 69 | note = EXTERNAL_FOREIGN_DNS; 70 | nmsg = fmt("An external foreign(%s) DNS server is in use %s(%s).", rec$cc, resp_h, hostname); 71 | } 72 | local ident = fmt("%s-%s", orig_h, resp_h); 73 | NOTICE([$note=note, 74 | $msg=nmsg, 75 | $sub=hostname, 76 | $identifier=ident, 77 | $remote_location=loc, 78 | $suppress_for=1day, 79 | $conn=c]); 80 | } 81 | } 82 | 83 | event bro_init() 84 | { 85 | Log::add_filter(DNS::LOG, [$name = "external-dns", 86 | $path = "external_dns", 87 | $exclude = set("uid", "proto", "trans_id","qclass", "qclass_name", "qtype", "rcode", 88 | "QR","AA","TC","RD","RA","Z","answers","TTLs"), 89 | $pred(rec: DNS::Info) = { 90 | return rec$is_external; 91 | } ]); 92 | } 93 | -------------------------------------------------------------------------------- /log-external-names.bro: -------------------------------------------------------------------------------- 1 | event bro_init() 2 | { 3 | Log::add_filter(HTTP::LOG, [$name = "http-external", 4 | $path = "http_external", 5 | $pred(rec: HTTP::Info) = { return Site::is_local_addr(rec$id$resp_h) && rec?$host && !Site::is_local_name(rec$host); } 6 | ]); 7 | } 8 | -------------------------------------------------------------------------------- /log-http-sqli.bro: -------------------------------------------------------------------------------- 1 | event bro_init() 2 | { 3 | Log::add_filter(HTTP::LOG, [$name = "http-sqli", 4 | $path = "http_sqli", 5 | $pred(rec: HTTP::Info) = { return HTTP::URI_SQLI in rec$tags && Site::is_local_addr(rec$id$resp_h); } 6 | ]); 7 | } 8 | -------------------------------------------------------------------------------- /metrics.http-ext.bro: -------------------------------------------------------------------------------- 1 | #output something like this 2 | #http_metrics total=343243 inbound=102313 outbound=3423432 exe_download=23 3 | 4 | @load global-ext 5 | @load http-ext 6 | 7 | export { 8 | global http_metrics: table[string] of count &default=0; #&synchronized; 9 | global http_metrics_interval = +60sec; 10 | const http_metrics_log = open_log_file("http-ext-metrics"); 11 | } 12 | 13 | event http_write_stats() 14 | { 15 | if (http_metrics["total"]!=0) 16 | { 17 | print http_metrics_log, fmt("http_metrics time=%.6f total=%d inbound=%d outbound=%d video_download=%d youtube_watches=%d", 18 | network_time(), 19 | http_metrics["total"], 20 | http_metrics["inbound"], 21 | http_metrics["outbound"], 22 | http_metrics["video_download"], 23 | http_metrics["youtube_watches"] 24 | ); 25 | clear_table(http_metrics); 26 | } 27 | schedule http_metrics_interval { http_write_stats() }; 28 | } 29 | 30 | event bro_init() 31 | { 32 | set_buf(http_metrics_log, F); 33 | schedule http_metrics_interval { http_write_stats() }; 34 | } 35 | 36 | 37 | event http_ext(id: conn_id, si: http_ext_session_info) &priority=-10 38 | { 39 | ++http_metrics["total"]; 40 | if(is_local_addr(id$orig_h)) 41 | ++http_metrics["outbound"]; 42 | else 43 | ++http_metrics["inbound"]; 44 | 45 | if (/\.(avi|flv|mp4|mpg)/ in si$uri) 46 | ++http_metrics["video_download"]; 47 | if (/watch\?v=/ in si$uri && /youtube/ in si$host) 48 | ++http_metrics["youtube_watches"]; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /metrics.smtp-ext.bro: -------------------------------------------------------------------------------- 1 | #output something like this 2 | #smtp_metrics time=1261506588.216565 total=54 inbound=49 outbound=5 inbound_err=17 outbound_err=0 3 | 4 | @load global-ext 5 | @load smtp-ext 6 | 7 | export { 8 | global smtp_metrics: table[string] of count &default=0; #&synchronized; 9 | global smtp_metrics_interval = +60sec; 10 | const smtp_metrics_log = open_log_file("smtp-ext-metrics"); 11 | } 12 | 13 | event smtp_write_stats() 14 | { 15 | if (smtp_metrics["total"]!=0) 16 | { 17 | print smtp_metrics_log, fmt("smtp_metrics time=%.6f total=%d inbound=%d outbound=%d inbound_err=%d outbound_err=%d", 18 | network_time(), 19 | smtp_metrics["total"], 20 | smtp_metrics["inbound"], 21 | smtp_metrics["outbound"], 22 | smtp_metrics["inbound_err"], 23 | smtp_metrics["outbound_err"]); 24 | clear_table(smtp_metrics); 25 | } 26 | schedule smtp_metrics_interval { smtp_write_stats() }; 27 | } 28 | 29 | event bro_init() 30 | { 31 | set_buf(smtp_metrics_log, F); 32 | schedule smtp_metrics_interval { smtp_write_stats() }; 33 | } 34 | 35 | 36 | event smtp_ext(id: conn_id, si: smtp_ext_session_info) &priority=-10 37 | { 38 | ++smtp_metrics["total"]; 39 | if(is_local_addr(id$orig_h)) 40 | { 41 | ++smtp_metrics["outbound"]; 42 | if(si$last_reply!="") 43 | ++smtp_metrics["outbound_err"]; 44 | } 45 | else 46 | { 47 | ++smtp_metrics["inbound"]; 48 | if(si$last_reply!="") 49 | ++smtp_metrics["inbound_err"]; 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /notice.bro.patch: -------------------------------------------------------------------------------- 1 | --- notice.bro.orig 2010-03-18 09:52:21.000000000 -0400 2 | +++ notice.bro 2010-03-18 09:54:23.000000000 -0400 3 | @@ -277,11 +277,29 @@ 4 | if ( reading_traces() || dest == "" ) 5 | return; 6 | 7 | + local message = fmt("%s\n\n", n$msg); 8 | + 9 | + if ( n?$sub ) 10 | + message = fmt("%Assub=%s\n", message, n$sub); 11 | + 12 | + if ( n?$aux ) 13 | + { 14 | + for ( a in n$aux ) 15 | + message = fmt("%As%s=%s\n", message, a, n$aux[a]); 16 | + } 17 | + if ( n?$id ) 18 | + { 19 | + local id_string = fmt("%s/%d > %s/%d", 20 | + n$id$orig_h, n$id$orig_p, 21 | + n$id$resp_h, n$id$resp_p); 22 | + message = fmt("%Asconn=%s\n", message, id_string); 23 | + } 24 | + 25 | # The contortions here ensure that the arguments to the mail 26 | # script will not be confused. Re-evaluate if 'system' is reworked. 27 | local mail_cmd = 28 | - fmt("echo \"%s\" | %s -s \"[Bro Alarm] %s\" %s", 29 | - str_shell_escape(n$msg), mail_script, n$note, dest); 30 | + fmt("echo \"%As\" | %s -s \"[Bro Alarm] %s\" %s", 31 | + str_shell_escape(message), mail_script, n$note, dest); 32 | 33 | system(mail_cmd); 34 | } 35 | -------------------------------------------------------------------------------- /rdp.bro: -------------------------------------------------------------------------------- 1 | # $Id$ 2 | @load notice 3 | global rdp_connection: event(c: connection); 4 | 5 | @load signatures 6 | redef signature_files += "rdp.sig"; 7 | redef signature_actions += { ["dpd_rdp"] = SIG_IGNORE }; 8 | 9 | 10 | global rdp_ports = { 11 | 3389/tcp 12 | }; 13 | redef capture_filters += { ["rdp"] = "tcp and port 3389"}; 14 | 15 | event signature_match(state: signature_state, msg: string, data: string) 16 | { 17 | if (state$id == "dpd_rdp"){ 18 | add state$conn$service["RDP"]; 19 | event rdp_connection(state$conn); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rdp.sig: -------------------------------------------------------------------------------- 1 | signature dpd_rdp { 2 | ip-proto == tcp 3 | payload /^\x03\x00.*Cookie: mstshash=/ 4 | tcp-state originator 5 | event "RDP Detected" 6 | } 7 | -------------------------------------------------------------------------------- /rogue-access-points.bro: -------------------------------------------------------------------------------- 1 | @load base/frameworks/notice 2 | @load base/protocols/http 3 | 4 | export { 5 | redef enum Notice::Type += { 6 | Rogue_Access_Point 7 | }; 8 | 9 | const mobile_browsers = 10 | /i(Phone|Pod|Pad)/ | 11 | /Android/ &redef; 12 | 13 | const wireless_nets: set[subnet] &redef; 14 | global rogue_access_points : set[addr] &redef; 15 | } 16 | 17 | event http_header(c: connection, is_orig: bool, name: string, value: string) &priority=2 18 | { 19 | if (!is_orig ) 20 | return; 21 | local ip = c$id$orig_h; 22 | 23 | if (!Site::is_local_addr(ip) || ip in wireless_nets || ip in rogue_access_points) 24 | return; 25 | 26 | if ( name == "USER-AGENT" && mobile_browsers in value){ 27 | local message = "Rogue access point detected"; 28 | local submessage = value; 29 | NOTICE([$note=Rogue_Access_Point, $msg=message, $sub=submessage, 30 | $id=c$id]); 31 | add rogue_access_points[ip]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /simple-clear-passwords.bro: -------------------------------------------------------------------------------- 1 | redef capture_filters += { ["pop3"] = "port 110" }; 2 | 3 | global pop3_ports = { 110/tcp } &redef; 4 | redef dpd_config += { [ANALYZER_POP3] = [$ports = pop3_ports] }; 5 | 6 | module ClearPasswords; 7 | 8 | export { 9 | global clear_log_file = open_log_file("clear-password-users") &raw_output; 10 | global seen_clear_users: set[addr, string] &create_expire=1day &synchronized &persistent; 11 | } 12 | 13 | 14 | 15 | event pop3_request(c: connection, is_orig: bool, command: string, arg: string) 16 | { 17 | } 18 | 19 | function log_clear_pw(c: connection, status: string, user: string) 20 | { 21 | if(is_local_addr(c$id$orig_h)) 22 | return; 23 | if([c$id$orig_h, user] in seen_clear_users) 24 | return; 25 | add seen_clear_users[c$id$orig_h, user]; 26 | 27 | local loc = lookup_location(c$id$orig_h); 28 | when( local hostname = lookup_addr(c$id$orig_h) ){ 29 | print clear_log_file, cat_sep("\t", "\\N", 30 | network_time(), 31 | c$id$orig_h, 32 | c$id$resp_h, 33 | port_to_count(c$id$resp_p), 34 | hostname, 35 | loc$country_code, 36 | loc$region, 37 | "success", 38 | user); 39 | } 40 | 41 | } 42 | 43 | event pop3_login_success(c: connection, is_orig: bool, 44 | user: string, password: string) 45 | { 46 | log_clear_pw(c, "success", user); 47 | } 48 | 49 | 50 | event pop3_login_failure(c: connection, is_orig: bool, 51 | user: string, password: string) 52 | { 53 | log_clear_pw(c, "failure", user); 54 | } 55 | -------------------------------------------------------------------------------- /smtp-ext-count-rejects.bro: -------------------------------------------------------------------------------- 1 | # For the SMTP_StrangeRejectBehavior notice to work, you must define a 2 | # local_mail table listing all of your known mail sending hosts. 3 | # i.e. const local_mail: set[subnet] = { 1.2.3.4/32 }; 4 | @load smtp 5 | 6 | module SMTP; 7 | 8 | type smtp_counter: record { 9 | rejects: count &default=0; 10 | total: count &default=0; 11 | }; 12 | 13 | export { 14 | # The idea for this is that if a host makes more than reject_threshold 15 | # smtp connections per hour of which at least reject_percent of those are 16 | # rejected and the host is not a known mail sending host, then it's likely 17 | # sending spam or viruses. 18 | # 19 | const reject_threshold = 100 &redef; 20 | const reject_percent = 30 &redef; 21 | 22 | # These are smtp status codes that are considered "rejected". 23 | const bad_address_reject_codes: set[count] = { 24 | 501, # Bad sender address syntax 25 | 550, # Requested action not taken: mailbox unavailable 26 | 551, # User not local; please try 27 | 553, # Requested action not taken: mailbox name not allowed 28 | }; 29 | 30 | redef enum Notice += { 31 | SMTP_PossibleSpam, # Host sending mail *to* internal hosts is suspicious 32 | SMTP_StrangeRejectBehavior, # Local mail server is getting high numbers of rejects 33 | }; 34 | 35 | # This variable keeps track of the number of rejected and accepted 36 | # RCPT TO's a host has per hour. 37 | global reject_counter: table[addr] of smtp_counter &create_expire=1hr &redef; 38 | 39 | # Reduce the volume of notices raised by filtering out host that have 40 | # already been detected as having too many rejected RCPT TOs. 41 | global notified_reject_spammers: set[addr] &create_expire=1hr &redef; 42 | } 43 | 44 | event smtp_reply(c: connection, is_orig: bool, code: count, cmd: string, 45 | msg: string, cont_resp: bool) 46 | { 47 | # If this is a continued response, it could be something like 48 | # the multiline rejections that gmail gives. We only want to count 49 | # the first rejection in that case. 50 | if ( cont_resp ) return; 51 | 52 | if ( c$id$orig_h !in reject_counter ) 53 | { 54 | local t: smtp_counter; 55 | reject_counter[c$id$orig_h] = t; 56 | } 57 | # Set the smtp_counter to the local var "sc" 58 | local sc = reject_counter[c$id$orig_h]; 59 | 60 | # Whenever a "RCPT TO" is done, we add that to the total. 61 | if ( /^([rR][cC][pP][tT]|[mM][aA][iI][lL])/ in cmd ) 62 | { 63 | ++sc$total; 64 | if ( code in bad_address_reject_codes ) 65 | ++sc$rejects; 66 | 67 | if ( sc$total >= reject_threshold ) 68 | { 69 | local percent = (sc$rejects*100) / sc$total; 70 | local host = c$id$orig_h; 71 | if ( percent >= reject_percent && 72 | host !in notified_reject_spammers ) 73 | { 74 | local notice_type = SMTP_PossibleSpam; 75 | @ifdef ( local_mail ) 76 | if ( host in local_mail ) 77 | notice_type = SMTP_StrangeRejectBehavior; 78 | @endif 79 | NOTICE([$note=notice_type, 80 | $msg=fmt("%s is having a large number of attempted recipients rejected", host), 81 | $sub=fmt("attempted: %d rejected: %d percent", 82 | sc$total, percent), 83 | $conn=c]); 84 | 85 | add notified_reject_spammers[host]; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /smtp-ext-phish-passwords.bro: -------------------------------------------------------------------------------- 1 | @load global-ext 2 | @load smtp-ext 3 | 4 | module PHISH; 5 | 6 | global smtp_password_conns: set[conn_id] &read_expire=2mins; 7 | 8 | 9 | export { 10 | redef enum Notice += { 11 | SMTP_PossiblePWPhish, 12 | SMTP_PossiblePWPhishReply, 13 | }; 14 | global phishing_counter: table[string] of count &default=0 &create_expire=1hr &synchronized; 15 | global phishing_reply_tos: set[string] &synchronized &redef; 16 | global phishing_ignore_froms: set[string] &redef; 17 | global phishing_threshold = 50; 18 | 19 | const phish_keywords = 20 | /[pP][aA][sS][sS][wW][oO][rR][dD]/ 21 | | /[uU][sS][eE][rR].?[nN][aA][mM][eE]/ 22 | | /[nN][eE][tT][iI][dD]/ &redef; 23 | } 24 | 25 | 26 | event bro_init() 27 | { 28 | LOG::create_logs("password-mail", All, F, T); 29 | LOG::define_header("password-mail", cat_sep("\t", "", 30 | "ts", 31 | "orig_h", "orig_p", 32 | "resp_h", "resp_p", 33 | "helo", "message-id", "in-reply-to", 34 | "mailfrom", "rcptto", 35 | "date", "from", "reply_to", "to", "subject", 36 | "files", "last_reply", "x-originating-ip", 37 | "path", "is_webmail", "agent")); 38 | } 39 | 40 | event bro_done() 41 | { 42 | print "Counter"; 43 | print phishing_counter; 44 | print "bad reply-tos"; 45 | print phishing_reply_tos; 46 | } 47 | 48 | event smtp_data(c: connection, is_orig: bool, data: string) 49 | { 50 | if(is_local_addr(c$id$orig_h)) 51 | return; 52 | # look for 'password' 53 | if(phish_keywords in data) 54 | add smtp_password_conns[c$id]; 55 | } 56 | 57 | event smtp_ext(id: conn_id, si: smtp_ext_session_info) 58 | { 59 | if(is_local_addr(id$orig_h)) { 60 | for (to in si$rcptto){ 61 | if(to in phishing_reply_tos){ 62 | NOTICE([$note=SMTP_PossiblePWPhishReply, 63 | $msg=fmt("%s replied to %s - %s", si$mailfrom, to, si$subject), 64 | $id=id, 65 | $sub=si$mailfrom 66 | ]); 67 | } 68 | } 69 | } else { 70 | if (id !in smtp_password_conns) 71 | return; 72 | if(si$mailfrom in phishing_ignore_froms) 73 | return; 74 | phishing_counter[si$mailfrom] += |si$rcptto|; 75 | if(phishing_counter[si$mailfrom] > phishing_threshold){ 76 | local to_add =""; 77 | if(si$reply_to != "") 78 | to_add = si$reply_to; 79 | else if(si$from != "") 80 | to_add = si$from; 81 | else 82 | to_add = si$mailfrom; 83 | if(to_add !in phishing_reply_tos){ 84 | add phishing_reply_tos[to_add]; 85 | NOTICE([$note=SMTP_PossiblePWPhish, 86 | $msg=fmt("%s(%s) may be phishing - %s", si$mailfrom, si$reply_to, si$subject), 87 | $id=id, 88 | $sub=si$mailfrom 89 | ]); 90 | } 91 | } 92 | 93 | local log = LOG::get_file_by_id("password-mail", id, F); 94 | print log, cat_sep("\t", "\\N", 95 | network_time(), 96 | id$orig_h, port_to_count(id$orig_p), id$resp_h, port_to_count(id$resp_p), 97 | si$helo, 98 | si$msg_id, 99 | si$in_reply_to, 100 | si$mailfrom, 101 | fmt_str_set(si$rcptto, /["'<>]|([[:blank:]].*$)/), 102 | si$date, 103 | si$from, 104 | si$reply_to, 105 | fmt_str_set(si$to, /["']/), 106 | si$subject, 107 | fmt_str_set(si$files, /["']/), 108 | si$last_reply, 109 | si$x_originating_ip == 0.0.0.0 ? "" : fmt("%s", si$x_originating_ip), 110 | si$path, 111 | si$is_webmail, 112 | si$agent); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /ssh-ext-block.bro: -------------------------------------------------------------------------------- 1 | @load global-ext 2 | @load ssh-ext 3 | @load subnet-helper 4 | @load ipblocker 5 | @load notice 6 | 7 | module SSH; 8 | 9 | export { 10 | global ssh_attacked: table[addr] of addr_set &create_expire=30mins &synchronized;# default isn't working &default=function(a:addr):addr_set { print a;return set();}; 11 | global libssh_scanners: set[addr] &create_expire=10mins &synchronized; 12 | const subnet_threshold = 3 &redef; 13 | 14 | redef enum Notice += { 15 | SSH_Libssh_Scanner, 16 | }; 17 | const scanner_clients = 18 | /libssh/ 19 | | /dropbear/ &redef; 20 | } 21 | 22 | redef notice_action_filters += { 23 | [SSH_Libssh_Scanner] = notice_exec_ipblocker, 24 | }; 25 | 26 | 27 | event ssh_ext(id: conn_id, si: ssh_ext_session_info) &priority=-10 28 | { 29 | if(is_local_addr(id$orig_h) || 30 | scanner_clients !in si$client || 31 | si$status == "success") 32 | return; 33 | 34 | local subnets = add_attack(ssh_attacked, id$orig_h, id$resp_h); 35 | print fmt("%s scanned %d subnets", id$orig_h, subnets); 36 | 37 | if(subnets >= subnet_threshold && id$orig_h !in libssh_scanners){ 38 | add libssh_scanners[id$orig_h]; 39 | 40 | NOTICE([$note=SSH_Libssh_Scanner, 41 | $id=id, 42 | $msg=fmt("SSH libssh scanning. %s scanned %d subnets", id$orig_h, subnets), 43 | $sub="ssh-ext", 44 | $n=subnets]); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /subnet-admins.bro: -------------------------------------------------------------------------------- 1 | @load my-notice-action-filter 2 | global subnet_admins: table[subnet] of string &redef; 3 | 4 | function notice_email_subnet_admins(n: notice_info, a: NoticeAction): NoticeAction 5 | { 6 | local id = n$id; 7 | local host = is_local_addr(id$orig_h) ? id$orig_h : id$resp_h; 8 | local admin = ""; 9 | 10 | if(host !in subnet_admins) 11 | admin = mail_dest; 12 | else 13 | admin = subnet_admins[host]; 14 | email_notice_to(n, admin); 15 | event notice_alarm(n, NOTICE_EMAIL); 16 | return NOTICE_FILE; 17 | } 18 | 19 | function notice_email_subnet_admins_then_tally(n: notice_info, a: NoticeAction): NoticeAction 20 | { 21 | a = notice_email_then_tally(n, a); 22 | if(a == NOTICE_EMAIL){ 23 | a = notice_email_subnet_admins(n, a); 24 | } 25 | return a; 26 | } 27 | -------------------------------------------------------------------------------- /subnet-helper.bro: -------------------------------------------------------------------------------- 1 | function get_subnet(a: addr): addr 2 | { 3 | return remask_addr(a,0.0.0.0, 24); 4 | } 5 | 6 | function add_attack(tab: table[addr] of addr_set, orig_h: addr, resp_h: addr): count 7 | { 8 | if(orig_h !in tab) 9 | tab[orig_h] = set(); 10 | add tab[orig_h][get_subnet(resp_h)]; 11 | return |tab[orig_h]|; 12 | } 13 | -------------------------------------------------------------------------------- /tablize: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sed "s/fields.//;s/types.//" | column -s ' ' -t | less -S 3 | -------------------------------------------------------------------------------- /testing/http-watch-header-order.bro: -------------------------------------------------------------------------------- 1 | @load http 2 | @load http-request 3 | @load http-ext 4 | 5 | module HTTP; 6 | 7 | type browser_header_info: record { 8 | name: string &default=""; 9 | user_agent_regex: pattern; 10 | required_headers: vector of string; 11 | headers: vector of string; 12 | rev_headers: table[string] of int; 13 | }; 14 | 15 | export { 16 | # Domains where header order is frequently messed up for various reasons. 17 | const ignore_header_order_at = /\.facebook\.com$/ | 18 | /\.fbcdn\.net$/ | 19 | /\.apmebf\.com$/ | 20 | /\.qq\.com$/ | 21 | /\.yahoo\.com$/ | 22 | /\.mozilla\.com$/ | 23 | /\.google\.com$/ &redef; 24 | 25 | # This is a set of local proxies (proxies frequently rewrite headers) 26 | const local_http_proxies: set[addr] &redef; 27 | } 28 | 29 | const BROWSER_HEADERS: table[string] of browser_header_info = { 30 | ["IE6"] = record($name = "IE6", 31 | $user_agent_regex = /Mozilla\/.*compatible; MSIE 6/ | 32 | #/^iTunes\/.*Windows/ | /^Microsoft-CryptoAPI\// | 33 | /Windows-Update-Agent/, 34 | $required_headers = vector("ACCEPT", "USER-AGENT", "CONNECTION"), 35 | $headers = vector("ACCEPT", "REFERER", "ACCEPT-LANGUAGE", "ACCEPT-ENCODING", "USER-AGENT", "CONNECTION"), 36 | $rev_headers = table([""]=0)), 37 | 38 | ["IE7"] = record($name = "IE7", 39 | $user_agent_regex = /Mozilla\/.*compatible; MSIE 7/, 40 | $required_headers = vector("ACCEPT", "UA-CPU", "USER-AGENT", "CONNECTION"), 41 | $headers = vector("ACCEPT", "REFERER", "ACCEPT-LANGUAGE", "UA-CPU", "ACCEPT-ENCODING", "ACCEPT-CHARSET", "IF-MODIFIED-SINCE", "IF-NONE-MATCH", "USER-AGENT", "CONNECTION", "KEEP-ALIVE"), 42 | $rev_headers = table([""]=0)), 43 | 44 | ["IE8"] = record($name = "IE8", 45 | $user_agent_regex = /Mozilla\/.*MSIE 8/ | 46 | /Mozilla\/.*compatible; MSIE 7.*Trident\/4\.0/, 47 | $required_headers = vector("ACCEPT", "USER-AGENT", "UA-CPU", "HOST", "CONNECTION"), 48 | $headers = vector("ACCEPT", "REFERER", "ACCEPT-LANGUAGE", "USER-AGENT", "UA-CPU", "ACCEPT-ENCODING", "HOST", "CONNECTION", "COOKIE"), 49 | $rev_headers = table([""]=0)), 50 | 51 | ["MSOffice"] = record($name = "MSOffice", 52 | $user_agent_regex = /MSOffice/, 53 | $required_headers = vector("ACCEPT", "USER-AGENT", "UA-CPU", "CONNECTION"), 54 | $headers = vector("ACCEPT", "REFERER", "ACCEPT-LANGUAGE", "USER-AGENT", "UA-CPU", "ACCEPT-ENCODING", "CONNECTION", "COOKIE"), 55 | $rev_headers = table([""]=0)), 56 | 57 | ["FIREFOX"] = record($name = "FIREFOX", 58 | $user_agent_regex = /Gecko\/.*(Firefox|Thunderbird|Netscape)\// | 59 | /^mozbar [0-9\.]* xpi/, 60 | $required_headers = vector("USER-AGENT", "ACCEPT", "ACCEPT-LANGUAGE", "ACCEPT-CHARSET", "CONNECTION"), 61 | $headers = vector("HOST", "USER-AGENT", "ACCEPT", "ACCEPT-LANGUAGE", "ACCEPT-ENCODING", "ACCEPT-CHARSET", "CONTENT-TYPE", "REFERER", "CONTENT-LENGTH", "COOKIE", "RANGE", "CONNECTION"), 62 | $rev_headers = table([""]=0)), 63 | 64 | ["WEBKIT_OSX_<=312"] = record($name="WEBKIT_OSX_<=312", 65 | $user_agent_regex = /(PPC|Intel) Mac OS X;.*Safari\//, 66 | $required_headers = vector("HOST", "CONNECTION", "USER-AGENT", "ACCEPT", "ACCEPT-LANGUAGE"), 67 | $headers = vector("HOST", "CONNECTION", "REFERER", "USER-AGENT", "IF-MODIFIED-SINCE", "ACCEPT", "ACCEPT-ENCODING", "ACCEPT-LANGUAGE", "COOKIE"), 68 | $rev_headers = table([""]=0)), 69 | 70 | ["WEBKIT_OSX_PPC"] = record($name = "WEBKIT_OSX_PPC", 71 | $user_agent_regex = /PPC Mac OS X.*AppleWebKit\/.*(Safari\/)?/, 72 | $required_headers = vector("HOST", "CONNECTION", "USER-AGENT", "ACCEPT", "ACCEPT-LANGUAGE"), 73 | $headers = vector("HOST", "CONNECTION", "REFERER", "USER-AGENT", "ACCEPT", "ACCEPT-ENCODING", "ACCEPT-LANGUAGE"), 74 | $rev_headers = table([""]=0)), 75 | 76 | # ACCEPT was removed as a header because it is put in two different locations at different times. 77 | ["WEBKIT_OSX_10.4"] = record($name = "WEBKIT_OSX_10.4", 78 | $user_agent_regex = /^AppleSyndication/ | 79 | /Mac OS X.*AppleWebKit\/.*(Safari\/)?/, 80 | $required_headers = vector("ACCEPT-LANGUAGE", "ACCEPT-ENCODING", "USER-AGENT", "CONNECTION"), 81 | $headers = vector("ACCEPT-LANGUAGE", "ACCEPT-ENCODING", "COOKIE", "REFERER", "USER-AGENT", "CONNECTION"), 82 | $rev_headers = table([""]=0)), 83 | 84 | ["WEBKIT_OSX_10.5"] = record($name = "WEBKIT_OSX_10.5", 85 | $user_agent_regex = /^Apple-PubSub/ | 86 | /CFNetwork\/.*Darwin\// | 87 | /(Windows|Mac OS X|iPhone OS).*AppleWebKit\/.*(Safari\/)?/, 88 | $required_headers = vector("USER-AGENT", "ACCEPT", "ACCEPT-LANGUAGE", "CONNECTION"), 89 | $headers = vector("USER-AGENT", "REFERER", "ACCEPT", "ACCEPT-LANGUAGE", "COOKIE", "CONNECTION"), 90 | $rev_headers = table([""]=0)), 91 | 92 | ["CHROME_<4.0"] = record($name = "CHROME_<4.0", 93 | $user_agent_regex = /Chrome\/.*Safari\//, 94 | $required_headers = vector("USER-AGENT", "ACCEPT-LANGUAGE", "ACCEPT-CHARSET", "HOST", "CONNECTION"), 95 | $headers = vector("USER-AGENT", "REFERER", "CONTENT-LENGTH", "CONTENT-TYPE", "ACCEPT", "RANGE", "COOKIE", "ACCEPT-LANGUAGE", "ACCEPT-CHARSET", "HOST", "CONNECTION"), 96 | $rev_headers = table([""]=0)), 97 | 98 | ["CHROME_>=4.0"] = record($name = "CHROME_>=4.0", 99 | $user_agent_regex = /Chrome\/.*Safari\//, 100 | $required_headers = vector("HOST", "CONNECTION", "USER-AGENT", "ACCEPT", "ACCEPT-ENCODING", "ACCEPT-LANGUAGE", "ACCEPT-CHARSET"), 101 | $headers = vector("HOST", "CONNECTION", "USER-AGENT", "REFERER", "CONTENT-LENGTH", "CONTENT-TYPE", "ACCEPT", "RANGE", "ACCEPT-ENCODING", "COOKIE", "ACCEPT-LANGUAGE", "ACCEPT-CHARSET"), 102 | $rev_headers = table([""]=0)), 103 | 104 | ["FLASH"] = record($name = "FLASH", 105 | $user_agent_regex = /blah... nothing matches/, 106 | $required_headers = vector("ACCEPT", "ACCEPT-LANGUAGE", "REFERER", "X-FLASH-VERSION", "ACCEPT-ENCODING", "USER-AGENT", "COOKIE", "CONNECTION"), 107 | $headers = vector("ACCEPT", "ACCEPT-LANGUAGE", "REFERER", "X-FLASH-VERSION", "ACCEPT-ENCODING", "USER-AGENT", "COOKIE", "CONNECTION", "HOST"), 108 | $rev_headers = table([""]=0)), 109 | }; 110 | 111 | # Generate all of the reverse header tables. 112 | event bro_init() 113 | { 114 | for ( browser_name in BROWSER_HEADERS ) 115 | { 116 | local browser = BROWSER_HEADERS[browser_name]; 117 | delete browser$rev_headers[""]; 118 | for ( i in browser$headers ) 119 | { 120 | browser$rev_headers[browser$headers[i]] = i; 121 | } 122 | } 123 | } 124 | 125 | 126 | const ordered_headers: set[string] = { 127 | "HOST", 128 | "USER-AGENT", 129 | "ACCEPT", 130 | "ACCEPT-LANGUAGE", 131 | "ACCEPT-ENCODING", 132 | "ACCEPT-CHARSET", 133 | "KEEP-ALIVE", 134 | "CONNECTION", 135 | "CONTENT-TYPE", 136 | "REFERER", 137 | "CONTENT-LENGTH", 138 | "COOKIE", 139 | "RANGE", 140 | "UA-CPU", 141 | "X-FLASH-VERSION", 142 | }; 143 | 144 | type header_tracker: record { 145 | ua_identified: set[string]; 146 | identified: set[string]; 147 | broken: set[string]; 148 | possibles: table[string] of count; 149 | }; 150 | 151 | global tracking_headers: table[conn_id] of header_tracker &read_expire=30secs; 152 | global recently_examined: set[addr] &create_expire=30secs &redef; 153 | 154 | event http_header(c: connection, is_orig: bool, name: string, value: string) 155 | { 156 | if ( !is_orig || 157 | name !in ordered_headers ) 158 | return; 159 | 160 | if ( !is_local_addr(c$id$orig_h) || 161 | #c$id$orig_h in recently_examined || 162 | c$id$orig_h in local_http_proxies ) 163 | return; 164 | 165 | local header = name; 166 | if ( c$id !in tracking_headers ) 167 | { 168 | tracking_headers[c$id] = [$ua_identified=set(""), 169 | $identified=set(""), 170 | $broken=set(""), 171 | $possibles=table(["IE6"]=0, ["IE7"]=0, ["IE8"]=0, ["MSOffice"]=0, ["FIREFOX"]=0, ["WEBKIT_OSX_PPC"]=0, ["WEBKIT_OSX_10.4"]=0, ["WEBKIT_OSX_10.5"]=0, ["CHROME_<4.0"]=0, ["CHROME_>=4.0"]=0, ["FLASH"]=0)]; 172 | # FIXME: this is a hack because set("") above needed an empty element for some reason. 173 | delete tracking_headers[c$id]$identified[""]; 174 | delete tracking_headers[c$id]$ua_identified[""]; 175 | delete tracking_headers[c$id]$broken[""]; 176 | } 177 | 178 | local ht = tracking_headers[c$id]; 179 | 180 | #print fmt("CHECKING HEADER: %s", header); 181 | for ( browser_name in BROWSER_HEADERS ) 182 | { 183 | if ( header == "USER-AGENT" ) 184 | { 185 | if ( BROWSER_HEADERS[browser_name]$user_agent_regex in value ) 186 | add ht$ua_identified[browser_name]; 187 | } 188 | 189 | if ( browser_name !in ht$identified ) 190 | { 191 | local browser = BROWSER_HEADERS[browser_name]; 192 | 193 | if ( browser_name in ht$possibles && 194 | header in browser$rev_headers ) 195 | { 196 | local possible_browser = ht$possibles[browser_name]; # count 197 | local browser_rev_headers = browser$rev_headers; # table[string] of int 198 | local h_position = browser_rev_headers[header]; # count 199 | local req_headers = browser$required_headers; # vector of string 200 | local next_required_header = req_headers[possible_browser+1]; # string 201 | local current_header_val = req_headers[h_position]; # string 202 | #print fmt("for browser: %s :: checking header: %s :: req position: %d :: next required: %s :: len of req headers: %d", browser_name, header, ht$possibles[browser_name], req_headers[ht$possibles[browser_name]+1], |req_headers|); 203 | 204 | if ( next_required_header == header ) 205 | { 206 | ++ht$possibles[browser_name]; 207 | } 208 | 209 | 210 | else if ( possible_browser == 0 || possible_browser == |req_headers| || 211 | (browser_rev_headers[req_headers[possible_browser]] < h_position && 212 | h_position < browser_rev_headers[next_required_header]) ) 213 | { 214 | #print fmt("%s is an optional header for %s (but it is in the correct position).", header, browser_name); 215 | } 216 | else 217 | { 218 | delete ht$possibles[browser_name]; 219 | } 220 | 221 | # Have we found a browser yet? 222 | if ( browser_name in ht$possibles && 223 | ht$possibles[browser_name] == |req_headers| ) 224 | { 225 | add ht$identified[browser_name]; 226 | } 227 | } 228 | } 229 | } 230 | } 231 | 232 | event http_ext(id: conn_id, si: http_ext_session_info) &priority=-10 233 | { 234 | if ( id in tracking_headers && 235 | id$orig_h !in local_http_proxies && 236 | si$proxied_for == "" ) 237 | { 238 | add recently_examined[id$orig_h]; 239 | 240 | if ( ignore_header_order_at in si$host ) 241 | { 242 | #print "we're going to ignore this entire request."; 243 | return; 244 | } 245 | 246 | local is_matched = F; 247 | #if ( |tracking_headers[id]$identified| > 0 ) 248 | # { 249 | # is_matched = F; 250 | # for ( b in tracking_headers[id]$identified ) 251 | # { 252 | # if ( BROWSER_HEADERS[b]$user_agent_regex in si$user_agent ) 253 | # { 254 | # is_matched = T; 255 | # } 256 | # } 257 | # if ( !is_matched ) 258 | # { 259 | # #print fmt("Headers look like %s, but User-Agent doesn't match.", fmt_str_set(tracking_headers[id]$identified, /blah/)); 260 | # print cat_sep("\t", "\\N", 261 | # si$start_time, 262 | # id$orig_h, port_to_count(id$orig_p), 263 | # id$resp_h, port_to_count(id$resp_p), 264 | # fmt_str_set(si$force_log_reasons, /DONTMATCH/), 265 | # si$method, si$url, si$referrer, 266 | # si$user_agent, si$proxied_for); 267 | # } 268 | # } 269 | 270 | # Do this in case the User-Agent is known, but the headers don't match it. 271 | is_matched=F; 272 | for ( b in tracking_headers[id]$ua_identified ) 273 | { 274 | if ( b in tracking_headers[id]$identified ) 275 | { 276 | is_matched=T; 277 | } 278 | } 279 | if ( |tracking_headers[id]$ua_identified| > 0 && !is_matched ) 280 | { 281 | print fmt("User-Agent looks like %s, but headers look like %s.", fmt_str_set(tracking_headers[id]$ua_identified, /blah/), fmt_str_set(tracking_headers[id]$identified, /blah/)); 282 | print cat_sep(" :: ", "\\N", 283 | si$start_time, 284 | id$orig_h, port_to_count(id$orig_p), 285 | id$resp_h, port_to_count(id$resp_p), 286 | fmt_str_set(si$force_log_reasons, /DONTMATCH/), 287 | si$method, si$url, si$referrer, 288 | si$user_agent, si$proxied_for); 289 | 290 | } 291 | } 292 | delete tracking_headers[id]; 293 | } 294 | 295 | -------------------------------------------------------------------------------- /urlsnarf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | FILE=$1 4 | 5 | DIR=$(mktemp -t -d bro_urlsnarf.XXXXXXXXX) 6 | 7 | cd $DIR || exit 1 8 | 9 | bro -f 'not ip6' -C -r $FILE || true 10 | 11 | cat http.log | bro-cut -d -c | sed "s/fields.//;s/types.//" | egrep -v "^#(sep|set|emp|unset|path)" 12 | 13 | rm -rf $DIR 14 | --------------------------------------------------------------------------------