Cobalt Strike is a well known framework used to perform adversary simulation exercises by offensive security professionals. Its flexibility and broad feature set have made it the de facto framework for red team operations.
Cobalt Strike's implant, known as "beacon", has the ability to communicate back to a Command & Control (C2) server using different protocols:
Before jumping in the actual detection logic, it is important to understand how the standard DNS server within Cobalt Strike is deployed for an operation. The concept of a "redirector" is something that was introduced to further harden the attack infrastructure and decouple what is exposed by the attacked component and the actual C2 servers. This grants the operator greater flexibility in case any of their internet-facing endpoints get "burned" during the attack; in fact, spinning up a new redirector can take hours if not minutes whilst rebuilding a new C2 server might have a greater impact on the operations.
Typically, the standard Cobalt Strike DNS redirector is created using either socat or iptables. The
As an example, the following commands can be used to create a simple redirector for DNS:
# socat will listen on TCP 5353 and redirect to cobalt strike's DNS server
socat tcp4-listen:5353,reuseaddr,fork UDP:127.0.0.1:53
# port 5353 will be exposed via an SSH tunnel on the external redirector
ssh ubuntu@redir.c2 -R 5353:127.0.0.1:5353
# on the redirector, socat will listen on 53 and forward the data to the SSH tunnel, that eventually will reach the C2 server
socat udp4-listen:53,reuseaddr,fork tcp:localhost:53535
The creation of a DNS listener within Cobalt Strike will not be covered as it is outside the scope of this research. For more details it is recommended to check out Steve Borosh's
The resulting communication flow will look like the one schematised below:
The setup can be validated by querying the hostname that was initially configured for DNS C2. If the setup is working properly, the DNS response should be the one configured in the dns_idle malleable profile option, and by default it's equal to "0.0.0.0":
nslookup malware.c2
Non-authoritative answer:
Name: malware.c2
Address: 0.0.0.0
The "0.0.0.0" reply, as previously said, is the Cobalt Strike's default and as every default value, should be changed.
Cobalt Strike's DNS listeners will reply using the value defined in the dns_idle field regardless of the query received, as long as it is not part of a C2 communication
In fact, the dns_idle field is used by the beacon as a heartbeat to check in for new tasks. The "problem" is that the default DNS server will reply using that value to all the other queries for other domains as well.
The hypothesis is that if a DNS server replies to all the queries with always the same IP address, it might be an indicator of the presence of Cobalt Strike. Of course this approach is not free of false-positives, and part of the research was to quantify the fidelity of this mechanism,
It was possible to validate our hypothesis using a live Cobalt Strike server:
dig @70.35.206.199 +short amazon.com
0.0.0.0
dig @70.35.206.199 +short google.com
0.0.0.0
dig @70.35.206.199 +short nonexistent.domain
0.0.0.0
dig @70.35.206.199 +short -t TXT google.com
0.0.0.0
As it is possible to see, the server replied to all the queries with "0.0.0.0", the default dns_idle value. This approach would have worked even if the default value was changed, since all the queries would still return the same value. The interesting aspect was that even for queries different than "A", Cobalt Strike still returned an "A" record. This broke the parsing logic of most of the common libraries for name resolution. The snippet below shows an example of that:
dig TXT test @35.178.76.239
; <<>> DiG 9.10.6 <<>> TXT test @35.178.76.239
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45988
;; flags: qr rd ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available
;; QUESTION SECTION:
;test. IN TXT
;; ANSWER SECTION:
test. 1 IN A 8.8.8.8
;; Query time: 97 msec
;; SERVER: 35.178.76.239#53(35.178.76.239)
;; WHEN: Tue Mar 23 10:11:00 CET 2021
;; MSG SIZE rcvd: 42
In order to avoid higher level parsing logic, the Scapy library was used to forge raw DNS packets and build an automated scanner.
The snippet below shows the core function where the packers are sent and the responses are compared:
def checkDNS(name):
ip = name
try:
ans = sr1(IP(dst=ip)/UDP(sport=RandShort(), dport=53)/DNS(rd=1,qd=DNSQR(qname="google.com",qtype="TXT")), verbose=False, timeout=0.5)
ansTXT = ans[DNSRR][0].rdata
ans = sr1(IP(dst=ip)/UDP(sport=RandShort(), dport=53)/DNS(rd=1,qd=DNSQR(qname="amazon.com",qtype="A")), verbose=False, timeout=0.5)
ansA1 = ans[DNSRR][0].rdata
ans = sr1(IP(dst=ip)/UDP(sport=RandShort(), dport=53)/DNS(rd=1,qd=DNSQR(qname="google.com",qtype="A")), verbose=False, timeout=0.5)
ansA2 = ans[DNSRR][0].rdata
if ansTXT == ansA1 and ansA1 == ansA2 and ansA2 != '' and valid_ip(ansA1):
print("[+] Cs Detected: " + ip + " replied with: " + ansTXT)
except Exception as e:
pass
The code is quite self-explanatory and simply translates the initial hypothesis into actual Python code. The rest of the script's functionalities only added multi threading and better output formats, but the main logic is the one shown above.
The scanner was tested against a small subset of data, specifically using all the HTTP servers exposed on the internet that had a JARM signature equals to the default Cobalt Strike C2 server. The data was gathered using the
All the resulting data was downloaded and subsequently parsed via the Shodan CLI in order to extract the IP addresses:
# add shodan query
shodan parse --fields ip_str e5a2e2e7-e029-4b10-a5f8-63687aa7a09c.json.gz > cobalt.txt
The results against the initial dataset were positive, with multiple matches:
python scan.py ~/Downloads/cs-dns.txt
[+] Cs Detected: 24.252.133.75 replied with: 10.0.0.1
[+] Cs Detected: 193.202.92.36 replied with: 193.202.92.36
[+] Cs Detected: 194.204.33.130 replied with: 127.53.53.53
[+] Cs Detected: 194.62.33.31 replied with: 192.168.33.31
[+] Cs Detected: 5.8.16.29 replied with: 72.5.65.111
[+] Cs Detected: 37.191.180.47 replied with: 37.191.180.47
[+] Cs Detected: 35.178.76.239 replied with: 8.8.8.8
[+] Cs Detected: 85.27.174.244 replied with: 85.27.174.244
[+] Cs Detected: 70.35.206.199 replied with: 0.0.0.0
[+] Cs Detected: 209.9.227.212 replied with: 209.9.227.212
[+] Cs Detected: 175.176.185.229 replied with: 103.60.101.170
[+] Cs Detected: 179.191.84.68 replied with: 10.0.0.1
Scanning what is already known to be bad is funny, but only to a certain extent. The next step of the research was to expand the scope of our analysis to the whole internet.
In order to obtain the needed results, it was necessary to perform an internet wide scan of all the hosts that exposed the port 53/UDP. Instead of manually doing the scan, it was decided to use Project Sonar's dataset, which already collected the information needed. This allowed us to reduce the scope of the scan from billions of IPs to something around 7 million records. This was found to be particularly useful considering that the detection used a UDP-based protocol, which in general is slower and not as reliable as TCP.
Running our scanner against the dataset produced approximately 1400 results, a number that we believed to be too high and needed further validation and false positive checks. We started the result sanitisation by filtering out the hosts that presented additional Cobalt Strike specific behaviour, such as:
The results of the process were the following:
The root cause that allowed us to perform this research is that the DNS redirector is "dumb", meaning that it forwards DNS requests to Cobalt Strike without any logic. If, for example, we examine the evolution of HTTP based redirectors we see that using socat or iptables was quickly abandoned in favour of better alternatives such as Apache with mod_rewrite or Nginx.
The problem is that we haven't seen the same approach applied to DNS redirectors. A possible solution would be to create a "smart" redirector capable of proxying DNS requests to specific DNS servers based on the domain name that is being requested:
We found that using open source DNS servers, such as CoreDNS, solved the issue. In fact, using a "Corefile" such as the one shown below, it would be possible to avoid this trivial detection:
. {
forward . 9.9.9.9
}
malware.c2 {
forward . malware.c2:5353
}
CoreDNS can then be deployed on an internet exposed VPS and act as a "smart" redirector. This configuration will proxy to Cobalt Strike only the requests made for the "malware.c2" domain, everything else will be resolved using the "9.9.9.9" public resolver.
The research showed one of the many approaches that can be used to track Cobalt Strike servers exposed on the internet. Whilst not perfect, this can certainly be used to enrich threat intelligence data to achieve better detections.
For red teamers and penetration testers that use either Cobalt Strike or any other C2 framework that supports DNS, we provided an approach that can be used to build better and smarter DNS redirectors using open source tools.