# $Id: portmapper.bro 4758 2007-08-10 06:49:23Z vern $ @load notice @load hot @load conn @load weird @load scan module Portmapper; export { redef enum Notice += { # Some combination of the service looked up, the host doing the # request, and the server contacted is considered sensitive. SensitivePortmapperAccess, }; # Kudos to Job de Haas for a lot of these entries. const rpc_programs = { [200] = "aarp", [100000] = "portmapper", [100001] = "rstatd", [100002] = "rusersd", [100003] = "nfs", [100004] = "ypserv", [100005] = "mountd", [100007] = "ypbind", [100008] = "walld", [100009] = "yppasswdd", [100010] = "etherstatd", [100011] = "rquotad", [100012] = "sprayd", [100013] = "3270_mapper", [100014] = "rje_mapper", [100015] = "selection_svc", [100016] = "database_svc", [100017] = "rexd", [100018] = "alis", [100019] = "sched", [100020] = "llockmgr", [100021] = "nlockmgr", [100022] = "x25.inr", [100023] = "statmon", [100024] = "status", [100026] = "bootparam", [100028] = "ypupdate", [100029] = "keyserv", [100033] = "sunlink_mapper", [100036] = "pwdauth", [100037] = "tfsd", [100038] = "nsed", [100039] = "nsemntd", [100041] = "pnpd", [100042] = "ipalloc", [100043] = "filehandle", [100055] = "ioadmd", [100062] = "NETlicense", [100065] = "sunisamd", [100066] = "debug_svc", [100068] = "cms", [100069] = "ypxfrd", [100071] = "bugtraqd", [100078] = "kerbd", [100083] = "tooltalkdb", [100087] = "admind", [100099] = "autofsd", [100101] = "event", [100102] = "logger", [100104] = "sync", [100105] = "diskinfo", [100106] = "iostat", [100107] = "hostperf", [100109] = "activity", [100111] = "lpstat", [100112] = "hostmem", [100113] = "sample", [100114] = "x25", [100115] = "ping", [100116] = "rpcnfs", [100117] = "hostif", [100118] = "etherif", [100119] = "ippath", [100120] = "iproutes", [100121] = "layers", [100122] = "snmp", [100123] = "traffic", [100131] = "layers2", [100135] = "etherif2", [100136] = "hostmem2", [100137] = "iostat2", [100138] = "snmpv2", [100139] = "sender", [100221] = "kcms", [100227] = "nfs_acl", [100229] = "metad", [100230] = "metamhd", [100232] = "sadmind", [100233] = "ufsd", [100235] = "cachefsd", [100249] = "snmpXdmid", [100300] = "nisd", [100301] = "nis_cache", [100302] = "nis_callback", [100303] = "nispasswd", [120126] = "nf_snmd", [120127] = "nf_snmd", [150001] = "pcnfsd", [300004] = "frameuser", [300009] = "stdfm", [300019] = "amd", [300433] = "bssd", [300434] = "drdd", [300598] = "dmispd", [390100] = "prestoctl_svc", [390600] = "arserverd", [390601] = "ntserverd", [390604] = "arservtcd", [391000] = "SGI_snoopd", [391001] = "SGI_toolkitbus", [391002] = "SGI_fam", [391003] = "SGI_notepad", [391004] = "SGI_mountd", [391005] = "SGI_smtd", [391006] = "SGI_pcsd", [391007] = "SGI_nfs", [391008] = "SGI_rfind", [391009] = "SGI_pod", [391010] = "SGI_iphone", [391011] = "SGI_videod", [391012] = "SGI_testcd", [391013] = "SGI_ha_hb", [391014] = "SGI_ha_nc", [391015] = "SGI_ha_appmon", [391016] = "SGI_xfsmd", [391017] = "SGI_mediad", # 391018 - 391063 = "SGI_reserved" [545580417] = "bwnfsd", [555555554] = "inetray.start", [555555555] = "inetray", [555555556] = "inetray", [555555557] = "inetray", [555555558] = "inetray", [555555559] = "inetray", [555555560] = "inetray", [600100069] = "fypxfrd", [1342177279] = "Solaris/CDE", # = 0x4fffffff # Some services that choose numbers but start often at these values. [805306368] = "dmispd", [824395111] = "cfsd", [1092830567] = "cfsd", } &redef; const NFS_services = { "mountd", "nfs", "pcnfsd", "nlockmgr", "rquotad", "status" } &redef; # Indexed by the host providing the service, the host requesting it, # and the service. const RPC_okay: set[addr, addr, string] &redef; const RPC_okay_nets: set[subnet] &redef; const RPC_okay_services: set[string] &redef; const NFS_world_servers: set[addr] &redef; # Indexed by the portmapper request and a boolean that's T if # the request was answered, F it was attempted but not answered. # If there's an entry in the set, then the access won't lead to a # NOTICE (unless the connection is hot for some other reason). const RPC_do_not_complain: set[string, bool] = { ["pm_null", [T, F]], } &redef; # Indexed by the host requesting the dump and the host from which it's # requesting it. const RPC_dump_okay: set[addr, addr] &redef; # Indexed by the host providing the service - any host can request it. const any_RPC_okay = { [NFS_world_servers, NFS_services], [sun-rpc.mcast.net, "ypserv"], # sigh } &redef; # Logs all portmapper activity as readable "messages" # Format: timestamp orig_p resp_h resp_p proto localInit PortmapProcedure success details const log_file = open_log_file("portmapper") &redef; # Logs all portmapper mappings that we observe (i.e., getport and # dump replies. Format: # timestamp orig_h orig_p resp_h resp_p proto localInit PortmapProcedure RPCprogram version port proto # the mapping is then: accepts with # calls on . We learned this mapping via const mapping_log_file = open_log_file("portmapper-maps") &redef; } redef capture_filters += { ["portmapper"] = "port 111" }; const portmapper_ports = { 111/tcp, 111/udp } &redef; redef dpd_config += { [ANALYZER_PORTMAPPER] = [$ports = portmapper_ports] }; # Indexed by source and destination addresses, plus the portmapper service. # If the tuple is in the set, then we already created a NOTICE for it and # shouldn't do so again. global did_pm_notice: set[addr, addr, string]; # Indexed by source and portmapper service. If set, we already created # a notice and shouldn't do so again. global suppress_pm_notice: set[addr, string]; function RPC_weird_action_filter(c: connection): Weird::WeirdAction { if ( c$id$orig_h in RPC_okay_nets ) return Weird::WEIRD_FILE; else return Weird::WEIRD_UNSPECIFIED; } redef Weird::weird_action_filters += { [["bad_RPC", "excess_RPC", "multiple_RPCs", "partial_RPC"]] = RPC_weird_action_filter, }; function rpc_prog(p: count): string { if ( p in rpc_programs ) return rpc_programs[p]; else return fmt("unknown-%d", p); } function pm_get_conn_string(cid: conn_id) : string { return fmt("%s %d %s %d %s %s", cid$orig_h, cid$orig_p, cid$resp_h, cid$resp_p, get_port_transport_proto(cid$resp_p), is_local_addr(cid$orig_h) ? "L" : "X" ); } # Log a pm_request or pm_attempt to the log file function pm_log(r: connection, proc: string, msg: string, success: bool) { print log_file, fmt("%f %s %s %s %s", network_time(), pm_get_conn_string(r$id), proc, success, msg); } # Log portmapper mappings received from a dump procedure function pm_log_mapping_dump(r: connection, m: pm_mappings) { # TODO: sort by program and version for ( mp in m ) { local prog = rpc_prog(m[mp]$program); local ver = m[mp]$version; local p = m[mp]$p; print mapping_log_file, fmt("%f %s pm_dump %s %d %d %s", network_time(), pm_get_conn_string(r$id), prog, ver, p, get_port_transport_proto(p)); } } # Log portmapper mappings received from a getport procedure # Unfortunately, pm_request_getport doesn't return pm_mapping, # but returns the parameters separately .... function pm_log_mapping_getport(r: connection, pr: pm_port_request, p: port) { local prog = rpc_prog(pr$program); local ver = pr$version; print mapping_log_file, fmt("%f %s pm_getport %s %d %d %s", network_time(), pm_get_conn_string(r$id), prog, ver, p, get_port_transport_proto(p)); } function pm_check_getport(r: connection, prog: string): bool { if ( prog in RPC_okay_services || [r$id$resp_h, prog] in any_RPC_okay || [r$id$resp_h, r$id$orig_h, prog] in RPC_okay ) return F; if ( r$id$orig_h in RPC_okay_nets ) return F; return T; } function pm_activity(r: connection, do_notice: bool, proc: string) { local id = r$id; if ( do_notice && [id$orig_h, id$resp_h, proc] !in did_pm_notice && [id$orig_h, proc] !in suppress_pm_notice ) { NOTICE([$note=SensitivePortmapperAccess, $conn=r, $msg=fmt("rpc: %s %s: %s", id_string(r$id), proc, r$addl)]); add did_pm_notice[id$orig_h, id$resp_h, proc]; } } function pm_request(r: connection, proc: string, addl: string, do_notice: bool) { if ( [proc, T] in RPC_do_not_complain ) do_notice = F; if ( ! is_tcp_port(r$id$orig_p) ) { # It's UDP, so no connection_established event - check for # scanning, hot access, here, instead. Scan::check_scan(r, T, F); Hot::check_hot(r, Hot::CONN_ESTABLISHED); } if ( r$addl == "" ) r$addl = addl; else { if ( byte_len(r$addl) > 80 ) { # r already has a lot of annotation. We can sometimes # get *zillions* of successive pm_request's with the # same connection ID, depending on how the RPC client # behaves. For those, don't add any further, except # add an ellipses if we don't already have one. append_addl(r, "..."); } else append_addl_marker(r, addl, ", "); } add r$service[proc]; Hot::check_hot(r, Hot::CONN_FINISHED); pm_activity(r, do_notice || r$hot > 0, proc); pm_log(r, proc, addl, T); } event pm_request_null(r: connection) { pm_request(r, "pm_null", "", F); } event pm_request_set(r: connection, m: pm_mapping, success: bool) { pm_request(r, "pm_set", fmt("%s %d (%s)", rpc_prog(m$program), m$p, success ? "ok" : "failed"), T); } event pm_request_unset(r: connection, m: pm_mapping, success: bool) { pm_request(r, "pm_unset", fmt("%s %d (%s)", rpc_prog(m$program), m$p, success ? "ok" : "failed"), T); } function update_RPC_server_map(server: addr, p: port, prog: string) { if ( [server, p] in RPC_server_map ) { if ( prog !in RPC_server_map[server, p] ) { RPC_server_map[server, p] = fmt("%s/%s", RPC_server_map[server, p], prog); } } else RPC_server_map[server, p] = prog; } event pm_request_getport(r: connection, pr: pm_port_request, p: port) { local prog = rpc_prog(pr$program); local do_notice = pm_check_getport(r, prog); update_RPC_server_map(r$id$resp_h, p, prog); pm_request(r, "pm_getport", fmt("%s -> %s", prog, p), do_notice); pm_log_mapping_getport(r, pr, p); } # Note, this function has the side effect of updating the # RPC_server_map function pm_mapping_to_text(server: addr, m: pm_mappings): string { # Used to suppress multiple entries for multiple versions. local mapping_seen: set[count, port]; local addls: vector of string; local num_addls = 0; for ( mp in m ) { local prog = m[mp]$program; local p = m[mp]$p; if ( [prog, p] !in mapping_seen ) { add mapping_seen[prog, p]; addls[num_addls] = fmt("%s -> %s", rpc_prog(prog), p); ++num_addls; update_RPC_server_map(server, p, rpc_prog(prog)); } } local addl_str = fmt("%s", sort(addls, strcmp)); # Lop off surrounding []'s for compatibility with previous # format. addl_str = sub(addl_str, /^\[/, ""); addl_str = sub(addl_str, /\]$/, ""); return addl_str; } event pm_request_dump(r: connection, m: pm_mappings) { local do_notice = [r$id$orig_h, r$id$resp_h] !in RPC_dump_okay; # pm_mapping_to_text has the side-effect of updating RPC_server_map pm_request(r, "pm_dump", length(m) == 0 ? "(nil)" : pm_mapping_to_text(r$id$resp_h, m), do_notice); pm_log_mapping_dump(r, m); } event pm_request_callit(r: connection, call: pm_callit_request, p: port) { local orig_h = r$id$orig_h; local prog = rpc_prog(call$program); local do_notice = [orig_h, prog] !in suppress_pm_notice; pm_request(r, "pm_callit", fmt("%s/%d (%d bytes) -> %s", prog, call$proc, call$arg_size, p), do_notice); if ( prog == "walld" ) add suppress_pm_notice[orig_h, prog]; } function pm_attempt(r: connection, proc: string, status: rpc_status, addl: string, do_notice: bool) { if ( [proc, F] in RPC_do_not_complain ) do_notice = F; if ( ! is_tcp_port(r$id$orig_p) ) { # It's UDP, so no connection_attempt event - check for # scanning here, instead. Scan::check_scan(r, F, F); Hot::check_hot(r, Hot::CONN_ATTEMPTED); } add r$service[proc]; append_addl(r, fmt("(%s)", RPC_status[status])); # Current policy is ignore any failed attempts. pm_activity(r, F, proc); pm_log(r, proc, addl, F); } event pm_attempt_null(r: connection, status: rpc_status) { pm_attempt(r, "pm_null", status, "", T); } event pm_attempt_set(r: connection, status: rpc_status, m: pm_mapping) { pm_attempt(r, "pm_set", status, fmt("%s %d", rpc_prog(m$program), m$p), T); } event pm_attempt_unset(r: connection, status: rpc_status, m: pm_mapping) { pm_attempt(r, "pm_unset", status, fmt("%s %d", rpc_prog(m$program), m$p), T); } event pm_attempt_getport(r: connection, status: rpc_status, pr: pm_port_request) { local prog = rpc_prog(pr$program); local do_notice = pm_check_getport(r, prog); pm_attempt(r, "pm_getport", status, prog, do_notice); } event pm_attempt_dump(r: connection, status: rpc_status) { local do_notice = [r$id$orig_h, r$id$resp_h] !in RPC_dump_okay; pm_attempt(r, "pm_dump", status, "", do_notice); } event pm_attempt_callit(r: connection, status: rpc_status, call: pm_callit_request) { local orig_h = r$id$orig_h; local prog = rpc_prog(call$program); local do_notice = [orig_h, prog] !in suppress_pm_notice; pm_attempt(r, "pm_callit", status, fmt("%s/%d (%d bytes)", prog, call$proc, call$arg_size), do_notice); if ( prog == "walld" ) add suppress_pm_notice[orig_h, prog]; } event pm_bad_port(r: connection, bad_p: count) { event conn_weird_addl("bad_pm_port", r, fmt("port %d", bad_p)); }