Overview
Command and Control servers, AKA C2 servers, are servers operated by threat actors and are used for maintaining communications with compromised systems within a target network. With the recent rise in double extortion ransomware campaigns, attackers are also sending exfiltrated data to C2 servers.
Needless to say, one of our most important tasks of a threat hunter is to identify potential C2 servers which are communicated to from within a corporate network.
In this post, we will share a few methodologies the Cortex XDR Managed Threat Hunting team uses to hunt for suspicious communication indicative of C2 communication.
Classic Hunting
One might argue that the traditional method of hunting for known C2 servers from various IOC feeds is more than enough to cover this area. However, history teaches us that as threat actors become more and more advanced, C2 servers will not be shared among different victims.
This means that a dedicated C2 server will be created for each victim the threat actor is targeting, rendering hunting for known IOCs almost useless when attempting to cover sophisticated attacks.
While this method is always a good practice and is relatively easy to hunt for (if someone already did the work for us, why wouldn’t we leverage on top of it?), we’ll focus on a more sophisticated approach to this.
Advanced Hunting - Laying Foundations
Our assumptions rely on the basic characteristics of a C2 server:
- The compromised host or hosts frequently communicate with the server.
- Few hosts in our network communicate with it.
In our queries, we will limit the results to show only communication originating from Windows Server operating systems and Linux systems. This allows us to have a manageable list of results to go through while filtering out false positives.
Our hunting focuses on the endpoint event data collected by the Cortex XDR agent.
If you have an extensive amount of results, you can follow these recommendations to try and narrow them down.
- Start on a smaller time frame, filter out false positives and then upscale to a larger time frame.
- Remove top-level domains that are attributed to your country of origin (e.g. “.co.uk”, “.fr”, “.br” etc.).
- Remove processes of third-party applications that you approve and consider to be safe, for example, server cloud backup software.
- Remove internally developed processes that you trust.
Query #1 - Weed out false positives and and generate investigation leads
We have created the following XQL query to initially start the hunt with.
The output of the query is a count of triplet combinations: Server_Name, Remote_Hostname, Process_Name sorted in ascending order.
This means that you will have a list of all least frequently contacted external domains, with their matching server name and communicating process.
Query note: On the following query, we have dropped all browser related processes; this is due to the sheer amount of false positives that is being added to the results.
We are aware of browser injected processes' false negatives, and when performing a smaller-scale hunt we will definitely drop the browser filtering processes.
To remove the browser filtering, delete the following row
“| filter actor_process_image_name not in ("chrome.exe", "iexplore.exe","MicrosoftEdgeCP.exe","firefox.exe", “opera.exe”,"MicrosoftEdgeSH.exe","msedge.exe","GoogleUpdate.exe")// Filtering out browser processes.”
config case_sensitive = false | //Setting query to be case-insensitive.
dataset = xdr_data // Using the xdr dataset. | alter host_broken = split(action_external_hostname, ".") //Extracting TLD from the remote host name. | alter TLD = arrayindex(host_broken , -1) | filter TLD not in ("com","local", "net", "org","gov")// Filtering large known TLDs. | filter (agent_os_sub_type contains "Server" or agent_os_type = AGENT_OS_LINUX) and action_external_hostname !="" //Filtering operation systems to be Windows server and Linux. |filter actor_process_image_name !="dns.exe" and action_external_hostname not contains "pki.goog"and action_external_hostname not contains "adobe.io" and action_external_hostname not contains "aka.ms" and action_external_hostname not contains "certum.pl" //Filtering out noisy processes and noisy external legitimate remote hosts. | filter actor_process_image_name not in ("chrome.exe", "iexplore.exe","MicrosoftEdgeCP.exe","firefox.exe", “opera.exe”,"MicrosoftEdgeSH.exe","msedge.exe","GoogleUpdate.exe")// Filtering out browser processes. | filter action_remote_ip != "10.*" and action_remote_ip != "192.168.*"//Filtering out internal IP addresses. | alter rfc1918_172 = incidr(action_remote_ip, "172.16.0.0/12") | filter rfc1918_172 = false | fields agent_hostname as Server_Name, action_remote_ip as remote_ip, action_external_hostname as remote_hostname, actor_process_image_name as process_name, actor_process_image_sha256 as sha_256 | comp count(_time) as Counter by remote_hostname, Server_Name, process_name // Counting how many times a remote_hostname/server/process triplets were used | sort asc Counter // Sorting by occurrences in a ascending order |
A screenshot of an example output is shown below:
Image 1: Example output of Query #1
Query #2 - Focus on leads determining rarity across network
This query is a followup query, which can be run after gathering the results of Query #1.
Our desired input to this query is some suspected domains, which we want to gather more information about.
Make sure to change the bolded remote hostnames in following line before searching:
| filter (action_external_hostname contains "docker.io" or action_external_hostname contains "segment.io")
The output of this query is a count of all internal workstations who are communicating with the suspected domains.
config case_sensitive = false | //Setting query to be case-insensitive.
dataset = xdr_data // Using the xdr dataset. | filter (action_external_hostname contains "docker.io" or action_external_hostname contains "segment.io") | fields agent_hostname as Server_Name, action_remote_ip as remote_ip, action_external_hostname as remote_hostname, actor_process_image_name as process_name, actor_process_image_sha256 as sha_256 | comp count_distinct(Server_Name) as Num_Of_Hosts by remote_hostname | sort asc Num_Of_Hosts // Sorting by occurrences in a ascending order |
A screenshot of an example output is shown below:
Image 2: Example output of Query #2
Query #3 - Digging more data on the suspected domain
In this section, we will list a few queries that can assist with enriching the suspected domain with some more contextual information.
Query 3.a will attempt to reveal which User-Agent is used when communicating with the suspected domain.
This potentially will enable you to hunt for additional C2 domains in your network, and potentially more infected hosts.
Similar to Query #2 - make sure to change the bolded remote hostnames in the following line before searching:
| filter (action_external_hostname contains "phicdn")
The output of this query is a count of how many unique User-Agents were spotted in a combination of Internal hostname, External Domain, Acting Process.
config case_sensitive = false | //Setting query to be case-insensitive.
dataset = xdr_data // Using the xdr dataset. | filter (action_external_hostname contains "phicdn") | alter User_Agent=json_extract(action_network_http, "$.headers.User-Agent") // Using json extract to get the user-agent out of the header | fields agent_hostname as Server_Name, action_remote_ip as remote_ip, action_external_hostname as remote_hostname, actor_process_image_name as process_name, actor_process_image_sha256 as sha_256, User_Agent | comp count_distinct(User_Agent) as Counter by remote_hostname, Server_Name, process_name, User_Agent | filter Counter >0 | sort asc Counter // Sorting by occurrences in a ascending order |
A screenshot of an example output is shown below:
Image 3: Example output of Query #3.a
Query 3.b will attempt to shed some light on which processes and command lines are primarily used to connect to the suspected domain.
Similar to Query #2 - make sure to change the bolded remote hostnames in the following line before searching: | filter (action_external_hostname contains "phicdn")
The output of this query is a count of how many unique Process Command Lines were spotted in a combination of Internal hostname, External Domain, Acting Process.
Query note: Note that the User-Agent field will be displayed only for non-encrypted traffic.
config case_sensitive = false | //Setting query to be case-insensitive.
dataset = xdr_data // Using the xdr dataset. | filter (action_external_hostname contains "phicdn") | fields agent_hostname as Server_Name, action_remote_ip as remote_ip, action_external_hostname as remote_hostname, actor_process_image_name as process_name, actor_process_image_sha256 as sha_256, actor_process_command_line | comp count(actor_process_command_line) as Counter by remote_hostname, Server_Name, process_name, actor_process_command_line | filter Counter >0 | sort asc Counter // Sorting by occurrences in a ascending order |
A screenshot of an example output is shown below:
Image 4: Example output of Query #3.b
Final Thoughts
In conclusion, the threat hunter’s job is far from routine and repetitive. A competent threat hunter will always strive to look for anomalies and events that aren’t necessarily caught by security products.
C2 server communication is something that will always keep defenders and threat hunters globally occupied in order to catch the next malware in their network.
Hoping these XQL queries will enable you to find the next hidden malware in your network.
Happy hunting!
Closing Notes - Upcoming Series
If you enjoyed what you just read, the Unit 42 team will be publishing a series around frequently used XQL queries, which are utilized by the Unit 42 - Managed Threat Hunting team for hunting unknown threats in the network.
Hunting efforts will revolve around both “Threat Actors” and “The Insider” queries in an attempt to assist security teams who use Cortex XDR expand their usability and proactive hunting use cases.