diff --git a/dissect/cobaltstrike/beacon.py b/dissect/cobaltstrike/beacon.py index 94a1973..918f6cc 100644 --- a/dissect/cobaltstrike/beacon.py +++ b/dissect/cobaltstrike/beacon.py @@ -175,6 +175,7 @@ SETTING_DATA_STORE_SIZE = 76, // CobaltStrike version >= 4.10 (Jul 16, 2024) + SETTING_HTTP_DATA_REQUIRED = 77, SETTING_BEACON_GATE = 78, }; diff --git a/dissect/cobaltstrike/c2profile.lark b/dissect/cobaltstrike/c2profile.lark index 1d80a2c..e3382c6 100644 --- a/dissect/cobaltstrike/c2profile.lark +++ b/dissect/cobaltstrike/c2profile.lark @@ -11,6 +11,7 @@ start: value* | "process-inject" "{" process_inject_options* "}" -> process_inject | "post-ex" "{" postex_options* "}" -> post_ex | "dns-beacon" "{" dns_beacon_options* "}" -> dns_beacon + | "http-beacon" "{" http_beacon_options* "}" -> http_beacon OPTION: "sample_name" | "data_jitter" @@ -214,6 +215,10 @@ dns_beacon_options: "set" "dns_idle" string ";" -> dns_idle | "set" "ns_response" string ";" -> ns_response | "#" "dns_resolver" string ";" -> comment_dns_resolver +http_beacon_options: "set" "library" string ";" -> library // introduced in Cobalt Strike 4.9 + | "set" "data_required" string ";" -> data_required // introduced in Cobalt Strike 4.10 + | "set" "data_required_length" string ";" -> data_required_length // introduced in Cobalt Strike 4.10 + header: string string: STRING variant: string diff --git a/dissect/cobaltstrike/c2profile.py b/dissect/cobaltstrike/c2profile.py index 3745946..1c985cf 100644 --- a/dissect/cobaltstrike/c2profile.py +++ b/dissect/cobaltstrike/c2profile.py @@ -309,6 +309,12 @@ class DnsBeaconBlock(ConfigBlock): __name__ = "dns_beacon" +class HttpBeaconBlock(ConfigBlock): + """`.http-beacon` block""" + + __name__ = "http_beacon" + + class ExecuteOptionsBlock(ConfigBlock): """`.process-inject.execute` block""" @@ -446,6 +452,7 @@ def from_beacon_config(cls, config: BeaconConfig) -> "C2Profile": http_post_client = HttpOptionsBlock() proc_inj = ProcessInjectBlock() dns_beacon = DnsBeaconBlock() + http_beacon = HttpBeaconBlock() # http_get_server = HttpOptionsBlock() for setting, value in config.settings_by_index.items(): @@ -668,6 +675,8 @@ def from_beacon_config(cls, config: BeaconConfig) -> "C2Profile": proc_inj.set_option("bof_allocator", value) elif setting == BeaconSetting.SETTING_DATA_STORE_SIZE: stage.set_option("data_store_size", value) + elif setting == BeaconSetting.SETTING_HTTP_DATA_REQUIRED and value: + http_beacon.set_option("data_required", "true") elif setting == BeaconSetting.SETTING_BEACON_GATE and value: block = BeaconGateBlock.from_beacon_gate_option_strings(value) stage.set_config_block("beacon_gate", block) @@ -681,6 +690,7 @@ def from_beacon_config(cls, config: BeaconConfig) -> "C2Profile": profile.set_non_empty_config_block("stage", stage) profile.set_non_empty_config_block("process_inject", proc_inj) profile.set_non_empty_config_block("dns_beacon", dns_beacon) + profile.set_non_empty_config_block("http_beacon", http_beacon) return profile def __str__(self) -> str: diff --git a/tests/test_c2profile.py b/tests/test_c2profile.py index 89243e6..46d2df5 100644 --- a/tests/test_c2profile.py +++ b/tests/test_c2profile.py @@ -395,3 +395,25 @@ def test_c2profile_beacon_gate(): bconfig = beacon.BeaconConfig(data) profile = c2profile.C2Profile.from_beacon_config(bconfig) assert "stage.beacon_gate" not in profile.properties + + +@pytest.mark.parametrize( + ("http_data_required",), + [ + (True,), + (False,), + ], +) +def test_c2profile_http_data_required(http_data_required: bool): + data = beacon.Setting( + index=beacon.BeaconSetting.SETTING_HTTP_DATA_REQUIRED, + type=beacon.SettingsType.TYPE_SHORT, + length=0x2, + value=beacon.cs_struct.uint16(http_data_required).dumps(), + ).dumps() + bconfig = beacon.BeaconConfig(data) + profile = c2profile.C2Profile.from_beacon_config(bconfig) + if not http_data_required: + assert "http-beacon.data_required" not in profile.properties + else: + assert profile.properties["http-beacon.data_required"] == ["true"]