zeek/policy.old/login.bro
2011-03-01 10:51:44 -05:00

677 lines
18 KiB
Text

# $Id: login.bro 6481 2008-12-15 00:47:57Z vern $
@load notice
@load weird
@load hot-ids
@load conn
# scan.bro is needed for "account_tried" event.
@load scan
@load demux
@load terminate-connection
module Login;
global telnet_ports = { 23/tcp } &redef;
redef dpd_config += { [ANALYZER_TELNET] = [$ports = telnet_ports] };
global rlogin_ports = { 513/tcp } &redef;
redef dpd_config += { [ANALYZER_RLOGIN] = [$ports = rlogin_ports] };
export {
redef enum Notice += {
SensitiveLogin, # interactive login using sensitive username
# Interactive login seen using forbidden username, but the analyzer
# was confused in following the login dialog, so may be in error.
LoginForbiddenButConfused,
# During a login dialog, a sensitive username (e.g., "rewt") was
# seen in the user's *password*. This is reported as a notice
# because it could be that the login analyzer didn't track the
# authentication dialog correctly, and in fact what it thinks is
# the user's password is instead the user's username.
SensitiveUsernameInPassword,
};
# If these patterns appear anywhere in the user's keystrokes, do a notice.
const input_trouble =
/rewt/
| /eggdrop/
| /\/bin\/eject/
| /oir##t/
| /ereeto/
| /(shell|xploit)_?code/
| /execshell/
| /ff\.core/
| /unset[ \t]+(histfile|history|HISTFILE|HISTORY)/
| /neet\.tar/
| /r0kk0/
| /su[ \t]+(daemon|news|adm)/
| /\.\/clean/
| /rm[ \t]+-rf[ \t]+secure/
| /cd[ \t]+\/dev\/[a-zA-Z]{3}/
| /solsparc_lpset/
| /\.\/[a-z]+[ \t]+passwd/
| /\.\/bnc/
| /bnc\.conf/
| /\"\/bin\/ksh\"/
| /LAST STAGE OF DELIRIUM/
| /SNMPXDMID_PROG/
| /snmpXdmid for solaris/
| /\"\/bin\/uname/
| /gcc[ \t]+1\.c/
| />\/etc\/passwd/
| /lynx[ \t]+-source[ \t]+.*(packetstorm|shellcode|linux|sparc)/
| /gcc.*\/bin\/login/
| /#define NOP.*0x/
| /printf\(\"overflowing/
| /exec[a-z]*\(\"\/usr\/openwin/
| /perl[ \t]+.*x.*[0-9][0-9][0-9][0-9]/
| /ping.*-s.*%d/
&redef;
# If this pattern appears anywhere in the user's input after applying
# <backspace>/<delete> editing, do a notice ...
const edited_input_trouble =
/[ \t]*(cd|pushd|more|less|cat|vi|emacs|pine)[ \t]+((['"]?\.\.\.)|(["'](\.*)[ \t]))/
&redef;
# ... *unless* the corresponding output matches this:
const output_indicates_input_not_trouble = /No such file or directory/ &redef;
# NOTICE on these, but only after waiting for the corresponding output,
# so it can be displayed at the same time.
const input_wait_for_output = edited_input_trouble &redef;
# If the user's entire input matches this pattern, do a notice. Putting
# "loadmodule" here rather than in input_trouble is just to illustrate
# the idea, it could go in either.
const full_input_trouble = /.*loadmodule.*/ &redef;
# If the following appears anywhere in the user's output, do a notice.
const output_trouble =
/^-r.s.*root.*\/bin\/(sh|csh|tcsh)/
| /Jumping to address/
| /Jumping Address/
| /smashdu\.c/
| /PATH_UTMP/
| /Log started at =/
| /www\.anticode\.com/
| /www\.uberhax0r\.net/
| /smurf\.c by TFreak/
| /Super Linux Xploit/
| /^# \[root@/
| /^-r.s.*root.*\/bin\/(time|sh|csh|tcsh|bash|ksh)/
| /invisibleX/
| /PATH_(UTMP|WTMP|LASTLOG)/
| /[0-9]{5,} bytes from/
| /(PATH|STAT):\ .*=>/
| /----- \[(FIN|RST|DATA LIMIT|Timed Out)\]/
| /IDLE TIMEOUT/
| /DATA LIMIT/
| /-- TCP\/IP LOG --/
| /STAT: (FIN|TIMED_OUT) /
| /(shell|xploit)_code/
| /execshell/
| /x86_bsd_compaexec/
| /\\xbf\\xee\\xee\\xee\\x08\\xb8/ # from x.c worm
| /Coded by James Seter/
| /Irc Proxy v/
| /Daemon port\.\.\.\./
| /BOT_VERSION/
| /NICKCRYPT/
| /\/etc\/\.core/
| /exec.*\/bin\/newgrp/
| /deadcafe/
| /[ \/]snap\.sh/
| /Secure atime,ctime,mtime/
| /Can\'t fix checksum/
| /Promisc Dectection/
| /ADMsn0ofID/
| /(cd \/; uname -a; pwd; id)/
| /drw0rm/
| /[Rr][Ee3][Ww][Tt][Ee3][Dd]/
| /rpc\.sadmin/
| /AbraxaS/
| /\[target\]/
| /ID_SENDSYN/
| /ID_DISTROIT/
| /by Mixter/
| /rap(e?)ing.*using weapons/
| /spsiod/
| /[aA][dD][oO][rR][eE][bB][sS][dD]/ # rootkit
&redef;
# Same, but must match entire output.
const full_output_trouble = /.*Trojaning in progress.*/ &redef;
const backdoor_prompts =
/^[!-~]*( ?)[#%$] /
| /.*no job control/
| /WinGate>/
&redef;
const non_backdoor_prompts = /^ *#.*#/ &redef;
const hot_terminal_types = /VT666|007/ &redef;
const hot_telnet_orig_ports = { 53982/tcp, } &redef;
const router_prompts: set[string] &redef;
const non_ASCII_hosts: set[addr] &redef;
const skip_logins_to = { non_ASCII_hosts, } &redef;
const always_hot_login_ids = { always_hot_ids } &redef;
const hot_login_ids = { hot_ids } &redef;
const rlogin_id_okay_if_no_password_exposed = { "root", } &redef;
const BS = "\x08";
const DEL = "\x7f";
global new_login_session:
function(c: connection, pid: peer_id, output_line: count);
global remove_login_session: function(c: connection, pid: peer_id);
global ext_set_login_state:
function(cid: conn_id, pid: peer_id, state: count);
global ext_get_login_state:
function(cid: conn_id, pid: peer_id): count;
}
redef capture_filters += { ["login"] = "port telnet or tcp port 513" };
redef skip_authentication = {
"WELCOME TO THE BERKELEY PUBLIC LIBRARY",
};
redef direct_login_prompts = { "TERMINAL?", };
redef login_prompts = {
"Login:", "login:", "Name:", "Username:", "User:", "Member Name",
"User Access Verification", "Cisco Systems Console",
direct_login_prompts
};
redef login_non_failure_msgs = {
"Failures", "failures", # probably is "<n> failures since last login"
"failure since last successful login",
"failures since last successful login",
};
redef login_non_failure_msgs = {
"Failures", "failures", # probably is "<n> failures since last login"
"failure since last successful login",
"failures since last successful login",
} &redef;
redef login_failure_msgs = {
"invalid", "Invalid", "incorrect", "Incorrect", "failure", "Failure",
# "Unable to authenticate", "unable to authenticate",
"User authorization failure",
"Login failed",
"INVALID", "Sorry.", "Sorry,",
};
redef login_success_msgs = {
"Last login",
"Last successful login", "Last successful login",
"checking for disk quotas", "unsuccessful login attempts",
"failure since last successful login",
"failures since last successful login",
router_prompts,
};
redef login_timeouts = {
"timeout", "timed out", "Timeout", "Timed out",
"Error reading command input", # VMS
};
type check_info: record {
expanded_line: string; # line with all possible editing seqs
hot: bool; # whether any editing sequence was a hot user id
hot_id: string; # the ID considered hot
forbidden: bool; # same, but forbidden user id
};
type login_session_info: record {
user: string;
output_line: count; # number of lines seen
# input string for which we want to match the output.
waiting_for_output: string;
waiting_for_output_line: count; # output line we want to match it to
state: count; # valid for external connections only
};
global login_sessions: table[peer_id, conn_id] of login_session_info;
# The next two functions are "external-to-the-event-engine",
# hence the ext_ prefix. They're used by the script to manage
# login state so that they can work with login sessions unknown
# to the event engine (such as those received from remote peers).
function ext_get_login_state(cid: conn_id, pid: peer_id): count
{
if ( pid == PEER_ID_NONE )
return get_login_state(cid);
return login_sessions[pid, cid]$state;
}
function ext_set_login_state(cid: conn_id, pid: peer_id, state: count)
{
if ( pid == PEER_ID_NONE )
set_login_state(cid, state);
else
login_sessions[pid, cid]$state = state;
}
function new_login_session(c: connection, pid: peer_id, output_line: count)
{
local s: login_session_info;
s$waiting_for_output = s$user = "";
s$output_line = output_line;
s$state = LOGIN_STATE_AUTHENTICATE;
login_sessions[pid, c$id] = s;
}
function remove_login_session(c: connection, pid: peer_id)
{
delete login_sessions[pid, c$id];
}
function is_login_conn(c: connection): bool
{
return c$id$resp_p == telnet || c$id$resp_p == rlogin;
}
function hot_login(c: connection, pid: peer_id, msg: string, tag: string)
{
if ( [pid, c$id] in login_sessions )
NOTICE([$note=SensitiveLogin, $conn=c,
$user=login_sessions[pid, c$id]$user, $msg=msg]);
else
NOTICE([$note=SensitiveLogin, $conn=c, $msg=msg]);
++c$hot;
demux_conn(c$id, tag, "keys", service_name(c));
}
function is_hot_id(id: string, successful: bool, confused: bool): bool
{
return successful ? id in hot_login_ids :
(confused ? id in forbidden_ids :
id in always_hot_login_ids);
}
function is_forbidden_id(id: string): bool
{
return id in forbidden_ids || id == forbidden_id_patterns;
}
function edit_and_check_line(c: connection, pid: peer_id, line: string,
successful: bool): check_info
{
line = to_lower(line);
local ctrl_H_edit = edit(line, BS);
local del_edit = edit(line, DEL);
local confused =
(ext_get_login_state(c$id, pid) == LOGIN_STATE_CONFUSED);
local hot = is_hot_id(line, successful, confused);
local hot_id = hot ? line : "";
local forbidden = is_forbidden_id(line);
local eline = line;
if ( ctrl_H_edit != line )
{
eline = fmt("%s,%s", eline, ctrl_H_edit);
if ( ! hot && is_hot_id(ctrl_H_edit, successful, confused) )
{
hot = T;
hot_id = ctrl_H_edit;
}
forbidden = forbidden || is_forbidden_id(ctrl_H_edit);
}
if ( del_edit != line )
{
eline = fmt("%s,%s", eline, del_edit);
if ( ! hot && is_hot_id(del_edit, successful, confused) )
{
hot = T;
hot_id = del_edit;
}
forbidden = forbidden || is_forbidden_id(del_edit);
}
local results: check_info;
results$expanded_line = eline;
results$hot = hot;
results$hot_id = hot_id;
results$forbidden = forbidden;
return results;
}
function edit_and_check_user(c: connection, pid: peer_id, user: string,
successful: bool, fmt_s: string): bool
{
local check = edit_and_check_line(c, pid, user, successful);
if ( [pid, c$id] !in login_sessions )
new_login_session(c, pid, 9999);
login_sessions[pid, c$id]$user = check$expanded_line;
c$addl = fmt(fmt_s, c$addl, check$expanded_line);
if ( check$hot )
{
++c$hot;
demux_conn(c$id, check$hot_id, "keys", service_name(c));
}
if ( check$forbidden )
{
if ( ext_get_login_state(c$id, pid) == LOGIN_STATE_CONFUSED )
NOTICE([$note=LoginForbiddenButConfused, $conn=c,
$user = user,
$msg=fmt("not terminating %s because confused about state", full_id_string(c))]);
else
TerminateConnection::terminate_connection(c);
}
return c$hot > 0;
}
function edit_and_check_password(c: connection, pid: peer_id, password: string)
{
local check = edit_and_check_line(c, pid, password, T);
if ( check$hot )
{
++c$hot;
NOTICE([$note=SensitiveUsernameInPassword, $conn=c,
$user=password,
$msg=fmt("%s password: \"%s\"",
id_string(c$id), check$expanded_line)]);
}
}
event login_failure(c: connection, user: string, client_user: string,
password: string, line: string)
{
local pid = get_event_peer()$id;
event account_tried(c, user, password);
edit_and_check_password(c, pid, password);
if ( c$hot == 0 && password == "" &&
! edit_and_check_line(c, pid, user, F)$hot )
# Don't both reporting it, this was clearly a half-hearted
# attempt and it's not a sensitive username.
return;
local user_hot = edit_and_check_user(c, pid, user, F, "%sfail/%s ");
if ( client_user != "" && client_user != user &&
edit_and_check_user(c, pid, client_user, F, "%s(%s) ") )
user_hot = T;
if ( user_hot || c$hot > 0 )
NOTICE([$note=SensitiveLogin, $conn=c,
$user=user, $sub=client_user,
$msg=fmt("%s %s", id_string(c$id), c$addl)]);
}
event login_success(c: connection, user: string, client_user: string,
password: string, line: string)
{
local pid = get_event_peer()$id;
Hot::check_hot(c, Hot::APPL_ESTABLISHED);
event account_tried(c, user, password);
edit_and_check_password(c, pid, password);
# Look for whether the user name is sensitive; but allow for
# some ids being okay if no password was exposed accessing them.
local user_hot = F;
if ( c$id$resp_p == rlogin && password == "<none>" &&
user in rlogin_id_okay_if_no_password_exposed )
append_addl(c, fmt("\"%s\"", user));
else
user_hot = edit_and_check_user(c, pid, user, T, "%s\"%s\" ");
if ( c$id$resp_p == rlogin && client_user in always_hot_login_ids )
{
append_addl(c, fmt("(%s)", client_user));
demux_conn(c$id, client_user, "keys", service_name(c));
user_hot = T;
}
if ( user_hot || c$hot > 0 )
NOTICE([$note=SensitiveLogin, $conn=c,
$user=user, $sub=client_user,
$msg=fmt("%s %s", id_string(c$id), c$addl)]);
# else if ( password == "" )
# alarm fmt("%s %s <no password>", id_string(c$id), c$addl);
### use the following if no login_input_line/login_output_line
# else
# {
# set_record_packets(c$id, F);
# skip_further_processing(c$id);
# }
}
event login_input_line(c: connection, line: string)
{
local pid = get_event_peer()$id;
local BS_line = edit(line, BS);
local DEL_line = edit(line, DEL);
if ( input_trouble in line ||
### need to merge input_trouble and edited_input_trouble here
### ideally, match on input_trouble would tell whether we need
### to invoke the edit functions, as an attribute of a .*(^H|DEL)
### rule.
input_trouble in BS_line || input_trouble in DEL_line ||
(edited_input_trouble in BS_line &&
# If one is in but the other not, then the one that's not
# is presumably the correct edit, and the one that is, isn't
# in fact edited at all
edited_input_trouble in DEL_line) ||
line == full_input_trouble )
{
if ( [pid, c$id] !in login_sessions )
new_login_session(c, pid, 9999);
if ( edited_input_trouble in BS_line &&
edited_input_trouble in DEL_line )
{
login_sessions[pid, c$id]$waiting_for_output = line;
login_sessions[pid, c$id]$waiting_for_output_line =
# We don't want the *next* line, that's just
# the echo of this input.
login_sessions[pid, c$id]$output_line + 2;
}
else if ( ++c$hot <= 2 )
hot_login(c, pid, fmt("%s input \"%s\"", id_string(c$id), line), "trb");
}
}
event login_output_line(c: connection, line: string)
{
local pid = get_event_peer()$id;
if ( [pid, c$id] !in login_sessions )
new_login_session(c, pid, 9999);
local s = login_sessions[pid, c$id];
if ( line != "" && ++s$output_line == 1 )
{
if ( byte_len(line) < 40 &&
backdoor_prompts in line && non_backdoor_prompts !in line )
hot_login(c, pid, fmt("%s possible backdoor \"%s\"", id_string(c$id), line), "trb");
}
if ( s$waiting_for_output != "" &&
s$output_line >= s$waiting_for_output_line )
{
if ( output_indicates_input_not_trouble !in line )
hot_login(c, pid,
fmt("%s input \"%s\" yielded output \"%s\"",
id_string(c$id),
s$waiting_for_output,
line),
"trb");
s$waiting_for_output = "";
}
if ( byte_len(line) < 256 &&
(output_trouble in line || line == full_output_trouble) &&
++c$hot <= 2 )
hot_login(c, pid, fmt("%s output \"%s\"", id_string(c$id), line), "trb");
}
event login_confused(c: connection, msg: string, line: string)
{
Hot::check_hot(c, Hot::APPL_ESTABLISHED);
append_addl(c, "<confused>");
event conn_weird_addl(msg, c, line);
set_record_packets(c$id, T);
}
event login_confused_text(c: connection, line: string)
{
local pid = get_event_peer()$id;
if ( c$hot == 0 && edit_and_check_line(c, pid, line, F)$hot )
{
local ignore =
edit_and_check_user(c, pid, line, F, "%sconfused/%s ");
NOTICE([$note=SensitiveLogin, $conn=c,
$user=line,
$msg=fmt("%s %s", id_string(c$id), c$addl)]);
set_record_packets(c$id, T);
}
}
event login_terminal(c: connection, terminal: string)
{
local pid = get_event_peer()$id;
if ( hot_terminal_types in terminal )
hot_login(c, pid,
fmt("%s term %s", id_string(c$id), terminal), "trb");
}
event login_prompt(c: connection, prompt: string)
{
# Could check length >= 6, per Solaris exploit ...
local pid = get_event_peer()$id;
hot_login(c, pid,
fmt("%s $TTYPROMPT %s", id_string(c$id), prompt), "trb");
}
event excessive_line(c: connection)
{
if ( is_login_conn(c) )
{
local pid = get_event_peer()$id;
if ( ! c$hot && c$id$resp_h in non_ASCII_hosts )
{
ext_set_login_state(c$id, pid, LOGIN_STATE_SKIP);
set_record_packets(c$id, F);
}
else if ( ext_get_login_state(c$id, pid) == LOGIN_STATE_AUTHENTICATE )
{
event login_confused(c, "excessive_line", "");
ext_set_login_state(c$id, pid, LOGIN_STATE_CONFUSED);
}
}
}
event inconsistent_option(c: connection)
{
print Weird::weird_file, fmt("%.6f %s inconsistent option", network_time(), id_string(c$id));
}
event bad_option(c: connection)
{
print Weird::weird_file, fmt("%.6f %s bad option", network_time(), id_string(c$id));
}
event bad_option_termination(c: connection)
{
print Weird::weird_file, fmt("%.6f %s bad option termination", network_time(), id_string(c$id));
}
event authentication_accepted(name: string, c: connection)
{
local addl_msg = fmt("auth/%s", name);
append_addl(c, addl_msg);
}
event authentication_rejected(name: string, c: connection)
{
append_addl(c, fmt("auth-failed/%s", name));
}
event authentication_skipped(c: connection)
{
append_addl(c, "(skipped)");
skip_further_processing(c$id);
if ( ! c$hot )
set_record_packets(c$id, F);
}
event connection_established(c: connection)
{
if ( is_login_conn(c) )
{
local pid = get_event_peer()$id;
new_login_session(c, pid, 0);
if ( c$id$resp_h in skip_logins_to )
event authentication_skipped(c);
if ( c$id$resp_p == telnet &&
c$id$orig_p in hot_telnet_orig_ports )
hot_login(c, pid, fmt("%s hot_orig_port", id_string(c$id)), "orig");
}
}
event partial_connection(c: connection)
{
if ( is_login_conn(c) )
{
local pid = get_event_peer()$id;
new_login_session(c, pid, 9999);
ext_set_login_state(c$id, pid, LOGIN_STATE_CONFUSED);
if ( c$id$resp_p == telnet &&
c$id$orig_p in hot_telnet_orig_ports )
hot_login(c, pid, fmt("%s hot_orig_port", id_string(c$id)), "orig");
}
}
event connection_finished(c: connection)
{
local pid = get_event_peer()$id;
remove_login_session(c, pid);
}
event activating_encryption(c: connection)
{
if ( is_login_conn(c) )
append_addl(c, "(encrypted)");
}