[SHEHZADE//ABDULLAH ANSARI]
Red Team Consultant @ Google | MS in Cybersecurity @ NYU
● 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)
291 lines · 7.3 KB
| 1 | package proxy |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "net" |
| 7 | "os" |
| 8 | "strings" |
| 9 | "sync" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/shehzade/contraband/pkg/common" |
| 13 | ) |
| 14 | |
| 15 | // ProxyCandidate represents a potential proxy hop in the fallback chain. |
| 16 | type ProxyCandidate struct { |
| 17 | Name string |
| 18 | Dialer ProxyDialer |
| 19 | Source string |
| 20 | } |
| 21 | |
| 22 | // FallbackEngine implements an intelligent 5-tier proxy fallback cascade: |
| 23 | // 1. Explicit Flag -> 2. System Proxy -> 3. WPAD -> 4. Environment Variables -> 5. Direct Egress |
| 24 | type FallbackEngine struct { |
| 25 | opts Options |
| 26 | logger common.Logger |
| 27 | mu sync.RWMutex |
| 28 | lastHop string |
| 29 | lastMode string |
| 30 | directDialer *DirectDialer |
| 31 | customCandidates []ProxyCandidate |
| 32 | } |
| 33 | |
| 34 | // NewFallbackEngine initializes a new FallbackEngine. |
| 35 | func NewFallbackEngine(opts Options) *FallbackEngine { |
| 36 | timeout := opts.Timeout |
| 37 | if timeout <= 0 { |
| 38 | timeout = 10 * time.Second |
| 39 | } |
| 40 | return &FallbackEngine{ |
| 41 | opts: opts, |
| 42 | logger: opts.Logger, |
| 43 | directDialer: NewDirectDialer(timeout), |
| 44 | lastMode: "auto", |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | // SetCustomCandidates overrides candidate discovery with custom candidates (useful for testing). |
| 49 | func (fe *FallbackEngine) SetCustomCandidates(candidates []ProxyCandidate) { |
| 50 | fe.mu.Lock() |
| 51 | defer fe.mu.Unlock() |
| 52 | fe.customCandidates = candidates |
| 53 | } |
| 54 | |
| 55 | func (fe *FallbackEngine) ProxyHop() string { |
| 56 | fe.mu.RLock() |
| 57 | defer fe.mu.RUnlock() |
| 58 | return fe.lastHop |
| 59 | } |
| 60 | |
| 61 | func (fe *FallbackEngine) Mode() string { |
| 62 | fe.mu.RLock() |
| 63 | defer fe.mu.RUnlock() |
| 64 | return fe.lastMode |
| 65 | } |
| 66 | |
| 67 | // DialContext traverses the candidate chain until a successful connection is established. |
| 68 | func (fe *FallbackEngine) DialContext(ctx context.Context, network, targetAddr string) (net.Conn, error) { |
| 69 | candidates := fe.buildCandidates(ctx, targetAddr) |
| 70 | |
| 71 | var lastErr error |
| 72 | var explicitProxyErr error |
| 73 | for _, c := range candidates { |
| 74 | fe.logger.Debugf("Attempting egress via %s [%s]", c.Name, c.Source) |
| 75 | |
| 76 | conn, err := c.Dialer.DialContext(ctx, network, targetAddr) |
| 77 | if err == nil { |
| 78 | fe.mu.Lock() |
| 79 | fe.lastHop = c.Dialer.ProxyHop() |
| 80 | fe.lastMode = c.Dialer.Mode() |
| 81 | fe.mu.Unlock() |
| 82 | |
| 83 | if lastErr != nil { |
| 84 | fe.logger.Warnf("Primary proxy candidate failed (%v); fell back to %s (Hop: %s, Mode: %s)", lastErr, c.Name, c.Dialer.ProxyHop(), c.Dialer.Mode()) |
| 85 | } else { |
| 86 | fe.logger.Infof("Egress established via %s (Hop: %s, Mode: %s)", c.Name, c.Dialer.ProxyHop(), c.Dialer.Mode()) |
| 87 | } |
| 88 | return conn, nil |
| 89 | } |
| 90 | |
| 91 | fe.logger.Warnf("Egress candidate %s [%s] failed: %v", c.Name, c.Source, err) |
| 92 | if c.Name == "ExplicitProxy" { |
| 93 | explicitProxyErr = err |
| 94 | } |
| 95 | lastErr = err |
| 96 | } |
| 97 | |
| 98 | if explicitProxyErr != nil { |
| 99 | return nil, fmt.Errorf("explicit proxy traversal failed: %w", explicitProxyErr) |
| 100 | } |
| 101 | return nil, fmt.Errorf("all proxy traversal candidates exhausted, last error: %w", lastErr) |
| 102 | } |
| 103 | |
| 104 | func (fe *FallbackEngine) buildCandidates(ctx context.Context, targetAddr string) []ProxyCandidate { |
| 105 | fe.mu.RLock() |
| 106 | if len(fe.customCandidates) > 0 { |
| 107 | c := make([]ProxyCandidate, len(fe.customCandidates)) |
| 108 | copy(c, fe.customCandidates) |
| 109 | fe.mu.RUnlock() |
| 110 | return c |
| 111 | } |
| 112 | fe.mu.RUnlock() |
| 113 | |
| 114 | var candidates []ProxyCandidate |
| 115 | |
| 116 | // 1. Explicit Flag (if specified and not auto/direct) |
| 117 | mode := strings.ToLower(fe.opts.Mode) |
| 118 | if mode != "" && mode != "auto" && mode != "direct" { |
| 119 | d, err := NewDialerFromOptions(fe.opts) |
| 120 | if err != nil { |
| 121 | fe.logger.Warnf("Failed to initialize explicit proxy dialer %q: %v", fe.opts.Mode, err) |
| 122 | } else { |
| 123 | candidates = append(candidates, ProxyCandidate{ |
| 124 | Name: "ExplicitProxy", |
| 125 | Dialer: d, |
| 126 | Source: "CLI / Configuration Flag", |
| 127 | }) |
| 128 | // If explicitly given, do not try other proxies before Direct |
| 129 | candidates = append(candidates, ProxyCandidate{ |
| 130 | Name: "DirectFallback", |
| 131 | Dialer: fe.directDialer, |
| 132 | Source: "Direct Egress (Fallback)", |
| 133 | }) |
| 134 | return candidates |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | // 2. System Proxy (Windows WinHTTP / Registry) |
| 139 | if winCfg, _ := GetWindowsProxyConfig(); winCfg != nil { |
| 140 | if winCfg.Proxy != "" && !shouldBypass(targetAddr, winCfg.ProxyBypass) { |
| 141 | sysOpts := fe.opts |
| 142 | sysOpts.Mode = winCfg.Proxy |
| 143 | if d, err := NewDialerFromOptions(sysOpts); err == nil { |
| 144 | candidates = append(candidates, ProxyCandidate{ |
| 145 | Name: "WindowsSystemProxy", |
| 146 | Dialer: d, |
| 147 | Source: "Windows Registry Internet Settings", |
| 148 | }) |
| 149 | } |
| 150 | } |
| 151 | if winCfg.AutoConfigURL != "" && fe.opts.PACURL == "" { |
| 152 | fe.opts.PACURL = winCfg.AutoConfigURL |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | // 3. WPAD Auto-Discovery |
| 157 | wpadCtx, cancel := context.WithTimeout(ctx, fe.opts.WPADTimeout) |
| 158 | wpadRes, err := DiscoverWPAD(wpadCtx, fe.opts) |
| 159 | cancel() |
| 160 | if err == nil && wpadRes != nil && wpadRes.Script != "" { |
| 161 | host, _, _ := net.SplitHostPort(targetAddr) |
| 162 | if host == "" { |
| 163 | host = targetAddr |
| 164 | } |
| 165 | directives, err := EvaluatePACScript(ctx, wpadRes.Script, "http://"+targetAddr, host) |
| 166 | if err == nil { |
| 167 | for i, d := range directives { |
| 168 | if d.Type == DirectiveDirect { |
| 169 | continue |
| 170 | } |
| 171 | pacOpts := fe.opts |
| 172 | pacOpts.Mode = d.ToURL() |
| 173 | if pd, err := NewDialerFromOptions(pacOpts); err == nil { |
| 174 | candidates = append(candidates, ProxyCandidate{ |
| 175 | Name: fmt.Sprintf("WPADDirective_%d", i+1), |
| 176 | Dialer: pd, |
| 177 | Source: fmt.Sprintf("WPAD PAC (%s)", wpadRes.PACURL), |
| 178 | }) |
| 179 | } |
| 180 | } |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | // 4. Environment Variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, with NO_PROXY) |
| 185 | if !isNoProxy(targetAddr) { |
| 186 | envProxy := getEnvProxy(targetAddr) |
| 187 | if envProxy != "" { |
| 188 | envOpts := fe.opts |
| 189 | envOpts.Mode = envProxy |
| 190 | if ed, err := NewDialerFromOptions(envOpts); err == nil { |
| 191 | candidates = append(candidates, ProxyCandidate{ |
| 192 | Name: "EnvProxy", |
| 193 | Dialer: ed, |
| 194 | Source: "Environment Variables", |
| 195 | }) |
| 196 | } |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | // 5. Direct Egress |
| 201 | candidates = append(candidates, ProxyCandidate{ |
| 202 | Name: "DirectEgress", |
| 203 | Dialer: fe.directDialer, |
| 204 | Source: "Direct TCP Socket", |
| 205 | }) |
| 206 | |
| 207 | return candidates |
| 208 | } |
| 209 | |
| 210 | func getEnvProxy(targetAddr string) string { |
| 211 | if strings.HasSuffix(targetAddr, ":443") { |
| 212 | if p := os.Getenv("HTTPS_PROXY"); p != "" { |
| 213 | return p |
| 214 | } |
| 215 | if p := os.Getenv("https_proxy"); p != "" { |
| 216 | return p |
| 217 | } |
| 218 | } |
| 219 | if p := os.Getenv("HTTP_PROXY"); p != "" { |
| 220 | return p |
| 221 | } |
| 222 | if p := os.Getenv("http_proxy"); p != "" { |
| 223 | return p |
| 224 | } |
| 225 | if p := os.Getenv("ALL_PROXY"); p != "" { |
| 226 | return p |
| 227 | } |
| 228 | if p := os.Getenv("all_proxy"); p != "" { |
| 229 | return p |
| 230 | } |
| 231 | return "" |
| 232 | } |
| 233 | |
| 234 | func isNoProxy(targetAddr string) bool { |
| 235 | noProxy := os.Getenv("NO_PROXY") |
| 236 | if noProxy == "" { |
| 237 | noProxy = os.Getenv("no_proxy") |
| 238 | } |
| 239 | if noProxy == "" { |
| 240 | return false |
| 241 | } |
| 242 | return shouldBypass(targetAddr, noProxy) |
| 243 | } |
| 244 | |
| 245 | func shouldBypass(targetAddr, bypassRules string) bool { |
| 246 | if bypassRules == "" { |
| 247 | return false |
| 248 | } |
| 249 | host, _, err := net.SplitHostPort(targetAddr) |
| 250 | if err != nil { |
| 251 | host = targetAddr |
| 252 | } |
| 253 | host = strings.ToLower(strings.TrimSpace(host)) |
| 254 | |
| 255 | rules := strings.FieldsFunc(bypassRules, func(r rune) bool { |
| 256 | return r == ',' || r == ';' || r == ' ' |
| 257 | }) |
| 258 | |
| 259 | targetIP := net.ParseIP(host) |
| 260 | |
| 261 | for _, rule := range rules { |
| 262 | rule = strings.ToLower(strings.TrimSpace(rule)) |
| 263 | if rule == "" { |
| 264 | continue |
| 265 | } |
| 266 | if rule == "*" { |
| 267 | return true |
| 268 | } |
| 269 | if rule == "<local>" && !strings.Contains(host, ".") { |
| 270 | return true |
| 271 | } |
| 272 | if rule == host { |
| 273 | return true |
| 274 | } |
| 275 | if strings.HasPrefix(rule, ".") && strings.HasSuffix(host, rule) { |
| 276 | return true |
| 277 | } |
| 278 | if !strings.HasPrefix(rule, ".") && strings.HasSuffix(host, "."+rule) { |
| 279 | return true |
| 280 | } |
| 281 | // CIDR check |
| 282 | if targetIP != nil { |
| 283 | if _, cidr, err := net.ParseCIDR(rule); err == nil { |
| 284 | if cidr.Contains(targetIP) { |
| 285 | return true |
| 286 | } |
| 287 | } |
| 288 | } |
| 289 | } |
| 290 | return false |
| 291 | } |