Merge pull request #26 from mandiant/kerberos-proxy-leak

kerberos, dcerpc: tunnel KDC traffic through pkg/transport
This commit is contained in:
Jacob Paullus
2026-05-11 23:24:03 -05:00
committed by GitHub
113 changed files with 11381 additions and 193 deletions
+21
View File
@@ -94,6 +94,27 @@ When reporting, please include:
**Status:** By design. UDP tunneling over SOCKS5 is not a gopacket goal. If your workflow genuinely needs UDP over a proxy, use `proxychains` (which hooks libc at a lower level and can intercept UDP sockets) or a full VPN instead of `-proxy`.
### Kerberos KDC traffic under `-proxy`
All Kerberos KDC traffic (AS-REQ/AS-REP, TGS-REQ/TGS-REP, kpasswd) is tunneled through `-proxy` alongside the rest of the tool. This is enforced two ways:
1. The embedded gokrb5 library has been forked in-tree at `pkg/third_party/gokrb5`. Every client constructor (`NewWithPassword`, `NewWithKeytab`, `NewFromCCache`) takes a required `KDCDialer` as its first argument, making proxy-bypass a compile error rather than a runtime leak. Production call sites all pass `kerberos.TransportKDCDialer{}`, which delegates to `pkg/transport`.
2. The synthesized krb5 config in `pkg/kerberos` sets `udp_preference_limit = 1` unconditionally, so KRB5 never even attempts UDP/88 — direct and proxied paths behave identically. Modern KRB5 already needs TCP for PAC-bearing TGS responses; the legacy UDP "optimization" is dropped.
`/etc/krb5.conf` and `$KRB5_CONFIG` are intentionally not consulted; the in-memory config is the only one used. A misconfigured host cannot subvert the proxy/DNS guarantees by re-enabling `dns_lookup_kdc` or lowering `udp_preference_limit`.
If you add a new code path that constructs a gokrb5 client, the type system will require a `KDCDialer`. Use `kerberos.TransportKDCDialer{}`; the alternative `client.DirectDialer{}` exists only as an explicit escape hatch.
### DCERPC Kerberos traffic under `-proxy` (secretsdump DCSync, wmiexec, wmiquery, wmipersist, dcomexec)
The DCERPC Kerberos auth path uses a separate Kerberos library (`oiweiwei/gokrb5.fork/v9` reached via `oiweiwei/go-msrpc/ssp/krb5`) because go-msrpc's RPC machinery embeds it. Three things are required to keep that path leak-free, and the codebase enforces all three at every call site:
1. **KDC dialer on the krb5 config.** `pkg/dcerpc/auth_kerberos.go` and the four DCOM tools set `krbConfig.KDCDialer = kerberos.TransportKDCDialer{}` on every `krb5.Config` they construct. Same dialer, same proxy guarantee as the rest of Kerberos.
2. **DCERPC transport dialer.** Every `dcerpc.Dial(...)` is passed `dcerpc.WithDialer(transport.ContextDialer{})` so the TCP step honors the proxy.
3. **StringBinding form for the OXID-pivot dial.** The second `dcerpc.Dial` per tool uses `"ncacn_ip_tcp:" + target.Host`, not the bare hostname. The prefix forces go-msrpc to parse the address as a StringBinding instead of triggering its hard-coded pre-dial `net.LookupIP`, which would otherwise leak the target hostname to the operator's local resolver. The FQDN is then handed verbatim to the SOCKS5 dialer so resolution stays proxy-side.
Operator-visible behavior under `-proxy`: every byte of AD attack traffic (Kerberos, SMB, DCERPC, kpasswd, DCSync DRSUAPI, DCOM activation, WMI) is sent through the configured SOCKS5 proxy. Tcpdump on the operator host shows no traffic to the AD subnet at all — only SOCKS5 frames to the proxy. Verified end-to-end in a GOAD lab with secretsdump DCSync extracting `krbtgt`, wmiexec returning `whoami` output, and getTGT/getST/GetUserSPNs/GetNPUsers exchanging with the KDC — all silent on the wire, while the same getTGT without `-proxy` immediately emits direct SYN packets to the KDC.
---
## 7. Remaining Gaps (Low Priority)
+12
View File
@@ -35,3 +35,15 @@ the full license text.
A vendored SMB2/SMB3 client library originally authored by Hiroshi Ioka
and contributors, distributed under the BSD 3-Clause License.
Upstream: https://github.com/hirochachacha/go-smb2
* pkg/third_party/gokrb5/
A vendored Kerberos 5 client library originally authored by Jonathan
Turner and contributors, distributed under the Apache License, Version
2.0. See pkg/third_party/gokrb5/LICENSE for the full text. The vendored
copy has been modified by this project: the gokrb5 client constructors
(NewWithPassword, NewWithKeytab, NewFromCCache) now require a KDCDialer
as their first argument, KDC TCP/UDP I/O routes through the supplied
dialer instead of net.DialTimeout, and unused subpackages (service/,
spnego/) have been removed. See the commit log and KNOWN_ISSUES.md for
the full change record.
Upstream: https://github.com/jcmturner/gokrb5
+5 -5
View File
@@ -9,8 +9,11 @@ require (
github.com/go-ldap/ldap/v3 v3.4.12
github.com/google/gopacket v1.1.19
github.com/google/uuid v1.6.0
github.com/hashicorp/go-uuid v1.0.3
github.com/jcmturner/aescts/v2 v2.0.0
github.com/jcmturner/dnsutils/v2 v2.0.0
github.com/jcmturner/gofork v1.7.6
github.com/jcmturner/gokrb5/v8 v8.4.4
github.com/jcmturner/rpc/v2 v2.0.3
github.com/oiweiwei/go-msrpc v1.2.12
github.com/oiweiwei/gokrb5.fork/v9 v9.0.6
github.com/rs/zerolog v1.32.0
@@ -23,12 +26,9 @@ require (
require (
github.com/Azure/go-ntlmssp v0.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/indece-official/go-ebcdic v1.2.0 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/oiweiwei/go-smb2.fork v1.0.0 // indirect
+11
View File
@@ -33,6 +33,7 @@ import (
"github.com/oiweiwei/gokrb5.fork/v9/iana/flags"
"github.com/mandiant/gopacket/internal/build"
"github.com/mandiant/gopacket/pkg/kerberos"
"github.com/mandiant/gopacket/pkg/session"
)
@@ -70,6 +71,14 @@ func NewKerberosAuthHandler(creds *session.Credentials, target session.Target, d
krbConfig := krb5.NewConfig()
krbConfig.DCEStyle = true // Required for DCE/RPC
// Route every KDC call this auth handler makes through pkg/transport.
// Without this, go-msrpc's krb5 SSP calls oiweiwei/gokrb5.fork/v9's
// client which falls back to net.Dial and leaks the operator IP to the
// KDC — see KNOWN_ISSUES.md §8 (Kerberos KDC traffic under -proxy).
// kerberos.TransportKDCDialer structurally satisfies the v9 KDCDialer
// interface (same Dial signature).
krbConfig.KDCDialer = kerberos.TransportKDCDialer{}
// Check if we should use ccache (no password provided)
if creds.Password == "" && creds.Hash == "" {
// First check KRB5CCNAME environment variable
@@ -195,6 +204,8 @@ func NewKerberosAuthHandlerMultiRealm(creds *session.Credentials, target session
krbConfig := krb5.NewConfig()
krbConfig.DCEStyle = true
// Same proxy-routing requirement as NewKerberosAuthHandler.
krbConfig.KDCDialer = kerberos.TransportKDCDialer{}
// Use ccache-based credential
ccachePath := os.Getenv("KRB5CCNAME")
+10 -5
View File
@@ -20,10 +20,10 @@ import (
"github.com/mandiant/gopacket/pkg/transport"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/config"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/nametype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// GetASREP fetches the AS-REP for a user and returns the hash.
@@ -39,6 +39,11 @@ func GetASREP(username, domain, kdcHost string, format ...string) (string, error
cfg := config.New()
cfg.LibDefaults.DefaultRealm = realm
// Defense-in-depth: this file dials the KDC via transport.Dial directly
// (gokrb5's sendToKDC is never invoked), so the opsec keys are currently
// no-ops here. Stamp them anyway so a future refactor introducing a
// gokrb5 client.Client to this path inherits the proxy/DNS guarantees.
ApplyKrb5OpsecDefaults(cfg)
// Request RC4 (etype 23) for faster cracking - matches Impacket behavior
cfg.LibDefaults.DefaultTktEnctypes = []string{"rc4-hmac"}
cfg.LibDefaults.DefaultTktEnctypeIDs = []int32{23}
@@ -62,7 +67,7 @@ func GetASREP(username, domain, kdcHost string, format ...string) (string, error
}
// Connect to KDC (Port 88)
conn, err := transport.Dial("tcp", fmt.Sprintf("%s:%d", kdcHost, 88))
conn, err := transport.Dial("tcp", FormatKDC(kdcHost, "88"))
if err != nil {
return "", fmt.Errorf("failed to connect to KDC: %v", err)
}
+111 -34
View File
@@ -21,20 +21,119 @@ import (
"encoding/binary"
"encoding/hex"
"fmt"
"net"
"os"
"strings"
"github.com/jcmturner/gokrb5/v8/client"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/gssapi"
"github.com/jcmturner/gokrb5/v8/keytab"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/mandiant/gopacket/internal/build"
"github.com/mandiant/gopacket/pkg/session"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/client"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/config"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/credentials"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/gssapi"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/keytab"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
"github.com/mandiant/gopacket/pkg/transport"
)
// TransportKDCDialer routes every KDC connection (AS, TGS, kpasswd) through
// pkg/transport, so a configured -proxy / ALL_PROXY tunnels Kerberos traffic
// in lockstep with the rest of the tool. Without this, gokrb5's default path
// uses raw net.Dial and leaks the operator's real source IP to the KDC.
//
// Every gokrb5 client constructor in our fork (NewWithPassword, NewWithKeytab,
// NewFromCCache) requires a KDCDialer; passing this type is how the rest of
// the codebase satisfies that contract while keeping the proxy guarantee.
type TransportKDCDialer struct{}
func (TransportKDCDialer) Dial(network, address string) (net.Conn, error) {
return transport.DialTimeout(network, address, 10)
}
// The opsec invariants every gokrb5 config in this project must enforce:
//
// - dns_lookup_realm / dns_lookup_kdc = false → no SRV lookups via the OS
// resolver, which would bypass our KDCDialer.
// - udp_preference_limit = 1 → TCP-only KDC. Modern KRB5 already requires
// TCP for PAC-bearing TGS responses >1500 bytes; forcing it uniformly
// deletes a code path and keeps direct vs proxied behavior identical.
//
// /etc/krb5.conf and $KRB5_CONFIG are intentionally never consulted; the
// in-memory cfg is the only source. ApplyKrb5OpsecDefaults is the choke point
// for programmatic callers; SynthesizeKrb5Config is the choke point for
// string-template callers. Anything else risks drift.
// ApplyKrb5OpsecDefaults stamps the proxy-/DNS-safety invariants onto a
// programmatically-built *config.Config. Call it after config.New() and
// before constructing a gokrb5 client. Safe to call repeatedly.
func ApplyKrb5OpsecDefaults(cfg *config.Config) {
cfg.LibDefaults.UDPPreferenceLimit = 1
cfg.LibDefaults.DNSLookupKDC = false
cfg.LibDefaults.DNSLookupRealm = false
}
// FormatKDC normalizes a "host" or "host:port" string into a canonical
// "host:port" using net.JoinHostPort, which correctly brackets IPv6 literals.
// Required so that callers passing "[::1]:88", bare "::1", "10.0.0.1:8888",
// or "dc.example.com" all produce valid krb5.conf entries — the naïve
// fmt.Sprintf("%s:88", kdc) form mangles every IPv6 case.
//
// A user-supplied port is preserved: "10.0.0.1:8888" stays on 8888. The
// defaultPort applies only when input has no port (bare host) or a malformed
// trailing-colon form ("host:"). This matches operator expectation for
// lab/tunnel routing where the KDC may not be on the canonical 88/464.
func FormatKDC(kdc, defaultPort string) string {
host, port, err := net.SplitHostPort(kdc)
if err != nil {
// Bare host: IPv4 ("10.0.0.1"), hostname ("dc.example.com"),
// unbracketed IPv6 ("::1"), or bracketed-but-portless IPv6 ("[::1]").
// Strip wrapping brackets before re-joining so JoinHostPort doesn't
// double-bracket ("[::1]" → "[[::1]]:88"). TrimPrefix/TrimSuffix is
// idempotent if the brackets aren't present.
host = strings.TrimSuffix(strings.TrimPrefix(kdc, "["), "]")
port = defaultPort
} else if port == "" {
// "host:" — port-less colon-suffix form.
port = defaultPort
}
return net.JoinHostPort(host, port)
}
// SynthesizeKrb5Config builds the in-memory krb5.conf text for callers that
// need only the KDC. For kpasswd callers, use SynthesizeKrb5ConfigWithKpasswd.
// kdc may be a bare host, host:port, or bracketed IPv6 — FormatKDC normalizes.
func SynthesizeKrb5Config(realm, kdc string) string {
return fmt.Sprintf(`
[libdefaults]
default_realm = %s
dns_lookup_realm = false
dns_lookup_kdc = false
udp_preference_limit = 1
[realms]
%s = {
kdc = %s
}
`, realm, realm, FormatKDC(kdc, "88"))
}
// SynthesizeKrb5ConfigWithKpasswd is SynthesizeKrb5Config plus a kpasswd_server
// per-realm line. Both kdc and kpasswdServer accept the same input forms.
func SynthesizeKrb5ConfigWithKpasswd(realm, kdc, kpasswdServer string) string {
return fmt.Sprintf(`
[libdefaults]
default_realm = %s
dns_lookup_realm = false
dns_lookup_kdc = false
udp_preference_limit = 1
[realms]
%s = {
kdc = %s
kpasswd_server = %s
}
`, realm, realm, FormatKDC(kdc, "88"), FormatKDC(kpasswdServer, "464"))
}
// OID for Kerberos V5 (GSSAPI)
var oidKerberos = asn1.ObjectIdentifier{1, 2, 840, 113554, 1, 2, 2}
@@ -102,18 +201,7 @@ func NewClientFromSession(creds *session.Credentials, target session.Target, dcI
realm = ccacheRealm
}
// Create config with the correct realm from ccache
cfgStr := fmt.Sprintf(`
[libdefaults]
default_realm = %s
dns_lookup_realm = false
dns_lookup_kdc = false
[realms]
%s = {
kdc = %s:88
}
`, realm, realm, kdc)
cfg, err := config.NewFromString(cfgStr)
cfg, err := config.NewFromString(SynthesizeKrb5Config(realm, kdc))
if err != nil {
return nil, fmt.Errorf("failed to create krb5 config: %v", err)
}
@@ -126,7 +214,7 @@ func NewClientFromSession(creds *session.Credentials, target session.Target, dcI
}
// Try to create client from ccache (requires TGT)
cl, err := client.NewFromCCache(ccache, cfg)
cl, err := client.NewFromCCache(TransportKDCDialer{}, ccache, cfg)
if err == nil {
krbClient.KrbClient = cl
}
@@ -136,18 +224,7 @@ func NewClientFromSession(creds *session.Credentials, target session.Target, dcI
}
}
// Create config for non-ccache cases
cfgStr := fmt.Sprintf(`
[libdefaults]
default_realm = %s
dns_lookup_realm = false
dns_lookup_kdc = false
[realms]
%s = {
kdc = %s:88
}
`, realm, realm, kdc)
cfg, err := config.NewFromString(cfgStr)
cfg, err := config.NewFromString(SynthesizeKrb5Config(realm, kdc))
if err != nil {
return nil, fmt.Errorf("failed to create krb5 config: %v", err)
}
@@ -164,7 +241,7 @@ func NewClientFromSession(creds *session.Credentials, target session.Target, dcI
if err != nil {
return nil, fmt.Errorf("failed to load keytab %s: %v", creds.Keytab, err)
}
krbClient.KrbClient = client.NewWithKeytab(creds.Username, realm, kt, cfg, client.DisablePAFXFAST(true))
krbClient.KrbClient = client.NewWithKeytab(TransportKDCDialer{}, creds.Username, realm, kt, cfg, client.DisablePAFXFAST(true))
return krbClient, nil
}
@@ -173,7 +250,7 @@ func NewClientFromSession(creds *session.Credentials, target session.Target, dcI
// Check if it's an AES key (64 hex chars)?
// Note: Manual Keytab construction is blocked by unexported keytab.entry in gokrb5 v8.
// For now, treat everything as a password.
krbClient.KrbClient = client.NewWithPassword(creds.Username, realm, creds.Password, cfg, client.DisablePAFXFAST(true))
krbClient.KrbClient = client.NewWithPassword(TransportKDCDialer{}, creds.Username, realm, creds.Password, cfg, client.DisablePAFXFAST(true))
return krbClient, nil
}
return nil, fmt.Errorf("no valid kerberos credentials found (set KRB5CCNAME, provide password, or use -keytab)")
+14 -10
View File
@@ -21,15 +21,15 @@ import (
"strings"
"time"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/crypto"
"github.com/jcmturner/gokrb5/v8/iana"
"github.com/jcmturner/gokrb5/v8/iana/flags"
"github.com/jcmturner/gokrb5/v8/iana/msgtype"
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/iana/patype"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/config"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/flags"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/msgtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/nametype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/patype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// PACRequest holds configuration for a PAC retrieval request.
@@ -48,9 +48,13 @@ type PACRequest struct {
func GetPAC(req *PACRequest) (*PAC, error) {
realm := strings.ToUpper(req.Domain)
// Build kerberos config
// Build kerberos config. Defense-in-depth: networking in this file is
// hand-rolled via sendKDCRequest → transport.Dial, so the opsec keys
// are no-ops here today. Stamp them so future use of a gokrb5 client
// inherits the proxy/DNS guarantees automatically.
cfg := config.New()
cfg.LibDefaults.DefaultRealm = realm
ApplyKrb5OpsecDefaults(cfg)
cfg.LibDefaults.DefaultTktEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "rc4-hmac"}
cfg.LibDefaults.DefaultTktEnctypeIDs = []int32{18, 17, 23}
cfg.LibDefaults.DefaultTGSEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "rc4-hmac"}
+18 -13
View File
@@ -24,17 +24,17 @@ import (
"strings"
"time"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/crypto"
"github.com/jcmturner/gokrb5/v8/iana"
"github.com/jcmturner/gokrb5/v8/iana/flags"
"github.com/jcmturner/gokrb5/v8/iana/keyusage"
"github.com/jcmturner/gokrb5/v8/iana/msgtype"
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/iana/patype"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/config"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/credentials"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/flags"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/msgtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/nametype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/patype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// STRequest holds configuration for a service ticket request.
@@ -73,9 +73,13 @@ type STResult struct {
func GetST(req *STRequest) (*STResult, error) {
realm := strings.ToUpper(req.Domain)
// Build kerberos config
// Build kerberos config. Defense-in-depth: networking in this file is
// hand-rolled via sendKDCRequest → transport.Dial, so the opsec keys
// are no-ops here today. Stamp them so future use of a gokrb5 client
// inherits the proxy/DNS guarantees automatically.
cfg := config.New()
cfg.LibDefaults.DefaultRealm = realm
ApplyKrb5OpsecDefaults(cfg)
cfg.LibDefaults.DefaultTktEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "rc4-hmac"}
cfg.LibDefaults.DefaultTktEnctypeIDs = []int32{18, 17, 23}
cfg.LibDefaults.DefaultTGSEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "rc4-hmac"}
@@ -811,9 +815,10 @@ func RequestTGS(tgtBytes []byte, sessionKey types.EncryptionKey,
return nil, fmt.Errorf("failed to unmarshal TGT: %v", err)
}
// Build minimal config
// Build minimal config. Defense-in-depth opsec stamp; see GetST for rationale.
cfg := config.New()
cfg.LibDefaults.DefaultRealm = realm
ApplyKrb5OpsecDefaults(cfg)
cfg.LibDefaults.DefaultTGSEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "rc4-hmac"}
cfg.LibDefaults.DefaultTGSEnctypeIDs = []int32{18, 17, 23}
+11 -7
View File
@@ -26,11 +26,11 @@ import (
"github.com/mandiant/gopacket/pkg/transport"
"unicode/utf16"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/crypto"
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/config"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/nametype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
"golang.org/x/crypto/md4"
)
@@ -111,9 +111,13 @@ func GetTGT(req *TGTRequest) (*TGTResult, error) {
return nil, fmt.Errorf("no authentication method specified (need password, hash, or aesKey)")
}
// Build AS-REQ
// Build AS-REQ. Defense-in-depth: networking below uses transport.Dial
// directly (gokrb5's sendToKDC is never invoked), so the opsec keys are
// no-ops on this path. Stamping them now means a future refactor that
// introduces a gokrb5 client.Client inherits the proxy/DNS guarantees.
cfg := config.New()
cfg.LibDefaults.DefaultRealm = realm
ApplyKrb5OpsecDefaults(cfg)
cfg.LibDefaults.DefaultTktEnctypes = []string{etypeName(encType)}
cfg.LibDefaults.DefaultTktEnctypeIDs = []int32{encType}
cfg.LibDefaults.Forwardable = true
@@ -263,7 +267,7 @@ func buildPAEncTimestamp(key []byte, encType int32) (types.PAData, error) {
// sendKDCRequest sends a Kerberos message to the KDC and returns the response.
func sendKDCRequest(kdcHost string, data []byte) ([]byte, error) {
addr := fmt.Sprintf("%s:88", kdcHost)
addr := FormatKDC(kdcHost, "88")
conn, err := transport.Dial("tcp", addr)
if err != nil {
return nil, fmt.Errorf("failed to connect to KDC %s: %v", addr, err)
+2 -2
View File
@@ -23,8 +23,8 @@ import (
"strings"
"time"
"github.com/jcmturner/gokrb5/v8/crypto"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
)
// PA-DATA type for KERB-KEY-LIST-REQ (MS-KILE 2.2.11)
+2 -2
View File
@@ -22,8 +22,8 @@ import (
"strings"
"time"
"github.com/jcmturner/gokrb5/v8/iana/etypeID"
"github.com/jcmturner/gokrb5/v8/keytab"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/keytab"
)
// BuildKeytabFromNTHash creates a keytab from an NTLM hash for pass-the-hash attacks.
+11 -9
View File
@@ -20,11 +20,11 @@ import (
"strings"
"time"
"github.com/jcmturner/gokrb5/v8/client"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/iana/etypeID"
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/client"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/config"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/nametype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// TGSResult holds the result of a TGS request for Kerberoasting
@@ -78,9 +78,11 @@ func GetTGSWithHash(username, nthash, domain, kdcHost, targetUser, spn string) (
func GetTGSWithOptions(opts TGSOptions) (*TGSResult, error) {
realm := strings.ToUpper(opts.Domain)
// Create Kerberos config
// Create Kerberos config; ApplyKrb5OpsecDefaults stamps udp_preference_limit=1
// and disables OS-resolver SRV lookups — see pkg/kerberos/client.go.
cfg := config.New()
cfg.LibDefaults.DefaultRealm = realm
ApplyKrb5OpsecDefaults(cfg)
// Request RC4 (etype 23) for faster cracking - matches Impacket behavior
cfg.LibDefaults.DefaultTktEnctypes = []string{"rc4-hmac"}
cfg.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.RC4_HMAC}
@@ -93,7 +95,7 @@ func GetTGSWithOptions(opts TGSOptions) (*TGSResult, error) {
cfg.Realms = []config.Realm{
{
Realm: realm,
KDC: []string{fmt.Sprintf("%s:88", opts.KDCHost)},
KDC: []string{FormatKDC(opts.KDCHost, "88")},
DefaultDomain: strings.ToLower(opts.Domain),
},
}
@@ -110,10 +112,10 @@ func GetTGSWithOptions(opts TGSOptions) (*TGSResult, error) {
if err != nil {
return nil, fmt.Errorf("failed to create keytab from hash: %v", err)
}
cl = client.NewWithKeytab(opts.Username, realm, kt, cfg, client.DisablePAFXFAST(true))
cl = client.NewWithKeytab(TransportKDCDialer{}, opts.Username, realm, kt, cfg, client.DisablePAFXFAST(true))
} else if opts.Password != "" {
// Password authentication
cl = client.NewWithPassword(opts.Username, realm, opts.Password, cfg, client.DisablePAFXFAST(true))
cl = client.NewWithPassword(TransportKDCDialer{}, opts.Username, realm, opts.Password, cfg, client.DisablePAFXFAST(true))
} else {
return nil, fmt.Errorf("either password or NT hash must be provided")
}
+6 -6
View File
@@ -26,12 +26,12 @@ import (
"time"
gokrbasn1 "github.com/jcmturner/gofork/encoding/asn1"
"github.com/jcmturner/gokrb5/v8/crypto"
"github.com/jcmturner/gokrb5/v8/iana/adtype"
"github.com/jcmturner/gokrb5/v8/iana/etypeID"
"github.com/jcmturner/gokrb5/v8/iana/flags"
"github.com/jcmturner/gokrb5/v8/keytab"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/adtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/flags"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/keytab"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// TicketConfig holds configuration for ticket creation
+3 -3
View File
@@ -21,9 +21,9 @@ import (
"log"
"os"
"github.com/jcmturner/gokrb5/v8/gssapi"
"github.com/jcmturner/gokrb5/v8/iana/keyusage"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/gssapi"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
"github.com/mandiant/gopacket/pkg/kerberos"
)
+2 -2
View File
@@ -17,8 +17,8 @@ package smb
import (
"encoding/asn1"
"github.com/jcmturner/gokrb5/v8/gssapi"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/gssapi"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
"github.com/mandiant/gopacket/pkg/kerberos"
"github.com/mandiant/gopacket/pkg/third_party/smb2"
)
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+95
View File
@@ -0,0 +1,95 @@
# gokrb5
It is recommended to use the latest version: [![Version](https://img.shields.io/github/release/jcmturner/gokrb5.svg)](https://github.com/jcmturner/gokrb5/releases)
Development will be focused on the latest major version. New features will only be targeted at this version.
| Versions | Dependency Management | Import Path | Usage | Godoc | Go Report Card |
|----------|-----------------------|-------------|-------|-------|----------------|
| [![v8](https://github.com/jcmturner/gokrb5/workflows/v8/badge.svg)](https://github.com/jcmturner/gokrb5/actions?query=workflow%3Av8) | Go modules | import "github.com/jcmturner/gokrb5/v8/{sub-package}" | [![Usage](https://img.shields.io/badge/v8-usage-blue)](https://github.com/jcmturner/gokrb5/blob/master/v8/USAGE.md) | [![GoDoc](https://img.shields.io/badge/godoc-reference-blue)](https://pkg.go.dev/github.com/jcmturner/gokrb5/v8) | [![Go Report Card](https://goreportcard.com/badge/github.com/jcmturner/gokrb5/v8)](https://goreportcard.com/report/github.com/jcmturner/gokrb5/v8) |
| [![v7](https://github.com/jcmturner/gokrb5/workflows/v7/badge.svg)](https://github.com/jcmturner/gokrb5/actions?query=workflow%3Av7) | gopkg.in | import "gopkg.in/jcmturner/gokrb5.v7/{sub-package}" | [![Usage](https://img.shields.io/badge/v7-usage-blue)](https://github.com/jcmturner/gokrb5/blob/master/USAGE.md) | [![GoDoc](https://img.shields.io/badge/godoc-reference-blue)](https://pkg.go.dev/github.com/jcmturner/gokrb5@v7.5.0+incompatible) | [![Go Report Card](https://goreportcard.com/badge/gopkg.in/jcmturner/gokrb5.v7)](https://goreportcard.com/report/gopkg.in/jcmturner/gokrb5.v7) |
#### Go Version Support
![Go version](https://img.shields.io/badge/Go-1.18-brightgreen.svg)
![Go version](https://img.shields.io/badge/Go-1.17-brightgreen.svg)
![Go version](https://img.shields.io/badge/Go-1.16-brightgreen.svg)
gokrb5 may work with other versions of Go but they are not formally tested.
It has been reported that gokrb5 also works with the [gollvm](https://go.googlesource.com/gollvm/) compiler but this is not formally tested.
## Features
* **Pure Go** - no dependency on external libraries
* No platform specific code
* Server Side
* HTTP handler wrapper implements SPNEGO Kerberos authentication
* HTTP handler wrapper decodes Microsoft AD PAC authorization data
* Client Side
* Client that can authenticate to an SPNEGO Kerberos authenticated web service
* Ability to change client's password
* General
* Kerberos libraries for custom integration
* Parsing Keytab files
* Parsing krb5.conf files
* Parsing client credentials cache files such as `/tmp/krb5cc_$(id -u $(whoami))`
#### Implemented Encryption & Checksum Types
| Implementation | Encryption ID | Checksum ID | RFC |
|-------|-------------|------------|------|
| des3-cbc-sha1-kd | 16 | 12 | 3961 |
| aes128-cts-hmac-sha1-96 | 17 | 15 | 3962 |
| aes256-cts-hmac-sha1-96 | 18 | 16 | 3962 |
| aes128-cts-hmac-sha256-128 | 19 | 19 | 8009 |
| aes256-cts-hmac-sha384-192 | 20 | 20 | 8009 |
| rc4-hmac | 23 | -138 | 4757 |
The following is working/tested:
* Tested against MIT KDC (1.6.3 is the oldest version tested against) and Microsoft Active Directory (Windows 2008 R2)
* Tested against a KDC that supports PA-FX-FAST.
* Tested against users that have pre-authentication required using PA-ENC-TIMESTAMP.
* Microsoft PAC Authorization Data is processed and exposed in the HTTP request context. Available if Microsoft Active Directory is used as the KDC.
## Contributing
If you are interested in contributing to gokrb5, great! Please read the [contribution guidelines](https://github.com/jcmturner/gokrb5/blob/master/CONTRIBUTING.md).
---
## References
* [RFC 3244 Microsoft Windows 2000 Kerberos Change Password and Set Password Protocols](https://tools.ietf.org/html/rfc3244)
* [RFC 4120 The Kerberos Network Authentication Service (V5)](https://tools.ietf.org/html/rfc4120)
* [RFC 3961 Encryption and Checksum Specifications for Kerberos 5](https://tools.ietf.org/html/rfc3961)
* [RFC 3962 Advanced Encryption Standard (AES) Encryption for Kerberos 5](https://tools.ietf.org/html/rfc3962)
* [RFC 4121 The Kerberos Version 5 GSS-API Mechanism](https://tools.ietf.org/html/rfc4121)
* [RFC 4178 The Simple and Protected Generic Security Service Application Program Interface (GSS-API) Negotiation Mechanism](https://tools.ietf.org/html/rfc4178.html)
* [RFC 4559 SPNEGO-based Kerberos and NTLM HTTP Authentication in Microsoft Windows](https://tools.ietf.org/html/rfc4559.html)
* [RFC 4757 The RC4-HMAC Kerberos Encryption Types Used by Microsoft Windows](https://tools.ietf.org/html/rfc4757)
* [RFC 6806 Kerberos Principal Name Canonicalization and Cross-Realm Referrals](https://tools.ietf.org/html/rfc6806.html)
* [RFC 6113 A Generalized Framework for Kerberos Pre-Authentication](https://tools.ietf.org/html/rfc6113.html)
* [RFC 8009 AES Encryption with HMAC-SHA2 for Kerberos 5](https://tools.ietf.org/html/rfc8009)
* [IANA Assigned Kerberos Numbers](http://www.iana.org/assignments/kerberos-parameters/kerberos-parameters.xhtml)
* [HTTP-Based Cross-Platform Authentication by Using the Negotiate Protocol - Part 1](https://msdn.microsoft.com/en-us/library/ms995329.aspx)
* [HTTP-Based Cross-Platform Authentication by Using the Negotiate Protocol - Part 2](https://msdn.microsoft.com/en-us/library/ms995330.aspx)
* [Microsoft PAC Validation](https://blogs.msdn.microsoft.com/openspecification/2009/04/24/understanding-microsoft-kerberos-pac-validation/)
* [Microsoft Kerberos Protocol Extensions](https://msdn.microsoft.com/en-us/library/cc233855.aspx)
* [Windows Data Types](https://msdn.microsoft.com/en-us/library/cc230273.aspx)
### Useful Links
* https://en.wikipedia.org/wiki/Ciphertext_stealing#CBC_ciphertext_stealing
## Thanks
* Greg Hudson from the MIT Consortium for Kerberos and Internet Trust for providing useful advice.
## Contributing
Thank you for your interest in contributing to gokrb5 please read the
[contribution guide](https://github.com/jcmturner/gokrb5/blob/master/CONTRIBUTING.md) as it should help you get started.
## Known Issues
| Issue | Worked around? | References |
|-------|-------------|------------|
| The Go standard library's encoding/asn1 package cannot unmarshal into slice of asn1.RawValue | Yes | https://github.com/golang/go/issues/17321 |
| The Go standard library's encoding/asn1 package cannot marshal into a GeneralString | Yes - using https://github.com/jcmturner/gofork/tree/master/encoding/asn1 | https://github.com/golang/go/issues/18832 |
| The Go standard library's encoding/asn1 package cannot marshal into slice of strings and pass stringtype parameter tags to members | Yes - using https://github.com/jcmturner/gofork/tree/master/encoding/asn1 | https://github.com/golang/go/issues/18834 |
| The Go standard library's encoding/asn1 package cannot marshal with application tags | Yes | |
| The Go standard library's x/crypto/pbkdf2.Key function uses the int type for iteraction count limiting meaning the 4294967296 count specified in https://tools.ietf.org/html/rfc3962 section 4 cannot be met on 32bit systems | Yes - using https://github.com/jcmturner/gofork/tree/master/x/crypto/pbkdf2 | https://go-review.googlesource.com/c/crypto/+/85535 |
+245
View File
@@ -0,0 +1,245 @@
## Version 8 Usage
### Configuration
The gokrb5 libraries use the same krb5.conf configuration file format as MIT Kerberos,
described [here](https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html).
Config instances can be created by loading from a file path or by passing a string, io.Reader or bufio.Scanner to the
relevant method:
```go
import "github.com/jcmturner/gokrb5/v8/config"
cfg, err := config.Load("/path/to/config/file")
cfg, err := config.NewFromString(krb5Str) //String must have appropriate newline separations
cfg, err := config.NewFromReader(reader)
cfg, err := config.NewFromScanner(scanner)
```
### Keytab files
Standard keytab files can be read from a file or from a slice of bytes:
```go
import "github.com/jcmturner/gokrb5/v8/keytab"
ktFromFile, err := keytab.Load("/path/to/file.keytab")
ktFromBytes, err := keytab.Parse(b)
```
---
### Kerberos Client
**Create** a client instance with either a password or a keytab.
A configuration must also be passed. Additionally optional additional settings can be provided.
```go
import "github.com/jcmturner/gokrb5/v8/client"
cl := client.NewWithPassword("username", "REALM.COM", "password", cfg)
cl := client.NewWithKeytab("username", "REALM.COM", kt, cfg)
```
Optional settings are provided using the functions defined in the ``client/settings.go`` source file.
**Login**:
```go
err := cl.Login()
```
Kerberos Ticket Granting Tickets (TGT) will be automatically renewed unless the client was created from a CCache.
A client can be **destroyed** with the following method:
```go
cl.Destroy()
```
#### Active Directory KDC and FAST negotiation
Active Directory does not commonly support FAST negotiation so you will need to disable this on the client.
If this is the case you will see this error:
```KDC did not respond appropriately to FAST negotiation```
To resolve this disable PA-FX-Fast on the client before performing Login().
This is done with one of the optional client settings as shown below:
```go
cl := client.NewWithPassword("username", "REALM.COM", "password", cfg, client.DisablePAFXFAST(true))
```
#### Authenticate to a Service
##### HTTP SPNEGO
Create the HTTP request object and then create an SPNEGO client and use this to process the request with methods that
are the same as on a HTTP client.
If nil is passed as the HTTP client when creating the SPNEGO client the http.DefaultClient is used.
When creating the SPNEGO client pass the Service Principal Name (SPN) or auto generate the SPN from the request
object by passing a null string "".
```go
r, _ := http.NewRequest("GET", "http://host.test.gokrb5/index.html", nil)
spnegoCl := spnego.NewClient(cl, nil, "")
resp, err := spnegoCl.Do(r)
```
##### Generic Kerberos Client
To authenticate to a service a client will need to request a service ticket for a Service Principal Name (SPN) and form
into an AP_REQ message along with an authenticator encrypted with the session key that was delivered from the KDC along
with the service ticket.
The steps below outline how to do this.
* Get the service ticket and session key for the service the client is authenticating to.
The following method will use the client's cache either returning a valid cached ticket, renewing a cached ticket with
the KDC or requesting a new ticket from the KDC.
Therefore the GetServiceTicket method can be continually used for the most efficient interaction with the KDC.
```go
tkt, key, err := cl.GetServiceTicket("HTTP/host.test.gokrb5")
```
The steps after this will be specific to the application protocol but it will likely involve a client/server
Authentication Protocol exchange (AP exchange).
This will involve these steps:
* Generate a new Authenticator and generate a sequence number and subkey:
```go
auth, _ := types.NewAuthenticator(cl.Credentials.Realm, cl.Credentials.CName)
etype, _ := crypto.GetEtype(key.KeyType)
auth.GenerateSeqNumberAndSubKey(key.KeyType, etype.GetKeyByteSize())
```
* Set the checksum on the authenticator
The checksum is an application specific value. Set as follows:
```go
auth.Cksum = types.Checksum{
CksumType: checksumIDint,
Checksum: checksumBytesSlice,
}
```
* Create the AP_REQ:
```go
APReq, err := messages.NewAPReq(tkt, key, auth)
```
Now send the AP_REQ to the service. How this is done will be specific to the application use case.
#### Changing a Client Password
This feature uses the Microsoft Kerberos Password Change protocol (RFC 3244).
This is implemented in Microsoft Active Directory and in MIT krb5kdc as of version 1.7.
Typically the kpasswd server listens on port 464.
Below is example code for how to use this feature:
```go
cfg, err := config.Load("/path/to/config/file")
if err != nil {
panic(err.Error())
}
kt, err := keytab.Load("/path/to/file.keytab")
if err != nil {
panic(err.Error())
}
cl := client.NewWithKeytab("username", "REALM.COM", kt)
cl.WithConfig(cfg)
ok, err := cl.ChangePasswd("newpassword")
if err != nil {
panic(err.Error())
}
if !ok {
panic("failed to change password")
}
```
The client kerberos config (krb5.conf) will need to have either the kpassd_server or admin_server defined in the
relevant [realms] section. For example:
```
REALM.COM = {
kdc = 127.0.0.1:88
kpasswd_server = 127.0.0.1:464
default_domain = realm.com
}
```
See https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html#realms for more information.
#### Client Diagnostics
In the event of issues the configuration of a client can be investigated with its ``Diagnostics`` method.
This will check that the required enctypes defined in the client's krb5 config are available in its keytab.
It will also check that KDCs can be resolved for the client's REALM.
The error returned will contain details of any failed checks.
The configuration details of the client will be written to the ``io.Writer`` provided.
---
### Kerberised Service
#### SPNEGO/Kerberos HTTP Service
A HTTP handler wrapper can be used to implement Kerberos SPNEGO authentication for web services.
To configure the wrapper the keytab for the SPN and a Logger are required:
```go
kt, err := keytab.Load("/path/to/file.keytab")
l := log.New(os.Stderr, "GOKRB5 Service: ", log.Ldate|log.Ltime|log.Lshortfile)
```
Create a handler function of the application's handling method (apphandler in the example below):
```go
h := http.HandlerFunc(apphandler)
```
Configure the HTTP handler:
```go
http.Handler("/", spnego.SPNEGOKRB5Authenticate(h, &kt, service.Logger(l)))
```
The handler to be wrapped and the keytab are required arguments.
Additional optional settings can be provided, such as the logger shown above.
Another example of optional settings may be that when using Active Directory where the SPN is mapped to a user account
the keytab may contain an entry for this user account. In this case this should be specified as below with the
``KeytabPrincipal``:
```go
http.Handler("/", spnego.SPNEGOKRB5Authenticate(h, &kt, service.Logger(l), service.KeytabPrincipal(pn)))
```
##### Session Management
For efficiency reasons it is not desirable to authenticate on every call to a web service.
Therefore most authenticated web applications implement some form of session with the user.
Such sessions can be supported by passing a "session manager" into the ``SPNEGOKRB5Authenticate`` wrapper handler.
In order to not demand a specific session manager solution, the session manager must implement a simple interface:
```go
type SessionMgr interface {
New(w http.ResponseWriter, r *http.Request, k string, v []byte) error
Get(r *http.Request, k string) ([]byte, error)
}
```
- New - creates a new session for the request and adds a piece of data (key/value pair) to the session
- Get - extract from an existing session the value held within it under the key provided.
This should return nil bytes or an error if there is no existing session.
The session manager (sm) that implements this interface should then be passed to the ``SPNEGOKRB5Authenticate`` wrapper
handler as below:
```go
http.Handler("/", spnego.SPNEGOKRB5Authenticate(h, &kt, service.Logger(l), service.SessionManager(sm)))
```
The ``httpServer.go`` source file in the examples directory shows how this can be used with the popular gorilla web toolkit.
##### Validating Users and Accessing Users' Details
If authentication succeeds then the request's context will have a credentials objected added to it.
This object implements the ``github.com/jcmturner/goidentity/identity`` interface.
If Microsoft Active Directory is used as the KDC then additional ADCredentials are available in the
``credentials.Attributes`` map under the key ``credentials.AttributeKeyADCredentials``.
For example the SIDs of the users group membership are available and can be used by your application for authorization.
Checking and access the credentials within your application:
```go
// Get a goidentity credentials object from the request's context
creds := goidentity.FromHTTPRequestContext(r)
// Check if it indicates it is authenticated
if creds != nil && creds.Authenticated() {
// Check for Active Directory attributes
if ADCredsJSON, ok := creds.Attributes()[credentials.AttributeKeyADCredentials]; ok {
ADCreds := new(credentials.ADCredentials)
// Unmarshal the AD attributes
err := json.Unmarshal([]byte(ADCredsJSON), ADCreds)
if err == nil {
// Now access the fields of the ADCredentials struct. For example: ADCreds.GroupMembershipSIDs
}
}
} else {
// Not authenticated user
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprint(w, "Authentication failed")
}
```
#### Generic Kerberised Service - Validating Client Details
To validate the AP_REQ sent by the client on the service side call this method:
```go
import "github.com/jcmturner/gokrb5/v8/service"
s := service.NewSettings(&kt) // kt is a keytab and optional settings can also be provided.
if ok, creds, err := service.VerifyAPREQ(&APReq, s); ok {
// Perform application specific actions
// creds object has details about the client identity
}
```
+86
View File
@@ -0,0 +1,86 @@
// Package asn1tools provides tools for managing ASN1 marshaled data.
package asn1tools
import (
"github.com/jcmturner/gofork/encoding/asn1"
)
// MarshalLengthBytes returns the ASN1 encoded bytes for the length 'l'
//
// There are two forms: short (for lengths between 0 and 127), and long definite (for lengths between 0 and 2^1008 -1).
//
// Short form: One octet. Bit 8 has value "0" and bits 7-1 give the length.
//
// Long form: Two to 127 octets. Bit 8 of first octet has value "1" and bits 7-1 give the number of additional length octets. Second and following octets give the length, base 256, most significant digit first.
func MarshalLengthBytes(l int) []byte {
if l <= 127 {
return []byte{byte(l)}
}
var b []byte
p := 1
for i := 1; i < 127; {
b = append([]byte{byte((l % (p * 256)) / p)}, b...)
p = p * 256
l = l - l%p
if l <= 0 {
break
}
}
return append([]byte{byte(128 + len(b))}, b...)
}
// GetLengthFromASN returns the length of a slice of ASN1 encoded bytes from the ASN1 length header it contains.
func GetLengthFromASN(b []byte) int {
if int(b[1]) <= 127 {
return int(b[1])
}
// The bytes that indicate the length
lb := b[2 : 2+int(b[1])-128]
base := 1
l := 0
for i := len(lb) - 1; i >= 0; i-- {
l += int(lb[i]) * base
base = base * 256
}
return l
}
// GetNumberBytesInLengthHeader returns the number of bytes in the ASn1 header that indicate the length.
func GetNumberBytesInLengthHeader(b []byte) int {
if int(b[1]) <= 127 {
return 1
}
// The bytes that indicate the length
return 1 + int(b[1]) - 128
}
// AddASNAppTag adds an ASN1 encoding application tag value to the raw bytes provided.
func AddASNAppTag(b []byte, tag int) []byte {
r := asn1.RawValue{
Class: asn1.ClassApplication,
IsCompound: true,
Tag: tag,
Bytes: b,
}
ab, _ := asn1.Marshal(r)
return ab
}
/*
// The Marshal method of golang's asn1 package does not enable you to define wrapping the output in an application tag.
// This method adds that wrapping tag.
func AddASNAppTag(b []byte, tag int) []byte {
// The ASN1 wrapping consists of 2 bytes:
// 1st byte -> Identifier Octet - Application Tag
// 2nd byte -> The length (this will be the size indicated in the input bytes + 2 for the additional bytes we add here.
// Application Tag:
//| Bit: | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
//| Value: | 0 | 1 | 1 | From the RFC spec 4120 |
//| Explanation | Defined by the ASN1 encoding rules for an application tag | A value of 1 indicates a constructed type | The ASN Application tag value |
// Therefore the value of the byte is an integer = ( Application tag value + 96 )
//b = append(MarshalLengthBytes(int(b[1])+2), b...)
b = append(MarshalLengthBytes(len(b)), b...)
b = append([]byte{byte(96 + tag)}, b...)
return b
}
*/
+182
View File
@@ -0,0 +1,182 @@
package client
import (
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/etype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/errorcode"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/patype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// ASExchange performs an AS exchange for the client to retrieve a TGT.
func (cl *Client) ASExchange(realm string, ASReq messages.ASReq, referral int) (messages.ASRep, error) {
if ok, err := cl.IsConfigured(); !ok {
return messages.ASRep{}, krberror.Errorf(err, krberror.ConfigError, "AS Exchange cannot be performed")
}
// Set PAData if required
err := setPAData(cl, nil, &ASReq)
if err != nil {
return messages.ASRep{}, krberror.Errorf(err, krberror.KRBMsgError, "AS Exchange Error: issue with setting PAData on AS_REQ")
}
b, err := ASReq.Marshal()
if err != nil {
return messages.ASRep{}, krberror.Errorf(err, krberror.EncodingError, "AS Exchange Error: failed marshaling AS_REQ")
}
var ASRep messages.ASRep
rb, err := cl.sendToKDC(b, realm)
if err != nil {
if e, ok := err.(messages.KRBError); ok {
switch e.ErrorCode {
case errorcode.KDC_ERR_PREAUTH_REQUIRED, errorcode.KDC_ERR_PREAUTH_FAILED:
// From now on assume this client will need to do this pre-auth and set the PAData
cl.settings.assumePreAuthentication = true
err = setPAData(cl, &e, &ASReq)
if err != nil {
return messages.ASRep{}, krberror.Errorf(err, krberror.KRBMsgError, "AS Exchange Error: failed setting AS_REQ PAData for pre-authentication required")
}
b, err := ASReq.Marshal()
if err != nil {
return messages.ASRep{}, krberror.Errorf(err, krberror.EncodingError, "AS Exchange Error: failed marshaling AS_REQ with PAData")
}
rb, err = cl.sendToKDC(b, realm)
if err != nil {
if _, ok := err.(messages.KRBError); ok {
return messages.ASRep{}, krberror.Errorf(err, krberror.KDCError, "AS Exchange Error: kerberos error response from KDC")
}
return messages.ASRep{}, krberror.Errorf(err, krberror.NetworkingError, "AS Exchange Error: failed sending AS_REQ to KDC")
}
case errorcode.KDC_ERR_WRONG_REALM:
// Client referral https://tools.ietf.org/html/rfc6806.html#section-7
if referral > 5 {
return messages.ASRep{}, krberror.Errorf(err, krberror.KRBMsgError, "maximum number of client referrals exceeded")
}
referral++
return cl.ASExchange(e.CRealm, ASReq, referral)
default:
return messages.ASRep{}, krberror.Errorf(err, krberror.KDCError, "AS Exchange Error: kerberos error response from KDC")
}
} else {
return messages.ASRep{}, krberror.Errorf(err, krberror.NetworkingError, "AS Exchange Error: failed sending AS_REQ to KDC")
}
}
err = ASRep.Unmarshal(rb)
if err != nil {
return messages.ASRep{}, krberror.Errorf(err, krberror.EncodingError, "AS Exchange Error: failed to process the AS_REP")
}
if ok, err := ASRep.Verify(cl.Config, cl.Credentials, ASReq); !ok {
return messages.ASRep{}, krberror.Errorf(err, krberror.KRBMsgError, "AS Exchange Error: AS_REP is not valid or client password/keytab incorrect")
}
return ASRep, nil
}
// setPAData adds pre-authentication data to the AS_REQ.
func setPAData(cl *Client, krberr *messages.KRBError, ASReq *messages.ASReq) error {
if !cl.settings.DisablePAFXFAST() {
pa := types.PAData{PADataType: patype.PA_REQ_ENC_PA_REP}
ASReq.PAData = append(ASReq.PAData, pa)
}
if cl.settings.AssumePreAuthentication() {
// Identify the etype to use to encrypt the PA Data
var et etype.EType
var err error
var key types.EncryptionKey
var kvno int
if krberr == nil {
// This is not in response to an error from the KDC. It is preemptive or renewal
// There is no KRB Error that tells us the etype to use
etn := cl.settings.preAuthEType // Use the etype that may have previously been negotiated
if etn == 0 {
etn = int32(cl.Config.LibDefaults.PreferredPreauthTypes[0]) // Resort to config
}
et, err = crypto.GetEtype(etn)
if err != nil {
return krberror.Errorf(err, krberror.EncryptingError, "error getting etype for pre-auth encryption")
}
key, kvno, err = cl.Key(et, 0, nil)
if err != nil {
return krberror.Errorf(err, krberror.EncryptingError, "error getting key from credentials")
}
} else {
// Get the etype to use from the PA data in the KRBError e-data
et, err = preAuthEType(krberr)
if err != nil {
return krberror.Errorf(err, krberror.EncryptingError, "error getting etype for pre-auth encryption")
}
cl.settings.preAuthEType = et.GetETypeID() // Set the etype that has been defined for potential future use
key, kvno, err = cl.Key(et, 0, krberr)
if err != nil {
return krberror.Errorf(err, krberror.EncryptingError, "error getting key from credentials")
}
}
// Generate the PA data
paTSb, err := types.GetPAEncTSEncAsnMarshalled()
if err != nil {
return krberror.Errorf(err, krberror.KRBMsgError, "error creating PAEncTSEnc for Pre-Authentication")
}
paEncTS, err := crypto.GetEncryptedData(paTSb, key, keyusage.AS_REQ_PA_ENC_TIMESTAMP, kvno)
if err != nil {
return krberror.Errorf(err, krberror.EncryptingError, "error encrypting pre-authentication timestamp")
}
pb, err := paEncTS.Marshal()
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error marshaling the PAEncTSEnc encrypted data")
}
pa := types.PAData{
PADataType: patype.PA_ENC_TIMESTAMP,
PADataValue: pb,
}
// Look for and delete any exiting patype.PA_ENC_TIMESTAMP
for i, pa := range ASReq.PAData {
if pa.PADataType == patype.PA_ENC_TIMESTAMP {
ASReq.PAData[i] = ASReq.PAData[len(ASReq.PAData)-1]
ASReq.PAData = ASReq.PAData[:len(ASReq.PAData)-1]
}
}
ASReq.PAData = append(ASReq.PAData, pa)
}
return nil
}
// preAuthEType establishes what encryption type to use for pre-authentication from the KRBError returned from the KDC.
func preAuthEType(krberr *messages.KRBError) (etype etype.EType, err error) {
//RFC 4120 5.2.7.5 covers the preference order of ETYPE-INFO2 and ETYPE-INFO.
var etypeID int32
var pas types.PADataSequence
e := pas.Unmarshal(krberr.EData)
if e != nil {
err = krberror.Errorf(e, krberror.EncodingError, "error unmashalling KRBError data")
return
}
Loop:
for _, pa := range pas {
switch pa.PADataType {
case patype.PA_ETYPE_INFO2:
info, e := pa.GetETypeInfo2()
if e != nil {
err = krberror.Errorf(e, krberror.EncodingError, "error unmashalling ETYPE-INFO2 data")
return
}
etypeID = info[0].EType
break Loop
case patype.PA_ETYPE_INFO:
info, e := pa.GetETypeInfo()
if e != nil {
err = krberror.Errorf(e, krberror.EncodingError, "error unmashalling ETYPE-INFO data")
return
}
etypeID = info[0].EType
}
}
etype, e = crypto.GetEtype(etypeID)
if e != nil {
err = krberror.Errorf(e, krberror.EncryptingError, "error creating etype")
return
}
return etype, nil
}
+108
View File
@@ -0,0 +1,108 @@
package client
import (
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/flags"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/nametype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// TGSREQGenerateAndExchange generates the TGS_REQ and performs a TGS exchange to retrieve a ticket to the specified SPN.
func (cl *Client) TGSREQGenerateAndExchange(spn types.PrincipalName, kdcRealm string, tgt messages.Ticket, sessionKey types.EncryptionKey, renewal bool) (tgsReq messages.TGSReq, tgsRep messages.TGSRep, err error) {
tgsReq, err = messages.NewTGSReq(cl.Credentials.CName(), kdcRealm, cl.Config, tgt, sessionKey, spn, renewal)
if err != nil {
return tgsReq, tgsRep, krberror.Errorf(err, krberror.KRBMsgError, "TGS Exchange Error: failed to generate a new TGS_REQ")
}
return cl.TGSExchange(tgsReq, kdcRealm, tgsRep.Ticket, sessionKey, 0)
}
// TGSExchange exchanges the provided TGS_REQ with the KDC to retrieve a TGS_REP.
// Referrals are automatically handled.
// The client's cache is updated with the ticket received.
func (cl *Client) TGSExchange(tgsReq messages.TGSReq, kdcRealm string, tgt messages.Ticket, sessionKey types.EncryptionKey, referral int) (messages.TGSReq, messages.TGSRep, error) {
var tgsRep messages.TGSRep
b, err := tgsReq.Marshal()
if err != nil {
return tgsReq, tgsRep, krberror.Errorf(err, krberror.EncodingError, "TGS Exchange Error: failed to marshal TGS_REQ")
}
r, err := cl.sendToKDC(b, kdcRealm)
if err != nil {
if _, ok := err.(messages.KRBError); ok {
return tgsReq, tgsRep, krberror.Errorf(err, krberror.KDCError, "TGS Exchange Error: kerberos error response from KDC when requesting for %s", tgsReq.ReqBody.SName.PrincipalNameString())
}
return tgsReq, tgsRep, krberror.Errorf(err, krberror.NetworkingError, "TGS Exchange Error: issue sending TGS_REQ to KDC")
}
err = tgsRep.Unmarshal(r)
if err != nil {
return tgsReq, tgsRep, krberror.Errorf(err, krberror.EncodingError, "TGS Exchange Error: failed to process the TGS_REP")
}
err = tgsRep.DecryptEncPart(sessionKey)
if err != nil {
return tgsReq, tgsRep, krberror.Errorf(err, krberror.EncodingError, "TGS Exchange Error: failed to process the TGS_REP")
}
if ok, err := tgsRep.Verify(cl.Config, tgsReq); !ok {
return tgsReq, tgsRep, krberror.Errorf(err, krberror.EncodingError, "TGS Exchange Error: TGS_REP is not valid")
}
if tgsRep.Ticket.SName.NameString[0] == "krbtgt" && !tgsRep.Ticket.SName.Equal(tgsReq.ReqBody.SName) {
if referral > 5 {
return tgsReq, tgsRep, krberror.Errorf(err, krberror.KRBMsgError, "TGS Exchange Error: maximum number of referrals exceeded")
}
// Server referral https://tools.ietf.org/html/rfc6806.html#section-8
// The TGS Rep contains a TGT for another domain as the service resides in that domain.
cl.addSession(tgsRep.Ticket, tgsRep.DecryptedEncPart)
realm := tgsRep.Ticket.SName.NameString[len(tgsRep.Ticket.SName.NameString)-1]
referral++
if types.IsFlagSet(&tgsReq.ReqBody.KDCOptions, flags.EncTktInSkey) && len(tgsReq.ReqBody.AdditionalTickets) > 0 {
tgsReq, err = messages.NewUser2UserTGSReq(cl.Credentials.CName(), kdcRealm, cl.Config, tgt, sessionKey, tgsReq.ReqBody.SName, tgsReq.Renewal, tgsReq.ReqBody.AdditionalTickets[0])
if err != nil {
return tgsReq, tgsRep, err
}
}
tgsReq, err = messages.NewTGSReq(cl.Credentials.CName(), realm, cl.Config, tgsRep.Ticket, tgsRep.DecryptedEncPart.Key, tgsReq.ReqBody.SName, tgsReq.Renewal)
if err != nil {
return tgsReq, tgsRep, err
}
return cl.TGSExchange(tgsReq, realm, tgsRep.Ticket, tgsRep.DecryptedEncPart.Key, referral)
}
cl.cache.addEntry(
tgsRep.Ticket,
tgsRep.DecryptedEncPart.AuthTime,
tgsRep.DecryptedEncPart.StartTime,
tgsRep.DecryptedEncPart.EndTime,
tgsRep.DecryptedEncPart.RenewTill,
tgsRep.DecryptedEncPart.Key,
)
cl.Log("ticket added to cache for %s (EndTime: %v)", tgsRep.Ticket.SName.PrincipalNameString(), tgsRep.DecryptedEncPart.EndTime)
return tgsReq, tgsRep, err
}
// GetServiceTicket makes a request to get a service ticket for the SPN specified
// SPN format: <SERVICE>/<FQDN> Eg. HTTP/www.example.com
// The ticket will be added to the client's ticket cache
func (cl *Client) GetServiceTicket(spn string) (messages.Ticket, types.EncryptionKey, error) {
var tkt messages.Ticket
var skey types.EncryptionKey
if tkt, skey, ok := cl.GetCachedTicket(spn); ok {
// Already a valid ticket in the cache
return tkt, skey, nil
}
princ := types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, spn)
realm := cl.spnRealm(princ)
// if we don't know the SPN's realm, ask the client realm's KDC
if realm == "" {
realm = cl.Credentials.Realm()
}
tgt, skey, err := cl.sessionTGT(realm)
if err != nil {
return tkt, skey, err
}
_, tgsRep, err := cl.TGSREQGenerateAndExchange(princ, realm, tgt, skey, false)
if err != nil {
return tkt, skey, err
}
return tgsRep.Ticket, tgsRep.DecryptedEncPart.Key, nil
}
+134
View File
@@ -0,0 +1,134 @@
package client
import (
"encoding/json"
"errors"
"sort"
"sync"
"time"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// Cache for service tickets held by the client.
type Cache struct {
Entries map[string]CacheEntry
mux sync.RWMutex
}
// CacheEntry holds details for a cache entry.
type CacheEntry struct {
SPN string
Ticket messages.Ticket `json:"-"`
AuthTime time.Time
StartTime time.Time
EndTime time.Time
RenewTill time.Time
SessionKey types.EncryptionKey `json:"-"`
}
// NewCache creates a new client ticket cache instance.
func NewCache() *Cache {
return &Cache{
Entries: map[string]CacheEntry{},
}
}
// getEntry returns a cache entry that matches the SPN.
func (c *Cache) getEntry(spn string) (CacheEntry, bool) {
c.mux.RLock()
defer c.mux.RUnlock()
e, ok := (*c).Entries[spn]
return e, ok
}
// JSON returns information about the cached service tickets in a JSON format.
func (c *Cache) JSON() (string, error) {
c.mux.RLock()
defer c.mux.RUnlock()
var es []CacheEntry
keys := make([]string, 0, len(c.Entries))
for k := range c.Entries {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
es = append(es, c.Entries[k])
}
b, err := json.MarshalIndent(&es, "", " ")
if err != nil {
return "", err
}
return string(b), nil
}
// addEntry adds a ticket to the cache.
func (c *Cache) addEntry(tkt messages.Ticket, authTime, startTime, endTime, renewTill time.Time, sessionKey types.EncryptionKey) CacheEntry {
spn := tkt.SName.PrincipalNameString()
c.mux.Lock()
defer c.mux.Unlock()
(*c).Entries[spn] = CacheEntry{
SPN: spn,
Ticket: tkt,
AuthTime: authTime,
StartTime: startTime,
EndTime: endTime,
RenewTill: renewTill,
SessionKey: sessionKey,
}
return c.Entries[spn]
}
// clear deletes all the cache entries
func (c *Cache) clear() {
c.mux.Lock()
defer c.mux.Unlock()
for k := range c.Entries {
delete(c.Entries, k)
}
}
// RemoveEntry removes the cache entry for the defined SPN.
func (c *Cache) RemoveEntry(spn string) {
c.mux.Lock()
defer c.mux.Unlock()
delete(c.Entries, spn)
}
// GetCachedTicket returns a ticket from the cache for the SPN.
// Only a ticket that is currently valid will be returned.
func (cl *Client) GetCachedTicket(spn string) (messages.Ticket, types.EncryptionKey, bool) {
if e, ok := cl.cache.getEntry(spn); ok {
//If within time window of ticket return it
if time.Now().UTC().After(e.StartTime) && time.Now().UTC().Before(e.EndTime) {
cl.Log("ticket received from cache for %s", spn)
return e.Ticket, e.SessionKey, true
} else if time.Now().UTC().Before(e.RenewTill) {
e, err := cl.renewTicket(e)
if err != nil {
return e.Ticket, e.SessionKey, false
}
return e.Ticket, e.SessionKey, true
}
}
var tkt messages.Ticket
var key types.EncryptionKey
return tkt, key, false
}
// renewTicket renews a cache entry ticket.
// To renew from outside the client package use GetCachedTicket
func (cl *Client) renewTicket(e CacheEntry) (CacheEntry, error) {
spn := e.Ticket.SName
_, _, err := cl.TGSREQGenerateAndExchange(spn, e.Ticket.Realm, e.Ticket, e.SessionKey, true)
if err != nil {
return e, err
}
e, ok := cl.cache.getEntry(e.Ticket.SName.PrincipalNameString())
if !ok {
return e, errors.New("ticket was not added to cache")
}
cl.Log("ticket renewed for %s (EndTime: %v)", spn.PrincipalNameString(), e.EndTime)
return e, nil
}
+354
View File
@@ -0,0 +1,354 @@
// Package client provides a client library and methods for Kerberos 5 authentication.
package client
import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"strings"
"time"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/config"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/credentials"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/etype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/errorcode"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/nametype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/keytab"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// KDCDialer is the interface a caller must satisfy to direct all KDC traffic
// (AS, TGS) through a custom transport. Required by every client constructor
// so that proxy-bypassing leaks are a compile error, not a runtime surprise.
// network is "tcp" or "udp"; address is "host:port" (host may be a hostname).
type KDCDialer interface {
Dial(network, address string) (net.Conn, error)
}
// Client side configuration and state.
type Client struct {
Credentials *credentials.Credentials
Config *config.Config
settings *Settings
sessions *sessions
cache *Cache
dialer KDCDialer
}
// NewWithPassword creates a new client from a password credential.
// Set the realm to empty string to use the default realm from config.
// dialer must be non-nil; pass DirectDialer{} for the legacy net.DialTimeout behavior.
func NewWithPassword(dialer KDCDialer, username, realm, password string, krb5conf *config.Config, settings ...func(*Settings)) *Client {
if dialer == nil {
panic("gokrb5 client: KDCDialer is required (pass client.DirectDialer{} for legacy direct net.Dial)")
}
creds := credentials.New(username, realm)
return &Client{
Credentials: creds.WithPassword(password),
Config: krb5conf,
settings: NewSettings(settings...),
sessions: &sessions{
Entries: make(map[string]*session),
},
cache: NewCache(),
dialer: dialer,
}
}
// NewWithKeytab creates a new client from a keytab credential.
// dialer must be non-nil; pass DirectDialer{} for the legacy net.DialTimeout behavior.
func NewWithKeytab(dialer KDCDialer, username, realm string, kt *keytab.Keytab, krb5conf *config.Config, settings ...func(*Settings)) *Client {
if dialer == nil {
panic("gokrb5 client: KDCDialer is required (pass client.DirectDialer{} for legacy direct net.Dial)")
}
creds := credentials.New(username, realm)
return &Client{
Credentials: creds.WithKeytab(kt),
Config: krb5conf,
settings: NewSettings(settings...),
sessions: &sessions{
Entries: make(map[string]*session),
},
cache: NewCache(),
dialer: dialer,
}
}
// NewFromCCache create a client from a populated client cache.
// dialer must be non-nil; pass DirectDialer{} for the legacy net.DialTimeout behavior.
//
// WARNING: A client created from CCache does not automatically renew TGTs and a failure will occur after the TGT expires.
func NewFromCCache(dialer KDCDialer, c *credentials.CCache, krb5conf *config.Config, settings ...func(*Settings)) (*Client, error) {
if dialer == nil {
panic("gokrb5 client: KDCDialer is required (pass client.DirectDialer{} for legacy direct net.Dial)")
}
cl := &Client{
Credentials: c.GetClientCredentials(),
Config: krb5conf,
settings: NewSettings(settings...),
sessions: &sessions{
Entries: make(map[string]*session),
},
cache: NewCache(),
dialer: dialer,
}
spn := types.PrincipalName{
NameType: nametype.KRB_NT_SRV_INST,
NameString: []string{"krbtgt", c.DefaultPrincipal.Realm},
}
cred, ok := c.GetEntry(spn)
if !ok {
return cl, errors.New("TGT not found in CCache")
}
var tgt messages.Ticket
err := tgt.Unmarshal(cred.Ticket)
if err != nil {
return cl, fmt.Errorf("TGT bytes in cache are not valid: %v", err)
}
cl.sessions.Entries[c.DefaultPrincipal.Realm] = &session{
realm: c.DefaultPrincipal.Realm,
authTime: cred.AuthTime,
endTime: cred.EndTime,
renewTill: cred.RenewTill,
tgt: tgt,
sessionKey: cred.Key,
}
for _, cred := range c.GetEntries() {
var tkt messages.Ticket
err = tkt.Unmarshal(cred.Ticket)
if err != nil {
return cl, fmt.Errorf("cache entry ticket bytes are not valid: %v", err)
}
cl.cache.addEntry(
tkt,
cred.AuthTime,
cred.StartTime,
cred.EndTime,
cred.RenewTill,
cred.Key,
)
}
return cl, nil
}
// Key returns the client's encryption key for the specified encryption type and its kvno (kvno of zero will find latest).
// The key can be retrieved either from the keytab or generated from the client's password.
// If the client has both a keytab and a password defined the keytab is favoured as the source for the key
// A KRBError can be passed in the event the KDC returns one of type KDC_ERR_PREAUTH_REQUIRED and is required to derive
// the key for pre-authentication from the client's password. If a KRBError is not available, pass nil to this argument.
func (cl *Client) Key(etype etype.EType, kvno int, krberr *messages.KRBError) (types.EncryptionKey, int, error) {
if cl.Credentials.HasKeytab() && etype != nil {
return cl.Credentials.Keytab().GetEncryptionKey(cl.Credentials.CName(), cl.Credentials.Domain(), kvno, etype.GetETypeID())
} else if cl.Credentials.HasPassword() {
if krberr != nil && krberr.ErrorCode == errorcode.KDC_ERR_PREAUTH_REQUIRED {
var pas types.PADataSequence
err := pas.Unmarshal(krberr.EData)
if err != nil {
return types.EncryptionKey{}, 0, fmt.Errorf("could not get PAData from KRBError to generate key from password: %v", err)
}
key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), krberr.CName, krberr.CRealm, etype.GetETypeID(), pas)
return key, 0, err
}
key, _, err := crypto.GetKeyFromPassword(cl.Credentials.Password(), cl.Credentials.CName(), cl.Credentials.Domain(), etype.GetETypeID(), types.PADataSequence{})
return key, 0, err
}
return types.EncryptionKey{}, 0, errors.New("credential has neither keytab or password to generate key")
}
// IsConfigured indicates if the client has the values required set.
func (cl *Client) IsConfigured() (bool, error) {
if cl.Credentials.UserName() == "" {
return false, errors.New("client does not have a username")
}
if cl.Credentials.Domain() == "" {
return false, errors.New("client does not have a define realm")
}
// Client needs to have either a password, keytab or a session already (later when loading from CCache)
if !cl.Credentials.HasPassword() && !cl.Credentials.HasKeytab() {
authTime, _, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
if err != nil || authTime.IsZero() {
return false, errors.New("client has neither a keytab nor a password set and no session")
}
}
if !cl.Config.LibDefaults.DNSLookupKDC {
for _, r := range cl.Config.Realms {
if r.Realm == cl.Credentials.Domain() {
if len(r.KDC) > 0 {
return true, nil
}
return false, errors.New("client krb5 config does not have any defined KDCs for the default realm")
}
}
}
return true, nil
}
// Login the client with the KDC via an AS exchange.
func (cl *Client) Login() error {
if ok, err := cl.IsConfigured(); !ok {
return err
}
if !cl.Credentials.HasPassword() && !cl.Credentials.HasKeytab() {
_, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
if err != nil {
return krberror.Errorf(err, krberror.KRBMsgError, "no user credentials available and error getting any existing session")
}
if time.Now().UTC().After(endTime) {
return krberror.New(krberror.KRBMsgError, "cannot login, no user credentials available and no valid existing session")
}
// no credentials but there is a session with tgt already
return nil
}
ASReq, err := messages.NewASReqForTGT(cl.Credentials.Domain(), cl.Config, cl.Credentials.CName())
if err != nil {
return krberror.Errorf(err, krberror.KRBMsgError, "error generating new AS_REQ")
}
ASRep, err := cl.ASExchange(cl.Credentials.Domain(), ASReq, 0)
if err != nil {
return err
}
cl.addSession(ASRep.Ticket, ASRep.DecryptedEncPart)
return nil
}
// AffirmLogin will only perform an AS exchange with the KDC if the client does not already have a TGT.
func (cl *Client) AffirmLogin() error {
_, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
if err != nil || time.Now().UTC().After(endTime) {
err := cl.Login()
if err != nil {
return fmt.Errorf("could not get valid TGT for client's realm: %v", err)
}
}
return nil
}
// realmLogin obtains or renews a TGT and establishes a session for the realm specified.
func (cl *Client) realmLogin(realm string) error {
if realm == cl.Credentials.Domain() {
return cl.Login()
}
_, endTime, _, _, err := cl.sessionTimes(cl.Credentials.Domain())
if err != nil || time.Now().UTC().After(endTime) {
err := cl.Login()
if err != nil {
return fmt.Errorf("could not get valid TGT for client's realm: %v", err)
}
}
tgt, skey, err := cl.sessionTGT(cl.Credentials.Domain())
if err != nil {
return err
}
spn := types.PrincipalName{
NameType: nametype.KRB_NT_SRV_INST,
NameString: []string{"krbtgt", realm},
}
_, tgsRep, err := cl.TGSREQGenerateAndExchange(spn, cl.Credentials.Domain(), tgt, skey, false)
if err != nil {
return err
}
cl.addSession(tgsRep.Ticket, tgsRep.DecryptedEncPart)
return nil
}
// Destroy stops the auto-renewal of all sessions and removes the sessions and cache entries from the client.
func (cl *Client) Destroy() {
creds := credentials.New("", "")
cl.sessions.destroy()
cl.cache.clear()
cl.Credentials = creds
cl.Log("client destroyed")
}
// Diagnostics runs a set of checks that the client is properly configured and writes details to the io.Writer provided.
func (cl *Client) Diagnostics(w io.Writer) error {
cl.Print(w)
var errs []string
if cl.Credentials.HasKeytab() {
var loginRealmEncTypes []int32
for _, e := range cl.Credentials.Keytab().Entries {
if e.Principal.Realm == cl.Credentials.Realm() {
loginRealmEncTypes = append(loginRealmEncTypes, e.Key.KeyType)
}
}
for _, et := range cl.Config.LibDefaults.DefaultTktEnctypeIDs {
var etInKt bool
for _, val := range loginRealmEncTypes {
if val == et {
etInKt = true
break
}
}
if !etInKt {
errs = append(errs, fmt.Sprintf("default_tkt_enctypes specifies %d but this enctype is not available in the client's keytab", et))
}
}
for _, et := range cl.Config.LibDefaults.PreferredPreauthTypes {
var etInKt bool
for _, val := range loginRealmEncTypes {
if int(val) == et {
etInKt = true
break
}
}
if !etInKt {
errs = append(errs, fmt.Sprintf("preferred_preauth_types specifies %d but this enctype is not available in the client's keytab", et))
}
}
}
udpCnt, udpKDC, err := cl.Config.GetKDCs(cl.Credentials.Realm(), false)
if err != nil {
errs = append(errs, fmt.Sprintf("error when resolving KDCs for UDP communication: %v", err))
}
if udpCnt < 1 {
errs = append(errs, "no KDCs resolved for communication via UDP.")
} else {
b, _ := json.MarshalIndent(&udpKDC, "", " ")
fmt.Fprintf(w, "UDP KDCs: %s\n", string(b))
}
tcpCnt, tcpKDC, err := cl.Config.GetKDCs(cl.Credentials.Realm(), false)
if err != nil {
errs = append(errs, fmt.Sprintf("error when resolving KDCs for TCP communication: %v", err))
}
if tcpCnt < 1 {
errs = append(errs, "no KDCs resolved for communication via TCP.")
} else {
b, _ := json.MarshalIndent(&tcpKDC, "", " ")
fmt.Fprintf(w, "TCP KDCs: %s\n", string(b))
}
if errs == nil || len(errs) < 1 {
return nil
}
err = fmt.Errorf("%s", strings.Join(errs, "\n"))
return err
}
// Print writes the details of the client to the io.Writer provided.
func (cl *Client) Print(w io.Writer) {
c, _ := cl.Credentials.JSON()
fmt.Fprintf(w, "Credentials:\n%s\n", c)
s, _ := cl.sessions.JSON()
fmt.Fprintf(w, "TGT Sessions:\n%s\n", s)
c, _ = cl.cache.JSON()
fmt.Fprintf(w, "Service ticket cache:\n%s\n", c)
s, _ = cl.settings.JSON()
fmt.Fprintf(w, "Settings:\n%s\n", s)
j, _ := cl.Config.JSON()
fmt.Fprintf(w, "Krb5 config:\n%s\n", j)
k, _ := cl.Credentials.Keytab().JSON()
fmt.Fprintf(w, "Keytab:\n%s\n", k)
}
+237
View File
@@ -0,0 +1,237 @@
package client
import (
"encoding/binary"
"fmt"
"io"
"net"
"strings"
"time"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/errorcode"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
)
// DirectDialer is a KDCDialer that uses net.DialTimeout directly. Restores the
// pre-fork behavior; intended only for callers that explicitly do not want
// their KDC traffic routed through a transport layer (e.g. unit tests).
type DirectDialer struct{}
func (DirectDialer) Dial(network, address string) (net.Conn, error) {
return net.DialTimeout(network, address, 5*time.Second)
}
// SendToKDC performs network actions to send data to the KDC.
func (cl *Client) sendToKDC(b []byte, realm string) ([]byte, error) {
var rb []byte
if cl.Config.LibDefaults.UDPPreferenceLimit == 1 {
//1 means we should always use TCP
rb, errtcp := cl.sendKDCTCP(realm, b)
if errtcp != nil {
if e, ok := errtcp.(messages.KRBError); ok {
return rb, e
}
return rb, fmt.Errorf("communication error with KDC via TCP: %v", errtcp)
}
return rb, nil
}
if len(b) <= cl.Config.LibDefaults.UDPPreferenceLimit {
//Try UDP first, TCP second
rb, errudp := cl.sendKDCUDP(realm, b)
if errudp != nil {
if e, ok := errudp.(messages.KRBError); ok && e.ErrorCode != errorcode.KRB_ERR_RESPONSE_TOO_BIG {
// Got a KRBError from KDC
// If this is not a KRB_ERR_RESPONSE_TOO_BIG we will return immediately otherwise will try TCP.
return rb, e
}
// Try TCP
r, errtcp := cl.sendKDCTCP(realm, b)
if errtcp != nil {
if e, ok := errtcp.(messages.KRBError); ok {
// Got a KRBError
return r, e
}
return r, fmt.Errorf("failed to communicate with KDC. Attempts made with UDP (%v) and then TCP (%v)", errudp, errtcp)
}
rb = r
}
return rb, nil
}
//Try TCP first, UDP second
rb, errtcp := cl.sendKDCTCP(realm, b)
if errtcp != nil {
if e, ok := errtcp.(messages.KRBError); ok {
// Got a KRBError from KDC so returning and not trying UDP.
return rb, e
}
rb, errudp := cl.sendKDCUDP(realm, b)
if errudp != nil {
if e, ok := errudp.(messages.KRBError); ok {
// Got a KRBError
return rb, e
}
return rb, fmt.Errorf("failed to communicate with KDC. Attempts made with TCP (%v) and then UDP (%v)", errtcp, errudp)
}
}
return rb, nil
}
// sendKDCUDP sends bytes to the KDC via UDP.
func (cl *Client) sendKDCUDP(realm string, b []byte) ([]byte, error) {
var r []byte
_, kdcs, err := cl.Config.GetKDCs(realm, false)
if err != nil {
return r, err
}
r, err = cl.dialSendUDP(kdcs, b)
if err != nil {
return r, err
}
return checkForKRBError(r)
}
// dialSendUDP establishes a UDP connection to a KDC via the injected dialer.
// The dialer receives the address verbatim (no local net.ResolveUDPAddr) so
// hostname resolution can be deferred to a SOCKS5 proxy when set.
func (cl *Client) dialSendUDP(kdcs map[int]string, b []byte) ([]byte, error) {
var errs []string
for i := 1; i <= len(kdcs); i++ {
conn, err := cl.dialer.Dial("udp", kdcs[i])
if err != nil {
errs = append(errs, fmt.Sprintf("error establishing UDP connection to %s: %v", kdcs[i], err))
continue
}
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
errs = append(errs, fmt.Sprintf("error setting deadline on connection to %s: %v", kdcs[i], err))
conn.Close()
continue
}
rb, err := sendUDP(conn, b)
if err != nil {
errs = append(errs, fmt.Sprintf("error sending to %s: %v", kdcs[i], err))
continue
}
return rb, nil
}
return nil, fmt.Errorf("error sending to a KDC: %s", strings.Join(errs, "; "))
}
// sendUDP sends bytes to connection over UDP. Uses the net.Conn Read/Write
// contract directly rather than the *net.UDPConn type assertion, so custom
// dialers returning any connected-UDP conn work without panic.
func sendUDP(conn net.Conn, b []byte) ([]byte, error) {
var r []byte
defer conn.Close()
_, err := conn.Write(b)
if err != nil {
return r, fmt.Errorf("error sending to (%s): %v", conn.RemoteAddr().String(), err)
}
// Max UDP payload — UDP datagrams larger than the buffer are silently
// truncated by conn.Read. 65535 covers the protocol-max datagram so a
// large KRB-ERROR or AS-REP cannot be silently mangled if UDP is ever
// re-enabled.
udpbuf := make([]byte, 65535)
n, err := conn.Read(udpbuf)
r = udpbuf[:n]
if err != nil {
return r, fmt.Errorf("sending over UDP failed to %s: %v", conn.RemoteAddr().String(), err)
}
if len(r) < 1 {
return r, fmt.Errorf("no response data from %s", conn.RemoteAddr().String())
}
return r, nil
}
// sendKDCTCP sends bytes to the KDC via TCP.
func (cl *Client) sendKDCTCP(realm string, b []byte) ([]byte, error) {
var r []byte
_, kdcs, err := cl.Config.GetKDCs(realm, true)
if err != nil {
return r, err
}
r, err = cl.dialSendTCP(kdcs, b)
if err != nil {
return r, err
}
return checkForKRBError(r)
}
// dialSendTCP establishes a TCP connection to a KDC via the injected dialer.
// The dialer receives the address verbatim (no local net.ResolveTCPAddr) so
// hostname resolution can be deferred to a SOCKS5 proxy when set.
func (cl *Client) dialSendTCP(kdcs map[int]string, b []byte) ([]byte, error) {
var errs []string
for i := 1; i <= len(kdcs); i++ {
conn, err := cl.dialer.Dial("tcp", kdcs[i])
if err != nil {
errs = append(errs, fmt.Sprintf("error establishing TCP connection to %s: %v", kdcs[i], err))
continue
}
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
errs = append(errs, fmt.Sprintf("error setting deadline on connection to %s: %v", kdcs[i], err))
conn.Close()
continue
}
rb, err := sendTCP(conn, b)
if err != nil {
errs = append(errs, fmt.Sprintf("error sending to %s: %v", kdcs[i], err))
continue
}
return rb, nil
}
return nil, fmt.Errorf("error sending to a KDC: %s", strings.Join(errs, "; "))
}
// sendTCP sends bytes to connection over TCP.
func sendTCP(conn net.Conn, b []byte) ([]byte, error) {
defer conn.Close()
var r []byte
// RFC 4120 7.2.2 specifies the first 4 bytes indicate the length of the message in big endian order.
hb := make([]byte, 4, 4)
binary.BigEndian.PutUint32(hb, uint32(len(b)))
b = append(hb, b...)
_, err := conn.Write(b)
if err != nil {
return r, fmt.Errorf("error sending to KDC (%s): %v", conn.RemoteAddr().String(), err)
}
// io.ReadFull, not conn.Read: the 4-byte length header can arrive in
// fragments over a SOCKS5 tunnel or any conn that doesn't fill on first
// read. A short read here desyncs the stream and produces a corrupt length.
sh := make([]byte, 4, 4)
_, err = io.ReadFull(conn, sh)
if err != nil {
return r, fmt.Errorf("error reading response size header: %v", err)
}
s := binary.BigEndian.Uint32(sh)
// Bound the allocation. A hostile or garbage 4-byte length prefix can
// otherwise demand up to 4 GiB and OOM the process. Real KRB messages
// (including PAC-bearing TGS responses) sit well under a megabyte; this
// cap is generous but finite. Anything larger is almost certainly an
// attacker-controlled proxy spewing HTML/junk into the framed stream.
const maxKRBResponseBytes = 10 * 1024 * 1024
if s > maxKRBResponseBytes {
return r, fmt.Errorf("KDC response size %d exceeds %d-byte cap; refusing to allocate", s, maxKRBResponseBytes)
}
rb := make([]byte, s, s)
_, err = io.ReadFull(conn, rb)
if err != nil {
return r, fmt.Errorf("error reading response: %v", err)
}
if len(rb) < 1 {
return r, fmt.Errorf("no response data from KDC %s", conn.RemoteAddr().String())
}
return rb, nil
}
// checkForKRBError checks if the response bytes from the KDC are a KRBError.
func checkForKRBError(b []byte) ([]byte, error) {
var KRBErr messages.KRBError
if err := KRBErr.Unmarshal(b); err == nil {
return b, KRBErr
}
return b, nil
}
+75
View File
@@ -0,0 +1,75 @@
package client
import (
"fmt"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/kadmin"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
)
// Kpasswd server response codes.
const (
KRB5_KPASSWD_SUCCESS = 0
KRB5_KPASSWD_MALFORMED = 1
KRB5_KPASSWD_HARDERROR = 2
KRB5_KPASSWD_AUTHERROR = 3
KRB5_KPASSWD_SOFTERROR = 4
KRB5_KPASSWD_ACCESSDENIED = 5
KRB5_KPASSWD_BAD_VERSION = 6
KRB5_KPASSWD_INITIAL_FLAG_NEEDED = 7
)
// ChangePasswd changes the password of the client to the value provided.
func (cl *Client) ChangePasswd(newPasswd string) (bool, error) {
ASReq, err := messages.NewASReqForChgPasswd(cl.Credentials.Domain(), cl.Config, cl.Credentials.CName())
if err != nil {
return false, err
}
ASRep, err := cl.ASExchange(cl.Credentials.Domain(), ASReq, 0)
if err != nil {
return false, err
}
msg, key, err := kadmin.ChangePasswdMsg(cl.Credentials.CName(), cl.Credentials.Domain(), newPasswd, ASRep.Ticket, ASRep.DecryptedEncPart.Key)
if err != nil {
return false, err
}
r, err := cl.sendToKPasswd(msg)
if err != nil {
return false, err
}
err = r.Decrypt(key)
if err != nil {
return false, err
}
if r.ResultCode != KRB5_KPASSWD_SUCCESS {
return false, fmt.Errorf("error response from kadmin: code: %d; result: %s; krberror: %v", r.ResultCode, r.Result, r.KRBError)
}
cl.Credentials.WithPassword(newPasswd)
return true, nil
}
func (cl *Client) sendToKPasswd(msg kadmin.Request) (r kadmin.Reply, err error) {
_, kps, err := cl.Config.GetKpasswdServers(cl.Credentials.Domain(), true)
if err != nil {
return
}
b, err := msg.Marshal()
if err != nil {
return
}
// TCP-only. RFC 3244 allows both UDP and TCP for kpasswd; modern KDCs
// (every AD DC, MIT krb5, Heimdal) all listen on TCP/464. We skip UDP
// because (a) the UDPPreferenceLimit logic in the rest of this fork
// forces TCP anyway, (b) a UDP→TCP fallback for kpasswd cannot rely on
// checkForKRBError since kpasswd wraps responses in RFC 3244 framing
// rather than emitting a bare ASN.1 KRBError at offset 0, and (c) under
// the project's TransportKDCDialer + ErrUDPUnderProxy contract, UDP
// would fail-closed under -proxy anyway.
rb, err := cl.dialSendTCP(kps, b)
if err != nil {
return
}
err = r.Unmarshal(rb)
return
}
+297
View File
@@ -0,0 +1,297 @@
package client
import (
"encoding/json"
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/nametype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// sessions hold TGTs and are keyed on the realm name
type sessions struct {
Entries map[string]*session
mux sync.RWMutex
}
// destroy erases all sessions
func (s *sessions) destroy() {
s.mux.Lock()
defer s.mux.Unlock()
for k, e := range s.Entries {
e.destroy()
delete(s.Entries, k)
}
}
// update replaces a session with the one provided or adds it as a new one
func (s *sessions) update(sess *session) {
s.mux.Lock()
defer s.mux.Unlock()
// if a session already exists for this, cancel its auto renew.
if i, ok := s.Entries[sess.realm]; ok {
if i != sess {
// Session in the sessions cache is not the same as one provided.
// Cancel the one in the cache and add this one.
i.mux.Lock()
defer i.mux.Unlock()
if i.cancel != nil {
i.cancel <- true
}
s.Entries[sess.realm] = sess
return
}
}
// No session for this realm was found so just add it
s.Entries[sess.realm] = sess
}
// get returns the session for the realm specified
func (s *sessions) get(realm string) (*session, bool) {
s.mux.RLock()
defer s.mux.RUnlock()
sess, ok := s.Entries[realm]
return sess, ok
}
// session holds the TGT details for a realm
type session struct {
realm string
authTime time.Time
endTime time.Time
renewTill time.Time
tgt messages.Ticket
sessionKey types.EncryptionKey
sessionKeyExpiration time.Time
cancel chan bool
mux sync.RWMutex
}
// jsonSession is used to enable marshaling some information of a session in a JSON format
type jsonSession struct {
Realm string
AuthTime time.Time
EndTime time.Time
RenewTill time.Time
SessionKeyExpiration time.Time
}
// AddSession adds a session for a realm with a TGT to the client's session cache.
// A goroutine is started to automatically renew the TGT before expiry.
func (cl *Client) addSession(tgt messages.Ticket, dep messages.EncKDCRepPart) {
if strings.ToLower(tgt.SName.NameString[0]) != "krbtgt" {
// Not a TGT
return
}
realm := tgt.SName.NameString[len(tgt.SName.NameString)-1]
s := &session{
realm: realm,
authTime: dep.AuthTime,
endTime: dep.EndTime,
renewTill: dep.RenewTill,
tgt: tgt,
sessionKey: dep.Key,
sessionKeyExpiration: dep.KeyExpiration,
}
cl.sessions.update(s)
cl.enableAutoSessionRenewal(s)
cl.Log("TGT session added for %s (EndTime: %v)", realm, dep.EndTime)
}
// update overwrites the session details with those from the TGT and decrypted encPart
func (s *session) update(tgt messages.Ticket, dep messages.EncKDCRepPart) {
s.mux.Lock()
defer s.mux.Unlock()
s.authTime = dep.AuthTime
s.endTime = dep.EndTime
s.renewTill = dep.RenewTill
s.tgt = tgt
s.sessionKey = dep.Key
s.sessionKeyExpiration = dep.KeyExpiration
}
// destroy will cancel any auto renewal of the session and set the expiration times to the current time
func (s *session) destroy() {
s.mux.Lock()
defer s.mux.Unlock()
if s.cancel != nil {
s.cancel <- true
}
s.endTime = time.Now().UTC()
s.renewTill = s.endTime
s.sessionKeyExpiration = s.endTime
}
// valid informs if the TGT is still within the valid time window
func (s *session) valid() bool {
s.mux.RLock()
defer s.mux.RUnlock()
t := time.Now().UTC()
if t.Before(s.endTime) && s.authTime.Before(t) {
return true
}
return false
}
// tgtDetails is a thread safe way to get the session's realm, TGT and session key values
func (s *session) tgtDetails() (string, messages.Ticket, types.EncryptionKey) {
s.mux.RLock()
defer s.mux.RUnlock()
return s.realm, s.tgt, s.sessionKey
}
// timeDetails is a thread safe way to get the session's validity time values
func (s *session) timeDetails() (string, time.Time, time.Time, time.Time, time.Time) {
s.mux.RLock()
defer s.mux.RUnlock()
return s.realm, s.authTime, s.endTime, s.renewTill, s.sessionKeyExpiration
}
// JSON return information about the held sessions in a JSON format.
func (s *sessions) JSON() (string, error) {
s.mux.RLock()
defer s.mux.RUnlock()
var js []jsonSession
keys := make([]string, 0, len(s.Entries))
for k := range s.Entries {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
r, at, et, rt, kt := s.Entries[k].timeDetails()
j := jsonSession{
Realm: r,
AuthTime: at,
EndTime: et,
RenewTill: rt,
SessionKeyExpiration: kt,
}
js = append(js, j)
}
b, err := json.MarshalIndent(js, "", " ")
if err != nil {
return "", err
}
return string(b), nil
}
// enableAutoSessionRenewal turns on the automatic renewal for the client's TGT session.
func (cl *Client) enableAutoSessionRenewal(s *session) {
var timer *time.Timer
s.mux.Lock()
s.cancel = make(chan bool, 1)
s.mux.Unlock()
go func(s *session) {
for {
s.mux.RLock()
w := (s.endTime.Sub(time.Now().UTC()) * 5) / 6
s.mux.RUnlock()
if w < 0 {
return
}
timer = time.NewTimer(w)
select {
case <-timer.C:
renewal, err := cl.refreshSession(s)
if err != nil {
cl.Log("error refreshing session: %v", err)
}
if !renewal && err == nil {
// end this goroutine as there will have been a new login and new auto renewal goroutine created.
return
}
case <-s.cancel:
// cancel has been called. Stop the timer and exit.
timer.Stop()
return
}
}
}(s)
}
// renewTGT renews the client's TGT session.
func (cl *Client) renewTGT(s *session) error {
realm, tgt, skey := s.tgtDetails()
spn := types.PrincipalName{
NameType: nametype.KRB_NT_SRV_INST,
NameString: []string{"krbtgt", realm},
}
_, tgsRep, err := cl.TGSREQGenerateAndExchange(spn, cl.Credentials.Domain(), tgt, skey, true)
if err != nil {
return krberror.Errorf(err, krberror.KRBMsgError, "error renewing TGT for %s", realm)
}
s.update(tgsRep.Ticket, tgsRep.DecryptedEncPart)
cl.sessions.update(s)
cl.Log("TGT session renewed for %s (EndTime: %v)", realm, tgsRep.DecryptedEncPart.EndTime)
return nil
}
// refreshSession updates either through renewal or creating a new login.
// The boolean indicates if the update was a renewal.
func (cl *Client) refreshSession(s *session) (bool, error) {
s.mux.RLock()
realm := s.realm
renewTill := s.renewTill
s.mux.RUnlock()
cl.Log("refreshing TGT session for %s", realm)
if time.Now().UTC().Before(renewTill) {
err := cl.renewTGT(s)
return true, err
}
err := cl.realmLogin(realm)
return false, err
}
// ensureValidSession makes sure there is a valid session for the realm
func (cl *Client) ensureValidSession(realm string) error {
s, ok := cl.sessions.get(realm)
if ok {
s.mux.RLock()
d := s.endTime.Sub(s.authTime) / 6
if s.endTime.Sub(time.Now().UTC()) > d {
s.mux.RUnlock()
return nil
}
s.mux.RUnlock()
_, err := cl.refreshSession(s)
return err
}
return cl.realmLogin(realm)
}
// sessionTGTDetails is a thread safe way to get the TGT and session key values for a realm
func (cl *Client) sessionTGT(realm string) (tgt messages.Ticket, sessionKey types.EncryptionKey, err error) {
err = cl.ensureValidSession(realm)
if err != nil {
return
}
s, ok := cl.sessions.get(realm)
if !ok {
err = fmt.Errorf("could not find TGT session for %s", realm)
return
}
_, tgt, sessionKey = s.tgtDetails()
return
}
// sessionTimes provides the timing information with regards to a session for the realm specified.
func (cl *Client) sessionTimes(realm string) (authTime, endTime, renewTime, sessionExp time.Time, err error) {
s, ok := cl.sessions.get(realm)
if !ok {
err = fmt.Errorf("could not find TGT session for %s", realm)
return
}
_, authTime, endTime, renewTime, sessionExp = s.timeDetails()
return
}
// spnRealm resolves the realm name of a service principal name
func (cl *Client) spnRealm(spn types.PrincipalName) string {
return cl.Config.ResolveRealm(spn.NameString[len(spn.NameString)-1])
}
+93
View File
@@ -0,0 +1,93 @@
package client
import (
"encoding/json"
"fmt"
"log"
)
// Settings holds optional client settings.
type Settings struct {
disablePAFXFast bool
assumePreAuthentication bool
preAuthEType int32
logger *log.Logger
}
// jsonSettings is used when marshaling the Settings details to JSON format.
type jsonSettings struct {
DisablePAFXFast bool
AssumePreAuthentication bool
}
// NewSettings creates a new client settings struct.
func NewSettings(settings ...func(*Settings)) *Settings {
s := new(Settings)
for _, set := range settings {
set(s)
}
return s
}
// DisablePAFXFAST used to configure the client to not use PA_FX_FAST.
//
// s := NewSettings(DisablePAFXFAST(true))
func DisablePAFXFAST(b bool) func(*Settings) {
return func(s *Settings) {
s.disablePAFXFast = b
}
}
// DisablePAFXFAST indicates is the client should disable the use of PA_FX_FAST.
func (s *Settings) DisablePAFXFAST() bool {
return s.disablePAFXFast
}
// AssumePreAuthentication used to configure the client to assume pre-authentication is required.
//
// s := NewSettings(AssumePreAuthentication(true))
func AssumePreAuthentication(b bool) func(*Settings) {
return func(s *Settings) {
s.assumePreAuthentication = b
}
}
// AssumePreAuthentication indicates if the client should proactively assume using pre-authentication.
func (s *Settings) AssumePreAuthentication() bool {
return s.assumePreAuthentication
}
// Logger used to configure client with a logger.
//
// s := NewSettings(kt, Logger(l))
func Logger(l *log.Logger) func(*Settings) {
return func(s *Settings) {
s.logger = l
}
}
// Logger returns the client logger instance.
func (s *Settings) Logger() *log.Logger {
return s.logger
}
// Log will write to the service's logger if it is configured.
func (cl *Client) Log(format string, v ...interface{}) {
if cl.settings.Logger() != nil {
cl.settings.Logger().Output(2, fmt.Sprintf(format, v...))
}
}
// JSON returns a JSON representation of the settings.
func (s *Settings) JSON() (string, error) {
js := jsonSettings{
DisablePAFXFast: s.disablePAFXFast,
AssumePreAuthentication: s.assumePreAuthentication,
}
b, err := json.MarshalIndent(js, "", " ")
if err != nil {
return "", err
}
return string(b), nil
}
+30
View File
@@ -0,0 +1,30 @@
package config
import "fmt"
// UnsupportedDirective error.
type UnsupportedDirective struct {
text string
}
// Error implements the error interface for unsupported directives.
func (e UnsupportedDirective) Error() string {
return e.text
}
// Invalid config error.
type Invalid struct {
text string
}
// Error implements the error interface for invalid config error.
func (e Invalid) Error() string {
return e.text
}
// InvalidErrorf creates a new Invalid error.
func InvalidErrorf(format string, a ...interface{}) Invalid {
return Invalid{
text: fmt.Sprintf("invalid krb5 config "+format, a...),
}
}
+141
View File
@@ -0,0 +1,141 @@
package config
import (
"fmt"
"math/rand"
"net"
"strconv"
"strings"
"github.com/jcmturner/dnsutils/v2"
)
// GetKDCs returns the count of KDCs available and a map of KDC host names keyed on preference order.
func (c *Config) GetKDCs(realm string, tcp bool) (int, map[int]string, error) {
if realm == "" {
realm = c.LibDefaults.DefaultRealm
}
kdcs := make(map[int]string)
var count int
// Get the KDCs from the krb5.conf.
var ks []string
for _, r := range c.Realms {
if r.Realm != realm {
continue
}
ks = r.KDC
}
count = len(ks)
if count > 0 {
// Order the kdcs randomly for preference.
kdcs = randServOrder(ks)
return count, kdcs, nil
}
if !c.LibDefaults.DNSLookupKDC {
return count, kdcs, fmt.Errorf("no KDCs defined in configuration for realm %s", realm)
}
// Use DNS to resolve kerberos SRV records.
proto := "udp"
if tcp {
proto = "tcp"
}
index, addrs, err := dnsutils.OrderedSRV("kerberos", proto, realm)
if err != nil {
return count, kdcs, err
}
if len(addrs) < 1 {
return count, kdcs, fmt.Errorf("no KDC SRV records found for realm %s", realm)
}
count = index
for k, v := range addrs {
kdcs[k] = strings.TrimRight(v.Target, ".") + ":" + strconv.Itoa(int(v.Port))
}
return count, kdcs, nil
}
// GetKpasswdServers returns the count of kpasswd servers available and a map of kpasswd host names keyed on preference order.
// https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html#realms - see kpasswd_server section
func (c *Config) GetKpasswdServers(realm string, tcp bool) (int, map[int]string, error) {
kdcs := make(map[int]string)
var count int
// Use DNS to resolve kerberos SRV records if configured to do so in krb5.conf.
if c.LibDefaults.DNSLookupKDC {
proto := "udp"
if tcp {
proto = "tcp"
}
c, addrs, err := dnsutils.OrderedSRV("kpasswd", proto, realm)
if err != nil {
return count, kdcs, err
}
if c < 1 {
c, addrs, err = dnsutils.OrderedSRV("kerberos-adm", proto, realm)
if err != nil {
return count, kdcs, err
}
}
if len(addrs) < 1 {
return count, kdcs, fmt.Errorf("no kpasswd or kadmin SRV records found for realm %s", realm)
}
count = c
for k, v := range addrs {
kdcs[k] = strings.TrimRight(v.Target, ".") + ":" + strconv.Itoa(int(v.Port))
}
} else {
// Get the KDCs from the krb5.conf an order them randomly for preference.
var ks []string
var ka []string
for _, r := range c.Realms {
if r.Realm == realm {
ks = r.KPasswdServer
ka = r.AdminServer
break
}
}
if len(ks) < 1 {
for _, k := range ka {
h, _, err := net.SplitHostPort(k)
if err != nil {
continue
}
ks = append(ks, h+":464")
}
}
count = len(ks)
if count < 1 {
return count, kdcs, fmt.Errorf("no kpasswd or kadmin defined in configuration for realm %s", realm)
}
kdcs = randServOrder(ks)
}
return count, kdcs, nil
}
func randServOrder(ks []string) map[int]string {
kdcs := make(map[int]string)
count := len(ks)
i := 1
if count > 1 {
l := len(ks)
for l > 0 {
ri := rand.Intn(l)
kdcs[i] = ks[ri]
if l > 1 {
// Remove the entry from the source slice by swapping with the last entry and truncating
ks[len(ks)-1], ks[ri] = ks[ri], ks[len(ks)-1]
ks = ks[:len(ks)-1]
l = len(ks)
} else {
l = 0
}
i++
}
} else {
kdcs[i] = ks[0]
}
return kdcs
}
+732
View File
@@ -0,0 +1,732 @@
// Package config implements KRB5 client and service configuration as described at https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html
package config
import (
"bufio"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"os/user"
"regexp"
"strconv"
"strings"
"time"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
)
// Config represents the KRB5 configuration.
type Config struct {
LibDefaults LibDefaults
Realms []Realm
DomainRealm DomainRealm
//CaPaths
//AppDefaults
//Plugins
}
// WeakETypeList is a list of encryption types that have been deemed weak.
const WeakETypeList = "des-cbc-crc des-cbc-md4 des-cbc-md5 des-cbc-raw des3-cbc-raw des-hmac-sha1 arcfour-hmac-exp rc4-hmac-exp arcfour-hmac-md5-exp des"
// New creates a new config struct instance.
func New() *Config {
d := make(DomainRealm)
return &Config{
LibDefaults: newLibDefaults(),
DomainRealm: d,
}
}
// LibDefaults represents the [libdefaults] section of the configuration.
type LibDefaults struct {
AllowWeakCrypto bool //default false
// ap_req_checksum_type int //unlikely to support this
Canonicalize bool //default false
CCacheType int //default is 4. unlikely to implement older
Clockskew time.Duration //max allowed skew in seconds, default 300
//Default_ccache_name string // default /tmp/krb5cc_%{uid} //Not implementing as will hold in memory
DefaultClientKeytabName string //default /usr/local/var/krb5/user/%{euid}/client.keytab
DefaultKeytabName string //default /etc/krb5.keytab
DefaultRealm string
DefaultTGSEnctypes []string //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4
DefaultTktEnctypes []string //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4
DefaultTGSEnctypeIDs []int32 //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4
DefaultTktEnctypeIDs []int32 //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4
DNSCanonicalizeHostname bool //default true
DNSLookupKDC bool //default false
DNSLookupRealm bool
ExtraAddresses []net.IP //Not implementing yet
Forwardable bool //default false
IgnoreAcceptorHostname bool //default false
K5LoginAuthoritative bool //default false
K5LoginDirectory string //default user's home directory. Must be owned by the user or root
KDCDefaultOptions asn1.BitString //default 0x00000010 (KDC_OPT_RENEWABLE_OK)
KDCTimeSync int //default 1
//kdc_req_checksum_type int //unlikely to implement as for very old KDCs
NoAddresses bool //default true
PermittedEnctypes []string //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4
PermittedEnctypeIDs []int32
//plugin_base_dir string //not supporting plugins
PreferredPreauthTypes []int //default “17, 16, 15, 14”, which forces libkrb5 to attempt to use PKINIT if it is supported
Proxiable bool //default false
RDNS bool //default true
RealmTryDomains int //default -1
RenewLifetime time.Duration //default 0
SafeChecksumType int //default 8
TicketLifetime time.Duration //default 1 day
UDPPreferenceLimit int // 1 means to always use tcp. MIT krb5 has a default value of 1465, and it prevents user setting more than 32700.
VerifyAPReqNofail bool //default false
}
// Create a new LibDefaults struct.
func newLibDefaults() LibDefaults {
uid := "0"
var hdir string
usr, _ := user.Current()
if usr != nil {
uid = usr.Uid
hdir = usr.HomeDir
}
opts := asn1.BitString{}
opts.Bytes, _ = hex.DecodeString("00000010")
opts.BitLength = len(opts.Bytes) * 8
l := LibDefaults{
CCacheType: 4,
Clockskew: time.Duration(300) * time.Second,
DefaultClientKeytabName: fmt.Sprintf("/usr/local/var/krb5/user/%s/client.keytab", uid),
DefaultKeytabName: "/etc/krb5.keytab",
DefaultTGSEnctypes: []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "des3-cbc-sha1", "arcfour-hmac-md5", "camellia256-cts-cmac", "camellia128-cts-cmac", "des-cbc-crc", "des-cbc-md5", "des-cbc-md4"},
DefaultTktEnctypes: []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "des3-cbc-sha1", "arcfour-hmac-md5", "camellia256-cts-cmac", "camellia128-cts-cmac", "des-cbc-crc", "des-cbc-md5", "des-cbc-md4"},
DNSCanonicalizeHostname: true,
K5LoginDirectory: hdir,
KDCDefaultOptions: opts,
KDCTimeSync: 1,
NoAddresses: true,
PermittedEnctypes: []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "des3-cbc-sha1", "arcfour-hmac-md5", "camellia256-cts-cmac", "camellia128-cts-cmac", "des-cbc-crc", "des-cbc-md5", "des-cbc-md4"},
RDNS: true,
RealmTryDomains: -1,
SafeChecksumType: 8,
TicketLifetime: time.Duration(24) * time.Hour,
UDPPreferenceLimit: 1465,
PreferredPreauthTypes: []int{17, 16, 15, 14},
}
l.DefaultTGSEnctypeIDs = parseETypes(l.DefaultTGSEnctypes, l.AllowWeakCrypto)
l.DefaultTktEnctypeIDs = parseETypes(l.DefaultTktEnctypes, l.AllowWeakCrypto)
l.PermittedEnctypeIDs = parseETypes(l.PermittedEnctypes, l.AllowWeakCrypto)
return l
}
// Parse the lines of the [libdefaults] section of the configuration into the LibDefaults struct.
func (l *LibDefaults) parseLines(lines []string) error {
for _, line := range lines {
//Remove comments after the values
if idx := strings.IndexAny(line, "#;"); idx != -1 {
line = line[:idx]
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
if !strings.Contains(line, "=") {
return InvalidErrorf("libdefaults section line (%s)", line)
}
p := strings.Split(line, "=")
key := strings.TrimSpace(strings.ToLower(p[0]))
switch key {
case "allow_weak_crypto":
v, err := parseBoolean(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.AllowWeakCrypto = v
case "canonicalize":
v, err := parseBoolean(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.Canonicalize = v
case "ccache_type":
p[1] = strings.TrimSpace(p[1])
v, err := strconv.ParseUint(p[1], 10, 32)
if err != nil || v < 0 || v > 4 {
return InvalidErrorf("libdefaults section line (%s)", line)
}
l.CCacheType = int(v)
case "clockskew":
d, err := parseDuration(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.Clockskew = d
case "default_client_keytab_name":
l.DefaultClientKeytabName = strings.TrimSpace(p[1])
case "default_keytab_name":
l.DefaultKeytabName = strings.TrimSpace(p[1])
case "default_realm":
l.DefaultRealm = strings.TrimSpace(p[1])
case "default_tgs_enctypes":
l.DefaultTGSEnctypes = strings.Fields(p[1])
case "default_tkt_enctypes":
l.DefaultTktEnctypes = strings.Fields(p[1])
case "dns_canonicalize_hostname":
v, err := parseBoolean(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.DNSCanonicalizeHostname = v
case "dns_lookup_kdc":
v, err := parseBoolean(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.DNSLookupKDC = v
case "dns_lookup_realm":
v, err := parseBoolean(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.DNSLookupRealm = v
case "extra_addresses":
ipStr := strings.TrimSpace(p[1])
for _, ip := range strings.Split(ipStr, ",") {
if eip := net.ParseIP(ip); eip != nil {
l.ExtraAddresses = append(l.ExtraAddresses, eip)
}
}
case "forwardable":
v, err := parseBoolean(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.Forwardable = v
case "ignore_acceptor_hostname":
v, err := parseBoolean(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.IgnoreAcceptorHostname = v
case "k5login_authoritative":
v, err := parseBoolean(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.K5LoginAuthoritative = v
case "k5login_directory":
l.K5LoginDirectory = strings.TrimSpace(p[1])
case "kdc_default_options":
v := strings.TrimSpace(p[1])
v = strings.Replace(v, "0x", "", -1)
b, err := hex.DecodeString(v)
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.KDCDefaultOptions.Bytes = b
l.KDCDefaultOptions.BitLength = len(b) * 8
case "kdc_timesync":
p[1] = strings.TrimSpace(p[1])
v, err := strconv.ParseInt(p[1], 10, 32)
if err != nil || v < 0 {
return InvalidErrorf("libdefaults section line (%s)", line)
}
l.KDCTimeSync = int(v)
case "noaddresses":
v, err := parseBoolean(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.NoAddresses = v
case "permitted_enctypes":
l.PermittedEnctypes = strings.Fields(p[1])
case "preferred_preauth_types":
p[1] = strings.TrimSpace(p[1])
t := strings.Split(p[1], ",")
var v []int
for _, s := range t {
i, err := strconv.ParseInt(s, 10, 32)
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
v = append(v, int(i))
}
l.PreferredPreauthTypes = v
case "proxiable":
v, err := parseBoolean(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.Proxiable = v
case "rdns":
v, err := parseBoolean(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.RDNS = v
case "realm_try_domains":
p[1] = strings.TrimSpace(p[1])
v, err := strconv.ParseInt(p[1], 10, 32)
if err != nil || v < -1 {
return InvalidErrorf("libdefaults section line (%s)", line)
}
l.RealmTryDomains = int(v)
case "renew_lifetime":
d, err := parseDuration(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.RenewLifetime = d
case "safe_checksum_type":
p[1] = strings.TrimSpace(p[1])
v, err := strconv.ParseInt(p[1], 10, 32)
if err != nil || v < 0 {
return InvalidErrorf("libdefaults section line (%s)", line)
}
l.SafeChecksumType = int(v)
case "ticket_lifetime":
d, err := parseDuration(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.TicketLifetime = d
case "udp_preference_limit":
p[1] = strings.TrimSpace(p[1])
v, err := strconv.ParseUint(p[1], 10, 32)
if err != nil || v > 32700 {
return InvalidErrorf("libdefaults section line (%s)", line)
}
l.UDPPreferenceLimit = int(v)
case "verify_ap_req_nofail":
v, err := parseBoolean(p[1])
if err != nil {
return InvalidErrorf("libdefaults section line (%s): %v", line, err)
}
l.VerifyAPReqNofail = v
}
}
l.DefaultTGSEnctypeIDs = parseETypes(l.DefaultTGSEnctypes, l.AllowWeakCrypto)
l.DefaultTktEnctypeIDs = parseETypes(l.DefaultTktEnctypes, l.AllowWeakCrypto)
l.PermittedEnctypeIDs = parseETypes(l.PermittedEnctypes, l.AllowWeakCrypto)
return nil
}
// Realm represents an entry in the [realms] section of the configuration.
type Realm struct {
Realm string
AdminServer []string
//auth_to_local //Not implementing for now
//auth_to_local_names //Not implementing for now
DefaultDomain string
KDC []string
KPasswdServer []string //default admin_server:464
MasterKDC []string
}
// Parse the lines of a [realms] entry into the Realm struct.
func (r *Realm) parseLines(name string, lines []string) (err error) {
r.Realm = name
var adminServerFinal bool
var KDCFinal bool
var kpasswdServerFinal bool
var masterKDCFinal bool
var ignore bool
var c int // counts the depth of blocks within brackets { }
for _, line := range lines {
if ignore && c > 0 && !strings.Contains(line, "{") && !strings.Contains(line, "}") {
continue
}
//Remove comments after the values
if idx := strings.IndexAny(line, "#;"); idx != -1 {
line = line[:idx]
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
if !strings.Contains(line, "=") && !strings.Contains(line, "}") {
return InvalidErrorf("realms section line (%s)", line)
}
if strings.Contains(line, "v4_") {
ignore = true
err = UnsupportedDirective{"v4 configurations are not supported"}
}
if strings.Contains(line, "{") {
c++
if ignore {
continue
}
}
if strings.Contains(line, "}") {
c--
if c < 0 {
return InvalidErrorf("unpaired curly brackets")
}
if ignore {
if c < 1 {
c = 0
ignore = false
}
continue
}
}
p := strings.Split(line, "=")
key := strings.TrimSpace(strings.ToLower(p[0]))
v := strings.TrimSpace(p[1])
switch key {
case "admin_server":
appendUntilFinal(&r.AdminServer, v, &adminServerFinal)
case "default_domain":
r.DefaultDomain = v
case "kdc":
if !strings.Contains(v, ":") {
// No port number specified default to 88
if strings.HasSuffix(v, `*`) {
v = strings.TrimSpace(strings.TrimSuffix(v, `*`)) + ":88*"
} else {
v = strings.TrimSpace(v) + ":88"
}
}
appendUntilFinal(&r.KDC, v, &KDCFinal)
case "kpasswd_server":
appendUntilFinal(&r.KPasswdServer, v, &kpasswdServerFinal)
case "master_kdc":
appendUntilFinal(&r.MasterKDC, v, &masterKDCFinal)
}
}
//default for Kpasswd_server = admin_server:464
if len(r.KPasswdServer) < 1 {
for _, a := range r.AdminServer {
s := strings.Split(a, ":")
r.KPasswdServer = append(r.KPasswdServer, s[0]+":464")
}
}
return
}
// Parse the lines of the [realms] section of the configuration into an slice of Realm structs.
func parseRealms(lines []string) (realms []Realm, err error) {
var name string
var start int
var c int
for i, l := range lines {
//Remove comments after the values
if idx := strings.IndexAny(l, "#;"); idx != -1 {
l = l[:idx]
}
l = strings.TrimSpace(l)
if l == "" {
continue
}
//if strings.Contains(l, "v4_") {
// return nil, errors.New("v4 configurations are not supported in Realms section")
//}
if strings.Contains(l, "{") {
c++
if !strings.Contains(l, "=") {
return nil, fmt.Errorf("realm configuration line invalid: %s", l)
}
if c == 1 {
start = i
p := strings.Split(l, "=")
name = strings.TrimSpace(p[0])
}
}
if strings.Contains(l, "}") {
if c < 1 {
// but not started a block!!!
return nil, errors.New("invalid Realms section in configuration")
}
c--
if c == 0 {
var r Realm
e := r.parseLines(name, lines[start+1:i])
if e != nil {
if _, ok := e.(UnsupportedDirective); !ok {
err = e
return
}
err = e
}
realms = append(realms, r)
}
}
}
return
}
// DomainRealm maps the domains to realms representing the [domain_realm] section of the configuration.
type DomainRealm map[string]string
// Parse the lines of the [domain_realm] section of the configuration and add to the mapping.
func (d *DomainRealm) parseLines(lines []string) error {
for _, line := range lines {
//Remove comments after the values
if idx := strings.IndexAny(line, "#;"); idx != -1 {
line = line[:idx]
}
if strings.TrimSpace(line) == "" {
continue
}
if !strings.Contains(line, "=") {
return InvalidErrorf("realm line (%s)", line)
}
p := strings.Split(line, "=")
domain := strings.TrimSpace(strings.ToLower(p[0]))
realm := strings.TrimSpace(p[1])
d.addMapping(domain, realm)
}
return nil
}
// Add a domain to realm mapping.
func (d *DomainRealm) addMapping(domain, realm string) {
(*d)[domain] = realm
}
// Delete a domain to realm mapping.
func (d *DomainRealm) deleteMapping(domain, realm string) {
delete(*d, domain)
}
// ResolveRealm resolves the kerberos realm for the specified domain name from the domain to realm mapping.
// The most specific mapping is returned.
func (c *Config) ResolveRealm(domainName string) string {
domainName = strings.TrimSuffix(domainName, ".")
// Try to match the entire hostname first
if r, ok := c.DomainRealm[domainName]; ok {
return r
}
// Try to match all DNS domain parts
periods := strings.Count(domainName, ".") + 1
for i := 2; i <= periods; i++ {
z := strings.SplitN(domainName, ".", i)
if r, ok := c.DomainRealm["."+z[len(z)-1]]; ok {
return r
}
}
return ""
}
// Load the KRB5 configuration from the specified file path.
func Load(cfgPath string) (*Config, error) {
fh, err := os.Open(cfgPath)
if err != nil {
return nil, errors.New("configuration file could not be opened: " + cfgPath + " " + err.Error())
}
defer fh.Close()
scanner := bufio.NewScanner(fh)
return NewFromScanner(scanner)
}
// NewFromString creates a new Config struct from a string.
func NewFromString(s string) (*Config, error) {
reader := strings.NewReader(s)
return NewFromReader(reader)
}
// NewFromReader creates a new Config struct from an io.Reader.
func NewFromReader(r io.Reader) (*Config, error) {
scanner := bufio.NewScanner(r)
return NewFromScanner(scanner)
}
// NewFromScanner creates a new Config struct from a bufio.Scanner.
func NewFromScanner(scanner *bufio.Scanner) (*Config, error) {
c := New()
var e error
sections := make(map[int]string)
var sectionLineNum []int
var lines []string
for scanner.Scan() {
// Skip comments and blank lines
if matched, _ := regexp.MatchString(`^\s*(#|;|\n)`, scanner.Text()); matched {
continue
}
if matched, _ := regexp.MatchString(`^\s*\[libdefaults\]\s*`, scanner.Text()); matched {
sections[len(lines)] = "libdefaults"
sectionLineNum = append(sectionLineNum, len(lines))
continue
}
if matched, _ := regexp.MatchString(`^\s*\[realms\]\s*`, scanner.Text()); matched {
sections[len(lines)] = "realms"
sectionLineNum = append(sectionLineNum, len(lines))
continue
}
if matched, _ := regexp.MatchString(`^\s*\[domain_realm\]\s*`, scanner.Text()); matched {
sections[len(lines)] = "domain_realm"
sectionLineNum = append(sectionLineNum, len(lines))
continue
}
if matched, _ := regexp.MatchString(`^\s*\[.*\]\s*`, scanner.Text()); matched {
sections[len(lines)] = "unknown_section"
sectionLineNum = append(sectionLineNum, len(lines))
continue
}
lines = append(lines, scanner.Text())
}
for i, start := range sectionLineNum {
var end int
if i+1 >= len(sectionLineNum) {
end = len(lines)
} else {
end = sectionLineNum[i+1]
}
switch section := sections[start]; section {
case "libdefaults":
err := c.LibDefaults.parseLines(lines[start:end])
if err != nil {
if _, ok := err.(UnsupportedDirective); !ok {
return nil, fmt.Errorf("error processing libdefaults section: %v", err)
}
e = err
}
case "realms":
realms, err := parseRealms(lines[start:end])
if err != nil {
if _, ok := err.(UnsupportedDirective); !ok {
return nil, fmt.Errorf("error processing realms section: %v", err)
}
e = err
}
c.Realms = realms
case "domain_realm":
err := c.DomainRealm.parseLines(lines[start:end])
if err != nil {
if _, ok := err.(UnsupportedDirective); !ok {
return nil, fmt.Errorf("error processing domaain_realm section: %v", err)
}
e = err
}
}
}
return c, e
}
// Parse a space delimited list of ETypes into a list of EType numbers optionally filtering out weak ETypes.
func parseETypes(s []string, w bool) []int32 {
var eti []int32
for _, et := range s {
if !w {
var weak bool
for _, wet := range strings.Fields(WeakETypeList) {
if et == wet {
weak = true
break
}
}
if weak {
continue
}
}
i := etypeID.EtypeSupported(et)
if i != 0 {
eti = append(eti, i)
}
}
return eti
}
// Parse a time duration string in the configuration to a golang time.Duration.
func parseDuration(s string) (time.Duration, error) {
s = strings.Replace(strings.TrimSpace(s), " ", "", -1)
// handle Nd[NmNs]
if strings.Contains(s, "d") {
ds := strings.SplitN(s, "d", 2)
dn, err := strconv.ParseUint(ds[0], 10, 32)
if err != nil {
return time.Duration(0), errors.New("invalid time duration")
}
d := time.Duration(dn*24) * time.Hour
if ds[1] != "" {
dp, err := time.ParseDuration(ds[1])
if err != nil {
return time.Duration(0), errors.New("invalid time duration")
}
d = d + dp
}
return d, nil
}
// handle Nm[Ns]
d, err := time.ParseDuration(s)
if err == nil {
return d, nil
}
// handle N
v, err := strconv.ParseUint(s, 10, 32)
if err == nil && v > 0 {
return time.Duration(v) * time.Second, nil
}
// handle h:m[:s]
if strings.Contains(s, ":") {
t := strings.Split(s, ":")
if 2 > len(t) || len(t) > 3 {
return time.Duration(0), errors.New("invalid time duration value")
}
var i []int
for _, n := range t {
j, err := strconv.ParseInt(n, 10, 16)
if err != nil {
return time.Duration(0), errors.New("invalid time duration value")
}
i = append(i, int(j))
}
d := time.Duration(i[0])*time.Hour + time.Duration(i[1])*time.Minute
if len(i) == 3 {
d = d + time.Duration(i[2])*time.Second
}
return d, nil
}
return time.Duration(0), errors.New("invalid time duration value")
}
// Parse possible boolean values to golang bool.
func parseBoolean(s string) (bool, error) {
s = strings.TrimSpace(s)
v, err := strconv.ParseBool(s)
if err == nil {
return v, nil
}
switch strings.ToLower(s) {
case "yes":
return true, nil
case "y":
return true, nil
case "no":
return false, nil
case "n":
return false, nil
}
return false, errors.New("invalid boolean value")
}
// Parse array of strings but stop if an asterisk is placed at the end of a line.
func appendUntilFinal(s *[]string, value string, final *bool) {
if *final {
return
}
if last := len(value) - 1; last >= 0 && value[last] == '*' {
*final = true
value = value[:len(value)-1]
}
*s = append(*s, value)
}
// JSON return details of the config in a JSON format.
func (c *Config) JSON() (string, error) {
b, err := json.MarshalIndent(c, "", " ")
if err != nil {
return "", err
}
return string(b), nil
}
+333
View File
@@ -0,0 +1,333 @@
package credentials
import (
"bytes"
"encoding/binary"
"errors"
"os"
"strings"
"time"
"unsafe"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
const (
headerFieldTagKDCOffset = 1
)
// CCache is the file credentials cache as define here: https://web.mit.edu/kerberos/krb5-latest/doc/formats/ccache_file_format.html
type CCache struct {
Version uint8
Header header
DefaultPrincipal principal
Credentials []*Credential
Path string
}
type header struct {
length uint16
fields []headerField
}
type headerField struct {
tag uint16
length uint16
value []byte
}
// Credential cache entry principal struct.
type principal struct {
Realm string
PrincipalName types.PrincipalName
}
// Credential holds a Kerberos client's ccache credential information.
type Credential struct {
Client principal
Server principal
Key types.EncryptionKey
AuthTime time.Time
StartTime time.Time
EndTime time.Time
RenewTill time.Time
IsSKey bool
TicketFlags asn1.BitString
Addresses []types.HostAddress
AuthData []types.AuthorizationDataEntry
Ticket []byte
SecondTicket []byte
}
// LoadCCache loads a credential cache file into a CCache type.
func LoadCCache(cpath string) (*CCache, error) {
c := new(CCache)
b, err := os.ReadFile(cpath)
if err != nil {
return c, err
}
err = c.Unmarshal(b)
return c, err
}
// Unmarshal a byte slice of credential cache data into CCache type.
func (c *CCache) Unmarshal(b []byte) error {
p := 0
//The first byte of the file always has the value 5
if int8(b[p]) != 5 {
return errors.New("Invalid credential cache data. First byte does not equal 5")
}
p++
//Get credential cache version
//The second byte contains the version number (1 to 4)
c.Version = b[p]
if c.Version < 1 || c.Version > 4 {
return errors.New("Invalid credential cache data. Keytab version is not within 1 to 4")
}
p++
//Version 1 or 2 of the file format uses native byte order for integer representations. Versions 3 & 4 always uses big-endian byte order
var endian binary.ByteOrder
endian = binary.BigEndian
if (c.Version == 1 || c.Version == 2) && isNativeEndianLittle() {
endian = binary.LittleEndian
}
if c.Version == 4 {
err := parseHeader(b, &p, c, &endian)
if err != nil {
return err
}
}
c.DefaultPrincipal = parsePrincipal(b, &p, c, &endian)
for p < len(b) {
cred, err := parseCredential(b, &p, c, &endian)
if err != nil {
return err
}
c.Credentials = append(c.Credentials, cred)
}
return nil
}
func parseHeader(b []byte, p *int, c *CCache, e *binary.ByteOrder) error {
if c.Version != 4 {
return errors.New("Credentials cache version is not 4 so there is no header to parse.")
}
h := header{}
h.length = uint16(readInt16(b, p, e))
for *p <= int(h.length) {
f := headerField{}
f.tag = uint16(readInt16(b, p, e))
f.length = uint16(readInt16(b, p, e))
f.value = b[*p : *p+int(f.length)]
*p += int(f.length)
if !f.valid() {
return errors.New("Invalid credential cache header found")
}
h.fields = append(h.fields, f)
}
c.Header = h
return nil
}
// Parse the Keytab bytes of a principal into a Keytab entry's principal.
func parsePrincipal(b []byte, p *int, c *CCache, e *binary.ByteOrder) (princ principal) {
if c.Version != 1 {
//Name Type is omitted in version 1
princ.PrincipalName.NameType = readInt32(b, p, e)
}
nc := int(readInt32(b, p, e))
if c.Version == 1 {
//In version 1 the number of components includes the realm. Minus 1 to make consistent with version 2
nc--
}
lenRealm := readInt32(b, p, e)
princ.Realm = string(readBytes(b, p, int(lenRealm), e))
for i := 0; i < nc; i++ {
l := readInt32(b, p, e)
princ.PrincipalName.NameString = append(princ.PrincipalName.NameString, string(readBytes(b, p, int(l), e)))
}
return princ
}
func parseCredential(b []byte, p *int, c *CCache, e *binary.ByteOrder) (cred *Credential, err error) {
cred = new(Credential)
cred.Client = parsePrincipal(b, p, c, e)
cred.Server = parsePrincipal(b, p, c, e)
key := types.EncryptionKey{}
key.KeyType = int32(readInt16(b, p, e))
if c.Version == 3 {
//repeated twice in version 3
key.KeyType = int32(readInt16(b, p, e))
}
key.KeyValue = readData(b, p, e)
cred.Key = key
cred.AuthTime = readTimestamp(b, p, e)
cred.StartTime = readTimestamp(b, p, e)
cred.EndTime = readTimestamp(b, p, e)
cred.RenewTill = readTimestamp(b, p, e)
if ik := readInt8(b, p, e); ik == 0 {
cred.IsSKey = false
} else {
cred.IsSKey = true
}
cred.TicketFlags = types.NewKrbFlags()
cred.TicketFlags.Bytes = readBytes(b, p, 4, e)
l := int(readInt32(b, p, e))
cred.Addresses = make([]types.HostAddress, l, l)
for i := range cred.Addresses {
cred.Addresses[i] = readAddress(b, p, e)
}
l = int(readInt32(b, p, e))
cred.AuthData = make([]types.AuthorizationDataEntry, l, l)
for i := range cred.AuthData {
cred.AuthData[i] = readAuthDataEntry(b, p, e)
}
cred.Ticket = readData(b, p, e)
cred.SecondTicket = readData(b, p, e)
return
}
// GetClientPrincipalName returns a PrincipalName type for the client the credentials cache is for.
func (c *CCache) GetClientPrincipalName() types.PrincipalName {
return c.DefaultPrincipal.PrincipalName
}
// GetClientRealm returns the reals of the client the credentials cache is for.
func (c *CCache) GetClientRealm() string {
return c.DefaultPrincipal.Realm
}
// GetClientCredentials returns a Credentials object representing the client of the credentials cache.
func (c *CCache) GetClientCredentials() *Credentials {
return &Credentials{
username: c.DefaultPrincipal.PrincipalName.PrincipalNameString(),
realm: c.GetClientRealm(),
cname: c.DefaultPrincipal.PrincipalName,
}
}
// Contains tests if the cache contains a credential for the provided server PrincipalName
func (c *CCache) Contains(p types.PrincipalName) bool {
for _, cred := range c.Credentials {
if cred.Server.PrincipalName.Equal(p) {
return true
}
}
return false
}
// GetEntry returns a specific credential for the PrincipalName provided.
func (c *CCache) GetEntry(p types.PrincipalName) (*Credential, bool) {
cred := new(Credential)
var found bool
for i := range c.Credentials {
if c.Credentials[i].Server.PrincipalName.Equal(p) {
cred = c.Credentials[i]
found = true
break
}
}
if !found {
return cred, false
}
return cred, true
}
// GetEntries filters out configuration entries an returns a slice of credentials.
func (c *CCache) GetEntries() []*Credential {
creds := make([]*Credential, 0)
for _, cred := range c.Credentials {
// Filter out configuration entries
if strings.HasPrefix(cred.Server.Realm, "X-CACHECONF") {
continue
}
creds = append(creds, cred)
}
return creds
}
func (h *headerField) valid() bool {
// See https://web.mit.edu/kerberos/krb5-latest/doc/formats/ccache_file_format.html - Header format
switch h.tag {
case headerFieldTagKDCOffset:
if h.length != 8 || len(h.value) != 8 {
return false
}
return true
}
return false
}
func readData(b []byte, p *int, e *binary.ByteOrder) []byte {
l := readInt32(b, p, e)
return readBytes(b, p, int(l), e)
}
func readAddress(b []byte, p *int, e *binary.ByteOrder) types.HostAddress {
a := types.HostAddress{}
a.AddrType = int32(readInt16(b, p, e))
a.Address = readData(b, p, e)
return a
}
func readAuthDataEntry(b []byte, p *int, e *binary.ByteOrder) types.AuthorizationDataEntry {
a := types.AuthorizationDataEntry{}
a.ADType = int32(readInt16(b, p, e))
a.ADData = readData(b, p, e)
return a
}
// Read bytes representing a timestamp.
func readTimestamp(b []byte, p *int, e *binary.ByteOrder) time.Time {
return time.Unix(int64(readInt32(b, p, e)), 0)
}
// Read bytes representing an eight bit integer.
func readInt8(b []byte, p *int, e *binary.ByteOrder) (i int8) {
buf := bytes.NewBuffer(b[*p : *p+1])
binary.Read(buf, *e, &i)
*p++
return
}
// Read bytes representing a sixteen bit integer.
func readInt16(b []byte, p *int, e *binary.ByteOrder) (i int16) {
buf := bytes.NewBuffer(b[*p : *p+2])
binary.Read(buf, *e, &i)
*p += 2
return
}
// Read bytes representing a thirty two bit integer.
func readInt32(b []byte, p *int, e *binary.ByteOrder) (i int32) {
buf := bytes.NewBuffer(b[*p : *p+4])
binary.Read(buf, *e, &i)
*p += 4
return
}
func readBytes(b []byte, p *int, s int, e *binary.ByteOrder) []byte {
buf := bytes.NewBuffer(b[*p : *p+s])
r := make([]byte, s)
binary.Read(buf, *e, &r)
*p += s
return r
}
func isNativeEndianLittle() bool {
var x = 0x012345678
var p = unsafe.Pointer(&x)
var bp = (*[4]byte)(p)
var endian bool
if 0x01 == bp[0] {
endian = false
} else if (0x78 & 0xff) == (bp[0] & 0xff) {
endian = true
} else {
// Default to big endian
endian = false
}
return endian
}
+405
View File
@@ -0,0 +1,405 @@
// Package credentials provides credentials management for Kerberos 5 authentication.
package credentials
import (
"bytes"
"encoding/gob"
"encoding/json"
"time"
"github.com/hashicorp/go-uuid"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/nametype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/keytab"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
const (
// AttributeKeyADCredentials assigned number for AD credentials.
AttributeKeyADCredentials = "gokrb5AttributeKeyADCredentials"
)
// Credentials struct for a user.
// Contains either a keytab, password or both.
// Keytabs are used over passwords if both are defined.
type Credentials struct {
username string
displayName string
realm string
cname types.PrincipalName
keytab *keytab.Keytab
password string
attributes map[string]interface{}
validUntil time.Time
authenticated bool
human bool
authTime time.Time
groupMembership map[string]bool
sessionID string
}
// marshalCredentials is used to enable marshaling and unmarshaling of credentials
// without having exported fields on the Credentials struct
type marshalCredentials struct {
Username string
DisplayName string
Realm string
CName types.PrincipalName `json:"-"`
Keytab bool
Password bool
Attributes map[string]interface{} `json:"-"`
ValidUntil time.Time
Authenticated bool
Human bool
AuthTime time.Time
GroupMembership map[string]bool `json:"-"`
SessionID string
}
// ADCredentials contains information obtained from the PAC.
type ADCredentials struct {
EffectiveName string
FullName string
UserID int
PrimaryGroupID int
LogOnTime time.Time
LogOffTime time.Time
PasswordLastSet time.Time
GroupMembershipSIDs []string
LogonDomainName string
LogonDomainID string
LogonServer string
}
// New creates a new Credentials instance.
func New(username string, realm string) *Credentials {
uid, err := uuid.GenerateUUID()
if err != nil {
uid = "00unique-sess-ions-uuid-unavailable0"
}
return &Credentials{
username: username,
displayName: username,
realm: realm,
cname: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, username),
keytab: keytab.New(),
attributes: make(map[string]interface{}),
groupMembership: make(map[string]bool),
sessionID: uid,
human: true,
}
}
// NewFromPrincipalName creates a new Credentials instance with the user details provides as a PrincipalName type.
func NewFromPrincipalName(cname types.PrincipalName, realm string) *Credentials {
c := New(cname.PrincipalNameString(), realm)
c.cname = cname
return c
}
// WithKeytab sets the Keytab in the Credentials struct.
func (c *Credentials) WithKeytab(kt *keytab.Keytab) *Credentials {
c.keytab = kt
c.password = ""
return c
}
// Keytab returns the credential's Keytab.
func (c *Credentials) Keytab() *keytab.Keytab {
return c.keytab
}
// HasKeytab queries if the Credentials has a keytab defined.
func (c *Credentials) HasKeytab() bool {
if c.keytab != nil && len(c.keytab.Entries) > 0 {
return true
}
return false
}
// WithPassword sets the password in the Credentials struct.
func (c *Credentials) WithPassword(password string) *Credentials {
c.password = password
c.keytab = keytab.New() // clear any keytab
return c
}
// Password returns the credential's password.
func (c *Credentials) Password() string {
return c.password
}
// HasPassword queries if the Credentials has a password defined.
func (c *Credentials) HasPassword() bool {
if c.password != "" {
return true
}
return false
}
// SetValidUntil sets the expiry time of the credentials
func (c *Credentials) SetValidUntil(t time.Time) {
c.validUntil = t
}
// SetADCredentials adds ADCredentials attributes to the credentials
func (c *Credentials) SetADCredentials(a ADCredentials) {
c.SetAttribute(AttributeKeyADCredentials, a)
if a.FullName != "" {
c.SetDisplayName(a.FullName)
}
if a.EffectiveName != "" {
c.SetUserName(a.EffectiveName)
}
for i := range a.GroupMembershipSIDs {
c.AddAuthzAttribute(a.GroupMembershipSIDs[i])
}
}
// GetADCredentials returns ADCredentials attributes sorted in the credential
func (c *Credentials) GetADCredentials() ADCredentials {
if a, ok := c.attributes[AttributeKeyADCredentials].(ADCredentials); ok {
return a
}
return ADCredentials{}
}
// Methods to implement goidentity.Identity interface
// UserName returns the credential's username.
func (c *Credentials) UserName() string {
return c.username
}
// SetUserName sets the username value on the credential.
func (c *Credentials) SetUserName(s string) {
c.username = s
}
// CName returns the credential's client principal name.
func (c *Credentials) CName() types.PrincipalName {
return c.cname
}
// SetCName sets the client principal name on the credential.
func (c *Credentials) SetCName(pn types.PrincipalName) {
c.cname = pn
}
// Domain returns the credential's domain.
func (c *Credentials) Domain() string {
return c.realm
}
// SetDomain sets the domain value on the credential.
func (c *Credentials) SetDomain(s string) {
c.realm = s
}
// Realm returns the credential's realm. Same as the domain.
func (c *Credentials) Realm() string {
return c.Domain()
}
// SetRealm sets the realm value on the credential. Same as the domain
func (c *Credentials) SetRealm(s string) {
c.SetDomain(s)
}
// DisplayName returns the credential's display name.
func (c *Credentials) DisplayName() string {
return c.displayName
}
// SetDisplayName sets the display name value on the credential.
func (c *Credentials) SetDisplayName(s string) {
c.displayName = s
}
// Human returns if the credential represents a human or not.
func (c *Credentials) Human() bool {
return c.human
}
// SetHuman sets the credential as human.
func (c *Credentials) SetHuman(b bool) {
c.human = b
}
// AuthTime returns the time the credential was authenticated.
func (c *Credentials) AuthTime() time.Time {
return c.authTime
}
// SetAuthTime sets the time the credential was authenticated.
func (c *Credentials) SetAuthTime(t time.Time) {
c.authTime = t
}
// AuthzAttributes returns the credentials authorizing attributes.
func (c *Credentials) AuthzAttributes() []string {
s := make([]string, len(c.groupMembership))
i := 0
for a := range c.groupMembership {
s[i] = a
i++
}
return s
}
// Authenticated indicates if the credential has been successfully authenticated or not.
func (c *Credentials) Authenticated() bool {
return c.authenticated
}
// SetAuthenticated sets the credential as having been successfully authenticated.
func (c *Credentials) SetAuthenticated(b bool) {
c.authenticated = b
}
// AddAuthzAttribute adds an authorization attribute to the credential.
func (c *Credentials) AddAuthzAttribute(a string) {
c.groupMembership[a] = true
}
// RemoveAuthzAttribute removes an authorization attribute from the credential.
func (c *Credentials) RemoveAuthzAttribute(a string) {
if _, ok := c.groupMembership[a]; !ok {
return
}
delete(c.groupMembership, a)
}
// EnableAuthzAttribute toggles an authorization attribute to an enabled state on the credential.
func (c *Credentials) EnableAuthzAttribute(a string) {
if enabled, ok := c.groupMembership[a]; ok && !enabled {
c.groupMembership[a] = true
}
}
// DisableAuthzAttribute toggles an authorization attribute to a disabled state on the credential.
func (c *Credentials) DisableAuthzAttribute(a string) {
if enabled, ok := c.groupMembership[a]; ok && enabled {
c.groupMembership[a] = false
}
}
// Authorized indicates if the credential has the specified authorizing attribute.
func (c *Credentials) Authorized(a string) bool {
if enabled, ok := c.groupMembership[a]; ok && enabled {
return true
}
return false
}
// SessionID returns the credential's session ID.
func (c *Credentials) SessionID() string {
return c.sessionID
}
// Expired indicates if the credential has expired.
func (c *Credentials) Expired() bool {
if !c.validUntil.IsZero() && time.Now().UTC().After(c.validUntil) {
return true
}
return false
}
// ValidUntil returns the credential's valid until date
func (c *Credentials) ValidUntil() time.Time {
return c.validUntil
}
// Attributes returns the Credentials' attributes map.
func (c *Credentials) Attributes() map[string]interface{} {
return c.attributes
}
// SetAttribute sets the value of an attribute.
func (c *Credentials) SetAttribute(k string, v interface{}) {
c.attributes[k] = v
}
// SetAttributes replaces the attributes map with the one provided.
func (c *Credentials) SetAttributes(a map[string]interface{}) {
c.attributes = a
}
// RemoveAttribute deletes an attribute from the attribute map that has the key provided.
func (c *Credentials) RemoveAttribute(k string) {
delete(c.attributes, k)
}
// Marshal the Credentials into a byte slice
func (c *Credentials) Marshal() ([]byte, error) {
gob.Register(map[string]interface{}{})
gob.Register(ADCredentials{})
buf := new(bytes.Buffer)
enc := gob.NewEncoder(buf)
mc := marshalCredentials{
Username: c.username,
DisplayName: c.displayName,
Realm: c.realm,
CName: c.cname,
Keytab: c.HasKeytab(),
Password: c.HasPassword(),
Attributes: c.attributes,
ValidUntil: c.validUntil,
Authenticated: c.authenticated,
Human: c.human,
AuthTime: c.authTime,
GroupMembership: c.groupMembership,
SessionID: c.sessionID,
}
err := enc.Encode(&mc)
if err != nil {
return []byte{}, err
}
return buf.Bytes(), nil
}
// Unmarshal a byte slice into Credentials
func (c *Credentials) Unmarshal(b []byte) error {
gob.Register(map[string]interface{}{})
gob.Register(ADCredentials{})
mc := new(marshalCredentials)
buf := bytes.NewBuffer(b)
dec := gob.NewDecoder(buf)
err := dec.Decode(mc)
if err != nil {
return err
}
c.username = mc.Username
c.displayName = mc.DisplayName
c.realm = mc.Realm
c.cname = mc.CName
c.attributes = mc.Attributes
c.validUntil = mc.ValidUntil
c.authenticated = mc.Authenticated
c.human = mc.Human
c.authTime = mc.AuthTime
c.groupMembership = mc.GroupMembership
c.sessionID = mc.SessionID
return nil
}
// JSON return details of the Credentials in a JSON format.
func (c *Credentials) JSON() (string, error) {
mc := marshalCredentials{
Username: c.username,
DisplayName: c.displayName,
Realm: c.realm,
CName: c.cname,
Keytab: c.HasKeytab(),
Password: c.HasPassword(),
ValidUntil: c.validUntil,
Authenticated: c.authenticated,
Human: c.human,
AuthTime: c.authTime,
SessionID: c.sessionID,
}
b, err := json.MarshalIndent(mc, "", " ")
if err != nil {
return "", err
}
return string(b), nil
}
+129
View File
@@ -0,0 +1,129 @@
package crypto
import (
"crypto/aes"
"crypto/hmac"
"crypto/sha1"
"hash"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/common"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/rfc3961"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/rfc3962"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/chksumtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
)
// RFC 3962
// Aes128CtsHmacSha96 implements Kerberos encryption type aes128-cts-hmac-sha1-96
type Aes128CtsHmacSha96 struct {
}
// GetETypeID returns the EType ID number.
func (e Aes128CtsHmacSha96) GetETypeID() int32 {
return etypeID.AES128_CTS_HMAC_SHA1_96
}
// GetHashID returns the checksum type ID number.
func (e Aes128CtsHmacSha96) GetHashID() int32 {
return chksumtype.HMAC_SHA1_96_AES128
}
// GetKeyByteSize returns the number of bytes for key of this etype.
func (e Aes128CtsHmacSha96) GetKeyByteSize() int {
return 128 / 8
}
// GetKeySeedBitLength returns the number of bits for the seed for key generation.
func (e Aes128CtsHmacSha96) GetKeySeedBitLength() int {
return e.GetKeyByteSize() * 8
}
// GetHashFunc returns the hash function for this etype.
func (e Aes128CtsHmacSha96) GetHashFunc() func() hash.Hash {
return sha1.New
}
// GetMessageBlockByteSize returns the block size for the etype's messages.
func (e Aes128CtsHmacSha96) GetMessageBlockByteSize() int {
return 1
}
// GetDefaultStringToKeyParams returns the default key derivation parameters in string form.
func (e Aes128CtsHmacSha96) GetDefaultStringToKeyParams() string {
return "00001000"
}
// GetConfounderByteSize returns the byte count for confounder to be used during cryptographic operations.
func (e Aes128CtsHmacSha96) GetConfounderByteSize() int {
return aes.BlockSize
}
// GetHMACBitLength returns the bit count size of the integrity hash.
func (e Aes128CtsHmacSha96) GetHMACBitLength() int {
return 96
}
// GetCypherBlockBitLength returns the bit count size of the cypher block.
func (e Aes128CtsHmacSha96) GetCypherBlockBitLength() int {
return aes.BlockSize * 8
}
// StringToKey returns a key derived from the string provided.
func (e Aes128CtsHmacSha96) StringToKey(secret string, salt string, s2kparams string) ([]byte, error) {
return rfc3962.StringToKey(secret, salt, s2kparams, e)
}
// RandomToKey returns a key from the bytes provided.
func (e Aes128CtsHmacSha96) RandomToKey(b []byte) []byte {
return rfc3961.RandomToKey(b)
}
// EncryptData encrypts the data provided.
func (e Aes128CtsHmacSha96) EncryptData(key, data []byte) ([]byte, []byte, error) {
return rfc3962.EncryptData(key, data, e)
}
// EncryptMessage encrypts the message provided and concatenates it with the integrity hash to create an encrypted message.
func (e Aes128CtsHmacSha96) EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error) {
return rfc3962.EncryptMessage(key, message, usage, e)
}
// DecryptData decrypts the data provided.
func (e Aes128CtsHmacSha96) DecryptData(key, data []byte) ([]byte, error) {
return rfc3962.DecryptData(key, data, e)
}
// DecryptMessage decrypts the message provided and verifies the integrity of the message.
func (e Aes128CtsHmacSha96) DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error) {
return rfc3962.DecryptMessage(key, ciphertext, usage, e)
}
// DeriveKey derives a key from the protocol key based on the usage value.
func (e Aes128CtsHmacSha96) DeriveKey(protocolKey, usage []byte) ([]byte, error) {
return rfc3961.DeriveKey(protocolKey, usage, e)
}
// DeriveRandom generates data needed for key generation.
func (e Aes128CtsHmacSha96) DeriveRandom(protocolKey, usage []byte) ([]byte, error) {
return rfc3961.DeriveRandom(protocolKey, usage, e)
}
// VerifyIntegrity checks the integrity of the plaintext message.
func (e Aes128CtsHmacSha96) VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool {
return rfc3961.VerifyIntegrity(protocolKey, ct, pt, usage, e)
}
// GetChecksumHash returns a keyed checksum hash of the bytes provided.
func (e Aes128CtsHmacSha96) GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error) {
return common.GetHash(data, protocolKey, common.GetUsageKc(usage), e)
}
// VerifyChecksum compares the checksum of the message bytes is the same as the checksum provided.
func (e Aes128CtsHmacSha96) VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool {
c, err := e.GetChecksumHash(protocolKey, data, usage)
if err != nil {
return false
}
return hmac.Equal(chksum, c)
}
@@ -0,0 +1,132 @@
package crypto
import (
"crypto/aes"
"crypto/hmac"
"crypto/sha256"
"hash"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/common"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/rfc8009"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/chksumtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
)
// RFC https://tools.ietf.org/html/rfc8009
// Aes128CtsHmacSha256128 implements Kerberos encryption type aes128-cts-hmac-sha256-128
type Aes128CtsHmacSha256128 struct {
}
// GetETypeID returns the EType ID number.
func (e Aes128CtsHmacSha256128) GetETypeID() int32 {
return etypeID.AES128_CTS_HMAC_SHA256_128
}
// GetHashID returns the checksum type ID number.
func (e Aes128CtsHmacSha256128) GetHashID() int32 {
return chksumtype.HMAC_SHA256_128_AES128
}
// GetKeyByteSize returns the number of bytes for key of this etype.
func (e Aes128CtsHmacSha256128) GetKeyByteSize() int {
return 128 / 8
}
// GetKeySeedBitLength returns the number of bits for the seed for key generation.
func (e Aes128CtsHmacSha256128) GetKeySeedBitLength() int {
return e.GetKeyByteSize() * 8
}
// GetHashFunc returns the hash function for this etype.
func (e Aes128CtsHmacSha256128) GetHashFunc() func() hash.Hash {
return sha256.New
}
// GetMessageBlockByteSize returns the block size for the etype's messages.
func (e Aes128CtsHmacSha256128) GetMessageBlockByteSize() int {
return 1
}
// GetDefaultStringToKeyParams returns the default key derivation parameters in string form.
func (e Aes128CtsHmacSha256128) GetDefaultStringToKeyParams() string {
return "00008000"
}
// GetConfounderByteSize returns the byte count for confounder to be used during cryptographic operations.
func (e Aes128CtsHmacSha256128) GetConfounderByteSize() int {
return aes.BlockSize
}
// GetHMACBitLength returns the bit count size of the integrity hash.
func (e Aes128CtsHmacSha256128) GetHMACBitLength() int {
return 128
}
// GetCypherBlockBitLength returns the bit count size of the cypher block.
func (e Aes128CtsHmacSha256128) GetCypherBlockBitLength() int {
return aes.BlockSize * 8
}
// StringToKey returns a key derived from the string provided.
func (e Aes128CtsHmacSha256128) StringToKey(secret string, salt string, s2kparams string) ([]byte, error) {
saltp := rfc8009.GetSaltP(salt, "aes128-cts-hmac-sha256-128")
return rfc8009.StringToKey(secret, saltp, s2kparams, e)
}
// RandomToKey returns a key from the bytes provided.
func (e Aes128CtsHmacSha256128) RandomToKey(b []byte) []byte {
return rfc8009.RandomToKey(b)
}
// EncryptData encrypts the data provided.
func (e Aes128CtsHmacSha256128) EncryptData(key, data []byte) ([]byte, []byte, error) {
return rfc8009.EncryptData(key, data, e)
}
// EncryptMessage encrypts the message provided and concatenates it with the integrity hash to create an encrypted message.
func (e Aes128CtsHmacSha256128) EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error) {
return rfc8009.EncryptMessage(key, message, usage, e)
}
// DecryptData decrypts the data provided.
func (e Aes128CtsHmacSha256128) DecryptData(key, data []byte) ([]byte, error) {
return rfc8009.DecryptData(key, data, e)
}
// DecryptMessage decrypts the message provided and verifies the integrity of the message.
func (e Aes128CtsHmacSha256128) DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error) {
return rfc8009.DecryptMessage(key, ciphertext, usage, e)
}
// DeriveKey derives a key from the protocol key based on the usage value.
func (e Aes128CtsHmacSha256128) DeriveKey(protocolKey, usage []byte) ([]byte, error) {
return rfc8009.DeriveKey(protocolKey, usage, e), nil
}
// DeriveRandom generates data needed for key generation.
func (e Aes128CtsHmacSha256128) DeriveRandom(protocolKey, usage []byte) ([]byte, error) {
return rfc8009.DeriveRandom(protocolKey, usage, e)
}
// VerifyIntegrity checks the integrity of the ciphertext message.
// As the hash is calculated over the iv concatenated with the AES cipher output not the plaintext the pt value to this
// interface method is not use. Pass any []byte.
func (e Aes128CtsHmacSha256128) VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool {
// We don't need ib just there for the interface
return rfc8009.VerifyIntegrity(protocolKey, ct, usage, e)
}
// GetChecksumHash returns a keyed checksum hash of the bytes provided.
func (e Aes128CtsHmacSha256128) GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error) {
return common.GetHash(data, protocolKey, common.GetUsageKc(usage), e)
}
// VerifyChecksum compares the checksum of the message bytes is the same as the checksum provided.
func (e Aes128CtsHmacSha256128) VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool {
c, err := e.GetChecksumHash(protocolKey, data, usage)
if err != nil {
return false
}
return hmac.Equal(chksum, c)
}
+129
View File
@@ -0,0 +1,129 @@
package crypto
import (
"crypto/aes"
"crypto/hmac"
"crypto/sha1"
"hash"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/common"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/rfc3961"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/rfc3962"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/chksumtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
)
// RFC 3962
// Aes256CtsHmacSha96 implements Kerberos encryption type aes256-cts-hmac-sha1-96
type Aes256CtsHmacSha96 struct {
}
// GetETypeID returns the EType ID number.
func (e Aes256CtsHmacSha96) GetETypeID() int32 {
return etypeID.AES256_CTS_HMAC_SHA1_96
}
// GetHashID returns the checksum type ID number.
func (e Aes256CtsHmacSha96) GetHashID() int32 {
return chksumtype.HMAC_SHA1_96_AES256
}
// GetKeyByteSize returns the number of bytes for key of this etype.
func (e Aes256CtsHmacSha96) GetKeyByteSize() int {
return 256 / 8
}
// GetKeySeedBitLength returns the number of bits for the seed for key generation.
func (e Aes256CtsHmacSha96) GetKeySeedBitLength() int {
return e.GetKeyByteSize() * 8
}
// GetHashFunc returns the hash function for this etype.
func (e Aes256CtsHmacSha96) GetHashFunc() func() hash.Hash {
return sha1.New
}
// GetMessageBlockByteSize returns the block size for the etype's messages.
func (e Aes256CtsHmacSha96) GetMessageBlockByteSize() int {
return 1
}
// GetDefaultStringToKeyParams returns the default key derivation parameters in string form.
func (e Aes256CtsHmacSha96) GetDefaultStringToKeyParams() string {
return "00001000"
}
// GetConfounderByteSize returns the byte count for confounder to be used during cryptographic operations.
func (e Aes256CtsHmacSha96) GetConfounderByteSize() int {
return aes.BlockSize
}
// GetHMACBitLength returns the bit count size of the integrity hash.
func (e Aes256CtsHmacSha96) GetHMACBitLength() int {
return 96
}
// GetCypherBlockBitLength returns the bit count size of the cypher block.
func (e Aes256CtsHmacSha96) GetCypherBlockBitLength() int {
return aes.BlockSize * 8
}
// StringToKey returns a key derived from the string provided.
func (e Aes256CtsHmacSha96) StringToKey(secret string, salt string, s2kparams string) ([]byte, error) {
return rfc3962.StringToKey(secret, salt, s2kparams, e)
}
// RandomToKey returns a key from the bytes provided.
func (e Aes256CtsHmacSha96) RandomToKey(b []byte) []byte {
return rfc3961.RandomToKey(b)
}
// EncryptData encrypts the data provided.
func (e Aes256CtsHmacSha96) EncryptData(key, data []byte) ([]byte, []byte, error) {
return rfc3962.EncryptData(key, data, e)
}
// EncryptMessage encrypts the message provided and concatenates it with the integrity hash to create an encrypted message.
func (e Aes256CtsHmacSha96) EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error) {
return rfc3962.EncryptMessage(key, message, usage, e)
}
// DecryptData decrypts the data provided.
func (e Aes256CtsHmacSha96) DecryptData(key, data []byte) ([]byte, error) {
return rfc3962.DecryptData(key, data, e)
}
// DecryptMessage decrypts the message provided and verifies the integrity of the message.
func (e Aes256CtsHmacSha96) DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error) {
return rfc3962.DecryptMessage(key, ciphertext, usage, e)
}
// DeriveKey derives a key from the protocol key based on the usage value.
func (e Aes256CtsHmacSha96) DeriveKey(protocolKey, usage []byte) ([]byte, error) {
return rfc3961.DeriveKey(protocolKey, usage, e)
}
// DeriveRandom generates data needed for key generation.
func (e Aes256CtsHmacSha96) DeriveRandom(protocolKey, usage []byte) ([]byte, error) {
return rfc3961.DeriveRandom(protocolKey, usage, e)
}
// VerifyIntegrity checks the integrity of the plaintext message.
func (e Aes256CtsHmacSha96) VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool {
return rfc3961.VerifyIntegrity(protocolKey, ct, pt, usage, e)
}
// GetChecksumHash returns a keyed checksum hash of the bytes provided.
func (e Aes256CtsHmacSha96) GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error) {
return common.GetHash(data, protocolKey, common.GetUsageKc(usage), e)
}
// VerifyChecksum compares the checksum of the message bytes is the same as the checksum provided.
func (e Aes256CtsHmacSha96) VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool {
c, err := e.GetChecksumHash(protocolKey, data, usage)
if err != nil {
return false
}
return hmac.Equal(chksum, c)
}
@@ -0,0 +1,132 @@
package crypto
import (
"crypto/aes"
"crypto/hmac"
"crypto/sha512"
"hash"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/common"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/rfc8009"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/chksumtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
)
// RFC https://tools.ietf.org/html/rfc8009
// Aes256CtsHmacSha384192 implements Kerberos encryption type aes256-cts-hmac-sha384-192
type Aes256CtsHmacSha384192 struct {
}
// GetETypeID returns the EType ID number.
func (e Aes256CtsHmacSha384192) GetETypeID() int32 {
return etypeID.AES256_CTS_HMAC_SHA384_192
}
// GetHashID returns the checksum type ID number.
func (e Aes256CtsHmacSha384192) GetHashID() int32 {
return chksumtype.HMAC_SHA384_192_AES256
}
// GetKeyByteSize returns the number of bytes for key of this etype.
func (e Aes256CtsHmacSha384192) GetKeyByteSize() int {
return 192 / 8
}
// GetKeySeedBitLength returns the number of bits for the seed for key generation.
func (e Aes256CtsHmacSha384192) GetKeySeedBitLength() int {
return e.GetKeyByteSize() * 8
}
// GetHashFunc returns the hash function for this etype.
func (e Aes256CtsHmacSha384192) GetHashFunc() func() hash.Hash {
return sha512.New384
}
// GetMessageBlockByteSize returns the block size for the etype's messages.
func (e Aes256CtsHmacSha384192) GetMessageBlockByteSize() int {
return 1
}
// GetDefaultStringToKeyParams returns the default key derivation parameters in string form.
func (e Aes256CtsHmacSha384192) GetDefaultStringToKeyParams() string {
return "00008000"
}
// GetConfounderByteSize returns the byte count for confounder to be used during cryptographic operations.
func (e Aes256CtsHmacSha384192) GetConfounderByteSize() int {
return aes.BlockSize
}
// GetHMACBitLength returns the bit count size of the integrity hash.
func (e Aes256CtsHmacSha384192) GetHMACBitLength() int {
return 192
}
// GetCypherBlockBitLength returns the bit count size of the cypher block.
func (e Aes256CtsHmacSha384192) GetCypherBlockBitLength() int {
return aes.BlockSize * 8
}
// StringToKey returns a key derived from the string provided.
func (e Aes256CtsHmacSha384192) StringToKey(secret string, salt string, s2kparams string) ([]byte, error) {
saltp := rfc8009.GetSaltP(salt, "aes256-cts-hmac-sha384-192")
return rfc8009.StringToKey(secret, saltp, s2kparams, e)
}
// RandomToKey returns a key from the bytes provided.
func (e Aes256CtsHmacSha384192) RandomToKey(b []byte) []byte {
return rfc8009.RandomToKey(b)
}
// EncryptData encrypts the data provided.
func (e Aes256CtsHmacSha384192) EncryptData(key, data []byte) ([]byte, []byte, error) {
return rfc8009.EncryptData(key, data, e)
}
// EncryptMessage encrypts the message provided and concatenates it with the integrity hash to create an encrypted message.
func (e Aes256CtsHmacSha384192) EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error) {
return rfc8009.EncryptMessage(key, message, usage, e)
}
// DecryptData decrypts the data provided.
func (e Aes256CtsHmacSha384192) DecryptData(key, data []byte) ([]byte, error) {
return rfc8009.DecryptData(key, data, e)
}
// DecryptMessage decrypts the message provided and verifies the integrity of the message.
func (e Aes256CtsHmacSha384192) DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error) {
return rfc8009.DecryptMessage(key, ciphertext, usage, e)
}
// DeriveKey derives a key from the protocol key based on the usage value.
func (e Aes256CtsHmacSha384192) DeriveKey(protocolKey, usage []byte) ([]byte, error) {
return rfc8009.DeriveKey(protocolKey, usage, e), nil
}
// DeriveRandom generates data needed for key generation.
func (e Aes256CtsHmacSha384192) DeriveRandom(protocolKey, usage []byte) ([]byte, error) {
return rfc8009.DeriveRandom(protocolKey, usage, e)
}
// VerifyIntegrity checks the integrity of the ciphertext message.
// As the hash is calculated over the iv concatenated with the AES cipher output not the plaintext the pt value to this
// interface method is not use. Pass any []byte.
func (e Aes256CtsHmacSha384192) VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool {
// We don't need ib just there for the interface
return rfc8009.VerifyIntegrity(protocolKey, ct, usage, e)
}
// GetChecksumHash returns a keyed checksum hash of the bytes provided.
func (e Aes256CtsHmacSha384192) GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error) {
return common.GetHash(data, protocolKey, common.GetUsageKc(usage), e)
}
// VerifyChecksum compares the checksum of the message bytes is the same as the checksum provided.
func (e Aes256CtsHmacSha384192) VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool {
c, err := e.GetChecksumHash(protocolKey, data, usage)
if err != nil {
return false
}
return hmac.Equal(chksum, c)
}
+132
View File
@@ -0,0 +1,132 @@
// Package common provides encryption methods common across encryption types
package common
import (
"bytes"
"crypto/hmac"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/etype"
)
// ZeroPad pads bytes with zeros to nearest multiple of message size m.
func ZeroPad(b []byte, m int) ([]byte, error) {
if m <= 0 {
return nil, errors.New("Invalid message block size when padding")
}
if b == nil || len(b) == 0 {
return nil, errors.New("Data not valid to pad: Zero size")
}
if l := len(b) % m; l != 0 {
n := m - l
z := make([]byte, n)
b = append(b, z...)
}
return b, nil
}
// PKCS7Pad pads bytes according to RFC 2315 to nearest multiple of message size m.
func PKCS7Pad(b []byte, m int) ([]byte, error) {
if m <= 0 {
return nil, errors.New("Invalid message block size when padding")
}
if b == nil || len(b) == 0 {
return nil, errors.New("Data not valid to pad: Zero size")
}
n := m - (len(b) % m)
pb := make([]byte, len(b)+n)
copy(pb, b)
copy(pb[len(b):], bytes.Repeat([]byte{byte(n)}, n))
return pb, nil
}
// PKCS7Unpad removes RFC 2315 padding from byes where message size is m.
func PKCS7Unpad(b []byte, m int) ([]byte, error) {
if m <= 0 {
return nil, errors.New("invalid message block size when unpadding")
}
if b == nil || len(b) == 0 {
return nil, errors.New("padded data not valid: Zero size")
}
if len(b)%m != 0 {
return nil, errors.New("padded data not valid: Not multiple of message block size")
}
c := b[len(b)-1]
n := int(c)
if n == 0 || n > len(b) {
return nil, errors.New("padded data not valid: Data may not have been padded")
}
for i := 0; i < n; i++ {
if b[len(b)-n+i] != c {
return nil, errors.New("padded data not valid")
}
}
return b[:len(b)-n], nil
}
// GetHash generates the keyed hash value according to the etype's hash function.
func GetHash(pt, key []byte, usage []byte, etype etype.EType) ([]byte, error) {
k, err := etype.DeriveKey(key, usage)
if err != nil {
return nil, fmt.Errorf("unable to derive key for checksum: %v", err)
}
mac := hmac.New(etype.GetHashFunc(), k)
p := make([]byte, len(pt))
copy(p, pt)
mac.Write(p)
return mac.Sum(nil)[:etype.GetHMACBitLength()/8], nil
}
// GetChecksumHash returns a keyed checksum hash of the bytes provided.
func GetChecksumHash(b, key []byte, usage uint32, etype etype.EType) ([]byte, error) {
return GetHash(b, key, GetUsageKc(usage), etype)
}
// GetIntegrityHash returns a keyed integrity hash of the bytes provided.
func GetIntegrityHash(b, key []byte, usage uint32, etype etype.EType) ([]byte, error) {
return GetHash(b, key, GetUsageKi(usage), etype)
}
// VerifyChecksum compares the checksum of the msg bytes is the same as the checksum provided.
func VerifyChecksum(key, chksum, msg []byte, usage uint32, etype etype.EType) bool {
//The encrypted message is a concatenation of the encrypted output and the hash HMAC.
expectedMAC, _ := GetChecksumHash(msg, key, usage, etype)
return hmac.Equal(chksum, expectedMAC)
}
// GetUsageKc returns the checksum key usage value for the usage number un.
//
// See RFC 3961 5.3 key-derivation function definition.
func GetUsageKc(un uint32) []byte {
return getUsage(un, 0x99)
}
// GetUsageKe returns the encryption key usage value for the usage number un
//
// See RFC 3961 5.3 key-derivation function definition.
func GetUsageKe(un uint32) []byte {
return getUsage(un, 0xAA)
}
// GetUsageKi returns the integrity key usage value for the usage number un
//
// See RFC 3961 5.3 key-derivation function definition.
func GetUsageKi(un uint32) []byte {
return getUsage(un, 0x55)
}
func getUsage(un uint32, o byte) []byte {
var buf bytes.Buffer
binary.Write(&buf, binary.BigEndian, un)
return append(buf.Bytes(), o)
}
// IterationsToS2Kparams converts the number of iterations as an integer to a string representation.
func IterationsToS2Kparams(i uint32) string {
b := make([]byte, 4, 4)
binary.BigEndian.PutUint32(b, i)
return hex.EncodeToString(b)
}
+175
View File
@@ -0,0 +1,175 @@
// Package crypto implements cryptographic functions for Kerberos 5 implementation.
package crypto
import (
"encoding/hex"
"fmt"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/etype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/chksumtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/patype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// GetEtype returns an instances of the required etype struct for the etype ID.
func GetEtype(id int32) (etype.EType, error) {
switch id {
case etypeID.AES128_CTS_HMAC_SHA1_96:
var et Aes128CtsHmacSha96
return et, nil
case etypeID.AES256_CTS_HMAC_SHA1_96:
var et Aes256CtsHmacSha96
return et, nil
case etypeID.AES128_CTS_HMAC_SHA256_128:
var et Aes128CtsHmacSha256128
return et, nil
case etypeID.AES256_CTS_HMAC_SHA384_192:
var et Aes256CtsHmacSha384192
return et, nil
case etypeID.DES3_CBC_SHA1_KD:
var et Des3CbcSha1Kd
return et, nil
case etypeID.RC4_HMAC:
var et RC4HMAC
return et, nil
default:
return nil, fmt.Errorf("unknown or unsupported EType: %d", id)
}
}
// GetChksumEtype returns an instances of the required etype struct for the checksum ID.
func GetChksumEtype(id int32) (etype.EType, error) {
switch id {
case chksumtype.HMAC_SHA1_96_AES128:
var et Aes128CtsHmacSha96
return et, nil
case chksumtype.HMAC_SHA1_96_AES256:
var et Aes256CtsHmacSha96
return et, nil
case chksumtype.HMAC_SHA256_128_AES128:
var et Aes128CtsHmacSha256128
return et, nil
case chksumtype.HMAC_SHA384_192_AES256:
var et Aes256CtsHmacSha384192
return et, nil
case chksumtype.HMAC_SHA1_DES3_KD:
var et Des3CbcSha1Kd
return et, nil
case chksumtype.KERB_CHECKSUM_HMAC_MD5:
var et RC4HMAC
return et, nil
//case chksumtype.KERB_CHECKSUM_HMAC_MD5_UNSIGNED:
// var et RC4HMAC
// return et, nil
default:
return nil, fmt.Errorf("unknown or unsupported checksum type: %d", id)
}
}
// GetKeyFromPassword generates an encryption key from the principal's password.
func GetKeyFromPassword(passwd string, cname types.PrincipalName, realm string, etypeID int32, pas types.PADataSequence) (types.EncryptionKey, etype.EType, error) {
var key types.EncryptionKey
et, err := GetEtype(etypeID)
if err != nil {
return key, et, fmt.Errorf("error getting encryption type: %v", err)
}
sk2p := et.GetDefaultStringToKeyParams()
var salt string
var paID int32
for _, pa := range pas {
switch pa.PADataType {
case patype.PA_PW_SALT:
if paID > pa.PADataType {
continue
}
salt = string(pa.PADataValue)
case patype.PA_ETYPE_INFO:
if paID > pa.PADataType {
continue
}
var eti types.ETypeInfo
err := eti.Unmarshal(pa.PADataValue)
if err != nil {
return key, et, fmt.Errorf("error unmashaling PA Data to PA-ETYPE-INFO2: %v", err)
}
if etypeID != eti[0].EType {
et, err = GetEtype(eti[0].EType)
if err != nil {
return key, et, fmt.Errorf("error getting encryption type: %v", err)
}
}
salt = string(eti[0].Salt)
case patype.PA_ETYPE_INFO2:
if paID > pa.PADataType {
continue
}
var et2 types.ETypeInfo2
err := et2.Unmarshal(pa.PADataValue)
if err != nil {
return key, et, fmt.Errorf("error unmashalling PA Data to PA-ETYPE-INFO2: %v", err)
}
if etypeID != et2[0].EType {
et, err = GetEtype(et2[0].EType)
if err != nil {
return key, et, fmt.Errorf("error getting encryption type: %v", err)
}
}
if len(et2[0].S2KParams) == 4 {
sk2p = hex.EncodeToString(et2[0].S2KParams)
}
salt = et2[0].Salt
}
}
if salt == "" {
salt = cname.GetSalt(realm)
}
k, err := et.StringToKey(passwd, salt, sk2p)
if err != nil {
return key, et, fmt.Errorf("error deriving key from string: %+v", err)
}
key = types.EncryptionKey{
KeyType: etypeID,
KeyValue: k,
}
return key, et, nil
}
// GetEncryptedData encrypts the data provided and returns and EncryptedData type.
// Pass a usage value of zero to use the key provided directly rather than deriving one.
func GetEncryptedData(plainBytes []byte, key types.EncryptionKey, usage uint32, kvno int) (types.EncryptedData, error) {
var ed types.EncryptedData
et, err := GetEtype(key.KeyType)
if err != nil {
return ed, fmt.Errorf("error getting etype: %v", err)
}
_, b, err := et.EncryptMessage(key.KeyValue, plainBytes, usage)
if err != nil {
return ed, err
}
ed = types.EncryptedData{
EType: key.KeyType,
Cipher: b,
KVNO: kvno,
}
return ed, nil
}
// DecryptEncPart decrypts the EncryptedData.
func DecryptEncPart(ed types.EncryptedData, key types.EncryptionKey, usage uint32) ([]byte, error) {
return DecryptMessage(ed.Cipher, key, usage)
}
// DecryptMessage decrypts the ciphertext and verifies the integrity.
func DecryptMessage(ciphertext []byte, key types.EncryptionKey, usage uint32) ([]byte, error) {
et, err := GetEtype(key.KeyType)
if err != nil {
return []byte{}, fmt.Errorf("error decrypting: %v", err)
}
b, err := et.DecryptMessage(key.KeyValue, ciphertext, usage)
if err != nil {
return nil, fmt.Errorf("error decrypting: %v", err)
}
return b, nil
}
+139
View File
@@ -0,0 +1,139 @@
package crypto
import (
"crypto/des"
"crypto/hmac"
"crypto/sha1"
"errors"
"hash"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/common"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/rfc3961"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/chksumtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
)
//RFC: 3961 Section 6.3
// Des3CbcSha1Kd implements Kerberos encryption type des3-cbc-hmac-sha1-kd
type Des3CbcSha1Kd struct {
}
// GetETypeID returns the EType ID number.
func (e Des3CbcSha1Kd) GetETypeID() int32 {
return etypeID.DES3_CBC_SHA1_KD
}
// GetHashID returns the checksum type ID number.
func (e Des3CbcSha1Kd) GetHashID() int32 {
return chksumtype.HMAC_SHA1_DES3_KD
}
// GetKeyByteSize returns the number of bytes for key of this etype.
func (e Des3CbcSha1Kd) GetKeyByteSize() int {
return 24
}
// GetKeySeedBitLength returns the number of bits for the seed for key generation.
func (e Des3CbcSha1Kd) GetKeySeedBitLength() int {
return 21 * 8
}
// GetHashFunc returns the hash function for this etype.
func (e Des3CbcSha1Kd) GetHashFunc() func() hash.Hash {
return sha1.New
}
// GetMessageBlockByteSize returns the block size for the etype's messages.
func (e Des3CbcSha1Kd) GetMessageBlockByteSize() int {
//For traditional CBC mode with padding, it would be the underlying cipher's block size
return des.BlockSize
}
// GetDefaultStringToKeyParams returns the default key derivation parameters in string form.
func (e Des3CbcSha1Kd) GetDefaultStringToKeyParams() string {
var s string
return s
}
// GetConfounderByteSize returns the byte count for confounder to be used during cryptographic operations.
func (e Des3CbcSha1Kd) GetConfounderByteSize() int {
return des.BlockSize
}
// GetHMACBitLength returns the bit count size of the integrity hash.
func (e Des3CbcSha1Kd) GetHMACBitLength() int {
return e.GetHashFunc()().Size() * 8
}
// GetCypherBlockBitLength returns the bit count size of the cypher block.
func (e Des3CbcSha1Kd) GetCypherBlockBitLength() int {
return des.BlockSize * 8
}
// StringToKey returns a key derived from the string provided.
func (e Des3CbcSha1Kd) StringToKey(secret string, salt string, s2kparams string) ([]byte, error) {
if s2kparams != "" {
return []byte{}, errors.New("s2kparams must be an empty string")
}
return rfc3961.DES3StringToKey(secret, salt, e)
}
// RandomToKey returns a key from the bytes provided.
func (e Des3CbcSha1Kd) RandomToKey(b []byte) []byte {
return rfc3961.DES3RandomToKey(b)
}
// DeriveRandom generates data needed for key generation.
func (e Des3CbcSha1Kd) DeriveRandom(protocolKey, usage []byte) ([]byte, error) {
r, err := rfc3961.DeriveRandom(protocolKey, usage, e)
return r, err
}
// DeriveKey derives a key from the protocol key based on the usage value.
func (e Des3CbcSha1Kd) DeriveKey(protocolKey, usage []byte) ([]byte, error) {
r, err := e.DeriveRandom(protocolKey, usage)
if err != nil {
return nil, err
}
return e.RandomToKey(r), nil
}
// EncryptData encrypts the data provided.
func (e Des3CbcSha1Kd) EncryptData(key, data []byte) ([]byte, []byte, error) {
return rfc3961.DES3EncryptData(key, data, e)
}
// EncryptMessage encrypts the message provided and concatenates it with the integrity hash to create an encrypted message.
func (e Des3CbcSha1Kd) EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error) {
return rfc3961.DES3EncryptMessage(key, message, usage, e)
}
// DecryptData decrypts the data provided.
func (e Des3CbcSha1Kd) DecryptData(key, data []byte) ([]byte, error) {
return rfc3961.DES3DecryptData(key, data, e)
}
// DecryptMessage decrypts the message provided and verifies the integrity of the message.
func (e Des3CbcSha1Kd) DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error) {
return rfc3961.DES3DecryptMessage(key, ciphertext, usage, e)
}
// VerifyIntegrity checks the integrity of the plaintext message.
func (e Des3CbcSha1Kd) VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool {
return rfc3961.VerifyIntegrity(protocolKey, ct, pt, usage, e)
}
// GetChecksumHash returns a keyed checksum hash of the bytes provided.
func (e Des3CbcSha1Kd) GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error) {
return common.GetHash(data, protocolKey, common.GetUsageKc(usage), e)
}
// VerifyChecksum compares the checksum of the message bytes is the same as the checksum provided.
func (e Des3CbcSha1Kd) VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool {
c, err := e.GetChecksumHash(protocolKey, data, usage)
if err != nil {
return false
}
return hmac.Equal(chksum, c)
}
+29
View File
@@ -0,0 +1,29 @@
// Package etype provides the Kerberos Encryption Type interface
package etype
import "hash"
// EType is the interface defining the Encryption Type.
type EType interface {
GetETypeID() int32
GetHashID() int32
GetKeyByteSize() int
GetKeySeedBitLength() int
GetDefaultStringToKeyParams() string
StringToKey(string, salt, s2kparams string) ([]byte, error)
RandomToKey(b []byte) []byte
GetHMACBitLength() int
GetMessageBlockByteSize() int
EncryptData(key, data []byte) ([]byte, []byte, error)
EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error)
DecryptData(key, data []byte) ([]byte, error)
DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error)
GetCypherBlockBitLength() int
GetConfounderByteSize() int
DeriveKey(protocolKey, usage []byte) ([]byte, error)
DeriveRandom(protocolKey, usage []byte) ([]byte, error)
VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool
GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error)
VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool
GetHashFunc() func() hash.Hash
}
+133
View File
@@ -0,0 +1,133 @@
package crypto
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"hash"
"io"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/rfc3961"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/rfc4757"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/chksumtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
"golang.org/x/crypto/md4"
)
// RC4HMAC implements Kerberos encryption type rc4-hmac
type RC4HMAC struct {
}
// GetETypeID returns the EType ID number.
func (e RC4HMAC) GetETypeID() int32 {
return etypeID.RC4_HMAC
}
// GetHashID returns the checksum type ID number.
func (e RC4HMAC) GetHashID() int32 {
return chksumtype.KERB_CHECKSUM_HMAC_MD5
}
// GetKeyByteSize returns the number of bytes for key of this etype.
func (e RC4HMAC) GetKeyByteSize() int {
return 16
}
// GetKeySeedBitLength returns the number of bits for the seed for key generation.
func (e RC4HMAC) GetKeySeedBitLength() int {
return e.GetKeyByteSize() * 8
}
// GetHashFunc returns the hash function for this etype.
func (e RC4HMAC) GetHashFunc() func() hash.Hash {
return md5.New
}
// GetMessageBlockByteSize returns the block size for the etype's messages.
func (e RC4HMAC) GetMessageBlockByteSize() int {
return 1
}
// GetDefaultStringToKeyParams returns the default key derivation parameters in string form.
func (e RC4HMAC) GetDefaultStringToKeyParams() string {
return ""
}
// GetConfounderByteSize returns the byte count for confounder to be used during cryptographic operations.
func (e RC4HMAC) GetConfounderByteSize() int {
return 8
}
// GetHMACBitLength returns the bit count size of the integrity hash.
func (e RC4HMAC) GetHMACBitLength() int {
return md5.Size * 8
}
// GetCypherBlockBitLength returns the bit count size of the cypher block.
func (e RC4HMAC) GetCypherBlockBitLength() int {
return 8 // doesn't really apply
}
// StringToKey returns a key derived from the string provided.
func (e RC4HMAC) StringToKey(secret string, salt string, s2kparams string) ([]byte, error) {
return rfc4757.StringToKey(secret)
}
// RandomToKey returns a key from the bytes provided.
func (e RC4HMAC) RandomToKey(b []byte) []byte {
r := bytes.NewReader(b)
h := md4.New()
io.Copy(h, r)
return h.Sum(nil)
}
// EncryptData encrypts the data provided.
func (e RC4HMAC) EncryptData(key, data []byte) ([]byte, []byte, error) {
b, err := rfc4757.EncryptData(key, data, e)
return []byte{}, b, err
}
// EncryptMessage encrypts the message provided and concatenates it with the integrity hash to create an encrypted message.
func (e RC4HMAC) EncryptMessage(key, message []byte, usage uint32) ([]byte, []byte, error) {
b, err := rfc4757.EncryptMessage(key, message, usage, false, e)
return []byte{}, b, err
}
// DecryptData decrypts the data provided.
func (e RC4HMAC) DecryptData(key, data []byte) ([]byte, error) {
return rfc4757.DecryptData(key, data, e)
}
// DecryptMessage decrypts the message provided and verifies the integrity of the message.
func (e RC4HMAC) DecryptMessage(key, ciphertext []byte, usage uint32) ([]byte, error) {
return rfc4757.DecryptMessage(key, ciphertext, usage, false, e)
}
// DeriveKey derives a key from the protocol key based on the usage value.
func (e RC4HMAC) DeriveKey(protocolKey, usage []byte) ([]byte, error) {
return rfc4757.HMAC(protocolKey, usage), nil
}
// DeriveRandom generates data needed for key generation.
func (e RC4HMAC) DeriveRandom(protocolKey, usage []byte) ([]byte, error) {
return rfc3961.DeriveRandom(protocolKey, usage, e)
}
// VerifyIntegrity checks the integrity of the plaintext message.
func (e RC4HMAC) VerifyIntegrity(protocolKey, ct, pt []byte, usage uint32) bool {
return rfc4757.VerifyIntegrity(protocolKey, pt, ct, e)
}
// GetChecksumHash returns a keyed checksum hash of the bytes provided.
func (e RC4HMAC) GetChecksumHash(protocolKey, data []byte, usage uint32) ([]byte, error) {
return rfc4757.Checksum(protocolKey, usage, data)
}
// VerifyChecksum compares the checksum of the message bytes is the same as the checksum provided.
func (e RC4HMAC) VerifyChecksum(protocolKey, data, chksum []byte, usage uint32) bool {
checksum, err := rfc4757.Checksum(protocolKey, usage, data)
if err != nil {
return false
}
return hmac.Equal(checksum, chksum)
}
+119
View File
@@ -0,0 +1,119 @@
// Package rfc3961 provides encryption and checksum methods as specified in RFC 3961
package rfc3961
import (
"crypto/cipher"
"crypto/des"
"crypto/hmac"
"crypto/rand"
"errors"
"fmt"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/common"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/etype"
)
// DES3EncryptData encrypts the data provided using DES3 and methods specific to the etype provided.
func DES3EncryptData(key, data []byte, e etype.EType) ([]byte, []byte, error) {
if len(key) != e.GetKeyByteSize() {
return nil, nil, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
}
data, _ = common.ZeroPad(data, e.GetMessageBlockByteSize())
block, err := des.NewTripleDESCipher(key)
if err != nil {
return nil, nil, fmt.Errorf("error creating cipher: %v", err)
}
//RFC 3961: initial cipher state All bits zero
ivz := make([]byte, des.BlockSize)
ct := make([]byte, len(data))
mode := cipher.NewCBCEncrypter(block, ivz)
mode.CryptBlocks(ct, data)
return ct[len(ct)-e.GetMessageBlockByteSize():], ct, nil
}
// DES3EncryptMessage encrypts the message provided using DES3 and methods specific to the etype provided.
// The encrypted data is concatenated with its integrity hash to create an encrypted message.
func DES3EncryptMessage(key, message []byte, usage uint32, e etype.EType) ([]byte, []byte, error) {
//confounder
c := make([]byte, e.GetConfounderByteSize())
_, err := rand.Read(c)
if err != nil {
return []byte{}, []byte{}, fmt.Errorf("could not generate random confounder: %v", err)
}
plainBytes := append(c, message...)
plainBytes, _ = common.ZeroPad(plainBytes, e.GetMessageBlockByteSize())
// Derive key for encryption from usage
var k []byte
if usage != 0 {
k, err = e.DeriveKey(key, common.GetUsageKe(usage))
if err != nil {
return []byte{}, []byte{}, fmt.Errorf("error deriving key for encryption: %v", err)
}
}
iv, b, err := e.EncryptData(k, plainBytes)
if err != nil {
return iv, b, fmt.Errorf("error encrypting data: %v", err)
}
// Generate and append integrity hash
ih, err := common.GetIntegrityHash(plainBytes, key, usage, e)
if err != nil {
return iv, b, fmt.Errorf("error encrypting data: %v", err)
}
b = append(b, ih...)
return iv, b, nil
}
// DES3DecryptData decrypts the data provided using DES3 and methods specific to the etype provided.
func DES3DecryptData(key, data []byte, e etype.EType) ([]byte, error) {
if len(key) != e.GetKeyByteSize() {
return []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
}
if len(data) < des.BlockSize || len(data)%des.BlockSize != 0 {
return []byte{}, errors.New("ciphertext is not a multiple of the block size")
}
block, err := des.NewTripleDESCipher(key)
if err != nil {
return []byte{}, fmt.Errorf("error creating cipher: %v", err)
}
pt := make([]byte, len(data))
ivz := make([]byte, des.BlockSize)
mode := cipher.NewCBCDecrypter(block, ivz)
mode.CryptBlocks(pt, data)
return pt, nil
}
// DES3DecryptMessage decrypts the message provided using DES3 and methods specific to the etype provided.
// The integrity of the message is also verified.
func DES3DecryptMessage(key, ciphertext []byte, usage uint32, e etype.EType) ([]byte, error) {
//Derive the key
k, err := e.DeriveKey(key, common.GetUsageKe(usage))
if err != nil {
return nil, fmt.Errorf("error deriving key: %v", err)
}
// Strip off the checksum from the end
b, err := e.DecryptData(k, ciphertext[:len(ciphertext)-e.GetHMACBitLength()/8])
if err != nil {
return nil, fmt.Errorf("error decrypting: %v", err)
}
//Verify checksum
if !e.VerifyIntegrity(key, ciphertext, b, usage) {
return nil, errors.New("error decrypting: integrity verification failed")
}
//Remove the confounder bytes
return b[e.GetConfounderByteSize():], nil
}
// VerifyIntegrity verifies the integrity of cipertext bytes ct.
func VerifyIntegrity(key, ct, pt []byte, usage uint32, etype etype.EType) bool {
h := make([]byte, etype.GetHMACBitLength()/8)
copy(h, ct[len(ct)-etype.GetHMACBitLength()/8:])
expectedMAC, _ := common.GetIntegrityHash(pt, key, usage, etype)
return hmac.Equal(h, expectedMAC)
}
+169
View File
@@ -0,0 +1,169 @@
package rfc3961
import (
"bytes"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/etype"
)
const (
prfconstant = "prf"
)
// DeriveRandom implements the RFC 3961 defined function: DR(Key, Constant) = k-truncate(E(Key, Constant, initial-cipher-state)).
//
// key: base key or protocol key. Likely to be a key from a keytab file.
//
// usage: a constant.
//
// n: block size in bits (not bytes) - note if you use something like aes.BlockSize this is in bytes.
//
// k: key length / key seed length in bits. Eg. for AES256 this value is 256.
//
// e: the encryption etype function to use.
func DeriveRandom(key, usage []byte, e etype.EType) ([]byte, error) {
n := e.GetCypherBlockBitLength()
k := e.GetKeySeedBitLength()
//Ensure the usage constant is at least the size of the cypher block size. Pass it through the nfold algorithm that will "stretch" it if needs be.
nFoldUsage := Nfold(usage, n)
//k-truncate implemented by creating a byte array the size of k (k is in bits hence /8)
out := make([]byte, k/8)
// Keep feeding the output back into the encryption function until it is no longer short than k.
_, K, err := e.EncryptData(key, nFoldUsage)
if err != nil {
return out, err
}
for i := copy(out, K); i < len(out); {
_, K, _ = e.EncryptData(key, K)
i = i + copy(out[i:], K)
}
return out, nil
}
// DeriveKey derives a key from the protocol key based on the usage and the etype's specific methods.
func DeriveKey(protocolKey, usage []byte, e etype.EType) ([]byte, error) {
r, err := e.DeriveRandom(protocolKey, usage)
if err != nil {
return nil, err
}
return e.RandomToKey(r), nil
}
// RandomToKey returns a key from the bytes provided according to the definition in RFC 3961.
func RandomToKey(b []byte) []byte {
return b
}
// DES3RandomToKey returns a key from the bytes provided according to the definition in RFC 3961 for DES3 etypes.
func DES3RandomToKey(b []byte) []byte {
r := fixWeakKey(stretch56Bits(b[:7]))
r2 := fixWeakKey(stretch56Bits(b[7:14]))
r = append(r, r2...)
r3 := fixWeakKey(stretch56Bits(b[14:21]))
r = append(r, r3...)
return r
}
// DES3StringToKey returns a key derived from the string provided according to the definition in RFC 3961 for DES3 etypes.
func DES3StringToKey(secret, salt string, e etype.EType) ([]byte, error) {
s := secret + salt
tkey := e.RandomToKey(Nfold([]byte(s), e.GetKeySeedBitLength()))
return e.DeriveKey(tkey, []byte("kerberos"))
}
// PseudoRandom function as defined in RFC 3961
func PseudoRandom(key, b []byte, e etype.EType) ([]byte, error) {
h := e.GetHashFunc()()
h.Write(b)
tmp := h.Sum(nil)[:e.GetMessageBlockByteSize()]
k, err := e.DeriveKey(key, []byte(prfconstant))
if err != nil {
return []byte{}, err
}
_, prf, err := e.EncryptData(k, tmp)
if err != nil {
return []byte{}, err
}
return prf, nil
}
func stretch56Bits(b []byte) []byte {
d := make([]byte, len(b), len(b))
copy(d, b)
var lb byte
for i, v := range d {
bv, nb := calcEvenParity(v)
d[i] = nb
if bv != 0 {
lb = lb | (1 << uint(i+1))
} else {
lb = lb &^ (1 << uint(i+1))
}
}
_, lb = calcEvenParity(lb)
d = append(d, lb)
return d
}
func calcEvenParity(b byte) (uint8, uint8) {
lowestbit := b & 0x01
// c counter of 1s in the first 7 bits of the byte
var c int
// Iterate over the highest 7 bits (hence p starts at 1 not zero) and count the 1s.
for p := 1; p < 8; p++ {
v := b & (1 << uint(p))
if v != 0 {
c++
}
}
if c%2 == 0 {
//Even number of 1s so set parity to 1
b = b | 1
} else {
//Odd number of 1s so set parity to 0
b = b &^ 1
}
return lowestbit, b
}
func fixWeakKey(b []byte) []byte {
if weak(b) {
b[7] ^= 0xF0
}
return b
}
func weak(b []byte) bool {
// weak keys from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-67r1.pdf
weakKeys := [4][]byte{
{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01},
{0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE},
{0xE0, 0xE0, 0xE0, 0xE0, 0xF1, 0xF1, 0xF1, 0xF1},
{0x1F, 0x1F, 0x1F, 0x1F, 0x0E, 0x0E, 0x0E, 0x0E},
}
semiWeakKeys := [12][]byte{
{0x01, 0x1F, 0x01, 0x1F, 0x01, 0x0E, 0x01, 0x0E},
{0x1F, 0x01, 0x1F, 0x01, 0x0E, 0x01, 0x0E, 0x01},
{0x01, 0xE0, 0x01, 0xE0, 0x01, 0xF1, 0x01, 0xF1},
{0xE0, 0x01, 0xE0, 0x01, 0xF1, 0x01, 0xF1, 0x01},
{0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE},
{0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01},
{0x1F, 0xE0, 0x1F, 0xE0, 0x0E, 0xF1, 0x0E, 0xF1},
{0xE0, 0x1F, 0xE0, 0x1F, 0xF1, 0x0E, 0xF1, 0x0E},
{0x1F, 0xFE, 0x1F, 0xFE, 0x0E, 0xFE, 0x0E, 0xFE},
{0xFE, 0x1F, 0xFE, 0x1F, 0xFE, 0x0E, 0xFE, 0x0E},
{0xE0, 0xFE, 0xE0, 0xFE, 0xF1, 0xFE, 0xF1, 0xFE},
{0xFE, 0xE0, 0xFE, 0xE0, 0xFE, 0xF1, 0xFE, 0xF1},
}
for _, k := range weakKeys {
if bytes.Equal(b, k) {
return true
}
}
for _, k := range semiWeakKeys {
if bytes.Equal(b, k) {
return true
}
}
return false
}
+107
View File
@@ -0,0 +1,107 @@
package rfc3961
// Implementation of the n-fold algorithm as defined in RFC 3961.
/* Credits
This golang implementation of nfold used the following project for help with implementation detail.
Although their source is in java it was helpful as a reference implementation of the RFC.
You can find the source code of their open source project along with license information below.
We acknowledge and are grateful to these developers for their contributions to open source
Project: Apache Directory (http://http://directory.apache.org/)
https://svn.apache.org/repos/asf/directory/apacheds/tags/1.5.1/kerberos-shared/src/main/java/org/apache/directory/server/kerberos/shared/crypto/encryption/NFold.java
License: http://www.apache.org/licenses/LICENSE-2.0
*/
// Nfold expands the key to ensure it is not smaller than one cipher block.
// Defined in RFC 3961.
//
// m input bytes that will be "stretched" to the least common multiple of n bits and the bit length of m.
func Nfold(m []byte, n int) []byte {
k := len(m) * 8
//Get the lowest common multiple of the two bit sizes
lcm := lcm(n, k)
relicate := lcm / k
var sumBytes []byte
for i := 0; i < relicate; i++ {
rotation := 13 * i
sumBytes = append(sumBytes, rotateRight(m, rotation)...)
}
nfold := make([]byte, n/8)
sum := make([]byte, n/8)
for i := 0; i < lcm/n; i++ {
for j := 0; j < n/8; j++ {
sum[j] = sumBytes[j+(i*len(sum))]
}
nfold = onesComplementAddition(nfold, sum)
}
return nfold
}
func onesComplementAddition(n1, n2 []byte) []byte {
numBits := len(n1) * 8
out := make([]byte, numBits/8)
carry := 0
for i := numBits - 1; i > -1; i-- {
n1b := getBit(&n1, i)
n2b := getBit(&n2, i)
s := n1b + n2b + carry
if s == 0 || s == 1 {
setBit(&out, i, s)
carry = 0
} else if s == 2 {
carry = 1
} else if s == 3 {
setBit(&out, i, 1)
carry = 1
}
}
if carry == 1 {
carryArray := make([]byte, len(n1))
carryArray[len(carryArray)-1] = 1
out = onesComplementAddition(out, carryArray)
}
return out
}
func rotateRight(b []byte, step int) []byte {
out := make([]byte, len(b))
bitLen := len(b) * 8
for i := 0; i < bitLen; i++ {
v := getBit(&b, i)
setBit(&out, (i+step)%bitLen, v)
}
return out
}
func lcm(x, y int) int {
return (x * y) / gcd(x, y)
}
func gcd(x, y int) int {
for y != 0 {
x, y = y, x%y
}
return x
}
func getBit(b *[]byte, p int) int {
pByte := p / 8
pBit := uint(p % 8)
vByte := (*b)[pByte]
vInt := int(vByte >> (8 - (pBit + 1)) & 0x0001)
return vInt
}
func setBit(b *[]byte, p, v int) {
pByte := p / 8
pBit := uint(p % 8)
oldByte := (*b)[pByte]
var newByte byte
newByte = byte(v<<(8-(pBit+1))) | oldByte
(*b)[pByte] = newByte
}
+89
View File
@@ -0,0 +1,89 @@
// Package rfc3962 provides encryption and checksum methods as specified in RFC 3962
package rfc3962
import (
"crypto/rand"
"errors"
"fmt"
"github.com/jcmturner/aescts/v2"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/common"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/etype"
)
// EncryptData encrypts the data provided using methods specific to the etype provided as defined in RFC 3962.
func EncryptData(key, data []byte, e etype.EType) ([]byte, []byte, error) {
if len(key) != e.GetKeyByteSize() {
return []byte{}, []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
}
ivz := make([]byte, e.GetCypherBlockBitLength()/8)
return aescts.Encrypt(key, ivz, data)
}
// EncryptMessage encrypts the message provided using the methods specific to the etype provided as defined in RFC 3962.
// The encrypted data is concatenated with its integrity hash to create an encrypted message.
func EncryptMessage(key, message []byte, usage uint32, e etype.EType) ([]byte, []byte, error) {
if len(key) != e.GetKeyByteSize() {
return []byte{}, []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
}
//confounder
c := make([]byte, e.GetConfounderByteSize())
_, err := rand.Read(c)
if err != nil {
return []byte{}, []byte{}, fmt.Errorf("could not generate random confounder: %v", err)
}
plainBytes := append(c, message...)
// Derive key for encryption from usage
var k []byte
if usage != 0 {
k, err = e.DeriveKey(key, common.GetUsageKe(usage))
if err != nil {
return []byte{}, []byte{}, fmt.Errorf("error deriving key for encryption: %v", err)
}
}
// Encrypt the data
iv, b, err := e.EncryptData(k, plainBytes)
if err != nil {
return iv, b, fmt.Errorf("error encrypting data: %v", err)
}
// Generate and append integrity hash
ih, err := common.GetIntegrityHash(plainBytes, key, usage, e)
if err != nil {
return iv, b, fmt.Errorf("error encrypting data: %v", err)
}
b = append(b, ih...)
return iv, b, nil
}
// DecryptData decrypts the data provided using the methods specific to the etype provided as defined in RFC 3962.
func DecryptData(key, data []byte, e etype.EType) ([]byte, error) {
if len(key) != e.GetKeyByteSize() {
return []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
}
ivz := make([]byte, e.GetCypherBlockBitLength()/8)
return aescts.Decrypt(key, ivz, data)
}
// DecryptMessage decrypts the message provided using the methods specific to the etype provided as defined in RFC 3962.
// The integrity of the message is also verified.
func DecryptMessage(key, ciphertext []byte, usage uint32, e etype.EType) ([]byte, error) {
//Derive the key
k, err := e.DeriveKey(key, common.GetUsageKe(usage))
if err != nil {
return nil, fmt.Errorf("error deriving key: %v", err)
}
// Strip off the checksum from the end
b, err := e.DecryptData(k, ciphertext[:len(ciphertext)-e.GetHMACBitLength()/8])
if err != nil {
return nil, err
}
//Verify checksum
if !e.VerifyIntegrity(key, ciphertext, b, usage) {
return nil, errors.New("integrity verification failed")
}
//Remove the confounder bytes
return b[e.GetConfounderByteSize():], nil
}
+51
View File
@@ -0,0 +1,51 @@
package rfc3962
import (
"encoding/binary"
"encoding/hex"
"errors"
"github.com/jcmturner/gofork/x/crypto/pbkdf2"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/etype"
)
const (
s2kParamsZero = 4294967296
)
// StringToKey returns a key derived from the string provided according to the definition in RFC 3961.
func StringToKey(secret, salt, s2kparams string, e etype.EType) ([]byte, error) {
i, err := S2KparamsToItertions(s2kparams)
if err != nil {
return nil, err
}
return StringToKeyIter(secret, salt, i, e)
}
// StringToPBKDF2 generates an encryption key from a pass phrase and salt string using the PBKDF2 function from PKCS #5 v2.0
func StringToPBKDF2(secret, salt string, iterations int64, e etype.EType) []byte {
return pbkdf2.Key64([]byte(secret), []byte(salt), iterations, int64(e.GetKeyByteSize()), e.GetHashFunc())
}
// StringToKeyIter returns a key derived from the string provided according to the definition in RFC 3961.
func StringToKeyIter(secret, salt string, iterations int64, e etype.EType) ([]byte, error) {
tkey := e.RandomToKey(StringToPBKDF2(secret, salt, iterations, e))
return e.DeriveKey(tkey, []byte("kerberos"))
}
// S2KparamsToItertions converts the string representation of iterations to an integer
func S2KparamsToItertions(s2kparams string) (int64, error) {
//The s2kparams string should be hex string representing 4 bytes
//The 4 bytes represent a number in big endian order
//If the value is zero then the number of iterations should be 4,294,967,296 (2^32)
var i uint32
if len(s2kparams) != 8 {
return int64(s2kParamsZero), errors.New("invalid s2kparams length")
}
b, err := hex.DecodeString(s2kparams)
if err != nil {
return int64(s2kParamsZero), errors.New("invalid s2kparams, cannot decode string to bytes")
}
i = binary.BigEndian.Uint32(b)
return int64(i), nil
}
+40
View File
@@ -0,0 +1,40 @@
package rfc4757
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"io"
)
// Checksum returns a hash of the data in accordance with RFC 4757
func Checksum(key []byte, usage uint32, data []byte) ([]byte, error) {
// Create hashing key
s := append([]byte(`signaturekey`), byte(0x00)) //includes zero octet at end
mac := hmac.New(md5.New, key)
mac.Write(s)
Ksign := mac.Sum(nil)
// Format data
tb := UsageToMSMsgType(usage)
p := append(tb, data...)
h := md5.New()
rb := bytes.NewReader(p)
_, err := io.Copy(h, rb)
if err != nil {
return []byte{}, err
}
tmp := h.Sum(nil)
// Generate HMAC
mac = hmac.New(md5.New, Ksign)
mac.Write(tmp)
return mac.Sum(nil), nil
}
// HMAC returns a keyed MD5 checksum of the data
func HMAC(key []byte, data []byte) []byte {
mac := hmac.New(md5.New, key)
mac.Write(data)
return mac.Sum(nil)
}
+80
View File
@@ -0,0 +1,80 @@
// Package rfc4757 provides encryption and checksum methods as specified in RFC 4757
package rfc4757
import (
"crypto/hmac"
"crypto/rand"
"crypto/rc4"
"errors"
"fmt"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/etype"
)
// EncryptData encrypts the data provided using methods specific to the etype provided as defined in RFC 4757.
func EncryptData(key, data []byte, e etype.EType) ([]byte, error) {
if len(key) != e.GetKeyByteSize() {
return []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
}
rc4Cipher, err := rc4.NewCipher(key)
if err != nil {
return []byte{}, fmt.Errorf("error creating RC4 cipher: %v", err)
}
ed := make([]byte, len(data))
copy(ed, data)
rc4Cipher.XORKeyStream(ed, ed)
rc4Cipher.Reset()
return ed, nil
}
// DecryptData decrypts the data provided using the methods specific to the etype provided as defined in RFC 4757.
func DecryptData(key, data []byte, e etype.EType) ([]byte, error) {
return EncryptData(key, data, e)
}
// EncryptMessage encrypts the message provided using the methods specific to the etype provided as defined in RFC 4757.
// The encrypted data is concatenated with its RC4 header containing integrity checksum and confounder to create an encrypted message.
func EncryptMessage(key, data []byte, usage uint32, export bool, e etype.EType) ([]byte, error) {
confounder := make([]byte, e.GetConfounderByteSize()) // size = 8
_, err := rand.Read(confounder)
if err != nil {
return []byte{}, fmt.Errorf("error generating confounder: %v", err)
}
k1 := key
k2 := HMAC(k1, UsageToMSMsgType(usage))
toenc := append(confounder, data...)
chksum := HMAC(k2, toenc)
k3 := HMAC(k2, chksum)
ed, err := EncryptData(k3, toenc, e)
if err != nil {
return []byte{}, fmt.Errorf("error encrypting data: %v", err)
}
msg := append(chksum, ed...)
return msg, nil
}
// DecryptMessage decrypts the message provided using the methods specific to the etype provided as defined in RFC 4757.
// The integrity of the message is also verified.
func DecryptMessage(key, data []byte, usage uint32, export bool, e etype.EType) ([]byte, error) {
checksum := data[:e.GetHMACBitLength()/8]
ct := data[e.GetHMACBitLength()/8:]
_, k2, k3 := deriveKeys(key, checksum, usage, export)
pt, err := DecryptData(k3, ct, e)
if err != nil {
return []byte{}, fmt.Errorf("error decrypting data: %v", err)
}
if !VerifyIntegrity(k2, pt, data, e) {
return []byte{}, errors.New("integrity checksum incorrect")
}
return pt[e.GetConfounderByteSize():], nil
}
// VerifyIntegrity checks the integrity checksum of the data matches that calculated from the decrypted data.
func VerifyIntegrity(key, pt, data []byte, e etype.EType) bool {
chksum := HMAC(key, pt)
return hmac.Equal(chksum, data[:e.GetHMACBitLength()/8])
}
+40
View File
@@ -0,0 +1,40 @@
package rfc4757
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"io"
"golang.org/x/crypto/md4"
)
// StringToKey returns a key derived from the string provided according to the definition in RFC 4757.
func StringToKey(secret string) ([]byte, error) {
b := make([]byte, len(secret)*2, len(secret)*2)
for i, r := range secret {
u := fmt.Sprintf("%04x", r)
c, err := hex.DecodeString(u)
if err != nil {
return []byte{}, errors.New("character could not be encoded")
}
// Swap round the two bytes to make little endian as we put into byte slice
b[2*i] = c[1]
b[2*i+1] = c[0]
}
r := bytes.NewReader(b)
h := md4.New()
_, err := io.Copy(h, r)
if err != nil {
return []byte{}, err
}
return h.Sum(nil), nil
}
func deriveKeys(key, checksum []byte, usage uint32, export bool) (k1, k2, k3 []byte) {
k1 = key
k2 = HMAC(k1, UsageToMSMsgType(usage))
k3 = HMAC(k2, checksum)
return
}
+20
View File
@@ -0,0 +1,20 @@
package rfc4757
import "encoding/binary"
// UsageToMSMsgType converts Kerberos key usage numbers to Microsoft message type encoded as a little-endian four byte slice.
func UsageToMSMsgType(usage uint32) []byte {
// Translate usage numbers to the Microsoft T numbers
switch usage {
case 3:
usage = 8
case 9:
usage = 8
case 23:
usage = 13
}
// Now convert to bytes
tb := make([]byte, 4) // We force an int32 input so we can't go over 4 bytes
binary.PutUvarint(tb, uint64(usage))
return tb
}
+125
View File
@@ -0,0 +1,125 @@
// Package rfc8009 provides encryption and checksum methods as specified in RFC 8009
package rfc8009
import (
"crypto/aes"
"crypto/hmac"
"crypto/rand"
"errors"
"fmt"
"github.com/jcmturner/aescts/v2"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/common"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/etype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
)
// EncryptData encrypts the data provided using methods specific to the etype provided as defined in RFC 8009.
func EncryptData(key, data []byte, e etype.EType) ([]byte, []byte, error) {
kl := e.GetKeyByteSize()
if e.GetETypeID() == etypeID.AES256_CTS_HMAC_SHA384_192 {
kl = 32
}
if len(key) != kl {
return []byte{}, []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", e.GetKeyByteSize(), len(key))
}
ivz := make([]byte, aes.BlockSize)
return aescts.Encrypt(key, ivz, data)
}
// EncryptMessage encrypts the message provided using the methods specific to the etype provided as defined in RFC 8009.
// The encrypted data is concatenated with its integrity hash to create an encrypted message.
func EncryptMessage(key, message []byte, usage uint32, e etype.EType) ([]byte, []byte, error) {
kl := e.GetKeyByteSize()
if e.GetETypeID() == etypeID.AES256_CTS_HMAC_SHA384_192 {
kl = 32
}
if len(key) != kl {
return []byte{}, []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", kl, len(key))
}
if len(key) != e.GetKeyByteSize() {
}
//confounder
c := make([]byte, e.GetConfounderByteSize())
_, err := rand.Read(c)
if err != nil {
return []byte{}, []byte{}, fmt.Errorf("could not generate random confounder: %v", err)
}
plainBytes := append(c, message...)
// Derive key for encryption from usage
var k []byte
if usage != 0 {
k, err = e.DeriveKey(key, common.GetUsageKe(usage))
if err != nil {
return []byte{}, []byte{}, fmt.Errorf("error deriving key for encryption: %v", err)
}
}
// Encrypt the data
iv, b, err := e.EncryptData(k, plainBytes)
if err != nil {
return iv, b, fmt.Errorf("error encrypting data: %v", err)
}
ivz := make([]byte, e.GetConfounderByteSize())
ih, err := GetIntegityHash(ivz, b, key, usage, e)
if err != nil {
return iv, b, fmt.Errorf("error encrypting data: %v", err)
}
b = append(b, ih...)
return iv, b, nil
}
// DecryptData decrypts the data provided using the methods specific to the etype provided as defined in RFC 8009.
func DecryptData(key, data []byte, e etype.EType) ([]byte, error) {
kl := e.GetKeyByteSize()
if e.GetETypeID() == etypeID.AES256_CTS_HMAC_SHA384_192 {
kl = 32
}
if len(key) != kl {
return []byte{}, fmt.Errorf("incorrect keysize: expected: %v actual: %v", kl, len(key))
}
ivz := make([]byte, aes.BlockSize)
return aescts.Decrypt(key, ivz, data)
}
// DecryptMessage decrypts the message provided using the methods specific to the etype provided as defined in RFC 8009.
// The integrity of the message is also verified.
func DecryptMessage(key, ciphertext []byte, usage uint32, e etype.EType) ([]byte, error) {
//Derive the key
k, err := e.DeriveKey(key, common.GetUsageKe(usage))
if err != nil {
return nil, fmt.Errorf("error deriving key: %v", err)
}
// Strip off the checksum from the end
b, err := e.DecryptData(k, ciphertext[:len(ciphertext)-e.GetHMACBitLength()/8])
if err != nil {
return nil, err
}
//Verify checksum
if !e.VerifyIntegrity(key, ciphertext, b, usage) {
return nil, errors.New("integrity verification failed")
}
//Remove the confounder bytes
return b[e.GetConfounderByteSize():], nil
}
// GetIntegityHash returns a keyed integrity hash of the bytes provided as defined in RFC 8009
func GetIntegityHash(iv, c, key []byte, usage uint32, e etype.EType) ([]byte, error) {
// Generate and append integrity hash
// Rather than calculating the hash over the confounder and plaintext
// it is calculated over the iv concatenated with the AES cipher output.
ib := append(iv, c...)
return common.GetIntegrityHash(ib, key, usage, e)
}
// VerifyIntegrity verifies the integrity of cipertext bytes ct.
func VerifyIntegrity(key, ct []byte, usage uint32, etype etype.EType) bool {
h := make([]byte, etype.GetHMACBitLength()/8)
copy(h, ct[len(ct)-etype.GetHMACBitLength()/8:])
ivz := make([]byte, etype.GetConfounderByteSize())
ib := append(ivz, ct[:len(ct)-(etype.GetHMACBitLength()/8)]...)
expectedMAC, _ := common.GetIntegrityHash(ib, key, usage, etype)
return hmac.Equal(h, expectedMAC)
}
+135
View File
@@ -0,0 +1,135 @@
package rfc8009
import (
"crypto/hmac"
"encoding/binary"
"encoding/hex"
"errors"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/etype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/etypeID"
"golang.org/x/crypto/pbkdf2"
)
const (
s2kParamsZero = 32768
)
// DeriveRandom for key derivation as defined in RFC 8009
func DeriveRandom(protocolKey, usage []byte, e etype.EType) ([]byte, error) {
h := e.GetHashFunc()()
return KDF_HMAC_SHA2(protocolKey, []byte("prf"), usage, h.Size(), e), nil
}
// DeriveKey derives a key from the protocol key based on the usage and the etype's specific methods.
//
// https://tools.ietf.org/html/rfc8009#section-5
func DeriveKey(protocolKey, label []byte, e etype.EType) []byte {
var context []byte
var kl int
// Key length is longer for aes256-cts-hmac-sha384-192 is it is a Ke or from StringToKey (where label is "kerberos")
if e.GetETypeID() == etypeID.AES256_CTS_HMAC_SHA384_192 {
Swtch:
switch label[len(label)-1] {
case 0x73:
// 0x73 is "s" so label could be kerberos meaning StringToKey so now check if the label is "kerberos"
kerblabel := []byte("kerberos")
if len(label) != len(kerblabel) {
break
}
for i, b := range label {
if b != kerblabel[i] {
kl = e.GetKeySeedBitLength()
break Swtch
}
}
if kl == 0 {
// This is StringToKey
kl = 256
}
case 0xAA:
// This is a Ke
kl = 256
}
}
if kl == 0 {
kl = e.GetKeySeedBitLength()
}
return e.RandomToKey(KDF_HMAC_SHA2(protocolKey, label, context, kl, e))
}
// RandomToKey returns a key from the bytes provided according to the definition in RFC 8009.
func RandomToKey(b []byte) []byte {
return b
}
// StringToKey returns a key derived from the string provided according to the definition in RFC 8009.
func StringToKey(secret, salt, s2kparams string, e etype.EType) ([]byte, error) {
i, err := S2KparamsToItertions(s2kparams)
if err != nil {
return nil, err
}
return StringToKeyIter(secret, salt, i, e)
}
// StringToKeyIter returns a key derived from the string provided according to the definition in RFC 8009.
func StringToKeyIter(secret, salt string, iterations int, e etype.EType) ([]byte, error) {
tkey := e.RandomToKey(StringToPBKDF2(secret, salt, iterations, e))
return e.DeriveKey(tkey, []byte("kerberos"))
}
// StringToPBKDF2 generates an encryption key from a pass phrase and salt string using the PBKDF2 function from PKCS #5 v2.0
func StringToPBKDF2(secret, salt string, iterations int, e etype.EType) []byte {
kl := e.GetKeyByteSize()
if e.GetETypeID() == etypeID.AES256_CTS_HMAC_SHA384_192 {
kl = 32
}
return pbkdf2.Key([]byte(secret), []byte(salt), iterations, kl, e.GetHashFunc())
}
// KDF_HMAC_SHA2 key derivation: https://tools.ietf.org/html/rfc8009#section-3
func KDF_HMAC_SHA2(protocolKey, label, context []byte, kl int, e etype.EType) []byte {
//k: Length in bits of the key to be outputted, expressed in big-endian binary representation in 4 bytes.
k := make([]byte, 4, 4)
binary.BigEndian.PutUint32(k, uint32(kl))
c := make([]byte, 4, 4)
binary.BigEndian.PutUint32(c, uint32(1))
c = append(c, label...)
c = append(c, byte(0))
if len(context) > 0 {
c = append(c, context...)
}
c = append(c, k...)
mac := hmac.New(e.GetHashFunc(), protocolKey)
mac.Write(c)
return mac.Sum(nil)[:(kl / 8)]
}
// GetSaltP returns the salt value based on the etype name: https://tools.ietf.org/html/rfc8009#section-4
func GetSaltP(salt, ename string) string {
b := []byte(ename)
b = append(b, byte(0))
b = append(b, []byte(salt)...)
return string(b)
}
// S2KparamsToItertions converts the string representation of iterations to an integer for RFC 8009.
func S2KparamsToItertions(s2kparams string) (int, error) {
var i uint32
if len(s2kparams) != 8 {
return s2kParamsZero, errors.New("Invalid s2kparams length")
}
b, err := hex.DecodeString(s2kparams)
if err != nil {
return s2kParamsZero, errors.New("Invalid s2kparams, cannot decode string to bytes")
}
i = binary.BigEndian.Uint32(b)
//buf := bytes.NewBuffer(b)
//err = binary.Read(buf, binary.BigEndian, &i)
if err != nil {
return s2kParamsZero, errors.New("Invalid s2kparams, cannot convert to big endian int32")
}
return int(i), nil
}
+28
View File
@@ -0,0 +1,28 @@
/*
Package gokrb5 provides a Kerberos 5 implementation for Go.
This is a pure Go implementation and does not have dependencies on native libraries.
Feature include:
Server Side
HTTP handler wrapper implements SPNEGO Kerberos authentication.
HTTP handler wrapper decodes Microsoft AD PAC authorization data.
Client Side
Client that can authenticate to an SPNEGO Kerberos authenticated web service.
Ability to change client's password.
General
Kerberos libraries for custom integration.
Parsing Keytab files.
Parsing krb5.conf files.
*/
package gokrb5
+174
View File
@@ -0,0 +1,174 @@
package gssapi
import (
"bytes"
"crypto/hmac"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// RFC 4121, section 4.2.6.1
const (
// MICTokenFlagSentByAcceptor - this flag indicates the sender is the context acceptor. When not set, it indicates the sender is the context initiator
MICTokenFlagSentByAcceptor = 1 << iota
// MICTokenFlagSealed - this flag indicates confidentiality is provided for. It SHALL NOT be set in MIC tokens
MICTokenFlagSealed
// MICTokenFlagAcceptorSubkey - a subkey asserted by the context acceptor is used to protect the message
MICTokenFlagAcceptorSubkey
)
const (
micHdrLen = 16 // Length of the MIC Token's header
)
// MICToken represents a GSS API MIC token, as defined in RFC 4121.
// It contains the header fields, the payload (this is not transmitted) and
// the checksum, and provides the logic for converting to/from bytes plus
// computing and verifying checksums
type MICToken struct {
// const GSS Token ID: 0x0404
Flags byte // contains three flags: acceptor, sealed, acceptor subkey
// const Filler: 0xFF 0xFF 0xFF 0xFF 0xFF
SndSeqNum uint64 // sender's sequence number. big-endian
Payload []byte // your data! :)
Checksum []byte // checksum of { payload | header }
}
// Return the 2 bytes identifying a GSS API MIC token
func getGSSMICTokenID() *[2]byte {
return &[2]byte{0x04, 0x04}
}
// Return the filler bytes used in header
func fillerBytes() *[5]byte {
return &[5]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
}
// Marshal the MICToken into a byte slice.
// The payload should have been set and the checksum computed, otherwise an error is returned.
func (mt *MICToken) Marshal() ([]byte, error) {
if mt.Checksum == nil {
return nil, errors.New("checksum has not been set")
}
bytes := make([]byte, micHdrLen+len(mt.Checksum))
copy(bytes[0:micHdrLen], mt.getMICChecksumHeader()[:])
copy(bytes[micHdrLen:], mt.Checksum)
return bytes, nil
}
// SetChecksum uses the passed encryption key and key usage to compute the checksum over the payload and
// the header, and sets the Checksum field of this MICToken.
// If the payload has not been set or the checksum has already been set, an error is returned.
func (mt *MICToken) SetChecksum(key types.EncryptionKey, keyUsage uint32) error {
if mt.Checksum != nil {
return errors.New("checksum has already been computed")
}
checksum, err := mt.checksum(key, keyUsage)
if err != nil {
return err
}
mt.Checksum = checksum
return nil
}
// Compute and return the checksum of this token, computed using the passed key and key usage.
// Note: This will NOT update the struct's Checksum field.
func (mt *MICToken) checksum(key types.EncryptionKey, keyUsage uint32) ([]byte, error) {
if mt.Payload == nil {
return nil, errors.New("cannot compute checksum with uninitialized payload")
}
d := make([]byte, micHdrLen+len(mt.Payload))
copy(d[0:], mt.Payload)
copy(d[len(mt.Payload):], mt.getMICChecksumHeader())
encType, err := crypto.GetEtype(key.KeyType)
if err != nil {
return nil, err
}
return encType.GetChecksumHash(key.KeyValue, d, keyUsage)
}
// Build a header suitable for a checksum computation
func (mt *MICToken) getMICChecksumHeader() []byte {
header := make([]byte, micHdrLen)
copy(header[0:2], getGSSMICTokenID()[:])
header[2] = mt.Flags
copy(header[3:8], fillerBytes()[:])
binary.BigEndian.PutUint64(header[8:16], mt.SndSeqNum)
return header
}
// Verify computes the token's checksum with the provided key and usage,
// and compares it to the checksum present in the token.
// In case of any failure, (false, err) is returned, with err an explanatory error.
func (mt *MICToken) Verify(key types.EncryptionKey, keyUsage uint32) (bool, error) {
computed, err := mt.checksum(key, keyUsage)
if err != nil {
return false, err
}
if !hmac.Equal(computed, mt.Checksum) {
return false, fmt.Errorf(
"checksum mismatch. Computed: %s, Contained in token: %s",
hex.EncodeToString(computed), hex.EncodeToString(mt.Checksum))
}
return true, nil
}
// Unmarshal bytes into the corresponding MICToken.
// If expectFromAcceptor is true we expect the token to have been emitted by the gss acceptor,
// and will check the according flag, returning an error if the token does not match the expectation.
func (mt *MICToken) Unmarshal(b []byte, expectFromAcceptor bool) error {
if len(b) < micHdrLen {
return errors.New("bytes shorter than header length")
}
if !bytes.Equal(getGSSMICTokenID()[:], b[0:2]) {
return fmt.Errorf("wrong Token ID, Expected %s, was %s",
hex.EncodeToString(getGSSMICTokenID()[:]),
hex.EncodeToString(b[0:2]))
}
flags := b[2]
isFromAcceptor := flags&MICTokenFlagSentByAcceptor != 0
if isFromAcceptor && !expectFromAcceptor {
return errors.New("unexpected acceptor flag is set: not expecting a token from the acceptor")
}
if !isFromAcceptor && expectFromAcceptor {
return errors.New("unexpected acceptor flag is not set: expecting a token from the acceptor, not in the initiator")
}
if !bytes.Equal(b[3:8], fillerBytes()[:]) {
return fmt.Errorf("unexpected filler bytes: expecting %s, was %s",
hex.EncodeToString(fillerBytes()[:]),
hex.EncodeToString(b[3:8]))
}
mt.Flags = flags
mt.SndSeqNum = binary.BigEndian.Uint64(b[8:16])
mt.Checksum = b[micHdrLen:]
return nil
}
// NewInitiatorMICToken builds a new initiator token (acceptor flag will be set to 0) and computes the authenticated checksum.
// Other flags are set to 0.
// Note that in certain circumstances you may need to provide a sequence number that has been defined earlier.
// This is currently not supported.
func NewInitiatorMICToken(payload []byte, key types.EncryptionKey) (*MICToken, error) {
token := MICToken{
Flags: 0x00,
SndSeqNum: 0,
Payload: payload,
}
if err := token.SetChecksum(key, keyusage.GSSAPI_INITIATOR_SIGN); err != nil {
return nil, err
}
return &token, nil
}
+20
View File
@@ -0,0 +1,20 @@
# Notes on GSS-API Negotiation Mechanism
https://tools.ietf.org/html/rfc4178
Client sends an initial negotiation message to the server which specifies the list of mechanisms
the client can support in order of decreasing preference.
This message is generated with the ``NewNegTokenInitKrb5`` method.
The message generated by this function specifies only a kerberos v5 mechanism is supported.
The RFC states that this message can optionally contain the initial mechanism token
for the preferred mechanism (KRB5 in this case) of the client. The ``NewNegTokenInitKrb5``
includes this in the message.
The server side responds to this message with a one of four messages:
| Message Type/State | Description |
|--------------------|-------------|
| accept-completed | indicates that the initiator-selected mechanism was acceptable to the target, and that the security mechanism token embedded in the first negotiation message was sufficient to complete the authentication |
| accept-incomplete | At least one more message is needed from the client to establish security context. |
| reject | Negotiation is being terminated. |
| request-mic | (this state can only be present in the first reply message from the target) indicates that the MIC token exchange is REQUIRED if per-message integrity services are available |
+27
View File
@@ -0,0 +1,27 @@
package gssapi
import "github.com/jcmturner/gofork/encoding/asn1"
// GSS-API context flags assigned numbers.
const (
ContextFlagDeleg = 1
ContextFlagMutual = 2
ContextFlagReplay = 4
ContextFlagSequence = 8
ContextFlagConf = 16
ContextFlagInteg = 32
ContextFlagAnon = 64
)
// ContextFlags flags for GSSAPI
// DEPRECATED - do not use
type ContextFlags asn1.BitString
// NewContextFlags creates a new ContextFlags instance
// DEPRECATED - do not use
func NewContextFlags() ContextFlags {
var c ContextFlags
c.BitLength = 32
c.Bytes = make([]byte, 4)
return c
}
+202
View File
@@ -0,0 +1,202 @@
// Package gssapi implements Generic Security Services Application Program Interface required for SPNEGO kerberos authentication.
package gssapi
import (
"context"
"fmt"
"github.com/jcmturner/gofork/encoding/asn1"
)
// GSS-API OID names
const (
// GSS-API OID names
OIDKRB5 OIDName = "KRB5" // MechType OID for Kerberos 5
OIDMSLegacyKRB5 OIDName = "MSLegacyKRB5" // MechType OID for Kerberos 5
OIDSPNEGO OIDName = "SPNEGO"
OIDGSSIAKerb OIDName = "GSSIAKerb" // Indicates the client cannot get a service ticket and asks the server to serve as an intermediate to the target KDC. http://k5wiki.kerberos.org/wiki/Projects/IAKERB#IAKERB_mech
)
// GSS-API status values
const (
StatusBadBindings = 1 << iota
StatusBadMech
StatusBadName
StatusBadNameType
StatusBadStatus
StatusBadSig
StatusBadMIC
StatusContextExpired
StatusCredentialsExpired
StatusDefectiveCredential
StatusDefectiveToken
StatusFailure
StatusNoContext
StatusNoCred
StatusBadQOP
StatusUnauthorized
StatusUnavailable
StatusDuplicateElement
StatusNameNotMN
StatusComplete
StatusContinueNeeded
StatusDuplicateToken
StatusOldToken
StatusUnseqToken
StatusGapToken
)
// ContextToken is an interface for a GSS-API context token.
type ContextToken interface {
Marshal() ([]byte, error)
Unmarshal(b []byte) error
Verify() (bool, Status)
Context() context.Context
}
/*
CREDENTIAL MANAGEMENT
GSS_Acquire_cred acquire credentials for use
GSS_Release_cred release credentials after use
GSS_Inquire_cred display information about credentials
GSS_Add_cred construct credentials incrementally
GSS_Inquire_cred_by_mech display per-mechanism credential information
CONTEXT-LEVEL CALLS
GSS_Init_sec_context initiate outbound security context
GSS_Accept_sec_context accept inbound security context
GSS_Delete_sec_context flush context when no longer needed
GSS_Process_context_token process received control token on context
GSS_Context_time indicate validity time remaining on context
GSS_Inquire_context display information about context
GSS_Wrap_size_limit determine GSS_Wrap token size limit
GSS_Export_sec_context transfer context to other process
GSS_Import_sec_context import transferred context
PER-MESSAGE CALLS
GSS_GetMIC apply integrity check, receive as token separate from message
GSS_VerifyMIC validate integrity check token along with message
GSS_Wrap sign, optionally encrypt, encapsulate
GSS_Unwrap decapsulate, decrypt if needed, validate integrity check
SUPPORT CALLS
GSS_Display_status translate status codes to printable form
GSS_Indicate_mechs indicate mech_types supported on local system
GSS_Compare_name compare two names for equality
GSS_Display_name translate name to printable form
GSS_Import_name convert printable name to normalized form
GSS_Release_name free storage of normalized-form name
GSS_Release_buffer free storage of general GSS-allocated object
GSS_Release_OID_set free storage of OID set object
GSS_Create_empty_OID_set create empty OID set
GSS_Add_OID_set_member add member to OID set
GSS_Test_OID_set_member test if OID is member of OID set
GSS_Inquire_names_for_mech indicate name types supported by mechanism
GSS_Inquire_mechs_for_name indicates mechanisms supporting name type
GSS_Canonicalize_name translate name to per-mechanism form
GSS_Export_name externalize per-mechanism name
GSS_Duplicate_name duplicate name object
*/
// Mechanism is the GSS-API interface for authentication mechanisms.
type Mechanism interface {
OID() asn1.ObjectIdentifier
AcquireCred() error // acquire credentials for use (eg. AS exchange for KRB5)
InitSecContext() (ContextToken, error) // initiate outbound security context (eg TGS exchange builds AP_REQ to go into ContextToken to send to service)
AcceptSecContext(ct ContextToken) (bool, context.Context, Status) // service verifies the token server side to establish a context
MIC() MICToken // apply integrity check, receive as token separate from message
VerifyMIC(mt MICToken) (bool, error) // validate integrity check token along with message
Wrap(msg []byte) WrapToken // sign, optionally encrypt, encapsulate
Unwrap(wt WrapToken) []byte // decapsulate, decrypt if needed, validate integrity check
}
// OIDName is the type for defined GSS-API OIDs.
type OIDName string
// OID returns the OID for the provided OID name.
func (o OIDName) OID() asn1.ObjectIdentifier {
switch o {
case OIDSPNEGO:
return asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 2}
case OIDKRB5:
return asn1.ObjectIdentifier{1, 2, 840, 113554, 1, 2, 2}
case OIDMSLegacyKRB5:
return asn1.ObjectIdentifier{1, 2, 840, 48018, 1, 2, 2}
case OIDGSSIAKerb:
return asn1.ObjectIdentifier{1, 3, 6, 1, 5, 2, 5}
}
return asn1.ObjectIdentifier{}
}
// Status is the GSS-API status and implements the error interface.
type Status struct {
Code int
Message string
}
// Error returns the Status description.
func (s Status) Error() string {
var str string
switch s.Code {
case StatusBadBindings:
str = "channel binding mismatch"
case StatusBadMech:
str = "unsupported mechanism requested"
case StatusBadName:
str = "invalid name provided"
case StatusBadNameType:
str = "name of unsupported type provided"
case StatusBadStatus:
str = "invalid input status selector"
case StatusBadSig:
str = "token had invalid integrity check"
case StatusBadMIC:
str = "preferred alias for GSS_S_BAD_SIG"
case StatusContextExpired:
str = "specified security context expired"
case StatusCredentialsExpired:
str = "expired credentials detected"
case StatusDefectiveCredential:
str = "defective credential detected"
case StatusDefectiveToken:
str = "defective token detected"
case StatusFailure:
str = "failure, unspecified at GSS-API level"
case StatusNoContext:
str = "no valid security context specified"
case StatusNoCred:
str = "no valid credentials provided"
case StatusBadQOP:
str = "unsupported QOP valu"
case StatusUnauthorized:
str = "operation unauthorized"
case StatusUnavailable:
str = "operation unavailable"
case StatusDuplicateElement:
str = "duplicate credential element requested"
case StatusNameNotMN:
str = "name contains multi-mechanism elements"
case StatusComplete:
str = "normal completion"
case StatusContinueNeeded:
str = "continuation call to routine required"
case StatusDuplicateToken:
str = "duplicate per-message token detected"
case StatusOldToken:
str = "timed-out per-message token detected"
case StatusUnseqToken:
str = "reordered (early) per-message token detected"
case StatusGapToken:
str = "skipped predecessor token(s) detected"
default:
str = "unknown GSS-API error status"
}
if s.Message != "" {
return fmt.Sprintf("%s: %s", str, s.Message)
}
return str
}
+195
View File
@@ -0,0 +1,195 @@
package gssapi
import (
"bytes"
"crypto/hmac"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// RFC 4121, section 4.2.6.2
const (
// HdrLen is the length of the Wrap Token's header
HdrLen = 16
// FillerByte is a filler in the WrapToken structure
FillerByte byte = 0xFF
)
// WrapToken represents a GSS API Wrap token, as defined in RFC 4121.
// It contains the header fields, the payload and the checksum, and provides
// the logic for converting to/from bytes plus computing and verifying checksums
type WrapToken struct {
// const GSS Token ID: 0x0504
Flags byte // contains three flags: acceptor, sealed, acceptor subkey
// const Filler: 0xFF
EC uint16 // checksum length. big-endian
RRC uint16 // right rotation count. big-endian
SndSeqNum uint64 // sender's sequence number. big-endian
Payload []byte // your data! :)
CheckSum []byte // authenticated checksum of { payload | header }
}
// Return the 2 bytes identifying a GSS API Wrap token
func getGssWrapTokenId() *[2]byte {
return &[2]byte{0x05, 0x04}
}
// Marshal the WrapToken into a byte slice.
// The payload should have been set and the checksum computed, otherwise an error is returned.
func (wt *WrapToken) Marshal() ([]byte, error) {
if wt.CheckSum == nil {
return nil, errors.New("checksum has not been set")
}
if wt.Payload == nil {
return nil, errors.New("payload has not been set")
}
pldOffset := HdrLen // Offset of the payload in the token
chkSOffset := HdrLen + len(wt.Payload) // Offset of the checksum in the token
bytes := make([]byte, chkSOffset+int(wt.EC))
copy(bytes[0:], getGssWrapTokenId()[:])
bytes[2] = wt.Flags
bytes[3] = FillerByte
binary.BigEndian.PutUint16(bytes[4:6], wt.EC)
binary.BigEndian.PutUint16(bytes[6:8], wt.RRC)
binary.BigEndian.PutUint64(bytes[8:16], wt.SndSeqNum)
copy(bytes[pldOffset:], wt.Payload)
copy(bytes[chkSOffset:], wt.CheckSum)
return bytes, nil
}
// SetCheckSum uses the passed encryption key and key usage to compute the checksum over the payload and
// the header, and sets the CheckSum field of this WrapToken.
// If the payload has not been set or the checksum has already been set, an error is returned.
func (wt *WrapToken) SetCheckSum(key types.EncryptionKey, keyUsage uint32) error {
if wt.Payload == nil {
return errors.New("payload has not been set")
}
if wt.CheckSum != nil {
return errors.New("checksum has already been computed")
}
chkSum, cErr := wt.computeCheckSum(key, keyUsage)
if cErr != nil {
return cErr
}
wt.CheckSum = chkSum
return nil
}
// ComputeCheckSum computes and returns the checksum of this token, computed using the passed key and key usage.
// Note: This will NOT update the struct's Checksum field.
func (wt *WrapToken) computeCheckSum(key types.EncryptionKey, keyUsage uint32) ([]byte, error) {
if wt.Payload == nil {
return nil, errors.New("cannot compute checksum with uninitialized payload")
}
// Build a slice containing { payload | header }
checksumMe := make([]byte, HdrLen+len(wt.Payload))
copy(checksumMe[0:], wt.Payload)
copy(checksumMe[len(wt.Payload):], getChecksumHeader(wt.Flags, wt.SndSeqNum))
encType, err := crypto.GetEtype(key.KeyType)
if err != nil {
return nil, err
}
return encType.GetChecksumHash(key.KeyValue, checksumMe, keyUsage)
}
// Build a header suitable for a checksum computation
func getChecksumHeader(flags byte, senderSeqNum uint64) []byte {
header := make([]byte, 16)
copy(header[0:], []byte{0x05, 0x04, flags, 0xFF, 0x00, 0x00, 0x00, 0x00})
binary.BigEndian.PutUint64(header[8:], senderSeqNum)
return header
}
// Verify computes the token's checksum with the provided key and usage,
// and compares it to the checksum present in the token.
// In case of any failure, (false, Err) is returned, with Err an explanatory error.
func (wt *WrapToken) Verify(key types.EncryptionKey, keyUsage uint32) (bool, error) {
computed, cErr := wt.computeCheckSum(key, keyUsage)
if cErr != nil {
return false, cErr
}
if !hmac.Equal(computed, wt.CheckSum) {
return false, fmt.Errorf(
"checksum mismatch. Computed: %s, Contained in token: %s",
hex.EncodeToString(computed), hex.EncodeToString(wt.CheckSum))
}
return true, nil
}
// Unmarshal bytes into the corresponding WrapToken.
// If expectFromAcceptor is true, we expect the token to have been emitted by the gss acceptor,
// and will check the according flag, returning an error if the token does not match the expectation.
func (wt *WrapToken) Unmarshal(b []byte, expectFromAcceptor bool) error {
// Check if we can read a whole header
if len(b) < 16 {
return errors.New("bytes shorter than header length")
}
// Is the Token ID correct?
if !bytes.Equal(getGssWrapTokenId()[:], b[0:2]) {
return fmt.Errorf("wrong Token ID. Expected %s, was %s",
hex.EncodeToString(getGssWrapTokenId()[:]),
hex.EncodeToString(b[0:2]))
}
// Check the acceptor flag
flags := b[2]
isFromAcceptor := flags&0x01 == 1
if isFromAcceptor && !expectFromAcceptor {
return errors.New("unexpected acceptor flag is set: not expecting a token from the acceptor")
}
if !isFromAcceptor && expectFromAcceptor {
return errors.New("expected acceptor flag is not set: expecting a token from the acceptor, not the initiator")
}
// Check the filler byte
if b[3] != FillerByte {
return fmt.Errorf("unexpected filler byte: expecting 0xFF, was %s ", hex.EncodeToString(b[3:4]))
}
checksumL := binary.BigEndian.Uint16(b[4:6])
// Sanity check on the checksum length
if int(checksumL) > len(b)-HdrLen {
return fmt.Errorf("inconsistent checksum length: %d bytes to parse, checksum length is %d", len(b), checksumL)
}
wt.Flags = flags
wt.EC = checksumL
wt.RRC = binary.BigEndian.Uint16(b[6:8])
wt.SndSeqNum = binary.BigEndian.Uint64(b[8:16])
wt.Payload = b[16 : len(b)-int(checksumL)]
wt.CheckSum = b[len(b)-int(checksumL):]
return nil
}
// NewInitiatorWrapToken builds a new initiator token (acceptor flag will be set to 0) and computes the authenticated checksum.
// Other flags are set to 0, and the RRC and sequence number are initialized to 0.
// Note that in certain circumstances you may need to provide a sequence number that has been defined earlier.
// This is currently not supported.
func NewInitiatorWrapToken(payload []byte, key types.EncryptionKey) (*WrapToken, error) {
encType, err := crypto.GetEtype(key.KeyType)
if err != nil {
return nil, err
}
token := WrapToken{
Flags: 0x00, // all zeroed out (this is a token sent by the initiator)
// Checksum size: length of output of the HMAC function, in bytes.
EC: uint16(encType.GetHMACBitLength() / 8),
RRC: 0,
SndSeqNum: 0,
Payload: payload,
}
if err := token.SetCheckSum(key, keyusage.GSSAPI_INITIATOR_SEAL); err != nil {
return nil, err
}
return &token, nil
}
+15
View File
@@ -0,0 +1,15 @@
// Package addrtype provides Address type assigned numbers.
package addrtype
// Address type IDs.
const (
IPv4 int32 = 2
Directional int32 = 3
ChaosNet int32 = 5
XNS int32 = 6
ISO int32 = 7
DECNETPhaseIV int32 = 12
AppleTalkDDP int32 = 16
NetBios int32 = 20
IPv6 int32 = 24
)
+23
View File
@@ -0,0 +1,23 @@
// Package adtype provides Authenticator type assigned numbers.
package adtype
// Authenticator type IDs.
const (
ADIfRelevant int32 = 1
ADIntendedForServer int32 = 2
ADIntendedForApplicationClass int32 = 3
ADKDCIssued int32 = 4
ADAndOr int32 = 5
ADMandatoryTicketExtensions int32 = 6
ADInTicketExtensions int32 = 7
ADMandatoryForKDC int32 = 8
OSFDCE int32 = 64
SESAME int32 = 65
ADOSFDCEPKICertID int32 = 66
ADAuthenticationStrength int32 = 70
ADFXFastArmor int32 = 71
ADFXFastUsed int32 = 72
ADWin2KPAC int32 = 128
ADEtypeNegotiation int32 = 129
//Reserved values 9-63
)
+24
View File
@@ -0,0 +1,24 @@
// Package asnAppTag provides ASN1 application tag numbers.
package asnAppTag
// ASN1 application tag numbers.
const (
Ticket = 1
Authenticator = 2
EncTicketPart = 3
ASREQ = 10
TGSREQ = 12
ASREP = 11
TGSREP = 13
APREQ = 14
APREP = 15
KRBSafe = 20
KRBPriv = 21
KRBCred = 22
EncASRepPart = 25
EncTGSRepPart = 26
EncAPRepPart = 27
EncKrbPrivPart = 28
EncKrbCredPart = 29
KRBError = 30
)
+32
View File
@@ -0,0 +1,32 @@
// Package chksumtype provides Kerberos 5 checksum type assigned numbers.
package chksumtype
// Checksum type IDs.
const (
//RESERVED : 0
CRC32 int32 = 1
RSA_MD4 int32 = 2
RSA_MD4_DES int32 = 3
DES_MAC int32 = 4
DES_MAC_K int32 = 5
RSA_MD4_DES_K int32 = 6
RSA_MD5 int32 = 7
RSA_MD5_DES int32 = 8
RSA_MD5_DES3 int32 = 9
SHA1_ID10 int32 = 10
//UNASSIGNED : 11
HMAC_SHA1_DES3_KD int32 = 12
HMAC_SHA1_DES3 int32 = 13
SHA1_ID14 int32 = 14
HMAC_SHA1_96_AES128 int32 = 15
HMAC_SHA1_96_AES256 int32 = 16
CMAC_CAMELLIA128 int32 = 17
CMAC_CAMELLIA256 int32 = 18
HMAC_SHA256_128_AES128 int32 = 19
HMAC_SHA384_192_AES256 int32 = 20
//UNASSIGNED : 21-32770
GSSAPI int32 = 32771
//UNASSIGNED : 32772-2147483647
KERB_CHECKSUM_HMAC_MD5_UNSIGNED uint32 = 4294967158 // 0xFFFFFF76 documentation says this is -138 but in an unsigned int this is 4294967158
KERB_CHECKSUM_HMAC_MD5 int32 = -138
)
+5
View File
@@ -0,0 +1,5 @@
// Package iana provides Kerberos 5 assigned numbers.
package iana
// PVNO is the Protocol Version Number.
const PVNO = 5
+155
View File
@@ -0,0 +1,155 @@
// Package errorcode provides Kerberos 5 assigned error codes.
package errorcode
import "fmt"
// Kerberos error codes.
const (
KDC_ERR_NONE int32 = 0 //No error
KDC_ERR_NAME_EXP int32 = 1 //Client's entry in database has expired
KDC_ERR_SERVICE_EXP int32 = 2 //Server's entry in database has expired
KDC_ERR_BAD_PVNO int32 = 3 //Requested protocol version number not supported
KDC_ERR_C_OLD_MAST_KVNO int32 = 4 //Client's key encrypted in old master key
KDC_ERR_S_OLD_MAST_KVNO int32 = 5 //Server's key encrypted in old master key
KDC_ERR_C_PRINCIPAL_UNKNOWN int32 = 6 //Client not found in Kerberos database
KDC_ERR_S_PRINCIPAL_UNKNOWN int32 = 7 //Server not found in Kerberos database
KDC_ERR_PRINCIPAL_NOT_UNIQUE int32 = 8 //Multiple principal entries in database
KDC_ERR_NULL_KEY int32 = 9 //The client or server has a null key
KDC_ERR_CANNOT_POSTDATE int32 = 10 //Ticket not eligible for postdating
KDC_ERR_NEVER_VALID int32 = 11 //Requested starttime is later than end time
KDC_ERR_POLICY int32 = 12 //KDC policy rejects request
KDC_ERR_BADOPTION int32 = 13 //KDC cannot accommodate requested option
KDC_ERR_ETYPE_NOSUPP int32 = 14 //KDC has no support for encryption type
KDC_ERR_SUMTYPE_NOSUPP int32 = 15 //KDC has no support for checksum type
KDC_ERR_PADATA_TYPE_NOSUPP int32 = 16 //KDC has no support for padata type
KDC_ERR_TRTYPE_NOSUPP int32 = 17 //KDC has no support for transited type
KDC_ERR_CLIENT_REVOKED int32 = 18 //Clients credentials have been revoked
KDC_ERR_SERVICE_REVOKED int32 = 19 //Credentials for server have been revoked
KDC_ERR_TGT_REVOKED int32 = 20 //TGT has been revoked
KDC_ERR_CLIENT_NOTYET int32 = 21 //Client not yet valid; try again later
KDC_ERR_SERVICE_NOTYET int32 = 22 //Server not yet valid; try again later
KDC_ERR_KEY_EXPIRED int32 = 23 //Password has expired; change password to reset
KDC_ERR_PREAUTH_FAILED int32 = 24 //Pre-authentication information was invalid
KDC_ERR_PREAUTH_REQUIRED int32 = 25 //Additional pre-authentication required
KDC_ERR_SERVER_NOMATCH int32 = 26 //Requested server and ticket don't match
KDC_ERR_MUST_USE_USER2USER int32 = 27 //Server principal valid for user2user only
KDC_ERR_PATH_NOT_ACCEPTED int32 = 28 //KDC Policy rejects transited path
KDC_ERR_SVC_UNAVAILABLE int32 = 29 //A service is not available
KRB_AP_ERR_BAD_INTEGRITY int32 = 31 //Integrity check on decrypted field failed
KRB_AP_ERR_TKT_EXPIRED int32 = 32 //Ticket expired
KRB_AP_ERR_TKT_NYV int32 = 33 //Ticket not yet valid
KRB_AP_ERR_REPEAT int32 = 34 //Request is a replay
KRB_AP_ERR_NOT_US int32 = 35 //The ticket isn't for us
KRB_AP_ERR_BADMATCH int32 = 36 //Ticket and authenticator don't match
KRB_AP_ERR_SKEW int32 = 37 //Clock skew too great
KRB_AP_ERR_BADADDR int32 = 38 //Incorrect net address
KRB_AP_ERR_BADVERSION int32 = 39 //Protocol version mismatch
KRB_AP_ERR_MSG_TYPE int32 = 40 //Invalid msg type
KRB_AP_ERR_MODIFIED int32 = 41 //Message stream modified
KRB_AP_ERR_BADORDER int32 = 42 //Message out of order
KRB_AP_ERR_BADKEYVER int32 = 44 //Specified version of key is not available
KRB_AP_ERR_NOKEY int32 = 45 //Service key not available
KRB_AP_ERR_MUT_FAIL int32 = 46 //Mutual authentication failed
KRB_AP_ERR_BADDIRECTION int32 = 47 //Incorrect message direction
KRB_AP_ERR_METHOD int32 = 48 //Alternative authentication method required
KRB_AP_ERR_BADSEQ int32 = 49 //Incorrect sequence number in message
KRB_AP_ERR_INAPP_CKSUM int32 = 50 //Inappropriate type of checksum in message
KRB_AP_PATH_NOT_ACCEPTED int32 = 51 //Policy rejects transited path
KRB_ERR_RESPONSE_TOO_BIG int32 = 52 //Response too big for UDP; retry with TCP
KRB_ERR_GENERIC int32 = 60 //Generic error (description in e-text)
KRB_ERR_FIELD_TOOLONG int32 = 61 //Field is too long for this implementation
KDC_ERROR_CLIENT_NOT_TRUSTED int32 = 62 //Reserved for PKINIT
KDC_ERROR_KDC_NOT_TRUSTED int32 = 63 //Reserved for PKINIT
KDC_ERROR_INVALID_SIG int32 = 64 //Reserved for PKINIT
KDC_ERR_KEY_TOO_WEAK int32 = 65 //Reserved for PKINIT
KDC_ERR_CERTIFICATE_MISMATCH int32 = 66 //Reserved for PKINIT
KRB_AP_ERR_NO_TGT int32 = 67 //No TGT available to validate USER-TO-USER
KDC_ERR_WRONG_REALM int32 = 68 //Reserved for future use
KRB_AP_ERR_USER_TO_USER_REQUIRED int32 = 69 //Ticket must be for USER-TO-USER
KDC_ERR_CANT_VERIFY_CERTIFICATE int32 = 70 //Reserved for PKINIT
KDC_ERR_INVALID_CERTIFICATE int32 = 71 //Reserved for PKINIT
KDC_ERR_REVOKED_CERTIFICATE int32 = 72 //Reserved for PKINIT
KDC_ERR_REVOCATION_STATUS_UNKNOWN int32 = 73 //Reserved for PKINIT
KDC_ERR_REVOCATION_STATUS_UNAVAILABLE int32 = 74 //Reserved for PKINIT
KDC_ERR_CLIENT_NAME_MISMATCH int32 = 75 //Reserved for PKINIT
KDC_ERR_KDC_NAME_MISMATCH int32 = 76 //Reserved for PKINIT
)
// Lookup an error code description.
func Lookup(i int32) string {
if s, ok := errorcodeLookup[i]; ok {
return fmt.Sprintf("(%d) %s", i, s)
}
return fmt.Sprintf("Unknown ErrorCode %d", i)
}
var errorcodeLookup = map[int32]string{
KDC_ERR_NONE: "KDC_ERR_NONE No error",
KDC_ERR_NAME_EXP: "KDC_ERR_NAME_EXP Client's entry in database has expired",
KDC_ERR_SERVICE_EXP: "KDC_ERR_SERVICE_EXP Server's entry in database has expired",
KDC_ERR_BAD_PVNO: "KDC_ERR_BAD_PVNO Requested protocol version number not supported",
KDC_ERR_C_OLD_MAST_KVNO: "KDC_ERR_C_OLD_MAST_KVNO Client's key encrypted in old master key",
KDC_ERR_S_OLD_MAST_KVNO: "KDC_ERR_S_OLD_MAST_KVNO Server's key encrypted in old master key",
KDC_ERR_C_PRINCIPAL_UNKNOWN: "KDC_ERR_C_PRINCIPAL_UNKNOWN Client not found in Kerberos database",
KDC_ERR_S_PRINCIPAL_UNKNOWN: "KDC_ERR_S_PRINCIPAL_UNKNOWN Server not found in Kerberos database",
KDC_ERR_PRINCIPAL_NOT_UNIQUE: "KDC_ERR_PRINCIPAL_NOT_UNIQUE Multiple principal entries in database",
KDC_ERR_NULL_KEY: "KDC_ERR_NULL_KEY The client or server has a null key",
KDC_ERR_CANNOT_POSTDATE: "KDC_ERR_CANNOT_POSTDATE Ticket not eligible for postdating",
KDC_ERR_NEVER_VALID: "KDC_ERR_NEVER_VALID Requested starttime is later than end time",
KDC_ERR_POLICY: "KDC_ERR_POLICY KDC policy rejects request",
KDC_ERR_BADOPTION: "KDC_ERR_BADOPTION KDC cannot accommodate requested option",
KDC_ERR_ETYPE_NOSUPP: "KDC_ERR_ETYPE_NOSUPP KDC has no support for encryption type",
KDC_ERR_SUMTYPE_NOSUPP: "KDC_ERR_SUMTYPE_NOSUPP KDC has no support for checksum type",
KDC_ERR_PADATA_TYPE_NOSUPP: "KDC_ERR_PADATA_TYPE_NOSUPP KDC has no support for padata type",
KDC_ERR_TRTYPE_NOSUPP: "KDC_ERR_TRTYPE_NOSUPP KDC has no support for transited type",
KDC_ERR_CLIENT_REVOKED: "KDC_ERR_CLIENT_REVOKED Clients credentials have been revoked",
KDC_ERR_SERVICE_REVOKED: "KDC_ERR_SERVICE_REVOKED Credentials for server have been revoked",
KDC_ERR_TGT_REVOKED: "KDC_ERR_TGT_REVOKED TGT has been revoked",
KDC_ERR_CLIENT_NOTYET: "KDC_ERR_CLIENT_NOTYET Client not yet valid; try again later",
KDC_ERR_SERVICE_NOTYET: "KDC_ERR_SERVICE_NOTYET Server not yet valid; try again later",
KDC_ERR_KEY_EXPIRED: "KDC_ERR_KEY_EXPIRED Password has expired; change password to reset",
KDC_ERR_PREAUTH_FAILED: "KDC_ERR_PREAUTH_FAILED Pre-authentication information was invalid",
KDC_ERR_PREAUTH_REQUIRED: "KDC_ERR_PREAUTH_REQUIRED Additional pre-authentication required",
KDC_ERR_SERVER_NOMATCH: "KDC_ERR_SERVER_NOMATCH Requested server and ticket don't match",
KDC_ERR_MUST_USE_USER2USER: "KDC_ERR_MUST_USE_USER2USER Server principal valid for user2user only",
KDC_ERR_PATH_NOT_ACCEPTED: "KDC_ERR_PATH_NOT_ACCEPTED KDC Policy rejects transited path",
KDC_ERR_SVC_UNAVAILABLE: "KDC_ERR_SVC_UNAVAILABLE A service is not available",
KRB_AP_ERR_BAD_INTEGRITY: "KRB_AP_ERR_BAD_INTEGRITY Integrity check on decrypted field failed",
KRB_AP_ERR_TKT_EXPIRED: "KRB_AP_ERR_TKT_EXPIRED Ticket expired",
KRB_AP_ERR_TKT_NYV: "KRB_AP_ERR_TKT_NYV Ticket not yet valid",
KRB_AP_ERR_REPEAT: "KRB_AP_ERR_REPEAT Request is a replay",
KRB_AP_ERR_NOT_US: "KRB_AP_ERR_NOT_US The ticket isn't for us",
KRB_AP_ERR_BADMATCH: "KRB_AP_ERR_BADMATCH Ticket and authenticator don't match",
KRB_AP_ERR_SKEW: "KRB_AP_ERR_SKEW Clock skew too great",
KRB_AP_ERR_BADADDR: "KRB_AP_ERR_BADADDR Incorrect net address",
KRB_AP_ERR_BADVERSION: "KRB_AP_ERR_BADVERSION Protocol version mismatch",
KRB_AP_ERR_MSG_TYPE: "KRB_AP_ERR_MSG_TYPE Invalid msg type",
KRB_AP_ERR_MODIFIED: "KRB_AP_ERR_MODIFIED Message stream modified",
KRB_AP_ERR_BADORDER: "KRB_AP_ERR_BADORDER Message out of order",
KRB_AP_ERR_BADKEYVER: "KRB_AP_ERR_BADKEYVER Specified version of key is not available",
KRB_AP_ERR_NOKEY: "KRB_AP_ERR_NOKEY Service key not available",
KRB_AP_ERR_MUT_FAIL: "KRB_AP_ERR_MUT_FAIL Mutual authentication failed",
KRB_AP_ERR_BADDIRECTION: "KRB_AP_ERR_BADDIRECTION Incorrect message direction",
KRB_AP_ERR_METHOD: "KRB_AP_ERR_METHOD Alternative authentication method required",
KRB_AP_ERR_BADSEQ: "KRB_AP_ERR_BADSEQ Incorrect sequence number in message",
KRB_AP_ERR_INAPP_CKSUM: "KRB_AP_ERR_INAPP_CKSUM Inappropriate type of checksum in message",
KRB_AP_PATH_NOT_ACCEPTED: "KRB_AP_PATH_NOT_ACCEPTED Policy rejects transited path",
KRB_ERR_RESPONSE_TOO_BIG: "KRB_ERR_RESPONSE_TOO_BIG Response too big for UDP; retry with TCP",
KRB_ERR_GENERIC: "KRB_ERR_GENERIC Generic error (description in e-text)",
KRB_ERR_FIELD_TOOLONG: "KRB_ERR_FIELD_TOOLONG Field is too long for this implementation",
KDC_ERROR_CLIENT_NOT_TRUSTED: "KDC_ERROR_CLIENT_NOT_TRUSTED Reserved for PKINIT",
KDC_ERROR_KDC_NOT_TRUSTED: "KDC_ERROR_KDC_NOT_TRUSTED Reserved for PKINIT",
KDC_ERROR_INVALID_SIG: "KDC_ERROR_INVALID_SIG Reserved for PKINIT",
KDC_ERR_KEY_TOO_WEAK: "KDC_ERR_KEY_TOO_WEAK Reserved for PKINIT",
KDC_ERR_CERTIFICATE_MISMATCH: "KDC_ERR_CERTIFICATE_MISMATCH Reserved for PKINIT",
KRB_AP_ERR_NO_TGT: "KRB_AP_ERR_NO_TGT No TGT available to validate USER-TO-USER",
KDC_ERR_WRONG_REALM: "KDC_ERR_WRONG_REALM Reserved for future use",
KRB_AP_ERR_USER_TO_USER_REQUIRED: "KRB_AP_ERR_USER_TO_USER_REQUIRED Ticket must be for USER-TO-USER",
KDC_ERR_CANT_VERIFY_CERTIFICATE: "KDC_ERR_CANT_VERIFY_CERTIFICATE Reserved for PKINIT",
KDC_ERR_INVALID_CERTIFICATE: "KDC_ERR_INVALID_CERTIFICATE Reserved for PKINIT",
KDC_ERR_REVOKED_CERTIFICATE: "KDC_ERR_REVOKED_CERTIFICATE Reserved for PKINIT",
KDC_ERR_REVOCATION_STATUS_UNKNOWN: "KDC_ERR_REVOCATION_STATUS_UNKNOWN Reserved for PKINIT",
KDC_ERR_REVOCATION_STATUS_UNAVAILABLE: "KDC_ERR_REVOCATION_STATUS_UNAVAILABLE Reserved for PKINIT",
KDC_ERR_CLIENT_NAME_MISMATCH: "KDC_ERR_CLIENT_NAME_MISMATCH Reserved for PKINIT",
KDC_ERR_KDC_NAME_MISMATCH: "KDC_ERR_KDC_NAME_MISMATCH Reserved for PKINIT",
}
+101
View File
@@ -0,0 +1,101 @@
// Package etypeID provides Kerberos 5 encryption type assigned numbers.
package etypeID
// Kerberos encryption type assigned numbers.
const (
//RESERVED : 0
DES_CBC_CRC int32 = 1
DES_CBC_MD4 int32 = 2
DES_CBC_MD5 int32 = 3
DES_CBC_RAW int32 = 4
DES3_CBC_MD5 int32 = 5
DES3_CBC_RAW int32 = 6
DES3_CBC_SHA1 int32 = 7
DES_HMAC_SHA1 int32 = 8
DSAWITHSHA1_CMSOID int32 = 9
MD5WITHRSAENCRYPTION_CMSOID int32 = 10
SHA1WITHRSAENCRYPTION_CMSOID int32 = 11
RC2CBC_ENVOID int32 = 12
RSAENCRYPTION_ENVOID int32 = 13
RSAES_OAEP_ENV_OID int32 = 14
DES_EDE3_CBC_ENV_OID int32 = 15
DES3_CBC_SHA1_KD int32 = 16
AES128_CTS_HMAC_SHA1_96 int32 = 17
AES256_CTS_HMAC_SHA1_96 int32 = 18
AES128_CTS_HMAC_SHA256_128 int32 = 19
AES256_CTS_HMAC_SHA384_192 int32 = 20
//UNASSIGNED : 21-22
RC4_HMAC int32 = 23
RC4_HMAC_EXP int32 = 24
CAMELLIA128_CTS_CMAC int32 = 25
CAMELLIA256_CTS_CMAC int32 = 26
//UNASSIGNED : 27-64
SUBKEY_KEYMATERIAL int32 = 65
//UNASSIGNED : 66-2147483647
)
// ETypesByName is a map of EncType names to their assigned EncType number.
var ETypesByName = map[string]int32{
"des-cbc-crc": DES_CBC_CRC,
"des-cbc-md4": DES_CBC_MD4,
"des-cbc-md5": DES_CBC_MD5,
"des-cbc-raw": DES_CBC_RAW,
"des3-cbc-md5": DES3_CBC_MD5,
"des3-cbc-raw": DES3_CBC_RAW,
"des3-cbc-sha1": DES3_CBC_SHA1,
"des3-hmac-sha1": DES_HMAC_SHA1,
"des3-cbc-sha1-kd": DES3_CBC_SHA1_KD,
"des-hmac-sha1": DES_HMAC_SHA1,
"dsaWithSHA1-CmsOID": DSAWITHSHA1_CMSOID,
"md5WithRSAEncryption-CmsOID": MD5WITHRSAENCRYPTION_CMSOID,
"sha1WithRSAEncryption-CmsOID": SHA1WITHRSAENCRYPTION_CMSOID,
"rc2CBC-EnvOID": RC2CBC_ENVOID,
"rsaEncryption-EnvOID": RSAENCRYPTION_ENVOID,
"rsaES-OAEP-ENV-OID": RSAES_OAEP_ENV_OID,
"des-ede3-cbc-Env-OID": DES_EDE3_CBC_ENV_OID,
"aes128-cts-hmac-sha1-96": AES128_CTS_HMAC_SHA1_96,
"aes128-cts": AES128_CTS_HMAC_SHA1_96,
"aes128-sha1": AES128_CTS_HMAC_SHA1_96,
"aes256-cts-hmac-sha1-96": AES256_CTS_HMAC_SHA1_96,
"aes256-cts": AES256_CTS_HMAC_SHA1_96,
"aes256-sha1": AES256_CTS_HMAC_SHA1_96,
"aes128-cts-hmac-sha256-128": AES128_CTS_HMAC_SHA256_128,
"aes128-sha2": AES128_CTS_HMAC_SHA256_128,
"aes256-cts-hmac-sha384-192": AES256_CTS_HMAC_SHA384_192,
"aes256-sha2": AES256_CTS_HMAC_SHA384_192,
"arcfour-hmac": RC4_HMAC,
"rc4-hmac": RC4_HMAC,
"arcfour-hmac-md5": RC4_HMAC,
"arcfour-hmac-exp": RC4_HMAC_EXP,
"rc4-hmac-exp": RC4_HMAC_EXP,
"arcfour-hmac-md5-exp": RC4_HMAC_EXP,
"camellia128-cts-cmac": CAMELLIA128_CTS_CMAC,
"camellia128-cts": CAMELLIA128_CTS_CMAC,
"camellia256-cts-cmac": CAMELLIA256_CTS_CMAC,
"camellia256-cts": CAMELLIA256_CTS_CMAC,
"subkey-keymaterial": SUBKEY_KEYMATERIAL,
}
// EtypeSupported resolves the etype name string to the etype ID.
// If zero is returned the etype is not supported by gokrb5.
func EtypeSupported(etype string) int32 {
// Slice of supported enctype IDs
s := []int32{
AES128_CTS_HMAC_SHA1_96,
AES256_CTS_HMAC_SHA1_96,
AES128_CTS_HMAC_SHA256_128,
AES256_CTS_HMAC_SHA384_192,
DES3_CBC_SHA1_KD,
RC4_HMAC,
}
id := ETypesByName[etype]
if id == 0 {
return id
}
for _, sid := range s {
if id == sid {
return id
}
}
return 0
}
+36
View File
@@ -0,0 +1,36 @@
// Package flags provides Kerberos 5 flag assigned numbers.
package flags
// Flag values for KRB5 messages and tickets.
const (
Reserved = 0
Forwardable = 1
Forwarded = 2
Proxiable = 3
Proxy = 4
AllowPostDate = 5
MayPostDate = 5
PostDated = 6
Invalid = 7
Renewable = 8
Initial = 9
PreAuthent = 10
HWAuthent = 11
OptHardwareAuth = 11
RequestAnonymous = 12
TransitedPolicyChecked = 12
OKAsDelegate = 13
EncPARep = 15
Canonicalize = 15
DisableTransitedCheck = 26
RenewableOK = 27
EncTktInSkey = 28
Renew = 30
Validate = 31
// AP Option Flags
// 0 Reserved for future use.
APOptionUseSessionKey = 1
APOptionMutualRequired = 2
// 3-31 Reserved for future use.
)
+42
View File
@@ -0,0 +1,42 @@
// Package keyusage provides Kerberos 5 key usage assigned numbers.
package keyusage
// Key usage numbers.
const (
AS_REQ_PA_ENC_TIMESTAMP = 1
KDC_REP_TICKET = 2
AS_REP_ENCPART = 3
TGS_REQ_KDC_REQ_BODY_AUTHDATA_SESSION_KEY = 4
TGS_REQ_KDC_REQ_BODY_AUTHDATA_SUB_KEY = 5
TGS_REQ_PA_TGS_REQ_AP_REQ_AUTHENTICATOR_CHKSUM = 6
TGS_REQ_PA_TGS_REQ_AP_REQ_AUTHENTICATOR = 7
TGS_REP_ENCPART_SESSION_KEY = 8
TGS_REP_ENCPART_AUTHENTICATOR_SUB_KEY = 9
AP_REQ_AUTHENTICATOR_CHKSUM = 10
AP_REQ_AUTHENTICATOR = 11
AP_REP_ENCPART = 12
KRB_PRIV_ENCPART = 13
KRB_CRED_ENCPART = 14
KRB_SAFE_CHKSUM = 15
KERB_NON_KERB_SALT = 16
KERB_NON_KERB_CKSUM_SALT = 17
//18. Reserved for future use in Kerberos and related protocols.
AD_KDC_ISSUED_CHKSUM = 19
//20-21. Reserved for future use in Kerberos and related protocols.
GSSAPI_ACCEPTOR_SEAL = 22
GSSAPI_ACCEPTOR_SIGN = 23
GSSAPI_INITIATOR_SEAL = 24
GSSAPI_INITIATOR_SIGN = 25
KEY_USAGE_FAST_REQ_CHKSUM = 50
KEY_USAGE_FAST_ENC = 51
KEY_USAGE_FAST_REP = 52
KEY_USAGE_FAST_FINISHED = 53
KEY_USAGE_ENC_CHALLENGE_CLIENT = 54
KEY_USAGE_ENC_CHALLENGE_KDC = 55
KEY_USAGE_AS_REQ = 56
//26-511. Reserved for future use in Kerberos and related protocols.
//512-1023. Reserved for uses internal to a Kerberos implementation.
//1024. Encryption for application use in protocols that do not specify key usage values
//1025. Checksums for application use in protocols that do not specify key usage values
//1026-2047. Reserved for application use.
)
+18
View File
@@ -0,0 +1,18 @@
// Package msgtype provides Kerberos 5 message type assigned numbers.
package msgtype
// KRB message type IDs.
const (
KRB_AS_REQ = 10 //Request for initial authentication
KRB_AS_REP = 11 //Response to KRB_AS_REQ request
KRB_TGS_REQ = 12 //Request for authentication based on TGT
KRB_TGS_REP = 13 //Response to KRB_TGS_REQ request
KRB_AP_REQ = 14 //Application request to server
KRB_AP_REP = 15 //Response to KRB_AP_REQ_MUTUAL
KRB_RESERVED16 = 16 //Reserved for user-to-user krb_tgt_request
KRB_RESERVED17 = 17 //Reserved for user-to-user krb_tgt_reply
KRB_SAFE = 20 // Safe (checksummed) application message
KRB_PRIV = 21 // Private (encrypted) application message
KRB_CRED = 22 //Private (encrypted) message to forward credentials
KRB_ERROR = 30 //Error response
)
+15
View File
@@ -0,0 +1,15 @@
// Package nametype provides Kerberos 5 principal name type numbers.
package nametype
// Kerberos name type IDs.
const (
KRB_NT_UNKNOWN int32 = 0 //Name type not known
KRB_NT_PRINCIPAL int32 = 1 //Just the name of the principal as in DCE, or for users
KRB_NT_SRV_INST int32 = 2 //Service and other unique instance (krbtgt)
KRB_NT_SRV_HST int32 = 3 //Service with host name as instance (telnet, rcommands)
KRB_NT_SRV_XHST int32 = 4 //Service with host as remaining components
KRB_NT_UID int32 = 5 //Unique ID
KRB_NT_X500_PRINCIPAL int32 = 6 //Encoded X.509 Distinguished name [RFC2253]
KRB_NT_SMTP_NAME int32 = 7 //Name in form of SMTP email name (e.g., user@example.com)
KRB_NT_ENTERPRISE int32 = 10 //Enterprise name; may be mapped to principal name
)
+77
View File
@@ -0,0 +1,77 @@
// Package patype provides Kerberos 5 pre-authentication type assigned numbers.
package patype
// Kerberos pre-authentication type assigned numbers.
const (
PA_TGS_REQ int32 = 1
PA_ENC_TIMESTAMP int32 = 2
PA_PW_SALT int32 = 3
//RESERVED : 4
PA_ENC_UNIX_TIME int32 = 5
PA_SANDIA_SECUREID int32 = 6
PA_SESAME int32 = 7
PA_OSF_DCE int32 = 8
PA_CYBERSAFE_SECUREID int32 = 9
PA_AFS3_SALT int32 = 10
PA_ETYPE_INFO int32 = 11
PA_SAM_CHALLENGE int32 = 12
PA_SAM_RESPONSE int32 = 13
PA_PK_AS_REQ_OLD int32 = 14
PA_PK_AS_REP_OLD int32 = 15
PA_PK_AS_REQ int32 = 16
PA_PK_AS_REP int32 = 17
PA_PK_OCSP_RESPONSE int32 = 18
PA_ETYPE_INFO2 int32 = 19
PA_USE_SPECIFIED_KVNO int32 = 20
PA_SVR_REFERRAL_INFO int32 = 20
PA_SAM_REDIRECT int32 = 21
PA_GET_FROM_TYPED_DATA int32 = 22
TD_PADATA int32 = 22
PA_SAM_ETYPE_INFO int32 = 23
PA_ALT_PRINC int32 = 24
PA_SERVER_REFERRAL int32 = 25
//UNASSIGNED : 26-29
PA_SAM_CHALLENGE2 int32 = 30
PA_SAM_RESPONSE2 int32 = 31
//UNASSIGNED : 32-40
PA_EXTRA_TGT int32 = 41
//UNASSIGNED : 42-100
TD_PKINIT_CMS_CERTIFICATES int32 = 101
TD_KRB_PRINCIPAL int32 = 102
TD_KRB_REALM int32 = 103
TD_TRUSTED_CERTIFIERS int32 = 104
TD_CERTIFICATE_INDEX int32 = 105
TD_APP_DEFINED_ERROR int32 = 106
TD_REQ_NONCE int32 = 107
TD_REQ_SEQ int32 = 108
TD_DH_PARAMETERS int32 = 109
//UNASSIGNED : 110
TD_CMS_DIGEST_ALGORITHMS int32 = 111
TD_CERT_DIGEST_ALGORITHMS int32 = 112
//UNASSIGNED : 113-127
PA_PAC_REQUEST int32 = 128
PA_FOR_USER int32 = 129
PA_FOR_X509_USER int32 = 130
PA_FOR_CHECK_DUPS int32 = 131
PA_AS_CHECKSUM int32 = 132
PA_FX_COOKIE int32 = 133
PA_AUTHENTICATION_SET int32 = 134
PA_AUTH_SET_SELECTED int32 = 135
PA_FX_FAST int32 = 136
PA_FX_ERROR int32 = 137
PA_ENCRYPTED_CHALLENGE int32 = 138
//UNASSIGNED : 139-140
PA_OTP_CHALLENGE int32 = 141
PA_OTP_REQUEST int32 = 142
PA_OTP_CONFIRM int32 = 143
PA_OTP_PIN_CHANGE int32 = 144
PA_EPAK_AS_REQ int32 = 145
PA_EPAK_AS_REP int32 = 146
PA_PKINIT_KX int32 = 147
PA_PKU2U_NAME int32 = 148
PA_REQ_ENC_PA_REP int32 = 149
PA_AS_FRESHNESS int32 = 150
//UNASSIGNED : 151-164
PA_SUPPORTED_ETYPES int32 = 165
PA_EXTENDED_ERROR int32 = 166
)
+8
View File
@@ -0,0 +1,8 @@
// Package trtype provides Transited Encoding Type assigned numbers.
package trtype
// Transited Encoding Type IDs.
const (
DOMAIN_X500_COMPRESS int32 = 1
//Reserved values All others
)
+23
View File
@@ -0,0 +1,23 @@
package kadmin
import (
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// ChangePasswdData is the payload to a password change message.
type ChangePasswdData struct {
NewPasswd []byte `asn1:"explicit,tag:0"`
TargName types.PrincipalName `asn1:"explicit,optional,tag:1"`
TargRealm string `asn1:"generalstring,optional,explicit,tag:2"`
}
// Marshal ChangePasswdData into a byte slice.
func (c *ChangePasswdData) Marshal() ([]byte, error) {
b, err := asn1.Marshal(*c)
if err != nil {
return []byte{}, err
}
//b = asn1tools.AddASNAppTag(b, asnAppTag.)
return b, nil
}
+114
View File
@@ -0,0 +1,114 @@
package kadmin
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
const (
verisonHex = "ff80"
)
// Request message for changing password.
type Request struct {
APREQ messages.APReq
KRBPriv messages.KRBPriv
}
// Reply message for a password change.
type Reply struct {
MessageLength int
Version int
APREPLength int
APREP messages.APRep
KRBPriv messages.KRBPriv
KRBError messages.KRBError
IsKRBError bool
ResultCode uint16
Result string
}
// Marshal a Request into a byte slice.
func (m *Request) Marshal() (b []byte, err error) {
b = []byte{255, 128} // protocol version number: contains the hex constant 0xff80 (big-endian integer).
ab, e := m.APREQ.Marshal()
if e != nil {
err = fmt.Errorf("error marshaling AP_REQ: %v", e)
return
}
if len(ab) > math.MaxUint16 {
err = errors.New("length of AP_REQ greater then max Uint16 size")
return
}
al := make([]byte, 2)
binary.BigEndian.PutUint16(al, uint16(len(ab)))
b = append(b, al...)
b = append(b, ab...)
pb, e := m.KRBPriv.Marshal()
if e != nil {
err = fmt.Errorf("error marshaling KRB_Priv: %v", e)
return
}
b = append(b, pb...)
if len(b)+2 > math.MaxUint16 {
err = errors.New("length of message greater then max Uint16 size")
return
}
ml := make([]byte, 2)
binary.BigEndian.PutUint16(ml, uint16(len(b)+2))
b = append(ml, b...)
return
}
// Unmarshal a byte slice into a Reply.
func (m *Reply) Unmarshal(b []byte) error {
m.MessageLength = int(binary.BigEndian.Uint16(b[0:2]))
m.Version = int(binary.BigEndian.Uint16(b[2:4]))
if m.Version != 1 {
return fmt.Errorf("kadmin reply has incorrect protocol version number: %d", m.Version)
}
m.APREPLength = int(binary.BigEndian.Uint16(b[4:6]))
if m.APREPLength != 0 {
err := m.APREP.Unmarshal(b[6 : 6+m.APREPLength])
if err != nil {
return err
}
err = m.KRBPriv.Unmarshal(b[6+m.APREPLength : m.MessageLength])
if err != nil {
return err
}
} else {
m.IsKRBError = true
m.KRBError.Unmarshal(b[6:m.MessageLength])
m.ResultCode, m.Result = parseResponse(m.KRBError.EData)
}
return nil
}
func parseResponse(b []byte) (c uint16, s string) {
c = binary.BigEndian.Uint16(b[0:2])
buf := bytes.NewBuffer(b[2:])
m := make([]byte, len(b)-2)
binary.Read(buf, binary.BigEndian, &m)
s = string(m)
return
}
// Decrypt the encrypted part of the KRBError within the change password Reply.
func (m *Reply) Decrypt(key types.EncryptionKey) error {
if m.IsKRBError {
return m.KRBError
}
err := m.KRBPriv.DecryptEncPart(key)
if err != nil {
return err
}
m.ResultCode, m.Result = parseResponse(m.KRBPriv.DecryptedEncPart.UserData)
return nil
}
+68
View File
@@ -0,0 +1,68 @@
// Package kadmin provides Kerberos administration capabilities.
package kadmin
import (
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/messages"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// ChangePasswdMsg generate a change password request and also return the key needed to decrypt the reply.
func ChangePasswdMsg(cname types.PrincipalName, realm, password string, tkt messages.Ticket, sessionKey types.EncryptionKey) (r Request, k types.EncryptionKey, err error) {
// Create change password data struct and marshal to bytes
chgpasswd := ChangePasswdData{
NewPasswd: []byte(password),
TargName: cname,
TargRealm: realm,
}
chpwdb, err := chgpasswd.Marshal()
if err != nil {
err = krberror.Errorf(err, krberror.KRBMsgError, "error marshaling change passwd data")
return
}
// Generate authenticator
auth, err := types.NewAuthenticator(realm, cname)
if err != nil {
err = krberror.Errorf(err, krberror.KRBMsgError, "error generating new authenticator")
return
}
etype, err := crypto.GetEtype(sessionKey.KeyType)
if err != nil {
err = krberror.Errorf(err, krberror.KRBMsgError, "error generating subkey etype")
return
}
err = auth.GenerateSeqNumberAndSubKey(etype.GetETypeID(), etype.GetKeyByteSize())
if err != nil {
err = krberror.Errorf(err, krberror.KRBMsgError, "error generating subkey")
return
}
k = auth.SubKey
// Generate AP_REQ
APreq, err := messages.NewAPReq(tkt, sessionKey, auth)
if err != nil {
return
}
// Form the KRBPriv encpart data
kp := messages.EncKrbPrivPart{
UserData: chpwdb,
Timestamp: auth.CTime,
Usec: auth.Cusec,
SequenceNumber: auth.SeqNumber,
}
kpriv := messages.NewKRBPriv(kp)
err = kpriv.EncryptEncPart(k)
if err != nil {
err = krberror.Errorf(err, krberror.EncryptingError, "error encrypting change passwd data")
return
}
r = Request{
APREQ: APreq,
KRBPriv: kpriv,
}
return
}
+530
View File
@@ -0,0 +1,530 @@
// Package keytab implements Kerberos keytabs: https://web.mit.edu/kerberos/krb5-devel/doc/formats/keytab_file_format.html.
package keytab
import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strings"
"time"
"unsafe"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
const (
keytabFirstByte byte = 05
)
// Keytab struct.
type Keytab struct {
version uint8
Entries []entry
}
// Keytab entry struct.
type entry struct {
Principal principal
Timestamp time.Time
KVNO8 uint8
Key types.EncryptionKey
KVNO uint32
}
func (e entry) String() string {
return fmt.Sprintf("% 4d %s %-56s %2d %-64x",
e.KVNO8,
e.Timestamp.Format("02/01/06 15:04:05"),
e.Principal.String(),
e.Key.KeyType,
e.Key.KeyValue,
)
}
// Keytab entry principal struct.
type principal struct {
NumComponents int16 `json:"-"`
Realm string
Components []string
NameType int32
}
func (p principal) String() string {
return fmt.Sprintf("%s@%s", strings.Join(p.Components, "/"), p.Realm)
}
// New creates new, empty Keytab type.
func New() *Keytab {
var e []entry
return &Keytab{
version: 2,
Entries: e,
}
}
// GetEncryptionKey returns the EncryptionKey from the Keytab for the newest entry with the required kvno, etype and matching principal.
// If the kvno is zero then the latest kvno will be returned. The kvno is also returned for
func (kt *Keytab) GetEncryptionKey(princName types.PrincipalName, realm string, kvno int, etype int32) (types.EncryptionKey, int, error) {
var key types.EncryptionKey
var t time.Time
var kv int
for _, k := range kt.Entries {
if k.Principal.Realm == realm && len(k.Principal.Components) == len(princName.NameString) &&
k.Key.KeyType == etype &&
(k.KVNO == uint32(kvno) || kvno == 0) &&
k.Timestamp.After(t) {
p := true
for i, n := range k.Principal.Components {
if princName.NameString[i] != n {
p = false
break
}
}
if p {
key = k.Key
kv = int(k.KVNO)
t = k.Timestamp
}
}
}
if len(key.KeyValue) < 1 {
return key, 0, fmt.Errorf("matching key not found in keytab. Looking for %q realm: %v kvno: %v etype: %v", princName.PrincipalNameString(), realm, kvno, etype)
}
return key, kv, nil
}
// Create a new Keytab entry.
func newEntry() entry {
var b []byte
return entry{
Principal: newPrincipal(),
Timestamp: time.Time{},
KVNO8: 0,
Key: types.EncryptionKey{
KeyType: 0,
KeyValue: b,
},
KVNO: 0,
}
}
func (kt Keytab) String() string {
var s string
s = `KVNO Timestamp Principal ET Key
---- ----------------- -------------------------------------------------------- -- ----------------------------------------------------------------
`
for _, entry := range kt.Entries {
s += entry.String() + "\n"
}
return s
}
// AddEntry adds an entry to the keytab. The password should be provided in plain text and it will be converted using the defined enctype to be stored.
func (kt *Keytab) AddEntry(principalName, realm, password string, ts time.Time, KVNO uint8, encType int32) error {
// Generate a key from the password
princ, _ := types.ParseSPNString(principalName)
key, _, err := crypto.GetKeyFromPassword(password, princ, realm, encType, types.PADataSequence{})
if err != nil {
return err
}
// Populate the keytab entry principal
ktep := newPrincipal()
ktep.NumComponents = int16(len(princ.NameString))
if kt.version == 1 {
ktep.NumComponents += 1
}
ktep.Realm = realm
ktep.Components = princ.NameString
ktep.NameType = princ.NameType
// Populate the keytab entry
e := newEntry()
e.Principal = ktep
e.Timestamp = ts
e.KVNO8 = KVNO
e.KVNO = uint32(KVNO)
e.Key = key
kt.Entries = append(kt.Entries, e)
return nil
}
// Create a new principal.
func newPrincipal() principal {
var c []string
return principal{
NumComponents: 0,
Realm: "",
Components: c,
NameType: 0,
}
}
// Load a Keytab file into a Keytab type.
func Load(ktPath string) (*Keytab, error) {
kt := new(Keytab)
b, err := os.ReadFile(ktPath)
if err != nil {
return kt, err
}
err = kt.Unmarshal(b)
return kt, err
}
// Marshal keytab into byte slice
func (kt *Keytab) Marshal() ([]byte, error) {
b := []byte{keytabFirstByte, kt.version}
for _, e := range kt.Entries {
eb, err := e.marshal(int(kt.version))
if err != nil {
return b, err
}
b = append(b, eb...)
}
return b, nil
}
// Write the keytab bytes to io.Writer.
// Returns the number of bytes written
func (kt *Keytab) Write(w io.Writer) (int, error) {
b, err := kt.Marshal()
if err != nil {
return 0, fmt.Errorf("error marshaling keytab: %v", err)
}
return w.Write(b)
}
// Unmarshal byte slice of Keytab data into Keytab type.
func (kt *Keytab) Unmarshal(b []byte) error {
if len(b) < 2 {
return fmt.Errorf("byte array is less than 2 bytes: %d", len(b))
}
//The first byte of the file always has the value 5
if b[0] != keytabFirstByte {
return errors.New("invalid keytab data. First byte does not equal 5")
}
//Get keytab version
//The 2nd byte contains the version number (1 or 2)
kt.version = b[1]
if kt.version != 1 && kt.version != 2 {
return errors.New("invalid keytab data. Keytab version is neither 1 nor 2")
}
//Version 1 of the file format uses native byte order for integer representations. Version 2 always uses big-endian byte order
var endian binary.ByteOrder
endian = binary.BigEndian
if kt.version == 1 && isNativeEndianLittle() {
endian = binary.LittleEndian
}
// n tracks position in the byte array
n := 2
l, err := readInt32(b, &n, &endian)
if err != nil {
return err
}
for l != 0 {
if l < 0 {
//Zero padded so skip over
l = l * -1
n = n + int(l)
} else {
if n < 0 {
return fmt.Errorf("%d can't be less than zero", n)
}
if n+int(l) > len(b) {
return fmt.Errorf("%s's length is less than %d", b, n+int(l))
}
eb := b[n : n+int(l)]
n = n + int(l)
ke := newEntry()
// p keeps track as to where we are in the byte stream
var p int
var err error
parsePrincipal(eb, &p, kt, &ke, &endian)
ke.Timestamp, err = readTimestamp(eb, &p, &endian)
if err != nil {
return err
}
rei8, err := readInt8(eb, &p, &endian)
if err != nil {
return err
}
ke.KVNO8 = uint8(rei8)
rei16, err := readInt16(eb, &p, &endian)
if err != nil {
return err
}
ke.Key.KeyType = int32(rei16)
rei16, err = readInt16(eb, &p, &endian)
if err != nil {
return err
}
kl := int(rei16)
ke.Key.KeyValue, err = readBytes(eb, &p, kl, &endian)
if err != nil {
return err
}
// The 32-bit key version overrides the 8-bit key version.
// If at least 4 bytes are left after the other fields are read and they are non-zero
// this indicates the 32-bit version is present.
if len(eb)-p >= 4 {
// The 32-bit key may be present
ri32, err := readInt32(eb, &p, &endian)
if err != nil {
return err
}
ke.KVNO = uint32(ri32)
}
if ke.KVNO == 0 {
// Handles if the value from the last 4 bytes was zero and also if there are not the 4 bytes present. Makes sense to put the same value here as KVNO8
ke.KVNO = uint32(ke.KVNO8)
}
// Add the entry to the keytab
kt.Entries = append(kt.Entries, ke)
}
// Check if there are still 4 bytes left to read
// Also check that n is greater than zero
if n < 0 || n > len(b) || len(b[n:]) < 4 {
break
}
// Read the size of the next entry
l, err = readInt32(b, &n, &endian)
if err != nil {
return err
}
}
return nil
}
func (e entry) marshal(v int) ([]byte, error) {
var b []byte
pb, err := e.Principal.marshal(v)
if err != nil {
return b, err
}
b = append(b, pb...)
var endian binary.ByteOrder
endian = binary.BigEndian
if v == 1 && isNativeEndianLittle() {
endian = binary.LittleEndian
}
t := make([]byte, 9)
endian.PutUint32(t[0:4], uint32(e.Timestamp.Unix()))
t[4] = e.KVNO8
endian.PutUint16(t[5:7], uint16(e.Key.KeyType))
endian.PutUint16(t[7:9], uint16(len(e.Key.KeyValue)))
b = append(b, t...)
buf := new(bytes.Buffer)
err = binary.Write(buf, endian, e.Key.KeyValue)
if err != nil {
return b, err
}
b = append(b, buf.Bytes()...)
t = make([]byte, 4)
endian.PutUint32(t, e.KVNO)
b = append(b, t...)
// Add the length header
t = make([]byte, 4)
endian.PutUint32(t, uint32(len(b)))
b = append(t, b...)
return b, nil
}
// Parse the Keytab bytes of a principal into a Keytab entry's principal.
func parsePrincipal(b []byte, p *int, kt *Keytab, ke *entry, e *binary.ByteOrder) error {
var err error
ke.Principal.NumComponents, err = readInt16(b, p, e)
if err != nil {
return err
}
if kt.version == 1 {
//In version 1 the number of components includes the realm. Minus 1 to make consistent with version 2
ke.Principal.NumComponents--
}
lenRealm, err := readInt16(b, p, e)
if err != nil {
return err
}
realmB, err := readBytes(b, p, int(lenRealm), e)
if err != nil {
return err
}
ke.Principal.Realm = string(realmB)
for i := 0; i < int(ke.Principal.NumComponents); i++ {
l, err := readInt16(b, p, e)
if err != nil {
return err
}
compB, err := readBytes(b, p, int(l), e)
if err != nil {
return err
}
ke.Principal.Components = append(ke.Principal.Components, string(compB))
}
if kt.version != 1 {
//Name Type is omitted in version 1
ke.Principal.NameType, err = readInt32(b, p, e)
if err != nil {
return err
}
}
return nil
}
func (p principal) marshal(v int) ([]byte, error) {
//var b []byte
b := make([]byte, 2)
var endian binary.ByteOrder
endian = binary.BigEndian
if v == 1 && isNativeEndianLittle() {
endian = binary.LittleEndian
}
endian.PutUint16(b[0:], uint16(p.NumComponents))
realm, err := marshalString(p.Realm, v)
if err != nil {
return b, err
}
b = append(b, realm...)
for _, c := range p.Components {
cb, err := marshalString(c, v)
if err != nil {
return b, err
}
b = append(b, cb...)
}
if v != 1 {
t := make([]byte, 4)
endian.PutUint32(t, uint32(p.NameType))
b = append(b, t...)
}
return b, nil
}
func marshalString(s string, v int) ([]byte, error) {
sb := []byte(s)
b := make([]byte, 2)
var endian binary.ByteOrder
endian = binary.BigEndian
if v == 1 && isNativeEndianLittle() {
endian = binary.LittleEndian
}
endian.PutUint16(b[0:], uint16(len(sb)))
buf := new(bytes.Buffer)
err := binary.Write(buf, endian, sb)
if err != nil {
return b, err
}
b = append(b, buf.Bytes()...)
return b, err
}
// Read bytes representing a timestamp.
func readTimestamp(b []byte, p *int, e *binary.ByteOrder) (time.Time, error) {
i32, err := readInt32(b, p, e)
if err != nil {
return time.Time{}, err
}
return time.Unix(int64(i32), 0), nil
}
// Read bytes representing an eight bit integer.
func readInt8(b []byte, p *int, e *binary.ByteOrder) (i int8, err error) {
if *p < 0 {
return 0, fmt.Errorf("%d cannot be less than zero", *p)
}
if (*p + 1) > len(b) {
return 0, fmt.Errorf("%s's length is less than %d", b, *p+1)
}
buf := bytes.NewBuffer(b[*p : *p+1])
binary.Read(buf, *e, &i)
*p++
return
}
// Read bytes representing a sixteen bit integer.
func readInt16(b []byte, p *int, e *binary.ByteOrder) (i int16, err error) {
if *p < 0 {
return 0, fmt.Errorf("%d cannot be less than zero", *p)
}
if (*p + 2) > len(b) {
return 0, fmt.Errorf("%s's length is less than %d", b, *p+2)
}
buf := bytes.NewBuffer(b[*p : *p+2])
binary.Read(buf, *e, &i)
*p += 2
return
}
// Read bytes representing a thirty two bit integer.
func readInt32(b []byte, p *int, e *binary.ByteOrder) (i int32, err error) {
if *p < 0 {
return 0, fmt.Errorf("%d cannot be less than zero", *p)
}
if (*p + 4) > len(b) {
return 0, fmt.Errorf("%s's length is less than %d", b, *p+4)
}
buf := bytes.NewBuffer(b[*p : *p+4])
binary.Read(buf, *e, &i)
*p += 4
return
}
func readBytes(b []byte, p *int, s int, e *binary.ByteOrder) ([]byte, error) {
if s < 0 {
return nil, fmt.Errorf("%d cannot be less than zero", s)
}
i := *p + s
if i > len(b) {
return nil, fmt.Errorf("%s's length is greater than %d", b, i)
}
buf := bytes.NewBuffer(b[*p:i])
r := make([]byte, s)
if err := binary.Read(buf, *e, &r); err != nil {
return nil, err
}
*p += s
return r, nil
}
func isNativeEndianLittle() bool {
var x = 0x012345678
var p = unsafe.Pointer(&x)
var bp = (*[4]byte)(p)
var endian bool
if 0x01 == bp[0] {
endian = false
} else if (0x78 & 0xff) == (bp[0] & 0xff) {
endian = true
} else {
// Default to big endian
endian = false
}
return endian
}
// JSON return information about the keys held in the keytab in a JSON format.
func (kt *Keytab) JSON() (string, error) {
b, err := json.MarshalIndent(kt, "", " ")
if err != nil {
return "", err
}
return string(b), nil
}
+67
View File
@@ -0,0 +1,67 @@
// Package krberror provides error type and functions for gokrb5.
package krberror
import (
"fmt"
"strings"
)
// Error type descriptions.
const (
separator = " < "
EncodingError = "Encoding_Error"
NetworkingError = "Networking_Error"
DecryptingError = "Decrypting_Error"
EncryptingError = "Encrypting_Error"
ChksumError = "Checksum_Error"
KRBMsgError = "KRBMessage_Handling_Error"
ConfigError = "Configuration_Error"
KDCError = "KDC_Error"
)
// Krberror is an error type for gokrb5
type Krberror struct {
RootCause string
EText []string
}
// Error function to implement the error interface.
func (e Krberror) Error() string {
return fmt.Sprintf("[Root cause: %s] ", e.RootCause) + strings.Join(e.EText, separator)
}
// Add another error statement to the error.
func (e *Krberror) Add(et string, s string) {
e.EText = append([]string{fmt.Sprintf("%s: %s", et, s)}, e.EText...)
}
// New creates a new instance of Krberror.
func New(et, s string) Krberror {
return Krberror{
RootCause: et,
EText: []string{s},
}
}
// Errorf appends to or creates a new Krberror.
func Errorf(err error, et, format string, a ...interface{}) Krberror {
if e, ok := err.(Krberror); ok {
e.Add(et, fmt.Sprintf(format, a...))
return e
}
return NewErrorf(et, format+": %s", append(a, err)...)
}
// NewErrorf creates a new Krberror from a formatted string.
func NewErrorf(et, format string, a ...interface{}) Krberror {
var s string
if len(a) > 0 {
s = fmt.Sprintf("%s: %s", et, fmt.Sprintf(format, a...))
} else {
s = fmt.Sprintf("%s: %s", et, format)
}
return Krberror{
RootCause: et,
EText: []string{s},
}
}
+49
View File
@@ -0,0 +1,49 @@
package messages
import (
"fmt"
"time"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/asnAppTag"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/msgtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// APRep implements RFC 4120 KRB_AP_REP: https://tools.ietf.org/html/rfc4120#section-5.5.2.
type APRep struct {
PVNO int `asn1:"explicit,tag:0"`
MsgType int `asn1:"explicit,tag:1"`
EncPart types.EncryptedData `asn1:"explicit,tag:2"`
}
// EncAPRepPart is the encrypted part of KRB_AP_REP.
type EncAPRepPart struct {
CTime time.Time `asn1:"generalized,explicit,tag:0"`
Cusec int `asn1:"explicit,tag:1"`
Subkey types.EncryptionKey `asn1:"optional,explicit,tag:2"`
SequenceNumber int64 `asn1:"optional,explicit,tag:3"`
}
// Unmarshal bytes b into the APRep struct.
func (a *APRep) Unmarshal(b []byte) error {
_, err := asn1.UnmarshalWithParams(b, a, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.APREP))
if err != nil {
return processUnmarshalReplyError(b, err)
}
expectedMsgType := msgtype.KRB_AP_REP
if a.MsgType != expectedMsgType {
return krberror.NewErrorf(krberror.KRBMsgError, "message ID does not indicate a KRB_AP_REP. Expected: %v; Actual: %v", expectedMsgType, a.MsgType)
}
return nil
}
// Unmarshal bytes b into the APRep encrypted part struct.
func (a *EncAPRepPart) Unmarshal(b []byte) error {
_, err := asn1.UnmarshalWithParams(b, a, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.EncAPRepPart))
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "AP_REP unmarshal error")
}
return nil
}
+199
View File
@@ -0,0 +1,199 @@
package messages
import (
"fmt"
"time"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/asn1tools"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/asnAppTag"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/errorcode"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/msgtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/keytab"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
type marshalAPReq struct {
PVNO int `asn1:"explicit,tag:0"`
MsgType int `asn1:"explicit,tag:1"`
APOptions asn1.BitString `asn1:"explicit,tag:2"`
// Ticket needs to be a raw value as it is wrapped in an APPLICATION tag
Ticket asn1.RawValue `asn1:"explicit,tag:3"`
EncryptedAuthenticator types.EncryptedData `asn1:"explicit,tag:4"`
}
// APReq implements RFC 4120 KRB_AP_REQ: https://tools.ietf.org/html/rfc4120#section-5.5.1.
type APReq struct {
PVNO int `asn1:"explicit,tag:0"`
MsgType int `asn1:"explicit,tag:1"`
APOptions asn1.BitString `asn1:"explicit,tag:2"`
Ticket Ticket `asn1:"explicit,tag:3"`
EncryptedAuthenticator types.EncryptedData `asn1:"explicit,tag:4"`
Authenticator types.Authenticator `asn1:"optional"`
}
// NewAPReq generates a new KRB_AP_REQ struct.
func NewAPReq(tkt Ticket, sessionKey types.EncryptionKey, auth types.Authenticator) (APReq, error) {
var a APReq
ed, err := encryptAuthenticator(auth, sessionKey, tkt)
if err != nil {
return a, krberror.Errorf(err, krberror.KRBMsgError, "error creating Authenticator for AP_REQ")
}
a = APReq{
PVNO: iana.PVNO,
MsgType: msgtype.KRB_AP_REQ,
APOptions: types.NewKrbFlags(),
Ticket: tkt,
EncryptedAuthenticator: ed,
}
return a, nil
}
// Encrypt Authenticator
func encryptAuthenticator(a types.Authenticator, sessionKey types.EncryptionKey, tkt Ticket) (types.EncryptedData, error) {
var ed types.EncryptedData
m, err := a.Marshal()
if err != nil {
return ed, krberror.Errorf(err, krberror.EncodingError, "marshaling error of EncryptedData form of Authenticator")
}
usage := authenticatorKeyUsage(tkt.SName)
ed, err = crypto.GetEncryptedData(m, sessionKey, uint32(usage), tkt.EncPart.KVNO)
if err != nil {
return ed, krberror.Errorf(err, krberror.EncryptingError, "error encrypting Authenticator")
}
return ed, nil
}
// DecryptAuthenticator decrypts the Authenticator within the AP_REQ.
// sessionKey may simply be the key within the decrypted EncPart of the ticket within the AP_REQ.
func (a *APReq) DecryptAuthenticator(sessionKey types.EncryptionKey) error {
usage := authenticatorKeyUsage(a.Ticket.SName)
ab, e := crypto.DecryptEncPart(a.EncryptedAuthenticator, sessionKey, uint32(usage))
if e != nil {
return fmt.Errorf("error decrypting authenticator: %v", e)
}
err := a.Authenticator.Unmarshal(ab)
if err != nil {
return fmt.Errorf("error unmarshaling authenticator: %v", err)
}
return nil
}
func authenticatorKeyUsage(pn types.PrincipalName) int {
if pn.NameString[0] == "krbtgt" {
return keyusage.TGS_REQ_PA_TGS_REQ_AP_REQ_AUTHENTICATOR
}
return keyusage.AP_REQ_AUTHENTICATOR
}
// Unmarshal bytes b into the APReq struct.
func (a *APReq) Unmarshal(b []byte) error {
var m marshalAPReq
_, err := asn1.UnmarshalWithParams(b, &m, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.APREQ))
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "unmarshal error of AP_REQ")
}
if m.MsgType != msgtype.KRB_AP_REQ {
return NewKRBError(types.PrincipalName{}, "", errorcode.KRB_AP_ERR_MSG_TYPE, errorcode.Lookup(errorcode.KRB_AP_ERR_MSG_TYPE))
}
a.PVNO = m.PVNO
a.MsgType = m.MsgType
a.APOptions = m.APOptions
a.EncryptedAuthenticator = m.EncryptedAuthenticator
a.Ticket, err = unmarshalTicket(m.Ticket.Bytes)
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "unmarshaling error of Ticket within AP_REQ")
}
return nil
}
// Marshal APReq struct.
func (a *APReq) Marshal() ([]byte, error) {
m := marshalAPReq{
PVNO: a.PVNO,
MsgType: a.MsgType,
APOptions: a.APOptions,
EncryptedAuthenticator: a.EncryptedAuthenticator,
}
var b []byte
b, err := a.Ticket.Marshal()
if err != nil {
return b, err
}
m.Ticket = asn1.RawValue{
Class: asn1.ClassContextSpecific,
IsCompound: true,
Tag: 3,
Bytes: b,
}
mk, err := asn1.Marshal(m)
if err != nil {
return mk, krberror.Errorf(err, krberror.EncodingError, "marshaling error of AP_REQ")
}
mk = asn1tools.AddASNAppTag(mk, asnAppTag.APREQ)
return mk, nil
}
// Verify an AP_REQ using service's keytab, spn and max acceptable clock skew duration.
// The service ticket encrypted part and authenticator will be decrypted as part of this operation.
func (a *APReq) Verify(kt *keytab.Keytab, d time.Duration, cAddr types.HostAddress, snameOverride *types.PrincipalName) (bool, error) {
// Decrypt ticket's encrypted part with service key
//TODO decrypt with service's session key from its TGT is use-to-user. Need to figure out how to get TGT.
//if types.IsFlagSet(&a.APOptions, flags.APOptionUseSessionKey) {
// err := a.Ticket.Decrypt(tgt.DecryptedEncPart.Key)
// if err != nil {
// return false, krberror.Errorf(err, krberror.DecryptingError, "error decrypting encpart of ticket provided using session key")
// }
//} else {
// err := a.Ticket.DecryptEncPart(*kt, &a.Ticket.SName)
// if err != nil {
// return false, krberror.Errorf(err, krberror.DecryptingError, "error decrypting encpart of service ticket provided")
// }
//}
sname := &a.Ticket.SName
if snameOverride != nil {
sname = snameOverride
}
err := a.Ticket.DecryptEncPart(kt, sname)
if err != nil {
return false, krberror.Errorf(err, krberror.DecryptingError, "error decrypting encpart of service ticket provided")
}
// Check time validity of ticket
ok, err := a.Ticket.Valid(d)
if err != nil || !ok {
return ok, err
}
// Check client's address is listed in the client addresses in the ticket
if len(a.Ticket.DecryptedEncPart.CAddr) > 0 {
//If client addresses are present check if any of them match the source IP that sent the APReq
//If there is no match return KRB_AP_ERR_BADADDR error.
if !types.HostAddressesContains(a.Ticket.DecryptedEncPart.CAddr, cAddr) {
return false, NewKRBError(a.Ticket.SName, a.Ticket.Realm, errorcode.KRB_AP_ERR_BADADDR, "client address not within the list contained in the service ticket")
}
}
// Decrypt authenticator with session key from ticket's encrypted part
err = a.DecryptAuthenticator(a.Ticket.DecryptedEncPart.Key)
if err != nil {
return false, NewKRBError(a.Ticket.SName, a.Ticket.Realm, errorcode.KRB_AP_ERR_BAD_INTEGRITY, "could not decrypt authenticator")
}
// Check CName in authenticator is the same as that in the ticket
if !a.Authenticator.CName.Equal(a.Ticket.DecryptedEncPart.CName) {
return false, NewKRBError(a.Ticket.SName, a.Ticket.Realm, errorcode.KRB_AP_ERR_BADMATCH, "CName in Authenticator does not match that in service ticket")
}
// Check the clock skew between the client and the service server
ct := a.Authenticator.CTime.Add(time.Duration(a.Authenticator.Cusec) * time.Microsecond)
t := time.Now().UTC()
if t.Sub(ct) > d || ct.Sub(t) > d {
return false, NewKRBError(a.Ticket.SName, a.Ticket.Realm, errorcode.KRB_AP_ERR_SKEW, fmt.Sprintf("clock skew with client too large. greater than %v seconds", d))
}
return true, nil
}
+360
View File
@@ -0,0 +1,360 @@
package messages
// Reference: https://www.ietf.org/rfc/rfc4120.txt
// Section: 5.4.2
import (
"fmt"
"time"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/asn1tools"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/config"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/credentials"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/asnAppTag"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/flags"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/msgtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/patype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
type marshalKDCRep struct {
PVNO int `asn1:"explicit,tag:0"`
MsgType int `asn1:"explicit,tag:1"`
PAData types.PADataSequence `asn1:"explicit,optional,tag:2"`
CRealm string `asn1:"generalstring,explicit,tag:3"`
CName types.PrincipalName `asn1:"explicit,tag:4"`
// Ticket needs to be a raw value as it is wrapped in an APPLICATION tag
Ticket asn1.RawValue `asn1:"explicit,tag:5"`
EncPart types.EncryptedData `asn1:"explicit,tag:6"`
}
// KDCRepFields represents the KRB_KDC_REP fields.
type KDCRepFields struct {
PVNO int
MsgType int
PAData []types.PAData
CRealm string
CName types.PrincipalName
Ticket Ticket
EncPart types.EncryptedData
DecryptedEncPart EncKDCRepPart
}
// ASRep implements RFC 4120 KRB_AS_REP: https://tools.ietf.org/html/rfc4120#section-5.4.2.
type ASRep struct {
KDCRepFields
}
// TGSRep implements RFC 4120 KRB_TGS_REP: https://tools.ietf.org/html/rfc4120#section-5.4.2.
type TGSRep struct {
KDCRepFields
}
// EncKDCRepPart is the encrypted part of KRB_KDC_REP.
type EncKDCRepPart struct {
Key types.EncryptionKey `asn1:"explicit,tag:0"`
LastReqs []LastReq `asn1:"explicit,tag:1"`
Nonce int `asn1:"explicit,tag:2"`
KeyExpiration time.Time `asn1:"generalized,explicit,optional,tag:3"`
Flags asn1.BitString `asn1:"explicit,tag:4"`
AuthTime time.Time `asn1:"generalized,explicit,tag:5"`
StartTime time.Time `asn1:"generalized,explicit,optional,tag:6"`
EndTime time.Time `asn1:"generalized,explicit,tag:7"`
RenewTill time.Time `asn1:"generalized,explicit,optional,tag:8"`
SRealm string `asn1:"generalstring,explicit,tag:9"`
SName types.PrincipalName `asn1:"explicit,tag:10"`
CAddr []types.HostAddress `asn1:"explicit,optional,tag:11"`
EncPAData types.PADataSequence `asn1:"explicit,optional,tag:12"`
}
// LastReq part of KRB_KDC_REP.
type LastReq struct {
LRType int32 `asn1:"explicit,tag:0"`
LRValue time.Time `asn1:"generalized,explicit,tag:1"`
}
// Unmarshal bytes b into the ASRep struct.
func (k *ASRep) Unmarshal(b []byte) error {
var m marshalKDCRep
_, err := asn1.UnmarshalWithParams(b, &m, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.ASREP))
if err != nil {
return processUnmarshalReplyError(b, err)
}
if m.MsgType != msgtype.KRB_AS_REP {
return krberror.NewErrorf(krberror.KRBMsgError, "message ID does not indicate an AS_REP. Expected: %v; Actual: %v", msgtype.KRB_AS_REP, m.MsgType)
}
//Process the raw ticket within
tkt, err := unmarshalTicket(m.Ticket.Bytes)
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error unmarshaling Ticket within AS_REP")
}
k.KDCRepFields = KDCRepFields{
PVNO: m.PVNO,
MsgType: m.MsgType,
PAData: m.PAData,
CRealm: m.CRealm,
CName: m.CName,
Ticket: tkt,
EncPart: m.EncPart,
}
return nil
}
// Marshal ASRep struct.
func (k *ASRep) Marshal() ([]byte, error) {
m := marshalKDCRep{
PVNO: k.PVNO,
MsgType: k.MsgType,
PAData: k.PAData,
CRealm: k.CRealm,
CName: k.CName,
EncPart: k.EncPart,
}
b, err := k.Ticket.Marshal()
if err != nil {
return []byte{}, err
}
m.Ticket = asn1.RawValue{
Class: asn1.ClassContextSpecific,
IsCompound: true,
Tag: 5,
Bytes: b,
}
mk, err := asn1.Marshal(m)
if err != nil {
return mk, krberror.Errorf(err, krberror.EncodingError, "error marshaling AS_REP")
}
mk = asn1tools.AddASNAppTag(mk, asnAppTag.ASREP)
return mk, nil
}
// Unmarshal bytes b into the TGSRep struct.
func (k *TGSRep) Unmarshal(b []byte) error {
var m marshalKDCRep
_, err := asn1.UnmarshalWithParams(b, &m, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.TGSREP))
if err != nil {
return processUnmarshalReplyError(b, err)
}
if m.MsgType != msgtype.KRB_TGS_REP {
return krberror.NewErrorf(krberror.KRBMsgError, "message ID does not indicate an TGS_REP. Expected: %v; Actual: %v", msgtype.KRB_TGS_REP, m.MsgType)
}
//Process the raw ticket within
tkt, err := unmarshalTicket(m.Ticket.Bytes)
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error unmarshaling Ticket within TGS_REP")
}
k.KDCRepFields = KDCRepFields{
PVNO: m.PVNO,
MsgType: m.MsgType,
PAData: m.PAData,
CRealm: m.CRealm,
CName: m.CName,
Ticket: tkt,
EncPart: m.EncPart,
}
return nil
}
// Marshal TGSRep struct.
func (k *TGSRep) Marshal() ([]byte, error) {
m := marshalKDCRep{
PVNO: k.PVNO,
MsgType: k.MsgType,
PAData: k.PAData,
CRealm: k.CRealm,
CName: k.CName,
EncPart: k.EncPart,
}
b, err := k.Ticket.Marshal()
if err != nil {
return []byte{}, err
}
m.Ticket = asn1.RawValue{
Class: asn1.ClassContextSpecific,
IsCompound: true,
Tag: 5,
Bytes: b,
}
mk, err := asn1.Marshal(m)
if err != nil {
return mk, krberror.Errorf(err, krberror.EncodingError, "error marshaling TGS_REP")
}
mk = asn1tools.AddASNAppTag(mk, asnAppTag.TGSREP)
return mk, nil
}
// Unmarshal bytes b into encrypted part of KRB_KDC_REP.
func (e *EncKDCRepPart) Unmarshal(b []byte) error {
_, err := asn1.UnmarshalWithParams(b, e, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.EncASRepPart))
if err != nil {
// Try using tag 26
// Ref: RFC 4120 - mentions that some implementations use application tag number 26 wether or not the reply is
// a AS-REP or a TGS-REP.
_, err = asn1.UnmarshalWithParams(b, e, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.EncTGSRepPart))
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error unmarshaling encrypted part within KDC_REP")
}
}
return nil
}
// Marshal encrypted part of KRB_KDC_REP.
func (e *EncKDCRepPart) Marshal() ([]byte, error) {
b, err := asn1.Marshal(*e)
if err != nil {
return b, krberror.Errorf(err, krberror.EncodingError, "marshaling error of AS_REP encpart")
}
b = asn1tools.AddASNAppTag(b, asnAppTag.EncASRepPart)
return b, nil
}
// DecryptEncPart decrypts the encrypted part of an AS_REP.
func (k *ASRep) DecryptEncPart(c *credentials.Credentials) (types.EncryptionKey, error) {
var key types.EncryptionKey
var err error
if c.HasKeytab() {
key, _, err = c.Keytab().GetEncryptionKey(k.CName, k.CRealm, k.EncPart.KVNO, k.EncPart.EType)
if err != nil {
return key, krberror.Errorf(err, krberror.DecryptingError, "error decrypting AS_REP encrypted part")
}
}
if c.HasPassword() {
key, _, err = crypto.GetKeyFromPassword(c.Password(), k.CName, k.CRealm, k.EncPart.EType, k.PAData)
if err != nil {
return key, krberror.Errorf(err, krberror.DecryptingError, "error decrypting AS_REP encrypted part")
}
}
if !c.HasKeytab() && !c.HasPassword() {
return key, krberror.NewErrorf(krberror.DecryptingError, "no secret available in credentials to perform decryption of AS_REP encrypted part")
}
b, err := crypto.DecryptEncPart(k.EncPart, key, keyusage.AS_REP_ENCPART)
if err != nil {
return key, krberror.Errorf(err, krberror.DecryptingError, "error decrypting AS_REP encrypted part")
}
var denc EncKDCRepPart
err = denc.Unmarshal(b)
if err != nil {
return key, krberror.Errorf(err, krberror.EncodingError, "error unmarshaling decrypted encpart of AS_REP")
}
k.DecryptedEncPart = denc
return key, nil
}
// Verify checks the validity of AS_REP message.
func (k *ASRep) Verify(cfg *config.Config, creds *credentials.Credentials, asReq ASReq) (bool, error) {
//Ref RFC 4120 Section 3.1.5
if !k.CName.Equal(asReq.ReqBody.CName) {
return false, krberror.NewErrorf(krberror.KRBMsgError, "CName in response does not match what was requested. Requested: %+v; Reply: %+v", asReq.ReqBody.CName, k.CName)
}
if k.CRealm != asReq.ReqBody.Realm {
return false, krberror.NewErrorf(krberror.KRBMsgError, "CRealm in response does not match what was requested. Requested: %s; Reply: %s", asReq.ReqBody.Realm, k.CRealm)
}
key, err := k.DecryptEncPart(creds)
if err != nil {
return false, krberror.Errorf(err, krberror.DecryptingError, "error decrypting EncPart of AS_REP")
}
if k.DecryptedEncPart.Nonce != asReq.ReqBody.Nonce {
return false, krberror.NewErrorf(krberror.KRBMsgError, "possible replay attack, nonce in response does not match that in request")
}
if !k.DecryptedEncPart.SName.Equal(asReq.ReqBody.SName) {
return false, krberror.NewErrorf(krberror.KRBMsgError, "SName in response does not match what was requested. Requested: %v; Reply: %v", asReq.ReqBody.SName, k.DecryptedEncPart.SName)
}
if k.DecryptedEncPart.SRealm != asReq.ReqBody.Realm {
return false, krberror.NewErrorf(krberror.KRBMsgError, "SRealm in response does not match what was requested. Requested: %s; Reply: %s", asReq.ReqBody.Realm, k.DecryptedEncPart.SRealm)
}
if len(asReq.ReqBody.Addresses) > 0 {
if !types.HostAddressesEqual(k.DecryptedEncPart.CAddr, asReq.ReqBody.Addresses) {
return false, krberror.NewErrorf(krberror.KRBMsgError, "addresses listed in the AS_REP does not match those listed in the AS_REQ")
}
}
t := time.Now().UTC()
if t.Sub(k.DecryptedEncPart.AuthTime) > cfg.LibDefaults.Clockskew || k.DecryptedEncPart.AuthTime.Sub(t) > cfg.LibDefaults.Clockskew {
return false, krberror.NewErrorf(krberror.KRBMsgError, "clock skew with KDC too large. Greater than %v seconds", cfg.LibDefaults.Clockskew.Seconds())
}
// RFC 6806 https://tools.ietf.org/html/rfc6806.html#section-11
if asReq.PAData.Contains(patype.PA_REQ_ENC_PA_REP) && types.IsFlagSet(&k.DecryptedEncPart.Flags, flags.EncPARep) {
if len(k.DecryptedEncPart.EncPAData) < 2 || !k.DecryptedEncPart.EncPAData.Contains(patype.PA_FX_FAST) {
return false, krberror.NewErrorf(krberror.KRBMsgError, "KDC did not respond appropriately to FAST negotiation")
}
for _, pa := range k.DecryptedEncPart.EncPAData {
if pa.PADataType == patype.PA_REQ_ENC_PA_REP {
var pafast types.PAReqEncPARep
err := pafast.Unmarshal(pa.PADataValue)
if err != nil {
return false, krberror.Errorf(err, krberror.EncodingError, "KDC FAST negotiation response error, could not unmarshal PA_REQ_ENC_PA_REP")
}
etype, err := crypto.GetChksumEtype(pafast.ChksumType)
if err != nil {
return false, krberror.Errorf(err, krberror.ChksumError, "KDC FAST negotiation response error")
}
ab, _ := asReq.Marshal()
if !etype.VerifyChecksum(key.KeyValue, ab, pafast.Chksum, keyusage.KEY_USAGE_AS_REQ) {
return false, krberror.Errorf(err, krberror.ChksumError, "KDC FAST negotiation response checksum invalid")
}
}
}
}
return true, nil
}
// DecryptEncPart decrypts the encrypted part of an TGS_REP.
func (k *TGSRep) DecryptEncPart(key types.EncryptionKey) error {
b, err := crypto.DecryptEncPart(k.EncPart, key, keyusage.TGS_REP_ENCPART_SESSION_KEY)
if err != nil {
return krberror.Errorf(err, krberror.DecryptingError, "error decrypting TGS_REP EncPart")
}
var denc EncKDCRepPart
err = denc.Unmarshal(b)
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error unmarshaling encrypted part")
}
k.DecryptedEncPart = denc
return nil
}
// Verify checks the validity of the TGS_REP message.
func (k *TGSRep) Verify(cfg *config.Config, tgsReq TGSReq) (bool, error) {
if !k.CName.Equal(tgsReq.ReqBody.CName) {
return false, krberror.NewErrorf(krberror.KRBMsgError, "CName in response does not match what was requested. Requested: %+v; Reply: %+v", tgsReq.ReqBody.CName, k.CName)
}
if k.Ticket.Realm != tgsReq.ReqBody.Realm {
return false, krberror.NewErrorf(krberror.KRBMsgError, "realm in response ticket does not match what was requested. Requested: %s; Reply: %s", tgsReq.ReqBody.Realm, k.Ticket.Realm)
}
if k.DecryptedEncPart.Nonce != tgsReq.ReqBody.Nonce {
return false, krberror.NewErrorf(krberror.KRBMsgError, "possible replay attack, nonce in response does not match that in request")
}
//if k.Ticket.SName.NameType != tgsReq.ReqBody.SName.NameType || k.Ticket.SName.NameString == nil {
// return false, krberror.NewErrorf(krberror.KRBMsgError, "SName in response ticket does not match what was requested. Requested: %v; Reply: %v", tgsReq.ReqBody.SName, k.Ticket.SName)
//}
//for i := range k.Ticket.SName.NameString {
// if k.Ticket.SName.NameString[i] != tgsReq.ReqBody.SName.NameString[i] {
// return false, krberror.NewErrorf(krberror.KRBMsgError, "SName in response ticket does not match what was requested. Requested: %+v; Reply: %+v", tgsReq.ReqBody.SName, k.Ticket.SName)
// }
//}
//if k.DecryptedEncPart.SName.NameType != tgsReq.ReqBody.SName.NameType || k.DecryptedEncPart.SName.NameString == nil {
// return false, krberror.NewErrorf(krberror.KRBMsgError, "SName in response does not match what was requested. Requested: %v; Reply: %v", tgsReq.ReqBody.SName, k.DecryptedEncPart.SName)
//}
//for i := range k.DecryptedEncPart.SName.NameString {
// if k.DecryptedEncPart.SName.NameString[i] != tgsReq.ReqBody.SName.NameString[i] {
// return false, krberror.NewErrorf(krberror.KRBMsgError, "SName in response does not match what was requested. Requested: %+v; Reply: %+v", tgsReq.ReqBody.SName, k.DecryptedEncPart.SName)
// }
//}
if k.DecryptedEncPart.SRealm != tgsReq.ReqBody.Realm {
return false, krberror.NewErrorf(krberror.KRBMsgError, "SRealm in response does not match what was requested. Requested: %s; Reply: %s", tgsReq.ReqBody.Realm, k.DecryptedEncPart.SRealm)
}
if len(k.DecryptedEncPart.CAddr) > 0 {
if !types.HostAddressesEqual(k.DecryptedEncPart.CAddr, tgsReq.ReqBody.Addresses) {
return false, krberror.NewErrorf(krberror.KRBMsgError, "addresses listed in the TGS_REP does not match those listed in the TGS_REQ")
}
}
if time.Since(k.DecryptedEncPart.StartTime) > cfg.LibDefaults.Clockskew || k.DecryptedEncPart.StartTime.Sub(time.Now().UTC()) > cfg.LibDefaults.Clockskew {
if time.Since(k.DecryptedEncPart.AuthTime) > cfg.LibDefaults.Clockskew || k.DecryptedEncPart.AuthTime.Sub(time.Now().UTC()) > cfg.LibDefaults.Clockskew {
return false, krberror.NewErrorf(krberror.KRBMsgError, "clock skew with KDC too large. Greater than %v seconds.", cfg.LibDefaults.Clockskew.Seconds())
}
}
return true, nil
}
+432
View File
@@ -0,0 +1,432 @@
package messages
// Reference: https://www.ietf.org/rfc/rfc4120.txt
// Section: 5.4.1
import (
"crypto/rand"
"fmt"
"math"
"math/big"
"time"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/asn1tools"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/config"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/asnAppTag"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/flags"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/msgtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/nametype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/patype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
type marshalKDCReq struct {
PVNO int `asn1:"explicit,tag:1"`
MsgType int `asn1:"explicit,tag:2"`
PAData types.PADataSequence `asn1:"explicit,optional,tag:3"`
ReqBody asn1.RawValue `asn1:"explicit,tag:4"`
}
// KDCReqFields represents the KRB_KDC_REQ fields.
type KDCReqFields struct {
PVNO int
MsgType int
PAData types.PADataSequence
ReqBody KDCReqBody
Renewal bool
}
// ASReq implements RFC 4120 KRB_AS_REQ: https://tools.ietf.org/html/rfc4120#section-5.4.1.
type ASReq struct {
KDCReqFields
}
// TGSReq implements RFC 4120 KRB_TGS_REQ: https://tools.ietf.org/html/rfc4120#section-5.4.1.
type TGSReq struct {
KDCReqFields
}
type marshalKDCReqBody struct {
KDCOptions asn1.BitString `asn1:"explicit,tag:0"`
CName types.PrincipalName `asn1:"explicit,optional,tag:1"`
Realm string `asn1:"generalstring,explicit,tag:2"`
SName types.PrincipalName `asn1:"explicit,optional,tag:3"`
From time.Time `asn1:"generalized,explicit,optional,tag:4"`
Till time.Time `asn1:"generalized,explicit,tag:5"`
RTime time.Time `asn1:"generalized,explicit,optional,tag:6"`
Nonce int `asn1:"explicit,tag:7"`
EType []int32 `asn1:"explicit,tag:8"`
Addresses []types.HostAddress `asn1:"explicit,optional,tag:9"`
EncAuthData types.EncryptedData `asn1:"explicit,optional,tag:10"`
// Ticket needs to be a raw value as it is wrapped in an APPLICATION tag
AdditionalTickets asn1.RawValue `asn1:"explicit,optional,tag:11"`
}
// KDCReqBody implements the KRB_KDC_REQ request body.
type KDCReqBody struct {
KDCOptions asn1.BitString `asn1:"explicit,tag:0"`
CName types.PrincipalName `asn1:"explicit,optional,tag:1"`
Realm string `asn1:"generalstring,explicit,tag:2"`
SName types.PrincipalName `asn1:"explicit,optional,tag:3"`
From time.Time `asn1:"generalized,explicit,optional,tag:4"`
Till time.Time `asn1:"generalized,explicit,tag:5"`
RTime time.Time `asn1:"generalized,explicit,optional,tag:6"`
Nonce int `asn1:"explicit,tag:7"`
EType []int32 `asn1:"explicit,tag:8"`
Addresses []types.HostAddress `asn1:"explicit,optional,tag:9"`
EncAuthData types.EncryptedData `asn1:"explicit,optional,tag:10"`
AdditionalTickets []Ticket `asn1:"explicit,optional,tag:11"`
}
// NewASReqForTGT generates a new KRB_AS_REQ struct for a TGT request.
func NewASReqForTGT(realm string, c *config.Config, cname types.PrincipalName) (ASReq, error) {
sname := types.PrincipalName{
NameType: nametype.KRB_NT_SRV_INST,
NameString: []string{"krbtgt", realm},
}
return NewASReq(realm, c, cname, sname)
}
// NewASReqForChgPasswd generates a new KRB_AS_REQ struct for a change password request.
func NewASReqForChgPasswd(realm string, c *config.Config, cname types.PrincipalName) (ASReq, error) {
sname := types.PrincipalName{
NameType: nametype.KRB_NT_PRINCIPAL,
NameString: []string{"kadmin", "changepw"},
}
return NewASReq(realm, c, cname, sname)
}
// NewASReq generates a new KRB_AS_REQ struct for a given SNAME.
func NewASReq(realm string, c *config.Config, cname, sname types.PrincipalName) (ASReq, error) {
nonce, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt32))
if err != nil {
return ASReq{}, err
}
t := time.Now().UTC()
// Copy the default options to make this thread safe
kopts := types.NewKrbFlags()
copy(kopts.Bytes, c.LibDefaults.KDCDefaultOptions.Bytes)
kopts.BitLength = c.LibDefaults.KDCDefaultOptions.BitLength
a := ASReq{
KDCReqFields{
PVNO: iana.PVNO,
MsgType: msgtype.KRB_AS_REQ,
PAData: types.PADataSequence{},
ReqBody: KDCReqBody{
KDCOptions: kopts,
Realm: realm,
CName: cname,
SName: sname,
Till: t.Add(c.LibDefaults.TicketLifetime),
Nonce: int(nonce.Int64()),
EType: c.LibDefaults.DefaultTktEnctypeIDs,
},
},
}
if c.LibDefaults.Forwardable {
types.SetFlag(&a.ReqBody.KDCOptions, flags.Forwardable)
}
if c.LibDefaults.Canonicalize {
types.SetFlag(&a.ReqBody.KDCOptions, flags.Canonicalize)
}
if c.LibDefaults.Proxiable {
types.SetFlag(&a.ReqBody.KDCOptions, flags.Proxiable)
}
if c.LibDefaults.RenewLifetime != 0 {
types.SetFlag(&a.ReqBody.KDCOptions, flags.Renewable)
a.ReqBody.RTime = t.Add(c.LibDefaults.RenewLifetime)
a.ReqBody.RTime = t.Add(time.Duration(48) * time.Hour)
}
if !c.LibDefaults.NoAddresses {
ha, err := types.LocalHostAddresses()
if err != nil {
return a, fmt.Errorf("could not get local addresses: %v", err)
}
ha = append(ha, types.HostAddressesFromNetIPs(c.LibDefaults.ExtraAddresses)...)
a.ReqBody.Addresses = ha
}
return a, nil
}
// NewTGSReq generates a new KRB_TGS_REQ struct.
func NewTGSReq(cname types.PrincipalName, kdcRealm string, c *config.Config, tgt Ticket, sessionKey types.EncryptionKey, sname types.PrincipalName, renewal bool) (TGSReq, error) {
a, err := tgsReq(cname, sname, kdcRealm, renewal, c)
if err != nil {
return a, err
}
err = a.setPAData(tgt, sessionKey)
return a, err
}
// NewUser2UserTGSReq returns a TGS-REQ suitable for user-to-user authentication (https://tools.ietf.org/html/rfc4120#section-3.7)
func NewUser2UserTGSReq(cname types.PrincipalName, kdcRealm string, c *config.Config, clientTGT Ticket, sessionKey types.EncryptionKey, sname types.PrincipalName, renewal bool, verifyingTGT Ticket) (TGSReq, error) {
a, err := tgsReq(cname, sname, kdcRealm, renewal, c)
if err != nil {
return a, err
}
a.ReqBody.AdditionalTickets = []Ticket{verifyingTGT}
types.SetFlag(&a.ReqBody.KDCOptions, flags.EncTktInSkey)
err = a.setPAData(clientTGT, sessionKey)
return a, err
}
// tgsReq populates the fields for a TGS_REQ
func tgsReq(cname, sname types.PrincipalName, kdcRealm string, renewal bool, c *config.Config) (TGSReq, error) {
nonce, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt32))
if err != nil {
return TGSReq{}, err
}
t := time.Now().UTC()
k := KDCReqFields{
PVNO: iana.PVNO,
MsgType: msgtype.KRB_TGS_REQ,
ReqBody: KDCReqBody{
KDCOptions: types.NewKrbFlags(),
Realm: kdcRealm,
CName: cname, // Add the CName to make validation of the reply easier
SName: sname,
Till: t.Add(c.LibDefaults.TicketLifetime),
Nonce: int(nonce.Int64()),
EType: c.LibDefaults.DefaultTGSEnctypeIDs,
},
Renewal: renewal,
}
if c.LibDefaults.Forwardable {
types.SetFlag(&k.ReqBody.KDCOptions, flags.Forwardable)
}
if c.LibDefaults.Canonicalize {
types.SetFlag(&k.ReqBody.KDCOptions, flags.Canonicalize)
}
if c.LibDefaults.Proxiable {
types.SetFlag(&k.ReqBody.KDCOptions, flags.Proxiable)
}
if c.LibDefaults.RenewLifetime > time.Duration(0) {
types.SetFlag(&k.ReqBody.KDCOptions, flags.Renewable)
k.ReqBody.RTime = t.Add(c.LibDefaults.RenewLifetime)
}
if !c.LibDefaults.NoAddresses {
ha, err := types.LocalHostAddresses()
if err != nil {
return TGSReq{}, fmt.Errorf("could not get local addresses: %v", err)
}
ha = append(ha, types.HostAddressesFromNetIPs(c.LibDefaults.ExtraAddresses)...)
k.ReqBody.Addresses = ha
}
if renewal {
types.SetFlag(&k.ReqBody.KDCOptions, flags.Renew)
types.SetFlag(&k.ReqBody.KDCOptions, flags.Renewable)
}
return TGSReq{
k,
}, nil
}
func (k *TGSReq) setPAData(tgt Ticket, sessionKey types.EncryptionKey) error {
// Marshal the request and calculate checksum
b, err := k.ReqBody.Marshal()
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error marshaling TGS_REQ body")
}
etype, err := crypto.GetEtype(sessionKey.KeyType)
if err != nil {
return krberror.Errorf(err, krberror.EncryptingError, "error getting etype to encrypt authenticator")
}
cb, err := etype.GetChecksumHash(sessionKey.KeyValue, b, keyusage.TGS_REQ_PA_TGS_REQ_AP_REQ_AUTHENTICATOR_CHKSUM)
if err != nil {
return krberror.Errorf(err, krberror.ChksumError, "error getting etype checksum hash")
}
// Form PAData for TGS_REQ
// Create authenticator
auth, err := types.NewAuthenticator(tgt.Realm, k.ReqBody.CName)
if err != nil {
return krberror.Errorf(err, krberror.KRBMsgError, "error generating new authenticator")
}
auth.Cksum = types.Checksum{
CksumType: etype.GetHashID(),
Checksum: cb,
}
// Create AP_REQ
apReq, err := NewAPReq(tgt, sessionKey, auth)
if err != nil {
return krberror.Errorf(err, krberror.KRBMsgError, "error generating new AP_REQ")
}
apb, err := apReq.Marshal()
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error marshaling AP_REQ for pre-authentication data")
}
k.PAData = types.PADataSequence{
types.PAData{
PADataType: patype.PA_TGS_REQ,
PADataValue: apb,
},
}
return nil
}
// Unmarshal bytes b into the ASReq struct.
func (k *ASReq) Unmarshal(b []byte) error {
var m marshalKDCReq
_, err := asn1.UnmarshalWithParams(b, &m, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.ASREQ))
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error unmarshaling AS_REQ")
}
expectedMsgType := msgtype.KRB_AS_REQ
if m.MsgType != expectedMsgType {
return krberror.NewErrorf(krberror.KRBMsgError, "message ID does not indicate a AS_REQ. Expected: %v; Actual: %v", expectedMsgType, m.MsgType)
}
var reqb KDCReqBody
err = reqb.Unmarshal(m.ReqBody.Bytes)
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error processing AS_REQ body")
}
k.MsgType = m.MsgType
k.PAData = m.PAData
k.PVNO = m.PVNO
k.ReqBody = reqb
return nil
}
// Unmarshal bytes b into the TGSReq struct.
func (k *TGSReq) Unmarshal(b []byte) error {
var m marshalKDCReq
_, err := asn1.UnmarshalWithParams(b, &m, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.TGSREQ))
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error unmarshaling TGS_REQ")
}
expectedMsgType := msgtype.KRB_TGS_REQ
if m.MsgType != expectedMsgType {
return krberror.NewErrorf(krberror.KRBMsgError, "message ID does not indicate a TGS_REQ. Expected: %v; Actual: %v", expectedMsgType, m.MsgType)
}
var reqb KDCReqBody
err = reqb.Unmarshal(m.ReqBody.Bytes)
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error processing TGS_REQ body")
}
k.MsgType = m.MsgType
k.PAData = m.PAData
k.PVNO = m.PVNO
k.ReqBody = reqb
return nil
}
// Unmarshal bytes b into the KRB_KDC_REQ body struct.
func (k *KDCReqBody) Unmarshal(b []byte) error {
var m marshalKDCReqBody
_, err := asn1.Unmarshal(b, &m)
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error unmarshaling KDC_REQ body")
}
k.KDCOptions = m.KDCOptions
if len(k.KDCOptions.Bytes) < 4 {
tb := make([]byte, 4-len(k.KDCOptions.Bytes))
k.KDCOptions.Bytes = append(tb, k.KDCOptions.Bytes...)
k.KDCOptions.BitLength = len(k.KDCOptions.Bytes) * 8
}
k.CName = m.CName
k.Realm = m.Realm
k.SName = m.SName
k.From = m.From
k.Till = m.Till
k.RTime = m.RTime
k.Nonce = m.Nonce
k.EType = m.EType
k.Addresses = m.Addresses
k.EncAuthData = m.EncAuthData
if len(m.AdditionalTickets.Bytes) > 0 {
k.AdditionalTickets, err = unmarshalTicketsSequence(m.AdditionalTickets)
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error unmarshaling additional tickets")
}
}
return nil
}
// Marshal ASReq struct.
func (k *ASReq) Marshal() ([]byte, error) {
m := marshalKDCReq{
PVNO: k.PVNO,
MsgType: k.MsgType,
PAData: k.PAData,
}
b, err := k.ReqBody.Marshal()
if err != nil {
var mk []byte
return mk, err
}
m.ReqBody = asn1.RawValue{
Class: asn1.ClassContextSpecific,
IsCompound: true,
Tag: 4,
Bytes: b,
}
mk, err := asn1.Marshal(m)
if err != nil {
return mk, krberror.Errorf(err, krberror.EncodingError, "error marshaling AS_REQ")
}
mk = asn1tools.AddASNAppTag(mk, asnAppTag.ASREQ)
return mk, nil
}
// Marshal TGSReq struct.
func (k *TGSReq) Marshal() ([]byte, error) {
m := marshalKDCReq{
PVNO: k.PVNO,
MsgType: k.MsgType,
PAData: k.PAData,
}
b, err := k.ReqBody.Marshal()
if err != nil {
var mk []byte
return mk, err
}
m.ReqBody = asn1.RawValue{
Class: asn1.ClassContextSpecific,
IsCompound: true,
Tag: 4,
Bytes: b,
}
mk, err := asn1.Marshal(m)
if err != nil {
return mk, krberror.Errorf(err, krberror.EncodingError, "error marshaling AS_REQ")
}
mk = asn1tools.AddASNAppTag(mk, asnAppTag.TGSREQ)
return mk, nil
}
// Marshal KRB_KDC_REQ body struct.
func (k *KDCReqBody) Marshal() ([]byte, error) {
var b []byte
m := marshalKDCReqBody{
KDCOptions: k.KDCOptions,
CName: k.CName,
Realm: k.Realm,
SName: k.SName,
From: k.From,
Till: k.Till,
RTime: k.RTime,
Nonce: k.Nonce,
EType: k.EType,
Addresses: k.Addresses,
EncAuthData: k.EncAuthData,
}
rawtkts, err := MarshalTicketSequence(k.AdditionalTickets)
if err != nil {
return b, krberror.Errorf(err, krberror.EncodingError, "error in marshaling KDC request body additional tickets")
}
//The asn1.rawValue needs the tag setting on it for where it is in the KDCReqBody
rawtkts.Tag = 11
if len(rawtkts.Bytes) > 0 {
m.AdditionalTickets = rawtkts
}
b, err = asn1.Marshal(m)
if err != nil {
return b, krberror.Errorf(err, krberror.EncodingError, "error in marshaling KDC request body")
}
return b, nil
}
+102
View File
@@ -0,0 +1,102 @@
package messages
import (
"fmt"
"time"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/asnAppTag"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/msgtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
type marshalKRBCred struct {
PVNO int `asn1:"explicit,tag:0"`
MsgType int `asn1:"explicit,tag:1"`
Tickets asn1.RawValue `asn1:"explicit,tag:2"`
EncPart types.EncryptedData `asn1:"explicit,tag:3"`
}
// KRBCred implements RFC 4120 KRB_CRED: https://tools.ietf.org/html/rfc4120#section-5.8.1.
type KRBCred struct {
PVNO int
MsgType int
Tickets []Ticket
EncPart types.EncryptedData
DecryptedEncPart EncKrbCredPart
}
// EncKrbCredPart is the encrypted part of KRB_CRED.
type EncKrbCredPart struct {
TicketInfo []KrbCredInfo `asn1:"explicit,tag:0"`
Nouce int `asn1:"optional,explicit,tag:1"`
Timestamp time.Time `asn1:"generalized,optional,explicit,tag:2"`
Usec int `asn1:"optional,explicit,tag:3"`
SAddress types.HostAddress `asn1:"optional,explicit,tag:4"`
RAddress types.HostAddress `asn1:"optional,explicit,tag:5"`
}
// KrbCredInfo is the KRB_CRED_INFO part of KRB_CRED.
type KrbCredInfo struct {
Key types.EncryptionKey `asn1:"explicit,tag:0"`
PRealm string `asn1:"generalstring,optional,explicit,tag:1"`
PName types.PrincipalName `asn1:"optional,explicit,tag:2"`
Flags asn1.BitString `asn1:"optional,explicit,tag:3"`
AuthTime time.Time `asn1:"generalized,optional,explicit,tag:4"`
StartTime time.Time `asn1:"generalized,optional,explicit,tag:5"`
EndTime time.Time `asn1:"generalized,optional,explicit,tag:6"`
RenewTill time.Time `asn1:"generalized,optional,explicit,tag:7"`
SRealm string `asn1:"optional,explicit,ia5,tag:8"`
SName types.PrincipalName `asn1:"optional,explicit,tag:9"`
CAddr types.HostAddresses `asn1:"optional,explicit,tag:10"`
}
// Unmarshal bytes b into the KRBCred struct.
func (k *KRBCred) Unmarshal(b []byte) error {
var m marshalKRBCred
_, err := asn1.UnmarshalWithParams(b, &m, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.KRBCred))
if err != nil {
return processUnmarshalReplyError(b, err)
}
expectedMsgType := msgtype.KRB_CRED
if m.MsgType != expectedMsgType {
return krberror.NewErrorf(krberror.KRBMsgError, "message ID does not indicate a KRB_CRED. Expected: %v; Actual: %v", expectedMsgType, m.MsgType)
}
k.PVNO = m.PVNO
k.MsgType = m.MsgType
k.EncPart = m.EncPart
if len(m.Tickets.Bytes) > 0 {
k.Tickets, err = unmarshalTicketsSequence(m.Tickets)
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error unmarshaling tickets within KRB_CRED")
}
}
return nil
}
// DecryptEncPart decrypts the encrypted part of a KRB_CRED.
func (k *KRBCred) DecryptEncPart(key types.EncryptionKey) error {
b, err := crypto.DecryptEncPart(k.EncPart, key, keyusage.KRB_CRED_ENCPART)
if err != nil {
return krberror.Errorf(err, krberror.DecryptingError, "error decrypting KRB_CRED EncPart")
}
var denc EncKrbCredPart
err = denc.Unmarshal(b)
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error unmarshaling encrypted part of KRB_CRED")
}
k.DecryptedEncPart = denc
return nil
}
// Unmarshal bytes b into the encrypted part of KRB_CRED.
func (k *EncKrbCredPart) Unmarshal(b []byte) error {
_, err := asn1.UnmarshalWithParams(b, k, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.EncKrbCredPart))
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "error unmarshaling EncKrbCredPart")
}
return nil
}
+94
View File
@@ -0,0 +1,94 @@
// Package messages implements Kerberos 5 message types and methods.
package messages
import (
"fmt"
"time"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/asn1tools"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/asnAppTag"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/errorcode"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/msgtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// KRBError implements RFC 4120 KRB_ERROR: https://tools.ietf.org/html/rfc4120#section-5.9.1.
type KRBError struct {
PVNO int `asn1:"explicit,tag:0"`
MsgType int `asn1:"explicit,tag:1"`
CTime time.Time `asn1:"generalized,optional,explicit,tag:2"`
Cusec int `asn1:"optional,explicit,tag:3"`
STime time.Time `asn1:"generalized,explicit,tag:4"`
Susec int `asn1:"explicit,tag:5"`
ErrorCode int32 `asn1:"explicit,tag:6"`
CRealm string `asn1:"generalstring,optional,explicit,tag:7"`
CName types.PrincipalName `asn1:"optional,explicit,tag:8"`
Realm string `asn1:"generalstring,explicit,tag:9"`
SName types.PrincipalName `asn1:"explicit,tag:10"`
EText string `asn1:"generalstring,optional,explicit,tag:11"`
EData []byte `asn1:"optional,explicit,tag:12"`
}
// NewKRBError creates a new KRBError.
func NewKRBError(sname types.PrincipalName, realm string, code int32, etext string) KRBError {
t := time.Now().UTC()
return KRBError{
PVNO: iana.PVNO,
MsgType: msgtype.KRB_ERROR,
STime: t,
Susec: int((t.UnixNano() / int64(time.Microsecond)) - (t.Unix() * 1e6)),
ErrorCode: code,
SName: sname,
Realm: realm,
EText: etext,
}
}
// Unmarshal bytes b into the KRBError struct.
func (k *KRBError) Unmarshal(b []byte) error {
_, err := asn1.UnmarshalWithParams(b, k, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.KRBError))
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "KRB_ERROR unmarshal error")
}
expectedMsgType := msgtype.KRB_ERROR
if k.MsgType != expectedMsgType {
return krberror.NewErrorf(krberror.KRBMsgError, "message ID does not indicate a KRB_ERROR. Expected: %v; Actual: %v", expectedMsgType, k.MsgType)
}
return nil
}
// Marshal a KRBError into bytes.
func (k *KRBError) Marshal() ([]byte, error) {
b, err := asn1.Marshal(*k)
if err != nil {
return b, krberror.Errorf(err, krberror.EncodingError, "error marshaling KRBError")
}
b = asn1tools.AddASNAppTag(b, asnAppTag.KRBError)
return b, nil
}
// Error method implementing error interface on KRBError struct.
func (k KRBError) Error() string {
etxt := fmt.Sprintf("KRB Error: %s", errorcode.Lookup(k.ErrorCode))
if k.EText != "" {
etxt = fmt.Sprintf("%s - %s", etxt, k.EText)
}
return etxt
}
func processUnmarshalReplyError(b []byte, err error) error {
switch err.(type) {
case asn1.StructuralError:
var krberr KRBError
tmperr := krberr.Unmarshal(b)
if tmperr != nil {
return krberror.Errorf(err, krberror.EncodingError, "failed to unmarshal KDC's reply")
}
return krberr
default:
return krberror.Errorf(err, krberror.EncodingError, "failed to unmarshal KDC's reply")
}
}
+108
View File
@@ -0,0 +1,108 @@
package messages
import (
"fmt"
"time"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/asn1tools"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/asnAppTag"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/msgtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// KRBPriv implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.7.1.
type KRBPriv struct {
PVNO int `asn1:"explicit,tag:0"`
MsgType int `asn1:"explicit,tag:1"`
EncPart types.EncryptedData `asn1:"explicit,tag:3"`
DecryptedEncPart EncKrbPrivPart `asn1:"optional,omitempty"` // Not part of ASN1 bytes so marked as optional so unmarshalling works
}
// EncKrbPrivPart is the encrypted part of KRB_PRIV.
type EncKrbPrivPart struct {
UserData []byte `asn1:"explicit,tag:0"`
Timestamp time.Time `asn1:"generalized,optional,explicit,tag:1"`
Usec int `asn1:"optional,explicit,tag:2"`
SequenceNumber int64 `asn1:"optional,explicit,tag:3"`
SAddress types.HostAddress `asn1:"explicit,tag:4"`
RAddress types.HostAddress `asn1:"optional,explicit,tag:5"`
}
// NewKRBPriv returns a new KRBPriv type.
func NewKRBPriv(part EncKrbPrivPart) KRBPriv {
return KRBPriv{
PVNO: iana.PVNO,
MsgType: msgtype.KRB_PRIV,
DecryptedEncPart: part,
}
}
// Unmarshal bytes b into the KRBPriv struct.
func (k *KRBPriv) Unmarshal(b []byte) error {
_, err := asn1.UnmarshalWithParams(b, k, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.KRBPriv))
if err != nil {
return processUnmarshalReplyError(b, err)
}
expectedMsgType := msgtype.KRB_PRIV
if k.MsgType != expectedMsgType {
return krberror.NewErrorf(krberror.KRBMsgError, "message ID does not indicate a KRB_PRIV. Expected: %v; Actual: %v", expectedMsgType, k.MsgType)
}
return nil
}
// Unmarshal bytes b into the EncKrbPrivPart struct.
func (k *EncKrbPrivPart) Unmarshal(b []byte) error {
_, err := asn1.UnmarshalWithParams(b, k, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.EncKrbPrivPart))
if err != nil {
return krberror.Errorf(err, krberror.EncodingError, "KRB_PRIV unmarshal error")
}
return nil
}
// Marshal the KRBPriv.
func (k *KRBPriv) Marshal() ([]byte, error) {
tk := KRBPriv{
PVNO: k.PVNO,
MsgType: k.MsgType,
EncPart: k.EncPart,
}
b, err := asn1.Marshal(tk)
if err != nil {
return []byte{}, err
}
b = asn1tools.AddASNAppTag(b, asnAppTag.KRBPriv)
return b, nil
}
// EncryptEncPart encrypts the DecryptedEncPart within the KRBPriv.
// Use to prepare for marshaling.
func (k *KRBPriv) EncryptEncPart(key types.EncryptionKey) error {
b, err := asn1.Marshal(k.DecryptedEncPart)
if err != nil {
return err
}
b = asn1tools.AddASNAppTag(b, asnAppTag.EncKrbPrivPart)
k.EncPart, err = crypto.GetEncryptedData(b, key, keyusage.KRB_PRIV_ENCPART, 1)
if err != nil {
return err
}
return nil
}
// DecryptEncPart decrypts the encrypted part of the KRBPriv message.
func (k *KRBPriv) DecryptEncPart(key types.EncryptionKey) error {
b, err := crypto.DecryptEncPart(k.EncPart, key, keyusage.KRB_PRIV_ENCPART)
if err != nil {
return fmt.Errorf("error decrypting KRBPriv EncPart: %v", err)
}
err = k.DecryptedEncPart.Unmarshal(b)
if err != nil {
return fmt.Errorf("error unmarshaling encrypted part: %v", err)
}
return nil
}
+43
View File
@@ -0,0 +1,43 @@
package messages
import (
"fmt"
"time"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/asnAppTag"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/msgtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// KRBSafe implements RFC 4120 KRB_SAFE: https://tools.ietf.org/html/rfc4120#section-5.6.1.
type KRBSafe struct {
PVNO int `asn1:"explicit,tag:0"`
MsgType int `asn1:"explicit,tag:1"`
SafeBody KRBSafeBody `asn1:"explicit,tag:2"`
Cksum types.Checksum `asn1:"explicit,tag:3"`
}
// KRBSafeBody implements the KRB_SAFE_BODY of KRB_SAFE.
type KRBSafeBody struct {
UserData []byte `asn1:"explicit,tag:0"`
Timestamp time.Time `asn1:"generalized,optional,explicit,tag:1"`
Usec int `asn1:"optional,explicit,tag:2"`
SequenceNumber int64 `asn1:"optional,explicit,tag:3"`
SAddress types.HostAddress `asn1:"explicit,tag:4"`
RAddress types.HostAddress `asn1:"optional,explicit,tag:5"`
}
// Unmarshal bytes b into the KRBSafe struct.
func (s *KRBSafe) Unmarshal(b []byte) error {
_, err := asn1.UnmarshalWithParams(b, s, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.KRBSafe))
if err != nil {
return processUnmarshalReplyError(b, err)
}
expectedMsgType := msgtype.KRB_SAFE
if s.MsgType != expectedMsgType {
return krberror.NewErrorf(krberror.KRBMsgError, "message ID does not indicate a KRB_SAFE. Expected: %v; Actual: %v", expectedMsgType, s.MsgType)
}
return nil
}
+262
View File
@@ -0,0 +1,262 @@
package messages
import (
"fmt"
"log"
"time"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/asn1tools"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/adtype"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/asnAppTag"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/errorcode"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/flags"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/keytab"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/krberror"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/pac"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
)
// Reference: https://www.ietf.org/rfc/rfc4120.txt
// Section: 5.3
// Ticket implements the Kerberos ticket.
type Ticket struct {
TktVNO int `asn1:"explicit,tag:0"`
Realm string `asn1:"generalstring,explicit,tag:1"`
SName types.PrincipalName `asn1:"explicit,tag:2"`
EncPart types.EncryptedData `asn1:"explicit,tag:3"`
DecryptedEncPart EncTicketPart `asn1:"optional"` // Not part of ASN1 bytes so marked as optional so unmarshalling works
}
// EncTicketPart is the encrypted part of the Ticket.
type EncTicketPart struct {
Flags asn1.BitString `asn1:"explicit,tag:0"`
Key types.EncryptionKey `asn1:"explicit,tag:1"`
CRealm string `asn1:"generalstring,explicit,tag:2"`
CName types.PrincipalName `asn1:"explicit,tag:3"`
Transited TransitedEncoding `asn1:"explicit,tag:4"`
AuthTime time.Time `asn1:"generalized,explicit,tag:5"`
StartTime time.Time `asn1:"generalized,explicit,optional,tag:6"`
EndTime time.Time `asn1:"generalized,explicit,tag:7"`
RenewTill time.Time `asn1:"generalized,explicit,optional,tag:8"`
CAddr types.HostAddresses `asn1:"explicit,optional,tag:9"`
AuthorizationData types.AuthorizationData `asn1:"explicit,optional,tag:10"`
}
// TransitedEncoding part of the ticket's encrypted part.
type TransitedEncoding struct {
TRType int32 `asn1:"explicit,tag:0"`
Contents []byte `asn1:"explicit,tag:1"`
}
// NewTicket creates a new Ticket instance.
func NewTicket(cname types.PrincipalName, crealm string, sname types.PrincipalName, srealm string, flags asn1.BitString, sktab *keytab.Keytab, eTypeID int32, kvno int, authTime, startTime, endTime, renewTill time.Time) (Ticket, types.EncryptionKey, error) {
etype, err := crypto.GetEtype(eTypeID)
if err != nil {
return Ticket{}, types.EncryptionKey{}, krberror.Errorf(err, krberror.EncryptingError, "error getting etype for new ticket")
}
sessionKey, err := types.GenerateEncryptionKey(etype)
if err != nil {
return Ticket{}, types.EncryptionKey{}, krberror.Errorf(err, krberror.EncryptingError, "error generating session key")
}
etp := EncTicketPart{
Flags: flags,
Key: sessionKey,
CRealm: crealm,
CName: cname,
Transited: TransitedEncoding{},
AuthTime: authTime,
StartTime: startTime,
EndTime: endTime,
RenewTill: renewTill,
}
b, err := asn1.Marshal(etp)
if err != nil {
return Ticket{}, types.EncryptionKey{}, krberror.Errorf(err, krberror.EncodingError, "error marshalling ticket encpart")
}
b = asn1tools.AddASNAppTag(b, asnAppTag.EncTicketPart)
skey, _, err := sktab.GetEncryptionKey(sname, srealm, kvno, eTypeID)
if err != nil {
return Ticket{}, types.EncryptionKey{}, krberror.Errorf(err, krberror.EncryptingError, "error getting encryption key for new ticket")
}
ed, err := crypto.GetEncryptedData(b, skey, keyusage.KDC_REP_TICKET, kvno)
if err != nil {
return Ticket{}, types.EncryptionKey{}, krberror.Errorf(err, krberror.EncryptingError, "error encrypting ticket encpart")
}
tkt := Ticket{
TktVNO: iana.PVNO,
Realm: srealm,
SName: sname,
EncPart: ed,
}
return tkt, sessionKey, nil
}
// Unmarshal bytes b into a Ticket struct.
func (t *Ticket) Unmarshal(b []byte) error {
_, err := asn1.UnmarshalWithParams(b, t, fmt.Sprintf("application,explicit,tag:%d", asnAppTag.Ticket))
return err
}
// Marshal the Ticket.
func (t *Ticket) Marshal() ([]byte, error) {
b, err := asn1.Marshal(*t)
if err != nil {
return nil, err
}
b = asn1tools.AddASNAppTag(b, asnAppTag.Ticket)
return b, nil
}
// Unmarshal bytes b into the EncTicketPart struct.
func (t *EncTicketPart) Unmarshal(b []byte) error {
_, err := asn1.UnmarshalWithParams(b, t, fmt.Sprintf("application,explicit,tag:%d", asnAppTag.EncTicketPart))
return err
}
// unmarshalTicket returns a ticket from the bytes provided.
func unmarshalTicket(b []byte) (t Ticket, err error) {
err = t.Unmarshal(b)
return
}
// UnmarshalTicketsSequence returns a slice of Tickets from a raw ASN1 value.
func unmarshalTicketsSequence(in asn1.RawValue) ([]Ticket, error) {
//This is a workaround to a asn1 decoding issue in golang - https://github.com/golang/go/issues/17321. It's not pretty I'm afraid
//We pull out raw values from the larger raw value (that is actually the data of the sequence of raw values) and track our position moving along the data.
b := in.Bytes
// Ignore the head of the asn1 stream (1 byte for tag and those for the length) as this is what tells us its a sequence but we're handling it ourselves
p := 1 + asn1tools.GetNumberBytesInLengthHeader(in.Bytes)
var tkts []Ticket
var raw asn1.RawValue
for p < (len(b)) {
_, err := asn1.UnmarshalWithParams(b[p:], &raw, fmt.Sprintf("application,tag:%d", asnAppTag.Ticket))
if err != nil {
return nil, fmt.Errorf("unmarshaling sequence of tickets failed getting length of ticket: %v", err)
}
t, err := unmarshalTicket(b[p:])
if err != nil {
return nil, fmt.Errorf("unmarshaling sequence of tickets failed: %v", err)
}
p += len(raw.FullBytes)
tkts = append(tkts, t)
}
MarshalTicketSequence(tkts)
return tkts, nil
}
// MarshalTicketSequence marshals a slice of Tickets returning an ASN1 raw value containing the ticket sequence.
func MarshalTicketSequence(tkts []Ticket) (asn1.RawValue, error) {
raw := asn1.RawValue{
Class: 2,
IsCompound: true,
}
if len(tkts) < 1 {
// There are no tickets to marshal
return raw, nil
}
var btkts []byte
for i, t := range tkts {
b, err := t.Marshal()
if err != nil {
return raw, fmt.Errorf("error marshaling ticket number %d in sequence of tickets", i+1)
}
btkts = append(btkts, b...)
}
// The ASN1 wrapping consists of 2 bytes:
// 1st byte -> Identifier Octet - In this case an OCTET STRING (ASN TAG
// 2nd byte -> The length (this will be the size indicated in the input bytes + 2 for the additional bytes we add here.
// Application Tag:
//| Byte: | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
//| Value: | 0 | 1 | 1 | From the RFC spec 4120 |
//| Explanation | Defined by the ASN1 encoding rules for an application tag | A value of 1 indicates a constructed type | The ASN Application tag value |
btkts = append(asn1tools.MarshalLengthBytes(len(btkts)), btkts...)
btkts = append([]byte{byte(32 + asn1.TagSequence)}, btkts...)
raw.Bytes = btkts
// If we need to create the full bytes then identifier octet is "context-specific" = 128 + "constructed" + 32 + the wrapping explicit tag (11)
//fmt.Fprintf(os.Stderr, "mRaw fb: %v\n", raw.FullBytes)
return raw, nil
}
// DecryptEncPart decrypts the encrypted part of the ticket.
// The sname argument can be used to specify which service principal's key should be used to decrypt the ticket.
// If nil is passed as the sname then the service principal specified within the ticket it used.
func (t *Ticket) DecryptEncPart(keytab *keytab.Keytab, sname *types.PrincipalName) error {
if sname == nil {
sname = &t.SName
}
key, _, err := keytab.GetEncryptionKey(*sname, t.Realm, t.EncPart.KVNO, t.EncPart.EType)
if err != nil {
return NewKRBError(t.SName, t.Realm, errorcode.KRB_AP_ERR_NOKEY, fmt.Sprintf("Could not get key from keytab: %v", err))
}
return t.Decrypt(key)
}
// Decrypt decrypts the encrypted part of the ticket using the key provided.
func (t *Ticket) Decrypt(key types.EncryptionKey) error {
b, err := crypto.DecryptEncPart(t.EncPart, key, keyusage.KDC_REP_TICKET)
if err != nil {
return fmt.Errorf("error decrypting Ticket EncPart: %v", err)
}
var denc EncTicketPart
err = denc.Unmarshal(b)
if err != nil {
return fmt.Errorf("error unmarshaling encrypted part: %v", err)
}
t.DecryptedEncPart = denc
return nil
}
// GetPACType returns a Microsoft PAC that has been extracted from the ticket and processed.
func (t *Ticket) GetPACType(keytab *keytab.Keytab, sname *types.PrincipalName, l *log.Logger) (bool, pac.PACType, error) {
var isPAC bool
for _, ad := range t.DecryptedEncPart.AuthorizationData {
if ad.ADType == adtype.ADIfRelevant {
var ad2 types.AuthorizationData
err := ad2.Unmarshal(ad.ADData)
if err != nil {
l.Printf("PAC authorization data could not be unmarshaled: %v", err)
continue
}
if ad2[0].ADType == adtype.ADWin2KPAC {
isPAC = true
var p pac.PACType
err = p.Unmarshal(ad2[0].ADData)
if err != nil {
return isPAC, p, fmt.Errorf("error unmarshaling PAC: %v", err)
}
if sname == nil {
sname = &t.SName
}
key, _, err := keytab.GetEncryptionKey(*sname, t.Realm, t.EncPart.KVNO, t.EncPart.EType)
if err != nil {
return isPAC, p, NewKRBError(t.SName, t.Realm, errorcode.KRB_AP_ERR_NOKEY, fmt.Sprintf("Could not get key from keytab: %v", err))
}
err = p.ProcessPACInfoBuffers(key, l)
return isPAC, p, err
}
}
}
return isPAC, pac.PACType{}, nil
}
// Valid checks it the ticket is currently valid. Max duration passed endtime passed in as argument.
func (t *Ticket) Valid(d time.Duration) (bool, error) {
// Check for future tickets or invalid tickets
time := time.Now().UTC()
if t.DecryptedEncPart.StartTime.Sub(time) > d || types.IsFlagSet(&t.DecryptedEncPart.Flags, flags.Invalid) {
return false, NewKRBError(t.SName, t.Realm, errorcode.KRB_AP_ERR_TKT_NYV, "service ticket provided is not yet valid")
}
// Check for expired ticket
if time.Sub(t.DecryptedEncPart.EndTime) > d {
return false, NewKRBError(t.SName, t.Realm, errorcode.KRB_AP_ERR_TKT_EXPIRED, "service ticket provided has expired")
}
return true, nil
}
+34
View File
@@ -0,0 +1,34 @@
package pac
import (
"bytes"
"fmt"
"github.com/jcmturner/rpc/v2/mstypes"
"github.com/jcmturner/rpc/v2/ndr"
)
// Claims reference: https://msdn.microsoft.com/en-us/library/hh553895.aspx
// ClientClaimsInfo implements https://msdn.microsoft.com/en-us/library/hh536365.aspx
type ClientClaimsInfo struct {
ClaimsSetMetadata mstypes.ClaimsSetMetadata
ClaimsSet mstypes.ClaimsSet
}
// Unmarshal bytes into the ClientClaimsInfo struct
func (k *ClientClaimsInfo) Unmarshal(b []byte) (err error) {
dec := ndr.NewDecoder(bytes.NewReader(b))
m := new(mstypes.ClaimsSetMetadata)
err = dec.Decode(m)
if err != nil {
err = fmt.Errorf("error unmarshaling ClientClaimsInfo ClaimsSetMetadata: %v", err)
return
}
k.ClaimsSetMetadata = *m
k.ClaimsSet, err = k.ClaimsSetMetadata.ClaimsSet()
if err != nil {
err = fmt.Errorf("error unmarshaling ClientClaimsInfo ClaimsSet: %v", err)
}
return
}
+31
View File
@@ -0,0 +1,31 @@
package pac
import (
"bytes"
"github.com/jcmturner/rpc/v2/mstypes"
)
// ClientInfo implements https://msdn.microsoft.com/en-us/library/cc237951.aspx
type ClientInfo struct {
ClientID mstypes.FileTime // A FILETIME structure in little-endian format that contains the Kerberos initial ticket-granting ticket TGT authentication time
NameLength uint16 // An unsigned 16-bit integer in little-endian format that specifies the length, in bytes, of the Name field.
Name string // An array of 16-bit Unicode characters in little-endian format that contains the client's account name.
}
// Unmarshal bytes into the ClientInfo struct
func (k *ClientInfo) Unmarshal(b []byte) (err error) {
//The PAC_CLIENT_INFO structure is a simple structure that is not NDR-encoded.
r := mstypes.NewReader(bytes.NewReader(b))
k.ClientID, err = r.FileTime()
if err != nil {
return
}
k.NameLength, err = r.Uint16()
if err != nil {
return
}
k.Name, err = r.UTF16String(int(k.NameLength))
return
}
+86
View File
@@ -0,0 +1,86 @@
package pac
import (
"bytes"
"errors"
"fmt"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
"github.com/jcmturner/rpc/v2/mstypes"
"github.com/jcmturner/rpc/v2/ndr"
)
// https://msdn.microsoft.com/en-us/library/cc237931.aspx
// CredentialsInfo implements https://msdn.microsoft.com/en-us/library/cc237953.aspx
type CredentialsInfo struct {
Version uint32 // A 32-bit unsigned integer in little-endian format that defines the version. MUST be 0x00000000.
EType uint32
PACCredentialDataEncrypted []byte // Key usage number for encryption: KERB_NON_KERB_SALT (16)
PACCredentialData CredentialData
}
// Unmarshal bytes into the CredentialsInfo struct
func (c *CredentialsInfo) Unmarshal(b []byte, k types.EncryptionKey) (err error) {
//The CredentialsInfo structure is a simple structure that is not NDR-encoded.
r := mstypes.NewReader(bytes.NewReader(b))
c.Version, err = r.Uint32()
if err != nil {
return
}
if c.Version != 0 {
err = errors.New("credentials info version is not zero")
return
}
c.EType, err = r.Uint32()
if err != nil {
return
}
c.PACCredentialDataEncrypted, err = r.ReadBytes(len(b) - 8)
if err != nil {
err = fmt.Errorf("error reading PAC Credetials Data: %v", err)
return
}
err = c.DecryptEncPart(k)
if err != nil {
err = fmt.Errorf("error decrypting PAC Credentials Data: %v", err)
return
}
return
}
// DecryptEncPart decrypts the encrypted part of the CredentialsInfo.
func (c *CredentialsInfo) DecryptEncPart(k types.EncryptionKey) error {
if k.KeyType != int32(c.EType) {
return fmt.Errorf("key provided is not the correct type. Type needed: %d, type provided: %d", c.EType, k.KeyType)
}
pt, err := crypto.DecryptMessage(c.PACCredentialDataEncrypted, k, keyusage.KERB_NON_KERB_SALT)
if err != nil {
return err
}
err = c.PACCredentialData.Unmarshal(pt)
if err != nil {
return err
}
return nil
}
// CredentialData implements https://msdn.microsoft.com/en-us/library/cc237952.aspx
type CredentialData struct {
CredentialCount uint32
Credentials []SECPKGSupplementalCred // Size is the value of CredentialCount
}
// Unmarshal converts the bytes provided into a CredentialData type.
func (c *CredentialData) Unmarshal(b []byte) (err error) {
dec := ndr.NewDecoder(bytes.NewReader(b))
err = dec.Decode(c)
if err != nil {
err = fmt.Errorf("error unmarshaling KerbValidationInfo: %v", err)
}
return
}
+34
View File
@@ -0,0 +1,34 @@
package pac
import (
"bytes"
"fmt"
"github.com/jcmturner/rpc/v2/mstypes"
"github.com/jcmturner/rpc/v2/ndr"
)
// Claims reference: https://msdn.microsoft.com/en-us/library/hh553895.aspx
// DeviceClaimsInfo implements https://msdn.microsoft.com/en-us/library/hh554226.aspx
type DeviceClaimsInfo struct {
ClaimsSetMetadata mstypes.ClaimsSetMetadata
ClaimsSet mstypes.ClaimsSet
}
// Unmarshal bytes into the ClientClaimsInfo struct
func (k *DeviceClaimsInfo) Unmarshal(b []byte) (err error) {
dec := ndr.NewDecoder(bytes.NewReader(b))
m := new(mstypes.ClaimsSetMetadata)
err = dec.Decode(m)
if err != nil {
err = fmt.Errorf("error unmarshaling ClientClaimsInfo ClaimsSetMetadata: %v", err)
return
}
k.ClaimsSetMetadata = *m
k.ClaimsSet, err = k.ClaimsSetMetadata.ClaimsSet()
if err != nil {
err = fmt.Errorf("error unmarshaling ClientClaimsInfo ClaimsSet: %v", err)
}
return
}
+32
View File
@@ -0,0 +1,32 @@
package pac
import (
"bytes"
"fmt"
"github.com/jcmturner/rpc/v2/mstypes"
"github.com/jcmturner/rpc/v2/ndr"
)
// DeviceInfo implements https://msdn.microsoft.com/en-us/library/hh536402.aspx
type DeviceInfo struct {
UserID uint32 // A 32-bit unsigned integer that contains the RID of the account. If the UserId member equals 0x00000000, the first group SID in this member is the SID for this account.
PrimaryGroupID uint32 // A 32-bit unsigned integer that contains the RID for the primary group to which this account belongs.
AccountDomainID mstypes.RPCSID `ndr:"pointer"` // A SID structure that contains the SID for the domain of the account.This member is used in conjunction with the UserId, and GroupIds members to create the user and group SIDs for the client.
AccountGroupCount uint32 // A 32-bit unsigned integer that contains the number of groups within the account domain to which the account belongs
AccountGroupIDs []mstypes.GroupMembership `ndr:"pointer,conformant"` // A pointer to a list of GROUP_MEMBERSHIP (section 2.2.2) structures that contains the groups to which the account belongs in the account domain. The number of groups in this list MUST be equal to GroupCount.
SIDCount uint32 // A 32-bit unsigned integer that contains the total number of SIDs present in the ExtraSids member.
ExtraSIDs []mstypes.KerbSidAndAttributes `ndr:"pointer,conformant"` // A pointer to a list of KERB_SID_AND_ATTRIBUTES structures that contain a list of SIDs corresponding to groups not in domains. If the UserId member equals 0x00000000, the first group SID in this member is the SID for this account.
DomainGroupCount uint32 // A 32-bit unsigned integer that contains the number of domains with groups to which the account belongs.
DomainGroup []mstypes.DomainGroupMembership `ndr:"pointer,conformant"` // A pointer to a list of DOMAIN_GROUP_MEMBERSHIP structures (section 2.2.3) that contains the domains to which the account belongs to a group. The number of sets in this list MUST be equal to DomainCount.
}
// Unmarshal bytes into the DeviceInfo struct
func (k *DeviceInfo) Unmarshal(b []byte) (err error) {
dec := ndr.NewDecoder(bytes.NewReader(b))
err = dec.Decode(k)
if err != nil {
err = fmt.Errorf("error unmarshaling DeviceInfo: %v", err)
}
return
}
+110
View File
@@ -0,0 +1,110 @@
// Package pac implements Microsoft Privilege Attribute Certificate (PAC) processing.
package pac
import (
"bytes"
"fmt"
"github.com/jcmturner/rpc/v2/mstypes"
"github.com/jcmturner/rpc/v2/ndr"
)
// KERB_VALIDATION_INFO flags.
const (
USERFLAG_GUEST = 31 // Authentication was done via the GUEST account; no password was used.
USERFLAG_NO_ENCRYPTION_AVAILABLE = 30 // No encryption is available.
USERFLAG_LAN_MANAGER_KEY = 28 // LAN Manager key was used for authentication.
USERFLAG_SUB_AUTH = 25 // Sub-authentication used; session key came from the sub-authentication package.
USERFLAG_EXTRA_SIDS = 26 // Indicates that the ExtraSids field is populated and contains additional SIDs.
USERFLAG_MACHINE_ACCOUNT = 24 // Indicates that the account is a machine account.
USERFLAG_DC_NTLM2 = 23 // Indicates that the domain controller understands NTLMv2.
USERFLAG_RESOURCE_GROUPIDS = 22 // Indicates that the ResourceGroupIds field is populated.
USERFLAG_PROFILEPATH = 21 // Indicates that ProfilePath is populated.
USERFLAG_NTLM2_NTCHALLENGERESP = 20 // The NTLMv2 response from the NtChallengeResponseFields ([MS-NLMP] section 2.2.1.3) was used for authentication and session key generation.
USERFLAG_LM2_LMCHALLENGERESP = 19 // The LMv2 response from the LmChallengeResponseFields ([MS-NLMP] section 2.2.1.3) was used for authentication and session key generation.
USERFLAG_AUTH_LMCHALLENGERESP_KEY_NTCHALLENGERESP = 18 // The LMv2 response from the LmChallengeResponseFields ([MS-NLMP] section 2.2.1.3) was used for authentication and the NTLMv2 response from the NtChallengeResponseFields ([MS-NLMP] section 2.2.1.3) was used session key generation.
)
// KerbValidationInfo implement https://msdn.microsoft.com/en-us/library/cc237948.aspx
type KerbValidationInfo struct {
LogOnTime mstypes.FileTime
LogOffTime mstypes.FileTime
KickOffTime mstypes.FileTime
PasswordLastSet mstypes.FileTime
PasswordCanChange mstypes.FileTime
PasswordMustChange mstypes.FileTime
EffectiveName mstypes.RPCUnicodeString
FullName mstypes.RPCUnicodeString
LogonScript mstypes.RPCUnicodeString
ProfilePath mstypes.RPCUnicodeString
HomeDirectory mstypes.RPCUnicodeString
HomeDirectoryDrive mstypes.RPCUnicodeString
LogonCount uint16
BadPasswordCount uint16
UserID uint32
PrimaryGroupID uint32
GroupCount uint32
GroupIDs []mstypes.GroupMembership `ndr:"pointer,conformant"`
UserFlags uint32
UserSessionKey mstypes.UserSessionKey
LogonServer mstypes.RPCUnicodeString
LogonDomainName mstypes.RPCUnicodeString
LogonDomainID mstypes.RPCSID `ndr:"pointer"`
Reserved1 [2]uint32 // Has 2 elements
UserAccountControl uint32
SubAuthStatus uint32
LastSuccessfulILogon mstypes.FileTime
LastFailedILogon mstypes.FileTime
FailedILogonCount uint32
Reserved3 uint32
SIDCount uint32
ExtraSIDs []mstypes.KerbSidAndAttributes `ndr:"pointer,conformant"`
ResourceGroupDomainSID mstypes.RPCSID `ndr:"pointer"`
ResourceGroupCount uint32
ResourceGroupIDs []mstypes.GroupMembership `ndr:"pointer,conformant"`
}
// Unmarshal bytes into the DeviceInfo struct
func (k *KerbValidationInfo) Unmarshal(b []byte) (err error) {
dec := ndr.NewDecoder(bytes.NewReader(b))
err = dec.Decode(k)
if err != nil {
err = fmt.Errorf("error unmarshaling KerbValidationInfo: %v", err)
}
return
}
// GetGroupMembershipSIDs returns a slice of strings containing the group membership SIDs found in the PAC.
func (k *KerbValidationInfo) GetGroupMembershipSIDs() []string {
var g []string
lSID := k.LogonDomainID.String()
for i := range k.GroupIDs {
g = append(g, fmt.Sprintf("%s-%d", lSID, k.GroupIDs[i].RelativeID))
}
for _, s := range k.ExtraSIDs {
var exists = false
for _, es := range g {
if es == s.SID.String() {
exists = true
break
}
}
if !exists {
g = append(g, s.SID.String())
}
}
for _, r := range k.ResourceGroupIDs {
var exists = false
s := fmt.Sprintf("%s-%d", k.ResourceGroupDomainSID.String(), r.RelativeID)
for _, es := range g {
if es == s {
exists = true
break
}
}
if !exists {
g = append(g, s)
}
}
return g
}
+251
View File
@@ -0,0 +1,251 @@
package pac
import (
"bytes"
"errors"
"fmt"
"log"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/keyusage"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/types"
"github.com/jcmturner/rpc/v2/mstypes"
)
const (
infoTypeKerbValidationInfo uint32 = 1
infoTypeCredentials uint32 = 2
infoTypePACServerSignatureData uint32 = 6
infoTypePACKDCSignatureData uint32 = 7
infoTypePACClientInfo uint32 = 10
infoTypeS4UDelegationInfo uint32 = 11
infoTypeUPNDNSInfo uint32 = 12
infoTypePACClientClaimsInfo uint32 = 13
infoTypePACDeviceInfo uint32 = 14
infoTypePACDeviceClaimsInfo uint32 = 15
)
// PACType implements: https://msdn.microsoft.com/en-us/library/cc237950.aspx
type PACType struct {
CBuffers uint32
Version uint32
Buffers []InfoBuffer
Data []byte
KerbValidationInfo *KerbValidationInfo
CredentialsInfo *CredentialsInfo
ServerChecksum *SignatureData
KDCChecksum *SignatureData
ClientInfo *ClientInfo
S4UDelegationInfo *S4UDelegationInfo
UPNDNSInfo *UPNDNSInfo
ClientClaimsInfo *ClientClaimsInfo
DeviceInfo *DeviceInfo
DeviceClaimsInfo *DeviceClaimsInfo
ZeroSigData []byte
}
// InfoBuffer implements the PAC Info Buffer: https://msdn.microsoft.com/en-us/library/cc237954.aspx
type InfoBuffer struct {
ULType uint32 // A 32-bit unsigned integer in little-endian format that describes the type of data present in the buffer contained at Offset.
CBBufferSize uint32 // A 32-bit unsigned integer in little-endian format that contains the size, in bytes, of the buffer in the PAC located at Offset.
Offset uint64 // A 64-bit unsigned integer in little-endian format that contains the offset to the beginning of the buffer, in bytes, from the beginning of the PACTYPE structure. The data offset MUST be a multiple of eight. The following sections specify the format of each type of element.
}
// Unmarshal bytes into the PACType struct
func (pac *PACType) Unmarshal(b []byte) (err error) {
pac.Data = b
zb := make([]byte, len(b), len(b))
copy(zb, b)
pac.ZeroSigData = zb
r := mstypes.NewReader(bytes.NewReader(b))
pac.CBuffers, err = r.Uint32()
if err != nil {
return
}
pac.Version, err = r.Uint32()
if err != nil {
return
}
buf := make([]InfoBuffer, pac.CBuffers, pac.CBuffers)
for i := range buf {
buf[i].ULType, err = r.Uint32()
if err != nil {
return
}
buf[i].CBBufferSize, err = r.Uint32()
if err != nil {
return
}
buf[i].Offset, err = r.Uint64()
if err != nil {
return
}
}
pac.Buffers = buf
return nil
}
// ProcessPACInfoBuffers processes the PAC Info Buffers.
// https://msdn.microsoft.com/en-us/library/cc237954.aspx
func (pac *PACType) ProcessPACInfoBuffers(key types.EncryptionKey, l *log.Logger) error {
for _, buf := range pac.Buffers {
p := make([]byte, buf.CBBufferSize, buf.CBBufferSize)
copy(p, pac.Data[int(buf.Offset):int(buf.Offset)+int(buf.CBBufferSize)])
switch buf.ULType {
case infoTypeKerbValidationInfo:
if pac.KerbValidationInfo != nil {
//Must ignore subsequent buffers of this type
continue
}
var k KerbValidationInfo
err := k.Unmarshal(p)
if err != nil {
return fmt.Errorf("error processing KerbValidationInfo: %v", err)
}
pac.KerbValidationInfo = &k
case infoTypeCredentials:
// Currently PAC parsing is only useful on the service side in gokrb5
// The CredentialsInfo are only useful when gokrb5 has implemented RFC4556 and only applied on the client side.
// Skipping CredentialsInfo - will be revisited under RFC4556 implementation.
continue
//if pac.CredentialsInfo != nil {
// //Must ignore subsequent buffers of this type
// continue
//}
//var k CredentialsInfo
//err := k.Unmarshal(p, key) // The encryption key used is the AS reply key only available to the client.
//if err != nil {
// return fmt.Errorf("error processing CredentialsInfo: %v", err)
//}
//pac.CredentialsInfo = &k
case infoTypePACServerSignatureData:
if pac.ServerChecksum != nil {
//Must ignore subsequent buffers of this type
continue
}
var k SignatureData
zb, err := k.Unmarshal(p)
copy(pac.ZeroSigData[int(buf.Offset):int(buf.Offset)+int(buf.CBBufferSize)], zb)
if err != nil {
return fmt.Errorf("error processing ServerChecksum: %v", err)
}
pac.ServerChecksum = &k
case infoTypePACKDCSignatureData:
if pac.KDCChecksum != nil {
//Must ignore subsequent buffers of this type
continue
}
var k SignatureData
zb, err := k.Unmarshal(p)
copy(pac.ZeroSigData[int(buf.Offset):int(buf.Offset)+int(buf.CBBufferSize)], zb)
if err != nil {
return fmt.Errorf("error processing KDCChecksum: %v", err)
}
pac.KDCChecksum = &k
case infoTypePACClientInfo:
if pac.ClientInfo != nil {
//Must ignore subsequent buffers of this type
continue
}
var k ClientInfo
err := k.Unmarshal(p)
if err != nil {
return fmt.Errorf("error processing ClientInfo: %v", err)
}
pac.ClientInfo = &k
case infoTypeS4UDelegationInfo:
if pac.S4UDelegationInfo != nil {
//Must ignore subsequent buffers of this type
continue
}
var k S4UDelegationInfo
err := k.Unmarshal(p)
if err != nil {
l.Printf("could not process S4U_DelegationInfo: %v", err)
continue
}
pac.S4UDelegationInfo = &k
case infoTypeUPNDNSInfo:
if pac.UPNDNSInfo != nil {
//Must ignore subsequent buffers of this type
continue
}
var k UPNDNSInfo
err := k.Unmarshal(p)
if err != nil {
l.Printf("could not process UPN_DNSInfo: %v", err)
continue
}
pac.UPNDNSInfo = &k
case infoTypePACClientClaimsInfo:
if pac.ClientClaimsInfo != nil || len(p) < 1 {
//Must ignore subsequent buffers of this type
continue
}
var k ClientClaimsInfo
err := k.Unmarshal(p)
if err != nil {
l.Printf("could not process ClientClaimsInfo: %v", err)
continue
}
pac.ClientClaimsInfo = &k
case infoTypePACDeviceInfo:
if pac.DeviceInfo != nil {
//Must ignore subsequent buffers of this type
continue
}
var k DeviceInfo
err := k.Unmarshal(p)
if err != nil {
l.Printf("could not process DeviceInfo: %v", err)
continue
}
pac.DeviceInfo = &k
case infoTypePACDeviceClaimsInfo:
if pac.DeviceClaimsInfo != nil {
//Must ignore subsequent buffers of this type
continue
}
var k DeviceClaimsInfo
err := k.Unmarshal(p)
if err != nil {
l.Printf("could not process DeviceClaimsInfo: %v", err)
continue
}
pac.DeviceClaimsInfo = &k
}
}
if ok, err := pac.verify(key); !ok {
return err
}
return nil
}
func (pac *PACType) verify(key types.EncryptionKey) (bool, error) {
if pac.KerbValidationInfo == nil {
return false, errors.New("PAC Info Buffers does not contain a KerbValidationInfo")
}
if pac.ServerChecksum == nil {
return false, errors.New("PAC Info Buffers does not contain a ServerChecksum")
}
if pac.KDCChecksum == nil {
return false, errors.New("PAC Info Buffers does not contain a KDCChecksum")
}
if pac.ClientInfo == nil {
return false, errors.New("PAC Info Buffers does not contain a ClientInfo")
}
etype, err := crypto.GetChksumEtype(int32(pac.ServerChecksum.SignatureType))
if err != nil {
return false, err
}
if ok := etype.VerifyChecksum(key.KeyValue,
pac.ZeroSigData,
pac.ServerChecksum.Signature,
keyusage.KERB_NON_KERB_CKSUM_SALT); !ok {
return false, errors.New("PAC service checksum verification failed")
}
return true, nil
}
+26
View File
@@ -0,0 +1,26 @@
package pac
import (
"bytes"
"fmt"
"github.com/jcmturner/rpc/v2/mstypes"
"github.com/jcmturner/rpc/v2/ndr"
)
// S4UDelegationInfo implements https://msdn.microsoft.com/en-us/library/cc237944.aspx
type S4UDelegationInfo struct {
S4U2proxyTarget mstypes.RPCUnicodeString // The name of the principal to whom the application can forward the ticket.
TransitedListSize uint32
S4UTransitedServices []mstypes.RPCUnicodeString `ndr:"pointer,conformant"` // List of all services that have been delegated through by this client and subsequent services or servers.. Size is value of TransitedListSize
}
// Unmarshal bytes into the S4UDelegationInfo struct
func (k *S4UDelegationInfo) Unmarshal(b []byte) (err error) {
dec := ndr.NewDecoder(bytes.NewReader(b))
err = dec.Decode(k)
if err != nil {
err = fmt.Errorf("error unmarshaling S4UDelegationInfo: %v", err)
}
return
}
+67
View File
@@ -0,0 +1,67 @@
package pac
import (
"bytes"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/chksumtype"
"github.com/jcmturner/rpc/v2/mstypes"
)
/*
https://msdn.microsoft.com/en-us/library/cc237955.aspx
The Key Usage Value MUST be KERB_NON_KERB_CKSUM_SALT (17) [MS-KILE] (section 3.1.5.9).
Server Signature (SignatureType = 0x00000006)
https://msdn.microsoft.com/en-us/library/cc237957.aspx
KDC Signature (SignatureType = 0x00000007)
https://msdn.microsoft.com/en-us/library/dd357117.aspx
*/
// SignatureData implements https://msdn.microsoft.com/en-us/library/cc237955.aspx
type SignatureData struct {
SignatureType uint32 // A 32-bit unsigned integer value in little-endian format that defines the cryptographic system used to calculate the checksum. This MUST be one of the following checksum types: KERB_CHECKSUM_HMAC_MD5 (signature size = 16), HMAC_SHA1_96_AES128 (signature size = 12), HMAC_SHA1_96_AES256 (signature size = 12).
Signature []byte // Size depends on the type. See comment above.
RODCIdentifier uint16 // A 16-bit unsigned integer value in little-endian format that contains the first 16 bits of the key version number ([MS-KILE] section 3.1.5.8) when the KDC is an RODC. When the KDC is not an RODC, this field does not exist.
}
// Unmarshal bytes into the SignatureData struct
func (k *SignatureData) Unmarshal(b []byte) (rb []byte, err error) {
r := mstypes.NewReader(bytes.NewReader(b))
k.SignatureType, err = r.Uint32()
if err != nil {
return
}
var c int
switch k.SignatureType {
case chksumtype.KERB_CHECKSUM_HMAC_MD5_UNSIGNED:
c = 16
case uint32(chksumtype.HMAC_SHA1_96_AES128):
c = 12
case uint32(chksumtype.HMAC_SHA1_96_AES256):
c = 12
}
k.Signature, err = r.ReadBytes(c)
if err != nil {
return
}
// When the KDC is not an Read Only Domain Controller (RODC), this field does not exist.
if len(b) >= 4+c+2 {
k.RODCIdentifier, err = r.Uint16()
if err != nil {
return
}
}
// Create bytes with zeroed signature needed for checksum verification
rb = make([]byte, len(b), len(b))
copy(rb, b)
z := make([]byte, len(b), len(b))
copy(rb[4:4+c], z)
return
}
+87
View File
@@ -0,0 +1,87 @@
package pac
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"github.com/jcmturner/rpc/v2/mstypes"
"github.com/jcmturner/rpc/v2/ndr"
)
const (
// NTLMSupCredLMOWF indicates that the LM OWF member is present and valid.
NTLMSupCredLMOWF uint32 = 31
// NTLMSupCredNTOWF indicates that the NT OWF member is present and valid.
NTLMSupCredNTOWF uint32 = 30
)
// NTLMSupplementalCred implements https://msdn.microsoft.com/en-us/library/cc237949.aspx
type NTLMSupplementalCred struct {
Version uint32 // A 32-bit unsigned integer that defines the credential version.This field MUST be 0x00000000.
Flags uint32
LMPassword []byte // A 16-element array of unsigned 8-bit integers that define the LM OWF. The LMPassword member MUST be ignored if the L flag is not set in the Flags member.
NTPassword []byte // A 16-element array of unsigned 8-bit integers that define the NT OWF. The NTPassword member MUST be ignored if the N flag is not set in the Flags member.
}
// Unmarshal converts the bytes provided into a NTLMSupplementalCred.
func (c *NTLMSupplementalCred) Unmarshal(b []byte) (err error) {
r := mstypes.NewReader(bytes.NewReader(b))
c.Version, err = r.Uint32()
if err != nil {
return
}
if c.Version != 0 {
err = errors.New("NTLMSupplementalCred version is not zero")
return
}
c.Flags, err = r.Uint32()
if err != nil {
return
}
if isFlagSet(c.Flags, NTLMSupCredLMOWF) {
c.LMPassword, err = r.ReadBytes(16)
if err != nil {
return
}
}
if isFlagSet(c.Flags, NTLMSupCredNTOWF) {
c.NTPassword, err = r.ReadBytes(16)
if err != nil {
return
}
}
return
}
// isFlagSet tests if a flag is set in the uint32 little endian flag
func isFlagSet(f uint32, i uint32) bool {
//Which byte?
b := int(i / 8)
//Which bit in byte
p := uint(7 - (int(i) - 8*b))
fb := make([]byte, 4)
binary.LittleEndian.PutUint32(fb, f)
if fb[b]&(1<<p) != 0 {
return true
}
return false
}
// SECPKGSupplementalCred implements https://msdn.microsoft.com/en-us/library/cc237956.aspx
type SECPKGSupplementalCred struct {
PackageName mstypes.RPCUnicodeString
CredentialSize uint32
Credentials []uint8 `ndr:"pointer,conformant"` // Is a ptr. Size is the value of CredentialSize
}
// Unmarshal converts the bytes provided into a SECPKGSupplementalCred.
func (c *SECPKGSupplementalCred) Unmarshal(b []byte) (err error) {
dec := ndr.NewDecoder(bytes.NewReader(b))
err = dec.Decode(c)
if err != nil {
err = fmt.Errorf("error unmarshaling SECPKGSupplementalCred: %v", err)
}
return
}
+73
View File
@@ -0,0 +1,73 @@
package pac
import (
"bytes"
"github.com/jcmturner/rpc/v2/mstypes"
)
// UPNDNSInfo implements https://msdn.microsoft.com/en-us/library/dd240468.aspx
type UPNDNSInfo struct {
UPNLength uint16 // An unsigned 16-bit integer in little-endian format that specifies the length, in bytes, of the UPN field.
UPNOffset uint16 // An unsigned 16-bit integer in little-endian format that contains the offset to the beginning of the buffer, in bytes, from the beginning of the UPN_DNS_INFO structure.
DNSDomainNameLength uint16
DNSDomainNameOffset uint16
Flags uint32
UPN string
DNSDomain string
}
const (
upnNoUPNAttr = 31 // The user account object does not have the userPrincipalName attribute ([MS-ADA3] section 2.349) set. A UPN constructed by concatenating the user name with the DNS domain name of the account domain is provided.
)
// Unmarshal bytes into the UPN_DNSInfo struct
func (k *UPNDNSInfo) Unmarshal(b []byte) (err error) {
//The UPN_DNS_INFO structure is a simple structure that is not NDR-encoded.
r := mstypes.NewReader(bytes.NewReader(b))
k.UPNLength, err = r.Uint16()
if err != nil {
return
}
k.UPNOffset, err = r.Uint16()
if err != nil {
return
}
k.DNSDomainNameLength, err = r.Uint16()
if err != nil {
return
}
k.DNSDomainNameOffset, err = r.Uint16()
if err != nil {
return
}
k.Flags, err = r.Uint32()
if err != nil {
return
}
ub := mstypes.NewReader(bytes.NewReader(b[k.UPNOffset : k.UPNOffset+k.UPNLength]))
db := mstypes.NewReader(bytes.NewReader(b[k.DNSDomainNameOffset : k.DNSDomainNameOffset+k.DNSDomainNameLength]))
u := make([]rune, k.UPNLength/2, k.UPNLength/2)
for i := 0; i < len(u); i++ {
var r uint16
r, err = ub.Uint16()
if err != nil {
return
}
u[i] = rune(r)
}
k.UPN = string(u)
d := make([]rune, k.DNSDomainNameLength/2, k.DNSDomainNameLength/2)
for i := 0; i < len(d); i++ {
var r uint16
r, err = db.Uint16()
if err != nil {
return
}
d[i] = rune(r)
}
k.DNSDomain = string(d)
return
}
+81
View File
@@ -0,0 +1,81 @@
// Package types provides Kerberos 5 data types.
package types
import (
"crypto/rand"
"fmt"
"math"
"math/big"
"time"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/asn1tools"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/asnAppTag"
)
// Authenticator - A record containing information that can be shown to have been recently generated using the session
// key known only by the client and server.
// https://tools.ietf.org/html/rfc4120#section-5.5.1
type Authenticator struct {
AVNO int `asn1:"explicit,tag:0"`
CRealm string `asn1:"generalstring,explicit,tag:1"`
CName PrincipalName `asn1:"explicit,tag:2"`
Cksum Checksum `asn1:"explicit,optional,tag:3"`
Cusec int `asn1:"explicit,tag:4"`
CTime time.Time `asn1:"generalized,explicit,tag:5"`
SubKey EncryptionKey `asn1:"explicit,optional,tag:6"`
SeqNumber int64 `asn1:"explicit,optional,tag:7"`
AuthorizationData AuthorizationData `asn1:"explicit,optional,tag:8"`
}
// NewAuthenticator creates a new Authenticator.
func NewAuthenticator(realm string, cname PrincipalName) (Authenticator, error) {
seq, err := rand.Int(rand.Reader, big.NewInt(math.MaxUint32))
if err != nil {
return Authenticator{}, err
}
t := time.Now().UTC()
return Authenticator{
AVNO: iana.PVNO,
CRealm: realm,
CName: cname,
Cksum: Checksum{},
Cusec: int((t.UnixNano() / int64(time.Microsecond)) - (t.Unix() * 1e6)),
CTime: t,
SeqNumber: seq.Int64() & 0x3fffffff,
}, nil
}
// GenerateSeqNumberAndSubKey sets the Authenticator's sequence number and subkey.
func (a *Authenticator) GenerateSeqNumberAndSubKey(keyType int32, keySize int) error {
seq, err := rand.Int(rand.Reader, big.NewInt(math.MaxUint32))
if err != nil {
return err
}
a.SeqNumber = seq.Int64() & 0x3fffffff
//Generate subkey value
sk := make([]byte, keySize, keySize)
rand.Read(sk)
a.SubKey = EncryptionKey{
KeyType: keyType,
KeyValue: sk,
}
return nil
}
// Unmarshal bytes into the Authenticator.
func (a *Authenticator) Unmarshal(b []byte) error {
_, err := asn1.UnmarshalWithParams(b, a, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.Authenticator))
return err
}
// Marshal the Authenticator.
func (a *Authenticator) Marshal() ([]byte, error) {
b, err := asn1.Marshal(*a)
if err != nil {
return nil, err
}
b = asn1tools.AddASNAppTag(b, asnAppTag.Authenticator)
return b, nil
}
+55
View File
@@ -0,0 +1,55 @@
package types
import (
"github.com/jcmturner/gofork/encoding/asn1"
)
// Reference: https://www.ietf.org/rfc/rfc4120.txt
// Section: 5.2.6
// AuthorizationData implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.2.6
type AuthorizationData []AuthorizationDataEntry
// AuthorizationDataEntry implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.2.6
type AuthorizationDataEntry struct {
ADType int32 `asn1:"explicit,tag:0"`
ADData []byte `asn1:"explicit,tag:1"`
}
// ADIfRelevant implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.2.6.1
type ADIfRelevant AuthorizationData
// ADKDCIssued implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.2.6.2
type ADKDCIssued struct {
ADChecksum Checksum `asn1:"explicit,tag:0"`
IRealm string `asn1:"optional,generalstring,explicit,tag:1"`
Isname PrincipalName `asn1:"optional,explicit,tag:2"`
Elements AuthorizationData `asn1:"explicit,tag:3"`
}
// ADAndOr implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.2.6.3
type ADAndOr struct {
ConditionCount int32 `asn1:"explicit,tag:0"`
Elements AuthorizationData `asn1:"explicit,tag:1"`
}
// ADMandatoryForKDC implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.2.6.4
type ADMandatoryForKDC AuthorizationData
// Unmarshal bytes into the ADKDCIssued.
func (a *ADKDCIssued) Unmarshal(b []byte) error {
_, err := asn1.Unmarshal(b, a)
return err
}
// Unmarshal bytes into the AuthorizationData.
func (a *AuthorizationData) Unmarshal(b []byte) error {
_, err := asn1.Unmarshal(b, a)
return err
}
// Unmarshal bytes into the AuthorizationDataEntry.
func (a *AuthorizationDataEntry) Unmarshal(b []byte) error {
_, err := asn1.Unmarshal(b, a)
return err
}
+72
View File
@@ -0,0 +1,72 @@
package types
import (
"crypto/rand"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/crypto/etype"
)
// Reference: https://www.ietf.org/rfc/rfc4120.txt
// Section: 5.2.9
// EncryptedData implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.2.9
type EncryptedData struct {
EType int32 `asn1:"explicit,tag:0"`
KVNO int `asn1:"explicit,optional,tag:1"`
Cipher []byte `asn1:"explicit,tag:2"`
}
// EncryptionKey implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.2.9
// AKA KeyBlock
type EncryptionKey struct {
KeyType int32 `asn1:"explicit,tag:0"`
KeyValue []byte `asn1:"explicit,tag:1" json:"-"`
}
// Checksum implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.2.9
type Checksum struct {
CksumType int32 `asn1:"explicit,tag:0"`
Checksum []byte `asn1:"explicit,tag:1"`
}
// Unmarshal bytes into the EncryptedData.
func (a *EncryptedData) Unmarshal(b []byte) error {
_, err := asn1.Unmarshal(b, a)
return err
}
// Marshal the EncryptedData.
func (a *EncryptedData) Marshal() ([]byte, error) {
edb, err := asn1.Marshal(*a)
if err != nil {
return edb, err
}
return edb, nil
}
// Unmarshal bytes into the EncryptionKey.
func (a *EncryptionKey) Unmarshal(b []byte) error {
_, err := asn1.Unmarshal(b, a)
return err
}
// Unmarshal bytes into the Checksum.
func (a *Checksum) Unmarshal(b []byte) error {
_, err := asn1.Unmarshal(b, a)
return err
}
// GenerateEncryptionKey creates a new EncryptionKey with a random key value.
func GenerateEncryptionKey(etype etype.EType) (EncryptionKey, error) {
k := EncryptionKey{
KeyType: etype.GetETypeID(),
}
b := make([]byte, etype.GetKeyByteSize(), etype.GetKeyByteSize())
_, err := rand.Read(b)
if err != nil {
return k, err
}
k.KeyValue = b
return k, nil
}
+180
View File
@@ -0,0 +1,180 @@
package types
// Reference: https://www.ietf.org/rfc/rfc4120.txt
// Section: 5.2.5
import (
"bytes"
"fmt"
"net"
"github.com/jcmturner/gofork/encoding/asn1"
"github.com/mandiant/gopacket/pkg/third_party/gokrb5/iana/addrtype"
)
// HostAddresses implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.2.5
type HostAddresses []HostAddress
// HostAddress implements RFC 4120 type: https://tools.ietf.org/html/rfc4120#section-5.2.5
type HostAddress struct {
AddrType int32 `asn1:"explicit,tag:0"`
Address []byte `asn1:"explicit,tag:1"`
}
// GetHostAddress returns a HostAddress struct from a string in the format <hostname>:<port>
func GetHostAddress(s string) (HostAddress, error) {
var h HostAddress
cAddr, _, err := net.SplitHostPort(s)
if err != nil {
return h, fmt.Errorf("invalid format of client address: %v", err)
}
ip := net.ParseIP(cAddr)
var ht int32
if ip.To4() != nil {
ht = addrtype.IPv4
ip = ip.To4()
} else if ip.To16() != nil {
ht = addrtype.IPv6
ip = ip.To16()
} else {
return h, fmt.Errorf("could not determine client's address types: %v", err)
}
h = HostAddress{
AddrType: ht,
Address: ip,
}
return h, nil
}
// GetAddress returns a string representation of the HostAddress.
func (h *HostAddress) GetAddress() (string, error) {
var b []byte
_, err := asn1.Unmarshal(h.Address, &b)
return string(b), err
}
// LocalHostAddresses returns a HostAddresses struct for the local machines interface IP addresses.
func LocalHostAddresses() (ha HostAddresses, err error) {
ifs, err := net.Interfaces()
if err != nil {
return
}
for _, iface := range ifs {
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
// Interface is either loopback of not up
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
var a HostAddress
if ip.To16() == nil {
//neither IPv4 or IPv6
continue
}
if ip.To4() != nil {
//Is IPv4
a.AddrType = addrtype.IPv4
a.Address = ip.To4()
} else {
a.AddrType = addrtype.IPv6
a.Address = ip.To16()
}
ha = append(ha, a)
}
}
return ha, nil
}
// HostAddressesFromNetIPs returns a HostAddresses type from a slice of net.IP
func HostAddressesFromNetIPs(ips []net.IP) (ha HostAddresses) {
for _, ip := range ips {
ha = append(ha, HostAddressFromNetIP(ip))
}
return ha
}
// HostAddressFromNetIP returns a HostAddress type from a net.IP
func HostAddressFromNetIP(ip net.IP) HostAddress {
if ip.To4() != nil {
//Is IPv4
return HostAddress{
AddrType: addrtype.IPv4,
Address: ip.To4(),
}
}
return HostAddress{
AddrType: addrtype.IPv6,
Address: ip.To16(),
}
}
// HostAddressesEqual tests if two HostAddress slices are equal.
func HostAddressesEqual(h, a []HostAddress) bool {
if len(h) != len(a) {
return false
}
for _, e := range a {
var found bool
for _, i := range h {
if e.Equal(i) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// HostAddressesContains tests if a HostAddress is contained in a HostAddress slice.
func HostAddressesContains(h []HostAddress, a HostAddress) bool {
for _, e := range h {
if e.Equal(a) {
return true
}
}
return false
}
// Equal tests if the HostAddress is equal to another HostAddress provided.
func (h *HostAddress) Equal(a HostAddress) bool {
if h.AddrType != a.AddrType {
return false
}
return bytes.Equal(h.Address, a.Address)
}
// Contains tests if a HostAddress is contained within the HostAddresses struct.
func (h *HostAddresses) Contains(a HostAddress) bool {
for _, e := range *h {
if e.Equal(a) {
return true
}
}
return false
}
// Equal tests if a HostAddress slice is equal to the HostAddresses struct.
func (h *HostAddresses) Equal(a []HostAddress) bool {
if len(*h) != len(a) {
return false
}
for _, e := range a {
if !h.Contains(e) {
return false
}
}
return true
}

Some files were not shown because too many files have changed in this diff Show More