Identifying Cobalt Strike team servers in the wild by using ZoomEye
by Heige of KnownSec 404 Team 02/27/2019
Yesterday, Fox-it’s blog published an article entitled “Identifying Cobalt Strike team servers in the wild”( https://blog.fox-it.com/2019/02/26/identifying-cobalt-strike-team-servers-in-the-wild/ ), which showed how they found the Cobalt Strike server in the wild. This is a very exciting job. So I want to do this through ZoomEye (https://www.zoomeye.org/ ).
In the fox-it article we know that “The webserver of the team server in Cobalt Strike is based on NanoHTTPD (https://github.com/NanoHttpd/nanohttpd-java-1.1), an opensource webserver written in Java. However this webserver unintendedly returns a surplus whitespace (https://github.com/NanoHttpd/nanohttpd-java-1.1/blob/nanohttpd-for-java1.1/NanoHTTPD.java#L778) in all its HTTP responses.”
By testing our local Cobalt Strike webserver , Default access IP:Port, and it return 404 status :
➜ 404team curl http://x.x.x.x:8081 -v
* Rebuilt URL to: http://x.x.x.x:8081/
* Trying x.x.x.x…
* TCP_NODELAY set
* Connected to x.x.x.x (x.x.x.x) port 8081 (#0)
> GET / HTTP/1.1
> Host: x.x.x.x:8081
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Content-Type: text/plain
< Date: Wed, 27 Feb 2019 14:43:19 GMT
< Content-Length: 0
<
* Connection #0 to host x.x.x.x left intact
Xmap is Zoomeye’s core scanning framework, So when Xmap scans the Cobalt Strike webserver, it will also get the banner data of 404 state. In addition, we will finally determine our ZoomEye search dork by observing the order and byte size of these banner data,Of course, we can see this information from the NanoHTTPD code:
And finally got 3,643 results in ZoomEye by this dork:
Of course, I also found the “example of Cobalt Strike team servers” mentioned in the fox-it blog:
That in ZoomEye :
We noticed the difference in the total amount of this data : “In total Fox-IT has observed 7718 unique Cobalt Strike team server or NanoHTTPD hosts between the period of 2015–01 and 2019–02, when based on the current data (as of 26 Feb 2019) from Rapid7 Labs HTTP and HTTPS Sonar datasets.” We think ZoomEye uses 404’s banner data that more matching the characteristics of the Cobalt Strike webserver. Because there are others besides Cobalt Strike use NanoHTTPD.
Then we compared the data released by ZoomEye and fox-it (https://github.com/fox-it/cobaltstrike-extraneous-space ), 83% of the ZoomEye data exists in the fox-it data, and this shows that the zoomeye data is valid.
Although we think “extraneous space” is NanoHTTPD’s “bug”, not just the Cobalt Strike. Although Cobalt Strike declares their fix this “extraneous space” problem ,I think maybe this doesn’t matter at all, for the ZoomEye dork we use.
Note: The following work only shows the reliability of our data.
Pocsuite (https://github.com/knownsec/pocsuite) is an open-sourced remote vulnerability testing and proof-of-concept development framework developed by the Knownsec 404 Team. It has a very nice function to implement POC verification using ZoomEye’s API function.
Note: Currently using Pocsuite version 2, Pocsuite version 3 will be released soon.
the POC code:
#coding:utf-8
from pocsuite.poc import POCBase,Output
from pocsuite.utils import register
import random
import string
import socket
import ssl
def randomstr():
return random.choice(string.ascii_letters)*5
class TestPOC(POCBase):
name = ‘Cobalt Strike < 3.13 Space Check’
version = ‘1’
vulID = ‘1’
author = [‘dawu’]
vulType = ‘’
references = ‘https://blog.fox-it.com/2019/02/26/identifying-cobalt-strike-team-servers-in-the-wild/'
desc = ‘’’
‘’’
vulDate = ‘2019–02–26’
createDate = ‘2019–02–27’
updateDate = ‘2019–02–27’
appName = ‘’
appVersion = ‘’
appPowerLink = ‘’
samples = [‘’]
def _attack(self):
‘’’attack mode’’’
return self._verify()
def _verify(self):
‘’’verify mode’’’
result = {}
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
if self.url.startswith(“http://”):
ip = self.url.split(“http://”)[1].split(“:”)[0]
port = int(self.url.split(“http://”)[1].split(“:”)[1])
elif self.url.startswith(“https://”):
context = ssl._create_unverified_context()
s = context.wrap_socket(s)
ip = self.url.split(“https://”)[1].split(“:”)[0]
port = int(self.url.split(“https://”)[1].split(“:”)[1])
else:
return self.parse_output(result)
try:
s.connect((ip,port))
s.send(b”GET / HTTP/1.1\r\n\r\n”)
data = s.recv(1024)
s.close()
except Exception as e:
pass
#print(data)
if b’404 Not Found ‘ in data:
result[‘VerifyInfo’] = {}
result[‘VerifyInfo’][‘URL’] = self.url
return self.parse_output(result)
def parse_output(self,result):
output = Output(self)
if result:
output.success(result)
else:
output.fail(‘Internet nothing returned’)
return output
register(TestPOC)
And the results :
Finally, We found that its 404 state banner data, the order of the http header has been changed:
➜ 404team curl http://x.x.x.x:8001 -v
* Rebuilt URL to: http://x.x.x.x:8001/
* Trying x.x.x.x…
* TCP_NODELAY set
* Connected to x.x.x.x (x.x.x.x) port 8001 (#0)
> GET / HTTP/1.1
> Host: x.x.x.x:8001
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Date: Wed, 27 Feb 2019 14:30:45 GMT
< Content-Type: text/plain
< Content-Length: 0
<
* Connection #0 to host x.x.x.x left intact
So we may need to update our ZoomEye Dork , It may be relatively troublesome because of the uncertainty of Date.I want to throw this question to the guys who are interested in this matter, good luck!
At the end, We are very grateful to fox-it for working and sharing. thanks to all of the knownsec 404 team, especially to Zhutq and Lul of ZoomEye team.