mirror of
https://github.com/aljazceru/cowrie.git
synced 2026-01-31 12:04:24 +01:00
more VT functionality
This commit is contained in:
@@ -34,22 +34,14 @@ Work in Progress - not functional yet
|
||||
from zope.interface import implementer
|
||||
|
||||
import json
|
||||
import re
|
||||
import urllib
|
||||
import warnings
|
||||
import urllib2
|
||||
|
||||
from twisted.python import log
|
||||
from twisted.web.iweb import IBodyProducer
|
||||
from twisted.internet import abstract, defer, reactor, protocol
|
||||
from twisted.web import client, http_headers, iweb
|
||||
from twisted.internet import defer, reactor
|
||||
from twisted.web import client, http_headers
|
||||
from twisted.internet.ssl import ClientContextFactory
|
||||
|
||||
from twisted.web.error import SchemeNotSupported
|
||||
from twisted.web._newclient import Request, Response, HTTP11ClientProtocol
|
||||
from twisted.web._newclient import ResponseDone, ResponseFailed
|
||||
from twisted.web._newclient import PotentialDataLoss
|
||||
|
||||
import cowrie.core.output
|
||||
|
||||
|
||||
@@ -81,40 +73,72 @@ class Output(cowrie.core.output.Output):
|
||||
"""
|
||||
if entry["eventid"] == 'COW0007':
|
||||
log.msg("Sending url to VT")
|
||||
if self.posturl(entry["url"]) == 0:
|
||||
log.msg("Not seen before by VT")
|
||||
self.postcomment(entry["url"])
|
||||
self.posturl(entry["url"])
|
||||
|
||||
elif entry["eventid"] == 'COW0017':
|
||||
log.msg("Sending SFTP file to VT")
|
||||
if self.postfile(entry["outfile"], entry["filename"]) == 0:
|
||||
log.msg("Not seen before by VT")
|
||||
self.postcomment(entry["url"])
|
||||
self.postfile(entry["outfile"], entry["filename"])
|
||||
|
||||
|
||||
# def postfile(self, artifact, fileName):
|
||||
# """
|
||||
# Send a file to VirusTotal
|
||||
# """
|
||||
# vtUrl = "https://www.virustotal.com/vtapi/v2/file/scan"
|
||||
# fields = [("apikey", self.apiKey)]
|
||||
# files = {'file': (fileName, open(artifact, 'rb'))}
|
||||
#
|
||||
# agent = agent.request('POST', vtUrl, None, None)
|
||||
#
|
||||
# def cbResponse(ignored):
|
||||
# print 'Response received'
|
||||
# d.addCallback(cbResponse)
|
||||
#
|
||||
# r = requests.post(vtUrl, files=files, data=fields)
|
||||
# # if r.status_code != 200 # error
|
||||
# j = r.json()
|
||||
# log.msg( "Sent file to VT: %s" % (j,) )
|
||||
# return j["response_code"]
|
||||
#
|
||||
# #contentType = "multipart/form-data; boundary={}".format(boundary)
|
||||
# #headers.setRawHeaders("Content-Type", [contentType])
|
||||
# #headers.setRawHeaders("Content-Length", [len(body)])
|
||||
def postfile(self, artifact, fileName):
|
||||
"""
|
||||
Send a file to VirusTotal
|
||||
"""
|
||||
vtUrl = "https://www.virustotal.com/vtapi/v2/file/scan"
|
||||
contextFactory = WebClientContextFactory()
|
||||
fields = {('apikey', self.apiKey)}
|
||||
files = {('file', fileName, open(artifact, 'rb'))}
|
||||
contentType, body = encode_multipart_formdata(fields, files)
|
||||
producer = StringProducer(body)
|
||||
headers = http_headers.Headers({
|
||||
'User-Agent': ['Cowrie SSH Honeypot'],
|
||||
'Accept': ['*/*'],
|
||||
'Content-Type': [contentType]
|
||||
})
|
||||
|
||||
agent = client.Agent(reactor, contextFactory)
|
||||
d = agent.request('POST', vtUrl, headers, producer)
|
||||
|
||||
def cbBody(body):
|
||||
return processResult(body)
|
||||
|
||||
|
||||
def cbPartial(failure):
|
||||
"""
|
||||
Google HTTP Server does not set Content-Length. Twisted marks it as partial
|
||||
"""
|
||||
return processResult(failure.value.response)
|
||||
|
||||
|
||||
def cbResponse(response):
|
||||
if response.code == 200:
|
||||
d = client.readBody(response)
|
||||
d.addCallback(cbBody)
|
||||
d.addErrback(cbPartial)
|
||||
return d
|
||||
else:
|
||||
log.msg("VT Request failed: %s %s" % (response.code, response.phrase,))
|
||||
return
|
||||
|
||||
|
||||
def cbError(failure):
|
||||
failure.printTraceback()
|
||||
|
||||
|
||||
def processResult(result):
|
||||
log.msg( "VT postfile result: %s" % result)
|
||||
j = json.loads(result)
|
||||
#log.msg( "VT postfile result: %s", repr(j) )
|
||||
if j["response_code"] == 0:
|
||||
log.msg( "response=0: posting comment")
|
||||
d = self.postcomment(j["resource"])
|
||||
return d
|
||||
|
||||
|
||||
d.addCallback(cbResponse)
|
||||
d.addErrback(cbError)
|
||||
return d
|
||||
|
||||
|
||||
def posturl(self, scanUrl):
|
||||
"""
|
||||
@@ -141,18 +165,21 @@ class Output(cowrie.core.output.Output):
|
||||
|
||||
def cbPartial(failure):
|
||||
"""
|
||||
Google HTTP Server does not set Content-Length. Twisted marks it as partial
|
||||
Google HTTP Server does not set Content-Length. Twisted marks it as partial
|
||||
"""
|
||||
#failure.printTraceback()
|
||||
return processResult(failure.value.response)
|
||||
|
||||
|
||||
def cbResponse(response):
|
||||
# print 'Response code:', response.code
|
||||
# FIXME: Check for 200
|
||||
d = readBody(response)
|
||||
d.addCallback(cbBody)
|
||||
d.addErrback(cbPartial)
|
||||
return d
|
||||
if response.code == 200:
|
||||
d = client.readBody(response)
|
||||
d.addCallback(cbBody)
|
||||
d.addErrback(cbPartial)
|
||||
return d
|
||||
else:
|
||||
log.msg("VT Request failed: %s %s" % (response.code, response.phrase,))
|
||||
return
|
||||
|
||||
|
||||
def cbError(failure):
|
||||
@@ -163,10 +190,10 @@ class Output(cowrie.core.output.Output):
|
||||
j = json.loads(result)
|
||||
log.msg( "VT posturl result: %s", repr(j) )
|
||||
if j["response_code"] == 0:
|
||||
log.msg( "response=0: posting comment")
|
||||
log.msg( "response=0: posting comment")
|
||||
d = self.postcomment(j["resource"])
|
||||
return d
|
||||
|
||||
|
||||
|
||||
d.addCallback(cbResponse)
|
||||
d.addErrback(cbError)
|
||||
@@ -194,18 +221,20 @@ class Output(cowrie.core.output.Output):
|
||||
|
||||
def cbPartial(failure):
|
||||
"""
|
||||
Google HTTP Server does not set Content-Length. Twisted marks it as partial
|
||||
Google HTTP Server does not set Content-Length. Twisted marks it as partial
|
||||
"""
|
||||
return processResult(failure.value.response)
|
||||
|
||||
|
||||
def cbResponse(response):
|
||||
# print 'Response code:', response.code
|
||||
# FIXME: Check for 200
|
||||
d = readBody(response)
|
||||
d.addCallback(cbBody)
|
||||
d.addErrback(cbPartial)
|
||||
return d
|
||||
if response.code == 200:
|
||||
d = client.readBody(response)
|
||||
d.addCallback(cbBody)
|
||||
d.addErrback(cbPartial)
|
||||
return d
|
||||
else:
|
||||
log.msg("VT Request failed: %s %s" % (response.code, response.phrase,))
|
||||
return
|
||||
|
||||
|
||||
def cbError(failure):
|
||||
@@ -216,7 +245,7 @@ class Output(cowrie.core.output.Output):
|
||||
j = json.loads(result)
|
||||
log.msg( "VT postcomment result: %s", repr(j) )
|
||||
return j["response_code"]
|
||||
|
||||
|
||||
d.addCallback(cbResponse)
|
||||
d.addErrback(cbError)
|
||||
return d
|
||||
@@ -228,103 +257,6 @@ class WebClientContextFactory(ClientContextFactory):
|
||||
|
||||
|
||||
|
||||
class _ReadBodyProtocol(protocol.Protocol):
|
||||
"""
|
||||
Protocol that collects data sent to it.
|
||||
|
||||
This is a helper for L{IResponse.deliverBody}, which collects the body and
|
||||
fires a deferred with it.
|
||||
|
||||
@ivar deferred: See L{__init__}.
|
||||
@ivar status: See L{__init__}.
|
||||
@ivar message: See L{__init__}.
|
||||
|
||||
@ivar dataBuffer: list of byte-strings received
|
||||
@type dataBuffer: L{list} of L{bytes}
|
||||
"""
|
||||
|
||||
def __init__(self, status, message, deferred):
|
||||
"""
|
||||
@param status: Status of L{IResponse}
|
||||
@ivar status: L{int}
|
||||
|
||||
@param message: Message of L{IResponse}
|
||||
@type message: L{bytes}
|
||||
|
||||
@param deferred: deferred to fire when response is complete
|
||||
@type deferred: L{Deferred} firing with L{bytes}
|
||||
"""
|
||||
self.deferred = deferred
|
||||
self.status = status
|
||||
self.message = message
|
||||
self.dataBuffer = []
|
||||
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
Accumulate some more bytes from the response.
|
||||
"""
|
||||
self.dataBuffer.append(data)
|
||||
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
Deliver the accumulated response bytes to the waiting L{Deferred}, if
|
||||
the response body has been completely received without error.
|
||||
"""
|
||||
if reason.check(ResponseDone):
|
||||
self.deferred.callback(b''.join(self.dataBuffer))
|
||||
elif reason.check(PotentialDataLoss):
|
||||
self.deferred.errback(
|
||||
client.PartialDownloadError(self.status, self.message,
|
||||
b''.join(self.dataBuffer)))
|
||||
else:
|
||||
self.deferred.errback(reason)
|
||||
|
||||
|
||||
|
||||
def readBody(response):
|
||||
"""
|
||||
Get the body of an L{IResponse} and return it as a byte string.
|
||||
|
||||
This is a helper function for clients that don't want to incrementally
|
||||
receive the body of an HTTP response.
|
||||
|
||||
@param response: The HTTP response for which the body will be read.
|
||||
@type response: L{IResponse} provider
|
||||
|
||||
@return: A L{Deferred} which will fire with the body of the response.
|
||||
Cancelling it will close the connection to the server immediately.
|
||||
"""
|
||||
def cancel(deferred):
|
||||
"""
|
||||
Cancel a L{readBody} call, close the connection to the HTTP server
|
||||
immediately, if it is still open.
|
||||
|
||||
@param deferred: The cancelled L{defer.Deferred}.
|
||||
"""
|
||||
abort = getAbort()
|
||||
if abort is not None:
|
||||
abort()
|
||||
|
||||
d = defer.Deferred(cancel)
|
||||
protocol = _ReadBodyProtocol(response.code, response.phrase, d)
|
||||
def getAbort():
|
||||
return getattr(protocol.transport, 'abortConnection', None)
|
||||
|
||||
response.deliverBody(protocol)
|
||||
|
||||
if protocol.transport is not None and getAbort() is None:
|
||||
warnings.warn(
|
||||
'Using readBody with a transport that does not have an '
|
||||
'abortConnection method',
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2)
|
||||
|
||||
return d
|
||||
|
||||
|
||||
|
||||
@implementer(IBodyProducer)
|
||||
class StringProducer(object):
|
||||
|
||||
@@ -345,66 +277,31 @@ class StringProducer(object):
|
||||
def stopProducing(self):
|
||||
pass
|
||||
|
||||
"""
|
||||
def get_report(resource, filename, dl_url='unknown', honeypot=None, origin=None):
|
||||
|
||||
apikey = config().get('virustotal', 'apikey')
|
||||
url = "https://www.virustotal.com/vtapi/v2/file/report"
|
||||
parameters = {"resource": resource,
|
||||
"apikey": apikey }
|
||||
def encode_multipart_formdata(fields, files):
|
||||
"""
|
||||
fields is a sequence of (name, value) elements for regular form fields.
|
||||
files is a sequence of (name, filename, value) elements for data to be uploaded as files
|
||||
Return (content_type, body) ready for httplib.HTTPS instance
|
||||
"""
|
||||
BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
|
||||
CRLF = '\r\n'
|
||||
L = []
|
||||
for (key, value) in fields:
|
||||
L.append('--' + BOUNDARY)
|
||||
L.append('Content-Disposition: form-data; name="%s"' % key)
|
||||
L.append('')
|
||||
L.append(value)
|
||||
for (key, filename, value) in files:
|
||||
L.append('--' + BOUNDARY)
|
||||
L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
|
||||
L.append('Content-Type: application/octet-stream')
|
||||
L.append('')
|
||||
L.append(value.read())
|
||||
L.append('--' + BOUNDARY + '--')
|
||||
L.append('')
|
||||
body = CRLF.join(L)
|
||||
content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
|
||||
return content_type, body
|
||||
|
||||
data = urllib.urlencode(parameters)
|
||||
req = urllib2.Request(url, data)
|
||||
response = urllib2.urlopen(req)
|
||||
json = response.read()
|
||||
j = simplejson.loads(json)
|
||||
|
||||
if j['response_code'] == 1: # file known
|
||||
cfg = config()
|
||||
args = {'shasum': resource, 'url': dl_url, 'permalink': j['permalink']}
|
||||
|
||||
# we don't use dispatcher, so this check is needed
|
||||
if cfg.has_section('database_mysql'):
|
||||
mysql_logger = cowrie.output.mysql.DBLogger(cfg)
|
||||
mysql_logger.handleVirustotal(args)
|
||||
args_scan = {'shasum': resource, 'json': json}
|
||||
mysql_logger.handleVirustotalScan(args_scan)
|
||||
|
||||
if origin == 'db':
|
||||
# we don't use dispatcher, so this check is needed
|
||||
if cfg.has_section('database_textlog'):
|
||||
text_logger = cowrie.output.textlog.DBLogger(cfg)
|
||||
text_logger.handleVirustotalLog('log_from database', args)
|
||||
else:
|
||||
msg = 'Virustotal report of %s [%s] at %s' % \
|
||||
(resource, dl_url, j['permalink'])
|
||||
# we need to print msg, because logs from SFTP are dispatched this way
|
||||
print msg
|
||||
if honeypot:
|
||||
honeypot.logDispatch(msg)
|
||||
|
||||
elif j['response_code'] == 0: # file not known
|
||||
if origin == 'db':
|
||||
return j['response_code']
|
||||
|
||||
msg = 'Virustotal not known, response code: %s' % (j['response_code'])
|
||||
print msg
|
||||
host = "www.virustotal.com"
|
||||
url = "https://www.virustotal.com/vtapi/v2/file/scan"
|
||||
fields = [("apikey", apikey)]
|
||||
filepath = "dl/%s" % resource
|
||||
file_to_send = open(filepath, "rb").read()
|
||||
files = [("file", filename, file_to_send)]
|
||||
json = postfile.post_multipart(host, url, fields, files)
|
||||
print json
|
||||
|
||||
msg = 'insert to Virustotal backlog %s [%s]' % \
|
||||
(resource, dl_url)
|
||||
print msg
|
||||
virustotal_backlogs.insert(resource, dl_url)
|
||||
else:
|
||||
msg = 'Virustotal not known, response code: %s' % (j['response_code'])
|
||||
print msg
|
||||
return j['response_code']
|
||||
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user