Skip to content

Commit ce7596d

Browse files
authored
Auto-Argparse for smb{client,server} (#4313)
1 parent a05013c commit ce7596d

File tree

4 files changed

+145
-21
lines changed

4 files changed

+145
-21
lines changed

doc/scapy/layers/smb.rst

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ You might be wondering if you can pass the ``HashNT`` of the password of the use
9292
9393
If you pay very close attention, you'll notice that in this case we aren't using the :class:`~scapy.layers.spnego.SPNEGOSSP` wrapper. You could have used ``ssp=SPNEGOSSP([t.ssp(1)])``.
9494

95+
.. note::
96+
97+
It is also possible to start the :class:`~scapy.layers.smbclient.smbclient` directly from the OS, using the following::
98+
99+
$ python3 -m scapy.layers.smbclient server1.domain.local [email protected]
100+
101+
Use ``python3 -m scapy.layers.smbclient -h`` to see the list of available options.
102+
103+
95104
Programmatically
96105
________________
97106

@@ -201,17 +210,17 @@ It's also accessible as the ``ins`` attribute of a ``SMB_SOCKET``, or the ``sock
201210
SMB 2/3 server
202211
--------------
203212

204-
Scapy provides a SMB 2/3 server Automaton: :class:`~scapy.layers.smbclient.SMB_Server`
213+
Scapy provides a SMB 2/3 server Automaton: :class:`~scapy.layers.smbserver.SMB_Server`
205214

206215
.. image:: ../graphics/smb/smb_server.png
207216
:align: center
208217

209-
Once again, Scapy provides high level :class:`~scapy.layers.smbclient.smbserver` class that allows to spawn a SMB server.
218+
Once again, Scapy provides high level :class:`~scapy.layers.smbserver.smbserver` class that allows to spawn a SMB server.
210219

211-
High-Level :class:`~scapy.layers.smbclient.smbserver`
220+
High-Level :class:`~scapy.layers.smbserver.smbserver`
212221
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
213222

214-
The :class:`~scapy.layers.smbclient.smbserver` class allows to spawn a SMB server serving a selection of shares.
223+
The :class:`~scapy.layers.smbserver.smbserver` class allows to spawn a SMB server serving a selection of shares.
215224
A share is identified by a ``name`` and a ``path`` (+ an optional description called ``remark``).
216225

217226
**Start a SMB server with NTLM auth for 2 users:**
@@ -271,10 +280,19 @@ A share is identified by a ``name`` and a ``path`` (+ an optional description ca
271280
),
272281
)
273282
274-
Low-Level :class:`~scapy.layers.smbclient.SMB_Server`
283+
.. note::
284+
285+
It is possible to start the :class:`~scapy.layers.smbserver.smbserver` (albeit only in unauthenticated mode) directly from the OS, using the following::
286+
287+
$ python3 -m scapy.layers.smbserver --port 12345
288+
289+
Use ``python3 -m scapy.layers.smbserver -h`` to see the list of available options.
290+
291+
292+
Low-Level :class:`~scapy.layers.smbserver.SMB_Server`
275293
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
276294

277-
To change the functionality of the :class:`~scapy.layers.smbclient.SMB_Server`, you shall extend the server class (which is an automaton) and provide additional custom conditions (or overwrite existing ones).
295+
To change the functionality of the :class:`~scapy.layers.smbserver.SMB_Server`, you shall extend the server class (which is an automaton) and provide additional custom conditions (or overwrite existing ones).
278296

279297
.. code:: python
280298

scapy/layers/smbclient.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ def send_data(self, d):
450450

451451
class SMB_SOCKET(SuperSocket):
452452
"""
453-
High-level wrapper over SMB_Client.smblink that provides some basic SMB
453+
Mid-level wrapper over SMB_Client.smblink that provides some basic SMB
454454
client functions, such as tree connect, directory query, etc.
455455
"""
456456

@@ -827,19 +827,19 @@ class smbclient(CLIUtil):
827827

828828
def __init__(
829829
self,
830-
target,
831-
UPN=None,
832-
password=None,
830+
target: str,
831+
UPN: str = None,
832+
password: str = None,
833+
guest: bool = False,
834+
kerberos: bool = True,
835+
kerberos_required: bool = False,
836+
HashNt: str = None,
837+
port: int = 445,
838+
timeout: int = 2,
839+
debug: int = 0,
833840
ssp=None,
834-
guest=False,
835-
kerberos=True,
836-
kerberos_required=False,
837-
HashNt=None,
838841
ST=None,
839842
KEY=None,
840-
port=445,
841-
timeout=2,
842-
debug=0,
843843
cli=True,
844844
):
845845
if cli:
@@ -1196,7 +1196,7 @@ def _lfs_complete(self, arg, cond):
11961196
eltpar, eltname = self._parsepath(arg, remote=False)
11971197
eltpar = self.localpwd / eltpar
11981198
return [
1199-
self.normalize_path(eltpar / x)
1199+
str(x.relative_to(self.localpwd))
12001200
for x in eltpar.glob("*")
12011201
if (x.name.lower().startswith(eltname.lower()) and cond(x))
12021202
]
@@ -1471,3 +1471,8 @@ def rm_complete(self, file):
14711471
if self._require_share(silent=True):
14721472
return []
14731473
return self._fs_complete(file)
1474+
1475+
1476+
if __name__ == "__main__":
1477+
from scapy.utils import AutoArgparse
1478+
AutoArgparse(smbclient)

scapy/layers/smbserver.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,9 +1698,9 @@ class smbserver:
16981698
def __init__(
16991699
self,
17001700
shares=None,
1701-
iface=None,
1702-
port=445,
1703-
verb=2,
1701+
iface: str = None,
1702+
port: int = 445,
1703+
verb: int = 2,
17041704
# SMB arguments
17051705
ssp=None,
17061706
**kwargs,
@@ -1744,3 +1744,8 @@ def close(self):
17441744
if self.srv:
17451745
self.srv.shutdown(socket.SHUT_RDWR)
17461746
self.srv.close()
1747+
1748+
1749+
if __name__ == "__main__":
1750+
from scapy.utils import AutoArgparse
1751+
AutoArgparse(smbserver)

scapy/utils.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from itertools import zip_longest
1414

1515
import array
16+
import argparse
1617
import collections
1718
import decimal
1819
import difflib
@@ -3544,6 +3545,101 @@ def loop(self, debug: int = 0) -> None:
35443545
print("Output processor failed with error: %s" % ex)
35453546

35463547

3548+
def AutoArgparse(func: DecoratorCallable) -> None:
3549+
"""
3550+
Generate an Argparse call from a function, then call this function.
3551+
3552+
Notes:
3553+
3554+
- for the arguments to have a description, the sphinx docstring format
3555+
must be used. See
3556+
https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html
3557+
- the arguments must be typed in Python (we ignore Sphinx-specific types)
3558+
untyped arguments are ignored.
3559+
- only types that would be supported by argparse are supported. The others
3560+
are omitted.
3561+
"""
3562+
argsdoc = {}
3563+
if func.__doc__:
3564+
# Sphinx doc format parser
3565+
m = re.match(
3566+
r"((?:.|\n)*?)(\n\s*:(?:param|type|raises|return|rtype)(?:.|\n)*)",
3567+
func.__doc__.strip(),
3568+
)
3569+
if not m:
3570+
desc = func.__doc__.strip()
3571+
else:
3572+
desc = m.group(1)
3573+
sphinxargs = re.findall(
3574+
r"\s*:(param|type|raises|return|rtype)\s*([^:]*):(.*)",
3575+
m.group(2),
3576+
)
3577+
for argtype, argparam, argdesc in sphinxargs:
3578+
argparam = argparam.strip()
3579+
argdesc = argdesc.strip()
3580+
if argtype == "param":
3581+
if not argparam:
3582+
raise ValueError(":param: without a name !")
3583+
argsdoc[argparam] = argdesc
3584+
else:
3585+
desc = ""
3586+
# Now build the argparse.ArgumentParser
3587+
parser = argparse.ArgumentParser(
3588+
prog=func.__name__,
3589+
description=desc,
3590+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
3591+
)
3592+
# Process the parameters
3593+
positional = []
3594+
for param in inspect.signature(func).parameters.values():
3595+
if not param.annotation:
3596+
continue
3597+
parname = param.name
3598+
paramkwargs = {}
3599+
if param.annotation is bool:
3600+
if param.default is True:
3601+
parname = "no-" + parname
3602+
paramkwargs["action"] = "store_false"
3603+
else:
3604+
paramkwargs["action"] = "store_true"
3605+
elif param.annotation in [str, int, float]:
3606+
paramkwargs["type"] = param.annotation
3607+
else:
3608+
continue
3609+
if param.default != inspect.Parameter.empty:
3610+
if param.kind == inspect.Parameter.POSITIONAL_ONLY:
3611+
positional.append(param.name)
3612+
paramkwargs["nargs"] = '?'
3613+
else:
3614+
parname = "--" + parname
3615+
paramkwargs["default"] = param.default
3616+
else:
3617+
positional.append(param.name)
3618+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
3619+
paramkwargs["action"] = "append"
3620+
if param.name in argsdoc:
3621+
paramkwargs["help"] = argsdoc[param.name]
3622+
parser.add_argument(parname, **paramkwargs) # type: ignore
3623+
# Now parse the sys.argv parameters
3624+
params = vars(parser.parse_args())
3625+
# Act as in interactive mode
3626+
conf.logLevel = 20
3627+
from scapy.themes import DefaultTheme
3628+
conf.color_theme = DefaultTheme()
3629+
# And call the function
3630+
try:
3631+
func(
3632+
*[params.pop(x) for x in positional],
3633+
**{
3634+
(k[3:] if k.startswith("no_") else k): v
3635+
for k, v in params.items()
3636+
}
3637+
)
3638+
except AssertionError as ex:
3639+
print("ERROR: " + str(ex))
3640+
parser.print_help()
3641+
3642+
35473643
#######################
35483644
# PERIODIC SENDER #
35493645
#######################

0 commit comments

Comments
 (0)