Initial commit

This commit is contained in:
Erik Geiser
2025-02-19 11:31:01 +01:00
commit 616eaaa355
36 changed files with 5607 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
name: Check
on:
# run tests on push to main, but not when other branches are pushed to
push:
branches:
- main
# run tests for all pull requests
pull_request:
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.24.x
id: go
- name: Checkout code
uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.64
args: --verbose --timeout 5m
- name: Check go.mod
run: |
echo "check if go.mod is up to date"
go mod tidy
git diff --exit-code go.mod
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.24.x
id: go
- name: Checkout code
uses: actions/checkout@v3
- name: Run tests
run: |
go test ./...
build:
strategy:
matrix:
go-version:
- 1.24.x
runs-on: ubuntu-latest
name: Build with Go ${{ matrix.go-version }}
env:
GOPROXY: https://proxy.golang.org
steps:
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
id: go
- name: Checkout code
uses: actions/checkout@v3
- name: Build
run: go build ./...
+2
View File
@@ -0,0 +1,2 @@
/adauth
*.exe
+102
View File
@@ -0,0 +1,102 @@
linters:
disable-all: true
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- decorder
- dupl
- dupword
- durationcheck
- errchkjson
- errname
- errorlint
- fatcontext
- forcetypeassert
- ginkgolinter
- gocheckcompilerdirectives
- gochecksumtype
- goconst
- gocritic
- gocyclo
- godot
- godox
- gofmt
- goheader
- goimports
- gosec
- gomodguard
- goprintffuncname
- gosimple
- gosmopolitan
- govet
- grouper
- importas
- inamedparam
- ineffassign
- interfacebloat
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- nakedret
- nestif
- nilerr
- nilnil
- nlreturn
- noctx
- nolintlint
- nosprintfhostport
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- tagliatelle
- usetesting
- testableexamples
- testifylint
- thelper
- tparallel
- unconvert
- unparam
- unused
- usestdlibvars
- wastedassign
- whitespace
- wsl
- zerologlint
linters-settings:
godox:
keywords:
- FIXME # FIXME generates a linter warning
goconst:
min-occurrences: 5
tagliatelle:
# check the struck tag name case
case:
rules:
json: snake
yaml: snake
gosec:
excludes:
- G304 # command execution
- G204 # file inclusion
- G115 # integer overflow
- G401 # weak cryptographic primitive (tell Microsoft, not me)
- G501 # weak cryptographic primitive (tell Microsoft, not me)
- G505 # weak cryptographic primitive (tell Microsoft, not me)
- G402 # InsecureSkipVerify may be true
issues:
exclude-use-default: false
+8
View File
@@ -0,0 +1,8 @@
Copyright 2024 RedTeam Pentesting GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+101
View File
@@ -0,0 +1,101 @@
<p align="center">
<h1 align="center"><b>adauth</b></h1>
<p align="center"><i>Active Directory Authentication Library</i></p>
<p align="center">
<a href="https://github.com/RedTeamPentesting/adauth/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/RedTeamPentesting/adauth.svg?style=for-the-badge"></a>
<a href="https://pkg.go.dev/github.com/RedTeamPentesting/adauth"><img alt="Go Doc" src="https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge"></a>
<a href="https://github.com/RedTeamPentesting/adauth/actions?workflow=Check"><img alt="GitHub Action: Check" src="https://img.shields.io/github/actions/workflow/status/RedTeamPentesting/adauth/check.yml?branch=main&style=for-the-badge"></a>
<a href="/LICENSE"><img alt="Software License" src="https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge"></a>
<a href="https://goreportcard.com/report/github.com/RedTeamPentesting/adauth"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/RedTeamPentesting/adauth?style=for-the-badge"></a>
</p>
</p>
**Warning: The API of this library is not yet stable. Expect breaking changes.**
`adauth` is a Go library for active directory authentication. It can be used to
quickly set up authentication options:
```go
var (
ctx = context.Background()
authOpts = &adauth.Options{}
)
authOpts.RegisterFlags(pflag.CommandLine)
pflag.Parse()
// --aes-key string Kerberos AES key
// --ccache string Kerberos CCache file name (defaults to $KRB5CCNAME, currently unset)
// --dc string Domain controller
// --debug Enable debug output
// -k, --kerberos Use Kerberos authentication
// -H, --nt-hash string NT hash
// -p, --password string Password
// --pfx string Client certificate and private key in PFX format
// --pfx-password string Password for PFX file
// -u, --user string Username ('user@domain', 'domain\user', 'domain/user' or 'user')
// Credentials for an arbitrary target:
creds, target, err := authOpts.WithTarget(ctx, "smb", pflag.Arg(0))
if err != nil { /* error handling */ }
// Only credentials are need, no specific target:
creds, err := authOpts.NoTarget()
if err != nil { /* error handling */ }
// Credentials to authenticate to the corresponding DC:
creds, dc, err := authOpts.WithDCTarget(ctx, "ldap")
if err != nil { /* error handling */ }
```
It deduces as much information from the parameters as possible. For example,
Kerberos authentication is possible even when specifying the target via IP
address if reverse lookups are possible. Similarly, the domain can be omitted
when the target hostname contains the domain.
The library also contains helper packages for LDAP and DCERPC, a Kerebros PKINIT
implementation as well as helpers for creating and writing CCache files (see
examples).
## Features
* Kerberos
* PKINIT
* UnPAC-the-Hash
* Pass-the-Hash (RC4/NT or AES key)
* CCache (containing TGT or ST)
* NTLM
* Pass-the-Hash
* LDAP
* Kerberos, NTLM, Simple Bind
* mTLS Authentication / Pass-the-Certificate (LDAPS or LDAP+StartTLS)
* Channel Binding (Kerberos and NTLM)
* DCERPC:
* Kerberos, NTLM
* Raw endpoits (with port mapping)
* Named pipes (SMB)
* Signing
* Sealing
## Caveats
**LDAP:**
The LDAP helper package does not support authentication using RC4 service
tickets from `ccache`, since Windows returns unsupported GSSAPI wrap tokens
during the SASL handshake when presented with an RC4 service ticket (see
[github.com/jcmturner/gokrb5/pull/498](https://github.com/jcmturner/gokrb5/pull/498)).
However, it should still be possible to request an AES256 service ticket
instead, even when an NT hash was used for pre-authentication . Unfortunately,
[impacket](https://github.com/fortra/impacket) always requests RC4 tickets. This
behavior can be changed by adding
`int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value),` as the first
element of [this
list](https://github.com/fortra/impacket/blob/af91d617c382e1eb132506159debcbc10da7a567/impacket/krb5/kerberosv5.py#L447-L450).
The LDAP library does not (yet) support LDAP signing, but it supports channel
binding for LDAPS and LDAP+StartTLS which is typically sufficient as a
workaround unless the server lacks a TLS certificate.
+192
View File
@@ -0,0 +1,192 @@
package ccachetools
import (
"encoding/binary"
"fmt"
"strings"
"time"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
)
// NewCCache constructs an in-memory CCache.
func NewCCache(
ticket messages.Ticket, key types.EncryptionKey,
serverName types.PrincipalName, clientName types.PrincipalName, clientRealm string,
authTime time.Time, startTime time.Time, endTime time.Time, renewTill time.Time,
) (*credentials.CCache, error) {
ticketBytes, err := ticket.Marshal()
if err != nil {
return nil, fmt.Errorf("marshal ticket for ccache: %w", err)
}
entry := &credentials.Credential{
Key: key,
AuthTime: authTime,
StartTime: startTime,
EndTime: endTime,
RenewTill: renewTill,
Ticket: ticketBytes,
}
entry.Client.PrincipalName = clientName
entry.Client.Realm = clientRealm
entry.Server.PrincipalName = serverName
entry.Server.Realm = clientRealm
ccache := &credentials.CCache{
Credentials: []*credentials.Credential{entry},
}
ccache.DefaultPrincipal.PrincipalName = clientName
ccache.DefaultPrincipal.Realm = strings.ToUpper(clientRealm)
return ccache, nil
}
// NewCCacheFromASRep constructs an in-memory CCache based on the ticket and key
// in the provided Kerberos ASRep message. The ASRep message must already be
// decrypted. The CCache will contain only this ticket and the ticket user will
// be set as the default principal of the CCache.
func NewCCacheFromASRep(asRep messages.ASRep) (*credentials.CCache, error) {
if len(asRep.DecryptedEncPart.Key.KeyValue) == 0 {
return nil, fmt.Errorf("ASRep key was not decrypted")
}
return NewCCache(
asRep.Ticket, asRep.DecryptedEncPart.Key,
asRep.DecryptedEncPart.SName, asRep.CName, asRep.CRealm,
asRep.DecryptedEncPart.AuthTime, asRep.DecryptedEncPart.StartTime,
asRep.DecryptedEncPart.EndTime, asRep.DecryptedEncPart.RenewTill)
}
// NewCCacheFromTGSRep constructs an in-memory CCache based on the ticket and
// key in the provided Kerberos TGSRep message. The TGSRep message must already
// be decrypted. The CCache will contain only this ticket and the ticket user
// will be set as the default principal of the CCache.
func NewCCacheFromTGSRep(tgsRep messages.TGSRep) (*credentials.CCache, error) {
if len(tgsRep.DecryptedEncPart.Key.KeyValue) == 0 {
return nil, fmt.Errorf("TGSRep key was not decrypted")
}
return NewCCache(
tgsRep.Ticket, tgsRep.DecryptedEncPart.Key,
tgsRep.DecryptedEncPart.SName, tgsRep.CName, tgsRep.CRealm,
tgsRep.DecryptedEncPart.AuthTime, tgsRep.DecryptedEncPart.StartTime,
tgsRep.DecryptedEncPart.EndTime, tgsRep.DecryptedEncPart.RenewTill)
}
// MarshalCCache returns the byte representation of the provided CCache such
// that it can be saved on-disk.
func MarshalCCache(ccache *credentials.CCache) ([]byte, error) {
switch ccache.Version {
case 0, 1, 2, 3, 4:
default:
return nil, fmt.Errorf("unsupported CCache version: %d", ccache.Version)
}
version := ccache.Version
if version == 0 {
version = 4
}
var bo binary.AppendByteOrder = binary.BigEndian
if version == 1 || version == 2 {
bo = binary.LittleEndian
}
buf := []byte{5, version}
// header
if version == 4 {
buf = bo.AppendUint16(buf, 0)
}
// default principal
buf = append(buf, principalBytes(bo, version,
ccache.DefaultPrincipal.PrincipalName, ccache.DefaultPrincipal.Realm)...)
// credentials
for _, cred := range ccache.Credentials {
buf = append(buf, credentialBytes(bo, version, cred)...)
}
return buf, nil
}
func principalBytes(bo binary.AppendByteOrder, v uint8, p types.PrincipalName, realm string) (res []byte) {
if v != 1 {
res = bo.AppendUint32(res, uint32(p.NameType))
}
nCompontents := len(p.NameString)
if v == 1 {
nCompontents--
}
res = bo.AppendUint32(res, uint32(nCompontents))
res = bo.AppendUint32(res, uint32(len(realm)))
res = append(res, []byte(realm)...)
for _, part := range p.NameString {
res = bo.AppendUint32(res, uint32(len(part)))
res = append(res, []byte(part)...)
}
return res
}
func credentialBytes(bo binary.AppendByteOrder, v uint8, cred *credentials.Credential) (res []byte) {
res = append(res, principalBytes(bo, v, cred.Client.PrincipalName, cred.Client.Realm)...)
res = append(res, principalBytes(bo, v, cred.Server.PrincipalName, cred.Server.Realm)...)
res = bo.AppendUint16(res, uint16(cred.Key.KeyType))
res = bo.AppendUint32(res, uint32(len(cred.Key.KeyValue)))
res = append(res, cred.Key.KeyValue...)
res = bo.AppendUint32(res, uint32(cred.AuthTime.Unix()))
res = bo.AppendUint32(res, uint32(cred.StartTime.Unix()))
res = bo.AppendUint32(res, uint32(cred.EndTime.Unix()))
res = bo.AppendUint32(res, uint32(cred.RenewTill.Unix()))
if cred.IsSKey {
res = append(res, 1)
} else {
res = append(res, 0)
}
flags := cred.TicketFlags.Bytes
if len(flags) == 0 {
flags = make([]byte, 4)
}
res = append(res, flags...)
res = bo.AppendUint32(res, uint32(len(cred.Addresses)))
for _, addr := range cred.Addresses {
res = bo.AppendUint16(res, uint16(addr.AddrType))
res = bo.AppendUint32(res, uint32(len(addr.Address)))
res = append(res, addr.Address...)
}
res = bo.AppendUint32(res, uint32(len(cred.AuthData)))
for _, data := range cred.AuthData {
res = bo.AppendUint16(res, uint16(data.ADType))
res = bo.AppendUint32(res, uint32(len(data.ADData)))
res = append(res, data.ADData...)
}
res = bo.AppendUint32(res, uint32(len(cred.Ticket)))
res = append(res, cred.Ticket...)
res = bo.AppendUint32(res, uint32(len(cred.SecondTicket)))
res = append(res, cred.SecondTicket...)
return res
}
+271
View File
@@ -0,0 +1,271 @@
package ccachetools_test
import (
"bytes"
"slices"
"strings"
"testing"
"time"
"github.com/RedTeamPentesting/adauth/ccachetools"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
)
var (
testASRep = messages.ASRep{
KDCRepFields: messages.KDCRepFields{
PVNO: 5,
MsgType: 11,
CRealm: "realm",
CName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "user"),
Ticket: messages.Ticket{
TktVNO: 5,
Realm: "realm",
SName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "krbtgt/redteam"),
DecryptedEncPart: messages.EncTicketPart{
Key: types.EncryptionKey{},
CName: types.NewPrincipalName(0, ""),
StartTime: time.Now(),
AuthTime: time.Now(),
EndTime: time.Now(),
RenewTill: time.Now(),
},
},
EncPart: types.EncryptedData{
EType: 18,
KVNO: 2,
Cipher: []byte{1, 2, 3},
},
DecryptedEncPart: messages.EncKDCRepPart{
Key: types.EncryptionKey{
KeyType: 18,
KeyValue: []byte{1, 3, 3, 7},
},
SRealm: "realm",
SName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "krbtgt/redteam"),
StartTime: time.Now(),
AuthTime: time.Now(),
EndTime: time.Now(),
RenewTill: time.Now(),
},
},
}
testTGSRep = messages.TGSRep(testASRep)
)
func TestNewCCacheFromASRep(t *testing.T) {
t.Parallel()
ccache, err := ccachetools.NewCCacheFromASRep(testASRep)
if err != nil {
t.Fatalf("NewCCacheFromASRep: %v", err)
}
validateCCache(t, ccache)
}
func TestNewCCacheFromTGSRep(t *testing.T) {
t.Parallel()
ccache, err := ccachetools.NewCCacheFromTGSRep(testTGSRep)
if err != nil {
t.Fatalf("NewCCacheFromTGSRep: %v", err)
}
validateCCache(t, ccache)
}
func TestNewCCache(t *testing.T) {
t.Parallel()
ccache, err := ccachetools.NewCCache(
testASRep.Ticket, testASRep.DecryptedEncPart.Key,
testASRep.DecryptedEncPart.SName, testASRep.CName, testASRep.CRealm,
testASRep.DecryptedEncPart.AuthTime, testASRep.DecryptedEncPart.StartTime,
testASRep.DecryptedEncPart.EndTime, testASRep.DecryptedEncPart.RenewTill)
if err != nil {
t.Fatalf("NewCCacheFromTGSRep: %v", err)
}
validateCCache(t, ccache)
}
func TestMarshalCCache(t *testing.T) {
t.Parallel()
ccache, err := ccachetools.NewCCacheFromASRep(testASRep)
if err != nil {
t.Fatalf("new CCache: %v", err)
}
ccacheBytes, err := ccachetools.MarshalCCache(ccache)
if err != nil {
t.Fatalf("marshal CCache: %v", err)
}
var parsedCCache credentials.CCache
err = parsedCCache.Unmarshal(ccacheBytes)
if err != nil {
t.Fatalf("unmarshal CCache: %v", err)
}
validateCCache(t, &parsedCCache)
}
func TestUnencryptedASRep(t *testing.T) {
t.Parallel()
_, err := ccachetools.NewCCacheFromASRep(messages.ASRep{
KDCRepFields: messages.KDCRepFields{
PVNO: 5,
MsgType: 11,
CRealm: "realm",
CName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "user"),
Ticket: messages.Ticket{
TktVNO: 5,
Realm: "realm",
SName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "krbtgt/redteam"),
DecryptedEncPart: messages.EncTicketPart{
Key: types.EncryptionKey{},
CName: types.NewPrincipalName(0, ""),
StartTime: time.Now(),
AuthTime: time.Now(),
EndTime: time.Now(),
RenewTill: time.Now(),
},
},
EncPart: types.EncryptedData{
EType: 18,
KVNO: 2,
Cipher: []byte{1, 2, 3},
},
},
})
if err == nil {
t.Fatalf("NewCCache did not fail for undecrypted CCache")
}
}
func TestUnencryptedTGSRep(t *testing.T) {
t.Parallel()
_, err := ccachetools.NewCCacheFromTGSRep(messages.TGSRep{
KDCRepFields: messages.KDCRepFields{
PVNO: 5,
MsgType: 11,
CRealm: "realm",
CName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "user"),
Ticket: messages.Ticket{
TktVNO: 5,
Realm: "realm",
SName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "krbtgt/redteam"),
DecryptedEncPart: messages.EncTicketPart{
Key: types.EncryptionKey{},
CName: types.NewPrincipalName(0, ""),
StartTime: time.Now(),
AuthTime: time.Now(),
EndTime: time.Now(),
RenewTill: time.Now(),
},
},
EncPart: types.EncryptedData{
EType: 18,
KVNO: 2,
Cipher: []byte{1, 2, 3},
},
},
})
if err == nil {
t.Fatalf("NewCCache did not fail for undecrypted CCache")
}
}
func validateCCache(t *testing.T, ccache *credentials.CCache) {
t.Helper()
if ccache.DefaultPrincipal.Realm != strings.ToUpper(testASRep.CRealm) {
t.Errorf("default principal realm %q does not match %q",
ccache.DefaultPrincipal.Realm, strings.ToUpper(testASRep.CRealm))
}
if ccache.DefaultPrincipal.PrincipalName.NameType != testASRep.CName.NameType {
t.Errorf("default principal name type %d does not match %d",
ccache.DefaultPrincipal.PrincipalName.NameType, testASRep.CName.NameType)
}
if !slices.Equal(ccache.DefaultPrincipal.PrincipalName.NameString, testASRep.CName.NameString) {
t.Errorf("default principal name string %v does not match %v",
ccache.DefaultPrincipal.PrincipalName.NameString, testASRep.CName.NameString)
}
if len(ccache.Credentials) != 1 {
t.Fatalf("found %d credentials instead of 1", len(ccache.Credentials))
}
cred := ccache.Credentials[0]
if cred.Key.KeyType != testASRep.DecryptedEncPart.Key.KeyType {
t.Errorf("key type %d does not match %d",
cred.Key.KeyType, testASRep.DecryptedEncPart.Key.KeyType)
}
if !bytes.Equal(cred.Key.KeyValue, testASRep.DecryptedEncPart.Key.KeyValue) {
t.Errorf("key value does not match")
}
if cred.Ticket == nil {
t.Errorf("ticket is empty")
}
if cred.Client.Realm != testASRep.CRealm {
t.Errorf("client realm %q does not match %s", cred.Client.Realm, testASRep.CRealm)
}
if cred.Client.PrincipalName.NameType != testASRep.CName.NameType {
t.Errorf("client name type %d does not match %d",
cred.Client.PrincipalName.NameType, testASRep.CName.NameType)
}
if !slices.Equal(cred.Client.PrincipalName.NameString, testASRep.CName.NameString) {
t.Errorf("client name string %v does not match %v",
cred.Client.PrincipalName.NameString, testASRep.CName.NameString)
}
if cred.Server.Realm != testASRep.CRealm {
t.Errorf("server realm %q does not match %s", cred.Server.Realm, testASRep.CRealm)
}
if cred.Server.PrincipalName.NameType != testASRep.CName.NameType {
t.Errorf("server name type %d does not match %d",
cred.Server.PrincipalName.NameType, testASRep.DecryptedEncPart.SName.NameType)
}
if !slices.Equal(cred.Server.PrincipalName.NameString, testASRep.DecryptedEncPart.SName.NameString) {
t.Errorf("server name string %v does not match %v",
cred.Server.PrincipalName.NameString, testASRep.DecryptedEncPart.SName.NameString)
}
if cred.AuthTime.Unix() != testASRep.DecryptedEncPart.AuthTime.Unix() {
t.Errorf("auth time %s does not match %s",
cred.AuthTime, testASRep.DecryptedEncPart.AuthTime)
}
if cred.StartTime.Unix() != testASRep.DecryptedEncPart.StartTime.Unix() {
t.Errorf("start time %s does not match %s",
cred.StartTime, testASRep.DecryptedEncPart.StartTime)
}
if cred.EndTime.Unix() != testASRep.DecryptedEncPart.EndTime.Unix() {
t.Errorf("end time %s does not match %s",
cred.EndTime, testASRep.DecryptedEncPart.EndTime)
}
if cred.RenewTill.Unix() != testASRep.DecryptedEncPart.RenewTill.Unix() {
t.Errorf("renew time %s does not match %s",
cred.RenewTill, testASRep.DecryptedEncPart.RenewTill)
}
}
+348
View File
@@ -0,0 +1,348 @@
package adauth
import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"fmt"
"net"
"os"
"strings"
"time"
"github.com/RedTeamPentesting/adauth/othername"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/iana/etypeID"
"github.com/jcmturner/gokrb5/v8/keytab"
"software.sslmate.com/src/go-pkcs12"
)
// Credential represents Active Directory credentials.
type Credential struct {
// Username is the username without the domain.
Username string
// Password contains the users cleartext password if available.
Password string
// Domain holds the user's domain.
Domain string
// NTHash holds the user's NT hash or Kerberos RC4 key if available.
NTHash string
// AESKey holds the user's Kerberos AES128 or AES256 key if available.
AESKey string
// CCache contains the path to the user's CCache file.
CCache string
// ClientCert holds a client certificate for Kerberos or LDAP authentication if available.
ClientCert *x509.Certificate
// ClientCertKey holds the private key that corresponds to ClientCert.
ClientCertKey *rsa.PrivateKey
// CACerts holds CA certificates that were loaded alongside the ClientCert.
CACerts []*x509.Certificate
dc string
// PasswordIsEmptyString is true when an empty Password field should not be
// interpreted as a missing password but as a password that happens to be
// empty.
PasswordIsEmtpyString bool
// CCacheIsFromEnv indicates whether the CCache was set explicitly or
// implicitly through an environment variable.
CCacheIsFromEnv bool
// Resolver can be used to set an alternative DNS resolver. If empty,
// net.DefaultResolver is used.
Resolver Resolver
}
// CredentialFromPFX creates a Credential structure for certificate-based
// authentication based on a PFX file.
func CredentialFromPFX(
username string, domain string, pfxFile string, pfxPassword string,
) (*Credential, error) {
pfxData, err := os.ReadFile(pfxFile)
if err != nil {
return nil, fmt.Errorf("read PFX: %w", err)
}
return CredentialFromPFXBytes(username, domain, pfxData, pfxPassword)
}
// CredentialFromPFX creates a Credential structure for certificate-based
// authentication based on PFX data.
func CredentialFromPFXBytes(
username string, domain string, pfxData []byte, pfxPassword string,
) (*Credential, error) {
cred := &Credential{
Username: username,
Domain: domain,
}
key, cert, caCerts, err := pkcs12.DecodeChain(pfxData, pfxPassword)
if err != nil {
return nil, fmt.Errorf("decode PFX: %w", err)
}
rsaKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("PFX key is not an RSA private key but %T", rsaKey)
}
cred.ClientCert = cert
cred.ClientCertKey = rsaKey
cred.CACerts = caCerts
user, domain, err := othername.UserAndDomain(cert)
if err == nil {
if cred.Username == "" {
cred.Username = user
}
if cred.Domain == "" {
cred.Domain = domain
}
}
return cred, nil
}
// UPN is the user principal name (username@domain).
func (c *Credential) UPN() string {
return c.Username + "@" + c.Domain
}
// LogonName is the legacy logon name (domain\username).
func (c *Credential) LogonName() string {
return c.Domain + `\` + c.Username
}
// LogonNameWithUpperCaseDomain is like LogonName with the domain capitalized
// for compatibility with the Kerberos library (DOMAIN\username).
func (c *Credential) LogonNameWithUpperCaseDomain() string {
return strings.ToUpper(c.Domain) + `\` + c.Username
}
// ImpacketLogonName is the Impacket-style logon name (domain/username).
func (c *Credential) ImpacketLogonName() string {
return c.Domain + "/" + c.Username
}
// SetDC configures a specific domain controller for this credential.
func (c *Credential) SetDC(dc string) {
c.dc = dc
}
// DC returns the domain controller of the credential's domain as a target.
func (c *Credential) DC(ctx context.Context, protocol string) (*Target, error) {
if c.dc != "" {
return newTarget(protocol, c.dc, true, c.CCache, c.Resolver), nil
}
if c.Domain == "" {
return nil, fmt.Errorf("domain unknown")
}
_, addrs, err := ensureResolver(c.Resolver, nil).LookupSRV(ctx, "kerberos", "tcp", c.Domain)
if err != nil {
return nil, fmt.Errorf("lookup %q service of domain %q: %w", "kerberos", c.Domain, err)
}
if len(addrs) == 0 {
return nil, fmt.Errorf("no %q services were discovered for domain %q", "kerberos", c.Domain)
}
return newTarget(protocol, strings.TrimRight(addrs[0].Target, "."), true, c.CCache, c.Resolver), nil
}
func (c *Credential) mustUseKerberos() bool {
return c.Password == "" && c.NTHash == "" && (c.CCache != "" || c.AESKey != "" || c.ClientCert != nil)
}
// Keytab returns the Kerberos keytab containing the AES key and/or NT hash if
// they were supplied. If a password is supplied, the keys/hashes are not
// derived and the keytab will be empty.
func (c *Credential) Keytab() (*keytab.Keytab, error) {
kt := newKeytab()
if c.AESKey != "" {
err := addKeyToKeytab(kt, c.Username, c.Domain, c.AESKey, true, 1)
if err != nil {
return nil, fmt.Errorf("add AES key: %w", err)
}
}
if c.NTHash != "" {
err := addKeyToKeytab(kt, c.Username, c.Domain, c.NTHash, false, 1)
if err != nil {
return nil, fmt.Errorf("add RC4 key: %w", err)
}
}
return kt, nil
}
// KerberosConfig returns the Kerberos configuration for the credential's domain.
func (c *Credential) KerberosConfig(ctx context.Context) (*config.Config, error) {
dc, err := c.DC(ctx, "krbtgt")
if err != nil {
return nil, fmt.Errorf("find DC: %w", err)
}
krbConf := config.New()
krbConf.LibDefaults.DefaultRealm = strings.ToUpper(c.Domain)
krbConf.LibDefaults.AllowWeakCrypto = true
krbConf.LibDefaults.DNSLookupRealm = false
krbConf.LibDefaults.DNSLookupKDC = false
krbConf.LibDefaults.TicketLifetime = time.Duration(24) * time.Hour
krbConf.LibDefaults.RenewLifetime = time.Duration(24*7) * time.Hour
krbConf.LibDefaults.Forwardable = true
krbConf.LibDefaults.Proxiable = true
krbConf.LibDefaults.RDNS = false
krbConf.LibDefaults.UDPPreferenceLimit = 1 // Force use of tcp
if c.NTHash != "" {
// use RC4 for pre-auth but AES256 for ephemeral keys, otherwise we get
// unsupported GSSAPI tokens during LDAP SASL handshake
krbConf.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.AES256_CTS_HMAC_SHA1_96}
krbConf.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.RC4_HMAC}
krbConf.LibDefaults.PermittedEnctypeIDs = []int32{etypeID.AES256_CTS_HMAC_SHA1_96}
krbConf.LibDefaults.PreferredPreauthTypes = []int{int(etypeID.RC4_HMAC)}
} else {
krbConf.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.AES256_CTS_HMAC_SHA1_96}
krbConf.LibDefaults.DefaultTktEnctypeIDs = []int32{
etypeID.AES256_CTS_HMAC_SHA1_96, etypeID.AES128_CTS_HMAC_SHA1_96, etypeID.RC4_HMAC,
}
krbConf.LibDefaults.PermittedEnctypeIDs = []int32{etypeID.AES256_CTS_HMAC_SHA1_96}
krbConf.LibDefaults.PreferredPreauthTypes = []int{
int(etypeID.AES256_CTS_HMAC_SHA1_96), int(etypeID.AES128_CTS_HMAC_SHA1_96), int(etypeID.RC4_HMAC),
}
}
krbConf.Realms = []config.Realm{
{
Realm: strings.ToUpper(c.Domain),
DefaultDomain: strings.ToUpper(c.Domain),
AdminServer: []string{dc.AddressWithoutPort()},
KDC: []string{net.JoinHostPort(dc.AddressWithoutPort(), "88")},
KPasswdServer: []string{net.JoinHostPort(dc.AddressWithoutPort(), "464")},
MasterKDC: []string{dc.AddressWithoutPort()},
},
{
Realm: c.Domain,
DefaultDomain: c.Domain,
AdminServer: []string{dc.AddressWithoutPort()},
KDC: []string{net.JoinHostPort(dc.AddressWithoutPort(), "88")},
KPasswdServer: []string{net.JoinHostPort(dc.AddressWithoutPort(), "464")},
MasterKDC: []string{dc.AddressWithoutPort()},
},
}
krbConf.DomainRealm = map[string]string{
"." + c.Domain: strings.ToUpper(c.Domain),
c.Domain: strings.ToUpper(c.Domain),
}
return krbConf, nil
}
func splitUserIntoDomainAndUsername(user string) (domain string, username string) {
switch {
case strings.Contains(user, "@"):
parts := strings.Split(user, "@")
if len(parts) == 2 {
return parts[1], parts[0]
}
return "", user
case strings.Contains(user, `\`):
parts := strings.Split(user, `\`)
if len(parts) == 2 {
return parts[0], parts[1]
}
return "", user
case strings.Contains(user, "/"):
parts := strings.Split(user, "/")
if len(parts) == 2 {
return parts[0], parts[1]
}
return "", user
default:
return "", user
}
}
func newKeytab() *keytab.Keytab {
kt := &keytab.Keytab{}
err := kt.Unmarshal([]byte{
// header.
0x05, // first-byte
0x02, // version
0x00, 0x00, 0x00, 0x00, // entry-length
})
if err != nil {
panic(err.Error())
}
return kt
}
func addKeyToKeytab(kt *keytab.Keytab, username string, domain string, key string, aes bool, kvno uint32) error {
keyBytes, err := hex.DecodeString(key)
if err != nil {
return fmt.Errorf("decode AES key: %w", err)
}
var keyType int32
switch len(keyBytes) {
case 32:
keyType = etypeID.AES256_CTS_HMAC_SHA1_96
case 16:
if aes {
keyType = etypeID.AES128_CTS_HMAC_SHA1_96
} else {
keyType = etypeID.RC4_HMAC
}
default:
return fmt.Errorf("invalid AES128/AES256 key")
}
tmp := &keytab.Keytab{}
err = tmp.Unmarshal([]byte{
// header
0x05, // first-byte
0x02, // version
0x00, 0x00, 0x00, 0x11, // entry-length
// principal
0x00, 0x00, // num components
0x00, 0x00, // realm length
0x00, 0x00, 0x00, 0x00, // name type
// key
0x00, 0x00, 0x00, 0x00, // timestamp
0x00, // kvno8
0x00, 0x00, // key type
0x00, 0x00, // key length
})
if err != nil {
return fmt.Errorf("invalid dummy data: %w", err)
}
e := tmp.Entries[0]
krbCreds := credentials.New(username, domain)
e.Principal.NumComponents = int16(len(krbCreds.CName().NameString))
e.Principal.Components = krbCreds.CName().NameString
e.Principal.Realm = strings.ToUpper(krbCreds.Realm())
e.Principal.NameType = krbCreds.CName().NameType
e.Timestamp = time.Now()
e.KVNO8 = 0
e.Key.KeyType = keyType
e.Key.KeyValue = keyBytes
e.KVNO = kvno
kt.Entries = append(kt.Entries, e)
return nil
}
+193
View File
@@ -0,0 +1,193 @@
package adauth_test
import (
"context"
"encoding/hex"
"net"
"strconv"
"strings"
"testing"
"github.com/RedTeamPentesting/adauth"
"github.com/jcmturner/gokrb5/v8/iana/etypeID"
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/types"
)
func TestKeytab(t *testing.T) {
expectedNTHash := hex.EncodeToString(make([]byte, 16))
expectedAES256Key := hex.EncodeToString(make([]byte, 32))
expectedAES128Key := hex.EncodeToString(make([]byte, 16))
principal := types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, testUser)
testCases := []struct {
Cred adauth.Credential
ShouldHaveRC4Key bool
ShouldHaveAES128Key bool
ShouldHaveAES256Key bool
}{
{
Cred: adauth.Credential{},
ShouldHaveRC4Key: false,
ShouldHaveAES128Key: false,
ShouldHaveAES256Key: false,
},
{
Cred: adauth.Credential{
NTHash: expectedNTHash,
},
ShouldHaveRC4Key: true,
ShouldHaveAES128Key: false,
ShouldHaveAES256Key: false,
},
{
Cred: adauth.Credential{
NTHash: expectedNTHash,
AESKey: expectedAES128Key,
},
ShouldHaveRC4Key: true,
ShouldHaveAES128Key: true,
ShouldHaveAES256Key: false,
},
{
Cred: adauth.Credential{
NTHash: expectedNTHash,
AESKey: expectedAES256Key,
},
ShouldHaveRC4Key: true,
ShouldHaveAES128Key: false,
ShouldHaveAES256Key: true,
},
{
Cred: adauth.Credential{
AESKey: expectedAES128Key,
},
ShouldHaveRC4Key: false,
ShouldHaveAES128Key: true,
ShouldHaveAES256Key: false,
},
{
Cred: adauth.Credential{
AESKey: expectedAES256Key,
},
ShouldHaveRC4Key: false,
ShouldHaveAES128Key: false,
ShouldHaveAES256Key: true,
},
}
for i, testCase := range testCases {
testCase := testCase
t.Run(strconv.Itoa(i), func(t *testing.T) {
testCase.Cred.Username = testUser
testCase.Cred.Domain = testDomain
keyTab, err := testCase.Cred.Keytab()
if err != nil {
t.Fatalf("create keytab: %v", err)
}
rc4Key, _, rc4Err := keyTab.GetEncryptionKey(principal, strings.ToUpper(testDomain), 0, etypeID.RC4_HMAC)
switch {
case testCase.ShouldHaveRC4Key && rc4Err != nil:
t.Errorf("expected RC4 key but got error: %v", rc4Err)
case testCase.ShouldHaveRC4Key && len(rc4Key.KeyValue) != 16:
t.Errorf("RC4 key has %d bytes instead of %d", len(rc4Key.KeyValue), 16)
case testCase.ShouldHaveRC4Key && rc4Key.KeyType != etypeID.RC4_HMAC:
t.Errorf("RC4 key type is %d instead of %d", rc4Key.KeyType, etypeID.RC4_HMAC)
case !testCase.ShouldHaveRC4Key && (rc4Err == nil || len(rc4Key.KeyValue) > 0):
t.Errorf("RC4 key should not exist")
}
aes128Key, _, aes128Err := keyTab.GetEncryptionKey(
principal, strings.ToUpper(testDomain), 0, etypeID.AES128_CTS_HMAC_SHA1_96)
switch {
case testCase.ShouldHaveAES128Key && aes128Err != nil:
t.Errorf("expected AES128 key but got error: %v:\n%#v\n", aes128Err, keyTab.Entries)
case testCase.ShouldHaveAES128Key && len(aes128Key.KeyValue) != 16:
t.Errorf("AES128 key has %d bytes instead of %d", len(aes128Key.KeyValue), 16)
case testCase.ShouldHaveAES128Key && aes128Key.KeyType != etypeID.AES128_CTS_HMAC_SHA1_96:
t.Errorf("AES128 key type is %d instead of %d", aes128Key.KeyType, etypeID.AES128_CTS_HMAC_SHA1_96)
case !testCase.ShouldHaveAES128Key && (aes128Err == nil || len(aes128Key.KeyValue) > 0):
t.Errorf("AES128 key should not exist")
}
aes256Key, _, aes256Err := keyTab.GetEncryptionKey(
principal, strings.ToUpper(testDomain), 0, etypeID.AES256_CTS_HMAC_SHA1_96)
switch {
case testCase.ShouldHaveAES256Key && aes256Err != nil:
t.Errorf("expected AES256 key but got error: %v", aes256Err)
case testCase.ShouldHaveAES256Key && len(aes256Key.KeyValue) != 32:
t.Errorf("AES256 key has %d bytes instead of %d", len(aes256Key.KeyValue), 32)
case testCase.ShouldHaveAES256Key && aes256Key.KeyType != etypeID.AES256_CTS_HMAC_SHA1_96:
t.Errorf("AES256 key type is %d instead of %d", aes256Key.KeyType, etypeID.AES256_CTS_HMAC_SHA1_96)
case !testCase.ShouldHaveAES256Key && (aes256Err == nil || len(aes256Key.KeyValue) > 0):
t.Errorf("AES256 key should not exist")
}
})
}
}
func TestSetDC(t *testing.T) {
creds := adauth.Credential{
Username: testUser,
Domain: testDomain,
Resolver: &testResolver{},
}
_, err := creds.DC(context.Background(), "host")
if err == nil {
t.Fatalf("expected creds.DC() to fail initially")
}
dcHostname := "dc." + testDomain
creds.SetDC(dcHostname)
dc, err := creds.DC(context.Background(), "host")
if err != nil {
t.Fatalf("get DC: %v", err)
}
if dc.Address() != dcHostname {
t.Fatalf("DC address is %q instead of %q", dc.Address(), dcHostname)
}
}
func TestLookupDC(t *testing.T) {
dcHostname := "dc." + testDomain
creds := adauth.Credential{
Username: testUser,
Domain: testDomain,
Resolver: &testResolver{
SRV: map[string]map[string]map[string]struct {
Name string
SRV []*net.SRV
}{
"kerberos": {
"tcp": {
testDomain: {
Name: dcHostname,
SRV: []*net.SRV{
{Target: dcHostname, Port: 88},
},
},
},
},
},
},
}
dc, err := creds.DC(context.Background(), "host")
if err != nil {
t.Fatalf("get DC: %v", err)
}
if dc.AddressWithoutPort() != dcHostname {
t.Fatalf("DC address is %q instead of %q", dc.Address(), dcHostname)
}
}
+189
View File
@@ -0,0 +1,189 @@
package dcerpcauth
import (
"context"
"fmt"
"strings"
"github.com/RedTeamPentesting/adauth"
"github.com/RedTeamPentesting/adauth/pkinit"
"github.com/jcmturner/gokrb5/v8/keytab"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/smb2"
"github.com/oiweiwei/go-msrpc/ssp"
"github.com/oiweiwei/go-msrpc/ssp/credential"
"github.com/oiweiwei/go-msrpc/ssp/gssapi"
"github.com/oiweiwei/go-msrpc/ssp/krb5"
)
// Options holds options that modify the behavior of the AuthenticationOptions
// function.
type Options struct {
// SMBOptions holds options for the SMB dialer. This dialer is only used
// with the named pipe transport. If SMBOptions is nil, sealing will be
// enabled for the smb dialer, specify an empty slice to disable this
// default.
SMBOptions []smb2.DialerOption
// PKINITOptions can be used to modify the Kerberos PKINIT behavior.
PKINITOptions []pkinit.Option
// Debug can be set to enable debug output, for example with
// adauth.NewDebugFunc(...).
Debug func(string, ...any)
}
func (opts *Options) debug(format string, a ...any) {
if opts == nil || opts.Debug == nil {
return
}
opts.Debug(format, a...)
}
// AuthenticationOptions returns dcerpc.Options for dcerpc.Dial or for
// constructing an DCERPC API client. It is possible to configure the SMB,
// PKINIT and debug behavior using the optional upstreamOptions argument.
func AuthenticationOptions(
ctx context.Context, creds *adauth.Credential, target *adauth.Target,
upstreamOptions *Options,
) (dcerpcOptions []dcerpc.Option, err error) {
if upstreamOptions == nil {
upstreamOptions = &Options{}
}
dcerpcCredentials, err := DCERPCCredentials(ctx, creds, upstreamOptions)
if err != nil {
return nil, err
}
dcerpcOptions = append(dcerpcOptions, dcerpc.WithMechanism(ssp.SPNEGO))
switch {
case target.UseKerberos:
spn, err := target.SPN(ctx)
if err != nil {
return nil, fmt.Errorf("build SPN: %w", err)
}
upstreamOptions.debug("Using Kerberos with SPN %q", spn)
krbConf, err := creds.KerberosConfig(ctx)
if err != nil {
return nil, fmt.Errorf("generate Kerberos config: %w", err)
}
smbOptions := upstreamOptions.SMBOptions
if smbOptions == nil {
smbOptions = append(smbOptions, smb2.WithSeal())
}
smbOptions = append(smbOptions, smb2.WithSecurity(
gssapi.WithTargetName(spn),
gssapi.WithCredential(dcerpcCredentials),
gssapi.WithMechanismFactory(ssp.KRB5, &krb5.Config{
KRB5Config: krbConf,
CCachePath: creds.CCache,
DisablePAFXFAST: true,
DCEStyle: true,
}),
))
dcerpcOptions = append(dcerpcOptions,
dcerpc.WithTargetName(spn),
dcerpc.WithMechanism(ssp.KRB5),
dcerpc.WithSecurityConfig(&krb5.Config{
KRB5Config: krbConf,
CCachePath: creds.CCache,
DisablePAFXFAST: true,
DCEStyle: true,
}),
dcerpc.WithSMBDialer(smb2.NewDialer(smbOptions...)),
)
default:
upstreamOptions.debug("Using NTLM")
dcerpcOptions = append(dcerpcOptions,
dcerpc.WithMechanism(ssp.NTLM),
)
spn, err := target.SPN(ctx)
if err == nil {
dcerpcOptions = append(dcerpcOptions, dcerpc.WithTargetName(spn))
}
}
dcerpcOptions = append(dcerpcOptions, dcerpc.WithCredentials(dcerpcCredentials))
return dcerpcOptions, nil
}
func DCERPCCredentials(ctx context.Context, creds *adauth.Credential, options *Options) (credential.Credential, error) {
switch {
case creds.Password != "":
options.debug("Authenticating with password")
return credential.NewFromPassword(creds.LogonNameWithUpperCaseDomain(), creds.Password), nil
case creds.AESKey != "":
options.debug("Authenticating with AES key")
keyTab, err := creds.Keytab()
if err != nil {
return nil, fmt.Errorf("create keytab: %w", err)
}
return &keytabCredentials{username: creds.Username, domain: creds.Domain, keytab: keyTab}, nil
case creds.NTHash != "":
options.debug("Authenticating with NT hash")
return credential.NewFromNTHash(creds.LogonNameWithUpperCaseDomain(), creds.NTHash), nil
case creds.PasswordIsEmtpyString:
options.debug("Authenticating with empty password")
return credential.NewFromPassword(strings.ToUpper(creds.Domain)+`\`+creds.Username, ""), nil
case creds.ClientCert != nil:
options.debug("Authenticating with client certificate (PKINIT)")
krbConf, err := creds.KerberosConfig(ctx)
if err != nil {
return nil, fmt.Errorf("generate kerberos config: %w", err)
}
ccache, err := pkinit.Authenticate(ctx, creds.Username, strings.ToUpper(creds.Domain),
creds.ClientCert, creds.ClientCertKey, krbConf, options.PKINITOptions...)
if err != nil {
return nil, fmt.Errorf("PKINIT: %w", err)
}
return credential.NewFromCCache(creds.LogonNameWithUpperCaseDomain(), ccache), nil
case creds.CCache != "":
options.debug("Authenticating with ccache")
return credential.NewFromPassword(creds.LogonNameWithUpperCaseDomain(), ""), nil
default:
return nil, fmt.Errorf("no credentials available")
}
}
type keytabCredentials struct {
keytab *keytab.Keytab
username string
domain string
}
var _ credential.Keytab = &keytabCredentials{}
func (ktc *keytabCredentials) DomainName() string {
return strings.ToUpper(ktc.domain)
}
func (ctc *keytabCredentials) Workstation() string {
return ""
}
func (ctc *keytabCredentials) UserName() string {
return ctc.username
}
func (ctc *keytabCredentials) Keytab() *keytab.Keytab {
return ctc.keytab
}
+115
View File
@@ -0,0 +1,115 @@
package main
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"github.com/RedTeamPentesting/adauth"
"github.com/RedTeamPentesting/adauth/dcerpcauth"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/msrpc/dtyp"
"github.com/oiweiwei/go-msrpc/msrpc/epm/epm/v3"
"github.com/oiweiwei/go-msrpc/msrpc/samr/samr/v1"
"github.com/oiweiwei/go-msrpc/ssp/gssapi"
"github.com/spf13/pflag"
)
func run() error {
var (
debug bool
authOpts = &adauth.Options{
Debug: adauth.NewDebugFunc(&debug, os.Stderr, true),
}
dcerpcauthOpts = &dcerpcauth.Options{
Debug: authOpts.Debug,
}
namedPipe bool
)
pflag.CommandLine.BoolVar(&debug, "debug", false, "Enable debug output")
pflag.CommandLine.BoolVar(&namedPipe, "named-pipe", false, "Use named pipe (SMB) as transport")
authOpts.RegisterFlags(pflag.CommandLine)
pflag.Parse()
if len(pflag.Args()) != 1 {
return fmt.Errorf("usage: %s [options] <target>", binaryName())
}
creds, target, err := authOpts.WithTarget(context.Background(), "host", pflag.Arg(0))
if err != nil {
return err
}
ctx := gssapi.NewSecurityContext(context.Background())
dcerpcOpts, err := dcerpcauth.AuthenticationOptions(ctx, creds, target, dcerpcauthOpts)
if err != nil {
return err
}
dcerpcOpts = append(dcerpcOpts, epm.EndpointMapper(ctx,
net.JoinHostPort(target.AddressWithoutPort(), "135"),
dcerpc.WithInsecure(),
))
proto := "ncacn_ip_tcp:"
if namedPipe {
proto = "ncacn_np:"
}
conn, err := dcerpc.Dial(ctx, proto+target.Address(), dcerpcOpts...)
if err != nil {
return fmt.Errorf("dial DCERPC: %w", err)
}
defer conn.Close(ctx) //nolint:errcheck
samrClient, err := samr.NewSamrClient(ctx, conn, dcerpc.WithSeal())
if err != nil {
return fmt.Errorf("create SAMR client: %w", err)
}
connectResponse, err := samrClient.Connect(ctx, &samr.ConnectRequest{DesiredAccess: 0x02000000})
if err != nil {
return fmt.Errorf("SAMR connect: %w", err)
}
lookupDomainResponse, err := samrClient.LookupDomainInSAMServer(ctx, &samr.LookupDomainInSAMServerRequest{
Server: connectResponse.Server,
Name: &dtyp.UnicodeString{
Buffer: creds.Domain,
},
})
if err != nil {
return fmt.Errorf("SAMR lookup domain %q: %w", creds.Domain, err)
}
fmt.Println("Domain SID:", lookupDomainResponse.DomainID)
return nil
}
func binaryName() string {
executable, err := os.Executable()
if err == nil {
return filepath.Base(executable)
}
if len(os.Args) > 0 {
return filepath.Base(os.Args[0])
}
return "msrpc"
}
func main() {
err := run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"context"
"fmt"
"os"
"github.com/RedTeamPentesting/adauth"
"github.com/RedTeamPentesting/adauth/ldapauth"
"github.com/spf13/pflag"
)
func run() error {
var (
debug bool
authOpts = &adauth.Options{
Debug: adauth.NewDebugFunc(&debug, os.Stderr, true),
}
ldapOpts = &ldapauth.Options{
Debug: adauth.NewDebugFunc(&debug, os.Stderr, true),
}
)
pflag.CommandLine.BoolVar(&debug, "debug", false, "Enable debug output")
authOpts.RegisterFlags(pflag.CommandLine)
ldapOpts.RegisterFlags(pflag.CommandLine)
pflag.Parse()
conn, err := ldapauth.Connect(context.Background(), authOpts, ldapOpts)
if err != nil {
return fmt.Errorf("%s connect: %w", ldapOpts.Scheme, err)
}
defer conn.Close() //nolint:errcheck
res, err := conn.WhoAmI(nil)
if err != nil {
return fmt.Errorf("whoami: %w", err)
}
fmt.Println("whoami:", res.AuthzID)
return nil
}
func main() {
err := run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"context"
"fmt"
"os"
"github.com/RedTeamPentesting/adauth/ccachetools"
"github.com/RedTeamPentesting/adauth/pkinit"
"github.com/spf13/pflag"
)
func run() error {
var (
username string
domain string
pfxFile string
pfxPassword string
ccacheName string
dc string
)
pflag.StringVarP(&username, "username", "u", "", "Username (overrides UPN in PFX)")
pflag.StringVarP(&domain, "domain", "d", "", "Domain (overrides UPN in PFX)")
pflag.StringVar(&pfxFile, "pfx", "", "PFX file")
pflag.StringVarP(&pfxPassword, "pfx-password", "p", "", "PFX file password")
pflag.StringVar(&ccacheName, "cache", "", "CCache output file name")
pflag.StringVar(&dc, "dc", "", "Domain controller (optional)")
pflag.Parse()
ccache, hash, err := pkinit.UnPACTheHashFromPFX(context.Background(), username, domain, pfxFile, pfxPassword, dc)
if err != nil {
return fmt.Errorf("UnPAC-the-Hash: %w", err)
}
fmt.Println("Authentication was successful")
fmt.Printf("%s\\%s: %s\n", ccache.DefaultPrincipal.Realm,
ccache.DefaultPrincipal.PrincipalName.PrincipalNameString(), hash.Combined())
if ccacheName != "" {
ccacheBytes, err := ccachetools.MarshalCCache(ccache)
if err != nil {
return fmt.Errorf("marshal CCache: %w", err)
}
err = os.WriteFile(ccacheName, ccacheBytes, 0o600)
if err != nil {
return fmt.Errorf("write CCache: %w", err)
}
fmt.Println("Saved CCache at", ccacheName)
}
return nil
}
func main() {
err := run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
+35
View File
@@ -0,0 +1,35 @@
module github.com/RedTeamPentesting/adauth
go 1.23.3
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
github.com/go-ldap/ldap/v3 v3.4.11-0.20250110131057-5d1b644709df
github.com/jcmturner/gokrb5/v8 v8.4.4
github.com/oiweiwei/go-msrpc v1.1.1
github.com/spf13/pflag v1.0.5
github.com/vadimi/go-ntlm v1.2.1
software.sslmate.com/src/go-pkcs12 v0.5.0
)
require (
github.com/geoffgarside/ber v1.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hirochachacha/go-smb2 v1.1.0 // 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/gofork v1.7.6 // indirect
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/oiweiwei/go-smb2.fork v0.0.0-20241220141821-a5dd82e09d5f // indirect
github.com/rs/zerolog v1.33.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
)
+157
View File
@@ -0,0 +1,157 @@
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w=
github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.11-0.20250110131057-5d1b644709df h1:d8rpIi5VcFIXam3EpxsxJa8vUQl0C+D16xdY86XdtpE=
github.com/go-ldap/ldap/v3 v3.4.11-0.20250110131057-5d1b644709df/go.mod h1:WfOSWXTbpVbGgruuVrZQzPR810wu/qBWTzYA5FZA+Es=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
github.com/indece-official/go-ebcdic v1.2.0 h1:nKCubkNoXrGvBp3MSYuplOQnhANCDEY512Ry5Mwr4a0=
github.com/indece-official/go-ebcdic v1.2.0/go.mod h1:RBddVJt0Ks0eDLRG5dhPwBDRiTNA7n+yv0dVFpSs46Q=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/oiweiwei/go-msrpc v1.1.1 h1:QhWTLSFFC+2weuymZsjsHmY/LaaVg3fdTmyn/2ZQHLM=
github.com/oiweiwei/go-msrpc v1.1.1/go.mod h1:AiQkqNul+mAkvjlI8lla2q7aJCgQ47+PnJubD/d5Tak=
github.com/oiweiwei/go-smb2.fork v0.0.0-20241220141821-a5dd82e09d5f h1:IETUC4J3re2E3/4on55u4eKiD66+sd92sQkEwjWMIsU=
github.com/oiweiwei/go-smb2.fork v0.0.0-20241220141821-a5dd82e09d5f/go.mod h1:h0CzLVvGAmq39izdYVHKyI5cLv6aHdbQAMKEe4dz4N8=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/vadimi/go-ntlm v1.2.1 h1:y2xZf/a5+BJlYNJIIulP1q8F438H9bU7aGcYE53vghQ=
github.com/vadimi/go-ntlm v1.2.1/go.mod h1:hPTY60eLSKGj9oUJAB+kZiLs2Cg5eKdH60aLczM9rMg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M=
software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
+366
View File
@@ -0,0 +1,366 @@
package ldapauth
import (
"bytes"
"context"
"crypto/rsa"
"crypto/x509"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"strings"
"github.com/RedTeamPentesting/adauth/pkinit"
"github.com/jcmturner/gokrb5/v8/client"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/iana/chksumtype"
"github.com/jcmturner/gokrb5/v8/iana/etypeID"
"github.com/jcmturner/gokrb5/v8/iana/flags"
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/krberror"
"github.com/jcmturner/gokrb5/v8/types"
krb5GSSAPI "github.com/jcmturner/gokrb5/v8/gssapi"
"github.com/jcmturner/gokrb5/v8/spnego"
krb5Crypto "github.com/jcmturner/gokrb5/v8/crypto"
"github.com/jcmturner/gokrb5/v8/iana/keyusage"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/credentials"
)
type gssapiClient struct {
*client.Client
ccache *credentials.CCache
ekey types.EncryptionKey
Subkey types.EncryptionKey
BindCertificate *x509.Certificate
}
func newClientFromCCache(
username string, domain string, ccachePath string, krb5Conf *config.Config,
) (*gssapiClient, error) {
ccache, err := credentials.LoadCCache(ccachePath)
if err != nil {
return nil, err
}
c, err := client.NewFromCCache(ccache, krb5Conf, client.DisablePAFXFAST(true))
if err != nil && strings.Contains(strings.ToLower(err.Error()), "tgt not found") {
// client.NewFromCCache only accepts ccaches that contain at least one
// TGT, however, we want to support ccaches that only contain a service
// ticket. Therefore, we use a dummy client, and pull the service ticket
// from the ccache ourselves instead of asking the client.
return &gssapiClient{
Client: client.NewWithPassword(username, domain, "", krb5Conf, client.DisablePAFXFAST(true)),
ccache: ccache,
}, nil
}
if err != nil {
return nil, err
}
return &gssapiClient{Client: c}, nil
}
func newPKINITClient(
ctx context.Context, username string, domain string, cert *x509.Certificate, key *rsa.PrivateKey,
krb5Conf *config.Config, opts ...pkinit.Option,
) (*gssapiClient, error) {
ccache, err := pkinit.Authenticate(ctx, username, domain, cert, key, krb5Conf, opts...)
if err != nil {
return nil, fmt.Errorf("pkinit: %w", err)
}
c, err := client.NewFromCCache(ccache, krb5Conf, client.DisablePAFXFAST(true))
if err != nil {
return nil, fmt.Errorf("initialize Kerberos client from PKINIT ccache: %w", err)
}
return &gssapiClient{Client: c}, nil
}
// Close deletes any established secure context and closes the client.
func (client *gssapiClient) Close() error {
client.Client.Destroy()
return nil
}
// DeleteSecContext destroys any established secure context.
func (client *gssapiClient) DeleteSecContext() error {
client.ekey = types.EncryptionKey{}
client.Subkey = types.EncryptionKey{}
return nil
}
func (client *gssapiClient) getServiceTicket(target string) (tkt messages.Ticket, key types.EncryptionKey, err error) {
// ask our own copy of the ccache for a suitable service ticket before asking the client
if client.ccache != nil {
entry, ok := client.ccache.GetEntry(types.NewPrincipalName(nametype.KRB_NT_SRV_INST, target))
if !ok {
return tkt, key, fmt.Errorf("CCACHE does not contain service ticket for %q", target)
}
if entry.Key.KeyType == etypeID.RC4_HMAC {
return tkt, key, fmt.Errorf("RC4 tickets from ccache are currently not supported " +
"(see https://github.com/jcmturner/gokrb5/pull/498), but you should be able " +
"to request an AES256 ticket instead (even with NT hash)")
}
return tkt, entry.Key, tkt.Unmarshal(entry.Ticket)
}
return client.Client.GetServiceTicket(target)
}
func (client *gssapiClient) newKRB5TokenAPREQ(
tkt messages.Ticket, ekey types.EncryptionKey,
) (*spnego.KRB5Token, error) {
gssapiFlags := []int{krb5GSSAPI.ContextFlagInteg, krb5GSSAPI.ContextFlagConf, krb5GSSAPI.ContextFlagMutual}
// this actually does nothing important, we simply use it to obtain a dummy
// KRB5Token with tokID set, which unfortunately is private, so we cannot
// initialize it ourselves.
token, err := spnego.NewKRB5TokenAPREQ(client.Client, tkt, ekey, gssapiFlags, []int{flags.APOptionMutualRequired})
if err != nil {
return nil, err
}
// build a custom authenticator that supports channel binding
authenticator, err := krb5TokenAuthenticator(
client.Credentials.Realm(), client.Credentials.CName(), client.BindCertificate, gssapiFlags)
if err != nil {
return nil, fmt.Errorf("create authenticator: %w", err)
}
APReq, err := messages.NewAPReq(
tkt,
ekey,
authenticator,
)
if err != nil {
return nil, err
}
types.SetFlag(&APReq.APOptions, flags.APOptionMutualRequired)
// put the APReq with custom authenticator into the dummy RKB5Token
token.APReq = APReq
return &token, nil
}
// InitSecContext initiates the establishment of a security context for
// GSS-API between the client and server.
// See RFC 4752 section 3.1.
func (client *gssapiClient) InitSecContext(target string, input []byte) ([]byte, bool, error) {
switch input {
case nil:
tkt, ekey, err := client.getServiceTicket(target)
if err != nil {
return nil, false, err
}
client.ekey = ekey
token, err := client.newKRB5TokenAPREQ(tkt, ekey)
if err != nil {
return nil, false, err
}
output, err := token.Marshal()
if err != nil {
return nil, false, err
}
return output, true, nil
default:
var token spnego.KRB5Token
err := token.Unmarshal(input)
if err != nil {
return nil, false, err
}
var completed bool
if token.IsAPRep() {
completed = true
encpart, err := krb5Crypto.DecryptEncPart(token.APRep.EncPart, client.ekey, keyusage.AP_REP_ENCPART)
if err != nil {
return nil, false, err
}
part := &messages.EncAPRepPart{}
if err = part.Unmarshal(encpart); err != nil {
return nil, false, err
}
client.Subkey = part.Subkey
}
if token.IsKRBError() {
return nil, !false, token.KRBError
}
return make([]byte, 0), !completed, nil
}
}
// Fixed version of SASL authentication based on https://github.com/go-ldap/ldap/pull/537.
func (client *gssapiClient) NegotiateSaslAuth(input []byte, authzid string) ([]byte, error) {
token := &krb5GSSAPI.WrapToken{}
err := unmarshalWrapToken(token, input, true)
if err != nil {
return nil, err
}
if (token.Flags & 0b1) == 0 {
return nil, fmt.Errorf("got a Wrapped token that's not from the server")
}
key := client.ekey
if (token.Flags & 0b100) != 0 {
key = client.Subkey
}
_, err = token.Verify(key, keyusage.GSSAPI_ACCEPTOR_SEAL)
if err != nil {
return nil, err
}
pl := token.Payload
if len(pl) != 4 {
return nil, fmt.Errorf("server send bad final token for SASL GSSAPI Handshake")
}
// We never want a security layer
payload := []byte{0, 0, 0, 0}
encType, err := krb5Crypto.GetEtype(key.KeyType)
if err != nil {
return nil, err
}
token = &krb5GSSAPI.WrapToken{
Flags: 0b100,
EC: uint16(encType.GetHMACBitLength() / 8),
RRC: 0,
SndSeqNum: 1,
Payload: payload,
}
if err := token.SetCheckSum(key, keyusage.GSSAPI_INITIATOR_SEAL); err != nil {
return nil, err
}
output, err := token.Marshal()
if err != nil {
return nil, err
}
return output, nil
}
func unmarshalWrapToken(wt *krb5GSSAPI.WrapToken, data []byte, expectFromAcceptor bool) error {
// Check if we can read a whole header
if len(data) < 16 {
return errors.New("bytes shorter than header length")
}
// Is the Token ID correct?
expectedWrapTokenId := []byte{0x05, 0x04}
if !bytes.Equal(expectedWrapTokenId, data[0:2]) {
return fmt.Errorf("wrong Token ID. Expected %s, was %s",
hex.EncodeToString(expectedWrapTokenId), hex.EncodeToString(data[0:2]))
}
// Check the acceptor flag
flags := data[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 data[3] != krb5GSSAPI.FillerByte {
return fmt.Errorf("unexpected filler byte: expecting 0xFF, was %s ", hex.EncodeToString(data[3:4]))
}
checksumL := binary.BigEndian.Uint16(data[4:6])
// Sanity check on the checksum length
if int(checksumL) > len(data)-krb5GSSAPI.HdrLen {
return fmt.Errorf("inconsistent checksum length: %d bytes to parse, checksum length is %d", len(data), checksumL)
}
payloadStart := 16 + checksumL
wt.Flags = flags
wt.EC = checksumL
wt.RRC = binary.BigEndian.Uint16(data[6:8])
wt.SndSeqNum = binary.BigEndian.Uint64(data[8:16])
wt.CheckSum = data[16:payloadStart]
wt.Payload = data[payloadStart:]
return nil
}
func krb5TokenAuthenticator(
realm string, cname types.PrincipalName, cert *x509.Certificate, flags []int,
) (types.Authenticator, error) {
// RFC 4121 Section 4.1.1
auth, err := types.NewAuthenticator(realm, cname)
if err != nil {
return auth, krberror.Errorf(err, krberror.KRBMsgError, "error generating new authenticator")
}
// https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1
checksum := make([]byte, 24)
if cert != nil {
hash := ChannelBindingHash(cert)
if len(hash) != 16 {
return auth, fmt.Errorf("unexpected channel binding hash size: %d, expected 16", len(hash))
}
binary.LittleEndian.PutUint32(checksum[:4], uint32(len(hash)))
copy(checksum[4:20], hash)
}
binary.LittleEndian.PutUint32(checksum[:4], 16)
for _, flag := range flags {
if flag == krb5GSSAPI.ContextFlagDeleg {
checksum = append(checksum, make([]byte, 28-len(checksum))...) //nolint:makezero
}
f := binary.LittleEndian.Uint32(checksum[20:24])
f |= uint32(flag)
binary.LittleEndian.PutUint32(checksum[20:24], f)
}
auth.Cksum = types.Checksum{
CksumType: chksumtype.GSSAPI,
Checksum: checksum,
}
return auth, nil
}
+487
View File
@@ -0,0 +1,487 @@
package ldapauth
import (
"context"
"crypto"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/md5"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/binary"
"encoding/pem"
"fmt"
"os"
"strings"
"time"
"github.com/RedTeamPentesting/adauth"
"github.com/RedTeamPentesting/adauth/othername"
"github.com/RedTeamPentesting/adauth/pkinit"
"software.sslmate.com/src/go-pkcs12"
"github.com/go-ldap/ldap/v3"
"github.com/jcmturner/gokrb5/v8/client"
"github.com/spf13/pflag"
)
// Options holds LDAP specific options.
type Options struct {
// LDAP scheme (ldap or ldaps).
Scheme string
// Verify indicates whether TLS verification should be performed.
Verify bool
// Timeout sets the request timeout for the LDAP connection.
Timeout time.Duration
// Debug can be set to enable debug output, for example with
// adauth.NewDebugFunc(...).
Debug func(string, ...any)
// SimpleBind indicates that SimpleBind authentication should be used
// instead of NTLM, Kerberos or mTLS. For this, a cleartext password is
// required.
SimpleBind bool
// TLSConfig for LDAPS or LDAP+StartTLS. InsecureSkipVerify is ignored and
// set according to Options.Verify. MaxVersion will be changed to 1.2 unless
// Options.DisableChannelBinding is set.
TLSConfig *tls.Config
// DisableChannelBinding omits the TLS certificate hash in Kerberos and NTLM
// authentication.
DisableChannelBinding bool
// StartTLS indicates that a TLS connection should be established even for
// non-LDAPS connections before authenticating. For client-certificate
// authentication on regular LDAP connections, StartTLS will be used even if
// this option is disabled.
StartTLS bool
// DialOptions can be used to customize the connection. However
// ldap.DialWithTLSConfig will be ignored, because TLS setup is handled
// internally.
DialOptions []ldap.DialOpt
// PKINITOptions can be used to modify the behavior of PKINIT when it is
// used.
PKINITOptions []pkinit.Option
}
// RegisterFlags registers LDAP specific flags to a pflag.FlagSet such as the
// default flagset pflag.CommandLine.
func (opts *Options) RegisterFlags(flagset *pflag.FlagSet) {
flagset.StringVar(&opts.Scheme, "scheme", "ldaps", "Scheme (ldap or ldaps)")
flagset.DurationVar(&opts.Timeout, "timeout", 5*time.Second, "LDAP connection timeout")
flagset.BoolVar(&opts.SimpleBind, "simple-bind", false, "Authenticate with simple bind")
flagset.BoolVar(&opts.Verify, "verify", false, "Verify LDAP TLS certificate")
flagset.BoolVar(&opts.StartTLS, "start-tls", false,
"Negotiate StartTLS before authenticating on regular LDAP connection")
}
// Connect returns an authenticated LDAP connection to the domain controller's
// LDAP server.
func Connect(ctx context.Context, authOpts *adauth.Options, ldapOpts *Options) (conn *ldap.Conn, err error) {
creds, target, err := authOpts.WithDCTarget(ctx, ldapOpts.Scheme)
if err != nil {
return nil, err
}
return ConnectTo(ctx, creds, target, ldapOpts)
}
// Connect returns an authenticated LDAP connection to the specified target.
func ConnectTo(
ctx context.Context, creds *adauth.Credential, target *adauth.Target, ldapOpts *Options,
) (conn *ldap.Conn, err error) {
opts := ldapOpts
if opts.Debug == nil {
opts.Debug = func(s string, a ...any) {}
}
opts.TLSConfig, err = TLSConfig(ldapOpts, creds.ClientCert, creds.ClientCertKey, creds.CACerts)
if err != nil {
return nil, fmt.Errorf("configure TLS: %w", err)
}
conn, err = connect(target, opts)
if err != nil {
return nil, err
}
if opts.Timeout == 0 {
conn.SetTimeout(3 * time.Second)
} else {
conn.SetTimeout(opts.Timeout)
}
err = bind(ctx, conn, creds, target, opts)
if err != nil {
return nil, err
}
return conn, nil
}
func connect(target *adauth.Target, opts *Options) (*ldap.Conn, error) {
switch strings.ToLower(opts.Scheme) {
case "ldaps", "":
conn, err := ldap.DialURL("ldaps://"+target.Address(),
append(opts.DialOptions, ldap.DialWithTLSConfig(opts.TLSConfig))...)
if err != nil {
return nil, fmt.Errorf("LDAPS dial: %w", err)
}
opts.Debug("connected to LDAPS server %s", target.Address())
return conn, nil
case "ldap":
conn, err := ldap.DialURL("ldap://"+target.Address(), opts.DialOptions...)
if err != nil {
return nil, fmt.Errorf("LDAP dial: %w", err)
}
opts.Debug("connected to LDAP server %s", target.Address())
if opts.StartTLS {
err = conn.StartTLS(opts.TLSConfig)
if err != nil {
_ = conn.Close()
return nil, fmt.Errorf("StartTLS: %w", err)
}
}
return conn, nil
default:
return nil, fmt.Errorf("invalid scheme: %q", opts.Scheme)
}
}
func bind(
ctx context.Context, conn *ldap.Conn, creds *adauth.Credential, target *adauth.Target, opts *Options,
) (err error) {
switch {
case opts.SimpleBind:
opts.Debug("authenticating using simple bind")
if creds.Password == "" && !creds.PasswordIsEmtpyString {
return fmt.Errorf("specify a password for simple bind")
}
_, err = conn.SimpleBind(&ldap.SimpleBindRequest{
Username: creds.UPN(),
Password: creds.Password,
})
if err != nil {
return fmt.Errorf("simple bind: %w", err)
}
case !target.UseKerberos && creds.ClientCert == nil:
opts.Debug("authenticating using NTLM bind")
if !creds.PasswordIsEmtpyString && (creds.Password == "" && creds.NTHash == "") {
return fmt.Errorf("no credentials available for NTLM")
}
bindRequest := &ldap.NTLMBindRequest{
Domain: creds.Domain,
Username: creds.Username,
Password: creds.Password,
Hash: creds.NTHash,
AllowEmptyPassword: creds.PasswordIsEmtpyString,
}
tlsState, ok := conn.TLSConnectionState()
if ok && !opts.DisableChannelBinding {
bindRequest.Negotiator = ntlmNegotiatorWithChannelBinding(tlsState.PeerCertificates[0])
}
_, err = conn.NTLMChallengeBind(bindRequest)
if err != nil {
return fmt.Errorf("NTLM bind: %w", err)
}
case target.UseKerberos:
authClient, err := kerberosClient(ctx, conn, creds, opts)
if err != nil {
return err
}
spn, err := target.SPN(ctx)
if err != nil {
return fmt.Errorf("build SPN: %w", err)
}
err = conn.GSSAPIBind(authClient, spn, creds.NTHash)
if err != nil {
return fmt.Errorf("GSSAPI bind: %w", err)
}
case creds.ClientCert != nil && strings.EqualFold(opts.Scheme, "ldap"):
opts.Debug("authenticating with client certificate via StartTLS")
if !opts.TLSConfig.InsecureSkipVerify {
hostname, err := target.Hostname(ctx)
if err != nil {
return fmt.Errorf("determine target hostname for TLS verification: %w", err)
}
opts.TLSConfig.ServerName = hostname
}
err = conn.StartTLS(opts.TLSConfig)
if err != nil {
return fmt.Errorf("StartTLS: %w", err)
}
err = conn.ExternalBind()
if err != nil {
if creds.ClientCert.Issuer.CommonName == "" ||
strings.EqualFold(creds.ClientCert.Subject.CommonName, creds.ClientCert.Issuer.CommonName) {
return fmt.Errorf("external bind: %w "+
"(certificate likely belongs to a KeyCredentialLink, try PKINIT with -k instead)", err)
}
return fmt.Errorf("external bind: %w", err)
}
case creds.ClientCert != nil:
opts.Debug("authenticating with client certificate")
res, err := conn.WhoAmI(nil)
if err != nil {
return fmt.Errorf("send whoami to verify certificate authentication: %w", err)
}
if res.AuthzID == "" {
if creds.ClientCert.Issuer.CommonName == "" ||
strings.EqualFold(creds.ClientCert.Subject.CommonName, creds.ClientCert.Issuer.CommonName) {
return fmt.Errorf("client certificate authentication failed " +
"(certificate likely belongs to a KeyCredentialLink, try PKINIT with -k instead)")
}
return fmt.Errorf("client certificate authentication failed")
}
default:
return fmt.Errorf("no credentials available")
}
return nil
}
func kerberosClient(
ctx context.Context, conn *ldap.Conn, creds *adauth.Credential, opts *Options,
) (*gssapiClient, error) {
krbConf, err := creds.KerberosConfig(ctx)
if err != nil {
return nil, fmt.Errorf("configure Kerberos: %w", err)
}
var (
authClient *gssapiClient
cert *x509.Certificate
)
tlsState, ok := conn.TLSConnectionState()
if ok && !opts.DisableChannelBinding {
cert = tlsState.PeerCertificates[0]
}
switch {
case creds.Password != "" || creds.PasswordIsEmtpyString:
opts.Debug("authenticating using GSSAPI bind (password)")
authClient = &gssapiClient{
Client: client.NewWithPassword(
creds.Username,
strings.ToUpper(creds.Domain),
creds.Password,
krbConf,
client.DisablePAFXFAST(true),
),
}
authClient.BindCertificate = cert
case creds.NTHash != "" || creds.AESKey != "":
opts.Debug("authenticating using GSSAPI bind (key)")
keyTab, err := creds.Keytab()
if err != nil {
return nil, fmt.Errorf("create keytab: %w", err)
}
authClient = &gssapiClient{
Client: client.NewWithKeytab(
creds.Username,
strings.ToUpper(creds.Domain),
keyTab,
krbConf,
client.DisablePAFXFAST(true),
),
BindCertificate: cert,
}
authClient.BindCertificate = cert
case creds.ClientCert != nil:
opts.Debug("authenticating using GSSAPI bind (PKINIT)")
return newPKINITClient(ctx, creds.Username, strings.ToUpper(creds.Domain),
creds.ClientCert, creds.ClientCertKey, krbConf, opts.PKINITOptions...)
case creds.CCache != "":
opts.Debug("authenticating using GSSAPI bind (ccache)")
authClient, err = newClientFromCCache(
creds.Username, strings.ToUpper(creds.Domain), creds.CCache, krbConf)
if err != nil {
return nil, fmt.Errorf("create GSSAPI client from CCACHE: %w", err)
}
authClient.BindCertificate = cert
default:
return nil, fmt.Errorf("no credentials available for Kerberos")
}
return authClient, nil
}
// TLSConfig returns a TLS config based on the default config in the provided
// LDAP options as well as PFX files.
func TLSConfig(
opts *Options, clientCert *x509.Certificate, clientCertKey crypto.PrivateKey, caCerts []*x509.Certificate,
) (*tls.Config, error) {
tlsConfig := opts.TLSConfig
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
tlsConfig.InsecureSkipVerify = !opts.Verify
if tlsConfig.MaxVersion == 0 && !opts.DisableChannelBinding {
tlsConfig.MaxVersion = tls.VersionTLS12 // channel binding is not supported for TLS1.3
}
if clientCert == nil {
return tlsConfig, nil
}
var (
keyBytes []byte
err error
)
switch v := clientCertKey.(type) {
case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey, *ecdh.PrivateKey:
keyBytes, err = x509.MarshalPKCS8PrivateKey(v)
if err != nil {
return nil, fmt.Errorf("marshal private key: %w", err)
}
default:
return nil, fmt.Errorf("unsupported client certificate key type: %T", clientCertKey)
}
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: clientCert.Raw,
})
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: keyBytes,
})
clientCertificate, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, fmt.Errorf("load client certificate: %w", err)
}
tlsConfig.Certificates = append(tlsConfig.Certificates, clientCertificate)
if len(caCerts) == 0 {
return tlsConfig, nil
}
if tlsConfig.RootCAs == nil {
tlsConfig.RootCAs = x509.NewCertPool()
}
for _, cert := range caCerts {
tlsConfig.RootCAs.AppendCertsFromPEM(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
}))
}
return tlsConfig, nil
}
// UserAndDomainFromPFX extracts the username and domain from UPNs in the
// certificate's otherName SAN extension.
func UserAndDomainFromPFX(pfxFile string, password string) (user string, domain string, err error) {
pfxData, err := os.ReadFile(pfxFile)
if err != nil {
return "", "", fmt.Errorf("read PFX: %w", err)
}
_, cert, _, err := pkcs12.DecodeChain(pfxData, password)
if err != nil {
return "", "", fmt.Errorf("decode PFX: %w", err)
}
user, domain = userAndDomainFromCert(cert)
return user, domain, nil
}
func userAndDomainFromCert(cert *x509.Certificate) (user string, domain string) {
upns, err := othername.UPNs(cert)
if err != nil {
return "", ""
}
for _, upn := range upns {
if !strings.Contains(upn, "@") {
continue
}
parts := strings.Split(upn, "@")
if len(parts) != 2 {
continue
}
return parts[0], parts[1]
}
return "", ""
}
// ChannelBinding hash computes the channel binding token that can be included
// in the authentication handshake to make sure that the client has established
// a TLS connection to the correct server.
func ChannelBindingHash(cert *x509.Certificate) []byte {
hashType := crypto.SHA256
switch cert.SignatureAlgorithm {
case x509.SHA384WithRSA, x509.ECDSAWithSHA384, x509.SHA384WithRSAPSS:
hashType = crypto.SHA384
case x509.SHA512WithRSA, x509.ECDSAWithSHA512, x509.SHA512WithRSAPSS:
hashType = crypto.SHA512
}
certHasher := hashType.New()
_, _ = certHasher.Write(cert.Raw)
certHash := certHasher.Sum(nil)
prefix := "tls-server-end-point:"
// https://learn.microsoft.com/en-us/windows/win32/api/sspi/ns-sspi-sec_channel_bindings
// https://github.com/jborean93/Mailozaurr/blob
// /6b565c4a1debdf301a95b93674ff12acdf8c762c/Classes/Class.SaslMechanismWindowsAuth.ps1#L67C19-L68C1
channelBindingStructure := []byte{
0, 0, 0, 0, // InitiatorAddrType
0, 0, 0, 0, // InitiatorLength
0, 0, 0, 0, // AcceptorAddrType,
0, 0, 0, 0, // AcceptorLength,
}
channelBindingStructure = binary.LittleEndian.AppendUint32(channelBindingStructure,
uint32(len(prefix)+len(certHash))) // ApplicationDataLength
channelBindingStructure = append(channelBindingStructure, []byte(prefix)...)
channelBindingStructure = append(channelBindingStructure, certHash...)
channelBindingHasher := md5.New()
channelBindingHasher.Write(channelBindingStructure)
hash := channelBindingHasher.Sum(nil)
return hash
}
+67
View File
@@ -0,0 +1,67 @@
package ldapauth
import (
"crypto/x509"
"fmt"
"github.com/Azure/go-ntlmssp"
"github.com/go-ldap/ldap/v3"
"github.com/vadimi/go-ntlm/ntlm"
)
type ntlmNegotiator struct {
cert *x509.Certificate
}
var _ ldap.NTLMNegotiator = &ntlmNegotiator{}
func ntlmNegotiatorWithChannelBinding(cert *x509.Certificate) ldap.NTLMNegotiator {
return &ntlmNegotiator{cert: cert}
}
func (n *ntlmNegotiator) Negotiate(domain string, worktation string) ([]byte, error) {
return ntlmssp.NewNegotiateMessage(domain, worktation)
}
func (n *ntlmNegotiator) ChallengeResponse(challenge []byte, username string, hash string) ([]byte, error) {
if n.cert == nil {
// no cert means no channel binding, so Azure/ntlmssp can handle
// authentication alone.
return ntlmssp.ProcessChallengeWithHash(challenge, username, hash)
}
// The authenticate message needs to include a channel binding hash, but the
// Azure/ntlmssp library does not support channel binding and offers no API
// to retrofit it. The only way to make it work is too parse the challenge,
// add the channel bindings to the challenge's TargetInfo field which will
// then be included in the authenticate message. Unfortunately we need
// another NTLM library because message marshalling and unmarshalling is
// also not exposed in Azure/ntlmssp.
cm, err := ntlm.ParseChallengeMessage(challenge)
if err != nil {
return nil, fmt.Errorf("parse NTLM challenge before injecting channel binding AVPair: %w", err)
}
// drop end-of-list marker if present because we want to add another entry
if len(cm.TargetInfo.List) > 0 && cm.TargetInfo.List[len(cm.TargetInfo.List)-1].AvId == ntlm.MsvAvEOL {
cm.TargetInfo.List = cm.TargetInfo.List[:len(cm.TargetInfo.List)-1]
}
// add channel bindings
cm.TargetInfo.AddAvPair(ntlm.MsvChannelBindings, ChannelBindingHash(n.cert))
cm.TargetInfo.AddAvPair(ntlm.MsvAvEOL, nil)
// make sure that the server cannot make cm.Bytes() panic by omitting the
// version.
if cm.Version == nil {
cm.Version = &ntlm.VersionStruct{}
}
cm.TargetInfoPayloadStruct, err = ntlm.CreateBytePayload(cm.TargetInfo.Bytes())
if err != nil {
return nil, fmt.Errorf("marshal AVPairs with injected channel binding AVPair: %w", err)
}
return ntlmssp.ProcessChallengeWithHash(cm.Bytes(), username, hash)
}
+351
View File
@@ -0,0 +1,351 @@
package adauth
import (
"context"
"crypto/rsa"
"encoding/hex"
"fmt"
"io"
"net"
"os"
"strconv"
"strings"
"github.com/RedTeamPentesting/adauth/othername"
"github.com/spf13/pflag"
"software.sslmate.com/src/go-pkcs12"
)
// Options holds command line options that are used to determine authentication
// credentials and target.
type Options struct {
// Username (with domain) in one of the following formats:
// `UPN`, `domain\user`, `domain/user` or `user`
User string
Password string
NTHash string
AESKey string
CCache string
DomainController string
ForceKerberos bool
PFXFileName string
PFXPassword string
credential *Credential
flagset *pflag.FlagSet
Debug func(fmt string, a ...any)
Resolver Resolver
}
// AuxiliaryInfo represents information that was obtained elsewhere that can be
// used to supplement the parsed options. However, parsed options always take
// precedent.
type AuxiliaryInfo struct {
Domain string
User string
DC string
}
// RegisterFlags registers authentication flags to a pflag.FlagSet such as the
// default flagset `pflag.CommandLine`.
func (opts *Options) RegisterFlags(flagset *pflag.FlagSet) {
defaultCCACHEFile := os.Getenv("KRB5CCNAME")
ccacheHint := ""
if defaultCCACHEFile == "" {
ccacheHint = " (defaults to $KRB5CCNAME, currently unset)"
}
flagset.StringVarP(&opts.User, "user", "u", "", "Username ('`user@domain`', 'domain\\user', 'domain/user' or 'user')")
flagset.StringVarP(&opts.Password, "password", "p", "", "Password")
flagset.StringVarP(&opts.NTHash, "nt-hash", "H", "", "NT `hash` ('NT', ':NT' or 'LM:NT')")
flagset.StringVar(&opts.AESKey, "aes-key", "", "Kerberos AES `hex key`")
flagset.StringVar(&opts.PFXFileName, "pfx", "", "Client certificate and private key as PFX `file`")
flagset.StringVar(&opts.PFXPassword, "pfx-password", "", "Password for PFX file")
flagset.StringVar(&opts.CCache, "ccache", defaultCCACHEFile, "Kerberos CCache `file` name"+ccacheHint)
flagset.StringVar(&opts.DomainController, "dc", "", "Domain controller")
flagset.BoolVarP(&opts.ForceKerberos, "kerberos", "k", false, "Use Kerberos authentication")
opts.flagset = flagset
}
func (opts *Options) debug(format string, a ...any) {
if opts.Debug != nil {
opts.Debug(format, a...)
}
}
func portForProtocol(protocol string) string {
switch strings.ToLower(protocol) {
case "ldap":
return "389"
case "ldaps":
return "636"
case "http":
return "80"
case "https":
return "443"
case "smb":
return "445"
case "rdp":
return "3389"
case "kerberos":
return "88"
default:
return ""
}
}
func addPortForProtocolIfMissing(protocol string, addr string) string {
host, port, err := net.SplitHostPort(addr)
if err != nil || port != "" {
return addr
}
port = portForProtocol(protocol)
if port == "" {
return addr
}
return net.JoinHostPort(host, port)
}
// WithDCTarget returns credentials and the domain controller for the
// corresponding domain as the target.
func (opts *Options) WithDCTarget(ctx context.Context, protocol string) (*Credential, *Target, error) {
if opts.DomainController != "" {
return opts.WithTarget(ctx, protocol, addPortForProtocolIfMissing(protocol, opts.DomainController))
}
cred, err := opts.preliminaryCredential()
if err != nil {
return nil, nil, err
}
if cred.Domain == "" {
return nil, nil, fmt.Errorf("domain unknown")
}
resolver := ensureResolver(opts.Resolver, opts.debug)
var dcAddr string
host, port, err := resolver.LookupFirstService(context.Background(), protocol, cred.Domain)
if err != nil {
lookupSRVErr := fmt.Errorf("could not lookup %q service of domain %q: %w", protocol, cred.Domain, err)
dcAddr, err = resolver.LookupDCByDomain(context.Background(), cred.Domain)
if err != nil {
return nil, nil, fmt.Errorf("could not find DC: %w and %w", lookupSRVErr, err)
}
port := portForProtocol(protocol)
if port != "" {
dcAddr = net.JoinHostPort(dcAddr, port)
}
opts.debug("using DC %s based on domain lookup for %s", dcAddr, cred.Domain)
} else {
dcAddr = net.JoinHostPort(host, strconv.Itoa(port))
opts.debug("using DC %s based on SRV lookup for domain %s", dcAddr, cred.Domain)
}
return cred, newTarget(
protocol, dcAddr, opts.ForceKerberos || cred.mustUseKerberos(), opts.CCache, opts.Resolver), nil
}
// WithTarget returns credentials and the specified target.
func (opts *Options) WithTarget(ctx context.Context, protocol string, target string) (*Credential, *Target, error) {
if protocol == "" {
protocol = "host"
}
cred, err := opts.preliminaryCredential()
if err != nil {
return nil, nil, err
}
t := newTarget(protocol, target, opts.ForceKerberos || cred.mustUseKerberos(), opts.CCache, opts.Resolver)
if cred.Domain == "" {
hostname, err := t.Hostname(ctx)
if err != nil {
return nil, nil, fmt.Errorf("lookup target hostname to determine domain: %w", err)
}
parts := strings.SplitN(hostname, ".", 2)
if len(parts) == 2 {
switch {
case strings.Contains(parts[1], "."):
cred.Domain = parts[1]
default:
cred.Domain = hostname
}
}
}
return cred, t, nil
}
// Username returns the user's name. Username may return an empty string.
func (opts *Options) Username() string {
cred, err := opts.preliminaryCredential()
if err != nil {
return ""
}
return cred.Username
}
// UPN returns the user's domain. Domain may return an empty string.
func (opts *Options) Domain() string {
cred, err := opts.preliminaryCredential()
if err != nil {
return ""
}
return cred.Domain
}
// UPN returns the user's universal principal name. UPN may return an empty
// string.
func (opts *Options) UPN() string {
cred, err := opts.preliminaryCredential()
if err != nil {
return ""
}
return cred.UPN()
}
// NoTarget returns the user credentials without supplementing it with
// information from a target.
func (opts *Options) NoTarget() (*Credential, error) {
return opts.preliminaryCredential()
}
func (opts *Options) preliminaryCredential() (*Credential, error) {
if opts.credential != nil {
return opts.credential, nil
}
domain, username := splitUserIntoDomainAndUsername(opts.User)
cleanedNTHash := cleanNTHash(opts.NTHash)
var ntHash string
if cleanedNTHash != "" {
ntHashBytes, err := hex.DecodeString(cleanedNTHash)
if err != nil {
return nil, fmt.Errorf("invalid NT hash: parse hex: %w", err)
} else if len(ntHashBytes) != 16 {
return nil, fmt.Errorf("invalid NT hash: %d bytes instead of 16", len(ntHashBytes))
}
ntHash = cleanedNTHash
}
var aesKey string
if opts.AESKey != "" {
aesKeyBytes, err := hex.DecodeString(opts.AESKey)
if err != nil {
return nil, fmt.Errorf("invalid AES key: parse hex: %w", err)
} else if len(aesKeyBytes) != 16 && len(aesKeyBytes) != 32 {
return nil, fmt.Errorf("invalid AES key: %d bytes instead of 16 or 32", len(aesKeyBytes))
}
aesKey = opts.AESKey
}
var ccache string
if opts.CCache != "" {
s, err := os.Stat(opts.CCache)
if err == nil && !s.IsDir() {
ccache = opts.CCache
}
}
cred := &Credential{
Username: username,
Password: opts.Password,
Domain: domain,
NTHash: cleanNTHash(ntHash),
AESKey: aesKey,
CCache: ccache,
dc: opts.DomainController,
PasswordIsEmtpyString: opts.Password == "" && (opts.flagset != nil && opts.flagset.Changed("password")),
CCacheIsFromEnv: opts.CCache != "" && (opts.flagset != nil && !opts.flagset.Changed("ccache")),
Resolver: opts.Resolver,
}
if opts.PFXFileName != "" {
pfxData, err := os.ReadFile(opts.PFXFileName)
if err != nil {
return nil, fmt.Errorf("read PFX: %w", err)
}
key, cert, caCerts, err := pkcs12.DecodeChain(pfxData, opts.PFXPassword)
if err != nil {
return nil, fmt.Errorf("decode PFX: %w", err)
}
rsaKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("PFX key is not an RSA private key but %T", rsaKey)
}
cred.ClientCert = cert
cred.ClientCertKey = rsaKey
cred.CACerts = caCerts
}
//nolint:nestif
if cred.ClientCert != nil {
user, domain, err := othername.UserAndDomain(cred.ClientCert)
if err == nil {
if cred.Username == "" {
cred.Username = user
}
if cred.Domain == "" {
cred.Domain = domain
}
}
}
opts.credential = cred
return cred, nil
}
// NewDebugFunc creates a debug output handler.
func NewDebugFunc(enabled *bool, writer io.Writer, colored bool) func(string, ...any) {
return func(format string, a ...any) {
if enabled == nil || !*enabled {
return
}
format = strings.TrimRight(format, "\n")
if colored {
format = "\033[2m" + format + "\033[0m"
}
_, _ = fmt.Fprintf(writer, format+"\n", a...)
}
}
func cleanNTHash(h string) string {
if !strings.Contains(h, ":") {
return h
}
parts := strings.Split(h, ":")
if len(parts) != 2 {
return h
}
return parts[1]
}
+337
View File
@@ -0,0 +1,337 @@
package adauth_test
import (
"context"
"encoding/hex"
"net"
"strconv"
"testing"
"github.com/RedTeamPentesting/adauth"
)
const (
testUser = "someuser"
testDomain = "domain.tld"
)
func TestUsernameAndDomainParsing(t *testing.T) {
expectedUPN := testUser + "@" + testDomain
expectedLogonName := testDomain + `\` + testUser
expectedImpacketLogonName := testDomain + "/" + testUser
testCases := []struct {
Name string
Opts adauth.Options
}{
{
Name: "UPN",
Opts: adauth.Options{User: testUser + "@" + testDomain},
},
{
Name: "logon-name",
Opts: adauth.Options{User: testDomain + `\` + testUser},
},
{
Name: "impacket-style",
Opts: adauth.Options{User: testDomain + `/` + testUser},
},
{
Name: "pfx",
Opts: adauth.Options{PFXFileName: "testdata/someuser@domain.tld.pfx"},
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.Name, func(t *testing.T) {
creds, err := testCase.Opts.NoTarget()
if err != nil {
t.Fatalf("get credentials: %v", err)
}
if creds.Username != testUser {
t.Errorf("username %q is not %q", creds.Username, testUser)
}
if creds.Domain != testDomain {
t.Errorf("domain %q is not %q", creds.Domain, testDomain)
}
upn := creds.UPN()
if upn != expectedUPN {
t.Errorf("UPN %q is not %q", upn, expectedUPN)
}
logonName := creds.LogonName()
if logonName != expectedLogonName {
t.Errorf("logon name %q is not %q", logonName, expectedLogonName)
}
impacketLogonName := creds.ImpacketLogonName()
if impacketLogonName != expectedImpacketLogonName {
t.Errorf("impacket-style logon name %q is not %q", impacketLogonName, expectedImpacketLogonName)
}
})
}
}
func TestDomainFromTargetHostname(t *testing.T) {
opts := adauth.Options{
User: testUser,
}
creds, _, err := opts.WithTarget(context.Background(), "cifs", "host."+testDomain)
if err != nil {
t.Fatalf("get credentials: %v", err)
}
if creds.Domain != testDomain {
t.Errorf("domain is %q instead of %q", creds.Domain, testDomain)
}
// ignore domain in hostname for local authentication
opts = adauth.Options{
User: "./" + testUser,
}
creds, _, err = opts.WithTarget(context.Background(), "cifs", "host."+testDomain)
if err != nil {
t.Fatalf("get credentials: %v", err)
}
if creds.Domain != "." {
t.Errorf("domain is %q instead of %q", creds.Domain, ".")
}
}
func TestDCTarget(t *testing.T) {
dcHost := "dc." + testDomain
expectedSPN := "ldap/" + dcHost
opts := adauth.Options{
User: testUser + `@` + testDomain,
Resolver: &testResolver{
SRV: map[string]map[string]map[string]struct {
Name string
SRV []*net.SRV
}{
"ldap": {
"tcp": {
testDomain: {
Name: dcHost,
SRV: []*net.SRV{
{Target: dcHost, Port: 389},
},
},
},
},
},
},
}
_, dc, err := opts.WithDCTarget(context.Background(), "ldap")
if err != nil {
t.Fatalf("get DC target: %v", err)
}
if dc.Address() != net.JoinHostPort(dcHost, "389") {
t.Fatalf("DC address is %q instead of %q", dc.Address(), net.JoinHostPort(dcHost, "389"))
}
if dc.AddressWithoutPort() != dcHost {
t.Fatalf("DC address without port is %q instead of %q", dc.Address(), dcHost)
}
if dc.Port != "389" {
t.Errorf("DC port is %q instead of %q", dc.Port, "389")
}
spn, err := dc.SPN(context.Background())
if err != nil {
t.Fatalf("get DC SPN: %v", err)
}
if spn != expectedSPN {
t.Errorf("DC SPN is %q instead of %q", spn, expectedSPN)
}
}
func TestDCTargetWithoutSRVRecord(t *testing.T) {
dcHost := "dc." + testDomain
dcIP := net.ParseIP("10.0.0.1")
expectedSPN := "ldap/" + dcHost
opts := adauth.Options{
User: testUser + `@` + testDomain,
Resolver: &testResolver{
HostToAddr: map[string][]net.IP{
testDomain: {dcIP},
},
AddrToHost: map[string][]string{
dcIP.String(): {dcHost},
},
},
}
_, dc, err := opts.WithDCTarget(context.Background(), "ldap")
if err != nil {
t.Fatalf("get DC target: %v", err)
}
if dc.Address() != net.JoinHostPort(dcHost, "389") {
t.Fatalf("DC address is %q instead of %q", dc.Address(), net.JoinHostPort(dcHost, "389"))
}
if dc.AddressWithoutPort() != dcHost {
t.Fatalf("DC address without port is %q instead of %q", dc.Address(), dcHost)
}
if dc.Port != "389" {
t.Errorf("DC port is %q instead of %q", dc.Port, "389")
}
spn, err := dc.SPN(context.Background())
if err != nil {
t.Fatalf("get DC SPN: %v", err)
}
if spn != expectedSPN {
t.Errorf("DC SPN is %q instead of %q", spn, expectedSPN)
}
}
func TestDCTargetNoReverseLookup(t *testing.T) {
dcIP := net.ParseIP("10.0.0.1")
opts := adauth.Options{
User: testUser + `@` + testDomain,
Resolver: &testResolver{
HostToAddr: map[string][]net.IP{
testDomain: {dcIP},
},
},
}
_, dc, err := opts.WithDCTarget(context.Background(), "ldap")
if err != nil {
t.Fatalf("get DC target: %v", err)
}
if dc.Address() != net.JoinHostPort(dcIP.String(), "389") {
t.Fatalf("DC address is %q instead of %q", dc.Address(), net.JoinHostPort(dcIP.String(), "389"))
}
}
func TestKerberos(t *testing.T) {
upn := testUser + `@` + testDomain
testCases := []struct {
Opts adauth.Options
ShouldUseKerberos bool
}{
{
Opts: adauth.Options{
User: upn,
Password: "pass",
},
ShouldUseKerberos: false,
},
{
Opts: adauth.Options{
User: upn,
AESKey: hex.EncodeToString(make([]byte, 16)),
},
ShouldUseKerberos: true,
},
{
Opts: adauth.Options{
User: upn,
Password: "test",
AESKey: hex.EncodeToString(make([]byte, 16)),
},
ShouldUseKerberos: false,
},
{
Opts: adauth.Options{
User: upn,
Password: "pass",
ForceKerberos: true,
},
ShouldUseKerberos: true,
},
{
Opts: adauth.Options{
User: upn,
PFXFileName: "testdata/someuser@domain.tld.pfx",
},
ShouldUseKerberos: true,
},
{
Opts: adauth.Options{
User: upn,
// CCache indicates kerberos usage, but only if file is present
// (even though it might be empty)
CCache: "testdata/empty.ccache",
},
ShouldUseKerberos: true,
},
{
Opts: adauth.Options{
User: upn,
// CCache does not matter if file is not even there
CCache: "testdata/doesnotexist",
},
ShouldUseKerberos: false,
},
}
for i, testCase := range testCases {
testCase := testCase
t.Run(strconv.Itoa(i), func(t *testing.T) {
testCase.Opts.DomainController = "dc.domain.tld"
_, target, err := testCase.Opts.WithDCTarget(context.Background(), "ldap")
if err != nil {
t.Fatalf("get target: %v", err)
}
switch {
case testCase.ShouldUseKerberos && !target.UseKerberos:
t.Errorf("target would not use Kerberos even though it should: %#v", testCase.Opts)
case !testCase.ShouldUseKerberos && target.UseKerberos:
t.Errorf("target would use Kerberos even though it should not: %#v", testCase.Opts)
}
})
}
}
func TestCleanNTHash(t *testing.T) {
ntHash := "31d6cfe0d16ae931b73c59d7e0c089c0"
testCases := []string{
ntHash,
":" + ntHash,
"aad3b435b51404eeaad3b435b51404ee:" + ntHash,
}
for i, testCase := range testCases {
testCase := testCase
t.Run(strconv.Itoa(i), func(t *testing.T) {
creds, err := (&adauth.Options{
User: testUser + "@" + testDomain,
NTHash: testCase,
}).NoTarget()
if err != nil {
t.Fatalf("get credentials: %v", err)
}
if creds.NTHash != ntHash {
t.Errorf("NT hash is %q instead of %q", creds.NTHash, ntHash)
}
})
}
}
+189
View File
@@ -0,0 +1,189 @@
// Package othername is a minimal and incomplete implementation of the otherName SAN extension.
package othername
import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"strings"
)
var (
UPNOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 20, 2, 3}
SANOID = asn1.ObjectIdentifier{2, 5, 29, 17}
)
type OtherName struct {
ID asn1.ObjectIdentifier
Value asn1.RawValue
}
// Extension generates an otherName extension.
func Extension(names ...*OtherName) (pkix.Extension, error) {
ext := pkix.Extension{
Id: SANOID,
Critical: false,
}
rawValues := make([]asn1.RawValue, 0, len(names))
for _, name := range names {
v := asn1.RawValue{
Class: asn1.ClassContextSpecific,
IsCompound: true,
}
id, err := asn1.Marshal(name.ID)
if err != nil {
return ext, fmt.Errorf("marshal oid: %w", err)
}
v.Bytes = append(v.Bytes, id...)
blob, err := asn1.Marshal(name.Value)
if err != nil {
return ext, fmt.Errorf("marshal raw name: %w", err)
}
v.Bytes = append(v.Bytes, blob...)
rawValues = append(rawValues, v)
}
extBytes, err := asn1.Marshal(rawValues)
if err != nil {
return ext, fmt.Errorf("marshal extension bytes: %w", err)
}
ext.Value = append(ext.Value, extBytes...)
return ext, nil
}
// ExtensionFromUPNs build an otherName extension based on the provided UPNs.
func ExtensionFromUPNs(upns ...string) (ext pkix.Extension, err error) {
otherNames := make([]*OtherName, 0, len(upns))
for _, upn := range upns {
utf8Name, err := asn1.MarshalWithParams(upn, "utf8")
if err != nil {
return ext, fmt.Errorf("marshal UTF8 name: %w", err)
}
otherNames = append(otherNames, &OtherName{
ID: UPNOID,
Value: asn1.RawValue{
Class: asn1.ClassContextSpecific,
IsCompound: true,
Bytes: utf8Name,
},
})
}
return Extension(otherNames...)
}
// Names returns the names from the otherName extension of the provided
// certificate. If it does not contain such an extension, it will return an
// empty slice and no error.
func Names(cert *x509.Certificate) ([]*OtherName, error) {
oidSubjectAltName := asn1.ObjectIdentifier{2, 5, 29, 17}
var otherNames []*OtherName
for _, extension := range append(cert.Extensions, cert.ExtraExtensions...) {
if !extension.Id.Equal(oidSubjectAltName) {
continue
}
ons, err := otherNamesFromSANBytes(extension.Value)
if err != nil {
return nil, fmt.Errorf("parse otherName data: %w", err)
}
otherNames = append(otherNames, ons...)
}
return otherNames, nil
}
// UPNs returns all UPNs that are stored in certificates otherName extension.
func UPNs(cert *x509.Certificate) (upns []string, err error) {
otherNames, err := Names(cert)
if err != nil {
return nil, err
}
for _, otherName := range otherNames {
if !otherName.ID.Equal(UPNOID) {
continue
}
var upn string
_, err = asn1.UnmarshalWithParams(otherName.Value.Bytes, &upn, "utf8")
if err != nil {
return nil, fmt.Errorf("unmarshal name: %w", err)
}
upns = append(upns, upn)
}
return upns, nil
}
// UserAndDomain returns the user and domain from the first valid UPN in the
// certificate's otherName extension.
func UserAndDomain(cert *x509.Certificate) (user string, domain string, err error) {
upns, err := UPNs(cert)
if err != nil {
return "", "", err
}
for _, upn := range upns {
parts := strings.Split(upn, "@")
if len(parts) == 2 {
return parts[0], parts[1], nil
}
}
return "", "", fmt.Errorf("found no suitable UPN in certificate")
}
func otherNamesFromSANBytes(bytes []byte) ([]*OtherName, error) {
values := []asn1.RawValue{}
_, err := asn1.Unmarshal(bytes, &values)
if err != nil {
return nil, fmt.Errorf("unmarshal raw values: %w", err)
}
otherNames := make([]*OtherName, 0, len(values))
for _, value := range values {
if value.Tag != 0 {
continue
}
otherName := &OtherName{}
value.Bytes, err = asn1.Unmarshal(value.Bytes, &otherName.ID)
if err != nil {
return nil, fmt.Errorf("unmarshal ID: %w", err)
}
value.Bytes, err = asn1.UnmarshalWithParams(value.Bytes, &otherName.Value, "utf8")
if err != nil {
return nil, fmt.Errorf("unmarshal raw name: %w", err)
}
if len(value.Bytes) != 0 {
return nil, fmt.Errorf("othername: entry contains trailing bytes")
}
otherNames = append(otherNames, otherName)
}
return otherNames, nil
}
+40
View File
@@ -0,0 +1,40 @@
package othername_test
import (
"crypto/x509"
"crypto/x509/pkix"
"testing"
"github.com/RedTeamPentesting/adauth/othername"
)
func TestOtherName(t *testing.T) {
t.Parallel()
names := []string{"a", "b", "c"}
ext, err := othername.ExtensionFromUPNs(names...)
if err != nil {
t.Fatalf("generate otherName extension: %v", err)
}
cert := &x509.Certificate{
ExtraExtensions: []pkix.Extension{ext},
}
parsedNames, err := othername.UPNs(cert)
if err != nil {
t.Fatalf("parse otherNames: %v", err)
}
if len(names) != len(parsedNames) {
t.Fatalf("got %d (%#v) names instead of %d (%#v)",
len(parsedNames), parsedNames, len(names), names)
}
for i := 0; i < len(names); i++ {
if names[i] != parsedNames[i] {
t.Errorf("got %q instead of %q", parsedNames[i], names[i])
}
}
}
+165
View File
@@ -0,0 +1,165 @@
package pkinit
import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"math/big"
"time"
"github.com/jcmturner/gokrb5/v8/types"
)
type SignerInfo struct {
Version int `asn1:"default:1"`
IssuerAndSerialNumber IssuerAndSerial
DigestAlgorithm pkix.AlgorithmIdentifier
AuthenticatedAttributes []Attribute `asn1:"optional,omitempty,tag:0"`
DigestEncryptionAlgorithm pkix.AlgorithmIdentifier
EncryptedDigest []byte
UnauthenticatedAttributes []pkix.AttributeTypeAndValue `asn1:"optional,omitempty,tag:1"`
}
type Attribute struct {
Type asn1.ObjectIdentifier
Value asn1.RawValue `asn1:"set"`
}
type IssuerAndSerial struct {
IssuerName asn1.RawValue
SerialNumber *big.Int
}
type ContentInfo struct {
ContentType asn1.ObjectIdentifier
Content asn1.RawValue `asn1:"explicit,optional,tag:0"`
}
type SignedData struct {
Version int `asn1:"default:1"`
DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"`
ContentInfo ContentInfo
Certificates RawCertificates `asn1:"optional,tag:0"`
CRLs []x509.RevocationList `asn1:"optional,tag:1"`
SignerInfos []SignerInfo `asn1:"set"`
}
type RawCertificates struct {
Raw asn1.RawContent
}
func RawCertificate(cert *x509.Certificate) (RawCertificates, error) {
val := asn1.RawValue{Bytes: cert.Raw, Class: 2, Tag: 0, IsCompound: true}
b, err := asn1.Marshal(val)
if err != nil {
return RawCertificates{}, err
}
return RawCertificates{Raw: b}, nil
}
type AuthPack struct {
// AuthPack ::= SEQUENCE {
// pkAuthenticator [0] PKAuthenticator,
// clientPublicValue [1] SubjectPublicKeyInfo OPTIONAL,
// supportedCMSTypes [2] SEQUENCE OF AlgorithmIdentifier OPTIONAL,
// clientDHNonce [3] DHNonce OPTIONAL,
// ...,
// supportedKDFs [4] SEQUENCE OF KDFAlgorithmId OPTIONAL,
// ...
// }
PKAuthenticator PKAuthenticator `asn1:"tag:0,explicit"`
ClientPublicValue SubjectPublicKeyInfo `asn1:"tag:1,explicit,optional"`
SupportedCMSTypes []pkix.AlgorithmIdentifier `asn1:"tag:2,explicit,optional"`
ClientDHNonce []byte `asn1:"tag:3,explicit,optional"`
}
type PKAuthenticator struct {
// PKAuthenticator ::= SEQUENCE {
// cusec [0] INTEGER -- (0..999999) --,
// ctime [1] KerberosTime,
// nonce [2] INTEGER (0..4294967295),
// paChecksum [3] OCTET STRING OPTIONAL,
// ...
// asn1
CUSec int `asn1:"tag:0,explicit"`
CTime time.Time `asn1:"tag:1,explicit,generalized"`
Nonce int `asn1:"tag:2,explicit"`
Checksum []byte `asn1:"tag:3,explicit,optional"`
}
type SubjectPublicKeyInfo struct {
// SubjectPublicKeyInfo ::= SEQUENCE {
// algorithm AlgorithmIdentifier{PUBLIC-KEY,
// {PublicKeyAlgorithms}},
// subjectPublicKey BIT STRING }
Algorithm AlgorithmIdentifier
PublicKey asn1.BitString
}
type AlgorithmIdentifier struct {
Algorithm asn1.ObjectIdentifier `asn1:"implicit"`
Parameters DomainParameters `asn1:"implicit,optional"`
}
type DomainParameters struct {
// DomainParameters ::= SEQUENCE {
// p INTEGER, -- odd prime, p=jq +1
// g INTEGER, -- generator, g
// q INTEGER, -- factor of p-1
// j INTEGER OPTIONAL, -- subgroup factor
// validationParams ValidationParams OPTIONAL }
P *big.Int
G int
Q int
}
type PAPKASRep struct {
DHInfo asn1.RawValue
}
type PAPACRequest struct {
IncludePAC bool `asn1:"explicit,tag:0"`
}
func (p *PAPACRequest) AsPAData() types.PAData {
// make sure we marshal the struct and not the pointer to the struct which
// would cause an error and also make sure we don't panic when receiver is
// nil.
pacRequest := PAPACRequest{}
if p != nil {
pacRequest.IncludePAC = p.IncludePAC
}
pacRequestBytes, err := asn1.Marshal(pacRequest)
if err != nil {
panic(fmt.Sprintf("unexpected error marshalling PAPACRequest: %v", err))
}
const paDataTypePACRequest = 128
return types.PAData{
PADataType: paDataTypePACRequest,
PADataValue: pacRequestBytes,
}
}
type DHRepInfo struct {
DHSignedData []byte `asn1:"tag:0"`
ServerDHNonce []byte `asn1:"tag:1,explicit,optional"`
}
type KDCDHKeyInfo struct {
SubjectPublicKey asn1.BitString `asn1:"tag:0,explicit"`
Nonce *big.Int `asn1:"tag:1,explicit"`
DHKeyExpication time.Time `asn1:"tag:2,explicit,optional,generalized"`
}
type AuthoirzationDataElement struct {
ADType int `asn1:"tag:0"`
ADData []byte `asn1:"tag:1"`
}
type AuthoirzationData []AuthoirzationDataElement
+167
View File
@@ -0,0 +1,167 @@
package pkinit
import (
"crypto/sha1"
"encoding/asn1"
"fmt"
"math/big"
krb5crypto "github.com/jcmturner/gokrb5/v8/crypto"
"github.com/jcmturner/gokrb5/v8/iana/etypeID"
"github.com/jcmturner/gokrb5/v8/iana/keyusage"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
)
// Decrypt decrypts the encrypted parts of an ASRep with the key derived during PKINIT.
func Decrypt(asRep *messages.ASRep, dhKey *big.Int, dhClientNonce []byte) (pkinitKey types.EncryptionKey, err error) {
ekey, err := ExtractNegotiatedKey(asRep, dhKey, dhClientNonce)
if err != nil {
return pkinitKey, fmt.Errorf("extract negotiated key: %w", err)
}
decrypted, err := krb5crypto.DecryptEncPart(asRep.EncPart, ekey, keyusage.AS_REP_ENCPART)
if err != nil {
return pkinitKey, fmt.Errorf("decrypt: %w", err)
}
err = asRep.DecryptedEncPart.Unmarshal(decrypted)
if err != nil {
return pkinitKey, fmt.Errorf("unmarshal encrypted part: %w", err)
}
return ekey, nil
}
// ExtractNegotiatedKey extracts the key derived during PKINIT.
func ExtractNegotiatedKey(
asRep *messages.ASRep, dhKey *big.Int, dhClientNonce []byte,
) (ekey types.EncryptionKey, err error) {
var paPKASRepBytes []byte
for _, paData := range asRep.PAData {
if paData.PADataType == 17 {
paPKASRepBytes = paData.PADataValue
}
}
if paPKASRepBytes == nil {
return ekey, fmt.Errorf("could not find pA-PK-AS-REP structure")
}
var paPKASRep DHRepInfo
err = unmarshalFromRawValue(paPKASRepBytes, &paPKASRep)
if err != nil {
return ekey, fmt.Errorf("unmarshal PA-PK-AS-Rep: %w", err)
}
var contentInfo ContentInfo
_, err = asn1.Unmarshal(paPKASRep.DHSignedData, &contentInfo)
if err != nil {
return ekey, fmt.Errorf("unmarshal signed data: %w", err)
}
if !contentInfo.ContentType.Equal(asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}) {
return ekey, fmt.Errorf("unexpected outer content type: %s", contentInfo.ContentType)
}
var signedData SignedData
_, err = asn1.Unmarshal(contentInfo.Content.Bytes, &signedData)
if err != nil {
return ekey, fmt.Errorf("unmarshal signed data: %w", err)
}
if !signedData.ContentInfo.ContentType.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 5, 2, 3, 2}) {
return ekey, fmt.Errorf("unexpected inner content type: %s", signedData.ContentInfo.ContentType)
}
var keyInfo KDCDHKeyInfo
err = unmarshalFromRawValue(signedData.ContentInfo.Content.Bytes, &keyInfo)
if err != nil {
return ekey, fmt.Errorf("unmarshal key info: %w", err)
}
if len(keyInfo.SubjectPublicKey.Bytes) < 7 {
return ekey, fmt.Errorf("public key is too short")
}
pubkeyData, err := asn1.Marshal(keyInfo.SubjectPublicKey)
if err != nil {
return ekey, fmt.Errorf("marshal public key: %w", err)
}
pubKey := big.NewInt(0)
pubKey.SetBytes(pubkeyData[7:])
sharedSecret := DiffieHellmanSharedSecret(dhKey, pubKey)
sharedKey := sharedSecret.Bytes()
sharedKey = append(sharedKey, dhClientNonce...)
sharedKey = append(sharedKey, paPKASRep.ServerDHNonce...)
var keyType int32
switch asRep.EncPart.EType {
case etypeID.AES256_CTS_HMAC_SHA1_96:
keyType = etypeID.AES256_CTS_HMAC_SHA1_96
sharedKey = truncateKey(sharedKey, 32)
case etypeID.AES128_CTS_HMAC_SHA1_96:
keyType = etypeID.AES128_CTS_HMAC_SHA1_96
sharedKey = truncateKey(sharedKey, 16)
default:
return ekey, fmt.Errorf("PKInit is not implemented for EType %d", asRep.EncPart.EType)
}
return types.EncryptionKey{
KeyType: keyType,
KeyValue: sharedKey,
}, nil
}
func unmarshalFromRawValue(data []byte, v any) error {
var raw asn1.RawValue
rest, err := asn1.Unmarshal(data, &raw)
if err != nil {
return fmt.Errorf("unmarshal raw: %w", err)
}
if len(rest) != 0 {
return fmt.Errorf("remaining data found after unmarshalling raw value")
}
rest, err = asn1.Unmarshal(raw.Bytes, v)
if err != nil {
return err
}
if len(rest) != 0 {
return fmt.Errorf("remaining data found after unmarshalling")
}
return nil
}
func truncateKey(key []byte, size int) []byte {
var output []byte
idx := byte(0)
for len(output) < size {
digest := sha1.Sum(append([]byte{idx}, key...))
if len(output)+len(digest) > size {
output = append(output, digest[:size-len(output)]...)
break
}
output = append(output, digest[:]...)
idx++
}
return output
}
+135
View File
@@ -0,0 +1,135 @@
package pkinit
import (
cryptRand "crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/asn1"
"fmt"
"math/big"
mathRand "math/rand"
"time"
"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"
)
// NewASReq generates an ASReq configured for PKINIT.
func NewASReq(
username string, domain string, cert *x509.Certificate, key *rsa.PrivateKey, dhKey *big.Int, config *config.Config,
) (asReq messages.ASReq, dhClientNonce []byte, err error) {
asReq, err = messages.NewASReqForTGT(domain, config, types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, username))
if err != nil {
return asReq, nil, fmt.Errorf("build generic asreq: %w", err)
}
dhClientNonce = NewDiffieHellmanNonce()
return asReq, dhClientNonce, ConfigureASReq(&asReq, cert, key, dhKey, dhClientNonce)
}
// ConfigureASReq configures an ASReq for PKINIT.
func ConfigureASReq(
asReq *messages.ASReq, cert *x509.Certificate, key *rsa.PrivateKey, dhKey *big.Int, dhClientNonce []byte,
) error {
pkAuthenticatorChecksum, err := calculatePKAuthenticatorChecksum(asReq.ReqBody)
if err != nil {
return fmt.Errorf("calculate checksum: %w", err)
}
publicKey := DiffieHellmanPublicKey(dhKey)
publicKeyBytes, err := asn1.MarshalWithParams(publicKey, "int")
if err != nil {
return err
}
now := time.Now().UTC()
authPack := AuthPack{
PKAuthenticator: PKAuthenticator{
CUSec: int(now.UnixMicro() - now.Truncate(time.Millisecond).UnixMicro()),
CTime: now,
Nonce: mathRand.Intn(4294967295), //nolint:gosec
Checksum: pkAuthenticatorChecksum,
},
ClientPublicValue: SubjectPublicKeyInfo{
Algorithm: AlgorithmIdentifier{
Algorithm: asn1.ObjectIdentifier{1, 2, 840, 10046, 2, 1},
Parameters: DomainParameters{
P: DiffieHellmanPrime,
G: 2,
Q: 0,
},
},
PublicKey: asn1.BitString{
Bytes: publicKeyBytes,
BitLength: len(publicKeyBytes) * 8,
},
},
SupportedCMSTypes: nil,
ClientDHNonce: dhClientNonce,
}
authPackBytes, err := asn1.Marshal(authPack)
if err != nil {
return fmt.Errorf("marshal auth pack: %w", err)
}
signedAuthPack, err := PKCS7Sign(authPackBytes, key, cert)
if err != nil {
return fmt.Errorf("create signed authpack: %w", err)
}
pkASReq := struct {
SignedAuthPack []byte `asn1:"tag:0"`
TrustedIdentifiers types.PrincipalName `asn1:"tag:1,explicit,optional"`
KDCPKID []byte `asn1:"tag:2,optional"`
}{
SignedAuthPack: signedAuthPack,
}
pkASReqBytes, err := asn1.Marshal(pkASReq)
if err != nil {
return fmt.Errorf("marshal ASReq: %w", err)
}
const paDataTypePKASReq = 16
asReq.PAData = append(asReq.PAData, types.PAData{
PADataType: paDataTypePKASReq,
PADataValue: pkASReqBytes,
})
return nil
}
// NewDiffieHellmanNonce generates a nonce for the Diffie Hellman key exchange.
func NewDiffieHellmanNonce() []byte {
return randomBytes(32)
}
func calculatePKAuthenticatorChecksum(asReqBody messages.KDCReqBody) ([]byte, error) {
bodyBytes, err := asReqBody.Marshal()
if err != nil {
return nil, fmt.Errorf("marshal ASReq body: %w", err)
}
hash := sha1.Sum(bodyBytes)
return hash[:], nil
}
func randomBytes(n int) []byte {
b := make([]byte, n)
_, err := cryptRand.Read(b)
if err != nil {
panic(err.Error())
}
return b
}
+43
View File
@@ -0,0 +1,43 @@
package pkinit
import (
"encoding/hex"
"fmt"
"math/big"
)
var (
// DiffieHellmanPrime is the Diffie Hellman prime (P) that is acccepted by PKINIT.
DiffieHellmanPrime = big.NewInt(0)
// DiffieHellmanPrime is the Diffie Hellman base (G) that is acccepted by PKINIT.
DiffieHellmanBase = big.NewInt(2)
)
func init() {
pBytes, err := hex.DecodeString(
"00ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020b" +
"bea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe135" +
"6d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb" +
"5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff")
if err != nil {
panic(fmt.Sprintf("decode Diffie-Hellman parameter p: %v", err))
}
DiffieHellmanPrime.SetBytes(pBytes)
}
// DiffieHellmanPublicKey derives the Diffie Hellman public key from the
// provided private key with the parameters that are accepted by PKINIT.
func DiffieHellmanPublicKey(privateKey *big.Int) *big.Int {
publicKey := new(big.Int).Exp(DiffieHellmanBase, privateKey, DiffieHellmanPrime)
return publicKey
}
// DiffieHellmanSharedSecret derives the Diffie Hellman shared secret with the
// parameters that are accepted by PKINIT.
func DiffieHellmanSharedSecret(privateKey *big.Int, publicKey *big.Int) *big.Int {
sharedSecret := new(big.Int).Exp(publicKey, privateKey, DiffieHellmanPrime)
return sharedSecret
}
+278
View File
@@ -0,0 +1,278 @@
package pkinit
import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/binary"
"fmt"
"io"
"net"
"time"
"github.com/RedTeamPentesting/adauth/ccachetools"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/messages"
)
// DefaultKerberosRoundtripDeadline is the maximum time a roundtrip with the KDC
// can take before it is aborted. This deadline is for each KDC that is
// considered.
var DefaultKerberosRoundtripDeadline = 5 * time.Second
// Authenticate obtains a ticket granting ticket using PKINIT and returns it in
// a CCache which can be serialized using ccachetools.MarshalCCache.
func Authenticate(
ctx context.Context, user string, domain string, cert *x509.Certificate, key *rsa.PrivateKey,
krbConfig *config.Config, opts ...Option,
) (*credentials.CCache, error) {
if user == "" {
return nil, fmt.Errorf("username is empty")
}
if domain == "" {
return nil, fmt.Errorf("domain is empty")
}
dialer, roundtripDeadline, err := processOptions(opts)
if err != nil {
return nil, err
}
asReq, dhClientNonce, err := NewASReq(user, domain, cert, key, key.D, krbConfig)
if err != nil {
return nil, fmt.Errorf("build ASReq: %w", err)
}
asRep, err := ASExchange(ctx, asReq, domain, krbConfig, dialer, roundtripDeadline)
if err != nil {
return nil, fmt.Errorf("exchange: %w", err)
}
_, err = Decrypt(&asRep, key.D, dhClientNonce)
if err != nil {
return nil, fmt.Errorf("decrypt ASRep: %w", err)
}
return ccachetools.NewCCacheFromASRep(asRep)
}
// ASExchange sends a ASReq to the KDC for the provided domain and returns the
// ASRep.
func ASExchange(
ctx context.Context, asReq messages.ASReq, domain string, config *config.Config,
dialer ContextDialer, roundtripDeadline time.Duration,
) (asRep messages.ASRep, err error) {
asReqBytes, err := asReq.Marshal()
if err != nil {
return asRep, fmt.Errorf("marshal ASReq: %w", err)
}
asRepBytes, err := roundtrip(ctx, asReqBytes, config, domain, dialer, roundtripDeadline)
if err != nil {
return asRep, fmt.Errorf("roundtrip: %w", err)
}
err = asRep.Unmarshal(asRepBytes)
if err != nil {
return asRep, fmt.Errorf("unmarshal ASRep: %w", err)
}
return asRep, nil
}
// TGSExchange sends a TGSReq to the KDC for the provided domain and returns the
// TGSRep.
func TGSExchange(
ctx context.Context, tgsReq messages.TGSReq, config *config.Config, domain string,
dialer ContextDialer, roundtripDeadline time.Duration,
) (tgsRep messages.TGSRep, err error) {
asReqBytes, err := tgsReq.Marshal()
if err != nil {
return tgsRep, fmt.Errorf("marshal ASReq: %w", err)
}
asRepBytes, err := roundtrip(ctx, asReqBytes, config, domain, dialer, roundtripDeadline)
if err != nil {
return tgsRep, fmt.Errorf("roundtrip: %w", err)
}
err = tgsRep.Unmarshal(asRepBytes)
if err != nil {
return tgsRep, fmt.Errorf("unmarshal ASRep: %w", err)
}
return tgsRep, nil
}
func roundtrip(
ctx context.Context, request []byte, config *config.Config, domain string,
dialer ContextDialer, roundtripDeadline time.Duration,
) (response []byte, err error) {
_, kdcs, err := config.GetKDCs(domain, true)
if err != nil {
return nil, fmt.Errorf("get KDCs from config: %w", err)
} else if len(kdcs) == 0 {
return nil, fmt.Errorf("no KDCs found in config")
}
for i := 1; i <= len(kdcs); i++ {
if ctx.Err() != nil {
return nil, context.Cause(ctx)
}
response, err = roundtripForSingleKDC(ctx, request, kdcs[i], dialer, roundtripDeadline)
if err == nil {
return response, nil
}
}
switch {
case err != nil:
return nil, err
case ctx.Err() != nil:
return nil, context.Cause(ctx)
default:
return nil, fmt.Errorf("unknown error")
}
}
// ContextDialer is a context aware dialer such as net.Dialer or the SOCKS5
// dialer returned by proxy.SOCKS5.
type ContextDialer interface {
DialContext(ctx context.Context, net string, addr string) (net.Conn, error)
}
func roundtripForSingleKDC(
ctx context.Context, request []byte, address string,
dialer ContextDialer, roundtripDeadline time.Duration,
) ([]byte, error) {
if dialer == nil {
dialer = &net.Dialer{Timeout: roundtripDeadline}
}
ctx, cancel := context.WithTimeout(ctx, roundtripDeadline)
defer cancel()
conn, err := dialer.DialContext(ctx, "tcp", address)
if err != nil {
return nil, fmt.Errorf("dial: %w", err)
}
var (
responseChan = make(chan []byte)
errChan = make(chan error)
)
go func() {
_ = conn.SetDeadline(time.Now().Add(roundtripDeadline))
response, err := sendRecv(conn, request)
_ = conn.Close()
switch {
case err != nil:
errChan <- err
default:
responseChan <- response
}
}()
select {
case response := <-responseChan:
return response, nil
case err := <-errChan:
return nil, err
case <-ctx.Done():
conn.Close() //nolint:gosec
return nil, context.Cause(ctx)
}
}
func sendRecv(conn net.Conn, request []byte) ([]byte, error) {
requestLengthBytes := make([]byte, 4)
binary.BigEndian.PutUint32(requestLengthBytes, uint32(len(request)))
request = append(requestLengthBytes, request...) //nolint:makezero
_, err := conn.Write(request)
if err != nil {
return nil, fmt.Errorf("error sending to KDC (%s): %w", conn.RemoteAddr().String(), err)
}
responseLengthBytes := make([]byte, 4)
_, err = conn.Read(responseLengthBytes)
if err != nil {
return nil, fmt.Errorf("error reading response size header: %w", err)
}
responseLength := binary.BigEndian.Uint32(responseLengthBytes)
responseBytes := make([]byte, responseLength)
_, err = io.ReadFull(conn, responseBytes)
if err != nil {
return nil, fmt.Errorf("error reading response: %w", err)
}
if len(responseBytes) < 1 {
return nil, fmt.Errorf("no response data from KDC %s", conn.RemoteAddr().String())
}
return responseBytes, nil
}
// Option can be passed to a function to modify the default behavior.
type Option interface {
isPKINITOption()
}
type option struct{}
func (option) isPKINITOption() {}
type dialerOption struct {
option
ContextDialer ContextDialer
}
// WithDialer can be used to set a custom dialer for communication with a DC.
func WithDialer(dialer ContextDialer) Option {
return dialerOption{ContextDialer: dialer}
}
type deadlineOption struct {
option
Deadline time.Duration
}
// WithRoundtripDeadline can be used to set a deadline for a single
// request-response roundtrip with a single KDC.
func WithRoundtripDeadline(deadline time.Duration) Option {
return deadlineOption{Deadline: deadline}
}
func processOptions(opts []Option) (dialer ContextDialer, roundtripDeadline time.Duration, err error) {
roundtripDeadline = DefaultKerberosRoundtripDeadline
for _, opt := range opts {
switch o := opt.(type) {
case dialerOption:
dialer = o.ContextDialer
case deadlineOption:
roundtripDeadline = o.Deadline
default:
return nil, 0, fmt.Errorf("unknown option: %T", o)
}
}
if dialer == nil {
dialer = &net.Dialer{Timeout: roundtripDeadline}
}
return dialer, roundtripDeadline, nil
}
+124
View File
@@ -0,0 +1,124 @@
package pkinit
import (
"crypto"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
)
// PKCS7Sign signs the data according to PKCS#7.
func PKCS7Sign(data []byte, key *rsa.PrivateKey, cert *x509.Certificate) ([]byte, error) {
serializedData, err := asn1.Marshal(data)
if err != nil {
return nil, fmt.Errorf("marshal data: %w", err)
}
rawCert, err := RawCertificate(cert)
if err != nil {
return nil, fmt.Errorf("marshal certificate: %w", err)
}
digest := sha1.Sum(data)
serializedDigest, err := asn1.Marshal(digest[:])
if err != nil {
return nil, fmt.Errorf("marshal digest: %w", err)
}
serializedPKInitOID, err := asn1.Marshal(asn1.ObjectIdentifier{1, 3, 6, 1, 5, 2, 3, 1})
if err != nil {
return nil, fmt.Errorf("marshal PKInit OID: %w", err)
}
sha1AlgorithmIdentifier := pkix.AlgorithmIdentifier{
Algorithm: asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26},
}
rsaWithSHA1AlgorithmIdentifier := pkix.AlgorithmIdentifier{
Algorithm: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5},
}
authenticatedAttributes := []Attribute{
{
// ContentType
Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3},
// id-pkinit-authData
Value: asn1.RawValue{Tag: 17, IsCompound: true, Bytes: serializedPKInitOID},
},
{
// MessageDigest
Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4},
Value: asn1.RawValue{Tag: 17, IsCompound: true, Bytes: serializedDigest},
},
}
signature, err := signAuthenicatedAttributes(authenticatedAttributes, key)
if err != nil {
return nil, fmt.Errorf("sign authenticated data: %w", err)
}
signedDataBytes, err := asn1.Marshal(SignedData{
Version: 3,
DigestAlgorithmIdentifiers: []pkix.AlgorithmIdentifier{sha1AlgorithmIdentifier},
ContentInfo: ContentInfo{
// id-pkinit-authData
ContentType: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 2, 3, 1},
Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: serializedData, IsCompound: true},
},
Certificates: rawCert,
SignerInfos: []SignerInfo{
{
Version: 1,
IssuerAndSerialNumber: IssuerAndSerial{
IssuerName: asn1.RawValue{FullBytes: cert.RawIssuer},
SerialNumber: cert.SerialNumber,
},
DigestAlgorithm: sha1AlgorithmIdentifier,
AuthenticatedAttributes: authenticatedAttributes,
DigestEncryptionAlgorithm: rsaWithSHA1AlgorithmIdentifier,
EncryptedDigest: signature,
},
},
})
if err != nil {
return nil, fmt.Errorf("marshal signed data: %w", err)
}
contentInfo := ContentInfo{
// signed data
ContentType: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2},
Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: signedDataBytes, IsCompound: true},
}
signedContent, err := asn1.Marshal(contentInfo)
if err != nil {
return nil, fmt.Errorf("marshal signed content: %w", err)
}
return signedContent, nil
}
func signAuthenicatedAttributes(attrs []Attribute, key *rsa.PrivateKey) ([]byte, error) {
rawAuthenticatedAttributesAsSequence, err := asn1.Marshal(struct {
A []Attribute `asn1:"set"`
}{A: attrs})
if err != nil {
return nil, fmt.Errorf("marshal authenticated data: %w", err)
}
// Remove the leading sequence octets
var rawAuthenticatedAttributes asn1.RawValue
_, err = asn1.Unmarshal(rawAuthenticatedAttributesAsSequence, &rawAuthenticatedAttributes)
if err != nil {
return nil, fmt.Errorf("remove sequence bytes: %w", err)
}
hash := sha1.Sum(rawAuthenticatedAttributes.Bytes)
return rsa.SignPKCS1v15(nil, key, crypto.SHA1, hash[:])
}
+351
View File
@@ -0,0 +1,351 @@
package pkinit
import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/hex"
"fmt"
"os"
"strings"
"github.com/RedTeamPentesting/adauth"
"github.com/RedTeamPentesting/adauth/ccachetools"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/iana/patype"
"github.com/jcmturner/gokrb5/v8/krberror"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/oiweiwei/go-msrpc/msrpc/pac"
"github.com/oiweiwei/go-msrpc/ndr"
)
// UnPACTheHash retrieves the user's NT hash via PKINIT using the provided PFX
// file. The DC argument is optional.
func UnPACTheHashFromPFX(
ctx context.Context, username string, domain string, pfxFile string, pfxPassword string,
dc string, opts ...Option,
) (*credentials.CCache, *Hash, error) {
pfxData, err := os.ReadFile(pfxFile)
if err != nil {
return nil, nil, fmt.Errorf("read PFX file: %w", err)
}
return UnPACTheHashFromPFXData(ctx, username, domain, pfxData, pfxPassword, dc, opts...)
}
// UnPACTheHash retrieves the user's NT hash via PKINIT using the provided PFX
// data. The DC argument is optional.
func UnPACTheHashFromPFXData(
ctx context.Context, username string, domain string, pfxData []byte, pfxPassword string,
dc string, opts ...Option,
) (*credentials.CCache, *Hash, error) {
cred, err := adauth.CredentialFromPFXBytes(username, domain, pfxData, pfxPassword)
if err != nil {
return nil, nil, fmt.Errorf("build credentials from PFX: %w", err)
}
if dc != "" {
cred.SetDC(dc)
}
krbConf, err := cred.KerberosConfig(ctx)
if err != nil {
return nil, nil, fmt.Errorf("configure Kerberos: %w", err)
}
return UnPACTheHash(ctx, cred.Username, cred.Domain, cred.ClientCert, cred.ClientCertKey, krbConf, opts...)
}
// UnPACTheHash retrieves the user's NT hash via PKINIT using the provided
// certificates.
func UnPACTheHash(
ctx context.Context, user string, domain string, cert *x509.Certificate, key *rsa.PrivateKey,
krbConfig *config.Config, opts ...Option,
) (*credentials.CCache, *Hash, error) {
dialer, roundtripDeadline, err := processOptions(opts)
if err != nil {
return nil, nil, err
}
krbConfig = &config.Config{
LibDefaults: krbConfig.LibDefaults,
Realms: krbConfig.Realms,
DomainRealm: krbConfig.DomainRealm,
}
krbConfig.LibDefaults.Proxiable = false
asReq, dhClientNonce, err := NewASReq(user, domain, cert, key, key.D, krbConfig)
if err != nil {
return nil, nil, fmt.Errorf("build ASReq: %w", err)
}
asReq.PAData = append(asReq.PAData, (&PAPACRequest{IncludePAC: true}).AsPAData())
asRep, err := ASExchange(ctx, asReq, domain, krbConfig, dialer, roundtripDeadline)
if err != nil {
return nil, nil, fmt.Errorf("AS exchange: %w", err)
}
pkinitKey, err := Decrypt(&asRep, key.D, dhClientNonce)
if err != nil {
return nil, nil, fmt.Errorf("decrypt ASRep: %w", err)
}
ccache, err := ccachetools.NewCCacheFromASRep(asRep)
if err != nil {
return nil, nil, fmt.Errorf("generate CCache: %w", err)
}
tgsReq, err := messages.NewUser2UserTGSReq(
types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, user), strings.ToUpper(asRep.CRealm), krbConfig,
asRep.Ticket, asRep.DecryptedEncPart.Key, types.NewPrincipalName(nametype.KRB_NT_UNKNOWN, user), false, asRep.Ticket)
if err != nil {
return ccache, nil, fmt.Errorf("generate TGSReq: %w", err)
}
tgsReq.ReqBody.CName = types.PrincipalName{}
tgsReq.PAData, err = generatePAData(user, asRep.Ticket, asRep.DecryptedEncPart.Key)
if err != nil {
return ccache, nil, fmt.Errorf("generate PAData sequence: %w", err)
}
tgsRep, err := TGSExchange(ctx, tgsReq, krbConfig, domain, dialer, roundtripDeadline)
if err != nil {
return ccache, nil, fmt.Errorf("TGS exchange: %w", err)
}
err = tgsRep.DecryptEncPart(asRep.DecryptedEncPart.Key)
if err != nil {
return ccache, nil, fmt.Errorf("decrypt TGSRep: %w", err)
}
err = tgsRep.Ticket.Decrypt(asRep.DecryptedEncPart.Key)
if err != nil {
return ccache, nil, fmt.Errorf("decrypt service ticket: %w", err)
}
ntHash, err := extractNTHash(tgsRep.Ticket, pkinitKey)
if err != nil {
return nil, nil, fmt.Errorf("extract NT hash: %w", err)
}
return ccache, ntHash, nil
}
func extractNTHash(
tkt messages.Ticket, pkinitKey types.EncryptionKey,
) (hash *Hash, err error) {
ifRelevant := types.ADIfRelevant{}
for _, authData := range tkt.DecryptedEncPart.AuthorizationData {
if authData.ADType != 1 {
continue
}
_, err = asn1.Unmarshal(authData.ADData, &ifRelevant)
if err != nil {
return nil, fmt.Errorf("unmarshal ADIfRelevant container: %w", err)
}
}
if len(ifRelevant) == 0 {
return nil, fmt.Errorf("no ADIfRelevant container present")
}
var pacData []byte
for _, authData := range ifRelevant {
if authData.ADType != 128 {
continue
}
pacData = authData.ADData
}
if len(pacData) == 0 {
return nil, fmt.Errorf("no PACTYPE container present: %w", err)
}
pacType := pac.PACType{}
err = ndr.Unmarshal(pacData, &pacType, ndr.Opaque)
if err != nil {
return nil, fmt.Errorf("unmarshal PACTYPE: %w", err)
}
var pacCredentialInfoBuffer []byte
for _, buffer := range pacType.Buffers {
if buffer.Type != 2 {
continue
}
if buffer.Offset+uint64(buffer.BufferLength) > uint64(len(pacData)) {
return nil, fmt.Errorf("PAC_CREDENTIAL_INFO buffer offset and length are outside of data range")
}
pacCredentialInfoBuffer = pacData[buffer.Offset : buffer.Offset+uint64(buffer.BufferLength)]
}
if len(pacCredentialInfoBuffer) == 0 {
return nil, fmt.Errorf("could not find PAC_CREDENTIAL_INFO buffer")
}
credInfo := pac.PACCredentialInfo{}
err = ndr.Unmarshal(pacCredentialInfoBuffer, &credInfo, ndr.Opaque)
if err != nil {
return nil, fmt.Errorf("unmarshal PAC_CREDENTIAL_INFO: %w", err)
}
credData, err := credInfo.DecryptCredentialData(pkinitKey)
if err != nil {
return nil, fmt.Errorf("decrypt PAC_CREDENTIAL_DATA: %w", err)
}
for _, cred := range credData.Credentials {
if cred.NTLMSupplementalCredential == nil {
continue
}
return NewHash(cred.NTLMSupplementalCredential)
}
return nil, fmt.Errorf("could not find NTLM_SUPPLEMENTAL_CREDENTIAL in SECPKG_SUPPLEMENTAL_CRED array")
}
func generatePAData(
username string, tgt messages.Ticket, sessionKey types.EncryptionKey,
) (paData types.PADataSequence, err error) {
auth, err := types.NewAuthenticator(tgt.Realm, types.NewPrincipalName(nametype.KRB_NT_UNKNOWN, username))
if err != nil {
return paData, krberror.Errorf(err, krberror.KRBMsgError, "error generating new authenticator")
}
auth.SeqNumber = 0
apReq, err := messages.NewAPReq(tgt, sessionKey, auth)
if err != nil {
return paData, krberror.Errorf(err, krberror.KRBMsgError, "error generating new AP_REQ")
}
apb, err := apReq.Marshal()
if err != nil {
return paData, krberror.Errorf(err, krberror.EncodingError, "error marshaling AP_REQ for pre-authentication data")
}
return types.PADataSequence{types.PAData{
PADataType: patype.PA_TGS_REQ,
PADataValue: apb,
}}, nil
}
// Hash represents LM and NT password hashes.
type Hash struct {
nt []byte
lm []byte
}
func NewHash(ntlmSupplementalCredential *pac.NTLMSupplementalCredential) (*Hash, error) {
hash := &Hash{}
if ntlmSupplementalCredential.Flags&1 > 0 {
hash.lm = ntlmSupplementalCredential.LMPassword
}
if ntlmSupplementalCredential.Flags&2 > 0 {
hash.nt = ntlmSupplementalCredential.NTPassword
}
// according to the flags, there are no hashes, but we better check for ourselves
if hash.Empty() {
if len(ntlmSupplementalCredential.NTPassword) == 16 && nonZero(ntlmSupplementalCredential.NTPassword) {
hash.nt = ntlmSupplementalCredential.NTPassword
}
if len(ntlmSupplementalCredential.LMPassword) == 16 && nonZero(ntlmSupplementalCredential.LMPassword) {
hash.lm = ntlmSupplementalCredential.LMPassword
}
}
if hash.Empty() {
return nil, fmt.Errorf("NTLM_SUPPLEMENTAL_CREDENTIAL does not contain hashes "+
"(Flags: 0x%x, NTPassword: 0x%x, LMPassword 0x%x)",
ntlmSupplementalCredential.Flags,
ntlmSupplementalCredential.NTPassword, ntlmSupplementalCredential.LMPassword)
}
return hash, nil
}
// NT returns the hex-encoded NT hash or an empty string if no NT hash is
// present.
func (h *Hash) NT() string {
return hex.EncodeToString(h.nt)
}
// NTBytes returns the binary NT hash or an empty slice if no NT hash is
// present.
func (h *Hash) NTBytes() []byte {
return h.nt
}
// NTPresent indicates whether or not an NT hash is present.
func (h *Hash) NTPresent() bool {
return h.nt != nil
}
// LM returns the hex-encoded LM hash or an empty string if no LM hash is
// present.
func (h *Hash) LM() string {
return hex.EncodeToString(h.lm)
}
// LMPresent indicates whether or not an LM hash is present.
func (h *Hash) LMPresent() bool {
return h.lm != nil
}
// LMBytes returns the binary LM hash or an empty slice if no LM hash is
// present.
func (h *Hash) LMBytes() []byte {
return h.lm
}
// Empty returns true if the structure contains neither LM nor NT hash data.
func (h *Hash) Empty() bool {
return h.nt == nil && h.lm == nil
}
// Combined returns the hex-encoded hashes in LM:NT format. If any of these
// hashes is not present, they are replaced by their respective empty hash
// value.
func (h *Hash) Combined() string {
lm := h.LM()
if lm == "" {
lm = "aad3b435b51404eeaad3b435b51404ee"
}
nt := h.NT()
if nt == "" {
nt = "31d6cfe0d16ae931b73c59d7e0c089c0"
}
return lm + ":" + nt
}
func nonZero(d []byte) bool {
for i := 0; i < len(d); i++ {
if d[i] != 0 {
return true
}
}
return false
}
+113
View File
@@ -0,0 +1,113 @@
package adauth
import (
"context"
"fmt"
"net"
"strings"
)
type Resolver interface {
LookupAddr(ctx context.Context, addr string) ([]string, error)
LookupIP(ctx context.Context, network string, host string) ([]net.IP, error)
LookupSRV(ctx context.Context, service string, proto string, name string) (string, []*net.SRV, error)
}
var _ Resolver = net.DefaultResolver
type resolver struct {
Resolver
debugFn func(string, ...any)
}
func ensureResolver(r Resolver, debug func(string, ...any)) *resolver {
if r == nil {
return &resolver{Resolver: net.DefaultResolver, debugFn: debug}
}
return &resolver{Resolver: r, debugFn: debug}
}
func (r *resolver) debug(format string, a ...any) {
if r.debugFn == nil {
return
}
r.debugFn(format, a...)
}
func (r *resolver) LookupFirstService(ctx context.Context, protocol string, domain string) (string, int, error) {
_, addrs, err := r.Resolver.LookupSRV(ctx, protocol, "tcp", domain)
if err != nil {
if strings.EqualFold(protocol, "ldaps") {
host, _, srvLDAPErr := r.LookupFirstService(ctx, "ldap", domain)
if srvLDAPErr == nil {
return host, 636, nil
}
}
return "", 0, fmt.Errorf("lookup %q service of domain %q: %w", protocol, domain, err)
}
if len(addrs) == 0 {
return "", 0, fmt.Errorf("no %q services were discovered for domain %q", protocol, domain)
}
return strings.TrimRight(addrs[0].Target, "."), int(addrs[0].Port), nil
}
func (r *resolver) LookupDCByDomain(ctx context.Context, domain string) (string, error) {
// Unfortunately, Go does not implement SOA lookups, so we lookup the domain
// for DC IPs and reverse lookup their hostnames instead.
dcAddrs, err := r.Resolver.LookupIP(context.Background(), "ip", domain)
if err != nil {
return "", fmt.Errorf("lookup domain itself: %w", err)
}
if len(dcAddrs) == 0 {
return "", fmt.Errorf("looking up domain itself returned no results")
}
dcAddr := dcAddrs[0].String()
names, err := r.Resolver.LookupAddr(context.Background(), dcAddr)
if err == nil {
domain, names = splitResultsInDomainAndHostname(names, domain)
switch {
case len(names) > 0:
dcAddr = strings.TrimRight(names[0], ".")
case domain != "":
dcAddr = domain
r.debug("Warning: reverse lookup of DC only returned domain name, using domain name %q as DC hostname",
domain)
default:
r.debug("Warning: reverse lookup of DC did not contain a hostname")
}
} else {
r.debug("Warning: could not reverse lookup DC IP: %v", err)
}
return dcAddr, nil
}
// A reverse lookup of the DC IP returns both the DCs hostname and the name of
// the domain. This function splits the results in these categories.
func splitResultsInDomainAndHostname(
hostnames []string, domain string,
) (domainFromHostnames string, filtered []string) {
var d string
for _, hostname := range hostnames {
hostname = strings.TrimRight(hostname, ".")
if strings.EqualFold(hostname, domain) {
d = hostname
} else {
filtered = append(filtered, hostname)
}
}
return d, filtered
}
+98
View File
@@ -0,0 +1,98 @@
package adauth_test
import (
"context"
"fmt"
"net"
"github.com/RedTeamPentesting/adauth"
)
type testResolver struct {
HostToAddr map[string][]net.IP
AddrToHost map[string][]string
SRV map[string]map[string]map[string]struct {
Name string
SRV []*net.SRV
}
Error error
}
func (r *testResolver) LookupAddr(ctx context.Context, addr string) ([]string, error) {
if r.Error != nil {
return nil, r.Error
}
if r.AddrToHost == nil {
return nil, nil
}
return r.AddrToHost[addr], nil
}
func (r *testResolver) LookupIP(ctx context.Context, network string, host string) ([]net.IP, error) {
if r.Error != nil {
return nil, r.Error
}
if r.HostToAddr == nil {
return nil, nil
}
addrs := r.HostToAddr[host]
switch network {
case "ip":
return addrs, nil
case "ip4":
var ipv4s []net.IP
for _, addr := range addrs {
if addr.To4() != nil {
ipv4s = append(ipv4s, addr)
}
}
return ipv4s, nil
case "ip6":
var ipv6s []net.IP
for _, addr := range addrs {
if addr.To4() == nil {
ipv6s = append(ipv6s, addr)
}
}
return ipv6s, nil
default:
return nil, fmt.Errorf("invalid network: %q", network)
}
}
func (r *testResolver) LookupSRV(
ctx context.Context, service string, proto string, name string,
) (string, []*net.SRV, error) {
if r.Error != nil {
return "", nil, r.Error
}
if r.SRV == nil {
return "", nil, nil
}
srvForService := r.SRV[service]
if srvForService == nil {
return "", nil, nil
}
srvForServiceAndProto := srvForService[proto]
if srvForServiceAndProto == nil {
return "", nil, nil
}
record := srvForServiceAndProto[name]
return record.Name, record.SRV, nil
}
var _ adauth.Resolver = &testResolver{}
+188
View File
@@ -0,0 +1,188 @@
package adauth
import (
"context"
"fmt"
"net"
"strings"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/types"
)
// Target holds information about the authentication target.
type Target struct {
// Port holds the target's port which may be empty.
Port string
addr string
hostname string
ip net.IP
// UseKerberos indicated that Kerberos authentication should be used to
// authenticate to this target.
UseKerberos bool
// Protocol is a string that represents the protocol that is used when
// communicating with this target. It is used to construct the SPN, however,
// some protocol name corrections may be applied in this case, such as 'smb'
// -> 'cifs'.
Protocol string
ccache string
// Resolver can be used to set an alternative DNS resolver. If empty,
// net.DefaultResolver is used.
Resolver Resolver
}
// NewTarget creates a new target. The provided protocol is used to construct
// the SPN, however, some protocol name corrections may be applied in this case,
// such as 'smb' -> 'cifs'. The target parameter may or may not contain a port
// and the protocol string will *not* influence the port of the resulting
// Target.
func NewTarget(protocol string, target string) *Target {
return newTarget(protocol, target, false, "", nil)
}
func newTarget(protocol string, target string, useKerberos bool, ccache string, resolver Resolver) *Target {
host, port, err := net.SplitHostPort(target)
if err != nil {
host = target
}
t := &Target{
addr: host,
Port: port,
Protocol: protocol,
UseKerberos: useKerberos,
ccache: ccache,
Resolver: resolver,
}
ip := net.ParseIP(host)
if ip == nil {
t.hostname = host
} else {
t.ip = ip
}
return t
}
// Address returns the address including the port if available. It will contain
// either a hostname or an IP address depending on how the target was
// constructed.
func (t *Target) Address() string {
if t.Port != "" {
return net.JoinHostPort(t.addr, t.Port)
} else {
return t.addr
}
}
// AddressWithoutPort is like Address but without the port.
func (t *Target) AddressWithoutPort() string {
return t.addr
}
// IP returns the target's IP address. If only the hostname is known, a lookup
// will be performed.
func (t *Target) IP(ctx context.Context) (net.IP, error) {
if t.ip != nil {
return t.ip, nil
}
if t.hostname == "" {
return nil, fmt.Errorf("no IP or hostname known")
}
addrs, err := ensureResolver(t.Resolver, nil).LookupIP(ctx, "ip", t.hostname)
switch {
case err != nil:
return nil, fmt.Errorf("lookup %s: %w", t.hostname, err)
case len(addrs) > 1:
return nil, fmt.Errorf("lookup of %s returned multiple names", t.hostname)
case len(addrs) == 0:
return nil, fmt.Errorf("lookup of %s returned no names", t.hostname)
}
t.ip = addrs[0]
return addrs[0], nil
}
// Hostname returns the target's hostname. If only the IP address is known, a
// lookup will be performed.
func (t *Target) Hostname(ctx context.Context) (string, error) {
if t.hostname != "" {
return t.hostname, nil
}
if t.ip == nil {
return "", fmt.Errorf("no IP or hostname known")
}
names, err := ensureResolver(t.Resolver, nil).LookupAddr(ctx, t.ip.String())
switch {
case err != nil:
return "", fmt.Errorf("reverse lookup %s: %w", t.ip, err)
case len(names) > 1:
return "", fmt.Errorf("reverse lookup of %s returned multiple names", t.ip)
case len(names) == 0:
return "", fmt.Errorf("reverse lookup of %s returned no names", t.ip)
}
t.hostname = strings.TrimRight(names[0], ".")
return t.hostname, nil
}
// SPN returns the target's service principal name. The protocol part of the SPN
// *may* be changed to the generic 'host' in order to align with the SPN of
// service tickets in a CCACHE file. Some protocol name translations will also
// be applied such as 'smb' -> 'cifs'.
func (t *Target) SPN(ctx context.Context) (string, error) {
hostname, err := t.Hostname(ctx)
if err != nil {
return "", err
}
var spn string
switch strings.ToLower(t.Protocol) {
case "smb":
spn = "cifs/" + hostname
case "ldap", "ldaps":
spn = "ldap/" + hostname
case "http", "https":
spn = "http/" + hostname
case "kerberos":
spn = "krbtgt"
case "":
spn = "host/" + hostname
default:
spn = t.Protocol + `/` + hostname
}
if t.ccache == "" || strings.HasPrefix(spn, "host/") {
return spn, nil
}
ccache, err := credentials.LoadCCache(t.ccache)
if err != nil {
return spn, nil //nolint:nilerr
}
_, ok := ccache.GetEntry(types.NewPrincipalName(nametype.KRB_NT_SRV_INST, spn))
if ok {
return spn, nil
}
// change SPN to generic host SPN if a ticket exists ONLY for the generic host SPN
_, ok = ccache.GetEntry(types.NewPrincipalName(nametype.KRB_NT_SRV_INST, "host/"+hostname))
if ok {
return "host/" + hostname, nil
}
return spn, nil
}
+172
View File
@@ -0,0 +1,172 @@
package adauth_test
import (
"context"
"net"
"strconv"
"testing"
"github.com/RedTeamPentesting/adauth"
)
func TestNewTarget(t *testing.T) {
targetIP := net.ParseIP("10.0.0.1")
targetPort := "1234"
targetHostname := "computer.tld"
resolver := &testResolver{
HostToAddr: map[string][]net.IP{
targetHostname: {targetIP},
},
AddrToHost: map[string][]string{
targetIP.String(): {targetHostname},
},
}
t.Run("hostname_from_ip", func(t *testing.T) {
target := adauth.NewTarget("", targetIP.String())
target.Resolver = resolver
if target.Address() != targetIP.String() {
t.Errorf("target address is %q instead of %q", target.Address(), targetIP.String())
}
hostname, err := target.Hostname(context.Background())
if err != nil {
t.Errorf("get hostname: %v", err)
}
if hostname != targetHostname {
t.Errorf("hostname is %q instead of %q", hostname, targetHostname)
}
})
t.Run("ip_from_hostname", func(t *testing.T) {
target := adauth.NewTarget("", targetHostname)
target.Resolver = resolver
if target.Address() != targetHostname {
t.Errorf("target address is %q instead of %q", target.Address(), targetIP.String())
}
ip, err := target.IP(context.Background())
if err != nil {
t.Errorf("get hostname: %v", err)
}
if ip.String() != targetIP.String() {
t.Errorf("hostname is %q instead of %q", ip.String(), targetIP.String())
}
})
t.Run("hostname_and_port", func(t *testing.T) {
target := adauth.NewTarget("", net.JoinHostPort(targetHostname, targetPort))
target.Resolver = resolver
if target.Port != targetPort {
t.Errorf("target port is %q instead of %q", target.Port, targetPort)
}
_, port, err := net.SplitHostPort(target.Address())
if err != nil {
t.Fatalf("split target.Address(): %v", err)
}
if port != targetPort {
t.Errorf("target.Address() contains port %q instead of %q", port, targetPort)
}
})
t.Run("ip_and_port", func(t *testing.T) {
target := adauth.NewTarget("", net.JoinHostPort(targetIP.String(), targetPort))
target.Resolver = resolver
if target.Port != targetPort {
t.Errorf("target port is %q instead of %q", target.Port, targetPort)
}
_, port, err := net.SplitHostPort(target.Address())
if err != nil {
t.Fatalf("split target.Address(): %v", err)
}
if port != targetPort {
t.Errorf("target.Address() contains port %q instead of %q", port, targetPort)
}
})
}
func TestTargetAddressWithAndWithoutPort(t *testing.T) {
targetHostname := "computer.tld"
t.Run("with_port", func(t *testing.T) {
target := adauth.NewTarget("ldap", targetHostname+":389")
if target.Address() != targetHostname+":389" {
t.Errorf("target.Address() is %q instead of %q", target.Address(), targetHostname)
}
if target.AddressWithoutPort() != targetHostname {
t.Errorf("target.AddressWithoutPort() is %q instead of %q", target.AddressWithoutPort(), targetHostname)
}
})
t.Run("without_port", func(t *testing.T) {
target := adauth.NewTarget("ldap", targetHostname)
if target.Address() != targetHostname {
t.Errorf("target.Address() is %q instead of %q", target.Address(), targetHostname)
}
if target.AddressWithoutPort() != targetHostname {
t.Errorf("target.AddressWithoutPort() is %q instead of %q", target.AddressWithoutPort(), targetHostname)
}
})
}
func TestNewTargetSPN(t *testing.T) {
hostname := "computer.tld"
testCases := []struct {
InputProtocol string
SPNProtocol string
}{
{
InputProtocol: "",
SPNProtocol: "host",
},
{
InputProtocol: "ldaps",
SPNProtocol: "ldap",
},
{
InputProtocol: "smb",
SPNProtocol: "cifs",
},
{
InputProtocol: "foo",
SPNProtocol: "foo",
},
{
InputProtocol: "http",
SPNProtocol: "http",
},
{
InputProtocol: "https",
SPNProtocol: "http",
},
}
for i, testCase := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
spn, err := adauth.NewTarget(testCase.InputProtocol, hostname).SPN(context.Background())
if err != nil {
t.Fatalf("SPN: %v", err)
}
expectedSPN := testCase.SPNProtocol + `/` + hostname
if spn != expectedSPN {
t.Fatalf("SPN for %q is %q instead of %q", testCase.InputProtocol, spn, expectedSPN)
}
})
}
}
View File
Binary file not shown.
+32
View File
@@ -0,0 +1,32 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"go.useLanguageServer": true,
"go.lintTool": "golangci-lint",
"gopls": {
"gofumpt": true,
},
"go.formatTool": "gofumpt",
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
},
},
"[go.mod]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
},
}
},
"extensions": {
"recommendations": [
"golang.go"
]
}
}