class Parser(BaseParser): def __init__(self, parser_info): super().__init__(parser_info) self.mappers = parser_info['mappers'] self.used_facts = parser_info['used_facts'] self.log = Logger('parsing_svc') def parse(self, blob): relationships = [] json_output = self._load_json(blob) if json_output is not None: for mp in self.mappers: if 'json_key' not in dir(mp): self.log.warning( 'JSON Parser not given a json_key, not parsing') continue json_type = mp.json_type if 'json_type' in dir(mp) else None for match in self._get_vals_from_json(json_output, mp.json_key, json_type): source = self.set_value(mp.source, match, self.used_facts) target = self.set_value(mp.target, match, self.used_facts) relationships.append( Relationship(source=(mp.source, source), edge=mp.edge, target=(mp.target, target))) return relationships def _load_json(self, blob): try: return json.loads(blob) except Exception: self.log.warning('Output not JSON, use a different parser') return None def _get_vals_from_json(self, json_output, key, json_type): """ Get all values for a specified key recursively from JSON output. :param json_output: :param key: :param json_type: a list of types to filter matches by. Example options are - 'str', 'int', 'list', 'dict' :return: generator that yields matched values """ if isinstance(json_output, list): for item in json_output: for res in self._get_vals_from_json(item, key, json_type): yield res elif isinstance(json_output, dict): for k, v in json_output.items(): if k == key and (json_type is None or v.__class__.__name__ in json_type): yield json.dumps(v) if v.__class__.__name__ in [ 'list', 'dict' ] else v if isinstance(v, list) or isinstance(v, dict): for res in self._get_vals_from_json(v, key, json_type): yield res
class Parser(BaseParser): def __init__(self, parser_info): self.mappers = parser_info['mappers'] self.used_facts = parser_info['used_facts'] self.log = Logger('Parser') def gd_parser(self, text): results = dict() for block in text.split('\r\n\r\n'): if block: hostname = None pvi = None for line in block.splitlines(): hostname = self._parse_hostname(line, hostname) pvi = self._parse_version(line, pvi) if line.startswith('Exception') and '(0x80005000)' in line: # Domain communication error self.log.warning('Get-Domain parser: Domain Issue 0x80005000: Verify that the rat is running ' 'under a Domain Account, and that the Domain Controller can be reached.') if hostname and pvi: results[hostname] = dict(parsed_version_info=pvi) if not results: self.log.warning('Get-Domain Parser: Returned data contained no parseable information!') return results def parse(self, blob): relationships = [] try: parse_data = self.gd_parser(blob) for match in parse_data: for mp in self.mappers: relationships.append( Relationship(source=(mp.source, match), edge=mp.edge, target=(mp.target, None))) except Exception as error: self.log.warning('Get-Domain parser encountered an error - {}. Continuing...'.format(error)) return relationships ''' PRIVATE FUNCTION ''' @staticmethod def _parse_hostname(line, current): if line.startswith('dnshostname'): field_name, value = [c.strip() for c in line.split(':')] return value.lower() return current @staticmethod def _parse_version(line, current): if line.startswith('operatingsystemversion'): value = line.split(':')[-1].strip() # Looks like: '10.0 (14393)' os_version, build_number = value.split(' ') build_number = build_number[1:-1] # remove parens major_version, minor_version = os_version.split('.') return dict(os_name='windows', major_version=major_version, minor_version=minor_version, build_number=build_number) return current
class Parser(BaseParser): def __init__(self, parser_info): self.mappers = parser_info['mappers'] self.used_facts = parser_info['used_facts'] self.parse_mode = 'wdigest' self.log = Logger('parsing_svc') def parse_katz(self, output): """ Parses mimikatz output with the logonpasswords command and returns a list of dicts of the credentials. Args: output: stdout of "mimikatz.exe privilege::debug sekurlsa::logonpasswords exit" Returns: A list of MimikatzSection objects """ sections = output.split('Authentication Id') # split sections using "Authentication Id" as separator creds = [] for section in sections: mk_section = MimikatzBlock() package = {} package_name = '' in_header = True pstate = False for line in section.splitlines(): line = line.strip() if in_header: in_header = self._parse_header(line, mk_section) if in_header: continue # avoid excess parsing work pstate, package_name = self._process_package(line, package, package_name, mk_section) if pstate: pstate = False package = {} self._package_extend(package, package_name, mk_section) # save the current ssp if necessary if mk_section.packages: # save this section creds.append(mk_section) return creds def parse(self, blob): relationships = [] try: parse_data = self.parse_katz(blob) for match in parse_data: if self.parse_mode in match.packages: for mp in self.mappers: relationships.append( Relationship(source=(mp.source, match.packages[self.parse_mode][0]['Username']), edge=mp.edge, target=(mp.target, match.packages[self.parse_mode][0]['Password'])) ) except Exception as error: self.log.warning('Mimikatz parser encountered an error - {}. Continuing...'.format(error)) return relationships """ PRIVATE FUNCTION """ @staticmethod def _parse_header(line, mk_section): if line.startswith('msv'): return False session = re.match(r'^\s*Session\s*:\s*([^\r\n]*)', line) if session: mk_section.session = session.group(1) username = re.match(r'^\s*User Name\s*:\s*([^\r\n]*)', line) if username: mk_section.username = username.group(1) domain = re.match(r'^\s*Domain\s*:\s*([^\r\n]*)', line) if domain: mk_section.domain = domain.group(1) logon_server = re.match(r'^\s*Logon Server\s*:\s*([^\r\n]*)', line) if logon_server: mk_section.logon_server = logon_server.group(1) logon_time = re.match(r'^\s*Logon Time\s*:\s*([^\r\n]*)', line) if logon_time: mk_section.logon_time = logon_time.group(1) sid = re.match(r'^\s*SID\s*:\s*([^\r\n]*)', line) if sid: mk_section.sid = sid.group(1) return True def _process_package(self, line, package, package_name, mk_section): if line.startswith('['): self._package_extend(package, package_name, mk_section) # this might indicate the start of a new account return True, package_name # reset the package elif line.startswith('*'): m = re.match(r'\s*\* (.*?)\s*: (.*)', line) if m: package[m.group(1)] = m.group(2) elif line: match_group = re.match(r'([a-z]+) :', line) # parse out the new section name if match_group: # this is the start of a new ssp self._package_extend(package, package_name, mk_section) # save the current ssp if necessary return True, match_group.group(1) # reset the package return False, package_name @staticmethod def _package_extend(package, package_name, mk_section): if 'Username' in package and package['Username'] != '(null)' and \ (('Password' in package and package['Password'] != '(null)') or 'NTLM' in package): mk_section.packages[package_name].append(package)