aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-x[-rw-r--r--].config/ci/zipapp.sh3
-rw-r--r--.config/codespell_ignore.txt1
-rw-r--r--doc/scapy/layers/http.rst97
-rw-r--r--doc/scapy/usage.rst9
-rw-r--r--pyproject.toml3
-rw-r--r--scapy/arch/__init__.py8
-rw-r--r--scapy/asn1/asn1.py2
-rw-r--r--scapy/asn1fields.py29
-rw-r--r--scapy/automaton.py2
-rwxr-xr-xscapy/config.py2
-rw-r--r--scapy/contrib/gtp.py2
-rw-r--r--scapy/contrib/isotp/isotp_native_socket.py73
-rw-r--r--scapy/contrib/isotp/isotp_soft_socket.py1
-rw-r--r--scapy/layers/dns.py412
-rw-r--r--scapy/layers/gssapi.py16
-rw-r--r--scapy/layers/http.py503
-rw-r--r--scapy/layers/inet6.py16
-rw-r--r--scapy/layers/kerberos.py105
-rw-r--r--scapy/layers/l2.py57
-rw-r--r--scapy/layers/llmnr.py10
-rw-r--r--scapy/layers/msrpce/rpcclient.py1
-rw-r--r--scapy/layers/ntlm.py21
-rw-r--r--scapy/layers/sixlowpan.py2
-rw-r--r--scapy/layers/smb.py6
-rw-r--r--scapy/layers/smb2.py2
-rw-r--r--scapy/layers/smbclient.py2
-rw-r--r--scapy/layers/spnego.py3
-rw-r--r--scapy/layers/tls/crypto/cipher_block.py25
-rw-r--r--scapy/layers/tls/crypto/cipher_stream.py9
-rw-r--r--scapy/layers/tls/handshake.py2
-rw-r--r--scapy/py.typed0
-rw-r--r--scapy/supersocket.py35
-rw-r--r--scapy/utils.py100
-rw-r--r--scapy/utils6.py24
-rw-r--r--test/answering_machines.uts65
-rw-r--r--test/regression.uts26
-rw-r--r--test/scapy/layers/dns.uts4
-rw-r--r--test/scapy/layers/http.uts98
-rw-r--r--test/scapy/layers/l2.uts2
-rw-r--r--test/scapy/layers/smb2.uts4
40 files changed, 1514 insertions, 268 deletions
diff --git a/.config/ci/zipapp.sh b/.config/ci/zipapp.sh
index 525facf3..af7b8018 100644..100755
--- a/.config/ci/zipapp.sh
+++ b/.config/ci/zipapp.sh
@@ -69,6 +69,9 @@ if [ ! -d "./dist" ]; then
mkdir dist
fi
+# Copy version
+echo "$SCPY_VERSION" > "./dist/version"
+
echo "$SCPY"
# Build the zipapp
echo "Building zipapp"
diff --git a/.config/codespell_ignore.txt b/.config/codespell_ignore.txt
index a057b8e5..cfdb00d5 100644
--- a/.config/codespell_ignore.txt
+++ b/.config/codespell_ignore.txt
@@ -23,6 +23,7 @@ iff
implementors
inout
interaktive
+joinin
merchantibility
microsof
mitre
diff --git a/doc/scapy/layers/http.rst b/doc/scapy/layers/http.rst
index aef49738..ec9879d1 100644
--- a/doc/scapy/layers/http.rst
+++ b/doc/scapy/layers/http.rst
@@ -84,19 +84,87 @@ All common header fields should be supported.
Use Scapy to send/receive HTTP 1.X
__________________________________
-To handle this decompression, Scapy uses `Sessions classes <../usage.html#advanced-sniffing-sessions>`_, more specifically the ``TCPSession`` class.
-You have several ways of using it:
+Scapy uses `Sessions classes <../usage.html#advanced-sniffing-sessions>`_ (more specifically the ``TCPSession`` class), in order to dissect and reconstruct HTTP packets.
+This handles Content-Length, chunks and/or compression.
-+--------------------------------------------+-------------------------------------------+
-| ``sniff(session=TCPSession, [...])`` | ``TCP_client.tcplink(HTTP, host, 80)`` |
-+============================================+===========================================+
-| | Perform decompression / defragmentation | | Acts as a TCP client: handles SYN/ACK, |
-| | on all TCP streams simultaneously, but | | and all TCP actions, but only creates |
-| | only acts passively. | | one stream. |
-+--------------------------------------------+-------------------------------------------+
+Here are the main ways of using HTTP 1.X with Scapy:
+
+- :class:`~scapy.layers.http.HTTP_Client`: Automata that send HTTP requests. It supports the :func:`~scapy.layers.gssapi.SSP` mechanism to support authorization with NTLM, Kerberos, etc.
+- :class:`~scapy.layers.http.HTTP_Server`: Automata to handle incoming HTTP requests. Also supports :func:`~scapy.layers.gssapi.SSP`.
+- ``sniff(session=TCPSession, [...])``: Perform decompression / defragmentation on all TCP streams simultaneously, but only acts passively.
+- ``TCP_client.tcplink(HTTP, host, 80)``: Acts as a raw TCP client, handles SYN/ACK, and all TCP actions, but only creates one stream. It however supports some specific features, such as changing the source IP.
**Examples:**
+- :class:`~scapy.layers.http.HTTP_Client`:
+
+Let's perform a very simple GET request to an HTTP server:
+
+.. code:: python
+
+ from scapy.layers.http import * # or load_layer("http")
+ client = HTTP_Client()
+ resp = client.request("http://127.0.0.1:8080")
+ client.close()
+
+You can use the following shorthand to do the same very basic feature: :func:`~scapy.layers.http.http_request`, usable as so:
+
+.. code:: python
+
+ load_layer("http")
+ http_request("www.google.com", "/") # first argument is Host, second is Path
+
+Let's do the same request, but this time to a server that requires NTLM authentication:
+
+.. code:: python
+
+ from scapy.layers.http import * # or load_layer("http")
+ client = HTTP_Client(
+ HTTP_AUTH_MECHS.NTLM,
+ ssp=NTLMSSP(UPN="user", PASSWORD="password"),
+ )
+ resp = client.request("http://127.0.0.1:8080")
+ client.close()
+
+- :class:`~scapy.layers.http.HTTP_Server`:
+
+Start an unauthenticated HTTP server automaton:
+
+.. code:: python
+
+ from scapy.layers.http import *
+ from scapy.layers.ntlm import *
+
+ class Custom_HTTP_Server(HTTP_Server):
+ def answer(self, pkt):
+ if pkt.Path == b"/":
+ return HTTPResponse() / (
+ "<!doctype html><html><body><h1>OK</h1></body></html>"
+ )
+ else:
+ return HTTPResponse(
+ Status_Code=b"404",
+ Reason_Phrase=b"Not Found",
+ ) / (
+ "<!doctype html><html><body><h1>404 - Not Found</h1></body></html>"
+ )
+
+ server = HTTP_Server.spawn(
+ port=8080,
+ iface="eth0",
+ )
+
+We could also have started the same server, but requiring NTLM authorization using:
+
+.. code:: python
+
+ server = HTTP_Server.spawn(
+ port=8080,
+ iface="eth0",
+ HTTP_AUTH_MECHS.NTLM,
+ ssp=NTLMSSP(IDENTITIES={"user": MD4le("password")}),
+ )
+
- ``TCP_client.tcplink``:
Send an HTTPRequest to ``www.secdev.org`` and write the result in a file:
@@ -120,18 +188,9 @@ Send an HTTPRequest to ``www.secdev.org`` and write the result in a file:
``TCP_client.tcplink`` makes it feel like it only received one packet, but in reality it was recombined in ``TCPSession``.
If you performed a plain ``sniff()``, you would have seen those packets.
-**This code is implemented in a utility function:** ``http_request()``, usable as so:
-
-.. code:: python
-
- load_layer("http")
- http_request("www.google.com", "/", display=True)
-
-This will open the webpage in your default browser thanks to ``display=True``.
-
- ``sniff()``:
-Dissect a pcap which contains a JPEG image that was sent over HTTP using chunks.
+Dissect a pcap which contains a JPEG image that was sent over HTTP using chunks. This is able to reconstruct all HTTP streams in parallel.
.. note::
diff --git a/doc/scapy/usage.rst b/doc/scapy/usage.rst
index 67b3f02a..d22ee028 100644
--- a/doc/scapy/usage.rst
+++ b/doc/scapy/usage.rst
@@ -1449,6 +1449,15 @@ By default, ``dnsd`` uses a joker (IPv4 only): it answers to all unknown servers
You can also use ``relay=True`` to replace the joker behavior with a forward to a server included in ``conf.nameservers``.
+mDNS server
+------------
+
+See :class:`~scapy.layers.dns.mDNS_am`::
+
+ >>> mdnsd(iface="eth0", joker="192.168.1.1")
+
+Note that ``mdnsd`` extends the ``dnsd`` API.
+
LLMNR server
------------
diff --git a/pyproject.toml b/pyproject.toml
index 32993eb7..dcbfcc67 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -66,6 +66,9 @@ doc = [
# setuptools specific
+[tool.setuptools.package-data]
+"scapy" = ["py.typed"]
+
[tool.setuptools.packages.find]
include = [
"scapy*",
diff --git a/scapy/arch/__init__.py b/scapy/arch/__init__.py
index 0a692a6c..776bbe0e 100644
--- a/scapy/arch/__init__.py
+++ b/scapy/arch/__init__.py
@@ -18,7 +18,8 @@ from scapy.data import (
ARPHDR_LOOPBACK,
ARPHDR_PPP,
ARPHDR_TUN,
- IPV6_ADDR_GLOBAL
+ IPV6_ADDR_GLOBAL,
+ IPV6_ADDR_LOOPBACK,
)
from scapy.error import log_loading, Scapy_Exception
from scapy.interfaces import _GlobInterfaceType, network_name
@@ -104,8 +105,11 @@ def get_if_addr6(niff):
None is returned.
"""
iff = network_name(niff)
+ scope = IPV6_ADDR_GLOBAL
+ if iff == conf.loopback_name:
+ scope = IPV6_ADDR_LOOPBACK
return next((x[0] for x in in6_getifaddr()
- if x[2] == iff and x[1] == IPV6_ADDR_GLOBAL), None)
+ if x[2] == iff and x[1] == scope), None)
def get_if_raw_addr6(iff):
diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py
index 0a101d16..5df2810e 100644
--- a/scapy/asn1/asn1.py
+++ b/scapy/asn1/asn1.py
@@ -501,6 +501,8 @@ class ASN1_BIT_STRING(ASN1_Object[str]):
"""
val = str(val)
assert val in ['0', '1']
+ if len(self.val) < i:
+ self.val += "0" * (i - len(self.val))
self.val = self.val[:i] + val + self.val[i + 1:]
def __repr__(self):
diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py
index 2f1da38b..e4957b09 100644
--- a/scapy/asn1fields.py
+++ b/scapy/asn1fields.py
@@ -513,7 +513,12 @@ class ASN1F_SET(ASN1F_SEQUENCE):
ASN1_tag = ASN1_Class_UNIVERSAL.SET
-_SEQ_T = Union['ASN1_Packet', Type[ASN1F_field[Any, Any]], 'ASN1F_PACKET']
+_SEQ_T = Union[
+ 'ASN1_Packet',
+ Type[ASN1F_field[Any, Any]],
+ 'ASN1F_PACKET',
+ ASN1F_field[Any, Any],
+]
class ASN1F_SEQUENCE_OF(ASN1F_field[List[_SEQ_T],
@@ -533,10 +538,13 @@ class ASN1F_SEQUENCE_OF(ASN1F_field[List[_SEQ_T],
explicit_tag=None, # type: Optional[Any]
):
# type: (...) -> None
- if isinstance(cls, type) and issubclass(cls, ASN1F_field):
- self.fld = cls
- self._extract_packet = lambda s, pkt: self.fld(
- self.name, b"").m2i(pkt, s)
+ if isinstance(cls, type) and issubclass(cls, ASN1F_field) or \
+ isinstance(cls, ASN1F_field):
+ if isinstance(cls, type):
+ self.fld = cls(name, b"")
+ else:
+ self.fld = cls
+ self._extract_packet = lambda s, pkt: self.fld.m2i(pkt, s)
self.holds_packets = 0
elif hasattr(cls, "ASN1_root") or callable(cls):
self.cls = cast("Type[ASN1_Packet]", cls)
@@ -594,12 +602,21 @@ class ASN1F_SEQUENCE_OF(ASN1F_field[List[_SEQ_T],
s = b"".join(raw(i) for i in val)
return self.i2m(pkt, s)
+ def i2repr(self, pkt, x):
+ # type: (ASN1_Packet, _I) -> str
+ if self.holds_packets:
+ return super(ASN1F_SEQUENCE_OF, self).i2repr(pkt, x) # type: ignore
+ else:
+ return "[%s]" % ", ".join(
+ self.fld.i2repr(pkt, x) for x in x # type: ignore
+ )
+
def randval(self):
# type: () -> Any
if self.holds_packets:
return packet.fuzz(self.cls())
else:
- return self.fld(self.name, b"").randval()
+ return self.fld.randval()
def __repr__(self):
# type: () -> str
diff --git a/scapy/automaton.py b/scapy/automaton.py
index 94ecdce6..3f8862fe 100644
--- a/scapy/automaton.py
+++ b/scapy/automaton.py
@@ -1520,6 +1520,8 @@ class Automaton(metaclass=Automaton_metaclass):
Destroys a stopped Automaton: this cleanups all opened file descriptors.
Required on PyPy for instance where the garbage collector behaves differently.
"""
+ if not hasattr(self, "started"):
+ return # was never started.
if self.isrunning():
raise ValueError("Can't close running Automaton ! Call stop() beforehand")
# Close command pipes
diff --git a/scapy/config.py b/scapy/config.py
index 1f75e94e..3fe04388 100755
--- a/scapy/config.py
+++ b/scapy/config.py
@@ -1111,7 +1111,7 @@ class Conf(ConfClass):
#: When TCPSession is used, parse DCE/RPC sessions automatically.
#: This should be used for passive sniffing.
dcerpc_session_enable = False
- #: If a capture is missing the first DCE/RPC bindin message, we might incorrectly
+ #: If a capture is missing the first DCE/RPC binding message, we might incorrectly
#: assume that header signing isn't used. This forces it on.
dcerpc_force_header_signing = False
#: Windows SSPs for sniffing. This is used with
diff --git a/scapy/contrib/gtp.py b/scapy/contrib/gtp.py
index 1c12e8cf..66a911b6 100644
--- a/scapy/contrib/gtp.py
+++ b/scapy/contrib/gtp.py
@@ -861,7 +861,7 @@ class IE_EvolvedAllocationRetentionPriority(IE_Base):
class IE_CharginGatewayAddress(IE_Base):
- name = "Chargin Gateway Address"
+ name = "Charging Gateway Address"
fields_desc = [ByteEnumField("ietype", 251, IEType),
ShortField("length", 4),
ConditionalField(IPField("ipv4_address", "127.0.0.1"),
diff --git a/scapy/contrib/isotp/isotp_native_socket.py b/scapy/contrib/isotp/isotp_native_socket.py
index 127e91f6..949d24bd 100644
--- a/scapy/contrib/isotp/isotp_native_socket.py
+++ b/scapy/contrib/isotp/isotp_native_socket.py
@@ -319,44 +319,59 @@ class ISOTPNativeSocket(SuperSocket):
raise Scapy_Exception("Provide a string or a CANSocket "
"object as iface parameter")
- self.iface = cast(str, iface) or conf.contribs['NativeCANSocket']['iface'] # noqa: E501
- self.can_socket = socket.socket(socket.PF_CAN, socket.SOCK_DGRAM,
- CAN_ISOTP)
- self.__set_option_flags(self.can_socket,
- ext_address,
- rx_ext_address,
- listen_only,
- padding,
- frame_txtime)
-
+ self.iface: str = cast(str, iface) or conf.contribs['NativeCANSocket']['iface'] # noqa: E501
+ # store arguments internally
self.tx_id = tx_id
self.rx_id = rx_id
self.ext_address = ext_address
self.rx_ext_address = rx_ext_address
-
- self.can_socket.setsockopt(SOL_CAN_ISOTP,
- CAN_ISOTP_RECV_FC,
- self.__build_can_isotp_fc_options(
- stmin=stmin, bs=bs))
- self.can_socket.setsockopt(SOL_CAN_ISOTP,
- CAN_ISOTP_LL_OPTS,
- self.__build_can_isotp_ll_options(
- mtu=CAN_ISOTP_CANFD_MTU if fd
- else CAN_ISOTP_DEFAULT_LL_MTU,
- tx_dl=CAN_FD_ISOTP_DEFAULT_LL_TX_DL if fd
- else CAN_ISOTP_DEFAULT_LL_TX_DL))
- self.can_socket.setsockopt(
+ self.bs = bs
+ self.stmin = stmin
+ self.padding = padding
+ self.listen_only = listen_only
+ self.frame_txtime = frame_txtime
+ self.fd = fd
+ if basecls is None:
+ log_isotp.warning('Provide a basecls ')
+ self.basecls = basecls
+ self._init_socket()
+
+ def _init_socket(self) -> None:
+ can_socket = socket.socket(socket.PF_CAN, socket.SOCK_DGRAM,
+ CAN_ISOTP)
+ self.__set_option_flags(can_socket,
+ self.ext_address,
+ self.rx_ext_address,
+ self.listen_only,
+ self.padding,
+ self.frame_txtime)
+
+ can_socket.setsockopt(SOL_CAN_ISOTP,
+ CAN_ISOTP_RECV_FC,
+ self.__build_can_isotp_fc_options(
+ stmin=self.stmin, bs=self.bs))
+ can_socket.setsockopt(SOL_CAN_ISOTP,
+ CAN_ISOTP_LL_OPTS,
+ self.__build_can_isotp_ll_options(
+ mtu=CAN_ISOTP_CANFD_MTU if self.fd
+ else CAN_ISOTP_DEFAULT_LL_MTU,
+ tx_dl=CAN_FD_ISOTP_DEFAULT_LL_TX_DL if self.fd
+ else CAN_ISOTP_DEFAULT_LL_TX_DL))
+ can_socket.setsockopt(
socket.SOL_SOCKET,
SO_TIMESTAMPNS,
1
)
- self.__bind_socket(self.can_socket, self.iface, tx_id, rx_id)
- self.ins = self.can_socket
- self.outs = self.can_socket
- if basecls is None:
- log_isotp.warning('Provide a basecls ')
- self.basecls = basecls
+ self.__bind_socket(can_socket, self.iface, self.tx_id, self.rx_id)
+ # make sure existing sockets are closed,
+ # required in case of a reconnect.
+ self.closed = False
+ self.close()
+
+ self.ins = can_socket
+ self.outs = can_socket
+ self.closed = False
def recv_raw(self, x=0xffff):
# type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501
diff --git a/scapy/contrib/isotp/isotp_soft_socket.py b/scapy/contrib/isotp/isotp_soft_socket.py
index 119917e5..81c6d3fe 100644
--- a/scapy/contrib/isotp/isotp_soft_socket.py
+++ b/scapy/contrib/isotp/isotp_soft_socket.py
@@ -39,6 +39,7 @@ from typing import (
Callable,
TYPE_CHECKING,
)
+
if TYPE_CHECKING:
from scapy.contrib.cansocket import CANSocket
diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py
index 0a7a3b70..eb330187 100644
--- a/scapy/layers/dns.py
+++ b/scapy/layers/dns.py
@@ -8,6 +8,7 @@ DNS: Domain Name System.
"""
import abc
+import collections
import operator
import itertools
import socket
@@ -39,6 +40,7 @@ from scapy.fields import (
I,
IP6Field,
IntField,
+ MACField,
MultipleTypeField,
PacketListField,
ShortEnumField,
@@ -49,6 +51,7 @@ from scapy.fields import (
XStrFixedLenField,
XStrLenField,
)
+from scapy.interfaces import resolve_iface
from scapy.sendrecv import sr1
from scapy.supersocket import StreamSocket
from scapy.pton_ntop import inet_ntop, inet_pton
@@ -243,7 +246,7 @@ def dns_compress(pkt):
for field in current.fields_desc:
if isinstance(field, DNSStrField) or \
(isinstance(field, MultipleTypeField) and
- current.type in [2, 3, 4, 5, 12, 15, 39]):
+ current.type in [2, 3, 4, 5, 12, 15, 39, 47]):
# Get the associated data and store it accordingly # noqa: E501
dat = current.getfieldval(field.name)
yield current, field.name, dat
@@ -423,7 +426,7 @@ class DNSTextField(StrLenField):
# RFC 2671 - Extension Mechanisms for DNS (EDNS0)
-edns0types = {0: "Reserved", 1: "LLQ", 2: "UL", 3: "NSID", 4: "Reserved",
+edns0types = {0: "Reserved", 1: "LLQ", 2: "UL", 3: "NSID", 4: "Owner",
5: "DAU", 6: "DHU", 7: "N3U", 8: "edns-client-subnet", 10: "COOKIE",
15: "Extended DNS Error"}
@@ -458,7 +461,7 @@ class DNSRROPT(Packet):
name = "DNS OPT Resource Record"
fields_desc = [DNSStrField("rrname", ""),
ShortEnumField("type", 41, dnstypes),
- ShortField("rclass", 4096),
+ ShortEnumField("rclass", 4096, dnsclasses),
ByteField("extrcode", 0),
ByteField("version", 0),
# version 0 means EDNS0
@@ -469,6 +472,30 @@ class DNSRROPT(Packet):
length_from=lambda pkt: pkt.rdlen)]
+# draft-cheshire-edns0-owner-option-01 - EDNS0 OWNER Option
+
+class EDNS0OWN(_EDNS0Dummy):
+ name = "EDNS0 Owner (OWN)"
+ fields_desc = [ShortEnumField("optcode", 4, edns0types),
+ FieldLenField("optlen", None, count_of="primary_mac", fmt="H"),
+ ByteField("v", 0),
+ ByteField("s", 0),
+ MACField("primary_mac", "00:00:00:00:00:00"),
+ ConditionalField(
+ MACField("wakeup_mac", "00:00:00:00:00:00"),
+ lambda pkt: (pkt.optlen or 0) >= 18),
+ ConditionalField(
+ StrLenField("password", "",
+ length_from=lambda pkt: pkt.optlen - 18),
+ lambda pkt: (pkt.optlen or 0) >= 22)]
+
+ def post_build(self, pkt, pay):
+ pkt += pay
+ if self.optlen is None:
+ pkt = pkt[:2] + struct.pack("!H", len(pkt) - 4) + pkt[4:]
+ return pkt
+
+
# RFC 6975 - Signaling Cryptographic Algorithm Understanding in
# DNS Security Extensions (DNSSEC)
@@ -637,6 +664,7 @@ class EDNS0ExtendedDNSError(_EDNS0Dummy):
EDNS0OPT_DISPATCHER = {
+ 4: EDNS0OWN,
5: EDNS0DAU,
6: EDNS0DHU,
7: EDNS0N3U,
@@ -741,12 +769,16 @@ def RRlist2bitmap(lst):
class RRlistField(StrField):
+ islist = 1
+
def h2i(self, pkt, x):
- if isinstance(x, list):
+ if x and isinstance(x, list):
return RRlist2bitmap(x)
return x
def i2repr(self, pkt, x):
+ if not x:
+ return "[]"
x = self.i2h(pkt, x)
rrlist = bitmap2RRlist(x)
return [dnstypes.get(rr, rr) for rr in rrlist] if rrlist else repr(x)
@@ -774,7 +806,8 @@ class DNSRRHINFO(_DNSRRdummy):
name = "DNS HINFO Resource Record"
fields_desc = [DNSStrField("rrname", ""),
ShortEnumField("type", 13, dnstypes),
- ShortEnumField("rclass", 1, dnsclasses),
+ BitField("cacheflush", 0, 1), # mDNS RFC 6762
+ BitEnumField("rclass", 1, 15, dnsclasses),
IntField("ttl", 0),
ShortField("rdlen", None),
FieldLenField("cpulen", None, fmt="!B", length_of="cpu"),
@@ -787,7 +820,8 @@ class DNSRRMX(_DNSRRdummy):
name = "DNS MX Resource Record"
fields_desc = [DNSStrField("rrname", ""),
ShortEnumField("type", 15, dnstypes),
- ShortEnumField("rclass", 1, dnsclasses),
+ BitField("cacheflush", 0, 1), # mDNS RFC 6762
+ BitEnumField("rclass", 1, 15, dnsclasses),
IntField("ttl", 0),
ShortField("rdlen", None),
ShortField("preference", 0),
@@ -816,7 +850,8 @@ class DNSRRRSIG(_DNSRRdummy):
name = "DNS RRSIG Resource Record"
fields_desc = [DNSStrField("rrname", ""),
ShortEnumField("type", 46, dnstypes),
- ShortEnumField("rclass", 1, dnsclasses),
+ BitField("cacheflush", 0, 1), # mDNS RFC 6762
+ BitEnumField("rclass", 1, 15, dnsclasses),
IntField("ttl", 0),
ShortField("rdlen", None),
ShortEnumField("typecovered", 1, dnstypes),
@@ -835,11 +870,12 @@ class DNSRRNSEC(_DNSRRdummy):
name = "DNS NSEC Resource Record"
fields_desc = [DNSStrField("rrname", ""),
ShortEnumField("type", 47, dnstypes),
- ShortEnumField("rclass", 1, dnsclasses),
+ BitField("cacheflush", 0, 1), # mDNS RFC 6762
+ BitEnumField("rclass", 1, 15, dnsclasses),
IntField("ttl", 0),
ShortField("rdlen", None),
DNSStrField("nextname", ""),
- RRlistField("typebitmaps", "")
+ RRlistField("typebitmaps", [])
]
@@ -847,7 +883,8 @@ class DNSRRDNSKEY(_DNSRRdummy):
name = "DNS DNSKEY Resource Record"
fields_desc = [DNSStrField("rrname", ""),
ShortEnumField("type", 48, dnstypes),
- ShortEnumField("rclass", 1, dnsclasses),
+ BitField("cacheflush", 0, 1), # mDNS RFC 6762
+ BitEnumField("rclass", 1, 15, dnsclasses),
IntField("ttl", 0),
ShortField("rdlen", None),
FlagsField("flags", 256, 16, "S???????Z???????"),
@@ -863,7 +900,8 @@ class DNSRRDS(_DNSRRdummy):
name = "DNS DS Resource Record"
fields_desc = [DNSStrField("rrname", ""),
ShortEnumField("type", 43, dnstypes),
- ShortEnumField("rclass", 1, dnsclasses),
+ BitField("cacheflush", 0, 1), # mDNS RFC 6762
+ BitEnumField("rclass", 1, 15, dnsclasses),
IntField("ttl", 0),
ShortField("rdlen", None),
ShortField("keytag", 0),
@@ -889,7 +927,8 @@ class DNSRRNSEC3(_DNSRRdummy):
name = "DNS NSEC3 Resource Record"
fields_desc = [DNSStrField("rrname", ""),
ShortEnumField("type", 50, dnstypes),
- ShortEnumField("rclass", 1, dnsclasses),
+ BitField("cacheflush", 0, 1), # mDNS RFC 6762
+ BitEnumField("rclass", 1, 15, dnsclasses),
IntField("ttl", 0),
ShortField("rdlen", None),
ByteField("hashalg", 0),
@@ -899,7 +938,7 @@ class DNSRRNSEC3(_DNSRRdummy):
StrLenField("salt", "", length_from=lambda x: x.saltlength),
FieldLenField("hashlength", 0, fmt="!B", length_of="nexthashedownername"), # noqa: E501
StrLenField("nexthashedownername", "", length_from=lambda x: x.hashlength), # noqa: E501
- RRlistField("typebitmaps", "")
+ RRlistField("typebitmaps", [])
]
@@ -907,7 +946,8 @@ class DNSRRNSEC3PARAM(_DNSRRdummy):
name = "DNS NSEC3PARAM Resource Record"
fields_desc = [DNSStrField("rrname", ""),
ShortEnumField("type", 51, dnstypes),
- ShortEnumField("rclass", 1, dnsclasses),
+ BitField("cacheflush", 0, 1), # mDNS RFC 6762
+ BitEnumField("rclass", 1, 15, dnsclasses),
IntField("ttl", 0),
ShortField("rdlen", None),
ByteField("hashalg", 0),
@@ -976,7 +1016,8 @@ class DNSRRSVCB(_DNSRRdummy):
name = "DNS SVCB Resource Record"
fields_desc = [DNSStrField("rrname", ""),
ShortEnumField("type", 64, dnstypes),
- ShortEnumField("rclass", 1, dnsclasses),
+ BitField("cacheflush", 0, 1), # mDNS RFC 6762
+ BitEnumField("rclass", 1, 15, dnsclasses),
IntField("ttl", 0),
ShortField("rdlen", None),
ShortField("svc_priority", 0),
@@ -998,7 +1039,8 @@ class DNSRRSRV(_DNSRRdummy):
name = "DNS SRV Resource Record"
fields_desc = [DNSStrField("rrname", ""),
ShortEnumField("type", 33, dnstypes),
- ShortEnumField("rclass", 1, dnsclasses),
+ BitField("cacheflush", 0, 1), # mDNS RFC 6762
+ BitEnumField("rclass", 1, 15, dnsclasses),
IntField("ttl", 0),
ShortField("rdlen", None),
ShortField("priority", 0),
@@ -1093,7 +1135,8 @@ class DNSRR(Packet):
show_indent = 0
fields_desc = [DNSStrField("rrname", ""),
ShortEnumField("type", 1, dnstypes),
- ShortEnumField("rclass", 1, dnsclasses),
+ BitField("cacheflush", 0, 1), # mDNS RFC 6762
+ BitEnumField("rclass", 1, 15, dnsclasses),
IntField("ttl", 0),
FieldLenField("rdlen", None, length_of="rdata", fmt="H"),
MultipleTypeField(
@@ -1150,7 +1193,8 @@ class DNSQR(Packet):
show_indent = 0
fields_desc = [DNSStrField("qname", "www.example.com"),
ShortEnumField("qtype", 1, dnsqtypes),
- ShortEnumField("qclass", 1, dnsclasses)]
+ BitField("unicastresponse", 0, 1), # mDNS RFC 6762
+ BitEnumField("qclass", 1, 15, dnsclasses)]
def default_payload_class(self, payload):
return conf.padding_layer
@@ -1246,7 +1290,13 @@ class DNS(DNSCompressedPacket):
type = "Qry"
if self.qd and isinstance(self.qd[0], DNSQR):
name = ' %s' % self.qd[0].qname
- return 'DNS %s%s' % (type, name)
+ return "%sDNS %s%s" % (
+ "m"
+ if isinstance(self.underlayer, UDP) and self.underlayer.dport == 5353
+ else "",
+ type,
+ name,
+ )
def post_build(self, pkt, pay):
if isinstance(self.underlayer, TCP) and self.length is None:
@@ -1418,50 +1468,113 @@ RFC2136
class DNS_am(AnsweringMachine):
function_name = "dnsd"
filter = "udp port 53"
- cls = DNS # We also use this automaton for llmnrd
+ cls = DNS # We also use this automaton for llmnrd / mdnsd
def parse_options(self, joker=None,
match=None,
srvmatch=None,
joker6=False,
+ send_error=False,
relay=False,
- from_ip=None,
- from_ip6=None,
+ from_ip=True,
+ from_ip6=False,
src_ip=None,
src_ip6=None,
ttl=10,
- jokerarpa=None):
+ jokerarpa=False):
"""
- :param joker: default IPv4 for unresolved domains. (Default: None)
+ Simple DNS answering machine.
+
+ :param joker: default IPv4 for unresolved domains.
Set to False to disable, None to mirror the interface's IP.
- :param joker6: default IPv6 for unresolved domains (Default: False)
- set to False to disable, None to mirror the interface's IPv6.
- :param jokerarpa: answer for .in-addr.arpa PTR requests. (Default: None)
+ Defaults to None, unless 'match' is used, then it defaults to
+ False.
+ :param joker6: default IPv6 for unresolved domains.
+ Set to False to disable, None to mirror the interface's IPv6.
+ Defaults to False.
+ :param match: queries to match.
+ This can be a dictionary of {name: val} where name is a string
+ representing a domain name (A, AAAA) and val is a tuple of 2
+ elements, each representing an IP or a list of IPs. If val is
+ a single element, (A, None) is assumed.
+ This can also be a list or names, in which case joker(6) are
+ used as a response.
+ :param jokerarpa: answer for .in-addr.arpa PTR requests. (Default: False)
:param relay: relay unresolved domains to conf.nameservers (Default: False).
- :param match: a dictionary of {name: val} where name is a string representing
- a domain name (A, AAAA) and val is a tuple of 2 elements, each
- representing an IP or a list of IPs. If val is a single element,
- (A, None) is assumed.
+ :param send_error: send an error message when this server can't answer
+ (Default: False)
:param srvmatch: a dictionary of {name: (port, target)} used for SRV
- :param from_ip: an source IP to filter. Can contain a netmask
- :param from_ip6: an source IPv6 to filter. Can contain a netmask
+ :param from_ip: an source IP to filter. Can contain a netmask. True for all,
+ False for none. Default True
+ :param from_ip6: an source IPv6 to filter. Can contain a netmask. True for all,
+ False for none. Default False
:param ttl: the DNS time to live (in seconds)
:param src_ip: override the source IP
:param src_ip6:
- Example:
+ Examples:
+
+ - Answer all 'A' and 'AAAA' requests::
$ sudo iptables -I OUTPUT -p icmp --icmp-type 3/3 -j DROP
- >>> dnsd(match={"google.com": "1.1.1.1"}, joker="192.168.0.2", iface="eth0")
- >>> dnsd(srvmatch={
- ... "_ldap._tcp.dc._msdcs.DOMAIN.LOCAL.": (389, "srv1.domain.local")
- ... })
+ >>> dnsd(joker="192.168.0.2", joker6="fe80::260:8ff:fe52:f9d8",
+ ... iface="eth0")
+
+ - Answer only 'A' query for google.com with 192.168.0.2::
+
+ >>> dnsd(match={"google.com": "192.168.0.2"}, iface="eth0")
+
+ - Answer DNS for a Windows domain controller ('SRV', 'A' and 'AAAA')::
+
+ >>> dnsd(
+ ... srvmatch={
+ ... "_ldap._tcp.dc._msdcs.DOMAIN.LOCAL.": (389,
+ ... "srv1.domain.local"),
+ ... },
+ ... match={"src1.domain.local": ("192.168.0.102",
+ ... "fe80::260:8ff:fe52:f9d8")},
+ ... )
+
+ - Relay all queries to another DNS server, except some::
+
+ >>> conf.nameservers = ["1.1.1.1"] # server to relay to
+ >>> dnsd(
+ ... match={"test.com": "1.1.1.1"},
+ ... relay=True,
+ ... )
"""
+ from scapy.layers.inet6 import Net6
+
+ self.mDNS = isinstance(self, mDNS_am)
+ self.llmnr = self.cls != DNS
+
+ # Add some checks (to help)
+ if not isinstance(joker, (str, bool)) and joker is not None:
+ raise ValueError("Bad 'joker': should be an IPv4 (str) or False !")
+ if not isinstance(joker6, (str, bool)) and joker6 is not None:
+ raise ValueError("Bad 'joker6': should be an IPv6 (str) or False !")
+ if not isinstance(jokerarpa, (str, bool)):
+ raise ValueError("Bad 'jokerarpa': should be a hostname or False !")
+ if not isinstance(from_ip, (str, Net, bool)):
+ raise ValueError("Bad 'from_ip': should be an IPv4 (str), Net or False !")
+ if not isinstance(from_ip6, (str, Net6, bool)):
+ raise ValueError("Bad 'from_ip6': should be an IPv6 (str), Net or False !")
+ if self.mDNS and src_ip:
+ raise ValueError("Cannot use 'src_ip' in mDNS !")
+ if self.mDNS and src_ip6:
+ raise ValueError("Cannot use 'src_ip6' in mDNS !")
+
+ if joker is None and match is not None:
+ joker = False
+ self.joker = joker
+ self.joker6 = joker6
+ self.jokerarpa = jokerarpa
+
def normv(v):
if isinstance(v, (tuple, list)) and len(v) == 2:
- return v
+ return tuple(v)
elif isinstance(v, str):
- return (v, None)
+ return (v, joker6)
else:
raise ValueError("Bad match value: '%s'" % repr(v))
@@ -1470,24 +1583,26 @@ class DNS_am(AnsweringMachine):
if not k.endswith(b"."):
k += b"."
return k
- if match is None:
- self.match = {}
- else:
- self.match = {normk(k): normv(v) for k, v in match.items()}
+
+ self.match = collections.defaultdict(lambda: (joker, joker6))
+ if match:
+ if isinstance(match, (list, set)):
+ self.match.update({normk(k): (None, None) for k in match})
+ else:
+ self.match.update({normk(k): normv(v) for k, v in match.items()})
if srvmatch is None:
self.srvmatch = {}
else:
self.srvmatch = {normk(k): normv(v) for k, v in srvmatch.items()}
- self.joker = joker
- self.joker6 = joker6
- self.jokerarpa = jokerarpa
+
+ self.send_error = send_error
self.relay = relay
if isinstance(from_ip, str):
self.from_ip = Net(from_ip)
else:
self.from_ip = from_ip
if isinstance(from_ip6, str):
- self.from_ip6 = Net(from_ip6)
+ self.from_ip6 = Net6(from_ip6)
else:
self.from_ip6 = from_ip6
self.src_ip = src_ip
@@ -1500,29 +1615,57 @@ class DNS_am(AnsweringMachine):
req.haslayer(self.cls) and
req.getlayer(self.cls).qr == 0 and (
(
- not self.from_ip6 or req[IPv6].src in self.from_ip6
+ self.from_ip6 is True or
+ (self.from_ip6 and req[IPv6].src in self.from_ip6)
)
if IPv6 in req else
(
- not self.from_ip or req[IP].src in self.from_ip
+ self.from_ip is True or
+ (self.from_ip and req[IP].src in self.from_ip)
)
)
)
def make_reply(self, req):
+ # Build reply from the request
resp = req.copy()
if Ether in req:
- resp[Ether].src, resp[Ether].dst = (
- None if req[Ether].dst == "ff:ff:ff:ff:ff:ff" else req[Ether].dst,
- req[Ether].src,
- )
+ if self.mDNS:
+ resp[Ether].src, resp[Ether].dst = None, None
+ elif self.llmnr:
+ resp[Ether].src, resp[Ether].dst = None, req[Ether].src
+ else:
+ resp[Ether].src, resp[Ether].dst = (
+ None if req[Ether].dst in "ff:ff:ff:ff:ff:ff" else req[Ether].dst,
+ req[Ether].src,
+ )
from scapy.layers.inet6 import IPv6
if IPv6 in req:
resp[IPv6].underlayer.remove_payload()
- resp /= IPv6(dst=req[IPv6].src, src=self.src_ip6 or req[IPv6].dst)
+ if self.mDNS:
+ # "All Multicast DNS responses (including responses sent via unicast)
+ # SHOULD be sent with IP TTL set to 255."
+ resp /= IPv6(dst="ff02::fb", src=self.src_ip6,
+ fl=req[IPv6].fl, hlim=255)
+ elif self.llmnr:
+ resp /= IPv6(dst=req[IPv6].src, src=self.src_ip6,
+ fl=req[IPv6].fl, hlim=req[IPv6].hlim)
+ else:
+ resp /= IPv6(dst=req[IPv6].src, src=self.src_ip6 or req[IPv6].dst,
+ fl=req[IPv6].fl, hlim=req[IPv6].hlim)
elif IP in req:
resp[IP].underlayer.remove_payload()
- resp /= IP(dst=req[IP].src, src=self.src_ip or req[IP].dst)
+ if self.mDNS:
+ # "All Multicast DNS responses (including responses sent via unicast)
+ # SHOULD be sent with IP TTL set to 255."
+ resp /= IP(dst="224.0.0.251", src=self.src_ip,
+ id=req[IP].id, ttl=255)
+ elif self.llmnr:
+ resp /= IP(dst=req[IP].src, src=self.src_ip,
+ id=req[IP].id, ttl=req[IP].ttl)
+ else:
+ resp /= IP(dst=req[IP].src, src=self.src_ip or req[IP].dst,
+ id=req[IP].id, ttl=req[IP].ttl)
else:
warning("No IP or IPv6 layer in %s", req.command())
return
@@ -1531,7 +1674,6 @@ class DNS_am(AnsweringMachine):
except IndexError:
warning("No UDP layer in %s", req.command(), exc_info=True)
return
- ans = []
try:
req = req[self.cls]
except IndexError:
@@ -1547,47 +1689,84 @@ class DNS_am(AnsweringMachine):
except AttributeError:
warning("No qd attribute in %s", req.command(), exc_info=True)
return
+ # Special case: alias 'ALL' query as 'A' + 'AAAA'
+ try:
+ allquery = next(
+ (x for x in queries if getattr(x, "qtype", None) == 255)
+ )
+ queries.remove(allquery)
+ queries.extend([
+ DNSQR(
+ qtype=x,
+ qname=allquery.qname,
+ unicastresponse=allquery.unicastresponse,
+ qclass=allquery.qclass,
+ )
+ for x in [1, 28]
+ ])
+ except StopIteration:
+ pass
+ # Process each query
+ ans = []
+ ars = []
for rq in queries:
if isinstance(rq, Raw):
warning("Cannot parse qd element %s", rq.command(), exc_info=True)
continue
+ rqname = rq.qname.lower()
if rq.qtype in [1, 28]:
# A or AAAA
if rq.qtype == 28:
# AAAA
- try:
- rdata = self.match[rq.qname.lower()][1]
- except KeyError:
- if self.relay or self.joker6 is False:
- rdata = None
+ rdata = self.match[rqname][1]
+ if rdata is None and not self.relay:
+ # 'None' resolves to the default IPv6
+ iface = resolve_iface(self.optsniff.get("iface", conf.iface))
+ if self.mDNS:
+ # All IPs, as per mDNS.
+ rdata = iface.ips[6]
else:
- rdata = self.joker6 or get_if_addr6(
- self.optsniff.get("iface", conf.iface)
+ rdata = get_if_addr6(
+ iface
)
+ if self.mDNS and rdata and IPv6 in resp:
+ # For mDNS, we must replace the IPv6 src
+ resp[IPv6].src = rdata
elif rq.qtype == 1:
# A
- try:
- rdata = self.match[rq.qname.lower()][0]
- except KeyError:
- if self.relay or self.joker is False:
- rdata = None
+ rdata = self.match[rqname][0]
+ if rdata is None and not self.relay:
+ # 'None' resolves to the default IPv4
+ iface = resolve_iface(self.optsniff.get("iface", conf.iface))
+ if self.mDNS:
+ # All IPs, as per mDNS.
+ rdata = iface.ips[4]
else:
- rdata = self.joker or get_if_addr(
- self.optsniff.get("iface", conf.iface)
+ rdata = get_if_addr(
+ iface
)
- if rdata is not None:
+ if self.mDNS and rdata and IP in resp:
+ # For mDNS, we must replace the IP src
+ resp[IP].src = rdata
+ if rdata:
# Common A and AAAA
if not isinstance(rdata, list):
rdata = [rdata]
ans.extend([
- DNSRR(rrname=rq.qname, ttl=self.ttl, rdata=x, type=rq.qtype)
+ DNSRR(
+ rrname=rq.qname,
+ ttl=self.ttl,
+ rdata=x,
+ type=rq.qtype,
+ cacheflush=self.mDNS and rq.qtype == rq.qtype,
+ )
for x in rdata
])
continue # next
elif rq.qtype == 33:
# SRV
try:
- port, target = self.srvmatch[rq.qname.lower()]
+ port, target = self.srvmatch[rqname]
ans.append(DNSRRSRV(
rrname=rq.qname,
port=port,
@@ -1619,15 +1798,80 @@ class DNS_am(AnsweringMachine):
continue # next
except TimeoutError:
pass
- # Error
- break
+ # Still no answer.
+ if self.mDNS:
+ # "Any time a responder receives a query for a name for which it
+ # has verified exclusive ownership, for a type for which that name
+ # has no records, the responder MUST respond asserting the
+ # nonexistence of that record using a DNS NSEC record [RFC4034]."
+ ans.append(DNSRRNSEC(
+ # RFC6762 sect 6.1 - Negative Response
+ ttl=self.ttl,
+ rrname=rq.qname,
+ nextname=rq.qname,
+ typebitmaps=RRlist2bitmap([rq.qtype]),
+ ))
+ if self.mDNS and all(x.type == 47 for x in ans):
+ # If mDNS answers with only NSEC, discard.
+ return
+ if not ans:
+ # No answer is available.
+ if self.send_error:
+ resp /= self.cls(id=req.id, qr=1, qd=req.qd, rcode=3)
+ return resp
+ log_runtime.info("No answer could be provided to: %s" % req.summary())
+ return
+ # Handle Additional Records
+ if self.mDNS:
+ # Windows specific extension
+ ars.append(DNSRROPT(
+ z=0x1194,
+ rdata=[
+ EDNS0OWN(
+ primary_mac=resp[Ether].src,
+ ),
+ ],
+ ))
+ # All rq were answered
+ if self.mDNS:
+ # in mDNS mode, don't repeat the question, set aa=1, rd=0
+ dns = self.cls(id=req.id, aa=1, rd=0, qr=1, qd=[], ar=ars, an=ans)
+ else:
+ dns = self.cls(id=req.id, qr=1, qd=req.qd, ar=ars, an=ans)
+ # Compress DNS and mDNS
+ if not self.llmnr:
+ resp /= dns_compress(dns)
else:
- if not ans:
- # No rq was actually answered, as none was valid. Discard.
- return
- # All rq were answered
- resp /= self.cls(id=req.id, qr=1, qd=req.qd, an=ans)
- return resp
- # An error happened
- resp /= self.cls(id=req.id, qr=1, qd=req.qd, rcode=3)
+ resp /= dns
return resp
+
+
+class mDNS_am(DNS_am):
+ """
+ mDNS answering machine.
+
+ This has the same arguments as DNS_am. See help(DNS_am)
+
+ Example::
+
+ - Answer for 'TEST.local' with local IPv4::
+
+ >>> mdnsd(match=["TEST.local"])
+
+ - Answer all requests with other IP::
+
+ >>> mdnsd(joker="192.168.0.2", joker6="fe80::260:8ff:fe52:f9d8",
+ ... iface="eth0")
+
+ - Answer for multiple different mDNS names::
+
+ >>> mdnsd(match={"TEST.local": "192.168.0.100",
+ ... "BOB.local": "192.168.0.101"})
+
+ - Answer with both A and AAAA records::
+
+ >>> mdnsd(match={"TEST.local": ("192.168.0.100",
+ ... "fe80::260:8ff:fe52:f9d8")})
+ """
+ function_name = "mdnsd"
+ filter = "udp port 5353"
diff --git a/scapy/layers/gssapi.py b/scapy/layers/gssapi.py
index d3ca8cac..1c637a6c 100644
--- a/scapy/layers/gssapi.py
+++ b/scapy/layers/gssapi.py
@@ -142,9 +142,11 @@ class GSSAPI_BLOB_SIGNATURE(ASN1_Packet):
# heuristics.
if _pkt[:2] in [b"\x04\x04", b"\x05\x04"]:
from scapy.layers.kerberos import KRB_InnerToken
+
return KRB_InnerToken
elif len(_pkt) >= 4 and _pkt[:4] == b"\x01\x00\x00\x00":
from scapy.layers.ntlm import NTLMSSP_MESSAGE_SIGNATURE
+
return NTLMSSP_MESSAGE_SIGNATURE
return cls
@@ -260,6 +262,7 @@ class GSS_C_FLAGS(IntFlag):
"""
Authenticator Flags per RFC2744 req_flags
"""
+
GSS_C_DELEG_FLAG = 0x01
GSS_C_MUTUAL_FLAG = 0x02
GSS_C_REPLAY_FLAG = 0x04
@@ -297,12 +300,16 @@ class SSP:
if req_flags is None:
# Default
req_flags = (
- GSS_C_FLAGS.GSS_C_EXTENDED_ERROR_FLAG |
- GSS_C_FLAGS.GSS_C_MUTUAL_FLAG
+ GSS_C_FLAGS.GSS_C_EXTENDED_ERROR_FLAG
+ | GSS_C_FLAGS.GSS_C_MUTUAL_FLAG
)
self.flags = req_flags
self.passive = False
+ def clifailure(self):
+ # This allows to reset the client context without discarding it.
+ pass
+
def __repr__(self):
return "[Default SSP]"
@@ -312,8 +319,9 @@ class SSP:
"""
@abc.abstractmethod
- def GSS_Init_sec_context(self, Context: CONTEXT, val=None,
- req_flags: Optional[GSS_C_FLAGS] = None):
+ def GSS_Init_sec_context(
+ self, Context: CONTEXT, val=None, req_flags: Optional[GSS_C_FLAGS] = None
+ ):
"""
GSS_Init_sec_context: client-side call for the SSP
"""
diff --git a/scapy/layers/http.py b/scapy/layers/http.py
index 3f8812f5..8a12fdf3 100644
--- a/scapy/layers/http.py
+++ b/scapy/layers/http.py
@@ -39,25 +39,37 @@ You can turn auto-decompression/auto-compression off with::
# It was reimplemented for scapy 2.4.3+ using sessions, stream handling.
# Original Authors : Steeve Barbeau, Luca Invernizzi
+import base64
+import datetime
import gzip
import io
import os
import re
import socket
+import ssl
import struct
import subprocess
+from enum import Enum
+
from scapy.compat import plain_str, bytes_encode
+from scapy.automaton import Automaton, ATMT
from scapy.config import conf
from scapy.consts import WINDOWS
-from scapy.error import warning, log_loading
+from scapy.error import warning, log_loading, log_interactive, Scapy_Exception
from scapy.fields import StrField
from scapy.packet import Packet, bind_layers, bind_bottom_up, Raw
-from scapy.supersocket import StreamSocket
+from scapy.supersocket import StreamSocket, SSLStreamSocket
from scapy.utils import get_temp_file, ContextManagerSubprocess
-from scapy.layers.inet import TCP, TCP_client
+from scapy.layers.gssapi import (
+ GSS_S_COMPLETE,
+ GSS_S_FAILURE,
+ GSS_S_CONTINUE_NEEDED,
+ GSSAPI_BLOB,
+)
+from scapy.layers.inet import TCP
try:
import brotli
@@ -400,6 +412,7 @@ class _HTTPContent(Packet):
if self.raw_packet_cache is not None:
return self.raw_packet_cache
p = b""
+ encodings = self._get_encodings()
# Walk all the fields, in order
for i, f in enumerate(self.fields_desc):
if f.name == "Unknown_Headers":
@@ -407,8 +420,16 @@ class _HTTPContent(Packet):
# Get the field value
val = self.getfieldval(f.name)
if not val:
- # Not specified. Skip
- continue
+ if f.name == "Content_Length" and "chunked" not in encodings:
+ # Add Content-Length anyways
+ val = str(len(self.payload or b""))
+ elif f.name == "Date" and isinstance(self, HTTPResponse):
+ val = datetime.datetime.utcnow().strftime(
+ '%a, %d %b %Y %H:%M:%S GMT'
+ )
+ else:
+ # Not specified. Skip
+ continue
if i >= 3:
val = _header_line(f.real_name, val)
@@ -454,6 +475,11 @@ class _HTTPHeaderField(StrField):
name = _strip_header_name(name)
StrField.__init__(self, name, default, fmt="H")
+ def i2repr(self, pkt, x):
+ if isinstance(x, bytes):
+ return x.decode(errors="backslashreplace")
+ return x
+
def _generate_headers(*args):
"""Generate the header fields based on their name"""
@@ -507,8 +533,7 @@ class HTTPRequest(_HTTPContent):
def mysummary(self):
return self.sprintf(
- "%HTTPRequest.Method% %HTTPRequest.Path% "
- "%HTTPRequest.Http_Version%"
+ "%HTTPRequest.Method% '%HTTPRequest.Path%' "
)
@@ -552,8 +577,7 @@ class HTTPResponse(_HTTPContent):
def mysummary(self):
return self.sprintf(
- "%HTTPResponse.Http_Version% %HTTPResponse.Status_Code% "
- "%HTTPResponse.Reason_Phrase%"
+ "%HTTPResponse.Status_Code% %HTTPResponse.Reason_Phrase%"
)
# General HTTP class + defragmentation
@@ -694,57 +718,212 @@ class HTTP(Packet):
return Raw
+class HTTP_AUTH_MECHS(Enum):
+ NONE = "NONE"
+ BASIC = "Basic"
+ NTLM = "NTLM"
+ NEGOTIATE = "Negotiate"
+
+
+class HTTP_Client(object):
+ """
+ A basic HTTP client
+
+ :param mech: one of HTTP_AUTH_MECHS
+ :param ssl: whether to use HTTPS or not
+ :param ssp: the SSP object to use for binding
+ """
+
+ def __init__(
+ self,
+ mech=HTTP_AUTH_MECHS.NONE,
+ verb=True,
+ sslcontext=None,
+ ssp=None,
+ no_check_certificate=False,
+ ):
+ self.sock = None
+ self._sockinfo = None
+ self.authmethod = mech
+ self.verb = verb
+ self.sslcontext = sslcontext
+ self.ssp = ssp
+ self.sspcontext = None
+ self.no_check_certificate = no_check_certificate
+
+ def _connect_or_reuse(self, host, port=None, tls=False, timeout=5):
+ # Get the port
+ if port is None:
+ if tls:
+ port = 443
+ else:
+ port = 80
+ # If the current socket matches, keep it.
+ if self._sockinfo == (host, port):
+ return
+ # A new socket is needed
+ if self._sockinfo:
+ self.close()
+ sock = socket.socket()
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
+ sock.settimeout(timeout)
+ if self.verb:
+ print(
+ "\u2503 Connecting to %s on port %s%s..."
+ % (
+ host,
+ port,
+ " with SSL" if tls else "",
+ )
+ )
+ sock.connect((host, port))
+ if self.verb:
+ print(
+ conf.color_theme.green(
+ "\u2514 Connected from %s" % repr(sock.getsockname())
+ )
+ )
+ if tls:
+ if self.sslcontext is None:
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ if self.no_check_certificate:
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+ else:
+ context = self.sslcontext
+ sock = context.wrap_socket(sock)
+ self.sock = SSLStreamSocket(sock, HTTP)
+ else:
+ self.sock = StreamSocket(sock, HTTP)
+ # Store information regarding the current socket
+ self._sockinfo = (host, port)
+
+ def sr1(self, req, **kwargs):
+ if self.verb:
+ print(conf.color_theme.opening(">> %s" % req.summary()))
+ resp = self.sock.sr1(
+ HTTP() / req,
+ verbose=0,
+ **kwargs,
+ )
+ if self.verb:
+ print(
+ conf.color_theme.success(
+ "<< %s" % (resp and resp.summary())
+ )
+ )
+ return resp
+
+ def request(self, url, data=b"", timeout=5, follow_redirects=True, **headers):
+ """
+ Perform a HTTP(s) request.
+ """
+ # Parse request url
+ m = re.match(r"(https?)://([^/:]+)(?:\:(\d+))?(?:/(.*))?", url)
+ if not m:
+ raise ValueError("Bad URL !")
+ transport, host, port, path = m.groups()
+ if transport == "https":
+ tls = True
+ else:
+ tls = False
+
+ path = path or "/"
+ port = port and int(port)
+
+ # Connect (or reuse) socket
+ self._connect_or_reuse(host, port=port, tls=tls, timeout=timeout)
+
+ # Build request
+ http_headers = {
+ "Accept_Encoding": b'gzip, deflate',
+ "Cache_Control": b'no-cache',
+ "Pragma": b'no-cache',
+ "Connection": b'keep-alive',
+ "Host": host,
+ "Path": path,
+ }
+ http_headers.update(headers)
+ req = HTTP() / HTTPRequest(**http_headers)
+ if data:
+ req /= data
+
+ while True:
+ # Perform the request.
+ resp = self.sr1(req)
+ if not resp:
+ break
+ # First case: auth was required. Handle that
+ if resp.Status_Code in [b"401", b"407"]:
+ # Authentication required
+ if self.authmethod in [
+ HTTP_AUTH_MECHS.NTLM,
+ HTTP_AUTH_MECHS.NEGOTIATE,
+ ]:
+ # Parse authenticate
+ if b" " in resp.WWW_Authenticate:
+ method, data = resp.WWW_Authenticate.split(b" ", 1)
+ try:
+ ssp_blob = GSSAPI_BLOB(base64.b64decode(data))
+ except Exception:
+ raise Scapy_Exception("Invalid WWW-Authenticate")
+ else:
+ method = resp.WWW_Authenticate
+ ssp_blob = None
+ if plain_str(method) != self.authmethod.value:
+ raise Scapy_Exception("Invalid WWW-Authenticate")
+ # SPNEGO / Kerberos / NTLM
+ self.sspcontext, token, status = self.ssp.GSS_Init_sec_context(
+ self.sspcontext,
+ ssp_blob,
+ req_flags=0,
+ )
+ if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]:
+ raise Scapy_Exception("Authentication failure")
+ req.Authorization = (
+ self.authmethod.value.encode() + b" " +
+ base64.b64encode(bytes(token))
+ )
+ continue
+ # Second case: follow redirection
+ if resp.Status_Code in [b"301", b"302"] and follow_redirects:
+ return self.request(
+ resp.Location.decode(),
+ data=data,
+ timeout=timeout,
+ follow_redirects=follow_redirects,
+ **headers,
+ )
+ break
+ return resp
+
+ def close(self):
+ if self.verb:
+ print("X Connection to %s closed\n" % repr(self.sock.ins.getpeername()))
+ self.sock.close()
+
+
def http_request(host, path="/", port=80, timeout=3,
- display=False, verbose=0,
- raw=False, iface=None,
- **headers):
- """Util to perform an HTTP request, using the TCP_client.
+ display=False, verbose=0, **headers):
+ """
+ Util to perform an HTTP request.
:param host: the host to connect to
:param path: the path of the request (default /)
:param port: the port (default 80)
:param timeout: timeout before None is returned
:param display: display the result in the default browser (default False)
- :param raw: opens a raw socket instead of going through the OS's TCP
- socket. Scapy will then use its own TCP client.
- Careful, the OS might cancel the TCP connection with RST.
:param iface: interface to use. Changing this turns on "raw"
:param headers: any additional headers passed to the request
:returns: the HTTPResponse packet
"""
- http_headers = {
- "Accept_Encoding": b'gzip, deflate',
- "Cache_Control": b'no-cache',
- "Pragma": b'no-cache',
- "Connection": b'keep-alive',
- "Host": host,
- "Path": path,
- }
- http_headers.update(headers)
- req = HTTP() / HTTPRequest(**http_headers)
- ans = None
-
- # Open a socket
- if iface is not None:
- raw = True
- if raw:
- sock = TCP_client.tcplink(HTTP, host, port, debug=verbose,
- iface=iface)
- else:
- # Use a native TCP socket
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.connect((host, port))
- sock = StreamSocket(sock, HTTP)
- # Send the request and wait for the answer
- try:
- ans = sock.sr1(
- req,
- timeout=timeout,
- verbose=verbose
- )
- finally:
- sock.close()
+ client = HTTP_Client(HTTP_AUTH_MECHS.NONE, verb=verbose)
+ ans = client.request(
+ "http://%s:%s%s" % (host, port, path),
+ timeout=timeout,
+ )
+
if ans:
if display:
if Raw not in ans:
@@ -772,3 +951,231 @@ bind_layers(TCP, HTTP, sport=80, dport=80)
bind_bottom_up(TCP, HTTP, sport=8080)
bind_bottom_up(TCP, HTTP, dport=8080)
+
+
+# Automatons
+
+class HTTP_Server(Automaton):
+ """
+ HTTP server automaton
+
+ :param ssp: the SSP to serve. If None, unauthenticated (or basic).
+ :param mech: the HTTP_AUTH_MECHS to use (default: NONE)
+
+ Other parameters:
+
+ :param BASIC_IDENTITIES: a dict that contains {"user": "password"} for Basic
+ authentication.
+ :param BASIC_REALM: the basic realm.
+ """
+
+ pkt_cls = HTTP
+
+ def __init__(
+ self,
+ mech=HTTP_AUTH_MECHS.NONE,
+ ssp=None,
+ verb=True,
+ *args,
+ **kwargs,
+ ):
+ self.verb = verb
+ if "sock" not in kwargs:
+ raise ValueError(
+ "HTTP_Server cannot be started directly ! Use SMB_Server.spawn"
+ )
+ self.ssp = ssp
+ self.authmethod = mech.value
+ self.sspcontext = None
+ self.basic = False
+ self.BASIC_IDENTITIES = kwargs.pop("BASIC_IDENTITIES", {})
+ self.BASIC_REALM = kwargs.pop("BASIC_REALM", "default")
+ if mech == HTTP_AUTH_MECHS.BASIC:
+ if not self.BASIC_IDENTITIES:
+ raise ValueError("Please provide 'BASIC_IDENTITIES' !")
+ if ssp is not None:
+ raise ValueError("Can't use 'BASIC_IDENTITIES' with 'ssp' !")
+ self.basic = True
+ elif mech == HTTP_AUTH_MECHS.NONE:
+ if ssp is not None:
+ raise ValueError("Cannot use ssp with mech=NONE !")
+ # Initialize
+ Automaton.__init__(self, *args, **kwargs)
+
+ def send(self, resp):
+ self.sock.send(HTTP() / resp)
+
+ def vprint(self, s=""):
+ """
+ Verbose print (if enabled)
+ """
+ if self.verb:
+ if conf.interactive:
+ log_interactive.info("> %s", s)
+ else:
+ print("> %s" % s)
+
+ @ATMT.state(initial=1)
+ def BEGIN(self):
+ self.authenticated = False
+ self.sspcontext = None
+
+ @ATMT.condition(BEGIN, prio=0)
+ def should_authenticate(self):
+ if self.authmethod == HTTP_AUTH_MECHS.NONE:
+ raise self.SERVE()
+ else:
+ raise self.AUTH()
+
+ @ATMT.state()
+ def AUTH(self):
+ pass
+
+ @ATMT.state()
+ def AUTH_ERROR(self, proxy):
+ self.sspcontext = None
+ self._ask_authorization(proxy, self.authmethod)
+ self.vprint("AUTH ERROR")
+
+ @ATMT.condition(AUTH_ERROR)
+ def allow_reauth(self):
+ raise self.AUTH()
+
+ def _ask_authorization(self, proxy, data):
+ if proxy:
+ self.send(
+ HTTPResponse(
+ Status_Code=b"407",
+ Reason_Phrase=b"Proxy Authentication Required",
+ Proxy_Authenticate=data,
+ )
+ )
+ else:
+ self.send(
+ HTTPResponse(
+ Status_Code=b"401",
+ Reason_Phrase=b"Unauthorized",
+ WWW_Authenticate=data,
+ )
+ )
+
+ @ATMT.receive_condition(AUTH, prio=1)
+ def received_unauthenticated(self, pkt):
+ if HTTPRequest in pkt:
+ self.vprint(pkt.summary())
+ if pkt.Method == b"CONNECT":
+ # HTTP tunnel (proxy)
+ proxy = True
+ else:
+ # HTTP non-tunnel
+ proxy = False
+ # Get authorization
+ if proxy:
+ authorization = pkt.Proxy_Authorization
+ else:
+ authorization = pkt.Authorization
+ if not authorization:
+ # Initial ask.
+ data = self.authmethod
+ if self.basic:
+ data += " realm='%s'" % self.BASIC_REALM
+ self._ask_authorization(proxy, data)
+ return
+ # Parse authorization
+ method, data = authorization.split(b" ", 1)
+ if plain_str(method) != self.authmethod:
+ raise self.AUTH_ERROR(proxy)
+ try:
+ data = base64.b64decode(data)
+ except Exception:
+ raise self.AUTH_ERROR(proxy)
+ # Now process the authorization
+ if not self.basic:
+ try:
+ ssp_blob = GSSAPI_BLOB(data)
+ except Exception:
+ self.sspcontext = None
+ self._ask_authorization(proxy, self.authmethod)
+ raise self.AUTH_ERROR(proxy)
+ # And call the SSP
+ self.sspcontext, tok, status = self.ssp.GSS_Accept_sec_context(
+ self.sspcontext, ssp_blob
+ )
+ else:
+ # This is actually Basic authentication
+ try:
+ next(
+ True
+ for k, v in self.BASIC_IDENTITIES.items()
+ if ("%s:%s" % (k, v)).encode() == data
+ )
+ tok, status = None, GSS_S_COMPLETE
+ except StopIteration:
+ tok, status = None, GSS_S_FAILURE
+ # Send answer
+ if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]:
+ raise self.AUTH_ERROR(proxy)
+ elif status == GSS_S_CONTINUE_NEEDED:
+ data = self.authmethod.encode()
+ if tok:
+ data += b" " + base64.b64encode(bytes(tok))
+ self._ask_authorization(proxy, data)
+ raise self.AUTH()
+ else:
+ # Authenticated !
+ self.authenticated = True
+ self.vprint("AUTH OK")
+ raise self.SERVE(pkt)
+
+ @ATMT.eof(AUTH)
+ def auth_eof(self):
+ raise self.CLOSED()
+
+ @ATMT.state(error=1)
+ def ERROR(self):
+ self.send(
+ HTTPResponse(
+ Status_Code="400",
+ Reason_Phrase="Bad Request",
+ )
+ )
+
+ @ATMT.state(final=1)
+ def CLOSED(self):
+ self.vprint("CLOSED")
+
+ # Serving
+
+ @ATMT.state()
+ def SERVE(self, pkt):
+ answer = self.answer(pkt)
+ if answer:
+ self.send(answer)
+ self.vprint("%s -> %s" % (pkt.summary(), answer.summary()))
+ else:
+ self.vprint("%s" % pkt.summary())
+
+ @ATMT.receive_condition(SERVE)
+ def new_request(self, pkt):
+ raise self.SERVE(pkt)
+
+ # DEV: overwrite this function
+
+ def answer(self, pkt):
+ """
+ HTTP_server answer function.
+
+ :param pkt: a HTTPRequest packet
+ :returns: a HTTPResponse packet
+ """
+ if pkt.Path == b"/":
+ return HTTPResponse() / (
+ "<!doctype html><html><body><h1>OK</h1></body></html>"
+ )
+ else:
+ return HTTPResponse(
+ Status_Code=b"404",
+ Reason_Phrase=b"Not Found",
+ ) / (
+ "<!doctype html><html><body><h1>404 - Not Found</h1></body></html>"
+ )
diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py
index 4602b324..ad6feaf1 100644
--- a/scapy/layers/inet6.py
+++ b/scapy/layers/inet6.py
@@ -84,6 +84,11 @@ from scapy.utils6 import in6_getnsma, in6_getnsmac, in6_isaddr6to4, \
in6_isllsnmaddr, in6_ismaddr, Net6, teredoAddrExtractInfo
from scapy.volatile import RandInt, RandShort
+# Typing
+from typing import (
+ Optional,
+)
+
if not socket.has_ipv6:
raise socket.error("can't use AF_INET6, IPv6 is disabled")
if not hasattr(socket, "IPPROTO_IPV6"):
@@ -135,19 +140,24 @@ def neighsol(addr, src, iface, timeout=1, chainCC=0):
@conf.commands.register
def getmacbyip6(ip6, chainCC=0):
- """Returns the MAC address corresponding to an IPv6 address
+ # type: (str, int) -> Optional[str]
+ """
+ Returns the MAC address used to reach a given IPv6 address.
neighborCache.get() method is used on instantiated neighbor cache.
Resolution mechanism is described in associated doc string.
(chainCC parameter value ends up being passed to sending function
used to perform the resolution, if needed)
- """
+ .. seealso:: :func:`~scapy.layers.l2.getmacbyip` for IPv4.
+ """
+ # Sanitize the IP
if isinstance(ip6, Net6):
ip6 = str(ip6)
- if in6_ismaddr(ip6): # Multicast
+ # Multicast
+ if in6_ismaddr(ip6): # mcast @
mac = in6_getnsmac(inet_pton(socket.AF_INET6, ip6))
return mac
diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py
index a98ba154..7b4c06f7 100644
--- a/scapy/layers/kerberos.py
+++ b/scapy/layers/kerberos.py
@@ -579,7 +579,7 @@ _AD_TYPES = {
141: "KERB-AUTH-DATA-TOKEN-RESTRICTIONS",
142: "KERB-LOCAL",
143: "AD-AUTH-DATA-AP-OPTIONS",
- 144: "AD-TARGET-PRINCIPAL", # not an official name
+ 144: "KERB-AUTH-DATA-CLIENT-TARGET",
}
@@ -665,11 +665,13 @@ _PADATA_TYPES = {
144: "PA-OTP-PIN-CHANGE",
145: "PA-EPAK-AS-REQ",
146: "PA-EPAK-AS-REP",
- 147: "PA_PKINIT_KX",
- 148: "PA_PKU2U_NAME",
+ 147: "PA-PKINIT-KX",
+ 148: "PA-PKU2U-NAME",
149: "PA-REQ-ENC-PA-REP",
- 150: "PA_AS_FRESHNESS",
+ 150: "PA-AS-FRESHNESS",
151: "PA-SPAKE",
+ 161: "KERB-KEY-LIST-REQ",
+ 162: "KERB-KEY-LIST-REP",
165: "PA-SUPPORTED-ENCTYPES",
166: "PA-EXTENDED-ERROR",
167: "PA-PAC-OPTIONS",
@@ -885,6 +887,7 @@ class KERB_AUTH_DATA_AP_OPTIONS(Packet):
0x4000,
{
0x4000: "KERB_AP_OPTIONS_CBT",
+ 0x8000: "KERB_AP_OPTIONS_UNVERIFIED_TARGET_NAME",
},
),
]
@@ -893,18 +896,17 @@ class KERB_AUTH_DATA_AP_OPTIONS(Packet):
_AUTHORIZATIONDATA_VALUES[143] = KERB_AUTH_DATA_AP_OPTIONS
-# This has no doc..? not in [MS-KILE] at least.
-# We use the name wireshark/samba gave it
+# This has no doc..? [MS-KILE] only mentions its name.
-class KERB_AD_TARGET_PRINCIPAL(Packet):
+class KERB_AUTH_DATA_CLIENT_TARGET(Packet):
name = "KERB-AD-TARGET-PRINCIPAL"
fields_desc = [
StrFieldUtf16("spn", ""),
]
-_AUTHORIZATIONDATA_VALUES[144] = KERB_AD_TARGET_PRINCIPAL
+_AUTHORIZATIONDATA_VALUES[144] = KERB_AUTH_DATA_CLIENT_TARGET
# RFC6806 sect 6
@@ -969,6 +971,69 @@ class PA_PAC_OPTIONS(ASN1_Packet):
_PADATA_CLASSES[167] = PA_PAC_OPTIONS
+# [MS-KILE] sect 2.2.11
+
+
+class KERB_KEY_LIST_REQ(ASN1_Packet):
+ ASN1_codec = ASN1_Codecs.BER
+ ASN1_root = ASN1F_SEQUENCE_OF(
+ "keytypes",
+ [],
+ ASN1F_enum_INTEGER("", 0, _KRB_E_TYPES),
+ )
+
+
+_PADATA_CLASSES[161] = KERB_KEY_LIST_REQ
+
+# [MS-KILE] sect 2.2.12
+
+
+class KERB_KEY_LIST_REP(ASN1_Packet):
+ ASN1_codec = ASN1_Codecs.BER
+ ASN1_root = ASN1F_SEQUENCE_OF(
+ "keys",
+ [],
+ ASN1F_PACKET("", None, EncryptionKey),
+ )
+
+
+_PADATA_CLASSES[162] = KERB_KEY_LIST_REP
+
+# [MS-KILE] sect 2.2.13
+
+
+class KERB_SUPERSEDED_BY_USER(ASN1_Packet):
+ ASN1_codec = ASN1_Codecs.BER
+ ASN1_root = ASN1F_SEQUENCE(
+ ASN1F_PACKET("name", None, PrincipalName, explicit_tag=0xA0),
+ Realm("realm", None, explicit_tag=0xA1),
+ )
+
+
+# [MS-KILE] sect 2.2.14
+
+
+class KERB_DMSA_KEY_PACKAGE(ASN1_Packet):
+ ASN1_codec = ASN1_Codecs.BER
+ ASN1_root = ASN1F_SEQUENCE(
+ ASN1F_SEQUENCE_OF(
+ "currentKeys",
+ [],
+ ASN1F_PACKET("", None, EncryptionKey),
+ explicit_tag=0xA0,
+ ),
+ ASN1F_optional(
+ ASN1F_SEQUENCE_OF(
+ "previousKeys",
+ [],
+ ASN1F_PACKET("", None, EncryptionKey),
+ explicit_tag=0xA0,
+ ),
+ ),
+ KerberosTime("expirationInterval", GeneralizedTime(), explicit_tag=0xA2),
+ KerberosTime("fetchInterval", GeneralizedTime(), explicit_tag=0xA4),
+ )
+
# RFC6113 sect 5.4.1
@@ -1737,7 +1802,8 @@ class _KRBERROR_data_Field(_ASN1FString_PacketField):
# 24: KDC_ERR_PREAUTH_FAILED
# 25: KDC_ERR_PREAUTH_REQUIRED
return MethodData(val[0].val, _underlayer=pkt), val[1]
- elif pkt.errorCode.val in [18, 29, 41, 60]:
+ elif pkt.errorCode.val in [13, 18, 29, 41, 60]:
+ # 13: KDC_ERR_BADOPTION
# 18: KDC_ERR_CLIENT_REVOKED
# 29: KDC_ERR_SVC_UNAVAILABLE
# 41: KRB_AP_ERR_MODIFIED
@@ -2444,6 +2510,7 @@ class KerberosClient(Automaton):
additional_tickets=[],
u2u=False,
for_user=None,
+ s4u2proxy=False,
etypes=None,
key=None,
port=88,
@@ -2519,6 +2586,7 @@ class KerberosClient(Automaton):
self.additional_tickets = additional_tickets # U2U + S4U2Proxy
self.u2u = u2u # U2U
self.for_user = for_user # FOR-USER
+ self.s4u2proxy = s4u2proxy # S4U2Proxy
self.key = key
# See RFC4120 - sect 7.2.2
# This marks whether we should follow-up after an EOF
@@ -2674,6 +2742,20 @@ class KerberosClient(Automaton):
)
)
+ # [MS-SFU] S4U2proxy - sect 3.1.5.2.1
+ if self.s4u2proxy:
+ # "PA-PAC-OPTIONS with resource-based constrained-delegation bit set"
+ tgsreq.root.padata.append(
+ PADATA(
+ padataType=ASN1_INTEGER(167), # PA-PAC-OPTIONS
+ padataValue=PA_PAC_OPTIONS(
+ options="Resource-based-constrained-delegation",
+ ),
+ )
+ )
+ # "kdc-options field: MUST include the new cname-in-addl-tkt options flag"
+ kdc_req.kdcOptions.set(14, 1)
+
# Compute checksum
if self.key.cksumtype:
authenticator.cksum = Checksum()
@@ -2901,6 +2983,7 @@ def krb_tgs_req(
u2u=False,
etypes=None,
for_user=None,
+ s4u2proxy=False,
**kwargs,
):
r"""
@@ -2950,6 +3033,7 @@ def krb_tgs_req(
u2u=u2u,
etypes=etypes,
for_user=for_user,
+ s4u2proxy=s4u2proxy,
**kwargs,
)
cli.run()
@@ -3233,6 +3317,9 @@ class KerberosSSP(SSP):
self.RecvSignKeyUsage = 23
super(KerberosSSP.CONTEXT, self).__init__(req_flags=req_flags)
+ def clifailure(self):
+ self.__init__(self.IsAcceptor, req_flags=self.flags)
+
def __repr__(self):
if self.U2U:
return "KerberosSSP-U2U"
diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py
index 427b11d3..49e2d0fd 100644
--- a/scapy/layers/l2.py
+++ b/scapy/layers/l2.py
@@ -15,7 +15,7 @@ import time
from scapy.ansmachine import AnsweringMachine
from scapy.arch import get_if_addr, get_if_hwaddr
from scapy.base_classes import Gen, Net
-from scapy.compat import chb, orb
+from scapy.compat import chb
from scapy.config import conf
from scapy import consts
from scapy.data import ARPHDR_ETHER, ARPHDR_LOOPBACK, ARPHDR_METRICOM, \
@@ -59,8 +59,20 @@ from scapy.plist import (
_PacketList,
)
from scapy.sendrecv import sendp, srp, srp1, srploop
-from scapy.utils import checksum, hexdump, hexstr, inet_ntoa, inet_aton, \
- mac2str, valid_mac, valid_net, valid_net6
+from scapy.utils import (
+ checksum,
+ hexdump,
+ hexstr,
+ in4_getnsmac,
+ in4_ismaddr,
+ inet_aton,
+ inet_ntoa,
+ mac2str,
+ pretty_list,
+ valid_mac,
+ valid_net,
+ valid_net6,
+)
# Typing imports
from typing import (
@@ -121,19 +133,36 @@ _arp_cache = conf.netcache.new_cache("arp_cache", 120)
@conf.commands.register
def getmacbyip(ip, chainCC=0):
# type: (str, int) -> Optional[str]
- """Return MAC address corresponding to a given IP address"""
+ """
+ Returns the MAC address used to reach a given IP address.
+
+ This will follow the routing table and will issue an ARP request if
+ necessary. Special cases (multicast, etc.) are also handled.
+
+ .. seealso:: :func:`~scapy.layers.inet6.getmacbyip6` for IPv6.
+ """
+ # Sanitize the IP
if isinstance(ip, Net):
ip = next(iter(ip))
ip = inet_ntoa(inet_aton(ip or "0.0.0.0"))
- tmp = [orb(e) for e in inet_aton(ip)]
- if (tmp[0] & 0xf0) == 0xe0: # mcast @
- return "01:00:5e:%.2x:%.2x:%.2x" % (tmp[1] & 0x7f, tmp[2], tmp[3])
+
+ # Multicast
+ if in4_ismaddr(ip): # mcast @
+ mac = in4_getnsmac(inet_aton(ip))
+ return mac
+
+ # Check the routing table
iff, _, gw = conf.route.route(ip)
+
+ # Broadcast case
if (iff == conf.loopback_name) or (ip in conf.route.get_if_bcast(iff)):
return "ff:ff:ff:ff:ff:ff"
+
+ # An ARP request is necessary
if gw != "0.0.0.0":
ip = gw
+ # Check the cache
mac = _arp_cache.get(ip)
if mac:
return mac
@@ -987,18 +1016,20 @@ class ARPingResult(SndRcvList):
"""
Print the list of discovered MAC addresses.
"""
-
- data = list()
- padding = 0
+ data = list() # type: List[Tuple[str | List[str], ...]]
for s, r in self.res:
manuf = conf.manufdb._get_short_manuf(r.src)
manuf = "unknown" if manuf == r.src else manuf
- padding = max(padding, len(manuf))
data.append((r[Ether].src, manuf, r[ARP].psrc))
- for src, manuf, psrc in data:
- print(" %-17s %-*s %s" % (src, padding, manuf, psrc))
+ print(
+ pretty_list(
+ data,
+ [("src", "manuf", "psrc")],
+ sortBy=2,
+ )
+ )
@conf.commands.register
diff --git a/scapy/layers/llmnr.py b/scapy/layers/llmnr.py
index 9872e417..1f328287 100644
--- a/scapy/layers/llmnr.py
+++ b/scapy/layers/llmnr.py
@@ -110,6 +110,16 @@ DestIP6Field.bind_addr(LLMNRResponse, _LLMNR_IPv6_mcast_Addr, dport=5355)
class LLMNR_am(DNS_am):
+ """
+ LLMNR answering machine.
+
+ This has the same arguments as DNS_am. See help(DNS_am)
+
+ Example::
+
+ >>> llmnrd(joker="192.168.0.2", iface="eth0")
+ >>> llmnrd(match={"TEST": "192.168.0.2"})
+ """
function_name = "llmnrd"
filter = "udp port 5355"
cls = LLMNRQuery
diff --git a/scapy/layers/msrpce/rpcclient.py b/scapy/layers/msrpce/rpcclient.py
index 251202e1..f377fd20 100644
--- a/scapy/layers/msrpce/rpcclient.py
+++ b/scapy/layers/msrpce/rpcclient.py
@@ -310,6 +310,7 @@ class DCERPC_Client(object):
)
if status not in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]:
# Authentication failed.
+ self.sspcontext.clifailure()
return False
resp = self.sr1(
reqcls(context_elem=self.get_bind_context(interface)),
diff --git a/scapy/layers/ntlm.py b/scapy/layers/ntlm.py
index 11eb4703..12cc8bf5 100644
--- a/scapy/layers/ntlm.py
+++ b/scapy/layers/ntlm.py
@@ -957,6 +957,7 @@ def MD4le(x):
def RC4Init(key):
"""Alleged RC4"""
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
+
try:
# cryptography > 43.0
from cryptography.hazmat.decrepit.ciphers import (
@@ -981,6 +982,7 @@ def RC4K(key, data):
RC4 algorithm.
"""
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
+
try:
# cryptography > 43.0
from cryptography.hazmat.decrepit.ciphers import (
@@ -1226,6 +1228,9 @@ class NTLMSSP(SSP):
self.IsAcceptor = IsAcceptor
super(NTLMSSP.CONTEXT, self).__init__(req_flags=req_flags)
+ def clifailure(self):
+ self.__init__(self.IsAcceptor, req_flags=self.flags)
+
def __repr__(self):
return "NTLMSSP"
@@ -1432,8 +1437,8 @@ class NTLMSSP(SSP):
"running in standalone !"
)
if not chall_tok or NTLM_CHALLENGE not in chall_tok:
- chall_tok.show()
- raise ValueError("NTLMSSP: Unexpected token. Expected NTLM Challenge")
+ log_runtime.debug("NTLMSSP: Unexpected token. Expected NTLM Challenge")
+ return Context, None, GSS_S_DEFECTIVE_TOKEN
# Take a default token
tok = NTLM_AUTHENTICATE_V2(
NegotiateFlags=chall_tok.NegotiateFlags,
@@ -1564,8 +1569,8 @@ class NTLMSSP(SSP):
# Server: challenge (val=negotiate)
nego_tok = val
if not nego_tok or NTLM_NEGOTIATE not in nego_tok:
- nego_tok.show()
- raise ValueError("NTLMSSP: Unexpected token. Expected NTLM Negotiate")
+ log_runtime.debug("NTLMSSP: Unexpected token. Expected NTLM Negotiate")
+ return Context, None, GSS_S_DEFECTIVE_TOKEN
# Take a default token
currentTime = (time.time() + 11644473600) * 1e7
tok = NTLM_CHALLENGE(
@@ -1654,10 +1659,10 @@ class NTLMSSP(SSP):
# server: OK or challenge again (val=auth)
auth_tok = val
if not auth_tok or NTLM_AUTHENTICATE_V2 not in auth_tok:
- auth_tok.show()
- raise ValueError(
+ log_runtime.debug(
"NTLMSSP: Unexpected token. Expected NTLM Authenticate v2"
)
+ return Context, None, GSS_S_DEFECTIVE_TOKEN
if self.DO_NOT_CHECK_LOGIN:
# Just trust me bro
return Context, None, GSS_S_COMPLETE
@@ -1778,7 +1783,7 @@ class NTLMSSP(SSP):
if auth_tok.DomainNameLen:
domain = auth_tok.DomainName
else:
- domain = self.DOMAIN_NB_NAME
+ domain = ""
if self.IDENTITIES and username in self.IDENTITIES:
ResponseKeyNT = NTOWFv2(
None, username, domain, HashNt=self.IDENTITIES[username]
@@ -1802,7 +1807,7 @@ class NTLMSSP(SSP):
if auth_tok.DomainNameLen:
domain = auth_tok.DomainName
else:
- domain = self.DOMAIN_NB_NAME
+ domain = ""
if username in self.IDENTITIES:
ResponseKeyNT = NTOWFv2(
None, username, domain, HashNt=self.IDENTITIES[username]
diff --git a/scapy/layers/sixlowpan.py b/scapy/layers/sixlowpan.py
index fd624771..22bf979c 100644
--- a/scapy/layers/sixlowpan.py
+++ b/scapy/layers/sixlowpan.py
@@ -1095,7 +1095,7 @@ class SixLoWPAN(Packet):
@classmethod
def dispatch_hook(cls, _pkt=b"", *args, **kargs):
- """Depending on the payload content, the frame type we should interpretate""" # noqa: E501
+ """Depending on the payload content, the frame type we should interpret"""
if _pkt and len(_pkt) >= 1:
fb = ord(_pkt[:1])
if fb == 0x41:
diff --git a/scapy/layers/smb.py b/scapy/layers/smb.py
index 34c1c1f1..89f19a8c 100644
--- a/scapy/layers/smb.py
+++ b/scapy/layers/smb.py
@@ -5,7 +5,11 @@
# Copyright (C) Gabriel Potter
"""
-SMB (Server Message Block), also known as CIFS.
+SMB 1.0 (Server Message Block), also known as CIFS.
+
+.. note::
+ You will find more complete documentation for this layer over at
+ `SMB <https://scapy.readthedocs.io/en/latest/layers/smb.html>`_
Specs:
diff --git a/scapy/layers/smb2.py b/scapy/layers/smb2.py
index e22698af..0d7d3ce1 100644
--- a/scapy/layers/smb2.py
+++ b/scapy/layers/smb2.py
@@ -4265,7 +4265,7 @@ class SMBSession(DefaultSession):
@crypto_validator
def computeSMBSessionKey(self):
- if not self.sspcontext.SessionKey:
+ if not getattr(self.sspcontext, "SessionKey", None):
# no signing key, no session key
return
# [MS-SMB2] sect 3.3.5.5.3
diff --git a/scapy/layers/smbclient.py b/scapy/layers/smbclient.py
index f63f0956..01553a7a 100644
--- a/scapy/layers/smbclient.py
+++ b/scapy/layers/smbclient.py
@@ -570,7 +570,7 @@ class SMB_Client(Automaton):
pass
elif SMB2_Error_Response in pkt:
# Authentication failure
- self.session.sspcontext = None
+ self.session.sspcontext.clifailure()
# Reset Session preauth (SMB 3.1.1)
self.session.SessionPreauthIntegrityHashValue = None
if not self.RETRY:
diff --git a/scapy/layers/spnego.py b/scapy/layers/spnego.py
index 89197948..d7a596e1 100644
--- a/scapy/layers/spnego.py
+++ b/scapy/layers/spnego.py
@@ -557,6 +557,9 @@ class SPNEGOSSP(SSP):
self.supported_mechtypes = force_supported_mechtypes
super(SPNEGOSSP.CONTEXT, self).__init__(req_flags=req_flags)
+ def clifailure(self):
+ self.sub_context.clifailure()
+
def __getattr__(self, attr):
try:
return object.__getattribute__(self, attr)
diff --git a/scapy/layers/tls/crypto/cipher_block.py b/scapy/layers/tls/crypto/cipher_block.py
index ee64fe1b..cca1387c 100644
--- a/scapy/layers/tls/crypto/cipher_block.py
+++ b/scapy/layers/tls/crypto/cipher_block.py
@@ -17,10 +17,21 @@ if conf.crypto_valid:
from cryptography.utils import (
CryptographyDeprecationWarning,
)
- from cryptography.hazmat.primitives.ciphers import (Cipher, algorithms, modes, # noqa: E501
- BlockCipherAlgorithm,
- CipherAlgorithm)
+ from cryptography.hazmat.primitives.ciphers import (
+ BlockCipherAlgorithm,
+ Cipher,
+ CipherAlgorithm,
+ algorithms,
+ modes,
+ )
from cryptography.hazmat.backends.openssl.backend import backend
+ try:
+ # cryptography > 43.0
+ from cryptography.hazmat.decrepit.ciphers import (
+ algorithms as decrepit_algorithms,
+ )
+ except ImportError:
+ decrepit_algorithms = algorithms
_tls_block_cipher_algs = {}
@@ -133,7 +144,7 @@ _sslv2_block_cipher_algs = {}
if conf.crypto_valid:
class Cipher_DES_CBC(_BlockCipher):
- pc_cls = algorithms.TripleDES
+ pc_cls = decrepit_algorithms.TripleDES
pc_cls_mode = modes.CBC
block_size = 8
key_len = 8
@@ -151,7 +162,7 @@ if conf.crypto_valid:
key_len = 5
class Cipher_3DES_EDE_CBC(_BlockCipher):
- pc_cls = algorithms.TripleDES
+ pc_cls = decrepit_algorithms.TripleDES
pc_cls_mode = modes.CBC
block_size = 8
key_len = 24
@@ -165,13 +176,13 @@ if conf.crypto_valid:
category=CryptographyDeprecationWarning)
class Cipher_IDEA_CBC(_BlockCipher):
- pc_cls = algorithms.IDEA
+ pc_cls = decrepit_algorithms.IDEA
pc_cls_mode = modes.CBC
block_size = 8
key_len = 16
class Cipher_SEED_CBC(_BlockCipher):
- pc_cls = algorithms.SEED
+ pc_cls = decrepit_algorithms.SEED
pc_cls_mode = modes.CBC
block_size = 16
key_len = 16
diff --git a/scapy/layers/tls/crypto/cipher_stream.py b/scapy/layers/tls/crypto/cipher_stream.py
index bbd6bbd0..5c95fadd 100644
--- a/scapy/layers/tls/crypto/cipher_stream.py
+++ b/scapy/layers/tls/crypto/cipher_stream.py
@@ -14,6 +14,13 @@ from scapy.layers.tls.crypto.common import CipherError
if conf.crypto_valid:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
from cryptography.hazmat.backends import default_backend
+ try:
+ # cryptography > 43.0
+ from cryptography.hazmat.decrepit.ciphers import (
+ algorithms as decrepit_algorithms,
+ )
+ except ImportError:
+ decrepit_algorithms = algorithms
_tls_stream_cipher_algs = {}
@@ -103,7 +110,7 @@ class _StreamCipher(metaclass=_StreamCipherMetaclass):
if conf.crypto_valid:
class Cipher_RC4_128(_StreamCipher):
- pc_cls = algorithms.ARC4
+ pc_cls = decrepit_algorithms.ARC4
key_len = 16
class Cipher_RC4_40(Cipher_RC4_128):
diff --git a/scapy/layers/tls/handshake.py b/scapy/layers/tls/handshake.py
index f767c15d..d60fdb2f 100644
--- a/scapy/layers/tls/handshake.py
+++ b/scapy/layers/tls/handshake.py
@@ -404,7 +404,7 @@ class TLS13ClientHello(_TLSHandshake):
# For a resumed PSK, the hash function use
# to compute the binder must be the same
# as the one used to establish the original
- # conntection. For that, we assume that
+ # connection. For that, we assume that
# the ciphersuite associate with the ticket
# is given as argument to tlsSession
# (see layers/tls/automaton_cli.py for an
diff --git a/scapy/py.typed b/scapy/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/scapy/py.typed
diff --git a/scapy/supersocket.py b/scapy/supersocket.py
index ca3aebf2..1f967044 100644
--- a/scapy/supersocket.py
+++ b/scapy/supersocket.py
@@ -102,6 +102,11 @@ class SuperSocket(metaclass=_SuperSocket_metaclass):
def send(self, x):
# type: (Packet) -> int
+ """Sends a `Packet` object
+
+ :param x: `Packet` to be send
+ :return: Number of bytes that have been sent
+ """
sx = raw(x)
try:
x.sent_time = time.time()
@@ -116,7 +121,12 @@ class SuperSocket(metaclass=_SuperSocket_metaclass):
if WINDOWS:
def _recv_raw(self, sock, x):
# type: (socket.socket, int) -> Tuple[bytes, Any, Optional[float]]
- """Internal function to receive a Packet"""
+ """Internal function to receive a Packet.
+
+ :param sock: Socket object from which data are received
+ :param x: Number of bytes to be received
+ :return: Received bytes, address information and no timestamp
+ """
pkt, sa_ll = sock.recvfrom(x)
return pkt, sa_ll, None
else:
@@ -124,6 +134,10 @@ class SuperSocket(metaclass=_SuperSocket_metaclass):
# type: (socket.socket, int) -> Tuple[bytes, Any, Optional[float]]
"""Internal function to receive a Packet,
and process ancillary data.
+
+ :param sock: Socket object from which data are received
+ :param x: Number of bytes to be received
+ :return: Received bytes, address information and an optional timestamp
"""
timestamp = None
if not self.auxdata_available:
@@ -172,11 +186,22 @@ class SuperSocket(metaclass=_SuperSocket_metaclass):
def recv_raw(self, x=MTU):
# type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501
- """Returns a tuple containing (cls, pkt_data, time)"""
+ """Returns a tuple containing (cls, pkt_data, time)
+
+
+ :param x: Maximum number of bytes to be received, defaults to MTU
+ :return: A tuple, consisting of a Packet type, the received data,
+ and a timestamp
+ """
return conf.raw_layer, self.ins.recv(x), None
def recv(self, x=MTU, **kwargs):
# type: (int, **Any) -> Optional[Packet]
+ """Receive a Packet according to the `basecls` of this socket
+
+ :param x: Maximum number of bytes to be received, defaults to MTU
+ :return: The received `Packet` object, or None
+ """
cls, val, ts = self.recv_raw(x)
if not val or not cls:
return None
@@ -200,6 +225,8 @@ class SuperSocket(metaclass=_SuperSocket_metaclass):
def close(self):
# type: () -> None
+ """Gracefully close this socket
+ """
if self.closed:
return
self.closed = True
@@ -213,11 +240,15 @@ class SuperSocket(metaclass=_SuperSocket_metaclass):
def sr(self, *args, **kargs):
# type: (Any, Any) -> Tuple[SndRcvList, PacketList]
+ """Send and Receive multiple packets
+ """
from scapy import sendrecv
return sendrecv.sndrcv(self, *args, **kargs)
def sr1(self, *args, **kargs):
# type: (Any, Any) -> Optional[Packet]
+ """Send one packet and receive one answer
+ """
from scapy import sendrecv
ans = sendrecv.sndrcv(self, *args, **kargs)[0] # type: SndRcvList
if len(ans) > 0:
diff --git a/scapy/utils.py b/scapy/utils.py
index c6624c87..c612e79b 100644
--- a/scapy/utils.py
+++ b/scapy/utils.py
@@ -704,13 +704,22 @@ def zerofree_randstring(length):
for _ in range(length))
+def stror(s1, s2):
+ # type: (bytes, bytes) -> bytes
+ """
+ Returns the binary OR of the 2 provided strings s1 and s2. s1 and s2
+ must be of same length.
+ """
+ return b"".join(map(lambda x, y: struct.pack("!B", x | y), s1, s2))
+
+
def strxor(s1, s2):
# type: (bytes, bytes) -> bytes
"""
Returns the binary XOR of the 2 provided strings s1 and s2. s1 and s2
must be of same length.
"""
- return b"".join(map(lambda x, y: chb(orb(x) ^ orb(y)), s1, s2))
+ return b"".join(map(lambda x, y: struct.pack("!B", x ^ y), s1, s2))
def strand(s1, s2):
@@ -719,7 +728,7 @@ def strand(s1, s2):
Returns the binary AND of the 2 provided strings s1 and s2. s1 and s2
must be of same length.
"""
- return b"".join(map(lambda x, y: chb(orb(x) & orb(y)), s1, s2))
+ return b"".join(map(lambda x, y: struct.pack("!B", x & y), s1, s2))
def strrot(s1, count, right=True):
@@ -819,6 +828,93 @@ def itom(x):
return (0xffffffff00000000 >> x) & 0xffffffff
+def in4_cidr2mask(m):
+ # type: (int) -> bytes
+ """
+ Return the mask (bitstring) associated with provided length
+ value. For instance if function is called on 20, return value is
+ b'\xff\xff\xf0\x00'.
+ """
+ if m > 32 or m < 0:
+ raise Scapy_Exception("value provided to in4_cidr2mask outside [0, 32] domain (%d)" % m) # noqa: E501
+
+ return strxor(
+ b"\xff" * 4,
+ struct.pack(">I", 2**(32 - m) - 1)
+ )
+
+
+def in4_isincluded(addr, prefix, mask):
+ # type: (str, str, int) -> bool
+ """
+ Returns True when 'addr' belongs to prefix/mask. False otherwise.
+ """
+ temp = inet_pton(socket.AF_INET, addr)
+ pref = in4_cidr2mask(mask)
+ zero = inet_pton(socket.AF_INET, prefix)
+ return zero == strand(temp, pref)
+
+
+def in4_ismaddr(str):
+ # type: (str) -> bool
+ """
+ Returns True if provided address in printable format belongs to
+ allocated Multicast address space (224.0.0.0/4).
+ """
+ return in4_isincluded(str, "224.0.0.0", 4)
+
+
+def in4_ismlladdr(str):
+ # type: (str) -> bool
+ """
+ Returns True if address belongs to link-local multicast address
+ space (224.0.0.0/24)
+ """
+ return in4_isincluded(str, "224.0.0.0", 24)
+
+
+def in4_ismgladdr(str):
+ # type: (str) -> bool
+ """
+ Returns True if address belongs to global multicast address
+ space (224.0.1.0-238.255.255.255).
+ """
+ return (
+ in4_isincluded(str, "224.0.0.0", 4) and
+ not in4_isincluded(str, "224.0.0.0", 24) and
+ not in4_isincluded(str, "239.0.0.0", 8)
+ )
+
+
+def in4_ismlsaddr(str):
+ # type: (str) -> bool
+ """
+ Returns True if address belongs to limited scope multicast address
+ space (239.0.0.0/8).
+ """
+ return in4_isincluded(str, "239.0.0.0", 8)
+
+
+def in4_isaddrllallnodes(str):
+ # type: (str) -> bool
+ """
+ Returns True if address is the link-local all-nodes multicast
+ address (224.0.0.1).
+ """
+ return (inet_pton(socket.AF_INET, "224.0.0.1") ==
+ inet_pton(socket.AF_INET, str))
+
+
+def in4_getnsmac(a):
+ # type: (bytes) -> str
+ """
+ Return the multicast mac address associated with provided
+ IPv4 address. Passed address must be in network format.
+ """
+
+ return "01:00:5e:%.2x:%.2x:%.2x" % (a[1] & 0x7f, a[2], a[3])
+
+
def decode_locale_str(x):
# type: (bytes) -> str
"""
diff --git a/scapy/utils6.py b/scapy/utils6.py
index 5343cdd7..5bc00e46 100644
--- a/scapy/utils6.py
+++ b/scapy/utils6.py
@@ -17,7 +17,11 @@ from scapy.base_classes import Net
from scapy.data import IPV6_ADDR_GLOBAL, IPV6_ADDR_LINKLOCAL, \
IPV6_ADDR_SITELOCAL, IPV6_ADDR_LOOPBACK, IPV6_ADDR_UNICAST,\
IPV6_ADDR_MULTICAST, IPV6_ADDR_6TO4, IPV6_ADDR_UNSPECIFIED
-from scapy.utils import strxor
+from scapy.utils import (
+ strxor,
+ stror,
+ strand,
+)
from scapy.compat import orb, chb
from scapy.pton_ntop import inet_pton, inet_ntop
from scapy.volatile import RandMAC, RandBin
@@ -591,18 +595,6 @@ def in6_isanycast(x): # RFC 2526
return False
-def _in6_bitops(xa1, xa2, operator=0):
- # type: (bytes, bytes, int) -> bytes
- a1 = struct.unpack('4I', xa1)
- a2 = struct.unpack('4I', xa2)
- fop = [lambda x, y: x | y,
- lambda x, y: x & y,
- lambda x, y: x ^ y
- ]
- ret = map(fop[operator % len(fop)], a1, a2)
- return b"".join(struct.pack('I', x) for x in ret)
-
-
def in6_or(a1, a2):
# type: (bytes, bytes) -> bytes
"""
@@ -610,7 +602,7 @@ def in6_or(a1, a2):
passed in network format. Return value is also an IPv6 address
in network format.
"""
- return _in6_bitops(a1, a2, 0)
+ return stror(a1, a2)
def in6_and(a1, a2):
@@ -620,7 +612,7 @@ def in6_and(a1, a2):
passed in network format. Return value is also an IPv6 address
in network format.
"""
- return _in6_bitops(a1, a2, 1)
+ return strand(a1, a2)
def in6_xor(a1, a2):
@@ -630,7 +622,7 @@ def in6_xor(a1, a2):
passed in network format. Return value is also an IPv6 address
in network format.
"""
- return _in6_bitops(a1, a2, 2)
+ return strxor(a1, a2)
def in6_cidr2mask(m):
diff --git a/test/answering_machines.uts b/test/answering_machines.uts
index b1bcdfe2..a9a8517a 100644
--- a/test/answering_machines.uts
+++ b/test/answering_machines.uts
@@ -126,6 +126,71 @@ assert DNS_am().make_reply(
Ether()/IP()/UDP()/DNS(b'q\xa04\x00\x00\xa0\x01\x00\xf3\x00\x01\x04\x01y')
) is None
+= LLMNR_am
+def check_LLMNR_am_am_reply(packet):
+ assert packet[Ether].src == get_if_hwaddr(conf.iface)
+ assert packet[Ether].dst == "aa:aa:aa:aa:aa:aa"
+ assert packet[IP].src == get_if_addr(conf.iface)
+ assert packet[IP].dst == "192.168.0.1"
+ assert packet[UDP].dport == 51938
+ assert packet[UDP].sport == 5355
+ assert LLMNRResponse in packet and packet[LLMNRResponse].ancount == 1 and packet[LLMNRResponse].qdcount == 1
+ assert packet[LLMNRResponse].qd[0].qname == b"TEST."
+ assert packet[LLMNRResponse].an[0].rdata == "192.168.1.1"
+ assert packet[LLMNRResponse].an[0].rrname == b"TEST."
+ assert packet[LLMNRResponse].an[0].ttl == 60
+
+test_am(LLMNR_am,
+ Ether(src="aa:aa:aa:aa:aa:aa", dst="01:00:5e:00:00:fc")/IP(src="192.168.0.1", dst="224.0.0.252")/UDP(dport=5355, sport=51938)/LLMNRQuery(qd=DNSQR(qname=b"TEST.", qtype="A")),
+ check_LLMNR_am_am_reply,
+ ttl=60,
+ match={"TEST": "192.168.1.1"})
+
+= mDNS_am
+def check_mDNS_am_reply(packet):
+ packet.show()
+ # assert packet[Ether].src == get_if_hwaddr(conf.iface)
+ assert packet[Ether].dst == "01:00:5e:00:00:fb"
+ # assert packet[IP].src == get_if_addr(conf.iface)
+ assert packet[IP].dst == "224.0.0.251"
+ assert packet[IP].ttl == 255
+ assert packet[UDP].dport == 5353
+ assert packet[UDP].sport == 5353
+ assert DNS in packet and packet[DNS].ancount == 1 and packet[DNS].qdcount == 0
+ assert packet[DNS].an[0].rdata == "192.168.1.1"
+ assert packet[DNS].an[0].rrname == b"TEST.local."
+ assert packet[DNS].an[0].ttl == 10
+
+test_am(mDNS_am,
+ Ether(src="aa:aa:aa:aa:aa:aa", dst="01:00:5e:00:00:fb")/IP(src="192.168.0.1", dst="224.0.0.251", ttl=1)/UDP(dport=5353, sport=5353)/DNS(qd=DNSQR(qname=b"TEST.local.", qtype="A")),
+ check_mDNS_am_reply,
+ joker="192.168.1.1")
+
+
+def check_mDNS_am_reply2(packet):
+ # $ avahi-resolve -n bonjour.local
+ packet.show()
+ # assert packet[Ether].src == get_if_hwaddr(conf.iface)
+ assert packet[Ether].dst == "01:00:5e:00:00:fb"
+ # assert packet[IP].src == get_if_addr(conf.iface)
+ assert packet[IP].dst == "224.0.0.251"
+ assert packet[IP].ttl == 255
+ assert packet[UDP].dport == 5353
+ assert packet[UDP].sport == 5353
+ assert DNS in packet and packet[DNS].ancount == 2 and packet[DNS].qdcount == 0
+ assert packet[DNS].an[0].rdata == "192.168.1.1"
+ assert packet[DNS].an[0].rrname == b"bonjour.local."
+ assert packet[DNS].an[0].ttl == 120
+ assert packet[DNS].an[1].type == 47
+ assert packet[DNS].an[1].rrname == b"bonjour.local."
+ assert packet[DNS].an[1].ttl == 120
+
+test_am(mDNS_am,
+ Ether(b'\x01\x00^\x00\x00\xfb\xaa\xaa\xaa\xaa\xaa\xaa\x08\x00E\x00\x00A\xce}@\x00\xff\x11\x0b\x89\xc0\xa8\x00\x01\xe0\x00\x00\xfb\x14\xe9\x14\xe9\x00-\xdbl\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x07bonjour\x05local\x00\x00\x01\x00\x01\xc0\x0c\x00\x1c\x00\x01'),
+ check_mDNS_am_reply2,
+ joker="192.168.1.1",
+ ttl=120)
+
= DHCPv6_am - Basic Instantiaion
~ osx netaccess
a = DHCPv6_am()
diff --git a/test/regression.uts b/test/regression.uts
index 868409a3..0f36e4d6 100644
--- a/test/regression.uts
+++ b/test/regression.uts
@@ -532,6 +532,28 @@ assert (Ether() / ARP()).route()[0] is not None
assert (Ether() / ARP()).payload.route()[0] is not None
assert (ARP(ptype=0, pdst="hello. this isn't a valid IP")).route()[0] is None
+= utils/in4_is*
+
+assert in4_ismaddr("224.0.0.1")
+assert not in4_ismaddr("192.168.0.1")
+assert in4_ismaddr("239.0.0.255")
+
+assert in4_ismlladdr("224.0.0.1")
+assert in4_ismlladdr("224.0.0.255")
+assert not in4_ismlladdr("224.0.1.255")
+
+assert in4_ismgladdr("235.0.0.1")
+assert not in4_ismgladdr("224.0.0.1")
+assert not in4_ismgladdr("239.0.0.1")
+
+assert in4_ismlsaddr("239.0.0.1")
+assert not in4_ismlsaddr("224.0.0.1")
+
+assert in4_isaddrllallnodes("224.0.0.1")
+assert not in4_isaddrllallnodes("224.0.0.3")
+
+assert in4_getnsmac(b'\xe0\x00\x00\x01') == '01:00:5e:00:00:01'
+assert getmacbyip("224.0.0.1") == '01:00:5e:00:00:01'
= plain_str test
@@ -944,7 +966,7 @@ random.seed(0x2807)
zerofree_randstring(4) in [b"\xd2\x12\xe4\x5b", b'\xd3\x8b\x13\x12']
= Test strand function
-assert strand("AC", "BC") == b'@C'
+assert strand(b"AC", b"BC") == b'@C'
= Test export_object and import_object functions
import mock
@@ -1462,7 +1484,7 @@ assert pkt.command() == "DNS(qd=[DNSQR(qname=b'google.com.', qtype=1)])"
= Test json() with nested packet
-assert pkt.json() == '{"length": null, "id": 0, "qr": 0, "opcode": 0, "aa": 0, "tc": 0, "rd": 1, "ra": 0, "z": 0, "ad": 0, "cd": 0, "rcode": 0, "qdcount": null, "ancount": null, "nscount": null, "arcount": null, "qd": [{"qname": "google.com.", "qtype": 1, "qclass": 1}]}'
+assert pkt.json() == '{"length": null, "id": 0, "qr": 0, "opcode": 0, "aa": 0, "tc": 0, "rd": 1, "ra": 0, "z": 0, "ad": 0, "cd": 0, "rcode": 0, "qdcount": null, "ancount": null, "nscount": null, "arcount": null, "qd": [{"qname": "google.com.", "qtype": 1, "unicastresponse": 0, "qclass": 1}]}'
= Test command() with ASN.1 packet
diff --git a/test/scapy/layers/dns.uts b/test/scapy/layers/dns.uts
index c4d0ab93..e2912ac4 100644
--- a/test/scapy/layers/dns.uts
+++ b/test/scapy/layers/dns.uts
@@ -124,8 +124,8 @@ pkt = DNS(qr=1, qd=[], aa=1, rd=1)
pkt.an = [
DNSRR(type=12, rrname='_raop._tcp.local.', rdata='140C768FFE28@Freebox Server._raop._tcp.local.'),
DNSRR(rrname='140C768FFE28@Freebox Server._raop._tcp.local.', type=16, rdata=[b'txtvers=1', b'vs=190.9', b'ch=2', b'sr=44100', b'ss=16', b'pw=false', b'et=0,1', b'ek=1', b'tp=TCP,UDP', b'am=FreeboxServer1,2', b'cn=0,1,2,3', b'md=0,2', b'sf=0x44', b'ft=0xBF0A00', b'sv=false', b'da=true', b'vn=65537', b'vv=2']),
- DNSRRSRV(rrname='140C768FFE28@Freebox Server._raop._tcp.local.', target='Freebox-Server-3.local.', port=5000, type=33, rclass=32769),
- DNSRR(rrname='Freebox-Server-3.local.', rdata='192.168.0.254', rclass=32769, type=1, ttl=120),
+ DNSRRSRV(rrname='140C768FFE28@Freebox Server._raop._tcp.local.', target='Freebox-Server-3.local.', port=5000, type=33, cacheflush=1, rclass=1),
+ DNSRR(rrname='Freebox-Server-3.local.', rdata='192.168.0.254', cacheflush=1, rclass=1, type=1, ttl=120),
]
pkt = DNS(raw(pkt))
diff --git a/test/scapy/layers/http.uts b/test/scapy/layers/http.uts
index c4ae211a..5d5a4642 100644
--- a/test/scapy/layers/http.uts
+++ b/test/scapy/layers/http.uts
@@ -221,7 +221,7 @@ assert int(pkts[7][HTTP].Content_Length.decode()) == len(pkts[7][Raw].load)
pkt = TCP()/HTTP()/HTTPRequest(Method=b'GET', Path=b'/download', Http_Version=b'HTTP/1.1', Accept=b'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', Accept_Encoding=b'gzip, deflate', Accept_Language=b'en-US,en;q=0.5', Cache_Control=b'max-age=0', Connection=b'keep-alive', Host=b'scapy.net', User_Agent=b'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0')
raw_pkt = raw(pkt)
raw_pkt
-assert raw_pkt == b'\x00P\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x00\x00\x00\x00GET /download HTTP/1.1\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: en-US,en;q=0.5\r\nCache-Control: max-age=0\r\nConnection: keep-alive\r\nHost: scapy.net\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0\r\n\r\n'
+assert raw_pkt == b'\x00P\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x00\x00\x00\x00GET /download HTTP/1.1\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: en-US,en;q=0.5\r\nCache-Control: max-age=0\r\nConnection: keep-alive\r\nContent-Length: 0\r\nHost: scapy.net\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0\r\n\r\n'
= HTTP 1.1 -> HTTP 2.0 Upgrade (h2c)
~ Test h2c
@@ -257,3 +257,99 @@ conf.contribs["http"]["auto_compression"] = True
c = sniff(offline=[xa, xb], session=TCPSession)[0]
import gzip
assert gzip.decompress(z) == c.load
+
++ Test HTTP client/server
+
+= Util function to launch HTTP_server
+~ http-client
+
+from scapy.layers.http import HTTP_Server, HTTP_AUTH_MECHS
+
+class run_httpserver:
+ def __init__(self, mech=None, ssp=None, **kwargs):
+ self.server = None
+ self.mech = mech
+ self.ssp = ssp
+ self.kwargs = kwargs
+ def __enter__(self):
+ print("@ Starting http server")
+ # Start server
+ self.server = HTTP_Server.spawn(
+ 8080,
+ iface=conf.loopback_name,
+ mech=self.mech, ssp=self.ssp,
+ bg=True,
+ **self.kwargs,
+ )
+ # wait for it to start
+ for i in range(10):
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(1)
+ try:
+ sock.connect(("127.0.0.1", 8080))
+ break
+ except Exception:
+ time.sleep(0.5)
+ finally:
+ sock.close()
+ else:
+ raise TimeoutError
+ print("@ Server started !")
+ def __exit__(self, exc_type, exc_value, traceback):
+ print("@ Stopping http server !")
+ try:
+ self.server.shutdown(socket.SHUT_RDWR)
+ except OSError:
+ pass
+ self.server.close()
+ if traceback:
+ # failed
+ print("\nTest failed.")
+ raise traceback
+ print("@ http server stopped !")
+
+
+= HTTP_client fails to ask HTTP_server that required authentication
+~ http-client
+
+from scapy.layers.http import HTTP_Client
+
+with run_httpserver(mech=HTTP_AUTH_MECHS.NTLM, ssp=NTLMSSP(IDENTITIES={"user": MD4le("password")})):
+ client = HTTP_Client()
+ resp = client.request("http://127.0.0.1:8080")
+ client.close()
+
+assert resp.Status_Code == b"401"
+
+= HTTP_client asks HTTP_server with NTLMSSP
+~ http-client
+
+from scapy.layers.http import HTTP_Client
+
+with run_httpserver(mech=HTTP_AUTH_MECHS.NTLM, ssp=NTLMSSP(IDENTITIES={"user": MD4le("password")})):
+ client = HTTP_Client(
+ HTTP_AUTH_MECHS.NTLM,
+ ssp=NTLMSSP(UPN="user", PASSWORD="password"),
+ )
+ resp = client.request("http://127.0.0.1:8080")
+ client.close()
+
+assert resp.load == b'<!doctype html><html><body><h1>OK</h1></body></html>'
+
+= HTTP_Server with native python client with Basic auth
+~ http-client
+
+import urllib.request
+from scapy.layers.http import HTTP_Client
+
+# https://docs.python.org/3/howto/urllib2.html#id5 (this is so complicated...)
+password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
+password_mgr.add_password(None, '127.0.0.1:8080', "user", "password")
+handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
+opener = urllib.request.build_opener(handler)
+
+with run_httpserver(mech=HTTP_AUTH_MECHS.BASIC, BASIC_IDENTITIES={"user": "password"}):
+ with opener.open('http://127.0.0.1:8080/') as f:
+ html = f.read().decode('utf-8')
+
+assert html == "<!doctype html><html><body><h1>OK</h1></body></html>"
diff --git a/test/scapy/layers/l2.uts b/test/scapy/layers/l2.uts
index d4d51856..4f055747 100644
--- a/test/scapy/layers/l2.uts
+++ b/test/scapy/layers/l2.uts
@@ -25,7 +25,7 @@ with ContextManagerCaptureOutput() as cmco:
ar.show()
result_ar = cmco.get_output()
-assert result_ar.startswith(" 70:ee:50:50:ee:70 Netatmo 192.168.0.1")
+assert "70:ee:50:50:ee:70 Netatmo 192.168.0.1" in result_ar
= arp_mitm - IP to IP
~ arp_mitm
diff --git a/test/scapy/layers/smb2.uts b/test/scapy/layers/smb2.uts
index 80fe34e2..f15db4ed 100644
--- a/test/scapy/layers/smb2.uts
+++ b/test/scapy/layers/smb2.uts
@@ -52,7 +52,7 @@ assert pkt[NBTSession].TYPE == 0x00 # session message
smb2 = pkt[SMB2_Header]
assert smb2.Start == b'\xfeSMB'
-+ SMB2 Negotiate Procotol Request Header dissecting
++ SMB2 Negotiate Protocol Request Header dissecting
= Common fields in header
@@ -209,7 +209,7 @@ pkt = IP() / TCP() / NBTSession() / SMB2_Header() / SMB2_Negotiate_Protocol_Resp
pkt = IP(raw(pkt))
assert SMB2_Negotiate_Protocol_Response in pkt
-+ SMB2 Negotiate Procotol Request Header with 1 dialect
++ SMB2 Negotiate Protocol Request Header with 1 dialect
= Common fields in header