diff --git a/README.md b/README.md index 07f0fa5..8ec1ecf 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Software required: * Twisted 8.0+ * PyCrypto * pyasn1 +* python-requests (for VirusTotal integration) * Zope Interface 3.6.0+ ## Files of interest: diff --git a/cowrie/output/virustotal.py b/cowrie/output/virustotal.py index 07224af..24d2e08 100644 --- a/cowrie/output/virustotal.py +++ b/cowrie/output/virustotal.py @@ -31,12 +31,25 @@ Send SSH logins to Virustotal Work in Progress - not functional yet """ +from zope.interface import implementer + import json import re +import requests 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.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 @@ -73,38 +86,90 @@ class Output(cowrie.core.output.Output): log.msg("Not seen before by VT") self.postcomment(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"]) - def postfile(self, scanFile): + + def postfile(self, artifact, fileName): """ Send a file to VirusTotal """ - vtHost = "www.virustotal.com" vtUrl = "https://www.virustotal.com/vtapi/v2/file/scan" fields = [("apikey", self.apiKey)] - file_to_send = open("test.txt", "rb").read() - files = [("file", "test.txt", file_to_send)] - response_data = postfile.post_multipart(vtHost, vtUrl, fields, files) - j = json.loads(response_data) + 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 posturl(self, scanUrl): """ - Send a URL to VirusTotal + Send a URL to VirusTotal with Twisted - response_code: if the item you searched for was not present in VirusTotal's dataset this result will be 0. + response_code: + If the item you searched for was not present in VirusTotal's dataset this result will be 0. If the requested item is still queued for analysis it will be -2. If the item was indeed present and it could be retrieved it will be 1. """ vtUrl = "https://www.virustotal.com/vtapi/v2/url/scan" + headers = http_headers.Headers({'User-Agent': ['Cowrie SSH Honeypot']}) fields = {"apikey": self.apiKey, "url": scanUrl} data = urllib.urlencode(fields) - req = urllib2.Request(vtUrl, data) - response = urllib2.urlopen(req) - response_data = response.read() - j = json.loads(response_data) - log.msg( "Sent URL to VT: %s" % (j,) ) - return j["response_code"] + body = StringProducer(data) + contextFactory = WebClientContextFactory() + + agent = client.Agent(reactor, contextFactory) + d = agent.request('POST', vtUrl, headers, body) + + + def cbResponse(response): + # print 'Response code:', response.code + # FIXME: Check for 200 + d = readBody(response) + d.addCallback(cbBody) + d.addErrback(cbPartial) + return d + + + def cbBody(body): + return logResult(body) + + + def cbPartial(failure): + """ + Google HTTP Server does not set Content-Length. Twisted marks it as partial + """ + return logResult(failure.value.response) + + + def cbError(failure): + failure.printTraceback() + + + def logResult(result): + j = json.loads(result) + log.msg( "VT result: %s", repr(j) ) + return j["response_code"] + + d.addCallback(cbResponse) + d.addErrback(cbError) + return d def postcomment(self, resource): @@ -123,6 +188,130 @@ class Output(cowrie.core.output.Output): log.msg( "Updated comment for %s to VT: %s" % (resource, j,) ) + +class WebClientContextFactory(ClientContextFactory): + def getContext(self, hostname, port): + return ClientContextFactory.getContext(self) + + + +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): + + def __init__(self, body): + self.body = body + self.length = len(body) + + + def startProducing(self, consumer): + consumer.write(self.body) + return defer.succeed(None) + + + def pauseProducing(self): + pass + + + def stopProducing(self): + pass + """ def get_report(resource, filename, dl_url='unknown', honeypot=None, origin=None):