Skip to content

Commit bba4ec0

Browse files
authored
Improve keytab/ccache support (#4935)
* Improve keytab/ccache support * Autoargparse: Compat with enums * LDAPHero: minor improvements * Ticketer++: better number of tickets * CLIUtil: improve autocompletion for multi-arg * Fix PEP8 problems
1 parent c3c62ec commit bba4ec0

File tree

12 files changed

+530
-95
lines changed

12 files changed

+530
-95
lines changed

doc/scapy/layers/kerberos.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,23 @@ As you can see, DMSA keys were imported in the keytab. You can use those as deta
238238
239239
No tickets in CCache.
240240
241+
- **Create server keytab:**
242+
243+
The following is equivalent to Windows' ``ktpass.exe /out kt.keytab /mapuser WKS02$@domain.local /princ host/WKS02.domain.local@domain.local /pass ScapyIsNice``.
244+
245+
.. code:: pycon
246+
247+
>>> t = Ticketer()
248+
>>> t.add_cred("host/WKS02.domain.local@domain.local", etypes="all", mapupn="WKS02$@domain.local", password="ScapyIsNice")
249+
Enter password: ************
250+
>>> t.show()
251+
Keytab name: UNSAVED
252+
Principal Timestamp KVNO Keytype
253+
host/WKS02$.domain.local@domain.local 25/02/26 15:40:27 1 AES256-CTS-HMAC-SHA1-96
254+
255+
No tickets in CCache.
256+
>>> t.save_keytab("kt.keytab")
257+
241258
- **Change password using kpasswd in 'set' mode:**
242259

243260
.. code:: pycon
@@ -370,6 +387,10 @@ Cheat sheet
370387
+---------------------------------------+--------------------------------+
371388
| ``t.renew(i, [...])`` | Renew a TGT/ST |
372389
+---------------------------------------+--------------------------------+
390+
| ``t.remove_krb(i)`` | Remove a TGT/ST |
391+
+---------------------------------------+--------------------------------+
392+
| ``t.set_primary(i)`` | Set the primary ticket |
393+
+---------------------------------------+--------------------------------+
373394

374395
Other useful commands
375396
---------------------

scapy/compat.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
# See https://scapy.net/ for more information
44

55
"""
6-
Python 2 and 3 link classes.
6+
Compatibility module to various older versions of Python
77
"""
88

99
import base64
1010
import binascii
11+
import enum
1112
import struct
1213
import sys
1314

@@ -39,14 +40,15 @@
3940
'orb',
4041
'plain_str',
4142
'raw',
43+
'StrEnum',
4244
]
4345

4446
# Typing compatibility
4547

4648
# Note:
4749
# supporting typing on multiple python versions is a nightmare.
4850
# we provide a FakeType class to be able to use types added on
49-
# later Python versions (since we run mypy on 3.12), on older
51+
# later Python versions (since we run mypy on 3.14), on older
5052
# ones.
5153

5254

@@ -100,6 +102,15 @@ class Protocol:
100102
else:
101103
Self = _FakeType("Self")
102104

105+
106+
# Python 3.11 Only
107+
if sys.version_info >= (3, 11):
108+
from enum import StrEnum
109+
else:
110+
class StrEnum(str, enum.Enum):
111+
pass
112+
113+
103114
###########
104115
# Python3 #
105116
###########

scapy/layers/kerberos.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3033,7 +3033,9 @@ class KerberosClient(Automaton):
30333033
:param dmsa: sets the 'unconditional delegation' mode for DMSA TGT retrieval
30343034
"""
30353035

3036-
RES_AS_MODE = namedtuple("AS_Result", ["asrep", "sessionkey", "kdcrep", "upn"])
3036+
RES_AS_MODE = namedtuple(
3037+
"AS_Result", ["asrep", "sessionkey", "kdcrep", "upn", "pa_type"]
3038+
)
30373039
RES_TGS_MODE = namedtuple("TGS_Result", ["tgsrep", "sessionkey", "kdcrep", "upn"])
30383040

30393041
class MODE(IntEnum):
@@ -3232,6 +3234,7 @@ def __init__(
32323234
self.fast_req_sent = False
32333235
# Session parameters
32343236
self.pre_auth = False
3237+
self.pa_type = None # preauth-type that's used
32353238
self.fast_rep = None
32363239
self.fast_error = None
32373240
self.fast_skey = None # The random subkey used for fast
@@ -3574,8 +3577,9 @@ def as_req(self):
35743577
)
35753578

35763579
# Build PA-DATA
3580+
self.pa_type = 16 # PA-PK-AS-REQ
35773581
pafactor = PADATA(
3578-
padataType=16, # PA-PK-AS-REQ
3582+
padataType=self.pa_type,
35793583
padataValue=PA_PK_AS_REQ(
35803584
signedAuthpack=signedAuthpack,
35813585
trustedCertifiers=None,
@@ -3596,15 +3600,17 @@ def as_req(self):
35963600
b"clientchallengearmor",
35973601
b"challengelongterm",
35983602
)
3603+
self.pa_type = 138 # PA-ENCRYPTED-CHALLENGE
35993604
pafactor = PADATA(
3600-
padataType=138, # PA-ENCRYPTED-CHALLENGE
3605+
padataType=self.pa_type,
36013606
padataValue=EncryptedData(),
36023607
)
36033608
else:
36043609
# Usual 'timestamp' factor
36053610
ts_key = self.key
3611+
self.pa_type = 2 # PA-ENC-TIMESTAMP
36063612
pafactor = PADATA(
3607-
padataType=2, # PA-ENC-TIMESTAMP
3613+
padataType=self.pa_type,
36083614
padataValue=EncryptedData(),
36093615
)
36103616
pafactor.padataValue.encrypt(
@@ -4078,6 +4084,7 @@ def decrypt_as_rep(self, pkt):
40784084
res.key.toKey(),
40794085
res,
40804086
pkt.root.getUPN(),
4087+
self.pa_type,
40814088
)
40824089

40834090
@ATMT.receive_condition(SENT_TGS_REQ)

scapy/layers/ldap.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
import struct
2727
import uuid
2828

29-
from enum import Enum
30-
3129
from scapy.arch import get_if_addr
3230
from scapy.ansmachine import AnsweringMachine
3331
from scapy.asn1.asn1 import (
@@ -62,6 +60,7 @@
6260
)
6361
from scapy.asn1packet import ASN1_Packet
6462
from scapy.config import conf
63+
from scapy.compat import StrEnum
6564
from scapy.error import log_runtime
6665
from scapy.fields import (
6766
FieldLenField,
@@ -90,6 +89,7 @@
9089
GSS_C_FLAGS,
9190
GSS_C_NO_CHANNEL_BINDINGS,
9291
GSS_S_COMPLETE,
92+
GSS_S_CONTINUE_NEEDED,
9393
GssChannelBindings,
9494
SSP,
9595
_GSSAPI_Field,
@@ -106,6 +106,7 @@
106106
Any,
107107
Dict,
108108
List,
109+
Optional,
109110
Union,
110111
)
111112

@@ -1642,8 +1643,8 @@ def dclocator(
16421643
#####################
16431644

16441645

1645-
class LDAP_BIND_MECHS(Enum):
1646-
NONE = "UNAUTHENTICATED"
1646+
class LDAP_BIND_MECHS(StrEnum):
1647+
NONE = "ANONYMOUS"
16471648
SIMPLE = "SIMPLE"
16481649
SASL_GSSAPI = "GSSAPI"
16491650
SASL_GSS_SPNEGO = "GSS-SPNEGO"
@@ -1949,8 +1950,8 @@ def bind(
19491950
self,
19501951
mech,
19511952
ssp=None,
1952-
sign=False,
1953-
encrypt=False,
1953+
sign: Optional[bool] = None,
1954+
encrypt: Optional[bool] = None,
19541955
simple_username=None,
19551956
simple_password=None,
19561957
):
@@ -1966,6 +1967,12 @@ def bind(
19661967
:
19671968
This acts differently based on the :mech: provided during initialization.
19681969
"""
1970+
# Bind default values: if NTLM then encrypt, else sign unless anonymous/simple
1971+
if encrypt is None:
1972+
encrypt = mech == LDAP_BIND_MECHS.SICILY
1973+
if sign is None and not encrypt:
1974+
sign = mech not in [LDAP_BIND_MECHS.NONE, LDAP_BIND_MECHS.SIMPLE]
1975+
19691976
# Store and check consistency
19701977
self.mech = mech
19711978
self.ssp = ssp # type: SSP
@@ -2000,6 +2007,9 @@ def bind(
20002007
elif mech in [LDAP_BIND_MECHS.NONE, LDAP_BIND_MECHS.SIMPLE]:
20012008
if self.sign or self.encrypt:
20022009
raise ValueError("Cannot use 'sign' or 'encrypt' with NONE or SIMPLE !")
2010+
else:
2011+
raise ValueError("Mech %s is still unimplemented !" % mech)
2012+
20032013
if self.ssp is not None and mech in [
20042014
LDAP_BIND_MECHS.NONE,
20052015
LDAP_BIND_MECHS.SIMPLE,
@@ -2105,6 +2115,10 @@ def bind(
21052115
),
21062116
chan_bindings=self.chan_bindings,
21072117
)
2118+
if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]:
2119+
raise RuntimeError(
2120+
"%s: GSS_Init_sec_context failed !" % self.mech.name,
2121+
)
21082122
while token:
21092123
resp = self.sr1(
21102124
LDAP_BindRequest(
@@ -2116,10 +2130,10 @@ def bind(
21162130
)
21172131
)
21182132
if not isinstance(resp.protocolOp, LDAP_BindResponse):
2119-
if self.verb:
2120-
print("%s bind failed !" % self.mech.name)
2121-
resp.show()
2122-
return
2133+
raise LDAP_Exception(
2134+
"%s bind failed !" % self.mech.name,
2135+
resp=resp,
2136+
)
21232137
val = resp.protocolOp.serverSaslCredsData
21242138
if not val:
21252139
status = resp.protocolOp.resultCode
@@ -2195,11 +2209,20 @@ def bind(
21952209
"GSSAPI SASL failed to negotiate client security flags !",
21962210
resp=resp,
21972211
)
2212+
2213+
# If we use SPNEGO and NTLMSSP was used, understand we can't use sign
2214+
if self.mech == LDAP_BIND_MECHS.SASL_GSS_SPNEGO:
2215+
from scapy.layers.ntlm import NTLMSSP
2216+
2217+
if isinstance(self.sspcontext.ssp, NTLMSSP):
2218+
self.sign = False
2219+
21982220
# SASL wrapping is now available.
21992221
self.sasl_wrap = self.encrypt or self.sign
22002222
if self.sasl_wrap:
22012223
self.sock.closed = True # prevent closing by marking it as already closed.
22022224
self.sock = StreamSocket(self.sock.ins, LDAP_SASL_Buffer)
2225+
22032226
# Success.
22042227
if self.verb:
22052228
print("%s bind succeeded !" % self.mech.name)
@@ -2460,3 +2483,4 @@ def close(self):
24602483
print("X Connection closed\n")
24612484
self.sock.close()
24622485
self.bound = False
2486+
self.sspcontext = None

scapy/layers/smb.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ class SMBNegotiate_Request(Packet):
286286

287287
bind_layers(SMB_Header, SMBNegotiate_Request, Command=0x72)
288288

289-
# SMBNegociate Protocol Response
289+
# SMBNegotiate Protocol Response
290290

291291

292292
def _SMBStrNullField(name, default):

scapy/layers/smbclient.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,7 +1323,7 @@ def shares_output(self, results):
13231323
"""
13241324
print(pretty_list(results, [("ShareName", "ShareType", "Comment")]))
13251325

1326-
@CLIUtil.addcommand(spaces=True)
1326+
@CLIUtil.addcommand(mono=True)
13271327
def use(self, share):
13281328
"""
13291329
Open a share
@@ -1391,7 +1391,7 @@ def _dir_complete(self, arg):
13911391
return [results[0] + "\\"]
13921392
return results
13931393

1394-
@CLIUtil.addcommand(spaces=True)
1394+
@CLIUtil.addcommand(mono=True)
13951395
def ls(self, parent=None):
13961396
"""
13971397
List the files in the remote directory
@@ -1466,7 +1466,7 @@ def ls_complete(self, folder):
14661466
return []
14671467
return self._dir_complete(folder)
14681468

1469-
@CLIUtil.addcommand(spaces=True)
1469+
@CLIUtil.addcommand(mono=True)
14701470
def cd(self, folder):
14711471
"""
14721472
Change the remote current directory
@@ -1534,7 +1534,7 @@ def lls_output(self, results):
15341534
pretty_list(results, [("FileName", "File Size", "Last Modification Time")])
15351535
)
15361536

1537-
@CLIUtil.addcommand(spaces=True)
1537+
@CLIUtil.addcommand(mono=True)
15381538
def lcd(self, folder):
15391539
"""
15401540
Change the local current directory
@@ -1663,7 +1663,7 @@ def _getr(self, directory, _root, _verb=True):
16631663
print(conf.color_theme.red(remote), "->", str(ex))
16641664
return size
16651665

1666-
@CLIUtil.addcommand(spaces=True, globsupport=True)
1666+
@CLIUtil.addcommand(mono=True, globsupport=True)
16671667
def get(self, file, _dest=None, _verb=True, *, r=False):
16681668
"""
16691669
Retrieve a file
@@ -1703,7 +1703,7 @@ def get_complete(self, file):
17031703
return []
17041704
return self._fs_complete(file)
17051705

1706-
@CLIUtil.addcommand(spaces=True, globsupport=True)
1706+
@CLIUtil.addcommand(mono=True, globsupport=True)
17071707
def cat(self, file):
17081708
"""
17091709
Print a file
@@ -1731,7 +1731,7 @@ def cat_complete(self, file):
17311731
return []
17321732
return self._fs_complete(file)
17331733

1734-
@CLIUtil.addcommand(spaces=True, globsupport=True)
1734+
@CLIUtil.addcommand(mono=True, globsupport=True)
17351735
def put(self, file):
17361736
"""
17371737
Upload a file
@@ -1756,7 +1756,7 @@ def put_complete(self, folder):
17561756
"""
17571757
return self._lfs_complete(folder, lambda x: not x.is_dir())
17581758

1759-
@CLIUtil.addcommand(spaces=True)
1759+
@CLIUtil.addcommand(mono=True)
17601760
def rm(self, file):
17611761
"""
17621762
Delete a file
@@ -1799,7 +1799,7 @@ def backup(self):
17991799
print("Backup Intent: On")
18001800
self.extra_create_options.append("FILE_OPEN_FOR_BACKUP_INTENT")
18011801

1802-
@CLIUtil.addcommand(spaces=True)
1802+
@CLIUtil.addcommand(mono=True)
18031803
def watch(self, folder):
18041804
"""
18051805
Watch file changes in folder (recursively)
@@ -1826,7 +1826,7 @@ def watch(self, folder):
18261826
pass
18271827
print("Cancelled.")
18281828

1829-
@CLIUtil.addcommand(spaces=True)
1829+
@CLIUtil.addcommand(mono=True)
18301830
def getsd(self, file):
18311831
"""
18321832
Get the Security Descriptor

0 commit comments

Comments
 (0)