● 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 / http_connect.go
← Back to directory
407 lines · 11.0 KB
Raw
1package proxy
2
3import (
4 "bufio"
5 "context"
6 "crypto/tls"
7 "fmt"
8 "io"
9 "net"
10 "net/http"
11 "net/url"
12 "strings"
13 "time"
14)
15
16// HTTPConnectDialer implements ProxyDialer for HTTP/HTTPS forward proxies using HTTP CONNECT.
17type HTTPConnectDialer struct {
18 proxyAddr string
19 isTLS bool
20 tlsConfig *tls.Config
21 username string
22 password string
23 domain string
24 workstation string
25 authType AuthType
26 timeout time.Duration
27 userAgent string
28 insecureSkipVerify bool
29}
30
31// NewHTTPConnectDialer creates a new HTTPConnectDialer from Options.
32func NewHTTPConnectDialer(opts Options) (*HTTPConnectDialer, error) {
33 rawURL := opts.Mode
34 if !strings.Contains(rawURL, "://") {
35 rawURL = "http://" + rawURL
36 }
37
38 u, err := url.Parse(rawURL)
39 if err != nil {
40 return nil, fmt.Errorf("invalid HTTP proxy URL %q: %w", opts.Mode, err)
41 }
42
43 isTLS := strings.EqualFold(u.Scheme, "https")
44 host := u.Host
45 if !strings.Contains(host, ":") {
46 if isTLS {
47 host += ":443"
48 } else {
49 host += ":8080"
50 }
51 }
52
53 username := opts.Username
54 password := opts.Password
55 if u.User != nil {
56 if username == "" {
57 username = u.User.Username()
58 }
59 if password == "" {
60 password, _ = u.User.Password()
61 }
62 }
63
64 timeout := opts.Timeout
65 if timeout <= 0 {
66 timeout = 10 * time.Second
67 }
68
69 userAgent := opts.UserAgent
70 if userAgent == "" {
71 userAgent = DefaultOptions().UserAgent
72 }
73
74 var tlsCfg *tls.Config
75 if isTLS {
76 serverName := host
77 if h, _, err := net.SplitHostPort(host); err == nil {
78 serverName = h
79 }
80 tlsCfg = &tls.Config{
81 ServerName: serverName,
82 InsecureSkipVerify: opts.InsecureSkipVerify,
83 }
84 }
85
86 return &HTTPConnectDialer{
87 proxyAddr: host,
88 isTLS: isTLS,
89 tlsConfig: tlsCfg,
90 username: username,
91 password: password,
92 domain: opts.Domain,
93 workstation: opts.Workstation,
94 authType: opts.AuthType,
95 timeout: timeout,
96 userAgent: userAgent,
97 insecureSkipVerify: opts.InsecureSkipVerify,
98 }, nil
99}
100
101func (h *HTTPConnectDialer) ProxyHop() string {
102 return h.proxyAddr
103}
104
105func (h *HTTPConnectDialer) Mode() string {
106 return "http_connect"
107}
108
109func (h *HTTPConnectDialer) dialProxy(ctx context.Context) (net.Conn, error) {
110 dialer := &net.Dialer{
111 Timeout: h.timeout,
112 }
113
114 rawConn, err := dialer.DialContext(ctx, "tcp", h.proxyAddr)
115 if err != nil {
116 return nil, fmt.Errorf("failed to dial proxy %s: %w", h.proxyAddr, err)
117 }
118
119 var conn net.Conn = rawConn
120 if h.isTLS {
121 tlsConn := tls.Client(rawConn, h.tlsConfig)
122 if err := tlsConn.HandshakeContext(ctx); err != nil {
123 rawConn.Close()
124 return nil, fmt.Errorf("TLS handshake to proxy %s failed: %w", h.proxyAddr, err)
125 }
126 conn = tlsConn
127 }
128 return conn, nil
129}
130
131func (h *HTTPConnectDialer) DialContext(ctx context.Context, network, targetAddr string) (net.Conn, error) {
132 conn, err := h.dialProxy(ctx)
133 if err != nil {
134 return nil, err
135 }
136
137 if deadline, ok := ctx.Deadline(); ok {
138 _ = conn.SetDeadline(deadline)
139 } else if h.timeout > 0 {
140 _ = conn.SetDeadline(time.Now().Add(h.timeout))
141 }
142 defer func() {
143 _ = conn.SetDeadline(time.Time{})
144 }()
145
146 br := bufio.NewReader(conn)
147
148 // Step 1: Initial CONNECT request
149 var initialAuthHeader string
150 if h.authType == AuthBasic && h.username != "" {
151 initialAuthHeader = BuildBasicAuthHeader(h.username, h.password)
152 }
153
154 if err := h.sendConnect(conn, targetAddr, initialAuthHeader); err != nil {
155 conn.Close()
156 return nil, fmt.Errorf("failed to send CONNECT request: %w", err)
157 }
158
159 resp, err := http.ReadResponse(br, &http.Request{Method: http.MethodConnect})
160 if err != nil {
161 conn.Close()
162 return nil, fmt.Errorf("failed to read CONNECT response: %w", err)
163 }
164
165 if resp.StatusCode == http.StatusOK {
166 return newBufferedConn(conn, br), nil
167 }
168
169 if resp.StatusCode == http.StatusForbidden {
170 if resp.Body != nil {
171 _ = resp.Body.Close()
172 }
173 conn.Close()
174 return nil, ErrProxyForbidden
175 }
176
177 if resp.StatusCode == http.StatusBadGateway || resp.StatusCode == http.StatusGatewayTimeout {
178 if resp.Body != nil {
179 _ = resp.Body.Close()
180 }
181 conn.Close()
182 return nil, ErrProxyGateway
183 }
184
185 if resp.StatusCode != http.StatusProxyAuthRequired {
186 if resp.Body != nil {
187 _ = resp.Body.Close()
188 }
189 conn.Close()
190 return nil, fmt.Errorf("proxy returned status %d: %s", resp.StatusCode, resp.Status)
191 }
192
193 // Step 2: Handle 407 Proxy Authentication Required
194 if resp.Body != nil {
195 _, _ = io.Copy(io.Discard, resp.Body)
196 _ = resp.Body.Close()
197 }
198
199 if resp.Close {
200 _ = conn.Close()
201 var dialErr error
202 conn, dialErr = h.dialProxy(ctx)
203 if dialErr != nil {
204 return nil, fmt.Errorf("failed to reconnect to proxy: %w", dialErr)
205 }
206 br = bufio.NewReaderSize(conn, 32768)
207 }
208
209 authHeaders := resp.Header.Values("Proxy-Authenticate")
210 if len(authHeaders) == 0 {
211 authHeaders = []string{resp.Header.Get("Proxy-Authenticate")}
212 }
213
214 // Check available mechanisms
215 hasNTLM := false
216 hasNegotiate := false
217 hasDigest := false
218 hasBasic := false
219 var digestChallenge string
220
221 for _, h := range authHeaders {
222 for _, part := range strings.Split(h, ",") {
223 part = strings.TrimSpace(part)
224 lower := strings.ToLower(part)
225 if strings.HasPrefix(lower, "ntlm") {
226 hasNTLM = true
227 } else if strings.HasPrefix(lower, "negotiate") {
228 hasNegotiate = true
229 } else if strings.HasPrefix(lower, "digest") {
230 hasDigest = true
231 digestChallenge = part
232 } else if strings.HasPrefix(lower, "basic") {
233 hasBasic = true
234 }
235 }
236 }
237
238 // Prioritize auth: NTLM -> Negotiate -> Digest -> Basic
239 if (h.authType == AuthNTLM || h.authType == AuthAuto) && hasNTLM && h.username != "" {
240 return h.handshakeNTLM(conn, br, targetAddr)
241 }
242
243 if (h.authType == AuthNegotiate || h.authType == AuthAuto) && hasNegotiate && h.username != "" {
244 // Try SPNEGO with NTLMSSP
245 spnegoInit, err := BuildSPNEGONTLMInit(h.domain, h.workstation)
246 if err == nil {
247 if err := h.sendConnect(conn, targetAddr, spnegoInit); err == nil {
248 resp2, err := http.ReadResponse(br, &http.Request{Method: http.MethodConnect})
249 if err == nil && resp2.StatusCode == http.StatusProxyAuthRequired {
250 if resp2.Body != nil {
251 _, _ = io.Copy(io.Discard, resp2.Body)
252 _ = resp2.Body.Close()
253 }
254 spnegoRespHeader := resp2.Header.Get("Proxy-Authenticate")
255 token, err := ExtractSPNEGOMechToken(spnegoRespHeader)
256 if err == nil {
257 t2, err := ParseNTLMType2(string(token))
258 if err == nil {
259 t3, err := BuildNTLMType3(h.username, h.password, h.domain, h.workstation, t2, nil)
260 if err == nil {
261 if err := h.sendConnect(conn, targetAddr, "NTLM "+t3); err == nil {
262 resp3, err := http.ReadResponse(br, &http.Request{Method: http.MethodConnect})
263 if err == nil && resp3.StatusCode == http.StatusOK {
264 return newBufferedConn(conn, br), nil
265 }
266 if resp3 != nil && resp3.Body != nil {
267 _, _ = io.Copy(io.Discard, resp3.Body)
268 _ = resp3.Body.Close()
269 }
270 }
271 }
272 }
273 }
274 }
275 }
276 }
277 }
278
279 if (h.authType == AuthDigest || h.authType == AuthAuto) && hasDigest && h.username != "" {
280 ch := ParseDigestChallenge(digestChallenge)
281 digestHeader := BuildDigestAuthHeader(h.username, h.password, targetAddr, ch, 1)
282 if err := h.sendConnect(conn, targetAddr, digestHeader); err != nil {
283 conn.Close()
284 return nil, fmt.Errorf("failed to send digest auth: %w", err)
285 }
286 resp2, err := http.ReadResponse(br, &http.Request{Method: http.MethodConnect})
287 if err != nil {
288 conn.Close()
289 return nil, fmt.Errorf("failed to read digest auth response: %w", err)
290 }
291 if resp2.Body != nil {
292 _, _ = io.Copy(io.Discard, resp2.Body)
293 _ = resp2.Body.Close()
294 }
295 if resp2.StatusCode == http.StatusOK {
296 return newBufferedConn(conn, br), nil
297 }
298 }
299
300 if (h.authType == AuthBasic || h.authType == AuthAuto) && hasBasic && h.username != "" {
301 basicHeader := BuildBasicAuthHeader(h.username, h.password)
302 if err := h.sendConnect(conn, targetAddr, basicHeader); err != nil {
303 conn.Close()
304 return nil, fmt.Errorf("failed to send basic auth: %w", err)
305 }
306 resp2, err := http.ReadResponse(br, &http.Request{Method: http.MethodConnect})
307 if err != nil {
308 conn.Close()
309 return nil, fmt.Errorf("failed to read basic auth response: %w", err)
310 }
311 if resp2.Body != nil {
312 _, _ = io.Copy(io.Discard, resp2.Body)
313 _ = resp2.Body.Close()
314 }
315 if resp2.StatusCode == http.StatusOK {
316 return newBufferedConn(conn, br), nil
317 }
318 if resp2.StatusCode == http.StatusProxyAuthRequired {
319 conn.Close()
320 return nil, ErrProxyAuthFailed
321 }
322 if resp2.StatusCode == http.StatusForbidden {
323 conn.Close()
324 return nil, ErrProxyForbidden
325 }
326 }
327
328 conn.Close()
329 return nil, ErrProxyAuthFailed
330}
331
332func (h *HTTPConnectDialer) sendConnect(conn net.Conn, targetAddr, authHeader string) error {
333 var req strings.Builder
334 req.WriteString(fmt.Sprintf("CONNECT %s HTTP/1.1\r\n", targetAddr))
335 req.WriteString(fmt.Sprintf("Host: %s\r\n", targetAddr))
336 req.WriteString(fmt.Sprintf("User-Agent: %s\r\n", h.userAgent))
337 req.WriteString("Proxy-Connection: Keep-Alive\r\n")
338 if authHeader != "" {
339 req.WriteString(fmt.Sprintf("Proxy-Authorization: %s\r\n", authHeader))
340 }
341 req.WriteString("\r\n")
342
343 _, err := conn.Write([]byte(req.String()))
344 return err
345}
346
347func (h *HTTPConnectDialer) handshakeNTLM(conn net.Conn, br *bufio.Reader, targetAddr string) (net.Conn, error) {
348 // Type 1 Negotiate
349 type1 := BuildNTLMType1(h.domain, h.workstation)
350 if err := h.sendConnect(conn, targetAddr, "NTLM "+type1); err != nil {
351 conn.Close()
352 return nil, fmt.Errorf("failed to send NTLM Type 1: %w", err)
353 }
354
355 resp, err := http.ReadResponse(br, &http.Request{Method: http.MethodConnect})
356 if err != nil {
357 conn.Close()
358 return nil, fmt.Errorf("failed to read NTLM Type 2 response: %w", err)
359 }
360
361 if resp.StatusCode != http.StatusProxyAuthRequired {
362 if resp.Body != nil {
363 _ = resp.Body.Close()
364 }
365 conn.Close()
366 return nil, fmt.Errorf("expected 407 on NTLM Type 1, got %d", resp.StatusCode)
367 }
368 if resp.Body != nil {
369 _, _ = io.Copy(io.Discard, resp.Body)
370 _ = resp.Body.Close()
371 }
372
373 type2Header := resp.Header.Get("Proxy-Authenticate")
374 t2, err := ParseNTLMType2(type2Header)
375 if err != nil {
376 conn.Close()
377 return nil, fmt.Errorf("failed to parse NTLM Type 2 challenge: %w", err)
378 }
379
380 // Type 3 Authenticate
381 type3, err := BuildNTLMType3(h.username, h.password, h.domain, h.workstation, t2, nil)
382 if err != nil {
383 conn.Close()
384 return nil, fmt.Errorf("failed to build NTLM Type 3 authenticate: %w", err)
385 }
386
387 if err := h.sendConnect(conn, targetAddr, "NTLM "+type3); err != nil {
388 conn.Close()
389 return nil, fmt.Errorf("failed to send NTLM Type 3: %w", err)
390 }
391
392 respFinal, err := http.ReadResponse(br, &http.Request{Method: http.MethodConnect})
393 if err != nil {
394 conn.Close()
395 return nil, fmt.Errorf("failed to read NTLM final response: %w", err)
396 }
397
398 if respFinal.StatusCode == http.StatusOK {
399 return newBufferedConn(conn, br), nil
400 }
401
402 if respFinal.Body != nil {
403 _ = respFinal.Body.Close()
404 }
405 conn.Close()
406 return nil, fmt.Errorf("%w: status %d %s", ErrProxyAuthFailed, respFinal.StatusCode, respFinal.Status)
407}