module CVE_2021_44228; redef enum Notice::Type += { LOG4J_ATTEMPT_HEADER, LOG4J_SUCCESS }; redef enum HTTP::Tags += { LOG4J_RCE }; redef enum Log::ID += { LOG }; type Info: record { ts: time &log; uid: string &log; http_uri: string &log; uri: string &log; stem: string &log; target_host: string &log; target_port: string &log; method: string &log; is_orig: bool &log; name: string &log; value: string &log; matched_name: bool &log; matched_value: bool &log; }; type PayloadParts: record { uri: string; stem: string; host: string; port_: string; }; # Very general, FPs expected but we're casting a wide net intentionally. global exploit_pattern: pattern = /\$\{/; export { option log = T; } # If split doesn't return the expected number of indices, return the default "-" function safe_split1_w_default(s: string, p: pattern, idx: count, missing: string &default="-"): string { local tmp = split_string1(s, p); if ( |tmp| > idx ) return tmp[idx]; else return missing; } # Assumes `name` or `value` string passed as `s` has the structure: # ${jdni:ldap://payload_host:payload_port/path} for the payload. Many examples # of more complicated obfuscation exist. If the structure is different, fill # missing fields with "-" so other structures in the wild can be explored in the # logs. For example, Binary Edge are using the following type of obfuscation: # ...value='${jndi:${lower:l}${lower:d}a${lower:p}://world443.log4j.bin${upper:a}ryedge.io:80/callback}' function parse_payload(s: string): PayloadParts { local tmp = split_string(s, /\/\//); local last: string = "-"; if ( |tmp| > 0 ) last = tmp[(|tmp| - 1)]; local payload_uri = safe_split1_w_default(last, /\}/, 0); local payload_stem = safe_split1_w_default(payload_uri, /\//, 0); local payload_host = safe_split1_w_default(payload_stem, /\:/, 0); local payload_port = safe_split1_w_default(payload_stem, /\:/, 1); return PayloadParts($uri=payload_uri, $stem=payload_stem, $host=payload_host, $port_=payload_port); } event http_header(c: connection, is_orig: bool, name: string, value: string) { # Focus is mainly on client headers, but not filtering right now to explore interesting cases in the wild # if (!is_orig) # return; # Focus is mainly on value of header, but adding 'name' to explore what is being used in the wild local matched_name = exploit_pattern in name; local matched_value = exploit_pattern in value; # Ignore matches that contain binary goop. This was a large contributor to # false positives. if ( matched_name && !is_ascii(name) ) return; if ( matched_value && !is_ascii(value) ) return; add c$http$tags[LOG4J_RCE]; local payload: PayloadParts; local info: Info; # TODO: add to a clusterized set for watching of subsequent traffic (LOG4J_SUCCESS notice). if ( matched_name ) { payload = parse_payload(name); info = Info($ts=network_time(), $uid=c$uid, $http_uri=c$http$uri, $uri=payload$uri, $stem=payload$stem, $target_host=payload$host, $target_port=payload$port_, $method=c$http$method, $is_orig=is_orig, $name=name, $value=value, $matched_name=matched_name, $matched_value=matched_value); NOTICE([$note=LOG4J_ATTEMPT_HEADER, $conn=c, $identifier=cat(c$id$orig_h,c$id$resp_h,c$id$resp_p,cat(name,value)), # $suppress_for=3600sec, $msg=fmt("Possible Log4j exploit CVE-2021-44228 exploit in header. Refer to sub field for sample of payload, original_URI and list of server headers"), $sub=fmt("uri='%s', payload_uri=%s, payload_stem=%s, payload_host=%s, payload_port=%s, method=%s, is_orig=%s, header name='%s', header value='%s' ", c$http$uri, payload$uri, payload$stem, payload$host, payload$port_, c$http$method, is_orig, name, value)]); if ( log ) Log::write(LOG, info); } if ( matched_value ) { payload = parse_payload(value); info = Info($ts=network_time(), $uid=c$uid, $http_uri=c$http$uri, $uri=payload$uri, $stem=payload$stem, $target_host=payload$host, $target_port=payload$port_, $method=c$http$method, $is_orig=is_orig, $name=name, $value=value, $matched_name=matched_name, $matched_value=matched_value); NOTICE([$note=LOG4J_ATTEMPT_HEADER, $conn=c, $identifier=cat(c$id$orig_h,c$id$resp_h,c$id$resp_p,cat(name,value)), # $suppress_for=3600sec, $msg=fmt("Possible Log4j exploit CVE-2021-44228 exploit in header. Refer to sub field for sample of payload, original_URI and list of server headers"), $sub=fmt("uri='%s', payload_uri=%s, payload_stem=%s, payload_host=%s, payload_port=%s, method=%s, is_orig=%s, header name='%s', header value='%s' ", c$http$uri, payload$uri, payload$stem, payload$host, payload$port_, c$http$method, is_orig, name, value)]); if ( log ) Log::write(LOG, info); } } event zeek_init() &priority=5 { if ( log ) Log::create_stream(CVE_2021_44228::LOG, [$columns=Info, $path="log4j"]); }