1 The short road to practicality
Much of what I covered in the first two posts didn't have much practical application as it was intended to illustrate what you can do with Bro scripting. We were working off a trace file and a set of questions and we worked to generate a report for those questions. If there's one thing I've learned from talking to Seth Hall (One of the Bro developers and @remor on Twitter) it's that a solution has to be deployable across an enterprise to be worth your time. If your detection method includes "Open up wireshark and load the pcap" that is detection after the fact and you should find a way to make that action automated. Wireshark has its place and it does the job it was designed for very well, however, it can't be deployed across a large scale production environment like Bro can. As I've done in the past posts, I still start with tshark to help me identify the behavior I'm interested with and then pivot to Bro scripts to deploy it widescale. With Bro, we want to leverage the scripting language to be able to define activities of interest and report on them.
2 Detecting web sites that use basic auth
require 'base64' username = 'srunnels' password = 'recursivehoff' p Base64.encode64("#{username}:#{password}")
require 'base64' base64_string = 'c3J1bm5lbHM6cmVjdXJzaXZlaG9mZg==' p Base64.decode64(base64_string)
3 How it looks on the line
mac@lubuntu-VM:~$ tshark -r tracefiles/20120503120402.lpc -R "http contains Auth" -O http -V | awk '/Authorization: Basic/ {print}' Authorization: Basic c3J1bm5lbHM6cmVjdXJzaXZlaG9mZg==\r\n Authorization: Basic c3J1bm5lbHM6cmVjdXJzaXZlaG9mZg==\r\n Authorization: Basic c3J1bm5lbHM6cmVjdXJzaXZlaG9mZg==\r\n Authorization: Basic c3J1bm5lbHM6cmVjdXJzaXZlaG9mZg==\r\nThe Base64 encoded string is sent to the server as part of the HTTP header, which means to start, we're going to take a look for any Bro events that correspond to http and http headers. Some easy grepping through the base scripts from Bro leads us to two events http_header() and http_all_headers(), the difference between the two being that http_header generates an event for every header while http_all_headers will generate a list of headers per request or response. For now we're going to work with http_header and see if it can detect a session using basic auth.
event http_header(c: connection, is_orig: bool, name: string, value: string) { if (/AUTHORIZATION/ in name && /Basic/ in value) print fmt("%s: %s", name, value); }
AUTHORIZATION: Basic c3J1bm5lbHM6cmVjdXJzaXZlaG9mZg== AUTHORIZATION: Basic c3J1bm5lbHM6cmVjdXJzaXZlaG9mZg== AUTHORIZATION: Basic c3J1bm5lbHM6cmVjdXJzaXZlaG9mZg== AUTHORIZATION: Basic c3J1bm5lbHM6cmVjdXJzaXZlaG9mZg==Three lines of Bro's scripting language and we can detect a server using Basic Access Authentication! Now, all that's left is to make Bro understand that we care about this kind of behavior!
4 Generating notices
module HTTP; export { redef enum Notice::Type += { ## Generated if a site is detected using Basic Access Authentication HTTP::Basic_Auth_Server }; }The other entry that stood out from poking around the default scripts was NOTICE(). NOTICE() takes one argument, the Notice::Info record, but it's a whopper. You can pass a rather massive amount of information into NOTICE() via the Notice::Info record but the only required argument to pass in is the Notice::Type. If we just wanted to generate a notice, albeit a somewhat unhelpful one, we could pass it just the Notice::Type we added.
module HTTP; export { redef enum Notice::Type += { ## Generated if a site is detected using Basic Access Authentication HTTP::Basic_Auth_Server }; } event http_header(c: connection, is_orig: bool, name: string, value: string) { if (/AUTHORIZATION/ in name && /Basic/ in value) { NOTICE([$note=HTTP::Basic_Auth_Server]); } }When we run the script against the tracefile, we get a notice.log in the current working directory.
#separator \x09 #set_separator , #empty_field (empty) #unset_field - #path notice #fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto note msg sub src dst p n peer_descr actions policy_items suppress_for dropped remote_location.countr #types time string addr port addr port enum enum string string addr addr port count string table[enum] table[count] interval bool string string string double double addr string 1336061141.701690 - - - - - - HTTP::Basic_Auth_Server - - - - - - bro Notice::ACTION_LOG 6 3600.000000 F - - - - 1336061141.914860 - - - - - - HTTP::Basic_Auth_Server - - - - - - bro Notice::ACTION_LOG 6 3600.000000 F - - - - 1336061141.918352 - - - - - - HTTP::Basic_Auth_Server - - - - - - bro Notice::ACTION_LOG 6 3600.000000 F - - - - 1336061147.472010 - - - - - - HTTP::Basic_Auth_Server - - - - - - bro Notice::ACTION_LOG 6 3600.000000 F - - - -Like I said, a notice albeit, an uninformative one! Let's take a look at what happens when we give NOTICE() a Notice::Type and a connection.
module HTTP; export { redef enum Notice::Type += { ## Generated if a site is detected using Basic Access Authentication HTTP::Basic_Auth_Server }; } event http_header(c: connection, is_orig: bool, name: string, value: string) { if (/AUTHORIZATION/ in name && /Basic/ in value) { NOTICE([$note=HTTP::Basic_Auth_Server, $conn=c ]); } }
#separator \x09 #set_separator , #empty_field (empty) #unset_field - #path notice #fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto note msg sub src dst p n peer_descr actions policy_items suppress_for dropped remote_location.countr #types time string addr port addr port enum enum string string addr addr port count string table[enum] table[count] interval bool string string string double double addr string 1336061141.701690 j931OBJ1895 192.168.164.198 51844 192.168.164.185 80 tcp HTTP::Basic_Auth_Server - - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 3600.000000 1336061141.914860 j931OBJ1895 192.168.164.198 51844 192.168.164.185 80 tcp HTTP::Basic_Auth_Server - - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 3600.000000 1336061141.918352 j931OBJ1895 192.168.164.198 51844 192.168.164.185 80 tcp HTTP::Basic_Auth_Server - - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 3600.000000 1336061147.472010 zKNVZaX8uS7 192.168.164.198 51845 192.168.164.185 80 tcp HTTP::Basic_Auth_Server - - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 3600.000000 (END)Simply adding a connection as an argument allowed Bro's Notice Framework to fill in the the uid, originator's host and port, and the responder's host and port. Now we have a notice that is of actual use! Can we make it better? I think so. There's something interesting to note in the connection that gets passed into http_header().
[id=[orig_h=192.168.164.198, orig_p=51845/tcp, resp_h=192.168.164.185, resp_p=80/tcp], orig=[size=359, state=1, num_pkts=2, num_bytes_ip=112], resp=[size=0, state=0, num_pkts=0, num_bytes_ip=0], start_time=1336061147.471671, duration=0.000339, service={^J^IHTTP^J}, addl=, hot=0, history=ScAD, uid=MBPKpZ7z43e, dpd=<uninitialized>, conn=<uninitialized>, extract_orig=F, extract_resp=F, dns=<uninitialized>, dns_state=<uninitialized>, ftp=<uninitialized>, http=[ts=1336061147.47201, uid=MBPKpZ7z43e, id=[orig_h=192.168.164.198, orig_p=51845/tcp, resp_h=192.168.164.185, resp_p=80/tcp], trans_depth=1, method=GET, host=192.168.164.185, uri=/test.html, referrer=<uninitialized>, user_agent=Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:10.0.2) Gecko/20100101 Firefox/10.0.2, request_body_len=0, response_body_len=0, status_code=<uninitialized>, status_msg=<uninitialized>, info_code=<uninitialized>, info_msg=<uninitialized>, filename=<uninitialized>, tags={^J^J}, username=srunnels, password=<uninitialized>, capture_password=F, proxied=<uninitialized>, mime_type=<uninitialized>, first_chunk=T, md5=<uninitialized>, calc_md5=F, calculating_md5=F, extraction_file=<uninitialized>, extract_file=F], http_state=[pending={^J^I[1] = [ts=1336061147.47201, uid=MBPKpZ7z43e, id=[orig_h=192.168.164.198, orig_p=51845/tcp, resp_h=192.168.164.185, resp_p=80/tcp], trans_depth=1, method=GET, host=192.168.164.185, uri=/test.html, referrer=<uninitialized>, user_agent=Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:10.0.2) Gecko/20100101 Firefox/10.0.2, request_body_len=0, response_body_len=0, status_code=<uninitialized>, status_msg=<uninitialized>, info_code=<uninitialized>, info_msg=<uninitialized>, filename=<uninitialized>, tags={^J^J^I}, username=srunnels, password=<uninitialized>, capture_password=F, proxied=<uninitialized>, mime_type=<uninitialized>, first_chunk=T, md5=<uninitialized>, calc_md5=F, calculating_md5=F, extraction_file=<uninitialized>, extract_file=F]^J}, current_request=1, current_response=0], irc=<uninitialized>, smtp=<uninitialized>, smtp_state=<uninitialized>, ssh=<uninitialized>, ssl=<uninitialized>, syslog=<uninitialized>]It's hard to see through the massive amount of information that Bro includes, but if you look you'll notice three fields are filled in that are directly related to what we're working on.
username=srunnels, password=<uninitialized>, capture_password=F,Bro has not only already detected that username and password was passed across the line but it already decrypted it! The capture_password field is set to False by default in Bro, but it clearly got our username so Bro must have a way of decrypting base64. To the grep-mobile!
1092 ## Decodes a Base64-encoded string. 1093 ## 1094 ## s: The Base64-encoded string. 1095 ## 1096 ## Returns: The decoded version of *s*. 1097 ## 1098 ## .. bro:see:: decode_base64_custom 1099 global decode_base64: function(s: string): string;Some day, I'll stop being shocked by everything Bro does and just accept that it's wall-to-wall awesome. Well, if Bro can do it, let's make our notice include it as well!
module HTTP; export { redef enum Notice::Type += { ## Generated if a site is detected using Basic Access Authentication HTTP::Basic_Auth_Server }; } event http_header(c: connection, is_orig: bool, name: string, value: string) { if (/AUTHORIZATION/ in name && /Basic/ in value) { local parts: string_array; parts = split1(decode_base64(sub_bytes(value, 7, |value|)), /:/); NOTICE([$note=HTTP::Basic_Auth_Server, $msg=fmt("username: %s password: %s", parts[1], HTTP::default_capture_password == F ? "Blocked" : parts[2]), $conn=c ]); } }Here, I've taken the value we find by checking for Authorization Basic in the header and break it into pieces using sub_bytes() and split1(). The function sub_bytes() will take a value, a position, and a length and return a string while split1() will take a string and a regexp and output a string_array whose members are the parts of the string once they've been split one time. In this case, I've used sub_bytes() to remove the "Basic " part of the string in value, then used split1() to break the resulting string on the colon. You'll also notice that I included a ternary operator as part of the fmt() operator. I'm of the opinion that if the Bro developers did it, we should follow suit and in this case, we're only going to include the password if HTTP::default_capture_password is true. Once we run the script against our trace file, we get the output we expected in notice.log.
1336061141.701690 MzLXbKyksE9 192.168.164.198 51844 192.168.164.185 80 tcp HTTP::Basic_Auth_Server username: srunnels password: Blocked - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 3600.000000 F - - - - - - - - 1336061141.914860 MzLXbKyksE9 192.168.164.198 51844 192.168.164.185 80 tcp HTTP::Basic_Auth_Server username: srunnels password: Blocked - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 3600.000000 F - - - - - - - - 1336061141.918352 MzLXbKyksE9 192.168.164.198 51844 192.168.164.185 80 tcp HTTP::Basic_Auth_Server username: srunnels password: Blocked - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 3600.000000 F - - - - - - - - 1336061147.472010 sO6OVrkv5G6 192.168.164.198 51845 192.168.164.185 80 tcp HTTP::Basic_Auth_Server username: srunnels password: Blocked - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 3600.000000 F - - - - - - - -If we include a command line directive to set HTTP::default_capture_password the decrypted password will be included in the notice.log file.
/usr/local/bro/bin/bro -r ~/tracefiles/20120503120402.lpc post3.bro "HTTP::default_capture_password = T;"
1336061141.701690 7VFRK2sOUl4 192.168.164.198 51844 192.168.164.185 80 tcp HTTP::Basic_Auth_Server username: srunnels password: recursivehoff - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 3600.000000 F - - - - - - - - 1336061141.914860 7VFRK2sOUl4 192.168.164.198 51844 192.168.164.185 80 tcp HTTP::Basic_Auth_Server username: srunnels password: recursivehoff - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 3600.000000 F - - - - - - - - 1336061141.918352 7VFRK2sOUl4 192.168.164.198 51844 192.168.164.185 80 tcp HTTP::Basic_Auth_Server username: srunnels password: recursivehoff - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 3600.000000 F - - - - - - - - 1336061147.472010 JDdZhg1kTol 192.168.164.198 51845 192.168.164.185 80 tcp HTTP::Basic_Auth_Server username: srunnels password: recursivehoff - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 3600.000000 F - - - - - - - -
5 Making our script more operationaly relevant
module HTTP; export { redef enum Notice::Type += { ## Generated if a site is detected using Basic Access Authentication HTTP::Basic_Auth_Server }; } redef Notice::Policy += { [$pred(n: Notice::Info) = { return n$note == HTTP::Basic_Auth_Server && Site::is_local_addr(n$id$resp_h); }, $action = Notice::ACTION_EMAIL ] }; event http_header(c: connection, is_orig: bool, name: string, value: string) { if (/AUTHORIZATION/ in name && /Basic/ in value) { local parts: string_array; parts = split1(decode_base64(sub_bytes(value, 7, |value|)), /:/); if (|parts| == 2) NOTICE([$note=HTTP::Basic_Auth_Server, $msg=fmt("username: %s", parts[1]), $identifier=cat(c$id$resp_h, c$id$resp_p), $suppress_for=1day, $conn=c ]); } }Now, when we run the script against the tracefile we only get one notice in notice.log.
1336061141.701690 ZI9LtsaV4Qh 192.168.164.198 51844 192.168.164.185 80 tcp HTTP::Basic_Auth username: srunnels password: recursivehoff - 192.168.164.198 192.168.164.185 80 - bro Notice::ACTION_LOG 6 86400.000000 F - - - - - - - -Another utility we might like to add is to detect when the server using basic auth is one of our servers. To tell Bro to send an email when a notice is generated, we need to write a Notice::policy item that includes an action ($action) and an anonymous function used to determine if sending the email is appropriate ($pred). The appropriate $action would be Notice::ACTION_EMAIL and the predicate depends on the situation and how you want to restrict it. For detecting sites using basic auth, logging a notice to file is fine for when a local system access a server using basic auth, but it could be pretty handy to receive an email if the server using basic auth turns out to be local. So for our script we'd use a Notice::policy like:
redef Notice::policy += { [$pred(n: Notice::Info) = { return n$note == HTTP::Basic_Auth_Server && Site::is_local_addr(n$id$resp_h); } $action = Notice::ACTION_EMAIL ] };Here, we've created an anonymous function that returns true or false depending on if our note is HTTP::Basic_Auth_Server and the host of the responder is determined to be local. If the $pred returns True, the $action is taken resulting in an email alert being sent.
6 Wrapping up
7 Repository
cd /usr/local/bro/share/bro/site git clone git://github.com/srunnels/bro-scripts srunnels-scripts echo "@load srunnels-scripts/http-basic-auth" >> local.bro # If you want to receieve emails about local basic auth notices echo "@load srunnels-scripts/notice-handling" >> local.bro
Date: 2012/05/03