# $Id: login.bro,v 1.30 2003/08/31 00:40:21 vern Exp $

@load alert
@load weird
@load conn
@load demux

redef enum Alert += {
	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 an alert
	# 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,
};

redef capture_filters += { ["login"] = "port telnet or tcp port 513" };

# If these patterns appear anywhere in the user's keystrokes, alert.
global input_trouble =
	  /rewt/
	| /eggdrop/
	| /\/bin\/eject/
&redef;

# If this pattern appears anywhere in the user's input after applying
# <backspace>/<delete> editing, alert.
global edited_input_trouble =
	/[ \t]*cd[ \t]+((['"]?\.\.\.)|(["'](\.[^"']*)[ \t]))/
&redef;

# Alert on these, but only after waiting for the corresponding output,
# so it can be displayed at the same time.
global input_wait_for_output = edited_input_trouble &redef;

# If the user's entire input matches this pattern, alert.  Putting
# "loadmodule" here rather than in input_trouble is just to illustrate
# the idea, it could go in either.
global full_input_trouble = /.*loadmodule.*/ &redef;

# If the following appears anywhere in the user's output, alert.
global output_trouble =
	  /^-r.s.*root.*\/bin\/(sh|csh|tcsh)/
	| /Jumping to address/
	| /smashdu\.c/
	| /PATH_UTMP/
	| /Log started at =/
	| /www\.anticode\.com/
	| /www\.uberhax0r\.net/
	| /smurf\.c by TFreak/
	| /Super Linux Xploit/
&redef;

# Same, but must match entire output.
global full_output_trouble = /.*Trojaning in progress.*/;

global backdoor_prompts = /^[!-~]*( ?)[#%$] / &redef;
global non_backdoor_prompts = /^ *#.*#/ &redef;

global hot_terminal_types = /VT666/ &redef;

global hot_telnet_orig_ports = { 53982/tcp, } &redef;

redef skip_authentication = {
	"WELCOME TO THE BERKELEY PUBLIC LIBRARY",
} &redef;

redef direct_login_prompts = { "TERMINAL?", } &redef;

redef login_prompts = {
	"Login:", "login:", "Name:", "Username:", "User:", "Member Name",
	direct_login_prompts
} &redef;

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;

global router_prompts: set[string] &redef;

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;

redef login_timeouts = {
	"timeout", "timed out", "Timeout", "Timed out",
	"Error reading command input",	# VMS
} &redef;

global non_ASCII_hosts: set[addr] &redef;

global skip_logins_to = { non_ASCII_hosts, } &redef;

@load hot-ids
global always_hot_login_ids = { always_hot_ids } &redef;
global hot_login_ids = { hot_ids } &redef;
global rlogin_id_okay_if_no_password_exposed = { "root", } &redef;

const BS = "\x08";
const DEL = "\x7f";


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
};

global login_sessions: table[conn_id] of login_session_info;

function new_login_session(c: connection, output_line: count): bool
	{
	local s: login_session_info;
	s$waiting_for_output = s$user = "";
	s$output_line = output_line;

	login_sessions[c$id] = s;
	}

function is_login_conn(c: connection): bool
	{
	return c$id$resp_p == telnet || c$id$resp_p == rlogin;
	}

function hot_login(c: connection, msg: string, tag: string): bool
	{
	ALERT([$alert=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, 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 = (get_login_state(c$id) == 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, user: string, successful: bool,
				fmt_s: string): bool
	{
	local check = edit_and_check_line(c, user, successful);

	if ( c$id !in login_sessions )
		new_login_session(c, 9999);

	login_sessions[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 ( get_login_state(c$id) == LOGIN_STATE_CONFUSED )
			ALERT([$alert=LoginForbiddenButConfused, $conn=c,
				$msg=fmt("not terminating %s because confused about state", full_id_string(c))]);
		else
			terminate_connection(c);
		}

	return c$hot > 0;
	}

function edit_and_check_password(c: connection, password: string): bool
	{
	local check = edit_and_check_line(c, password, T);
	if ( check$hot )
		{
		++c$hot;
		ALERT([$alert=SensitiveUsernameInPassword, $conn=c,
			$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)
	{
	event account_tried(c, user, password);
	edit_and_check_password(c, password);

	if ( c$hot == 0 && password == "" && ! edit_and_check_line(c, 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, user, F, "%sfail/%s ");
	if ( client_user != "" && client_user != user &&
	     edit_and_check_user(c, client_user, F, "%s(%s) ") )
		user_hot = T;

	if ( user_hot || c$hot > 0 )
		ALERT([$alert=SensitiveLogin, $conn=c,
			$msg=fmt("%s %s", id_string(c$id), c$addl)]);
	}

event login_success(c: connection, user: string, client_user: string,
			password: string, line: string)
	{
	check_hot(c, APPL_ESTABLISHED);
	event account_tried(c, user, password);
	edit_and_check_password(c, 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, 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 )
		ALERT([$alert=SensitiveLogin, $conn=c,
			$msg=fmt("%s %s", id_string(c$id), c$addl)]);

	# else if ( password == "" )
	# 	log 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 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 ( c$id !in login_sessions )
			new_login_session(c, 9999);

		if ( edited_input_trouble in BS_line &&
		     edited_input_trouble in DEL_line )
			{
			login_sessions[c$id]$waiting_for_output = line;
			login_sessions[c$id]$waiting_for_output_line =
				# We don't want the *next* line, that's just
				# the echo of this input.
				login_sessions[c$id]$output_line + 2;
			}

		else if ( ++c$hot <= 2 )
			hot_login(c, fmt("%s input \"%s\"", id_string(c$id), line), "trb");
		}
	}

event login_output_line(c: connection, line: string)
	{
	if ( c$id !in login_sessions )
		new_login_session(c, 9999);

	local s = login_sessions[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, 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 ( /No such file or directory/ !in line )
			hot_login(c,
				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, fmt("%s output \"%s\"", id_string(c$id), line), "trb");
	}

event login_confused(c: connection, msg: string, line: string)
	{
	check_hot(c, APPL_ESTABLISHED);

	append_addl(c, "<confused>");

	if ( line == "" )
		print weird_file, fmt("%.6f %s %s", network_time(), id_string(c$id), msg);
	else
		print weird_file, fmt("%.6f %s %s (%s)", network_time(), id_string(c$id), msg, line);

	set_record_packets(c$id, T);
	}

event login_confused_text(c: connection, line: string)
	{
	if ( c$hot == 0 && edit_and_check_line(c, line, F)$hot )
		{
		local ignore = edit_and_check_user(c, line, F, "%sconfused/%s ");
		ALERT([$alert=SensitiveLogin, $conn=c,
			$msg=fmt("%s %s", id_string(c$id), c$addl)]);
		set_record_packets(c$id, T);
		}
	}

event login_terminal(c: connection, terminal: string)
	{
	if ( hot_terminal_types in terminal )
		hot_login(c, fmt("%s term %s", id_string(c$id), terminal), "trb");
	}

event login_prompt(c: connection, prompt: string)
	{
	# Could check length >= 6, per Solaris exploit ...
	hot_login(c, fmt("%s $TTYPROMPT %s", id_string(c$id), prompt), "trb");
	}

event excessive_line(c: connection)
	{
	if ( is_login_conn(c) )
		{
		if ( ! c$hot && c$id$resp_h in non_ASCII_hosts )
			{
			set_login_state(c$id, LOGIN_STATE_SKIP);
			set_record_packets(c$id, F);
			}
		else if ( get_login_state(c$id) == LOGIN_STATE_AUTHENTICATE )
			{
			event login_confused(c, "excessive_line", "");
			set_login_state(c$id, LOGIN_STATE_CONFUSED);
			}
		}
	}

event inconsistent_option(c: connection)
	{
	print weird_file, fmt("%.6f %s inconsistent option", network_time(), id_string(c$id));
	}

event bad_option(c: connection)
	{
	print weird_file, fmt("%.6f %s bad option", network_time(), id_string(c$id));
	}

event bad_option_termination(c: connection)
	{
	print 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) )
		{
		new_login_session(c, 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, fmt("%s hot_orig_port", id_string(c$id)), "orig");
		}
	}

event partial_connection(c: connection)
	{
	if ( is_login_conn(c) )
		{
		new_login_session(c, 9999);
		set_login_state(c$id, LOGIN_STATE_CONFUSED);

		if ( c$id$resp_p == telnet &&
		     c$id$orig_p in hot_telnet_orig_ports )
			hot_login(c, fmt("%s hot_orig_port", id_string(c$id)), "orig");
		}
	}

event connection_finished(c: connection)
	{
	delete login_sessions[c$id];
	}

event activating_encryption(c: connection)
	{
	if ( is_login_conn(c) )
		append_addl(c, "(encrypted)");
	}
