[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)
177 lines · 5.4 KB
| 1 | package proxy |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "fmt" |
| 7 | "net" |
| 8 | "net/url" |
| 9 | "strings" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/shehzade/contraband/pkg/common" |
| 13 | ) |
| 14 | |
| 15 | var ( |
| 16 | ErrProxyForbidden = errors.New("proxy access forbidden (403)") |
| 17 | ErrProxyGateway = errors.New("proxy gateway error (502/504)") |
| 18 | ErrProxyAuthFailed = errors.New("proxy authentication failed (407)") |
| 19 | ErrWPADNotFound = errors.New("wpad pac script not discovered") |
| 20 | ErrNoProxyMatch = errors.New("no proxy found matching target") |
| 21 | ) |
| 22 | |
| 23 | // Dialer provides context-aware connection establishment to target destinations. |
| 24 | type Dialer interface { |
| 25 | DialContext(ctx context.Context, network, addr string) (net.Conn, error) |
| 26 | } |
| 27 | |
| 28 | // ProxyDialer extends Dialer to expose upstream proxy metadata for telemetry and forensic reporting. |
| 29 | type ProxyDialer interface { |
| 30 | Dialer |
| 31 | // ProxyHop returns the upstream proxy address (e.g. "10.0.0.1:8080" or "proxy.corp.local:3128"), |
| 32 | // or an empty string if direct connection was established. |
| 33 | ProxyHop() string |
| 34 | // Mode returns the operational mode: "direct", "http_connect", "socks5". |
| 35 | Mode() string |
| 36 | } |
| 37 | |
| 38 | // Resolver resolves the optimal ProxyDialer for a given target address. |
| 39 | type Resolver interface { |
| 40 | GetDialer(ctx context.Context, targetAddr string) (ProxyDialer, error) |
| 41 | } |
| 42 | |
| 43 | type AuthType string |
| 44 | |
| 45 | const ( |
| 46 | AuthNone AuthType = "none" |
| 47 | AuthBasic AuthType = "basic" |
| 48 | AuthDigest AuthType = "digest" |
| 49 | AuthNTLM AuthType = "ntlm" |
| 50 | AuthNegotiate AuthType = "negotiate" |
| 51 | AuthAuto AuthType = "auto" // Interrogate 407 Proxy-Authenticate headers |
| 52 | ) |
| 53 | |
| 54 | type Options struct { |
| 55 | // Mode: "auto", "direct", or an explicit proxy URL (e.g. "http://10.0.0.1:8080", "socks5://127.0.0.1:1080") |
| 56 | Mode string |
| 57 | AuthType AuthType |
| 58 | Username string |
| 59 | Password string |
| 60 | Domain string // Windows domain for NTLMv2 / Kerberos |
| 61 | Workstation string // Workstation name for NTLMv2 |
| 62 | PACURL string // Explicit PAC URL (skips WPAD discovery if provided) |
| 63 | Timeout time.Duration // Proxy connection & handshake timeout (default: 10s) |
| 64 | WPADTimeout time.Duration // WPAD discovery timeout (default: 3s) |
| 65 | UserAgent string // User-Agent string for HTTP CONNECT / PAC fetch |
| 66 | InsecureSkipVerify bool // Skip TLS cert validation for HTTPS proxies |
| 67 | Logger common.Logger |
| 68 | } |
| 69 | |
| 70 | // DefaultOptions returns production-grade defaults. |
| 71 | func DefaultOptions() Options { |
| 72 | return Options{ |
| 73 | Mode: "auto", |
| 74 | AuthType: AuthAuto, |
| 75 | Timeout: 10 * time.Second, |
| 76 | WPADTimeout: common.DefaultWPADTimeout, // 3s |
| 77 | UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", |
| 78 | Logger: common.GetLogger(), |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | // sanitizeProxyURL converts unencoded backslashes in userinfo (e.g. CORP\corpuser) to %5C. |
| 83 | func sanitizeProxyURL(rawURL string) string { |
| 84 | atIdx := strings.LastIndex(rawURL, "@") |
| 85 | if atIdx != -1 { |
| 86 | schemeIdx := strings.Index(rawURL, "://") |
| 87 | start := 0 |
| 88 | if schemeIdx != -1 { |
| 89 | start = schemeIdx + 3 |
| 90 | } |
| 91 | if start < atIdx { |
| 92 | userinfo := rawURL[start:atIdx] |
| 93 | if strings.Contains(userinfo, "\\") { |
| 94 | escapedUserinfo := strings.ReplaceAll(userinfo, "\\", "%5C") |
| 95 | return rawURL[:start] + escapedUserinfo + rawURL[atIdx:] |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | return rawURL |
| 100 | } |
| 101 | |
| 102 | // FromCommonConfig maps common.ProxyConfig into proxy.Options. |
| 103 | func FromCommonConfig(cfg common.ProxyConfig) Options { |
| 104 | opts := DefaultOptions() |
| 105 | if cfg.Mode != "" { |
| 106 | opts.Mode = sanitizeProxyURL(cfg.Mode) |
| 107 | } |
| 108 | if cfg.AuthType != "" { |
| 109 | opts.AuthType = AuthType(cfg.AuthType) |
| 110 | } |
| 111 | // Parse credentials if embedded in URL: scheme://user:pass@host:port |
| 112 | if strings.Contains(opts.Mode, "://") { |
| 113 | if u, err := url.Parse(opts.Mode); err == nil && u.User != nil { |
| 114 | opts.Username = u.User.Username() |
| 115 | opts.Password, _ = u.User.Password() |
| 116 | } |
| 117 | } |
| 118 | return opts |
| 119 | } |
| 120 | |
| 121 | // DirectDialer dials targets directly without any proxy hop. |
| 122 | type DirectDialer struct { |
| 123 | dialer *net.Dialer |
| 124 | } |
| 125 | |
| 126 | // NewDirectDialer creates a new DirectDialer with the configured timeout. |
| 127 | func NewDirectDialer(timeout time.Duration) *DirectDialer { |
| 128 | if timeout <= 0 { |
| 129 | timeout = 10 * time.Second |
| 130 | } |
| 131 | return &DirectDialer{ |
| 132 | dialer: &net.Dialer{ |
| 133 | Timeout: timeout, |
| 134 | KeepAlive: 30 * time.Second, |
| 135 | }, |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | func (d *DirectDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { |
| 140 | return d.dialer.DialContext(ctx, network, addr) |
| 141 | } |
| 142 | |
| 143 | func (d *DirectDialer) ProxyHop() string { return "" } |
| 144 | func (d *DirectDialer) Mode() string { return "direct" } |
| 145 | |
| 146 | // NewDialer creates a ProxyDialer from common.ProxyConfig, routing through the fallback engine if "auto". |
| 147 | func NewDialer(cfg common.ProxyConfig) (ProxyDialer, error) { |
| 148 | opts := FromCommonConfig(cfg) |
| 149 | return NewDialerFromOptions(opts) |
| 150 | } |
| 151 | |
| 152 | // NewDialerFromOptions creates a ProxyDialer based on the provided Options. |
| 153 | func NewDialerFromOptions(opts Options) (ProxyDialer, error) { |
| 154 | opts.Mode = sanitizeProxyURL(opts.Mode) |
| 155 | mode := strings.ToLower(opts.Mode) |
| 156 | if mode == "" || mode == "auto" { |
| 157 | return NewFallbackEngine(opts), nil |
| 158 | } |
| 159 | if mode == "direct" { |
| 160 | return NewDirectDialer(opts.Timeout), nil |
| 161 | } |
| 162 | |
| 163 | // Explicit proxy URL |
| 164 | u, err := url.Parse(opts.Mode) |
| 165 | if err != nil { |
| 166 | return nil, fmt.Errorf("invalid proxy URL %q: %w", opts.Mode, err) |
| 167 | } |
| 168 | |
| 169 | switch strings.ToLower(u.Scheme) { |
| 170 | case "http", "https": |
| 171 | return NewHTTPConnectDialer(opts) |
| 172 | case "socks5", "socks5h": |
| 173 | return NewSOCKS5Dialer(opts) |
| 174 | default: |
| 175 | return nil, fmt.Errorf("unsupported proxy protocol scheme %q", u.Scheme) |
| 176 | } |
| 177 | } |