Exemplo n.º 1
0
    class UploadSchema(schema.FileSchema):
        """Extends `FileSchema`."""

        args = fields.Dict(required=False, default={}, missing={})
        extract = fields.Bool(missing=False)
        name = fields.Str(missing=None)  # Override name
        password = fields.Str(missing=None)
Exemplo n.º 2
0
    class GetSchema(schema.Schema):
        """Extends `Schema`.

        Defines the valid schema for get request.
        """
        args = fields.Dict(required=False, default={}, missing={})
        command = fields.Str(required=False)
        format = fields.Str(type=enums.Format, missing=enums.Format.JSON)
        output = fields.Bool(required=False, default=True, missing=True)
        sha256_digests = fields.List(fields.Str(), required=False)
        scale = fields.Str(required=False)
Exemplo n.º 3
0
class Commands(scale.Commands):  # pylint: disable=too-many-public-methods
    def check(self):
        strings = shutil.which('radare2')
        if not strings:
            raise error.CommandWarning("binary 'radare2' not found")
        return

    @scale.command({
        'args': {
            'offset': fields.Str(required=True),
            'magic_bytes': fields.Str(default=None, missing=None),
            'patch': fields.Bool(default=True, missing=True),
            'size': fields.Str(required=True),
        },
        'info':
        'this function will carve binaries out of MDMP files'
    })
    def binary_carver(self, args, file, opts):
        sample = {}
        with tempfile.TemporaryDirectory(dir=path.abspath(
                path.expanduser(
                    config.snake_config['cache_dir']))) as temp_dir:
            # Try and carve
            file_path = r2_bin_carver.carve(file.file_path, temp_dir,
                                            args['offset'], args['size'],
                                            args['magic_bytes'])
            if not file_path:
                raise error.CommandError('failed to carve binary')
            if args['patch']:
                if not r2_bin_carver.patch(file_path):
                    raise error.CommandError(
                        'failed to patch binary, not a valid pe file')

            # Get file name
            document = db.file_collection.select(file.sha256_digest)
            if not document:
                raise error.SnakeError("failed to get sample's metadata")

            # Create schema and save
            name = '{}.{}'.format(document['name'], args['offset'])
            file_schema = schema.FileSchema().load({
                'name':
                name,
                'description':
                'extracted with radare2 script r2_bin_carver.py'
            })
            new_file = fs.FileStorage()
            new_file.create(file_path)
            sample = submitter.submit(file_schema, enums.FileType.FILE,
                                      new_file, file, NAME)
            sample = schema.FileSchema().dump(schema.FileSchema().load(
                sample))  # Required to clean the above

        return sample

    def binary_carver_markdown(self, json):
        output = md.table_header(('Name', 'SHA256 Digest', 'File Type'))
        output += md.table_row(
            (json['name'],
             md.url(
                 json['sha256_digest'],
                 '/#/{}/{}'.format(json['file_type'],
                                   json['sha256_digest'])), json['file_type']))
        if not json.keys():
            output += md.table_row(('-', '-', '-'))
        return output

    @scale.command({
        'args': {
            'bits': fields.Str(default='32', missing='32'),
            'technique': fields.Str(required=True)
        },
        'info': 'scan shellcode for hashed functions'
    })
    def hash_function_decoder(self, args, file, opts):
        # Validate
        if args['bits'] not in ['32', '64']:
            raise error.CommandError(
                'invalid bits provided, currently supported: 32, 64')
        if args['technique'] not in r2_hash_func_decoder.TECHNIQUES:
            raise error.CommandError(
                'invalid technique provided, currently supported: {}'.format(
                    r2_hash_func_decoder.TECHNIQUES))

        scale_dir = path.dirname(__file__)
        func_decoder = path.join(scale_dir, 'scripts/r2_hash_func_decoder.py')
        hash_db = path.join(scale_dir, 'scripts/r2_hash_func_decoder.db')

        # Get the output
        proc = subprocess.run([
            'python3', '{}'.format(func_decoder), 'analyse', '-f', '{}'.format(
                file.file_path), '-d', '{}'.format(hash_db), '{}'.format(
                    args['technique'])
        ],
                              stdout=subprocess.PIPE,
                              stderr=subprocess.PIPE)
        if proc.returncode:
            raise error.CommandError('failed to execute script')

        return {'analysis': str(proc.stdout, encoding='utf-8')}

    def hash_function_decoder_plaintext(self, json):
        return json['analysis']
Exemplo n.º 4
0
class Interface(scale.Interface):
    """
    Connect to MalwareBazaar and retrieve the JSON data for the malware hash.
    """
    def _malwarebazaar(self, sha256_digest, cache=True):
        params = {'query': 'get_info', 'hash': sha256_digest}

        document = db.file_collection.select(sha256_digest)
        if 'malwarebazaar' not in document or not cache:
            try:
                response = requests.post(API_ENDPOINT,
                                         data=params,
                                         headers=HEADERS,
                                         proxies=PROXIES,
                                         timeout=10)
            except Exception:
                raise error.InterfaceWarning(
                    'failed to connect to MalwareBazaar')
            if 'application/json' not in response.headers.get('content-type'):
                raise error.InterfaceWarning(
                    'invalid response received from MalwareBazaar')
            data = {'malwarebazaar': response.json()}
            db.file_collection.update(sha256_digest, data)
            document = db.file_collection.select(sha256_digest)
            if not document or 'malwarebazaar' not in document:
                raise error.MongoError(
                    'error adding malwarebazaar into file document %s' %
                    sha256_digest)
        if str(document['malwarebazaar']['query_status']) == 'hash_not_found':
            raise error.InterfaceWarning('File not present in MalwareBazaar')
        if str(document['malwarebazaar']['query_status']) != 'ok':
            raise error.InterfaceWarning('An unexpected error occured')

        return document['malwarebazaar']

    def check(self):
        """Self check are prerequisits set. API key is needed for upload."""
        if not API_KEY:
            raise error.InterfaceError(
                'config variable \'api_key\' has not been set')

    @scale.pull({
        'args': {
            'cache': fields.Bool(missing=True)
        },
        'info': 'MalwareBazaar results report'
    })
    def results(self, args, file, opts):
        """
        Parse the JSON and return the specific data part.
        """
        j = self._malwarebazaar(file.sha256_digest, cache=args['cache'])
        return j['data'][0]

    def results_markdown(self, json):
        """
        Convert the JSON result data to Markdown.
        """
        output = md.h2('General Information')
        output += md.paragraph('SHA256 hash: ' + str(json['sha256_hash']))
        output += md.paragraph('SHA1 hash: ' + str(json['sha1_hash']))
        output += md.paragraph('MD5 hash: ' + str(json['md5_hash']))
        output += md.paragraph('File name: ' + str(json['file_name']))
        output += md.paragraph('Signature: ' + str(json['signature']))
        output += md.paragraph('File size: ' + str(json['file_size']) +
                               " bytes")
        output += md.paragraph('First seen: ' + str(json['first_seen']))
        output += md.paragraph('Last seen: ' + str(json['last_seen']))
        output += md.paragraph('File type: ' + str(json['file_type']))
        output += md.paragraph('MIME type: ' + str(json['file_type_mime']))
        output += md.paragraph('imphash: ' + str(json['imphash']))
        output += md.paragraph('ssdeep: ' + str(json['ssdeep']))
        output += md.paragraph('Delivery Method: ' +
                               str(json['delivery_method']))
        if str(json['reporter']) == "anonymous":
            reporter = "*Anonymous*"
        else:
            reporter = "[@"
            reporter += str(json['reporter'])
            reporter += "](https://twitter.com/"
            reporter += str(json['reporter'])
            reporter += ")"

        output += md.paragraph('Reporter: ' + reporter)
        output += md.h2('Intelligence')
        output += md.paragraph('ClamAV: ' +
                               str(json['intelligence']['clamav']))
        output += md.paragraph('Number of downloads: ' +
                               str(json['intelligence']['downloads']))
        output += md.paragraph('Number of uploads: ' +
                               str(json['intelligence']['uploads']))
        output += md.paragraph('Mail intelligence: ' +
                               str(json['intelligence']['mail']))
        output += md.h2('File Information')
        if json['file_information']:
            for fileinfo in json['file_information']:
                output += md.paragraph('Contect: ' + str(fileinfo['context']))
                output += md.paragraph('Value: ' + str(fileinfo['value']))
        comment = str(json['comment']).replace('\r', '').replace('\n', '<br>')
        output += md.paragraph('Comment: ')
        output += md.paragraph(comment)
        taglist = ''
        if not json['tags']:
            taglist = 'None '
        else:
            for tag in json['tags']:
                taglist += tag + ','
        output += md.paragraph('Tags: ' + taglist[:-1])

        return output

    @scale.push({'info': 'submit file to MalwareBazaar'})
    def submit(self, args, file, opts):
        """
        Routine to submit a sample to MalwareBazaar.
        """
        document = db.file_collection.select(file.sha256_digest)
        with open(file.file_path, "rb") as sample:
            try:
                tags = []
                delivery_method = "other"
                data = {'tags': tags, 'delivery_method': delivery_method}
                files = {
                    'json_data': (None, js.dumps(data), 'application/json'),
                    'file': (document['name'], sample)
                }
                response = requests.post(API_ENDPOINT,
                                         files=files,
                                         headers=HEADERS,
                                         verify=True,
                                         timeout=10)
            except requests.exceptions.RequestException:
                raise error.InterfaceError("Failled to connect")

            json_response = response.json()

        return json_response

    @scale.pull({
        'args': {
            'cache': fields.Bool(missing=True)
        },
        'info': 'MalwareBazaar general info report'
    })
    def info(self, args, file, opts):
        """
        Retrieve the JSON information for the info block.
        """
        j = self._malwarebazaar(file.sha256_digest, cache=args['cache'])
        output = j['data'][0]
        return output

    def info_markdown(self, json):
        """
        Parse the JSON info block data to Markdown.
        """
        output = md.table_header(('Attribute', 'Value'))
        output += md.table_row(
            ('MB Link',
             'https://bazaar.abuse.ch/sample/' + str(json['sha256_hash'])))
        if str(json['reporter']) == "anonymous":
            reporter = "*Anonymous*"
        else:
            reporter = "[@"
            reporter += str(json['reporter'])
            reporter += "](https://twitter.com/"
            reporter += str(json['reporter'])
            reporter += ")"
        output += md.table_row(('Reporter', reporter))
        comment = str(json['comment']).partition('\n')[0].rstrip(':\r')
        output += md.table_row(('Comment', comment))
        taglist = ''
        if not json['tags']:
            taglist = 'None '
        else:
            for tag in json['tags']:
                taglist += tag + ','
        output += md.table_row(('Tags', taglist[:-1]))
        output += md.table_row(('ClamAV', str(json['intelligence']['clamav'])))
        output += md.table_row(('First seen', str(json['first_seen'])))
        output += md.table_row(('Last seen', str(json['last_seen'])))
        return output
Exemplo n.º 5
0
class Interface(scale.Interface):
    def _vt_scan(self, sha256_digest, cache=True):
        params = {
            'apikey': API_KEY,
            'resource': sha256_digest,
            'allinfo': 1
        }

        document = db.file_collection.select(sha256_digest)
        if 'vt' not in document or not cache:
            try:
                response = requests.get('https://www.virustotal.com/vtapi/v2/file/report',
                                        params=params,
                                        headers=HEADERS,
                                        proxies=PROXIES,
                                        timeout=10)
            except Exception:
                raise error.InterfaceWarning("failed to connect to VirusTotal")
            if 'application/json' not in response.headers.get('content-type'):
                raise error.InterfaceWarning("invalid response received from VirusTotal")
            if 'response_code' not in response.json():
                raise error.InterfaceWarning("unknown response from VirusTotal")
            data = {'vt': response.json()}
            db.file_collection.update(sha256_digest, data)
            document = db.file_collection.select(sha256_digest)
            if not document or 'vt' not in document:
                raise error.MongoError('error adding vt into file document %s' % sha256_digest)
        if document['vt']["response_code"] is 0:
            raise error.InterfaceWarning("file is not present on VirusTotal")

        # Check if we had public key but now its private, if so warn that cache is out of date
        # NOTE: we just check for missing info variable
        if IS_PRIVATE and 'first_seen' not in document['vt']:
            raise error.InterfaceWarning("private key specified but no private api data in cache, please flush vt cache for sample")

        return document['vt']

    def check(self):
        if not API_KEY:
            raise error.InterfaceError("config variable 'api_key' has not been set")

    @scale.pull({
        'args': {
            'cache': fields.Bool(missing=True)
        },
        'info': 'virustotal results report'
    })
    def results(self, args, file, opts):
        j = self._vt_scan(file.sha256_digest, cache=args['cache'])
        return j['scans']

    def results_markdown(self, json):
        scanresults = sorted(json)
        score = 0
        count = 0
        output = 'AV Vendor | Result\r\n'
        output += ':--- | :---\r\n'
        cleanoutput = ''
        for vendor in scanresults:
            count += 1
            if str(json[vendor]['detected']) == "True":
                score += 1
                output += vendor + ' | '
                output += str(json[vendor]['result']).replace('%', r'\%')
                output += ' | \r\n'
            else:
                cleanoutput += vendor + ' | '
                cleanoutput += 'Clean'
                cleanoutput += ' | \r\n'

        if score < 3:
            output = '**Score: ' + str(score) + '/' + str(count) + '**\r\n\r\n' + output
        else:
            output = '**Score: ' + str(score) + '/' + str(count) + '**\r\n\r\n' + output

        output = output + cleanoutput

        return output

    # TODO: Do It!
    # @scale.push({
    #     'info': 'submit file to virustotal'
    # })
    # def submit(self, args, file, opts):
    #     # TODO: Implement
    #     pass

    if IS_PRIVATE:
        @scale.pull({
            'args': {
                'cache': fields.Bool(missing=True)
            },
            'info': 'VirusTotal general info report'
        })
        def info(self, args, file, opts):
            j = self._vt_scan(file.sha256_digest, cache=args['cache'])
            score = 0
            for _, v in j['scans'].items():
                if v['detected'] is True:
                    score += 1
            output = {
                'vt_link': j['permalink'],
                'first_seen': j['first_seen'],
                'last_seen': j['last_seen'],
                'score': "%i/%i" % (score, len(j['scans'])),
                'times_submitted': j['times_submitted'],
                'type': j['type']
            }
            return output

        def info_markdown(self, json):
            output = md.table_header(('Attribute', 'Value'))
            output += md.table_row(('VT Link', json['vt_link']))
            output += md.table_row(('First Seen', json['first_seen']))
            output += md.table_row(('Last Seen', json['last_seen']))
            if int(json['score'].split('/')[0]) < 3:
                output += md.table_row(('Score', json['score']))
            else:
                output += md.table_row(('Score', json['score']))
            output += md.table_row(('Times Submitted', str(json['times_submitted'])))
            output += md.table_row(('Type', json['type']))
            return output

        @scale.pull({
            'args': {
                'cache': fields.Bool(missing=True)
            },
            'info': 'VirtualTotal submission names'
        })
        def names(self, args, file, opts):
            j = self._vt_scan(file.sha256_digest, cache=args['cache'])
            return j['submission_names']

        def names_markdown(self, json):
            output = '| Submission Names |\r\n'
            output += '| :------ |\r\n'
            for name in json:
                output += '| ' + name + ' |' + '\r\n'

            return output

        @scale.pull({
            'args': {
                'cache': fields.Bool(missing=True)
            },
            'info': 'VirusTotal associates URLs'
        })
        def urls(self, args, file, opts):
            j = self._vt_scan(file.sha256_digest, cache=args['cache'])
            return j['ITW_urls']

        def urls_markdown(self, json):
            output = '| Associated URLs |\r\n'
            output += '| :------ |\r\n'
            for url in json:
                # TODO: Determine some kind of URL sanitiser for use here
                output += '| ' + url.replace('http', 'hxxp') + ' |' + '\r\n'

            return output

        @scale.pull({
            'args': {
                'cache': fields.Bool(missing=True)
            },
            'info': 'VirtualTotal submission names'
        })
        def parents(self, args, file, opts):
            j = self._vt_scan(file.sha256_digest, cache=args['cache'])
            if 'compressed_parents' in j['additional_info']:
                return j['additional_info']['compressed_parents']
            return []

        def parents_markdown(self, json):
            output = '| Compressed Parents |\r\n'
            output += '| :------ |\r\n'
            for name in json:
                output += '| [' + name + '](https://www.virustotal.com/#/file/' + name + '/analysis) |' + '\r\n'
            if not json:
                output += md.table_row(('-'))
            return output
    else:
        @scale.pull({
            'args': {
                'cache': fields.Bool(missing=True)
            },
            'info': 'VirusTotal general info report'
        })
        def info(self, args, file, opts):
            j = self._vt_scan(file.sha256_digest, cache=args['cache'])
            score = 0
            for _, v in j['scans'].items():
                if v['detected'] is True:
                    score += 1
            output = {
                'vt_link': j['permalink'],
                'score': "%i/%i" % (score, len(j['scans'])),
            }
            return output

        def info_markdown(self, json):
            output = md.table_header(('Attribute', 'Value'))
            output += md.table_row(('VT Link', json['vt_link']))
            if int(json['score'].split('/')[0]) < 3:
                output += md.table_row(('Score', json['score']))
            else:
                output += md.table_row(('Score', json['score']))
            return output
Exemplo n.º 6
0
class CommandHandler(snake_handler.SnakeHandler):
    """Extends `SnakeHandler`."""
    @tornadoparser.use_args({
        # 'args': fields.Dict(required=False, default={}, missing={}),
        'command':
        fields.Str(required=True),
        'format':
        fields.Str(type=enums.Format, missing=enums.Format.JSON),
        'output':
        fields.Bool(required=False, default=True, missing=True),
        'scale':
        fields.Str(required=True),
        'sha256_digest':
        fields.Str(required=True)
    })
    async def get(self, data):
        # NOTE: Tornado/Marshmallow does not like Dict in args, will have to parse manually
        # TODO: Use marshmallow validation
        if 'args' in self.request.arguments and self.request.arguments['args']:
            data['args'] = json.loads(self.request.arguments['args'][0])
        else:
            data['args'] = {}
        document = await db.async_command_collection.select(
            data['sha256_digest'], data['scale'], data['command'],
            data['args'])
        if not document:
            self.write_warning("no output for given data", 404, data)
            self.finish()
            return

        if document['status'] == enums.Status.ERROR:
            self.write_warning("%s" % document['output'], 404, data)
            self.finish()
            return

        document = schema.CommandSchema().load(document)
        output = None
        if document['_output_id']:
            output = await db.async_command_output_collection.get(
                document['_output_id'])
        try:
            scale = scale_manager.get_scale(data['scale'])
            commands = scale_manager.get_component(
                scale, enums.ScaleComponent.COMMANDS)
            if data['output']:
                document['output'] = commands.snake.format(
                    data['format'], document['command'], output)
            document['format'] = data['format']
        except (SnakeError, TypeError) as err:
            self.write_warning("%s" % err, 404, data)
            self.finish()
            return

        document = schema.CommandSchema().dump(document)
        self.jsonify({'command': document})
        self.finish()

    @tornadoparser.use_args({
        'args':
        fields.Dict(required=False, default={}, missing={}),
        'asynchronous':
        fields.Bool(required=False),
        'command':
        fields.Str(required=True),
        'format':
        fields.Str(type=enums.Format, missing=enums.Format.JSON),
        'scale':
        fields.Str(required=True),
        'sha256_digest':
        fields.Str(required=True),
        'timeout':
        fields.Int(required=False)
    })
    async def post(self, data):
        # Check that there is a file for this hash
        document = await db.async_file_collection.select(data['sha256_digest'])
        if not document:
            self.write_warning("no sample for given data", 404, data)
            self.finish()
            return

        # Check scale support
        try:
            scale = scale_manager.get_scale(data['scale'],
                                            document['file_type'])
            commands = scale_manager.get_component(
                scale, enums.ScaleComponent.COMMANDS)
            cmd = commands.snake.command(data['command'])
        except SnakeError as err:
            self.write_warning("%s" % err, 404, data)
            self.finish()
            return

        # Validate arguments as to not waste users time, yes this is also done on execution
        result, args = validate_args(cmd, data['args'])
        if not result:
            self.write_warning(args, 422, data)
            self.finish()
            return
        data['args'] = args

        # Queue command
        try:
            document = await route_support.queue_command(data)
        except SnakeError as err:
            self.write_warning("%s" % err, 500, data)
            self.finish()
            return

        document = schema.CommandSchema().load(document)
        output = None
        if document['_output_id']:
            output = await db.async_command_output_collection.get(
                document['_output_id'])
        try:
            document['output'] = commands.snake.format(data['format'],
                                                       document['command'],
                                                       output)
            document['format'] = data['format']
        except SnakeError as err:
            self.write_warning("%s" % err, 404, data)
            self.finish()
            return

        # Dump and finish
        document = schema.CommandSchema().dump(document)
        self.jsonify({"command": document})
        self.finish()
Exemplo n.º 7
0
class UploadFileSchema(schema.FileSchema):
    """Extends `FileSchema`."""

    name = fields.Str(required=False)  # Override
    extract = fields.Bool(missing=False)
    password = fields.Str(missing=None)