Skip to content

Commit 5bb0ecc

Browse files
Generalize LLMNR response name spoofing for DNS
1 parent 76870b0 commit 5bb0ecc

8 files changed

Lines changed: 131 additions & 43 deletions

File tree

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,6 @@ For more information, run `pretender --help`.
110110
- If you only want to perform Kerberos relaying via dynamic updates you can
111111
specify `--no-lnr` and `--spoof-types SOA` to ignore any queries that are
112112
unrelated to the attack.
113-
- Kerberos relaying via spoofed LLMNR names (`--spoof-llmnr-name`) is more
114-
effective when mDNS and NetBIOS-NS are disabled (`--no-netbios --no-mdns`).
115113
- When conducting a Kerberos relay attack where `krbrelayx.py` runs on a
116114
different host than pretender (relay IPv4 address points to different host
117115
that runs `krbrelayx.py`), the host running `krbrelayx.py` will also need to
@@ -162,7 +160,7 @@ vendorInterface
162160
vendorRelayIPv4
163161
vendorRelayIPv6
164162
vendorSOAHostname
165-
vendorSpoofLLMNRName
163+
vendorSpoofResponseName
166164
vendorNoDHCPv6DNSTakeover
167165
vendorNoDHCPv6
168166
vendorNoDNS

cli.go

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@ var stdErr = os.Stderr // this is used to make stderr redirectable without side
1818

1919
// Config holds the configuration.
2020
type Config struct {
21-
RelayIPv4 net.IP
22-
RelayIPv6 net.IP
23-
SOAHostname string
24-
SpoofLLMNRName string
25-
Interface *net.Interface
26-
TTL time.Duration
27-
LeaseLifetime time.Duration
28-
RouterLifetime time.Duration
29-
LocalIPv6 net.IP
30-
RAPeriod time.Duration
31-
StatelessRA bool
32-
DNSTimeout time.Duration
21+
RelayIPv4 net.IP
22+
RelayIPv6 net.IP
23+
SOAHostname string
24+
SpoofResponseName string
25+
Interface *net.Interface
26+
TTL time.Duration
27+
LeaseLifetime time.Duration
28+
RouterLifetime time.Duration
29+
LocalIPv6 net.IP
30+
RAPeriod time.Duration
31+
StatelessRA bool
32+
DNSTimeout time.Duration
3333

3434
NoDHCPv6DNSTakeover bool
3535
NoDHCPv6 bool
@@ -147,16 +147,12 @@ func (c Config) PrintSummary() {
147147
}
148148
}
149149

150-
if c.SpoofLLMNRName != "" {
151-
fmt.Println("LLMNR response names spoofed as:", c.SpoofLLMNRName)
150+
if c.SpoofResponseName != "" {
151+
fmt.Println("DNS/LLMNR response names spoofed as:", c.SpoofResponseName)
152152

153-
switch {
154-
case c.NoLLMNR:
155-
fmt.Println(c.style(fgYellow, bold) + "Warning:" + c.style(reset) + c.style(fgYellow) +
156-
" LLMNR spoofing is enabled but LLMNR itself is disabled" + c.style())
157-
case !c.NoNetBIOS, !c.NoMDNS:
153+
if c.NoLLMNR && c.NoDNS {
158154
fmt.Println(c.style(fgYellow, bold) + "Warning:" + c.style(reset) + c.style(fgYellow) +
159-
" LLMNR name spoofing is more effective when mDNS and NetBIOS-NS are disabled" + c.style())
155+
" Response name spoofing is enabled but LLMNR and DNS are disabled" + c.style())
160156
}
161157
}
162158

@@ -244,8 +240,8 @@ func configFromCLI() (config *Config, logger *Logger, err error) {
244240
"Relay IPv6 address with which queries are answered, supports\nauto-detection by interface")
245241
pflag.StringVar(&config.SOAHostname, "soa-hostname", defaultSOAHostname,
246242
"Hostname for the SOA record (useful for Kerberos relaying)")
247-
pflag.StringVar(&config.SpoofLLMNRName, "spoof-llmnr-name", defaultSpoofLLMNRName,
248-
"Spoof name LLMNR replies to influnce SPNs (it is recommended to disable mDNS/NetBIOS-NS)")
243+
pflag.StringVar(&config.SpoofResponseName, "spoof-response-name", defaultSpoofResponseName,
244+
"Spoof response name to influnce SPNs (works with DNS and LLMNR, NetBIOS and mDNS will be ignored)")
249245

250246
pflag.BoolVar(&config.NoDHCPv6DNSTakeover, "no-dhcp-dns", defaultNoDHCPv6DNSTakeover,
251247
"Disable DHCPv6 DNS takeover attack (DHCPv6 and DNS, mutually\nexlusive with --stateless-ra)")

defaults.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ var (
1414
// vendors can set the following values to tweak the default configuration
1515
// during compilation as with -ldflags "-X main.vendorInterface=eth1".
1616

17-
vendorInterface = ""
18-
vendorRelayIPv4 = ""
19-
vendorRelayIPv6 = ""
20-
vendorSOAHostname = ""
21-
vendorSpoofLLMNRName = ""
17+
vendorInterface = ""
18+
vendorRelayIPv4 = ""
19+
vendorRelayIPv6 = ""
20+
vendorSOAHostname = ""
21+
vendorSpoofResponseName = ""
2222

2323
vendorNoDHCPv6DNSTakeover = ""
2424
vendorNoDHCPv6 = ""
@@ -63,11 +63,11 @@ var (
6363
)
6464

6565
var (
66-
defaultInterface = vendorInterface
67-
defaultRelayIPv4 = forceIP(vendorRelayIPv4, nil)
68-
defaultRelayIPv6 = forceIP(vendorRelayIPv6, nil)
69-
defaultSOAHostname = vendorSOAHostname
70-
defaultSpoofLLMNRName = vendorSpoofLLMNRName
66+
defaultInterface = vendorInterface
67+
defaultRelayIPv4 = forceIP(vendorRelayIPv4, nil)
68+
defaultRelayIPv6 = forceIP(vendorRelayIPv6, nil)
69+
defaultSOAHostname = vendorSOAHostname
70+
defaultSpoofResponseName = vendorSpoofResponseName
7171

7272
defaultNoDHCPv6DNSTakeover = forceBool(vendorNoDHCPv6DNSTakeover, false)
7373
defaultNoDHCPv6 = forceBool(vendorNoDHCPv6, false)

dns.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ func createDNSReplyFromRequest(
6969
questionName := normalizedNameFromQuery(q, handlerType)
7070
allQuestions = append(allQuestions, fmt.Sprintf("%q (%s)", questionName, queryType(q, request.Opcode)))
7171

72-
shouldRespond, reason := shouldRespondToNameResolutionQuery(config, questionName, q.Qtype, peer, peerHostnames)
72+
shouldRespond, reason := shouldRespondToNameResolutionQuery(
73+
config, questionName, q.Qtype, peer, peerHostnames, handlerType)
7374
if !shouldRespond {
7475
answers := handleIgnored(logger, q, questionName, queryType(q, request.Opcode),
7576
peer, reason, handlerType, rw.RemoteAddr().Network(), delegateQuestion)
@@ -79,8 +80,8 @@ func createDNSReplyFromRequest(
7980
}
8081

8182
answerName := q.Name
82-
if handlerType == HandlerTypeLLMNR && config.SpoofLLMNRName != "" {
83-
answerName = dns.Fqdn(config.SpoofLLMNRName)
83+
if config.SpoofResponseName != "" {
84+
answerName = dns.Fqdn(config.SpoofResponseName)
8485
}
8586

8687
switch q.Qtype {
@@ -160,7 +161,6 @@ func createDNSReplyFromRequest(
160161
}
161162
}
162163

163-
// don't send a reply at all if we don't actually spoof anything
164164
if len(reply.Answer) == 0 && len(reply.Ns) == 0 && len(reply.Extra) == 0 &&
165165
(handlerType != HandlerTypeDNS || config.DontSendEmptyReplies) {
166166
logger.Debugf("ignoring query for %s from %s because no answers were configured",

dns_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,37 @@ func TestDelegatedQueryUDP(t *testing.T) {
355355
}
356356
}
357357

358+
func TestDNSResponseNameSpoofing(t *testing.T) {
359+
aQuery := &dns.Msg{}
360+
aQuery.SetQuestion("test", dns.TypeAAAA)
361+
362+
relayIP := mustParseIP(t, "fe80::1")
363+
mockRW := mockResonseWriter{Remote: &net.UDPAddr{IP: mustParseIP(t, "10.0.0.1")}}
364+
365+
cfg := &Config{RelayIPv6: relayIP, TTL: 60 * time.Second, SpoofResponseName: "spoofedname"}
366+
367+
reply := createDNSReplyFromRequest(mockRW, aQuery, nil, cfg, HandlerTypeDNS, nil)
368+
if reply == nil {
369+
t.Fatalf("no message was created")
370+
}
371+
372+
if len(reply.Question) != 1 {
373+
t.Fatalf("reply does %d questions instead of 1", len(reply.Question))
374+
}
375+
376+
if reply.Question[0].Name != "test" {
377+
t.Fatalf("reply contains question %q instead of %q", reply.Question[0].Name, "test.")
378+
}
379+
380+
if len(reply.Answer) != 1 {
381+
t.Fatalf("reply contains %d answers instead of 1", len(reply.Answer))
382+
}
383+
384+
if reply.Answer[0].Header().Name != "spoofedname." {
385+
t.Fatalf("reply answer name is %q instead of %q", reply.Answer[0].Header().Name, "spoofedname.")
386+
}
387+
}
388+
358389
func TestDelegatedQueryTCP(t *testing.T) {
359390
aQuery := &dns.Msg{}
360391
aQuery.SetQuestion("host", dns.TypeA)

filter.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func containsDomain(haystack []string, needle string) bool {
4141
}
4242

4343
func shouldRespondToNameResolutionQuery(config *Config, host string, queryType uint16,
44-
from net.IP, fromHostnames []string,
44+
from net.IP, fromHostnames []string, handlerType HandlerType,
4545
) (bool, string) {
4646
if config.spoofingTemporarilyDisabled {
4747
return false, "spoofing is temporarily disabled"
@@ -51,6 +51,10 @@ func shouldRespondToNameResolutionQuery(config *Config, host string, queryType u
5151
return false, "dry mode"
5252
}
5353

54+
if config.SpoofResponseName != "" && (handlerType == HandlerTypeMDNS || handlerType == HandlerTypeNetBIOS) {
55+
return false, "response name spoofing not supported for " + string(handlerType)
56+
}
57+
5458
if strings.HasPrefix(strings.ToLower(host), isatapHostname) {
5559
return false, "ISATAP is always ignored"
5660
}

filter_test.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func TestFilterNameResolutionQuery(t *testing.T) { //nolint:maintidx
2626
NoRelayIPv4Configured bool
2727
NoRelayIPv6Configured bool
2828
SpoofingTemporarilyDisabled bool
29+
SpoofResponseName string
2930

3031
Host string
3132
QueryType uint16 // defaults to A
@@ -350,6 +351,38 @@ func TestFilterNameResolutionQuery(t *testing.T) { //nolint:maintidx
350351
SpoofingTemporarilyDisabled: true,
351352
ShouldRespond: false,
352353
},
354+
{
355+
TestName: "ignore mDNS when response name spoofing is active",
356+
Host: "foo",
357+
From: someIP,
358+
SpoofResponseName: "test",
359+
HandlerType: HandlerTypeMDNS,
360+
ShouldRespond: false,
361+
},
362+
{
363+
TestName: "ignore NetBIOS when response name spoofing is active",
364+
Host: "foo",
365+
From: someIP,
366+
SpoofResponseName: "test",
367+
HandlerType: HandlerTypeNetBIOS,
368+
ShouldRespond: false,
369+
},
370+
{
371+
TestName: "do not ignore DNS when response name spoofing is active",
372+
Host: "foo",
373+
From: someIP,
374+
SpoofResponseName: "test",
375+
HandlerType: HandlerTypeDNS,
376+
ShouldRespond: true,
377+
},
378+
{
379+
TestName: "do not ignore LLMNR when response name spoofing is active",
380+
Host: "foo",
381+
From: someIP,
382+
SpoofResponseName: "test",
383+
HandlerType: HandlerTypeLLMNR,
384+
ShouldRespond: true,
385+
},
353386
}
354387

355388
hostMatcherLookupFunction = func(host string, _ time.Duration) ([]net.IP, error) {
@@ -385,6 +418,7 @@ func TestFilterNameResolutionQuery(t *testing.T) { //nolint:maintidx
385418
DryWithDHCPv6Mode: testCase.DryWithDHCPv6Mode,
386419
SpoofTypes: types,
387420
SOAHostname: "test",
421+
SpoofResponseName: testCase.SpoofResponseName,
388422
spoofingTemporarilyDisabled: testCase.SpoofingTemporarilyDisabled,
389423
}
390424

@@ -407,7 +441,8 @@ func TestFilterNameResolutionQuery(t *testing.T) { //nolint:maintidx
407441
}
408442

409443
shouldRespond, _ := shouldRespondToNameResolutionQuery(cfg,
410-
normalizedName(testCase.Host, handlerType), testCase.QueryType, testCase.From, testCase.FromHostnames)
444+
normalizedName(testCase.Host, handlerType),
445+
testCase.QueryType, testCase.From, testCase.FromHostnames, testCase.HandlerType)
411446
if shouldRespond != testCase.ShouldRespond {
412447
t.Errorf("shouldRespondToNameResolutionQuery returned %v instead of %v",
413448
shouldRespond, testCase.ShouldRespond)

local_name_resolution_test.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ func TestNetBIOS(t *testing.T) {
8181
}
8282
}
8383

84-
func TestLLMNRNameSpoofing(t *testing.T) {
84+
func TestLLMNRResponseNameSpoofing(t *testing.T) {
8585
relayIP := mustParseIP(t, "fe80::1")
8686
mockRW := mockResonseWriter{Remote: &net.UDPAddr{IP: mustParseIP(t, "10.0.0.1")}}
8787
request := readNameServiceMessage(t, "testdata/llmnr_request.bin")
88-
cfg := &Config{RelayIPv6: relayIP, TTL: 60 * time.Second, SpoofLLMNRName: "spoofedname"}
88+
cfg := &Config{RelayIPv6: relayIP, TTL: 60 * time.Second, SpoofResponseName: "spoofedname"}
8989

9090
reply := createDNSReplyFromRequest(mockRW, request, nil, cfg, HandlerTypeLLMNR, nil)
9191
if reply == nil {
@@ -109,6 +109,30 @@ func TestLLMNRNameSpoofing(t *testing.T) {
109109
}
110110
}
111111

112+
func TestMDNSNoResponseNameSpoofing(t *testing.T) {
113+
relayIP := mustParseIP(t, "fe80::1")
114+
mockRW := mockResonseWriter{Remote: &net.UDPAddr{IP: mustParseIP(t, "10.0.0.1")}}
115+
request := readNameServiceMessage(t, "testdata/llmnr_request.bin")
116+
cfg := &Config{RelayIPv6: relayIP, TTL: 60 * time.Second, SpoofResponseName: "spoofedname"}
117+
118+
reply := createDNSReplyFromRequest(mockRW, request, nil, cfg, HandlerTypeMDNS, nil)
119+
if reply != nil {
120+
t.Fatalf("an mDNS response was created even though mDNS does not support response name spoofing")
121+
}
122+
}
123+
124+
func TestNetBIOSNoResponseNameSpoofing(t *testing.T) {
125+
relayIP := mustParseIP(t, "fe80::1")
126+
mockRW := mockResonseWriter{Remote: &net.UDPAddr{IP: mustParseIP(t, "10.0.0.1")}}
127+
request := readNameServiceMessage(t, "testdata/llmnr_request.bin")
128+
cfg := &Config{RelayIPv6: relayIP, TTL: 60 * time.Second, SpoofResponseName: "spoofedname"}
129+
130+
reply := createDNSReplyFromRequest(mockRW, request, nil, cfg, HandlerTypeNetBIOS, nil)
131+
if reply != nil {
132+
t.Fatalf("an NetBIOS response was created even though NetBIOS does not support response name spoofing")
133+
}
134+
}
135+
112136
func TestSubnetBroadcastListenIP(t *testing.T) {
113137
testCases := []struct {
114138
Net string

0 commit comments

Comments
 (0)