PR #2 added ticket end/renew reporting and expired-ticket skipping. Three
bugs came with it:
* klistwinrm.py called time.time() without importing time -- every dump
died with NameError on the first ticket.
* The skip was nested inside `if info["renew_till"]:`. parse_time() returns
0 for an absent field, so non-renewable tickets were never checked and
expired ones got written anyway -- the exact case the PR set out to catch.
* parse_time() tags klist's "(local)" wallclock as UTC, so its values are
target-local wallclock, not true epochs. Comparing them to time.time()
skewed every check by the UTC offset: valid tickets dropped west of UTC,
expired ones kept east of it.
Expiry now keys off end_time via now_ticket_frame(), which builds "now" in
the same frame parse_time() uses, so no time import is needed. A ticket past
end_time but still within renew_till is written rather than dropped, since
the ccache can be renewed.
Output follows PRTremote's convention: [*]/[X] markers, dot-aligned kv rows,
unmarked list rows, and a closing summary with the KRB5CCNAME hint. Skipped
expired tickets are counted so "0 ccaches written" explains itself instead of
just exiting 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit assumed the CG KerberosKeyWithMetadata blob held a
cleartext key at offset 28 and exported it. It does not: both opaque
regions in the blob are 48 bytes (a 32-byte secret block-aligned +
padding), i.e. the session key is wrapped/encrypted. Under Credential
Guard the key lives in the secure kernel (VTL1) and the unwrap key never
leaves it, so the cleartext key cannot be recovered offline. Feeding the
wrapped bytes into a ccache produced KRB_AP_ERR_BAD_INTEGRITY.
Stop extracting offset-28 bytes on the CG path. Instead detect the CG
layout and refuse with a clear message (klist2ccache exits non-zero;
klistremote/klistwinrm skip the account) so the failure is loud rather
than a plausible-looking but unusable ccache. Non-CG SYSTEM blobs and
raw-key paths are unchanged. README updated to explain the limitation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Under Credential Guard, klist emits the session key as a marshalled
KerberosKeyWithMetadata blob: self-size@0, typename-length@8,
typename-offset@12 pointing at the literal "KerberosKeyWithMetadata",
key payload@28, then the typename + metadata tail.
The old parser read the etype from offset 8, which in the CG blob is the
typename length (23 == 0x17, colliding with RC4) rather than the real
etype. That yielded a wrong key type and a truncated 16-byte key,
producing tickets that failed with BAD_INTEGRITY.
Detect the CG layout by checking that the offset-8/12 fields frame the
embedded "KerberosKeyWithMetadata" string, and in that case take the
etype from the "KeyType 0x.." header line instead of offset 8. Legacy
SYSTEM metadata blobs and raw-key paths are unchanged. Applied to all
three tools; prints a notice when the CG path triggers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- klistwinrm.py: new tool that lists/dumps Kerberos TGTs via WinRM
instead of Task Scheduler + SMB; supports all auth types (NTLM,
PTH, Kerberos, AES key, keytab) plus -ssl/-port options
- requirements.txt: pywinrm + impacket deps; requests-ntlm2 noted for PTH
- klistremote.py + klistwinrm.py: add --computer flag to include
machine account sessions (Kerberos:Network, e.g. HOSTNAME$)
- README: document all three tools with usage examples and auth matrix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>