1@load notice 2@load utils/thresholds 3 4module SSH; 5 6export { 7 redef enum Log::ID += { SSH }; 8 9 redef enum Notice::Type += { 10 Login, 11 Password_Guessing, 12 Login_By_Password_Guesser, 13 Login_From_Interesting_Hostname, 14 Bytecount_Inconsistency, 15 }; 16 17 type Info: record { 18 ts: time &log; 19 uid: string &log; 20 id: conn_id &log; 21 status: string &log &optional; 22 direction: string &log &optional; 23 remote_location: geo_location &log &optional; 24 client: string &log &optional; 25 server: string &log &optional; 26 resp_size: count &log &default=0; 27 28 ## Indicate if the SSH session is done being watched. 29 done: bool &default=F; 30 }; 31 32 const password_guesses_limit = 30 &redef; 33 34 # The size in bytes at which the SSH connection is presumed to be 35 # successful. 36 const authentication_data_size = 5500 &redef; 37 38 # The amount of time to remember presumed non-successful logins to build 39 # model of a password guesser. 40 const guessing_timeout = 30 mins &redef; 41 42 # The set of countries for which you'd like to throw notices upon successful login 43 # requires Bro compiled with libGeoIP support 44 const watched_countries: set[string] = {"RO"} &redef; 45 46 # Strange/bad host names to originate successful SSH logins 47 const interesting_hostnames = 48 /^d?ns[0-9]*\./ | 49 /^smtp[0-9]*\./ | 50 /^mail[0-9]*\./ | 51 /^pop[0-9]*\./ | 52 /^imap[0-9]*\./ | 53 /^www[0-9]*\./ | 54 /^ftp[0-9]*\./ &redef; 55 56 # This is a table with orig subnet as the key, and subnet as the value. 57 const ignore_guessers: table[subnet] of subnet &redef; 58 59 # If true, we tell the event engine to not look at further data 60 # packets after the initial SSH handshake. Helps with performance 61 # (especially with large file transfers) but precludes some 62 # kinds of analyses (e.g., tracking connection size). 63 const skip_processing_after_detection = F &redef; 64 65 # Keeps count of how many rejections a host has had 66 global password_rejections: table[addr] of TrackCount 67 &write_expire=guessing_timeout 68 &synchronized; 69 70 # Keeps track of hosts identified as guessing passwords 71 # TODO: guessing_timeout doesn't work correctly here. If a user redefs 72 # the variable, it won't take effect. 73 global password_guessers: set[addr] &read_expire=guessing_timeout+1hr &synchronized; 74 75 global log_ssh: event(rec: Info); 76} 77 78# Configure DPD and the packet filter 79redef capture_filters += { ["ssh"] = "tcp port 22" }; 80redef dpd_config += { [ANALYZER_SSH] = [$ports = set(22/tcp)] }; 81 82redef record connection += { 83 ssh: Info &optional; 84}; 85 86event bro_init() 87{ 88 Log::create_stream(SSH, [$columns=Info, $ev=log_ssh]); 89} 90 91function set_session(c: connection) 92 { 93 if ( ! c?$ssh ) 94 { 95 local info: Info; 96 info$ts=network_time(); 97 info$uid=c$uid; 98 info$id=c$id; 99 c$ssh = info; 100 } 101 } 102 103function check_ssh_connection(c: connection, done: bool) 104 { 105 # If done watching this connection, just return. 106 if ( c$ssh$done ) 107 return; 108 109 # If this is still a live connection and the byte count has not 110 # crossed the threshold, just return and let the resheduled check happen later. 111 if ( !done && c$resp$size < authentication_data_size ) 112 return; 113 114 # Make sure the server has sent back more than 50 bytes to filter out 115 # hosts that are just port scanning. Nothing is ever logged if the server 116 # doesn't send back at least 50 bytes. 117 if ( c$resp$size < 50 ) 118 return; 119 120 local status = "failure"; 121 local direction = Site::is_local_addr(c$id$orig_h) ? "to" : "from"; 122 local location: geo_location; 123 location = (direction == "to") ? lookup_location(c$id$resp_h) : lookup_location(c$id$orig_h); 124 125 if ( done && c$resp$size < authentication_data_size ) 126 { 127 # presumed failure 128 if ( c$id$orig_h !in password_rejections ) 129 password_rejections[c$id$orig_h] = new_track_count(); 130 131 # Track the number of rejections 132 if ( !(c$id$orig_h in ignore_guessers && 133 c$id$resp_h in ignore_guessers[c$id$orig_h]) ) 134 ++password_rejections[c$id$orig_h]$n; 135 136 if ( default_check_threshold(password_rejections[c$id$orig_h]) ) 137 { 138 add password_guessers[c$id$orig_h]; 139 NOTICE([$note=Password_Guessing, 140 $conn=c, 141 $msg=fmt("SSH password guessing by %s", c$id$orig_h), 142 $sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n), 143 $n=password_rejections[c$id$orig_h]$n]); 144 } 145 } 146 # TODO: This is to work around a quasi-bug in Bro which occasionally 147 # causes the byte count to be oversized. 148 # Watch for Gregors work that adds an actual counter of bytes transferred. 149 else if ( c$resp$size < 20000000 ) 150 { 151 # presumed successful login 152 status = "success"; 153 c$ssh$done = T; 154 155 if ( c$id$orig_h in password_rejections && 156 password_rejections[c$id$orig_h]$n > password_guesses_limit && 157 c$id$orig_h !in password_guessers ) 158 { 159 add password_guessers[c$id$orig_h]; 160 NOTICE([$note=Login_By_Password_Guesser, 161 $conn=c, 162 $n=password_rejections[c$id$orig_h]$n, 163 $msg=fmt("Successful SSH login by password guesser %s", c$id$orig_h), 164 $sub=fmt("%d failed logins", password_rejections[c$id$orig_h]$n)]); 165 } 166 167 local message = fmt("SSH login %s %s \"%s\" \"%s\" %f %f %s (triggered with %d bytes)", 168 direction, location$country_code, location$region, location$city, 169 location$latitude, location$longitude, 170 id_string(c$id), c$resp$size); 171 NOTICE([$note=Login, 172 $conn=c, 173 $msg=message, 174 $sub=location$country_code]); 175 176 # Check to see if this login came from an interesting hostname 177 when ( local hostname = lookup_addr(c$id$orig_h) ) 178 { 179 if ( interesting_hostnames in hostname ) 180 { 181 NOTICE([$note=Login_From_Interesting_Hostname, 182 $conn=c, 183 $msg=fmt("Strange login from %s", hostname), 184 $sub=hostname]); 185 } 186 } 187 188 if ( location$country_code in watched_countries ) 189 { 190 191 } 192 193 } 194 else if ( c$resp$size >= 200000000 ) 195 { 196 NOTICE([$note=Bytecount_Inconsistency, 197 $conn=c, 198 $msg="During byte counting in SSH analysis, an overly large value was seen.", 199 $sub=fmt("%d",c$resp$size)]); 200 } 201 202 c$ssh$remote_location = location; 203 c$ssh$status = status; 204 c$ssh$direction = direction; 205 c$ssh$resp_size = c$resp$size; 206 207 Log::write(SSH, c$ssh); 208 209 # Set the "done" flag to prevent the watching event from rescheduling 210 # after detection is done. 211 c$ssh$done; 212 213 # Stop watching this connection, we don't care about it anymore. 214 if ( skip_processing_after_detection ) 215 { 216 skip_further_processing(c$id); 217 set_record_packets(c$id, F); 218 } 219 } 220 221event connection_state_remove(c: connection) &priority=-5 222 { 223 if ( c?$ssh ) 224 check_ssh_connection(c, T); 225 } 226 227event ssh_watcher(c: connection) 228 { 229 local id = c$id; 230 # don't go any further if this connection is gone already! 231 if ( !connection_exists(id) ) 232 return; 233 234 check_ssh_connection(c, F); 235 if ( ! c$ssh$done ) 236 schedule +15secs { ssh_watcher(c) }; 237 } 238 239event ssh_server_version(c: connection, version: string) &priority=5 240 { 241 set_session(c); 242 c$ssh$server = version; 243 } 244 245event ssh_client_version(c: connection, version: string) &priority=5 246 { 247 set_session(c); 248 c$ssh$client = version; 249 schedule +15secs { ssh_watcher(c) }; 250 } 251