● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.
PUBLIC MIRRORGo v1.0

Multi-protocol network egress assessment and DLP verification framework featuring automated L4 sweeps, corporate proxy traversal, and covert protocol smuggling across 20+ channels.

$ git clone https://shehzade.io/repos/contraband.git Copy
Source ZIP (1.93 MB)
Code 174 Commits 1
contraband / pkg / client / proxy / wpad.go
← Back to directory
190 lines · 4.4 KB
Raw
1package proxy
2
3import (
4 "bufio"
5 "context"
6 "fmt"
7 "io"
8 "net"
9 "net/http"
10 "net/url"
11 "os"
12 "path/filepath"
13 "regexp"
14 "strings"
15 "time"
16
17 "github.com/shehzade/contraband/pkg/common"
18)
19
20var (
21 dhcpLeasePaths = []string{
22 "/var/lib/dhcp/dhclient.leases",
23 "/var/lib/NetworkManager/*.lease",
24 "/run/systemd/netif/leases/*",
25 "/var/db/dhclient.leases",
26 }
27
28 reDHCP252 = regexp.MustCompile(`(?i)option\s+(?:wpad-url|site-local-252|252)\s+"([^"]+)";`)
29)
30
31// WPADResult contains discovered WPAD PAC script content and source URL.
32type WPADResult struct {
33 PACURL string
34 Script string
35}
36
37// DiscoverWPAD performs WPAD discovery via Explicit URL, DHCP Option 252, or DNS hierarchy walk.
38func DiscoverWPAD(ctx context.Context, opts Options) (*WPADResult, error) {
39 timeout := opts.WPADTimeout
40 if timeout <= 0 {
41 timeout = common.DefaultWPADTimeout // 3s
42 }
43
44 ctx, cancel := context.WithTimeout(ctx, timeout)
45 defer cancel()
46
47 // 1. Explicit PAC URL
48 if opts.PACURL != "" {
49 script, err := fetchPAC(ctx, opts.PACURL, opts.UserAgent)
50 if err == nil && len(strings.TrimSpace(script)) > 0 {
51 return &WPADResult{PACURL: opts.PACURL, Script: script}, nil
52 }
53 }
54
55 // 2. DHCP Option 252 (Linux lease scanning)
56 if dhcpURL := ScanDHCPLeasesForWPAD(); dhcpURL != "" {
57 script, err := fetchPAC(ctx, dhcpURL, opts.UserAgent)
58 if err == nil && len(strings.TrimSpace(script)) > 0 {
59 return &WPADResult{PACURL: dhcpURL, Script: script}, nil
60 }
61 }
62
63 // 3. DNS Hierarchy Probing
64 domains := getSearchDomains()
65 for _, domain := range domains {
66 subdomains := generateWPADCandidates(domain)
67 for _, candidateURL := range subdomains {
68 select {
69 case <-ctx.Done():
70 return nil, ErrWPADNotFound
71 default:
72 }
73
74 script, err := fetchPAC(ctx, candidateURL, opts.UserAgent)
75 if err == nil && len(strings.TrimSpace(script)) > 0 {
76 return &WPADResult{PACURL: candidateURL, Script: script}, nil
77 }
78 }
79 }
80
81 return nil, ErrWPADNotFound
82}
83
84// ScanDHCPLeasesForWPAD inspects common Linux DHCP lease files for Option 252.
85func ScanDHCPLeasesForWPAD() string {
86 for _, pattern := range dhcpLeasePaths {
87 matches, err := filepath.Glob(pattern)
88 if err != nil {
89 continue
90 }
91 for _, file := range matches {
92 content, err := os.ReadFile(file)
93 if err != nil {
94 continue
95 }
96 m := reDHCP252.FindSubmatch(content)
97 if len(m) > 1 {
98 cleanURL := strings.Trim(string(m[1]), "\x00\r\n\t ")
99 if _, err := url.Parse(cleanURL); err == nil && cleanURL != "" {
100 return cleanURL
101 }
102 }
103 }
104 }
105 return ""
106}
107
108// getSearchDomains extracts DNS search domains from /etc/resolv.conf.
109func getSearchDomains() []string {
110 var domains []string
111 f, err := os.Open("/etc/resolv.conf")
112 if err == nil {
113 defer f.Close()
114 scanner := bufio.NewScanner(f)
115 for scanner.Scan() {
116 line := strings.TrimSpace(scanner.Text())
117 if strings.HasPrefix(line, "search ") || strings.HasPrefix(line, "domain ") {
118 fields := strings.Fields(line)
119 if len(fields) > 1 {
120 domains = append(domains, fields[1:]...)
121 }
122 }
123 }
124 }
125
126 if len(domains) == 0 {
127 if h, err := os.Hostname(); err == nil {
128 idx := strings.Index(h, ".")
129 if idx != -1 && idx < len(h)-1 {
130 domains = append(domains, h[idx+1:])
131 }
132 }
133 }
134
135 return domains
136}
137
138// generateWPADCandidates generates hierarchical WPAD URLs, avoiding single-label TLDs.
139func generateWPADCandidates(domain string) []string {
140 domain = strings.Trim(domain, ".")
141 parts := strings.Split(domain, ".")
142 var candidates []string
143
144 for i := 0; i < len(parts)-1; i++ {
145 // Stop before single-label TLD or 2-part public suffix
146 sub := strings.Join(parts[i:], ".")
147 if strings.Count(sub, ".") < 1 {
148 break
149 }
150 candidates = append(candidates, fmt.Sprintf("http://wpad.%s/wpad.dat", sub))
151 }
152 return candidates
153}
154
155func fetchPAC(ctx context.Context, pacURL, userAgent string) (string, error) {
156 req, err := http.NewRequestWithContext(ctx, http.MethodGet, pacURL, nil)
157 if err != nil {
158 return "", err
159 }
160 if userAgent != "" {
161 req.Header.Set("User-Agent", userAgent)
162 }
163
164 client := &http.Client{
165 Timeout: 3 * time.Second,
166 Transport: &http.Transport{
167 DialContext: (&net.Dialer{
168 Timeout: 2 * time.Second,
169 }).DialContext,
170 },
171 }
172
173 resp, err := client.Do(req)
174 if err != nil {
175 return "", err
176 }
177 defer resp.Body.Close()
178
179 if resp.StatusCode != http.StatusOK {
180 return "", fmt.Errorf("HTTP status %d", resp.StatusCode)
181 }
182
183 // Limit to 1MB
184 body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
185 if err != nil {
186 return "", err
187 }
188
189 return string(body), nil
190}