mirror of
https://github.com/owasp-amass/amass.git
synced 2026-10-12 02:20:44 +00:00
485 lines
11 KiB
Go
485 lines
11 KiB
Go
// Copyright © by Jeff Foley 2017-2023. All rights reserved.
|
|
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package scripting
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
amassnet "github.com/owasp-amass/amass/v3/net"
|
|
amassdns "github.com/owasp-amass/amass/v3/net/dns"
|
|
"github.com/owasp-amass/amass/v3/requests"
|
|
"github.com/owasp-amass/resolve"
|
|
"github.com/miekg/dns"
|
|
bf "github.com/tylertreat/BoomFilters"
|
|
lua "github.com/yuin/gopher-lua"
|
|
"golang.org/x/net/publicsuffix"
|
|
)
|
|
|
|
const (
|
|
defaultSweepSize = 250
|
|
activeSweepSize = 500
|
|
)
|
|
|
|
var (
|
|
sweepLock sync.Mutex
|
|
sweepFilter *bf.StableBloomFilter = bf.NewDefaultStableBloomFilter(1000000, 0.01)
|
|
)
|
|
|
|
// Wrapper so that scripts can make DNS queries.
|
|
func (s *Script) resolve(L *lua.LState) int {
|
|
ctx, err := extractContext(L.CheckUserData(1))
|
|
name := L.CheckString(2)
|
|
qtype := convertType(L.CheckString(3))
|
|
if err != nil || name == "" || qtype == 0 {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString("proper parameters were not provided"))
|
|
return 2
|
|
}
|
|
|
|
resp, err := s.fwdQuery(ctx, name, qtype)
|
|
if err != nil || resp.Rcode != dns.RcodeSuccess || len(resp.Answer) == 0 {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString("the query was unsuccessful for " + name))
|
|
return 2
|
|
}
|
|
|
|
detection := true
|
|
if L.GetTop() == 4 {
|
|
detection = L.CheckBool(4)
|
|
}
|
|
|
|
if detection {
|
|
domain, err := publicsuffix.EffectiveTLDPlusOne(name)
|
|
|
|
if err != nil || s.sys.TrustedResolvers().WildcardDetected(ctx, resp, domain) {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString("DNS wildcard detection made a positive match for " + name))
|
|
return 2
|
|
}
|
|
}
|
|
|
|
tb := L.NewTable()
|
|
if ans := resolve.ExtractAnswers(resp); len(ans) > 0 {
|
|
if records := resolve.AnswersByType(ans, qtype); len(records) > 0 {
|
|
for _, rr := range records {
|
|
entry := L.NewTable()
|
|
entry.RawSetString("rrname", lua.LString(rr.Name))
|
|
entry.RawSetString("rrtype", lua.LNumber(rr.Type))
|
|
entry.RawSetString("rrdata", lua.LString(rr.Data))
|
|
tb.Append(entry)
|
|
}
|
|
}
|
|
}
|
|
L.Push(tb)
|
|
L.Push(lua.LNil)
|
|
return 2
|
|
}
|
|
|
|
func (s *Script) fwdQuery(ctx context.Context, name string, qtype uint16) (*dns.Msg, error) {
|
|
msg := resolve.QueryMsg(name, qtype)
|
|
resp, err := s.dnsQuery(ctx, msg, s.sys.Resolvers(), 50)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
if resp == nil && err == nil {
|
|
return nil, errors.New("query failed")
|
|
}
|
|
|
|
resp, err = s.dnsQuery(ctx, msg, s.sys.TrustedResolvers(), 50)
|
|
if resp == nil && err == nil {
|
|
err = errors.New("query failed")
|
|
}
|
|
return resp, err
|
|
}
|
|
|
|
func (s *Script) dnsQuery(ctx context.Context, msg *dns.Msg, r *resolve.Resolvers, attempts int) (*dns.Msg, error) {
|
|
for num := 0; num < attempts; num++ {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, errors.New("context expired")
|
|
default:
|
|
}
|
|
|
|
resp, err := r.QueryBlocking(ctx, msg)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if resp.Rcode == dns.RcodeNameError {
|
|
return nil, errors.New("name does not exist")
|
|
}
|
|
if resp.Rcode == dns.RcodeSuccess && len(resp.Answer) == 0 {
|
|
return nil, errors.New("no record of this type")
|
|
}
|
|
if resp.Rcode == dns.RcodeSuccess {
|
|
return resp, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func convertType(qtype string) uint16 {
|
|
var t uint16
|
|
|
|
switch strings.ToLower(qtype) {
|
|
case "a":
|
|
t = dns.TypeA
|
|
case "aaaa":
|
|
t = dns.TypeAAAA
|
|
case "cname":
|
|
t = dns.TypeCNAME
|
|
case "ptr":
|
|
t = dns.TypePTR
|
|
case "ns":
|
|
t = dns.TypeNS
|
|
case "mx":
|
|
t = dns.TypeMX
|
|
case "txt":
|
|
t = dns.TypeTXT
|
|
case "soa":
|
|
t = dns.TypeSOA
|
|
case "srv":
|
|
t = dns.TypeSRV
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (s *Script) reverseSweep(L *lua.LState) int {
|
|
ctx, err := extractContext(L.CheckUserData(1))
|
|
if err != nil {
|
|
L.Push(lua.LString("failed to obtain the context"))
|
|
return 1
|
|
}
|
|
|
|
addr := L.CheckString(2)
|
|
if addr == "" {
|
|
L.Push(lua.LString("failed to obtain the IP address"))
|
|
return 1
|
|
}
|
|
|
|
size := defaultSweepSize
|
|
if s.sys.Config().Active {
|
|
size = activeSweepSize
|
|
}
|
|
|
|
var cidr *net.IPNet
|
|
if asn := s.sys.Cache().AddrSearch(addr); asn != nil {
|
|
if _, c, err := net.ParseCIDR(asn.Prefix); err == nil {
|
|
cidr = c
|
|
}
|
|
}
|
|
|
|
if cidr == nil {
|
|
ip := net.ParseIP(addr)
|
|
mask := net.CIDRMask(18, 32)
|
|
if amassnet.IsIPv6(ip) {
|
|
mask = net.CIDRMask(64, 128)
|
|
}
|
|
|
|
cidr = &net.IPNet{
|
|
IP: ip.Mask(mask),
|
|
Mask: mask,
|
|
}
|
|
}
|
|
|
|
var count int
|
|
ch := make(chan *resolve.ExtractedAnswer, 10)
|
|
for _, ip := range amassnet.CIDRSubset(cidr, addr, size) {
|
|
select {
|
|
case <-ctx.Done():
|
|
L.Push(lua.LString("the context expired"))
|
|
return 1
|
|
default:
|
|
}
|
|
|
|
sweepLock.Lock()
|
|
if a := ip.String(); !sweepFilter.TestAndAdd([]byte(a)) {
|
|
count++
|
|
go s.getPTR(ctx, a, ch)
|
|
}
|
|
sweepLock.Unlock()
|
|
}
|
|
|
|
var records []*resolve.ExtractedAnswer
|
|
for i := 0; i < count; i++ {
|
|
if rr := <-ch; rr != nil {
|
|
records = append(records, rr)
|
|
}
|
|
}
|
|
|
|
for _, rr := range records {
|
|
s.newPTR(ctx, rr)
|
|
}
|
|
L.Push(lua.LNil)
|
|
return 1
|
|
}
|
|
|
|
func (s *Script) getPTR(ctx context.Context, addr string, ch chan *resolve.ExtractedAnswer) {
|
|
if reserved, _ := amassnet.IsReservedAddress(addr); reserved {
|
|
ch <- nil
|
|
return
|
|
}
|
|
|
|
msg := resolve.ReverseMsg(addr)
|
|
resp, err := s.dnsQuery(ctx, msg, s.sys.Resolvers(), 10)
|
|
if err != nil || resp == nil {
|
|
ch <- nil
|
|
return
|
|
}
|
|
|
|
resp, err = s.dnsQuery(ctx, msg, s.sys.TrustedResolvers(), 10)
|
|
if err != nil || resp == nil {
|
|
ch <- nil
|
|
return
|
|
}
|
|
|
|
if ans := resolve.ExtractAnswers(resp); len(ans) > 0 {
|
|
if records := resolve.AnswersByType(ans, dns.TypePTR); len(records) > 0 {
|
|
ch <- records[0]
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Script) zoneWalk(L *lua.LState) int {
|
|
ctx, err := extractContext(L.CheckUserData(1))
|
|
if err != nil {
|
|
L.Push(lua.LString("failed to obtain the context"))
|
|
return 1
|
|
}
|
|
|
|
name := L.CheckString(2)
|
|
if name == "" {
|
|
L.Push(lua.LString("failed to obtain the DNS name"))
|
|
return 1
|
|
}
|
|
|
|
server := L.CheckString(3)
|
|
if server == "" {
|
|
L.Push(lua.LString("failed to obtain the nameserver"))
|
|
return 1
|
|
}
|
|
|
|
domain := s.sys.Config().WhichDomain(name)
|
|
if domain == "" {
|
|
L.Push(lua.LString("the name " + name + " was not in scope"))
|
|
return 1
|
|
}
|
|
|
|
r := resolve.NewResolvers()
|
|
r.SetLogger(s.sys.Config().Log)
|
|
_ = r.AddResolvers(15, server)
|
|
defer r.Stop()
|
|
|
|
names, err := r.NsecTraversal(ctx, name)
|
|
if err != nil {
|
|
L.Push(lua.LString(fmt.Sprintf("Zone Walk failed: %s: %v", name, err)))
|
|
return 1
|
|
}
|
|
|
|
for _, nsec := range names {
|
|
name := resolve.RemoveLastDot(nsec.NextDomain)
|
|
|
|
if domain := s.sys.Config().WhichDomain(name); domain != "" {
|
|
s.Output() <- &requests.DNSRequest{
|
|
Name: name,
|
|
Domain: domain,
|
|
Tag: requests.DNS,
|
|
Source: "NSEC Walk",
|
|
}
|
|
}
|
|
}
|
|
|
|
L.Push(lua.LNil)
|
|
return 1
|
|
}
|
|
|
|
func (s *Script) wrapZoneTransfer(L *lua.LState) int {
|
|
ctx, err := extractContext(L.CheckUserData(1))
|
|
if err != nil {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString("failed to obtain the context"))
|
|
return 2
|
|
}
|
|
|
|
name := L.CheckString(2)
|
|
if name == "" {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString("failed to obtain the DNS name"))
|
|
return 2
|
|
}
|
|
|
|
server := L.CheckString(3)
|
|
if server == "" {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString("failed to obtain the nameserver"))
|
|
return 2
|
|
}
|
|
|
|
domain := s.sys.Config().WhichDomain(name)
|
|
if domain == "" {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString("the name " + name + " was not in scope"))
|
|
return 2
|
|
}
|
|
|
|
tb := L.NewTable()
|
|
if reqs, err := ZoneTransfer(ctx, name, domain, server); err == nil && len(reqs) > 0 {
|
|
for _, req := range reqs {
|
|
for _, rr := range req.Records {
|
|
entry := L.NewTable()
|
|
entry.RawSetString("rrname", lua.LString(rr.Name))
|
|
entry.RawSetString("rrtype", lua.LNumber(rr.Type))
|
|
entry.RawSetString("rrdata", lua.LString(rr.Data))
|
|
tb.Append(entry)
|
|
}
|
|
// Zone Transfers can reveal DNS wildcards
|
|
if n := amassdns.RemoveAsteriskLabel(req.Name); len(n) < len(req.Name) {
|
|
// Signal the wildcard discovery
|
|
s.Output() <- &requests.DNSRequest{
|
|
Name: "www." + n,
|
|
Domain: req.Domain,
|
|
Tag: requests.DNS,
|
|
Source: "DNS",
|
|
}
|
|
} else {
|
|
s.Output() <- req
|
|
}
|
|
}
|
|
}
|
|
L.Push(tb)
|
|
L.Push(lua.LNil)
|
|
return 2
|
|
}
|
|
|
|
// ZoneTransfer attempts a DNS zone transfer using the provided server.
|
|
// The returned slice contains all the records discovered from the zone transfer.
|
|
func ZoneTransfer(ctx context.Context, sub, domain, server string) ([]*requests.DNSRequest, error) {
|
|
timeout := 15 * time.Second
|
|
var results []*requests.DNSRequest
|
|
|
|
// Set the maximum time allowed for making the connection
|
|
tctx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
addr := net.JoinHostPort(server, "53")
|
|
conn, err := amassnet.DialContext(tctx, "tcp", addr)
|
|
if err != nil {
|
|
return results, fmt.Errorf("zone xfr error: Failed to obtain TCP connection to [%s]: %v", addr, err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
xfr := &dns.Transfer{
|
|
Conn: &dns.Conn{Conn: conn},
|
|
ReadTimeout: timeout,
|
|
}
|
|
|
|
m := &dns.Msg{}
|
|
m.SetAxfr(dns.Fqdn(sub))
|
|
|
|
in, err := xfr.In(m, "")
|
|
if err != nil {
|
|
return results, fmt.Errorf("DNS zone transfer error for [%s]: %v", addr, err)
|
|
}
|
|
|
|
for en := range in {
|
|
reqs := getXfrRequests(en, domain)
|
|
if reqs == nil {
|
|
continue
|
|
}
|
|
|
|
results = append(results, reqs...)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func getXfrRequests(en *dns.Envelope, domain string) []*requests.DNSRequest {
|
|
if en.Error != nil {
|
|
return nil
|
|
}
|
|
|
|
reqs := make(map[string]*requests.DNSRequest)
|
|
for _, a := range en.RR {
|
|
var record requests.DNSAnswer
|
|
|
|
switch v := a.(type) {
|
|
case *dns.CNAME:
|
|
record.Type = int(dns.TypeCNAME)
|
|
record.Name = resolve.RemoveLastDot(v.Hdr.Name)
|
|
record.Data = resolve.RemoveLastDot(v.Target)
|
|
case *dns.A:
|
|
record.Type = int(dns.TypeA)
|
|
record.Name = resolve.RemoveLastDot(v.Hdr.Name)
|
|
record.Data = v.A.String()
|
|
case *dns.AAAA:
|
|
record.Type = int(dns.TypeAAAA)
|
|
record.Name = resolve.RemoveLastDot(v.Hdr.Name)
|
|
record.Data = v.AAAA.String()
|
|
case *dns.PTR:
|
|
record.Type = int(dns.TypePTR)
|
|
record.Name = resolve.RemoveLastDot(v.Hdr.Name)
|
|
record.Data = resolve.RemoveLastDot(v.Ptr)
|
|
case *dns.NS:
|
|
record.Type = int(dns.TypeNS)
|
|
record.Name = realName(v.Hdr)
|
|
record.Data = resolve.RemoveLastDot(v.Ns)
|
|
case *dns.MX:
|
|
record.Type = int(dns.TypeMX)
|
|
record.Name = resolve.RemoveLastDot(v.Hdr.Name)
|
|
record.Data = resolve.RemoveLastDot(v.Mx)
|
|
case *dns.TXT:
|
|
record.Type = int(dns.TypeTXT)
|
|
record.Name = resolve.RemoveLastDot(v.Hdr.Name)
|
|
for _, piece := range v.Txt {
|
|
record.Data += piece + " "
|
|
}
|
|
case *dns.SOA:
|
|
record.Type = int(dns.TypeSOA)
|
|
record.Name = resolve.RemoveLastDot(v.Hdr.Name)
|
|
record.Data = v.Ns + " " + v.Mbox
|
|
case *dns.SPF:
|
|
record.Type = int(dns.TypeSPF)
|
|
record.Name = resolve.RemoveLastDot(v.Hdr.Name)
|
|
for _, piece := range v.Txt {
|
|
record.Data += piece + " "
|
|
}
|
|
case *dns.SRV:
|
|
record.Type = int(dns.TypeSRV)
|
|
record.Name = resolve.RemoveLastDot(v.Hdr.Name)
|
|
record.Data = resolve.RemoveLastDot(v.Target)
|
|
default:
|
|
continue
|
|
}
|
|
|
|
if r, found := reqs[record.Name]; found {
|
|
r.Records = append(r.Records, record)
|
|
} else {
|
|
reqs[record.Name] = &requests.DNSRequest{
|
|
Name: record.Name,
|
|
Domain: domain,
|
|
Records: []requests.DNSAnswer{record},
|
|
Tag: requests.AXFR,
|
|
Source: "DNS Zone XFR",
|
|
}
|
|
}
|
|
}
|
|
|
|
var requests []*requests.DNSRequest
|
|
for _, r := range reqs {
|
|
requests = append(requests, r)
|
|
}
|
|
return requests
|
|
}
|
|
|
|
func realName(hdr dns.RR_Header) string {
|
|
pieces := strings.Split(hdr.Name, " ")
|
|
|
|
return resolve.RemoveLastDot(pieces[len(pieces)-1])
|
|
}
|